Projects STRLCPY termdash Commits 68bf0566
🤬
Revision indexing in progress... (symbol navigation in revisions will be accurate after indexed)
  • ■ ■ ■ ■
    .travis.yml
    skipped 7 lines
    8 8   - go get -t ./...
    9 9   - go get -u golang.org/x/lint/golint
    10 10   - go test ./...
    11  - - go test -race ./...
     11 + - CGO_ENABLED=1 go test -race ./...
    12 12   - go vet ./...
    13 13   - diff -u <(echo -n) <(gofmt -d -s .)
    14 14   - diff -u <(echo -n) <(./internal/scripts/autogen_licences.sh .)
    15 15   - diff -u <(echo -n) <(golint ./...)
    16 16  after_success:
    17 17   - ./internal/scripts/coverage.sh
     18 +env:
     19 + global:
     20 + - CGO_ENABLED=0
    18 21   
  • ■ ■ ■ ■ ■ ■
    CHANGELOG.md
    skipped 6 lines
    7 7   
    8 8  ## [Unreleased]
    9 9   
     10 +## [0.10.0] - 5-Jun-2019
     11 + 
     12 +### Added
     13 + 
     14 +- Added `time.Duration` based `ValueFormatter` for the `LineChart` Y-axis labels.
     15 +- Added round and suffix `ValueFormatter` for the `LineChart` Y-axis labels.
     16 +- Added decimal and suffix `ValueFormatter` for the `LineChart` Y-axis labels.
     17 +- Added a `container.SplitOption` that allows fixed size container splits.
     18 +- Added `grid` functions that allow fixed size rows and columns.
     19 + 
     20 +### Changed
     21 + 
     22 +- The `LineChart` can format the labels on the Y-axis with a `ValueFormatter`.
     23 +- The `SegmentDisplay` can now display dots and colons ('.' and ':').
     24 +- The `Donut` widget now guarantees spacing between the donut and its label.
     25 +- The continuous build on Travis CI now builds with cgo explicitly disabled to
     26 + ensure both Termdash and its dependencies use pure Go.
     27 + 
     28 +### Fixed
     29 + 
     30 +- Lint issues found on the Go report card.
     31 +- An internal library belonging to the `Text` widget was incorrectly passing
     32 + `math.MaxUint32` as an int argument.
     33 + 
    10 34  ## [0.9.1] - 15-May-2019
    11 35   
    12 36  ### Fixed
    skipped 258 lines
    271 295  - The Gauge widget.
    272 296  - The Text widget.
    273 297   
    274  -[unreleased]: https://github.com/mum4k/termdash/compare/v0.9.1...devel
     298 +[unreleased]: https://github.com/mum4k/termdash/compare/v0.10.0...devel
     299 +[0.10.0]: https://github.com/mum4k/termdash/compare/v0.9.1...v0.10.0
    275 300  [0.9.1]: https://github.com/mum4k/termdash/compare/v0.9.0...v0.9.1
    276 301  [0.9.0]: https://github.com/mum4k/termdash/compare/v0.8.0...v0.9.0
    277 302  [0.8.0]: https://github.com/mum4k/termdash/compare/v0.7.2...v0.8.0
    skipped 10 lines
  • ■ ■ ■ ■ ■ ■
    container/container.go
    skipped 170 lines
    171 171   if err != nil {
    172 172   return image.ZR, image.ZR, err
    173 173   }
     174 + if c.opts.splitFixed > DefaultSplitFixed {
     175 + if c.opts.split == splitTypeVertical {
     176 + return area.VSplitCells(ar, c.opts.splitFixed)
     177 + }
     178 + return area.HSplitCells(ar, c.opts.splitFixed)
     179 + }
     180 + 
    174 181   if c.opts.split == splitTypeVertical {
    175 182   return area.VSplit(ar, c.opts.splitPercent)
    176 183   }
    skipped 289 lines
  • ■ ■ ■ ■ ■ ■
    container/container_test.go
    skipped 490 lines
    491 491   wantContainerErr: true,
    492 492   },
    493 493   {
     494 + desc: "fails when both SplitFixed and SplitPercent are specified",
     495 + termSize: image.Point{10, 10},
     496 + container: func(ft *faketerm.Terminal) (*Container, error) {
     497 + return New(
     498 + ft,
     499 + SplitHorizontal(
     500 + Top(
     501 + Border(linestyle.Light),
     502 + ),
     503 + Bottom(
     504 + Border(linestyle.Light),
     505 + ),
     506 + SplitFixed(4),
     507 + SplitPercent(20),
     508 + ),
     509 + )
     510 + },
     511 + wantContainerErr: true,
     512 + },
     513 + {
     514 + desc: "fails on SplitFixed less than -1",
     515 + termSize: image.Point{10, 20},
     516 + container: func(ft *faketerm.Terminal) (*Container, error) {
     517 + return New(
     518 + ft,
     519 + SplitHorizontal(
     520 + Top(
     521 + Border(linestyle.Light),
     522 + ),
     523 + Bottom(
     524 + Border(linestyle.Light),
     525 + ),
     526 + SplitFixed(-2),
     527 + ),
     528 + )
     529 + },
     530 + wantContainerErr: true,
     531 + },
     532 + {
    494 533   desc: "empty container",
    495 534   termSize: image.Point{10, 10},
    496 535   container: func(ft *faketerm.Terminal) (*Container, error) {
    skipped 120 lines
    617 656   },
    618 657   },
    619 658   {
     659 + desc: "horizontal unequal split",
     660 + termSize: image.Point{10, 20},
     661 + container: func(ft *faketerm.Terminal) (*Container, error) {
     662 + return New(
     663 + ft,
     664 + SplitHorizontal(
     665 + Top(
     666 + Border(linestyle.Light),
     667 + ),
     668 + Bottom(
     669 + Border(linestyle.Light),
     670 + ),
     671 + SplitFixed(4),
     672 + ),
     673 + )
     674 + },
     675 + want: func(size image.Point) *faketerm.Terminal {
     676 + ft := faketerm.MustNew(size)
     677 + cvs := testcanvas.MustNew(ft.Area())
     678 + testdraw.MustBorder(cvs, image.Rect(0, 0, 10, 4))
     679 + testdraw.MustBorder(cvs, image.Rect(0, 4, 10, 20))
     680 + testcanvas.MustApply(cvs, ft)
     681 + return ft
     682 + },
     683 + },
     684 + {
    620 685   desc: "horizontal split, parent and children have borders",
    621 686   termSize: image.Point{10, 10},
    622 687   container: func(ft *faketerm.Terminal) (*Container, error) {
    skipped 107 lines
    730 795   Border(linestyle.Light),
    731 796   ),
    732 797   SplitPercent(20),
     798 + ),
     799 + )
     800 + },
     801 + want: func(size image.Point) *faketerm.Terminal {
     802 + ft := faketerm.MustNew(size)
     803 + cvs := testcanvas.MustNew(ft.Area())
     804 + testdraw.MustBorder(cvs, image.Rect(0, 0, 4, 10))
     805 + testdraw.MustBorder(cvs, image.Rect(4, 0, 20, 10))
     806 + testcanvas.MustApply(cvs, ft)
     807 + return ft
     808 + },
     809 + },
     810 + {
     811 + desc: "vertical fixed splits",
     812 + termSize: image.Point{20, 10},
     813 + container: func(ft *faketerm.Terminal) (*Container, error) {
     814 + return New(
     815 + ft,
     816 + SplitVertical(
     817 + Left(
     818 + Border(linestyle.Light),
     819 + ),
     820 + Right(
     821 + Border(linestyle.Light),
     822 + ),
     823 + SplitFixed(4),
    733 824   ),
    734 825   )
    735 826   },
    skipped 1635 lines
  • ■ ■ ■ ■ ■
    container/grid/grid.go
    skipped 34 lines
    35 35  // Add adds the specified elements.
    36 36  // The subElements can be either a single Widget or any combination of Rows and
    37 37  // Columns.
    38  -// Rows are created using RowHeightPerc() and Columns are created using
    39  -// ColWidthPerc().
     38 +// Rows are created using functions with the RowHeight prefix and Columns are
     39 +// created using functions with the ColWidth prefix
    40 40  // Can be called repeatedly, e.g. to add multiple Rows or Columns.
    41 41  func (b *Builder) Add(subElements ...Element) {
    42 42   b.elems = append(b.elems, subElements...)
    skipped 2 lines
    45 45  // Build builds the grid layout and returns the corresponding container
    46 46  // options.
    47 47  func (b *Builder) Build() ([]container.Option, error) {
    48  - if err := validate(b.elems); err != nil {
     48 + if err := validate(b.elems /* fixedSizeParent = */, false); err != nil {
    49 49   return nil, err
    50 50   }
    51 51   return build(b.elems, 100, 100), nil
    skipped 6 lines
    58 58  // Each individual width or height is in the range 0 < v < 100.
    59 59  // The sum of all widths is <= 100.
    60 60  // The sum of all heights is <= 100.
    61  -func validate(elems []Element) error {
    62  - heightSum := 0
    63  - widthSum := 0
     61 +// Argument fixedSizeParent indicates if any of the parent elements uses fixed
     62 +// size splitType.
     63 +func validate(elems []Element, fixedSizeParent bool) error {
     64 + heightPercSum := 0
     65 + widthPercSum := 0
    64 66   for _, elem := range elems {
    65 67   switch e := elem.(type) {
    66 68   case *row:
    67  - if min, max := 0, 100; e.heightPerc <= min || e.heightPerc >= max {
    68  - return fmt.Errorf("invalid row heightPerc(%d), must be a value in the range %d < v < %d", e.heightPerc, min, max)
     69 + if e.splitType == splitTypeRelative {
     70 + if min, max := 0, 100; e.heightPerc <= min || e.heightPerc >= max {
     71 + return fmt.Errorf("invalid row %v, must be a value in the range %d < v < %d", e, min, max)
     72 + }
     73 + }
     74 + heightPercSum += e.heightPerc
     75 + 
     76 + if fixedSizeParent && e.splitType == splitTypeRelative {
     77 + return fmt.Errorf("row %v cannot use relative height when one of its parent elements uses fixed height", e)
    69 78   }
    70  - heightSum += e.heightPerc
    71  - if err := validate(e.subElem); err != nil {
     79 + 
     80 + isFixed := fixedSizeParent || e.splitType == splitTypeFixed
     81 + if err := validate(e.subElem, isFixed); err != nil {
    72 82   return err
    73 83   }
    74 84   
    75 85   case *col:
    76  - if min, max := 0, 100; e.widthPerc <= min || e.widthPerc >= max {
    77  - return fmt.Errorf("invalid column widthPerc(%d), must be a value in the range %d < v < %d", e.widthPerc, min, max)
     86 + if e.splitType == splitTypeRelative {
     87 + if min, max := 0, 100; e.widthPerc <= min || e.widthPerc >= max {
     88 + return fmt.Errorf("invalid column %v, must be a value in the range %d < v < %d", e, min, max)
     89 + }
    78 90   }
    79  - widthSum += e.widthPerc
    80  - if err := validate(e.subElem); err != nil {
     91 + widthPercSum += e.widthPerc
     92 + 
     93 + if fixedSizeParent && e.splitType == splitTypeRelative {
     94 + return fmt.Errorf("column %v cannot use relative width when one of its parent elements uses fixed height", e)
     95 + }
     96 + 
     97 + isFixed := fixedSizeParent || e.splitType == splitTypeFixed
     98 + if err := validate(e.subElem, isFixed); err != nil {
    81 99   return err
    82 100   }
    83 101   
    skipped 4 lines
    88 106   }
    89 107   }
    90 108   
    91  - if max := 100; heightSum > max || widthSum > max {
    92  - return fmt.Errorf("the sum of all height percentages(%d) and width percentages(%d) at one element level cannot be larger than %d", heightSum, widthSum, max)
     109 + if max := 100; heightPercSum > max || widthPercSum > max {
     110 + return fmt.Errorf("the sum of all height percentages(%d) and width percentages(%d) at one element level cannot be larger than %d", heightPercSum, widthPercSum, max)
    93 111   }
    94 112   return nil
    95 113  }
    skipped 13 lines
    109 127   
    110 128   switch e := elem.(type) {
    111 129   case *row:
    112  - 
    113 130   if len(elems) > 0 {
    114 131   perc := innerPerc(e.heightPerc, parentHeightPerc)
    115 132   childHeightPerc := parentHeightPerc - e.heightPerc
     133 + 
     134 + var splitOpts []container.SplitOption
     135 + if e.splitType == splitTypeRelative {
     136 + splitOpts = append(splitOpts, container.SplitPercent(perc))
     137 + } else {
     138 + splitOpts = append(splitOpts, container.SplitFixed(e.heightFixed))
     139 + }
     140 + 
    116 141   return []container.Option{
    117 142   container.SplitHorizontal(
    118 143   container.Top(append(e.cOpts, build(e.subElem, 100, parentWidthPerc)...)...),
    119 144   container.Bottom(build(elems, childHeightPerc, parentWidthPerc)...),
    120  - container.SplitPercent(perc),
     145 + splitOpts...,
    121 146   ),
    122 147   }
    123 148   }
    skipped 3 lines
    127 152   if len(elems) > 0 {
    128 153   perc := innerPerc(e.widthPerc, parentWidthPerc)
    129 154   childWidthPerc := parentWidthPerc - e.widthPerc
     155 + 
     156 + var splitOpts []container.SplitOption
     157 + if e.splitType == splitTypeRelative {
     158 + splitOpts = append(splitOpts, container.SplitPercent(perc))
     159 + } else {
     160 + splitOpts = append(splitOpts, container.SplitFixed(e.widthFixed))
     161 + }
     162 + 
    130 163   return []container.Option{
    131 164   container.SplitVertical(
    132 165   container.Left(append(e.cOpts, build(e.subElem, parentHeightPerc, 100)...)...),
    133 166   container.Right(build(elems, parentHeightPerc, childWidthPerc)...),
    134  - container.SplitPercent(perc),
     167 + splitOpts...,
    135 168   ),
    136 169   }
    137 170   }
    skipped 41 lines
    179 212   isElement()
    180 213  }
    181 214   
     215 +// splitType represents
     216 +type splitType int
     217 + 
     218 +// String implements fmt.Stringer()
     219 +func (st splitType) String() string {
     220 + if n, ok := splitTypeNames[st]; ok {
     221 + return n
     222 + }
     223 + return "splitTypeUnknown"
     224 +}
     225 + 
     226 +// splitTypeNames maps splitType values to human readable names.
     227 +var splitTypeNames = map[splitType]string{
     228 + splitTypeRelative: "splitTypeRelative",
     229 + splitTypeFixed: "splitTypeFixed",
     230 +}
     231 + 
     232 +const (
     233 + splitTypeRelative splitType = iota
     234 + splitTypeFixed
     235 +)
     236 + 
    182 237  // row is a row in the grid.
    183 238  // row implements Element.
    184 239  type row struct {
     240 + // splitType identifies how the size of the split is determined.
     241 + splitType splitType
     242 + 
    185 243   // heightPerc is the height percentage this row occupies.
     244 + // Only set when splitType is splitTypeRelative.
    186 245   heightPerc int
     246 + 
     247 + // heightFixed is the height in cells this row occupies.
     248 + // Only set when splitType is splitTypeFixed.
     249 + heightFixed int
    187 250   
    188 251   // subElem are the sub Rows or Columns or a single widget.
    189 252   subElem []Element
    skipped 7 lines
    197 260   
    198 261  // String implements fmt.Stringer.
    199 262  func (r *row) String() string {
    200  - return fmt.Sprintf("row{height:%d, sub:%v}", r.heightPerc, r.subElem)
     263 + return fmt.Sprintf("row{splitType:%v, heightPerc:%d, heightFixed:%d, sub:%v}", r.splitType, r.heightPerc, r.heightFixed, r.subElem)
    201 264  }
    202 265   
    203 266  // col is a column in the grid.
    204 267  // col implements Element.
    205 268  type col struct {
     269 + // splitType identifies how the size of the split is determined.
     270 + splitType splitType
     271 + 
    206 272   // widthPerc is the width percentage this column occupies.
     273 + // Only set when splitType is splitTypeRelative.
    207 274   widthPerc int
    208 275   
     276 + // widthFixed is the width in cells thiw column occupies.
     277 + // Only set when splitType is splitTypeRelative.
     278 + widthFixed int
     279 + 
    209 280   // subElem are the sub Rows or Columns or a single widget.
    210 281   subElem []Element
    211 282   
    skipped 6 lines
    218 289   
    219 290  // String implements fmt.Stringer.
    220 291  func (c *col) String() string {
    221  - return fmt.Sprintf("col{width:%d, sub:%v}", c.widthPerc, c.subElem)
     292 + return fmt.Sprintf("col{splitType:%v, widthPerc:%d, widthFixed:%d, sub:%v}", c.splitType, c.widthPerc, c.widthFixed, c.subElem)
    222 293  }
    223 294   
    224 295  // widget is a widget placed into the grid.
    skipped 13 lines
    238 309  // isElement implements Element.isElement.
    239 310  func (widget) isElement() {}
    240 311   
    241  -// RowHeightPerc creates a row of the specified height.
     312 +// RowHeightPerc creates a row of the specified relative height.
    242 313  // The height is supplied as height percentage of the parent element.
    243 314  // The sum of all heights at the same level cannot be larger than 100%. If it
    244 315  // is less that 100%, the last element stretches to the edge of the screen.
    skipped 1 lines
    246 317  // Columns.
    247 318  func RowHeightPerc(heightPerc int, subElements ...Element) Element {
    248 319   return &row{
     320 + splitType: splitTypeRelative,
    249 321   heightPerc: heightPerc,
    250 322   subElem: subElements,
    251 323   }
    252 324  }
    253 325   
     326 +// RowHeightFixed creates a row of the specified fixed height.
     327 +// The height is supplied as a number of cells on the terminal.
     328 +// If the actual terminal size leaves the container with less than the
     329 +// specified amount of cells, the container will be created with zero cells and
     330 +// won't be drawn until the terminal size increases. If the sum of all the
     331 +// heights is less than 100% of the screen height, the last element stretches
     332 +// to the edge of the screen.
     333 +// The subElements can be either a single Widget or any combination of Rows and
     334 +// Columns.
     335 +// A row with fixed height cannot contain any sub-elements with relative size.
     336 +func RowHeightFixed(heightCells int, subElements ...Element) Element {
     337 + return &row{
     338 + splitType: splitTypeFixed,
     339 + heightFixed: heightCells,
     340 + subElem: subElements,
     341 + }
     342 +}
     343 + 
    254 344  // RowHeightPercWithOpts is like RowHeightPerc, but also allows to apply
    255 345  // additional options to the container that represents the row.
    256 346  func RowHeightPercWithOpts(heightPerc int, cOpts []container.Option, subElements ...Element) Element {
    257 347   return &row{
     348 + splitType: splitTypeRelative,
    258 349   heightPerc: heightPerc,
    259 350   subElem: subElements,
    260 351   cOpts: cOpts,
    261 352   }
    262 353  }
    263 354   
    264  -// ColWidthPerc creates a column of the specified width.
     355 +// RowHeightFixedWithOpts is like RowHeightFixed, but also allows to apply
     356 +// additional options to the container that represents the row.
     357 +func RowHeightFixedWithOpts(heightCells int, cOpts []container.Option, subElements ...Element) Element {
     358 + return &row{
     359 + splitType: splitTypeFixed,
     360 + heightFixed: heightCells,
     361 + subElem: subElements,
     362 + cOpts: cOpts,
     363 + }
     364 +}
     365 + 
     366 +// ColWidthPerc creates a column of the specified relative width.
    265 367  // The width is supplied as width percentage of the parent element.
    266 368  // The sum of all widths at the same level cannot be larger than 100%. If it
    267 369  // is less that 100%, the last element stretches to the edge of the screen.
    skipped 1 lines
    269 371  // Columns.
    270 372  func ColWidthPerc(widthPerc int, subElements ...Element) Element {
    271 373   return &col{
     374 + splitType: splitTypeRelative,
    272 375   widthPerc: widthPerc,
    273 376   subElem: subElements,
    274 377   }
    275 378  }
    276 379   
     380 +// ColWidthFixed creates a column of the specified fixed width.
     381 +// The width is supplied as a number of cells on the terminal.
     382 +// If the actual terminal size leaves the container with less than the
     383 +// specified amount of cells, the container will be created with zero cells and
     384 +// won't be drawn until the terminal size increases. If the sum of all the
     385 +// widths is less than 100% of the screen width, the last element stretches
     386 +// to the edge of the screen.
     387 +// The subElements can be either a single Widget or any combination of Rows and
     388 +// Columns.
     389 +// A column with fixed width cannot contain any sub-elements with relative size.
     390 +func ColWidthFixed(widthCells int, subElements ...Element) Element {
     391 + return &col{
     392 + splitType: splitTypeFixed,
     393 + widthFixed: widthCells,
     394 + subElem: subElements,
     395 + }
     396 +}
     397 + 
    277 398  // ColWidthPercWithOpts is like ColWidthPerc, but also allows to apply
    278 399  // additional options to the container that represents the column.
    279 400  func ColWidthPercWithOpts(widthPerc int, cOpts []container.Option, subElements ...Element) Element {
    280 401   return &col{
     402 + splitType: splitTypeRelative,
    281 403   widthPerc: widthPerc,
    282 404   subElem: subElements,
    283 405   cOpts: cOpts,
     406 + }
     407 +}
     408 + 
     409 +// ColWidthFixedWithOpts is like ColWidthFixed, but also allows to apply
     410 +// additional options to the container that represents the column.
     411 +func ColWidthFixedWithOpts(widthCells int, cOpts []container.Option, subElements ...Element) Element {
     412 + return &col{
     413 + splitType: splitTypeFixed,
     414 + widthFixed: widthCells,
     415 + subElem: subElements,
     416 + cOpts: cOpts,
    284 417   }
    285 418  }
    286 419   
    skipped 10 lines
  • ■ ■ ■ ■ ■ ■
    container/grid/grid_test.go
    skipped 234 lines
    235 235   wantErr: true,
    236 236   },
    237 237   {
     238 + desc: "fails when Row heightPerc used under Row heightFixed",
     239 + termSize: image.Point{10, 10},
     240 + builder: func() *Builder {
     241 + b := New()
     242 + b.Add(
     243 + RowHeightFixed(
     244 + 5,
     245 + RowHeightPerc(10),
     246 + ),
     247 + )
     248 + return b
     249 + }(),
     250 + wantErr: true,
     251 + },
     252 + {
     253 + desc: "fails when Row heightPerc used under Col widthFixed",
     254 + termSize: image.Point{10, 10},
     255 + builder: func() *Builder {
     256 + b := New()
     257 + b.Add(
     258 + ColWidthFixed(
     259 + 5,
     260 + RowHeightPerc(10),
     261 + ),
     262 + )
     263 + return b
     264 + }(),
     265 + wantErr: true,
     266 + },
     267 + {
    238 268   desc: "fails when Col widthPerc is too low at top level",
    239 269   termSize: image.Point{10, 10},
    240 270   builder: func() *Builder {
    skipped 48 lines
    289 319   wantErr: true,
    290 320   },
    291 321   {
     322 + desc: "fails when Col widthPerc used under Col widthFixed",
     323 + termSize: image.Point{10, 10},
     324 + builder: func() *Builder {
     325 + b := New()
     326 + b.Add(
     327 + ColWidthFixed(
     328 + 5,
     329 + ColWidthPerc(10),
     330 + ),
     331 + )
     332 + return b
     333 + }(),
     334 + wantErr: true,
     335 + },
     336 + {
     337 + desc: "fails when Col widthPerc used under Row heightFixed",
     338 + termSize: image.Point{10, 10},
     339 + builder: func() *Builder {
     340 + b := New()
     341 + b.Add(
     342 + RowHeightFixed(
     343 + 5,
     344 + ColWidthPerc(10),
     345 + ),
     346 + )
     347 + return b
     348 + }(),
     349 + wantErr: true,
     350 + },
     351 + {
    292 352   desc: "fails when height sum is too large at top level",
    293 353   termSize: image.Point{10, 10},
    294 354   builder: func() *Builder {
    skipped 95 lines
    390 450   },
    391 451   },
    392 452   {
     453 + desc: "two equal rows, fixed size",
     454 + termSize: image.Point{10, 10},
     455 + builder: func() *Builder {
     456 + b := New()
     457 + b.Add(RowHeightFixed(5, Widget(mirror())))
     458 + b.Add(RowHeightFixed(5, Widget(mirror())))
     459 + return b
     460 + }(),
     461 + want: func(size image.Point) *faketerm.Terminal {
     462 + ft := faketerm.MustNew(size)
     463 + top, bot := mustHSplit(ft.Area(), 50)
     464 + fakewidget.MustDraw(ft, testcanvas.MustNew(top), &widgetapi.Meta{}, widgetapi.Options{})
     465 + fakewidget.MustDraw(ft, testcanvas.MustNew(bot), &widgetapi.Meta{}, widgetapi.Options{})
     466 + return ft
     467 + },
     468 + },
     469 + {
    393 470   desc: "two equal rows with options",
    394 471   termSize: image.Point{10, 10},
    395 472   builder: func() *Builder {
    skipped 33 lines
    429 506   },
    430 507   },
    431 508   {
     509 + desc: "two equal rows with options, fixed size",
     510 + termSize: image.Point{10, 10},
     511 + builder: func() *Builder {
     512 + b := New()
     513 + b.Add(RowHeightFixedWithOpts(
     514 + 5,
     515 + []container.Option{
     516 + container.Border(linestyle.Double),
     517 + },
     518 + Widget(mirror()),
     519 + ))
     520 + b.Add(RowHeightFixedWithOpts(
     521 + 5,
     522 + []container.Option{
     523 + container.Border(linestyle.Double),
     524 + },
     525 + Widget(mirror()),
     526 + ))
     527 + return b
     528 + }(),
     529 + want: func(size image.Point) *faketerm.Terminal {
     530 + ft := faketerm.MustNew(size)
     531 + 
     532 + top, bot := mustHSplit(ft.Area(), 50)
     533 + topCvs := testcanvas.MustNew(top)
     534 + botCvs := testcanvas.MustNew(bot)
     535 + testdraw.MustBorder(topCvs, topCvs.Area(), draw.BorderLineStyle(linestyle.Double))
     536 + testdraw.MustBorder(botCvs, botCvs.Area(), draw.BorderLineStyle(linestyle.Double))
     537 + testcanvas.MustApply(topCvs, ft)
     538 + testcanvas.MustApply(botCvs, ft)
     539 + 
     540 + topWidget := testcanvas.MustNew(area.ExcludeBorder(top))
     541 + botWidget := testcanvas.MustNew(area.ExcludeBorder(bot))
     542 + fakewidget.MustDraw(ft, topWidget, &widgetapi.Meta{}, widgetapi.Options{})
     543 + fakewidget.MustDraw(ft, botWidget, &widgetapi.Meta{}, widgetapi.Options{})
     544 + return ft
     545 + },
     546 + },
     547 + {
    432 548   desc: "two unequal rows",
    433 549   termSize: image.Point{10, 10},
    434 550   builder: func() *Builder {
    skipped 11 lines
    446 562   },
    447 563   },
    448 564   {
     565 + desc: "two unequal rows, fixed size",
     566 + termSize: image.Point{10, 10},
     567 + builder: func() *Builder {
     568 + b := New()
     569 + b.Add(RowHeightFixed(2, Widget(mirror())))
     570 + b.Add(RowHeightFixed(8, Widget(mirror())))
     571 + return b
     572 + }(),
     573 + want: func(size image.Point) *faketerm.Terminal {
     574 + ft := faketerm.MustNew(size)
     575 + top, bot := mustHSplit(ft.Area(), 20)
     576 + fakewidget.MustDraw(ft, testcanvas.MustNew(top), &widgetapi.Meta{}, widgetapi.Options{})
     577 + fakewidget.MustDraw(ft, testcanvas.MustNew(bot), &widgetapi.Meta{}, widgetapi.Options{})
     578 + return ft
     579 + },
     580 + },
     581 + {
     582 + desc: "two equal columns",
     583 + termSize: image.Point{20, 10},
     584 + builder: func() *Builder {
     585 + b := New()
     586 + b.Add(ColWidthPerc(50, Widget(mirror())))
     587 + b.Add(ColWidthPerc(50, Widget(mirror())))
     588 + return b
     589 + }(),
     590 + want: func(size image.Point) *faketerm.Terminal {
     591 + ft := faketerm.MustNew(size)
     592 + left, right := mustVSplit(ft.Area(), 50)
     593 + fakewidget.MustDraw(ft, testcanvas.MustNew(left), &widgetapi.Meta{}, widgetapi.Options{})
     594 + fakewidget.MustDraw(ft, testcanvas.MustNew(right), &widgetapi.Meta{}, widgetapi.Options{})
     595 + return ft
     596 + },
     597 + },
     598 + {
     599 + desc: "two equal columns, fixed size",
     600 + termSize: image.Point{20, 10},
     601 + builder: func() *Builder {
     602 + b := New()
     603 + b.Add(ColWidthFixed(10, Widget(mirror())))
     604 + b.Add(ColWidthFixed(10, Widget(mirror())))
     605 + return b
     606 + }(),
     607 + want: func(size image.Point) *faketerm.Terminal {
     608 + ft := faketerm.MustNew(size)
     609 + left, right := mustVSplit(ft.Area(), 50)
     610 + fakewidget.MustDraw(ft, testcanvas.MustNew(left), &widgetapi.Meta{}, widgetapi.Options{})
     611 + fakewidget.MustDraw(ft, testcanvas.MustNew(right), &widgetapi.Meta{}, widgetapi.Options{})
     612 + return ft
     613 + },
     614 + },
     615 + {
    449 616   desc: "two equal columns with options",
    450 617   termSize: image.Point{20, 10},
    451 618   builder: func() *Builder {
    skipped 33 lines
    485 652   },
    486 653   },
    487 654   {
    488  - desc: "two equal columns",
     655 + desc: "two equal columns with options, fixed size",
    489 656   termSize: image.Point{20, 10},
    490 657   builder: func() *Builder {
    491 658   b := New()
    492  - b.Add(ColWidthPerc(50, Widget(mirror())))
    493  - b.Add(ColWidthPerc(50, Widget(mirror())))
     659 + b.Add(ColWidthFixedWithOpts(
     660 + 10,
     661 + []container.Option{
     662 + container.Border(linestyle.Double),
     663 + },
     664 + Widget(mirror()),
     665 + ))
     666 + b.Add(ColWidthFixedWithOpts(
     667 + 10,
     668 + []container.Option{
     669 + container.Border(linestyle.Double),
     670 + },
     671 + Widget(mirror()),
     672 + ))
    494 673   return b
    495 674   }(),
    496 675   want: func(size image.Point) *faketerm.Terminal {
    497 676   ft := faketerm.MustNew(size)
     677 + 
    498 678   left, right := mustVSplit(ft.Area(), 50)
    499  - fakewidget.MustDraw(ft, testcanvas.MustNew(left), &widgetapi.Meta{}, widgetapi.Options{})
    500  - fakewidget.MustDraw(ft, testcanvas.MustNew(right), &widgetapi.Meta{}, widgetapi.Options{})
     679 + leftCvs := testcanvas.MustNew(left)
     680 + rightCvs := testcanvas.MustNew(right)
     681 + testdraw.MustBorder(leftCvs, leftCvs.Area(), draw.BorderLineStyle(linestyle.Double))
     682 + testdraw.MustBorder(rightCvs, rightCvs.Area(), draw.BorderLineStyle(linestyle.Double))
     683 + testcanvas.MustApply(leftCvs, ft)
     684 + testcanvas.MustApply(rightCvs, ft)
     685 + 
     686 + leftWidget := testcanvas.MustNew(area.ExcludeBorder(left))
     687 + rightWidget := testcanvas.MustNew(area.ExcludeBorder(right))
     688 + fakewidget.MustDraw(ft, leftWidget, &widgetapi.Meta{}, widgetapi.Options{})
     689 + fakewidget.MustDraw(ft, rightWidget, &widgetapi.Meta{}, widgetapi.Options{})
    501 690   return ft
    502 691   },
    503 692   },
    skipped 4 lines
    508 697   b := New()
    509 698   b.Add(ColWidthPerc(20, Widget(mirror())))
    510 699   b.Add(ColWidthPerc(80, Widget(mirror())))
     700 + return b
     701 + }(),
     702 + want: func(size image.Point) *faketerm.Terminal {
     703 + ft := faketerm.MustNew(size)
     704 + left, right := mustVSplit(ft.Area(), 20)
     705 + fakewidget.MustDraw(ft, testcanvas.MustNew(left), &widgetapi.Meta{}, widgetapi.Options{})
     706 + fakewidget.MustDraw(ft, testcanvas.MustNew(right), &widgetapi.Meta{}, widgetapi.Options{})
     707 + return ft
     708 + },
     709 + },
     710 + {
     711 + desc: "two unequal columns, fixed size",
     712 + termSize: image.Point{40, 10},
     713 + builder: func() *Builder {
     714 + b := New()
     715 + b.Add(ColWidthFixed(8, Widget(mirror())))
     716 + b.Add(ColWidthFixed(32, Widget(mirror())))
    511 717   return b
    512 718   }(),
    513 719   want: func(size image.Point) *faketerm.Terminal {
    skipped 46 lines
    560 766   20,
    561 767   ColWidthPerc(20, Widget(mirror())),
    562 768   ColWidthPerc(80, Widget(mirror())),
     769 + ),
     770 + RowHeightPerc(
     771 + 80,
     772 + ColWidthPerc(80, Widget(mirror())),
     773 + ColWidthPerc(20, Widget(mirror())),
     774 + ),
     775 + )
     776 + return b
     777 + }(),
     778 + want: func(size image.Point) *faketerm.Terminal {
     779 + ft := faketerm.MustNew(size)
     780 + top, bot := mustHSplit(ft.Area(), 20)
     781 + 
     782 + topLeft, topRight := mustVSplit(top, 20)
     783 + botLeft, botRight := mustVSplit(bot, 80)
     784 + fakewidget.MustDraw(ft, testcanvas.MustNew(topLeft), &widgetapi.Meta{}, widgetapi.Options{})
     785 + fakewidget.MustDraw(ft, testcanvas.MustNew(topRight), &widgetapi.Meta{}, widgetapi.Options{})
     786 + fakewidget.MustDraw(ft, testcanvas.MustNew(botLeft), &widgetapi.Meta{}, widgetapi.Options{})
     787 + fakewidget.MustDraw(ft, testcanvas.MustNew(botRight), &widgetapi.Meta{}, widgetapi.Options{})
     788 + return ft
     789 + },
     790 + },
     791 + {
     792 + desc: "rows with columns (unequal), fixed and relative sizes mixed",
     793 + termSize: image.Point{40, 20},
     794 + builder: func() *Builder {
     795 + b := New()
     796 + b.Add(
     797 + RowHeightFixed(
     798 + 4,
     799 + ColWidthFixed(8, Widget(mirror())),
     800 + ColWidthFixed(32, Widget(mirror())),
    563 801   ),
    564 802   RowHeightPerc(
    565 803   80,
    skipped 234 lines
    800 1038   
    801 1039   gridOpts, err := tc.builder.Build()
    802 1040   if (err != nil) != tc.wantErr {
    803  - t.Errorf("tc.builder => unexpected error:%v, wantErr:%v", err, tc.wantErr)
     1041 + t.Errorf("tc.builder => unexpected error: %v, wantErr:%v", err, tc.wantErr)
    804 1042   }
    805 1043   if err != nil {
    806 1044   return
    skipped 27 lines
  • ■ ■ ■ ■ ■
    container/options.go
    skipped 37 lines
    38 38   return nil
    39 39  }
    40 40   
     41 +// ensure all the container identifiers are either empty or unique.
     42 +func validateIds(c *Container, seen map[string]bool) error {
     43 + if c.opts.id == "" {
     44 + return nil
     45 + } else if seen[c.opts.id] {
     46 + return fmt.Errorf("duplicate container ID %q", c.opts.id)
     47 + }
     48 + seen[c.opts.id] = true
     49 + 
     50 + return nil
     51 +}
     52 + 
     53 +// ensure all the container only have one split modifier.
     54 +func validateSplits(c *Container) error {
     55 + if c.opts.splitFixed > DefaultSplitFixed && c.opts.splitPercent != DefaultSplitPercent {
     56 + return fmt.Errorf(
     57 + "only one of splitFixed `%v` and splitPercent `%v` is allowed to be set per container",
     58 + c.opts.splitFixed,
     59 + c.opts.splitPercent,
     60 + )
     61 + }
     62 + 
     63 + return nil
     64 +}
     65 + 
    41 66  // validateOptions validates options set in the container tree.
    42 67  func validateOptions(c *Container) error {
    43  - // ensure all the container identifiers are either empty or unique.
    44 68   var errStr string
    45 69   seenID := map[string]bool{}
    46 70   preOrder(c, &errStr, func(c *Container) error {
    47  - if c.opts.id == "" {
    48  - return nil
     71 + if err := validateIds(c, seenID); err != nil {
     72 + return err
    49 73   }
    50  - 
    51  - if seenID[c.opts.id] {
    52  - return fmt.Errorf("duplicate container ID %q", c.opts.id)
     74 + if err := validateSplits(c); err != nil {
     75 + return err
    53 76   }
    54  - seenID[c.opts.id] = true
     77 + 
    55 78   return nil
    56 79   })
    57 80   if errStr != "" {
    58 81   return errors.New(errStr)
    59 82   }
     83 + 
    60 84   return nil
    61 85  }
    62 86   
    skipped 14 lines
    77 101   // split identifies how is this container split.
    78 102   split splitType
    79 103   splitPercent int
     104 + splitFixed int
    80 105   
    81 106   // widget is the widget in the container.
    82 107   // A container can have either two sub containers (left and right) or a
    skipped 84 lines
    167 192   hAlign: align.HorizontalCenter,
    168 193   vAlign: align.VerticalMiddle,
    169 194   splitPercent: DefaultSplitPercent,
     195 + splitFixed: DefaultSplitFixed,
    170 196   }
    171 197   if parent != nil {
    172 198   opts.inherited = parent.inherited
    skipped 26 lines
    199 225  // DefaultSplitPercent is the default value for the SplitPercent option.
    200 226  const DefaultSplitPercent = 50
    201 227   
     228 +// DefaultSplitFixed is the default value for the SplitFixed option.
     229 +const DefaultSplitFixed = -1
     230 + 
    202 231  // SplitPercent sets the relative size of the split as percentage of the available space.
    203 232  // When using SplitVertical, the provided size is applied to the new left
    204 233  // container, the new right container gets the reminder of the size.
    skipped 7 lines
    212 241   return fmt.Errorf("invalid split percentage %d, must be in range %d < p < %d", p, min, max)
    213 242   }
    214 243   opts.splitPercent = p
     244 + return nil
     245 + })
     246 +}
     247 + 
     248 +// SplitFixed sets the size of the first container to be a fixed value
     249 +// and makes the second container take up the remaining space.
     250 +// When using SplitVertical, the provided size is applied to the new left
     251 +// container, the new right container gets the reminder of the size.
     252 +// When using SplitHorizontal, the provided size is applied to the new top
     253 +// container, the new bottom container gets the reminder of the size.
     254 +// The provided value must be a positive number in the range 0 <= cells.
     255 +// If SplitFixed() is not specified, it defaults to SplitPercent() and its given value.
     256 +// Only one of SplitFixed() and SplitPercent() can be specified per container.
     257 +func SplitFixed(cells int) SplitOption {
     258 + return splitOption(func(opts *options) error {
     259 + if cells < 0 {
     260 + return fmt.Errorf("invalid fixed value %d, must be in range %d <= cells", cells, 0)
     261 + }
     262 + opts.splitFixed = cells
    215 263   return nil
    216 264   })
    217 265  }
    skipped 553 lines
  • doc/images/segmentdisplaydemo.gif
  • ■ ■ ■ ■ ■
    internal/alignfor/align.go internal/alignfor/alignfor.go
    skipped 21 lines
    22 22   
    23 23   "github.com/mum4k/termdash/align"
    24 24   "github.com/mum4k/termdash/internal/runewidth"
     25 + "github.com/mum4k/termdash/internal/wrap"
    25 26  )
    26 27   
    27 28  // hAlign aligns the given area in the rectangle horizontally.
    skipped 61 lines
    89 90  // Text aligns the text within the given rectangle, returns the start point for the text.
    90 91  // For the purposes of the alignment this assumes that text will be trimmed if
    91 92  // it overruns the rectangle.
    92  -// This only supports a single line of text, the text must not contain newlines.
     93 +// This only supports a single line of text, the text must not contain non-printable characters,
     94 +// allows empty text.
    93 95  func Text(rect image.Rectangle, text string, h align.Horizontal, v align.Vertical) (image.Point, error) {
    94 96   if strings.ContainsRune(text, '\n') {
    95 97   return image.ZP, fmt.Errorf("the provided text contains a newline character: %q", text)
     98 + }
     99 + 
     100 + if text != "" {
     101 + if err := wrap.ValidText(text); err != nil {
     102 + return image.ZP, fmt.Errorf("the provided text contains non printable character(s): %s", err)
     103 + }
    96 104   }
    97 105   
    98 106   cells := runewidth.StringWidth(text)
    skipped 23 lines
  • ■ ■ ■ ■ ■ ■
    internal/alignfor/align_test.go internal/alignfor/alignfor_test.go
    skipped 243 lines
    244 244   wantErr: true,
    245 245   },
    246 246   {
     247 + desc: "fails when text contains non-printable characters",
     248 + rect: image.Rect(0, 0, 3, 3),
     249 + text: "a\tb",
     250 + wantErr: true,
     251 + },
     252 + {
    247 253   desc: "aligns text top and left",
    248 254   rect: image.Rect(1, 1, 4, 4),
    249 255   text: "a",
    skipped 103 lines
  • ■ ■ ■ ■ ■
    internal/area/area.go
    skipped 185 lines
    186 186   }
    187 187   }
    188 188   
    189  - shrinked := area
    190  - shrinked.Min.X, _ = numbers.MinMaxInts([]int{shrinked.Min.X + leftCells, shrinked.Max.X})
    191  - _, shrinked.Max.X = numbers.MinMaxInts([]int{shrinked.Max.X - rightCells, shrinked.Min.X})
    192  - shrinked.Min.Y, _ = numbers.MinMaxInts([]int{shrinked.Min.Y + topCells, shrinked.Max.Y})
    193  - _, shrinked.Max.Y = numbers.MinMaxInts([]int{shrinked.Max.Y - bottomCells, shrinked.Min.Y})
     189 + shrunk := area
     190 + shrunk.Min.X, _ = numbers.MinMaxInts([]int{shrunk.Min.X + leftCells, shrunk.Max.X})
     191 + _, shrunk.Max.X = numbers.MinMaxInts([]int{shrunk.Max.X - rightCells, shrunk.Min.X})
     192 + shrunk.Min.Y, _ = numbers.MinMaxInts([]int{shrunk.Min.Y + topCells, shrunk.Max.Y})
     193 + _, shrunk.Max.Y = numbers.MinMaxInts([]int{shrunk.Max.Y - bottomCells, shrunk.Min.Y})
    194 194   
    195  - if shrinked.Dx() == 0 || shrinked.Dy() == 0 {
     195 + if shrunk.Dx() == 0 || shrunk.Dy() == 0 {
    196 196   return image.ZR, nil
    197 197   }
    198  - return shrinked, nil
     198 + return shrunk, nil
    199 199  }
    200 200   
    201 201  // ShrinkPercent returns a new area whose size is reduced by percentage of its
    skipped 23 lines
    225 225   return Shrink(area, top, right, bottom, left)
    226 226  }
    227 227   
     228 +// MoveUp returns a new area that is moved up by the specified amount of cells.
     229 +// Returns an error if the move would result in negative Y coordinates.
     230 +// The values must be zero or positive integers.
     231 +func MoveUp(area image.Rectangle, cells int) (image.Rectangle, error) {
     232 + if min := 0; cells < min {
     233 + return image.ZR, fmt.Errorf("cannot move area %v up by %d cells, must be in range %d <= value", area, cells, min)
     234 + }
     235 + 
     236 + if area.Min.Y < cells {
     237 + return image.ZR, fmt.Errorf("cannot move area %v up by %d cells, would result in negative Y coordinate", area, cells)
     238 + }
     239 + 
     240 + moved := area
     241 + moved.Min.Y -= cells
     242 + moved.Max.Y -= cells
     243 + return moved, nil
     244 +}
     245 + 
     246 +// MoveDown returns a new area that is moved down by the specified amount of
     247 +// cells.
     248 +// The values must be zero or positive integers.
     249 +func MoveDown(area image.Rectangle, cells int) (image.Rectangle, error) {
     250 + if min := 0; cells < min {
     251 + return image.ZR, fmt.Errorf("cannot move area %v down by %d cells, must be in range %d <= value", area, cells, min)
     252 + }
     253 + 
     254 + moved := area
     255 + moved.Min.Y += cells
     256 + moved.Max.Y += cells
     257 + return moved, nil
     258 +}
     259 + 
  • ■ ■ ■ ■ ■ ■
    internal/area/area_test.go
    skipped 871 lines
    872 872   }
    873 873  }
    874 874   
     875 +func TestMoveUp(t *testing.T) {
     876 + tests := []struct {
     877 + desc string
     878 + area image.Rectangle
     879 + cells int
     880 + want image.Rectangle
     881 + wantErr bool
     882 + }{
     883 + {
     884 + desc: "fails on negative cells",
     885 + area: image.Rect(0, 0, 1, 1),
     886 + cells: -1,
     887 + wantErr: true,
     888 + },
     889 + {
     890 + desc: "zero area cannot be moved",
     891 + area: image.ZR,
     892 + cells: 1,
     893 + wantErr: true,
     894 + },
     895 + {
     896 + desc: "cannot move area beyond zero Y coordinate",
     897 + area: image.Rect(0, 5, 1, 10),
     898 + cells: 6,
     899 + wantErr: true,
     900 + },
     901 + {
     902 + desc: "move by zero cells is idempotent",
     903 + area: image.Rect(0, 5, 1, 10),
     904 + cells: 0,
     905 + want: image.Rect(0, 5, 1, 10),
     906 + },
     907 + {
     908 + desc: "moves area up",
     909 + area: image.Rect(0, 5, 1, 10),
     910 + cells: 3,
     911 + want: image.Rect(0, 2, 1, 7),
     912 + },
     913 + }
     914 + 
     915 + for _, tc := range tests {
     916 + t.Run(tc.desc, func(t *testing.T) {
     917 + got, err := MoveUp(tc.area, tc.cells)
     918 + if (err != nil) != tc.wantErr {
     919 + t.Errorf("MoveUp => unexpected error: %v, wantErr: %v", err, tc.wantErr)
     920 + }
     921 + if err != nil {
     922 + return
     923 + }
     924 + 
     925 + if diff := pretty.Compare(tc.want, got); diff != "" {
     926 + t.Errorf("MoveUp => unexpected diff (-want, +got):\n%s", diff)
     927 + }
     928 + })
     929 + }
     930 +}
     931 + 
     932 +func TestMoveDown(t *testing.T) {
     933 + tests := []struct {
     934 + desc string
     935 + area image.Rectangle
     936 + cells int
     937 + want image.Rectangle
     938 + wantErr bool
     939 + }{
     940 + {
     941 + desc: "fails on negative cells",
     942 + area: image.Rect(0, 0, 1, 1),
     943 + cells: -1,
     944 + wantErr: true,
     945 + },
     946 + {
     947 + desc: "moves zero area",
     948 + area: image.ZR,
     949 + cells: 1,
     950 + want: image.Rect(0, 1, 0, 1),
     951 + },
     952 + {
     953 + desc: "move by zero cells is idempotent",
     954 + area: image.Rect(0, 5, 1, 10),
     955 + cells: 0,
     956 + want: image.Rect(0, 5, 1, 10),
     957 + },
     958 + {
     959 + desc: "moves area down",
     960 + area: image.Rect(0, 5, 1, 10),
     961 + cells: 3,
     962 + want: image.Rect(0, 8, 1, 13),
     963 + },
     964 + }
     965 + 
     966 + for _, tc := range tests {
     967 + t.Run(tc.desc, func(t *testing.T) {
     968 + got, err := MoveDown(tc.area, tc.cells)
     969 + if (err != nil) != tc.wantErr {
     970 + t.Errorf("MoveDown => unexpected error: %v, wantErr: %v", err, tc.wantErr)
     971 + }
     972 + if err != nil {
     973 + return
     974 + }
     975 + 
     976 + if diff := pretty.Compare(tc.want, got); diff != "" {
     977 + t.Errorf("MoveDown => unexpected diff (-want, +got):\n%s", diff)
     978 + }
     979 + })
     980 + }
     981 +}
     982 + 
  • ■ ■ ■ ■ ■ ■
    internal/segdisp/dotseg/attributes.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 dotseg
     16 + 
     17 +// attributes.go calculates attributes needed when determining placement of
     18 +// segments.
     19 + 
     20 +import (
     21 + "fmt"
     22 + "image"
     23 + "math"
     24 + 
     25 + "github.com/mum4k/termdash/align"
     26 + "github.com/mum4k/termdash/internal/alignfor"
     27 + "github.com/mum4k/termdash/internal/area"
     28 + "github.com/mum4k/termdash/internal/segdisp"
     29 + "github.com/mum4k/termdash/internal/segdisp/sixteen"
     30 +)
     31 + 
     32 +// attributes contains attributes needed to draw the segment display.
     33 +// Refer to doc/segment_placement.svg for a visual aid and explanation of the
     34 +// usage of the square roots.
     35 +type attributes struct {
     36 + // bcAr is the area the attributes were created for.
     37 + bcAr image.Rectangle
     38 + 
     39 + // segSize is the width of a vertical or height of a horizontal segment.
     40 + segSize int
     41 + 
     42 + // sixteen are attributes of a 16-segment display when placed on the same
     43 + // area.
     44 + sixteen *sixteen.Attributes
     45 +}
     46 + 
     47 +// newAttributes calculates attributes needed to place the segments for the
     48 +// provided pixel area.
     49 +func newAttributes(bcAr image.Rectangle) *attributes {
     50 + segSize := segdisp.SegmentSize(bcAr)
     51 + return &attributes{
     52 + bcAr: bcAr,
     53 + segSize: segSize,
     54 + sixteen: sixteen.NewAttributes(bcAr),
     55 + }
     56 +}
     57 + 
     58 +// segArea returns the area for the specified segment.
     59 +func (a *attributes) segArea(seg Segment) (image.Rectangle, error) {
     60 + // Dots have double width of normal segments to fill more space in the
     61 + // segment display.
     62 + segSize := a.segSize * 2
     63 + 
     64 + // An area representing the dot which gets aligned and moved into position
     65 + // below.
     66 + dotAr := image.Rect(
     67 + a.bcAr.Min.X,
     68 + a.bcAr.Min.Y,
     69 + a.bcAr.Min.X+segSize,
     70 + a.bcAr.Min.Y+segSize,
     71 + )
     72 + mid, err := alignfor.Rectangle(a.bcAr, dotAr, align.HorizontalCenter, align.VerticalMiddle)
     73 + if err != nil {
     74 + return image.ZR, err
     75 + }
     76 + 
     77 + // moveBySize is the multiplier of segment size to determine by how many
     78 + // pixels to move D1 and D2 up and down from the center.
     79 + const moveBySize = 1.5
     80 + moveBy := int(math.Round(moveBySize * float64(segSize)))
     81 + switch seg {
     82 + case D1:
     83 + moved, err := area.MoveUp(mid, moveBy)
     84 + if err != nil {
     85 + return image.ZR, err
     86 + }
     87 + return moved, nil
     88 + 
     89 + case D2:
     90 + moved, err := area.MoveDown(mid, moveBy)
     91 + if err != nil {
     92 + return image.ZR, err
     93 + }
     94 + return moved, nil
     95 + 
     96 + case D3:
     97 + // Align at the middle of the bottom.
     98 + bot, err := alignfor.Rectangle(a.bcAr, dotAr, align.HorizontalCenter, align.VerticalBottom)
     99 + if err != nil {
     100 + return image.ZR, err
     101 + }
     102 + 
     103 + // Shift up to where the sixteen segment actually places its bottom
     104 + // segments.
     105 + diff := bot.Min.Y - a.sixteen.VertBotY
     106 + // Shift further up by one segment size, since the dots have double width.
     107 + diff += a.segSize
     108 + moved, err := area.MoveUp(bot, diff)
     109 + if err != nil {
     110 + return image.ZR, err
     111 + }
     112 + return moved, nil
     113 + 
     114 + default:
     115 + return image.ZR, fmt.Errorf("cannot calculate area for %v(%d)", seg, seg)
     116 + }
     117 +}
     118 + 
  • ■ ■ ■ ■ ■ ■
    internal/segdisp/dotseg/attributes_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 dotseg
     16 + 
     17 +import (
     18 + "image"
     19 + "testing"
     20 + 
     21 + "github.com/kylelemons/godebug/pretty"
     22 +)
     23 + 
     24 +func TestAttributes(t *testing.T) {
     25 + tests := []struct {
     26 + desc string
     27 + brailleAr image.Rectangle
     28 + seg Segment
     29 + want image.Rectangle
     30 + wantErr bool
     31 + }{
     32 + {
     33 + desc: "fails on unsupported segment",
     34 + brailleAr: image.Rect(0, 0, 1, 1),
     35 + seg: Segment(-1),
     36 + wantErr: true,
     37 + },
     38 + }
     39 + 
     40 + for _, tc := range tests {
     41 + t.Run(tc.desc, func(t *testing.T) {
     42 + attr := newAttributes(tc.brailleAr)
     43 + got, err := attr.segArea(tc.seg)
     44 + if (err != nil) != tc.wantErr {
     45 + t.Errorf("segArea => unexpected error: %v, wantErr: %v", err, tc.wantErr)
     46 + }
     47 + if err != nil {
     48 + return
     49 + }
     50 + 
     51 + if diff := pretty.Compare(tc.want, got); diff != "" {
     52 + t.Errorf("segArea => unexpected diff (-want, +got):\n%s", diff)
     53 + }
     54 + })
     55 + }
     56 +}
     57 + 
  • ■ ■ ■ ■ ■ ■
    internal/segdisp/dotseg/dotseg.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 +/*
     16 +Package dotseg simulates a segment display that can draw dots.
     17 + 
     18 +Given a canvas, determines the placement and size of the individual
     19 +segments and exposes API that can turn individual segments on and off or
     20 +display dot characters.
     21 + 
     22 +The following outlines segments in the display and their names.
     23 + 
     24 + ---------------
     25 + | |
     26 + | |
     27 + | |
     28 + | o D1 |
     29 + | |
     30 + | |
     31 + | |
     32 + | o D2 |
     33 + | |
     34 + | |
     35 + | o D3 |
     36 + ---------------
     37 +*/
     38 +package dotseg
     39 + 
     40 +import (
     41 + "fmt"
     42 + "strings"
     43 + 
     44 + "github.com/mum4k/termdash/cell"
     45 + "github.com/mum4k/termdash/internal/canvas"
     46 + "github.com/mum4k/termdash/internal/segdisp"
     47 + "github.com/mum4k/termdash/internal/segdisp/segment"
     48 +)
     49 + 
     50 +// Segment represents a single segment in the display.
     51 +type Segment int
     52 + 
     53 +// String implements fmt.Stringer()
     54 +func (s Segment) String() string {
     55 + if n, ok := segmentNames[s]; ok {
     56 + return n
     57 + }
     58 + return "SegmentUnknown"
     59 +}
     60 + 
     61 +// segmentNames maps Segment values to human readable names.
     62 +var segmentNames = map[Segment]string{
     63 + D1: "D1",
     64 + D2: "D2",
     65 + D3: "D3",
     66 +}
     67 + 
     68 +const (
     69 + segmentUnknown Segment = iota
     70 + 
     71 + // D1 is a segment, see the diagram above.
     72 + D1
     73 + // D2 is a segment, see the diagram above.
     74 + D2
     75 + // D3 is a segment, see the diagram above.
     76 + D3
     77 + 
     78 + segmentMax // Used for validation.
     79 +)
     80 + 
     81 +// characterSegments maps characters that can be displayed on their segments.
     82 +var characterSegments = map[rune][]Segment{
     83 + ':': {D1, D2},
     84 + '.': {D3},
     85 +}
     86 + 
     87 +// SupportedChars returns all characters this display supports.
     88 +func SupportedChars() string {
     89 + var b strings.Builder
     90 + for r := range characterSegments {
     91 + b.WriteRune(r)
     92 + }
     93 + return b.String()
     94 +}
     95 + 
     96 +// AllSegments returns all segments in an undefined order.
     97 +func AllSegments() []Segment {
     98 + var res []Segment
     99 + for s := range segmentNames {
     100 + res = append(res, s)
     101 + }
     102 + return res
     103 +}
     104 + 
     105 +// Option is used to provide options.
     106 +type Option interface {
     107 + // set sets the provided option.
     108 + set(*Display)
     109 +}
     110 + 
     111 +// option implements Option.
     112 +type option func(*Display)
     113 + 
     114 +// set implements Option.set.
     115 +func (o option) set(d *Display) {
     116 + o(d)
     117 +}
     118 + 
     119 +// CellOpts sets the cell options on the cells that contain the segment display.
     120 +func CellOpts(cOpts ...cell.Option) Option {
     121 + return option(func(d *Display) {
     122 + d.cellOpts = cOpts
     123 + })
     124 +}
     125 + 
     126 +// Display represents the segment display.
     127 +// This object is not thread-safe.
     128 +type Display struct {
     129 + // segments maps segments to their current status.
     130 + segments map[Segment]bool
     131 + 
     132 + cellOpts []cell.Option
     133 +}
     134 + 
     135 +// New creates a new segment display.
     136 +// Initially all the segments are off.
     137 +func New(opts ...Option) *Display {
     138 + d := &Display{
     139 + segments: map[Segment]bool{},
     140 + }
     141 + 
     142 + for _, opt := range opts {
     143 + opt.set(d)
     144 + }
     145 + return d
     146 +}
     147 + 
     148 +// Clear clears the entire display, turning all segments off.
     149 +func (d *Display) Clear(opts ...Option) {
     150 + for _, opt := range opts {
     151 + opt.set(d)
     152 + }
     153 + 
     154 + d.segments = map[Segment]bool{}
     155 +}
     156 + 
     157 +// SetSegment sets the specified segment on.
     158 +// This method is idempotent.
     159 +func (d *Display) SetSegment(s Segment) error {
     160 + if s <= segmentUnknown || s >= segmentMax {
     161 + return fmt.Errorf("unknown segment %v(%d)", s, s)
     162 + }
     163 + d.segments[s] = true
     164 + return nil
     165 +}
     166 + 
     167 +// ClearSegment sets the specified segment off.
     168 +// This method is idempotent.
     169 +func (d *Display) ClearSegment(s Segment) error {
     170 + if s <= segmentUnknown || s >= segmentMax {
     171 + return fmt.Errorf("unknown segment %v(%d)", s, s)
     172 + }
     173 + d.segments[s] = false
     174 + return nil
     175 +}
     176 + 
     177 +// ToggleSegment toggles the state of the specified segment, i.e it either sets
     178 +// or clears it depending on its current state.
     179 +func (d *Display) ToggleSegment(s Segment) error {
     180 + if s <= segmentUnknown || s >= segmentMax {
     181 + return fmt.Errorf("unknown segment %v(%d)", s, s)
     182 + }
     183 + if d.segments[s] {
     184 + d.segments[s] = false
     185 + } else {
     186 + d.segments[s] = true
     187 + }
     188 + return nil
     189 +}
     190 + 
     191 +// SetCharacter sets all the segments that are needed to display the provided
     192 +// character.
     193 +// The display only supports characters returned by SupportedsChars().
     194 +// Doesn't clear the display of segments set previously.
     195 +func (d *Display) SetCharacter(c rune) error {
     196 + seg, ok := characterSegments[c]
     197 + if !ok {
     198 + return fmt.Errorf("display doesn't support character %q rune(%v)", c, c)
     199 + }
     200 + 
     201 + for _, s := range seg {
     202 + if err := d.SetSegment(s); err != nil {
     203 + return err
     204 + }
     205 + }
     206 + return nil
     207 +}
     208 + 
     209 +// Draw draws the current state of the segment display onto the canvas.
     210 +// The canvas must be at least MinCols x MinRows cells, or an error will be
     211 +// returned.
     212 +// Any options provided to draw overwrite the values provided to New.
     213 +func (d *Display) Draw(cvs *canvas.Canvas, opts ...Option) error {
     214 + for _, o := range opts {
     215 + o.set(d)
     216 + }
     217 + 
     218 + bc, bcAr, err := segdisp.ToBraille(cvs)
     219 + if err != nil {
     220 + return err
     221 + }
     222 + 
     223 + attr := newAttributes(bcAr)
     224 + for seg, isSet := range d.segments {
     225 + if !isSet {
     226 + continue
     227 + }
     228 + 
     229 + ar, err := attr.segArea(seg)
     230 + if err != nil {
     231 + return err
     232 + }
     233 + if err := segment.HV(bc, ar, segment.Vertical, segment.CellOpts(d.cellOpts...)); err != nil {
     234 + return err
     235 + }
     236 + }
     237 + return bc.CopyTo(cvs)
     238 +}
     239 + 
  • ■ ■ ■ ■ ■ ■
    internal/segdisp/dotseg/dotseg_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 dotseg
     16 + 
     17 +import (
     18 + "image"
     19 + "sort"
     20 + "testing"
     21 + 
     22 + "github.com/kylelemons/godebug/pretty"
     23 + "github.com/mum4k/termdash/cell"
     24 + "github.com/mum4k/termdash/internal/area"
     25 + "github.com/mum4k/termdash/internal/canvas"
     26 + "github.com/mum4k/termdash/internal/canvas/braille/testbraille"
     27 + "github.com/mum4k/termdash/internal/canvas/testcanvas"
     28 + "github.com/mum4k/termdash/internal/faketerm"
     29 + "github.com/mum4k/termdash/internal/segdisp"
     30 + "github.com/mum4k/termdash/internal/segdisp/segment"
     31 + "github.com/mum4k/termdash/internal/segdisp/segment/testsegment"
     32 +)
     33 + 
     34 +func TestSegmentString(t *testing.T) {
     35 + tests := []struct {
     36 + desc string
     37 + seg Segment
     38 + want string
     39 + }{
     40 + {
     41 + desc: "known segment",
     42 + seg: D1,
     43 + want: "D1",
     44 + },
     45 + {
     46 + desc: "unknown segment",
     47 + seg: Segment(-1),
     48 + want: "SegmentUnknown",
     49 + },
     50 + }
     51 + 
     52 + for _, tc := range tests {
     53 + t.Run(tc.desc, func(t *testing.T) {
     54 + got := tc.seg.String()
     55 + if got != tc.want {
     56 + t.Errorf("String => %q, want %q", got, tc.want)
     57 + }
     58 + })
     59 + }
     60 +}
     61 + 
     62 +func TestDraw(t *testing.T) {
     63 + tests := []struct {
     64 + desc string
     65 + opts []Option
     66 + drawOpts []Option
     67 + cellCanvas image.Rectangle
     68 + // If not nil, it is called before Draw is called and can set, clear or
     69 + // toggle segments or characters.
     70 + update func(*Display) error
     71 + want func(size image.Point) *faketerm.Terminal
     72 + wantErr bool
     73 + wantUpdateErr bool
     74 + }{
     75 + {
     76 + desc: "fails for area not wide enough",
     77 + cellCanvas: image.Rect(0, 0, segdisp.MinCols-1, segdisp.MinRows),
     78 + wantErr: true,
     79 + },
     80 + {
     81 + desc: "fails for area not tall enough",
     82 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows-1),
     83 + wantErr: true,
     84 + },
     85 + {
     86 + desc: "fails to set invalid segment (too small)",
     87 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
     88 + update: func(d *Display) error {
     89 + return d.SetSegment(Segment(-1))
     90 + },
     91 + wantUpdateErr: true,
     92 + },
     93 + {
     94 + desc: "fails to set invalid segment (too large)",
     95 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
     96 + update: func(d *Display) error {
     97 + return d.SetSegment(Segment(segmentMax))
     98 + },
     99 + wantUpdateErr: true,
     100 + },
     101 + {
     102 + desc: "fails to clear invalid segment (too small)",
     103 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
     104 + update: func(d *Display) error {
     105 + return d.ClearSegment(Segment(-1))
     106 + },
     107 + wantUpdateErr: true,
     108 + },
     109 + {
     110 + desc: "fails to clear invalid segment (too large)",
     111 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
     112 + update: func(d *Display) error {
     113 + return d.ClearSegment(Segment(segmentMax))
     114 + },
     115 + wantUpdateErr: true,
     116 + },
     117 + {
     118 + desc: "fails to toggle invalid segment (too small)",
     119 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
     120 + update: func(d *Display) error {
     121 + return d.ToggleSegment(Segment(-1))
     122 + },
     123 + wantUpdateErr: true,
     124 + },
     125 + {
     126 + desc: "fails to toggle invalid segment (too large)",
     127 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
     128 + update: func(d *Display) error {
     129 + return d.ToggleSegment(Segment(segmentMax))
     130 + },
     131 + wantUpdateErr: true,
     132 + },
     133 + {
     134 + desc: "empty when no segments set",
     135 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
     136 + },
     137 + {
     138 + desc: "smallest valid display 6x5, all segments",
     139 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
     140 + update: func(d *Display) error {
     141 + for _, seg := range AllSegments() {
     142 + if err := d.SetSegment(seg); err != nil {
     143 + return err
     144 + }
     145 + }
     146 + return nil
     147 + },
     148 + want: func(size image.Point) *faketerm.Terminal {
     149 + ft := faketerm.MustNew(size)
     150 + bc := testbraille.MustNew(ft.Area())
     151 + 
     152 + testsegment.MustHV(bc, image.Rect(5, 6, 7, 8), segment.Horizontal) // D1
     153 + testsegment.MustHV(bc, image.Rect(5, 12, 7, 14), segment.Horizontal) // D2
     154 + testsegment.MustHV(bc, image.Rect(5, 15, 7, 17), segment.Horizontal) // D3
     155 + testbraille.MustApply(bc, ft)
     156 + return ft
     157 + },
     158 + },
     159 + {
     160 + desc: "smallest valid display 6x5, all segments, New sets cell options",
     161 + opts: []Option{
     162 + CellOpts(
     163 + cell.FgColor(cell.ColorRed),
     164 + cell.BgColor(cell.ColorGreen),
     165 + ),
     166 + },
     167 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
     168 + update: func(d *Display) error {
     169 + for _, seg := range AllSegments() {
     170 + if err := d.SetSegment(seg); err != nil {
     171 + return err
     172 + }
     173 + }
     174 + return nil
     175 + },
     176 + want: func(size image.Point) *faketerm.Terminal {
     177 + ft := faketerm.MustNew(size)
     178 + bc := testbraille.MustNew(ft.Area())
     179 + 
     180 + opts := []segment.Option{
     181 + segment.CellOpts(
     182 + cell.FgColor(cell.ColorRed),
     183 + cell.BgColor(cell.ColorGreen),
     184 + ),
     185 + }
     186 + testsegment.MustHV(bc, image.Rect(5, 6, 7, 8), segment.Horizontal, opts...) // D1
     187 + testsegment.MustHV(bc, image.Rect(5, 12, 7, 14), segment.Horizontal, opts...) // D2
     188 + testsegment.MustHV(bc, image.Rect(5, 15, 7, 17), segment.Horizontal, opts...) // D5
     189 + testbraille.MustApply(bc, ft)
     190 + return ft
     191 + },
     192 + },
     193 + {
     194 + desc: "smallest valid display 6x5, all segments, Draw sets cell options",
     195 + drawOpts: []Option{
     196 + CellOpts(
     197 + cell.FgColor(cell.ColorRed),
     198 + cell.BgColor(cell.ColorGreen),
     199 + ),
     200 + },
     201 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
     202 + update: func(d *Display) error {
     203 + for _, seg := range AllSegments() {
     204 + if err := d.SetSegment(seg); err != nil {
     205 + return err
     206 + }
     207 + }
     208 + return nil
     209 + },
     210 + want: func(size image.Point) *faketerm.Terminal {
     211 + ft := faketerm.MustNew(size)
     212 + bc := testbraille.MustNew(ft.Area())
     213 + 
     214 + opts := []segment.Option{
     215 + segment.CellOpts(
     216 + cell.FgColor(cell.ColorRed),
     217 + cell.BgColor(cell.ColorGreen),
     218 + ),
     219 + }
     220 + testsegment.MustHV(bc, image.Rect(5, 6, 7, 8), segment.Horizontal, opts...) // D1
     221 + testsegment.MustHV(bc, image.Rect(5, 12, 7, 14), segment.Horizontal, opts...) // D2
     222 + testsegment.MustHV(bc, image.Rect(5, 15, 7, 17), segment.Horizontal, opts...) // D5
     223 + testbraille.MustApply(bc, ft)
     224 + return ft
     225 + },
     226 + },
     227 + {
     228 + desc: "smallest valid display 6x5, D1",
     229 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
     230 + update: func(d *Display) error {
     231 + return d.SetSegment(D1)
     232 + },
     233 + want: func(size image.Point) *faketerm.Terminal {
     234 + ft := faketerm.MustNew(size)
     235 + bc := testbraille.MustNew(ft.Area())
     236 + 
     237 + testsegment.MustHV(bc, image.Rect(5, 6, 7, 8), segment.Horizontal) // D1
     238 + testbraille.MustApply(bc, ft)
     239 + return ft
     240 + },
     241 + },
     242 + {
     243 + desc: "smallest valid display 6x5, D2",
     244 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
     245 + update: func(d *Display) error {
     246 + return d.SetSegment(D2)
     247 + },
     248 + want: func(size image.Point) *faketerm.Terminal {
     249 + ft := faketerm.MustNew(size)
     250 + bc := testbraille.MustNew(ft.Area())
     251 + 
     252 + testsegment.MustHV(bc, image.Rect(5, 12, 7, 14), segment.Horizontal) // D2
     253 + testbraille.MustApply(bc, ft)
     254 + return ft
     255 + },
     256 + },
     257 + {
     258 + desc: "smallest valid display 6x5, D3",
     259 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
     260 + update: func(d *Display) error {
     261 + return d.SetSegment(D3)
     262 + },
     263 + want: func(size image.Point) *faketerm.Terminal {
     264 + ft := faketerm.MustNew(size)
     265 + bc := testbraille.MustNew(ft.Area())
     266 + 
     267 + testsegment.MustHV(bc, image.Rect(5, 15, 7, 17), segment.Horizontal) // D3
     268 + testbraille.MustApply(bc, ft)
     269 + return ft
     270 + },
     271 + },
     272 + {
     273 + desc: "clears segment",
     274 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
     275 + update: func(d *Display) error {
     276 + for _, seg := range AllSegments() {
     277 + if err := d.SetSegment(seg); err != nil {
     278 + return err
     279 + }
     280 + }
     281 + return d.ClearSegment(D1)
     282 + },
     283 + want: func(size image.Point) *faketerm.Terminal {
     284 + ft := faketerm.MustNew(size)
     285 + bc := testbraille.MustNew(ft.Area())
     286 + 
     287 + testsegment.MustHV(bc, image.Rect(5, 12, 7, 14), segment.Horizontal) // D2
     288 + testsegment.MustHV(bc, image.Rect(5, 15, 7, 17), segment.Horizontal) // D3
     289 + testbraille.MustApply(bc, ft)
     290 + return ft
     291 + },
     292 + },
     293 + {
     294 + desc: "clears the display",
     295 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
     296 + update: func(d *Display) error {
     297 + for _, seg := range AllSegments() {
     298 + if err := d.SetSegment(seg); err != nil {
     299 + return err
     300 + }
     301 + }
     302 + d.Clear()
     303 + return nil
     304 + },
     305 + want: func(size image.Point) *faketerm.Terminal {
     306 + ft := faketerm.MustNew(size)
     307 + return ft
     308 + },
     309 + },
     310 + {
     311 + desc: "clear sets new cell options",
     312 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
     313 + update: func(d *Display) error {
     314 + d.Clear(CellOpts(
     315 + cell.FgColor(cell.ColorRed),
     316 + cell.BgColor(cell.ColorGreen),
     317 + ))
     318 + for _, seg := range AllSegments() {
     319 + if err := d.SetSegment(seg); err != nil {
     320 + return err
     321 + }
     322 + }
     323 + return nil
     324 + },
     325 + want: func(size image.Point) *faketerm.Terminal {
     326 + ft := faketerm.MustNew(size)
     327 + bc := testbraille.MustNew(ft.Area())
     328 + 
     329 + opts := []segment.Option{
     330 + segment.CellOpts(
     331 + cell.FgColor(cell.ColorRed),
     332 + cell.BgColor(cell.ColorGreen),
     333 + ),
     334 + }
     335 + testsegment.MustHV(bc, image.Rect(5, 6, 7, 8), segment.Horizontal, opts...) // D1
     336 + testsegment.MustHV(bc, image.Rect(5, 12, 7, 14), segment.Horizontal, opts...) // D2
     337 + testsegment.MustHV(bc, image.Rect(5, 15, 7, 17), segment.Horizontal, opts...) // D5
     338 + testbraille.MustApply(bc, ft)
     339 + return ft
     340 + },
     341 + },
     342 + {
     343 + desc: "toggles segment off",
     344 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
     345 + update: func(d *Display) error {
     346 + for _, seg := range AllSegments() {
     347 + if err := d.SetSegment(seg); err != nil {
     348 + return err
     349 + }
     350 + }
     351 + return d.ToggleSegment(D1)
     352 + },
     353 + want: func(size image.Point) *faketerm.Terminal {
     354 + ft := faketerm.MustNew(size)
     355 + bc := testbraille.MustNew(ft.Area())
     356 + 
     357 + testsegment.MustHV(bc, image.Rect(5, 12, 7, 14), segment.Horizontal) // D2
     358 + testsegment.MustHV(bc, image.Rect(5, 15, 7, 17), segment.Horizontal) // D3
     359 + testbraille.MustApply(bc, ft)
     360 + return ft
     361 + },
     362 + },
     363 + {
     364 + desc: "toggles segment on",
     365 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
     366 + update: func(d *Display) error {
     367 + for _, seg := range AllSegments() {
     368 + if err := d.SetSegment(seg); err != nil {
     369 + return err
     370 + }
     371 + }
     372 + if err := d.ToggleSegment(D1); err != nil {
     373 + return err
     374 + }
     375 + return d.ToggleSegment(D1)
     376 + },
     377 + want: func(size image.Point) *faketerm.Terminal {
     378 + ft := faketerm.MustNew(size)
     379 + bc := testbraille.MustNew(ft.Area())
     380 + 
     381 + testsegment.MustHV(bc, image.Rect(5, 6, 7, 8), segment.Horizontal) // D1
     382 + testsegment.MustHV(bc, image.Rect(5, 12, 7, 14), segment.Horizontal) // D2
     383 + testsegment.MustHV(bc, image.Rect(5, 15, 7, 17), segment.Horizontal) // D3
     384 + testbraille.MustApply(bc, ft)
     385 + return ft
     386 + },
     387 + },
     388 + 
     389 + {
     390 + desc: "larger display 18x15, all segments",
     391 + cellCanvas: image.Rect(0, 0, 3*segdisp.MinCols, 3*segdisp.MinRows),
     392 + update: func(d *Display) error {
     393 + for _, seg := range AllSegments() {
     394 + if err := d.SetSegment(seg); err != nil {
     395 + return err
     396 + }
     397 + }
     398 + return nil
     399 + },
     400 + want: func(size image.Point) *faketerm.Terminal {
     401 + ft := faketerm.MustNew(size)
     402 + bc := testbraille.MustNew(ft.Area())
     403 + 
     404 + testsegment.MustHV(bc, image.Rect(15, 18, 21, 24), segment.Horizontal) // D1
     405 + testsegment.MustHV(bc, image.Rect(15, 36, 21, 42), segment.Horizontal) // D2
     406 + testsegment.MustHV(bc, image.Rect(15, 51, 21, 57), segment.Horizontal) // D3
     407 + testbraille.MustApply(bc, ft)
     408 + return ft
     409 + },
     410 + },
     411 + }
     412 + 
     413 + for _, tc := range tests {
     414 + t.Run(tc.desc, func(t *testing.T) {
     415 + d := New(tc.opts...)
     416 + if tc.update != nil {
     417 + err := tc.update(d)
     418 + if (err != nil) != tc.wantUpdateErr {
     419 + t.Errorf("tc.update => unexpected error: %v, wantUpdateErr: %v", err, tc.wantUpdateErr)
     420 + }
     421 + if err != nil {
     422 + return
     423 + }
     424 + }
     425 + 
     426 + cvs, err := canvas.New(tc.cellCanvas)
     427 + if err != nil {
     428 + t.Fatalf("canvas.New => unexpected error: %v", err)
     429 + }
     430 + 
     431 + {
     432 + err := d.Draw(cvs, tc.drawOpts...)
     433 + if (err != nil) != tc.wantErr {
     434 + t.Errorf("Draw => unexpected error: %v, wantErr: %v", err, tc.wantErr)
     435 + }
     436 + if err != nil {
     437 + return
     438 + }
     439 + }
     440 + 
     441 + size := area.Size(tc.cellCanvas)
     442 + want := faketerm.MustNew(size)
     443 + if tc.want != nil {
     444 + want = tc.want(size)
     445 + }
     446 + 
     447 + got, err := faketerm.New(size)
     448 + if err != nil {
     449 + t.Fatalf("faketerm.New => unexpected error: %v", err)
     450 + }
     451 + if err := cvs.Apply(got); err != nil {
     452 + t.Fatalf("bc.Apply => unexpected error: %v", err)
     453 + }
     454 + if diff := faketerm.Diff(want, got); diff != "" {
     455 + t.Fatalf("Draw => %v", diff)
     456 + }
     457 + 
     458 + })
     459 + }
     460 +}
     461 + 
     462 +// mustDrawSegments returns a fake terminal of the specified size with the
     463 +// segments drawn on it or panics.
     464 +func mustDrawSegments(size image.Point, seg ...Segment) *faketerm.Terminal {
     465 + ft := faketerm.MustNew(size)
     466 + cvs := testcanvas.MustNew(ft.Area())
     467 + 
     468 + d := New()
     469 + for _, s := range seg {
     470 + if err := d.SetSegment(s); err != nil {
     471 + panic(err)
     472 + }
     473 + }
     474 + 
     475 + if err := d.Draw(cvs); err != nil {
     476 + panic(err)
     477 + }
     478 + 
     479 + testcanvas.MustApply(cvs, ft)
     480 + return ft
     481 +}
     482 + 
     483 +func TestSetCharacter(t *testing.T) {
     484 + tests := []struct {
     485 + desc string
     486 + char rune
     487 + // If not nil, it is called before Draw is called and can set, clear or
     488 + // toggle segments or characters.
     489 + update func(*Display) error
     490 + want func(size image.Point) *faketerm.Terminal
     491 + wantErr bool
     492 + }{
     493 + {
     494 + desc: "fails on unsupported character",
     495 + char: 'A',
     496 + wantErr: true,
     497 + },
     498 + {
     499 + desc: "doesn't clear the display",
     500 + update: func(d *Display) error {
     501 + return d.SetSegment(D3)
     502 + },
     503 + char: ':',
     504 + want: func(size image.Point) *faketerm.Terminal {
     505 + return mustDrawSegments(size, D1, D2, D3)
     506 + },
     507 + },
     508 + {
     509 + desc: "displays '.'",
     510 + char: '.',
     511 + want: func(size image.Point) *faketerm.Terminal {
     512 + return mustDrawSegments(size, D3)
     513 + },
     514 + },
     515 + {
     516 + desc: "displays ':'",
     517 + char: ':',
     518 + want: func(size image.Point) *faketerm.Terminal {
     519 + return mustDrawSegments(size, D1, D2)
     520 + },
     521 + },
     522 + }
     523 + 
     524 + for _, tc := range tests {
     525 + t.Run(tc.desc, func(t *testing.T) {
     526 + d := New()
     527 + if tc.update != nil {
     528 + err := tc.update(d)
     529 + if err != nil {
     530 + t.Fatalf("tc.update => unexpected error: %v", err)
     531 + }
     532 + }
     533 + 
     534 + {
     535 + err := d.SetCharacter(tc.char)
     536 + if (err != nil) != tc.wantErr {
     537 + t.Errorf("SetCharacter => unexpected error: %v, wantErr: %v", err, tc.wantErr)
     538 + }
     539 + if err != nil {
     540 + return
     541 + }
     542 + }
     543 + 
     544 + ar := image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows)
     545 + cvs, err := canvas.New(ar)
     546 + if err != nil {
     547 + t.Fatalf("canvas.New => unexpected error: %v", err)
     548 + }
     549 + 
     550 + if err := d.Draw(cvs); err != nil {
     551 + t.Fatalf("Draw => unexpected error: %v", err)
     552 + }
     553 + 
     554 + size := area.Size(ar)
     555 + want := faketerm.MustNew(size)
     556 + if tc.want != nil {
     557 + want = tc.want(size)
     558 + }
     559 + 
     560 + got, err := faketerm.New(size)
     561 + if err != nil {
     562 + t.Fatalf("faketerm.New => unexpected error: %v", err)
     563 + }
     564 + if err := cvs.Apply(got); err != nil {
     565 + t.Fatalf("bc.Apply => unexpected error: %v", err)
     566 + }
     567 + if diff := faketerm.Diff(want, got); diff != "" {
     568 + t.Fatalf("SetCharacter => %v", diff)
     569 + }
     570 + })
     571 + }
     572 +}
     573 + 
     574 +func TestAllSegments(t *testing.T) {
     575 + want := []Segment{D1, D2, D3}
     576 + got := AllSegments()
     577 + sort.Slice(got, func(i, j int) bool {
     578 + return int(got[i]) < int(got[j])
     579 + })
     580 + if diff := pretty.Compare(want, got); diff != "" {
     581 + t.Errorf("AllSegments => unexpected diff (-want, +got):\n%s", diff)
     582 + }
     583 +}
     584 + 
     585 +func TestSupportedsChars(t *testing.T) {
     586 + want := []rune{'.', ':'}
     587 + 
     588 + gotStr := SupportedChars()
     589 + var got []rune
     590 + for _, r := range gotStr {
     591 + got = append(got, r)
     592 + }
     593 + sort.Slice(got, func(i, j int) bool {
     594 + return int(got[i]) < int(got[j])
     595 + })
     596 + sort.Slice(want, func(i, j int) bool {
     597 + return int(want[i]) < int(want[j])
     598 + })
     599 + if diff := pretty.Compare(want, got); diff != "" {
     600 + t.Errorf("SupportedChars => unexpected diff (-want, +got):\n%s", diff)
     601 + }
     602 +}
     603 + 
  • ■ ■ ■ ■ ■ ■
    internal/segdisp/dotseg/testdotseg/testdotseg.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 testdotseg provides helpers for tests that use the dotseg package.
     16 +package testdotseg
     17 + 
     18 +import (
     19 + "fmt"
     20 + 
     21 + "github.com/mum4k/termdash/internal/canvas"
     22 + "github.com/mum4k/termdash/internal/segdisp/dotseg"
     23 +)
     24 + 
     25 +// MustSetCharacter sets the character on the display or panics.
     26 +func MustSetCharacter(d *dotseg.Display, c rune) {
     27 + if err := d.SetCharacter(c); err != nil {
     28 + panic(fmt.Errorf("dotseg.Display.SetCharacter => unexpected error: %v", err))
     29 + }
     30 +}
     31 + 
     32 +// MustDraw draws the display onto the canvas or panics.
     33 +func MustDraw(d *dotseg.Display, cvs *canvas.Canvas, opts ...dotseg.Option) {
     34 + if err := d.Draw(cvs, opts...); err != nil {
     35 + panic(fmt.Errorf("dotseg.Display.Draw => unexpected error: %v", err))
     36 + }
     37 +}
     38 + 
  • ■ ■ ■ ■ ■ ■
    internal/segdisp/segdisp.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 segdisp provides utilities used by all segment display types.
     16 +package segdisp
     17 + 
     18 +import (
     19 + "fmt"
     20 + "image"
     21 + "math"
     22 + 
     23 + "github.com/mum4k/termdash/internal/area"
     24 + "github.com/mum4k/termdash/internal/canvas"
     25 + "github.com/mum4k/termdash/internal/canvas/braille"
     26 +)
     27 + 
     28 +// Minimum valid size of a cell canvas in order to draw a segment display.
     29 +const (
     30 + // MinCols is the smallest valid amount of columns in a cell area.
     31 + MinCols = 6
     32 + // MinRowPixels is the smallest valid amount of rows in a cell area.
     33 + MinRows = 5
     34 +)
     35 + 
     36 +// aspectRatio is the desired aspect ratio of a single segment display.
     37 +var aspectRatio = image.Point{3, 5}
     38 + 
     39 +// Required when given an area of cells, returns either an area of the same
     40 +// size or a smaller area that is required to draw one segment display (i.e.
     41 +// one character).
     42 +// Returns a smaller area when the provided area didn't have the required
     43 +// aspect ratio.
     44 +// Returns an error if the area is too small to draw a segment display, i.e.
     45 +// smaller than MinCols x MinRows.
     46 +func Required(cellArea image.Rectangle) (image.Rectangle, error) {
     47 + if cols, rows := cellArea.Dx(), cellArea.Dy(); cols < MinCols || rows < MinRows {
     48 + return image.ZR, fmt.Errorf("cell area %v is too small to draw the segment display, has %dx%d cells, need at least %dx%d cells",
     49 + cellArea, cols, rows, MinCols, MinRows)
     50 + }
     51 + 
     52 + bcAr := image.Rect(cellArea.Min.X, cellArea.Min.Y, cellArea.Max.X*braille.ColMult, cellArea.Max.Y*braille.RowMult)
     53 + bcArAdj := area.WithRatio(bcAr, aspectRatio)
     54 + 
     55 + needCols := int(math.Ceil(float64(bcArAdj.Dx()) / braille.ColMult))
     56 + needRows := int(math.Ceil(float64(bcArAdj.Dy()) / braille.RowMult))
     57 + needAr := image.Rect(cellArea.Min.X, cellArea.Min.Y, cellArea.Min.X+needCols, cellArea.Min.Y+needRows)
     58 + return needAr, nil
     59 +}
     60 + 
     61 +// ToBraille converts the canvas into a braille canvas and returns a pixel area
     62 +// with aspect ratio adjusted for the segment display.
     63 +func ToBraille(cvs *canvas.Canvas) (*braille.Canvas, image.Rectangle, error) {
     64 + ar, err := Required(cvs.Area())
     65 + if err != nil {
     66 + return nil, image.ZR, fmt.Errorf("Required => %v", err)
     67 + }
     68 + 
     69 + bc, err := braille.New(ar)
     70 + if err != nil {
     71 + return nil, image.ZR, fmt.Errorf("braille.New => %v", err)
     72 + }
     73 + return bc, area.WithRatio(bc.Area(), aspectRatio), nil
     74 +}
     75 + 
     76 +// SegmentSize given an area for the display segment determines the size of
     77 +// individual segments, i.e. the width of a vertical or the height of a
     78 +// horizontal segment.
     79 +func SegmentSize(ar image.Rectangle) int {
     80 + // widthPerc is the relative width of a segment to the width of the canvas.
     81 + const widthPerc = 9
     82 + s := int(math.Round(float64(ar.Dx()) * widthPerc / 100))
     83 + if s > 3 && s%2 == 0 {
     84 + // Segments with odd number of pixels in their width/height look
     85 + // better, since the spike at the top of their slopes has only one
     86 + // pixel.
     87 + s++
     88 + }
     89 + return s
     90 +}
     91 + 
  • ■ ■ ■ ■ ■ ■
    internal/segdisp/segdisp_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 segdisp
     16 + 
     17 +import (
     18 + "image"
     19 + "testing"
     20 + 
     21 + "github.com/kylelemons/godebug/pretty"
     22 + "github.com/mum4k/termdash/internal/canvas"
     23 + "github.com/mum4k/termdash/internal/canvas/braille"
     24 + "github.com/mum4k/termdash/internal/canvas/braille/testbraille"
     25 +)
     26 + 
     27 +func TestRequired(t *testing.T) {
     28 + tests := []struct {
     29 + desc string
     30 + cellArea image.Rectangle
     31 + want image.Rectangle
     32 + wantErr bool
     33 + }{
     34 + {
     35 + desc: "fails when area isn't wide enough",
     36 + cellArea: image.Rect(0, 0, MinCols-1, MinRows),
     37 + wantErr: true,
     38 + },
     39 + {
     40 + desc: "fails when area isn't tall enough",
     41 + cellArea: image.Rect(0, 0, MinCols, MinRows-1),
     42 + wantErr: true,
     43 + },
     44 + {
     45 + desc: "returns same area when no adjustment needed",
     46 + cellArea: image.Rect(0, 0, MinCols, MinRows),
     47 + want: image.Rect(0, 0, MinCols, MinRows),
     48 + },
     49 + {
     50 + desc: "adjusts width to aspect ratio",
     51 + cellArea: image.Rect(0, 0, MinCols+100, MinRows),
     52 + want: image.Rect(0, 0, MinCols, MinRows),
     53 + },
     54 + {
     55 + desc: "adjusts height to aspect ratio",
     56 + cellArea: image.Rect(0, 0, MinCols, MinRows+100),
     57 + want: image.Rect(0, 0, MinCols, MinRows),
     58 + },
     59 + {
     60 + desc: "adjusts larger area to aspect ratio",
     61 + cellArea: image.Rect(0, 0, MinCols*2, MinRows*4),
     62 + want: image.Rect(0, 0, 12, 10),
     63 + },
     64 + }
     65 + 
     66 + for _, tc := range tests {
     67 + t.Run(tc.desc, func(t *testing.T) {
     68 + got, err := Required(tc.cellArea)
     69 + if (err != nil) != tc.wantErr {
     70 + t.Errorf("Required => unexpected error: %v, wantErr: %v", err, tc.wantErr)
     71 + }
     72 + if err != nil {
     73 + return
     74 + }
     75 + 
     76 + if diff := pretty.Compare(tc.want, got); diff != "" {
     77 + t.Errorf("Required => unexpected diff (-want, +got):\n%s", diff)
     78 + }
     79 + })
     80 + }
     81 +}
     82 + 
     83 +func TestToBraille(t *testing.T) {
     84 + tests := []struct {
     85 + desc string
     86 + cellArea image.Rectangle
     87 + wantBC *braille.Canvas
     88 + wantAr image.Rectangle
     89 + wantErr bool
     90 + }{
     91 + {
     92 + desc: "fails when area isn't wide enough",
     93 + cellArea: image.Rect(0, 0, MinCols-1, MinRows),
     94 + wantErr: true,
     95 + },
     96 + {
     97 + desc: "canvas creates braille with the desired aspect ratio",
     98 + cellArea: image.Rect(0, 0, MinCols, MinRows),
     99 + wantBC: testbraille.MustNew(image.Rect(0, 0, MinCols, MinRows)),
     100 + wantAr: image.Rect(0, 0, MinCols*braille.ColMult, MinRows*braille.RowMult),
     101 + },
     102 + }
     103 + 
     104 + for _, tc := range tests {
     105 + t.Run(tc.desc, func(t *testing.T) {
     106 + cvs, err := canvas.New(tc.cellArea)
     107 + if err != nil {
     108 + t.Fatalf("canvas.New => unexpected error: %v", err)
     109 + }
     110 + 
     111 + gotBC, gotAr, err := ToBraille(cvs)
     112 + if (err != nil) != tc.wantErr {
     113 + t.Errorf("ToBraille => unexpected error: %v, wantErr: %v", err, tc.wantErr)
     114 + }
     115 + if err != nil {
     116 + return
     117 + }
     118 + 
     119 + if diff := pretty.Compare(tc.wantBC, gotBC); diff != "" {
     120 + t.Errorf("ToBraille => unexpected braille canvas, diff (-want, +got):\n%s", diff)
     121 + }
     122 + if diff := pretty.Compare(tc.wantAr, gotAr); diff != "" {
     123 + t.Errorf("ToBraille => unexpected area, diff (-want, +got):\n%s", diff)
     124 + }
     125 + })
     126 + }
     127 +}
     128 + 
     129 +func TestSegmentSize(t *testing.T) {
     130 + tests := []struct {
     131 + desc string
     132 + ar image.Rectangle
     133 + want int
     134 + }{
     135 + {
     136 + desc: "zero area",
     137 + ar: image.ZR,
     138 + want: 0,
     139 + },
     140 + {
     141 + desc: "smallest segment size",
     142 + ar: image.Rect(0, 0, 15, 1),
     143 + want: 1,
     144 + },
     145 + {
     146 + desc: "allows even size of two",
     147 + ar: image.Rect(0, 0, 22, 1),
     148 + want: 2,
     149 + },
     150 + {
     151 + desc: "lands on even width, corrected to odd",
     152 + ar: image.Rect(0, 0, 44, 1),
     153 + want: 5,
     154 + },
     155 + {
     156 + desc: "lands on odd width",
     157 + ar: image.Rect(0, 0, 55, 1),
     158 + want: 5,
     159 + },
     160 + }
     161 + 
     162 + for _, tc := range tests {
     163 + t.Run(tc.desc, func(t *testing.T) {
     164 + got := SegmentSize(tc.ar)
     165 + if got != tc.want {
     166 + t.Errorf("SegmentSize => %d, want %d", got, tc.want)
     167 + }
     168 + })
     169 + }
     170 +}
     171 + 
  • ■ ■ ■ ■ ■ ■
    internal/segdisp/sixteen/attributes.go
    skipped 22 lines
    23 23   "math"
    24 24   
    25 25   "github.com/mum4k/termdash/internal/numbers"
     26 + "github.com/mum4k/termdash/internal/segdisp"
    26 27   "github.com/mum4k/termdash/internal/segdisp/segment"
    27 28  )
    28 29   
    skipped 21 lines
    50 51   L: segment.LeftToRight,
    51 52  }
    52 53   
    53  -// segmentSize given an area for the display determines the size of individual
    54  -// segments, i.e. the width of a vertical or the height of a horizontal
    55  -// segment.
    56  -func segmentSize(ar image.Rectangle) int {
    57  - // widthPerc is the relative width of a segment to the width of the canvas.
    58  - const widthPerc = 9
    59  - s := int(math.Round(float64(ar.Dx()) * widthPerc / 100))
    60  - if s > 3 && s%2 == 0 {
    61  - // Segments with odd number of pixels in their width/height look
    62  - // better, since the spike at the top of their slopes has only one
    63  - // pixel.
    64  - s++
    65  - }
    66  - return s
    67  -}
    68  - 
    69  -// attributes contains attributes needed to draw the segment display.
     54 +// Attributes contains attributes needed to draw the segment display.
    70 55  // Refer to doc/segment_placement.svg for a visual aid and explanation of the
    71 56  // usage of the square roots.
    72  -type attributes struct {
     57 +type Attributes struct {
    73 58   // segSize is the width of a vertical or height of a horizontal segment.
    74 59   segSize int
    75 60   
    skipped 31 lines
    107 92   // vertCenY is the Y coordinate where the area of the segment vertically
    108 93   // in the center starts, i.e. Y coordinate of G1 and G2.
    109 94   vertCenY int
    110  - // vertBotY is the Y coordinate where the area of the segment vertically
     95 + // VertBotY is the Y coordinate where the area of the segment vertically
    111 96   // at the bottom starts, i.e. Y coordinate of D1 and D2.
    112  - vertBotY int
     97 + VertBotY int
    113 98  }
    114 99   
    115  -// newAttributes calculates attributes needed to place the segments for the
     100 +// NewAttributes calculates attributes needed to place the segments for the
    116 101  // provided pixel area.
    117  -func newAttributes(bcAr image.Rectangle) *attributes {
    118  - segSize := segmentSize(bcAr)
     102 +func NewAttributes(bcAr image.Rectangle) *Attributes {
     103 + segSize := segdisp.SegmentSize(bcAr)
    119 104   
    120 105   // diaPerc is the size of the diaGap in percentage of the segment's size.
    121 106   const diaPerc = 40
    skipped 40 lines
    162 147   vertCenY := horizLeftX + longLen + offset
    163 148   vertBotY := horizLeftX + longLen + ptp + longLen + offset
    164 149   
    165  - return &attributes{
     150 + return &Attributes{
    166 151   segSize: segSize,
    167 152   diaGap: diaGap,
    168 153   segPeakDist: segPeakDist,
    skipped 6 lines
    175 160   horizMidX: horizMidX,
    176 161   horizRightX: horizRightX,
    177 162   vertCenY: vertCenY,
    178  - vertBotY: vertBotY,
     163 + VertBotY: vertBotY,
    179 164   }
    180 165  }
    181 166   
    182 167  // hvSegArea returns the area for the specified horizontal or vertical segment.
    183  -func (a *attributes) hvSegArea(s Segment) image.Rectangle {
     168 +func (a *Attributes) hvSegArea(s Segment) image.Rectangle {
    184 169   var (
    185 170   start image.Point
    186 171   length int
    skipped 46 lines
    233 218   length = a.longLen
    234 219   
    235 220   case D1:
    236  - start = image.Point{a.horizLeftX, a.vertBotY}
     221 + start = image.Point{a.horizLeftX, a.VertBotY}
    237 222   length = a.shortLen
    238 223   
    239 224   case D2:
    240 225   d1 := a.hvSegArea(D1)
    241  - start = image.Point{d1.Max.X + a.peakToPeak, a.vertBotY}
     226 + start = image.Point{d1.Max.X + a.peakToPeak, a.VertBotY}
    242 227   length = a.shortLen
    243 228   
    244 229   default:
    skipped 5 lines
    250 235   
    251 236  // hvArFromStart given start coordinates of a segment, its length and its type,
    252 237  // determines its area.
    253  -func (a *attributes) hvArFromStart(start image.Point, s Segment, length int) image.Rectangle {
     238 +func (a *Attributes) hvArFromStart(start image.Point, s Segment, length int) image.Rectangle {
    254 239   st := hvSegType[s]
    255 240   switch st {
    256 241   case segment.Horizontal:
    skipped 6 lines
    263 248  }
    264 249   
    265 250  // diaSegArea returns the area for the specified diagonal segment.
    266  -func (a *attributes) diaSegArea(s Segment) image.Rectangle {
     251 +func (a *Attributes) diaSegArea(s Segment) image.Rectangle {
    267 252   switch s {
    268 253   case H:
    269 254   return a.diaBetween(A1, F, J, G1)
    skipped 11 lines
    281 266   
    282 267  // diaBetween given four segments (two horizontal and two vertical) returns the
    283 268  // area between them for a diagonal segment.
    284  -func (a *attributes) diaBetween(top, left, right, bottom Segment) image.Rectangle {
     269 +func (a *Attributes) diaBetween(top, left, right, bottom Segment) image.Rectangle {
    285 270   topAr := a.hvSegArea(top)
    286 271   leftAr := a.hvSegArea(left)
    287 272   rightAr := a.hvSegArea(right)
    skipped 14 lines
  • ■ ■ ■ ■ ■
    internal/segdisp/sixteen/sixteen.go
    skipped 40 lines
    41 41   
    42 42  import (
    43 43   "fmt"
    44  - "image"
    45  - "math"
    46 44   "strings"
    47 45   
    48 46   "github.com/mum4k/termdash/cell"
    49  - "github.com/mum4k/termdash/internal/area"
    50 47   "github.com/mum4k/termdash/internal/canvas"
    51  - "github.com/mum4k/termdash/internal/canvas/braille"
     48 + "github.com/mum4k/termdash/internal/segdisp"
    52 49   "github.com/mum4k/termdash/internal/segdisp/segment"
    53 50  )
    54 51   
    skipped 85 lines
    140 137   '+': {J, G1, G2, M},
    141 138   ',': {N},
    142 139   '-': {G1, G2},
     140 + '.': {D1},
    143 141   '/': {N, K},
    144 142   
    145 143   '0': {A1, A2, F, K, B, E, N, C, D1, D2},
    skipped 229 lines
    375 373   return nil
    376 374  }
    377 375   
    378  -// Minimum valid size of a cell canvas in order to draw the segment display.
    379  -const (
    380  - // MinCols is the smallest valid amount of columns in a cell area.
    381  - MinCols = 6
    382  - // MinRowPixels is the smallest valid amount of rows in a cell area.
    383  - MinRows = 5
    384  -)
    385  - 
    386  -// aspectRatio is the desired aspect ratio of a single segment display.
    387  -var aspectRatio = image.Point{3, 5}
    388  - 
    389 376  // Draw draws the current state of the segment display onto the canvas.
    390 377  // The canvas must be at least MinCols x MinRows cells, or an error will be
    391 378  // returned.
    skipped 3 lines
    395 382   o.set(d)
    396 383   }
    397 384   
    398  - bc, bcAr, err := toBraille(cvs)
     385 + bc, bcAr, err := segdisp.ToBraille(cvs)
    399 386   if err != nil {
    400 387   return err
    401 388   }
    402 389   
    403  - attr := newAttributes(bcAr)
     390 + attr := NewAttributes(bcAr)
    404 391   var sOpts []segment.Option
    405 392   if len(d.cellOpts) > 0 {
    406 393   sOpts = append(sOpts, segment.CellOpts(d.cellOpts...))
    skipped 45 lines
    452 439   return bc.CopyTo(cvs)
    453 440  }
    454 441   
    455  -// Required when given an area of cells, returns either an area of the same
    456  -// size or a smaller area that is required to draw one display.
    457  -// Returns a smaller area when the provided area didn't have the required
    458  -// aspect ratio.
    459  -// Returns an error if the area is too small to draw a segment display, i.e.
    460  -// smaller than MinCols x MinRows.
    461  -func Required(cellArea image.Rectangle) (image.Rectangle, error) {
    462  - if cols, rows := cellArea.Dx(), cellArea.Dy(); cols < MinCols || rows < MinRows {
    463  - return image.ZR, fmt.Errorf("cell area %v is too small to draw the segment display, has %dx%d cells, need at least %dx%d cells",
    464  - cellArea, cols, rows, MinCols, MinRows)
    465  - }
    466  - 
    467  - bcAr := image.Rect(cellArea.Min.X, cellArea.Min.Y, cellArea.Max.X*braille.ColMult, cellArea.Max.Y*braille.RowMult)
    468  - bcArAdj := area.WithRatio(bcAr, aspectRatio)
    469  - 
    470  - needCols := int(math.Ceil(float64(bcArAdj.Dx()) / braille.ColMult))
    471  - needRows := int(math.Ceil(float64(bcArAdj.Dy()) / braille.RowMult))
    472  - needAr := image.Rect(cellArea.Min.X, cellArea.Min.Y, cellArea.Min.X+needCols, cellArea.Min.Y+needRows)
    473  - return needAr, nil
    474  -}
    475  - 
    476  -// toBraille converts the canvas into a braille canvas and returns a pixel area
    477  -// with aspect ratio adjusted for the segment display.
    478  -func toBraille(cvs *canvas.Canvas) (*braille.Canvas, image.Rectangle, error) {
    479  - ar, err := Required(cvs.Area())
    480  - if err != nil {
    481  - return nil, image.ZR, fmt.Errorf("Required => %v", err)
    482  - }
    483  - 
    484  - bc, err := braille.New(ar)
    485  - if err != nil {
    486  - return nil, image.ZR, fmt.Errorf("braille.New => %v", err)
    487  - }
    488  - return bc, area.WithRatio(bc.Area(), aspectRatio), nil
    489  -}
    490  - 
  • ■ ■ ■ ■ ■ ■
    internal/segdisp/sixteen/sixteen_test.go
    skipped 25 lines
    26 26   "github.com/mum4k/termdash/internal/canvas/braille/testbraille"
    27 27   "github.com/mum4k/termdash/internal/canvas/testcanvas"
    28 28   "github.com/mum4k/termdash/internal/faketerm"
     29 + "github.com/mum4k/termdash/internal/segdisp"
    29 30   "github.com/mum4k/termdash/internal/segdisp/segment"
    30 31   "github.com/mum4k/termdash/internal/segdisp/segment/testsegment"
    31 32  )
    skipped 13 lines
    45 46   }{
    46 47   {
    47 48   desc: "fails for area not wide enough",
    48  - cellCanvas: image.Rect(0, 0, MinCols-1, MinRows),
     49 + cellCanvas: image.Rect(0, 0, segdisp.MinCols-1, segdisp.MinRows),
    49 50   wantErr: true,
    50 51   },
    51 52   {
    52 53   desc: "fails for area not tall enough",
    53  - cellCanvas: image.Rect(0, 0, MinCols, MinRows-1),
     54 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows-1),
    54 55   wantErr: true,
    55 56   },
    56 57   {
    57 58   desc: "fails to set invalid segment (too small)",
    58  - cellCanvas: image.Rect(0, 0, MinCols, MinRows),
     59 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    59 60   update: func(d *Display) error {
    60 61   return d.SetSegment(Segment(-1))
    61 62   },
    skipped 1 lines
    63 64   },
    64 65   {
    65 66   desc: "fails to set invalid segment (too large)",
    66  - cellCanvas: image.Rect(0, 0, MinCols, MinRows),
     67 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    67 68   update: func(d *Display) error {
    68 69   return d.SetSegment(Segment(segmentMax))
    69 70   },
    skipped 1 lines
    71 72   },
    72 73   {
    73 74   desc: "fails to clear invalid segment (too small)",
    74  - cellCanvas: image.Rect(0, 0, MinCols, MinRows),
     75 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    75 76   update: func(d *Display) error {
    76 77   return d.ClearSegment(Segment(-1))
    77 78   },
    skipped 1 lines
    79 80   },
    80 81   {
    81 82   desc: "fails to clear invalid segment (too large)",
    82  - cellCanvas: image.Rect(0, 0, MinCols, MinRows),
     83 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    83 84   update: func(d *Display) error {
    84 85   return d.ClearSegment(Segment(segmentMax))
    85 86   },
    skipped 1 lines
    87 88   },
    88 89   {
    89 90   desc: "fails to toggle invalid segment (too small)",
    90  - cellCanvas: image.Rect(0, 0, MinCols, MinRows),
     91 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    91 92   update: func(d *Display) error {
    92 93   return d.ToggleSegment(Segment(-1))
    93 94   },
    skipped 1 lines
    95 96   },
    96 97   {
    97 98   desc: "fails to toggle invalid segment (too large)",
    98  - cellCanvas: image.Rect(0, 0, MinCols, MinRows),
     99 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    99 100   update: func(d *Display) error {
    100 101   return d.ToggleSegment(Segment(segmentMax))
    101 102   },
    skipped 1 lines
    103 104   },
    104 105   {
    105 106   desc: "empty when no segments set",
    106  - cellCanvas: image.Rect(0, 0, MinCols, MinRows),
     107 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    107 108   },
    108 109   {
    109 110   desc: "smallest valid display 6x5, A1",
    110  - cellCanvas: image.Rect(0, 0, MinCols, MinRows),
     111 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    111 112   update: func(d *Display) error {
    112 113   return d.SetSegment(A1)
    113 114   },
    skipped 8 lines
    122 123   },
    123 124   {
    124 125   desc: "smallest valid display 6x5, A2",
    125  - cellCanvas: image.Rect(0, 0, MinCols, MinRows),
     126 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    126 127   update: func(d *Display) error {
    127 128   return d.SetSegment(A2)
    128 129   },
    skipped 8 lines
    137 138   },
    138 139   {
    139 140   desc: "smallest valid display 6x5, F",
    140  - cellCanvas: image.Rect(0, 0, MinCols, MinRows),
     141 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    141 142   update: func(d *Display) error {
    142 143   return d.SetSegment(F)
    143 144   },
    skipped 8 lines
    152 153   },
    153 154   {
    154 155   desc: "smallest valid display 6x5, J",
    155  - cellCanvas: image.Rect(0, 0, MinCols, MinRows),
     156 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    156 157   update: func(d *Display) error {
    157 158   return d.SetSegment(J)
    158 159   },
    skipped 8 lines
    167 168   },
    168 169   {
    169 170   desc: "smallest valid display 6x5, B",
    170  - cellCanvas: image.Rect(0, 0, MinCols, MinRows),
     171 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    171 172   update: func(d *Display) error {
    172 173   return d.SetSegment(B)
    173 174   },
    skipped 8 lines
    182 183   },
    183 184   {
    184 185   desc: "smallest valid display 6x5, G1",
    185  - cellCanvas: image.Rect(0, 0, MinCols, MinRows),
     186 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    186 187   update: func(d *Display) error {
    187 188   return d.SetSegment(G1)
    188 189   },
    skipped 8 lines
    197 198   },
    198 199   {
    199 200   desc: "smallest valid display 6x5, G2",
    200  - cellCanvas: image.Rect(0, 0, MinCols, MinRows),
     201 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    201 202   update: func(d *Display) error {
    202 203   return d.SetSegment(G2)
    203 204   },
    skipped 8 lines
    212 213   },
    213 214   {
    214 215   desc: "smallest valid display 6x5, E",
    215  - cellCanvas: image.Rect(0, 0, MinCols, MinRows),
     216 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    216 217   update: func(d *Display) error {
    217 218   return d.SetSegment(E)
    218 219   },
    skipped 8 lines
    227 228   },
    228 229   {
    229 230   desc: "smallest valid display 6x5, M",
    230  - cellCanvas: image.Rect(0, 0, MinCols, MinRows),
     231 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    231 232   update: func(d *Display) error {
    232 233   return d.SetSegment(M)
    233 234   },
    skipped 8 lines
    242 243   },
    243 244   {
    244 245   desc: "smallest valid display 6x5, C",
    245  - cellCanvas: image.Rect(0, 0, MinCols, MinRows),
     246 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    246 247   update: func(d *Display) error {
    247 248   return d.SetSegment(C)
    248 249   },
    skipped 8 lines
    257 258   },
    258 259   {
    259 260   desc: "smallest valid display 6x5, D1",
    260  - cellCanvas: image.Rect(0, 0, MinCols, MinRows),
     261 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    261 262   update: func(d *Display) error {
    262 263   return d.SetSegment(D1)
    263 264   },
    skipped 8 lines
    272 273   },
    273 274   {
    274 275   desc: "smallest valid display 6x5, D2",
    275  - cellCanvas: image.Rect(0, 0, MinCols, MinRows),
     276 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    276 277   update: func(d *Display) error {
    277 278   return d.SetSegment(D2)
    278 279   },
    skipped 8 lines
    287 288   },
    288 289   {
    289 290   desc: "smallest valid display 6x5, H",
    290  - cellCanvas: image.Rect(0, 0, MinCols, MinRows),
     291 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    291 292   update: func(d *Display) error {
    292 293   return d.SetSegment(H)
    293 294   },
    skipped 8 lines
    302 303   },
    303 304   {
    304 305   desc: "smallest valid display 6x5, K",
    305  - cellCanvas: image.Rect(0, 0, MinCols, MinRows),
     306 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    306 307   update: func(d *Display) error {
    307 308   return d.SetSegment(K)
    308 309   },
    skipped 9 lines
    318 319   
    319 320   {
    320 321   desc: "smallest valid display 6x5, N",
    321  - cellCanvas: image.Rect(0, 0, MinCols, MinRows),
     322 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    322 323   update: func(d *Display) error {
    323 324   return d.SetSegment(N)
    324 325   },
    skipped 8 lines
    333 334   },
    334 335   {
    335 336   desc: "smallest valid display 6x5, L",
    336  - cellCanvas: image.Rect(0, 0, MinCols, MinRows),
     337 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    337 338   update: func(d *Display) error {
    338 339   return d.SetSegment(L)
    339 340   },
    skipped 8 lines
    348 349   },
    349 350   {
    350 351   desc: "smallest valid display 6x5, all segments",
    351  - cellCanvas: image.Rect(0, 0, MinCols, MinRows),
     352 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    352 353   update: func(d *Display) error {
    353 354   for _, s := range AllSegments() {
    354 355   if err := d.SetSegment(s); err != nil {
    skipped 39 lines
    394 395   cell.BgColor(cell.ColorGreen),
    395 396   ),
    396 397   },
    397  - cellCanvas: image.Rect(0, 0, MinCols, MinRows),
     398 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    398 399   update: func(d *Display) error {
    399 400   for _, s := range AllSegments() {
    400 401   if err := d.SetSegment(s); err != nil {
    skipped 43 lines
    444 445   cell.BgColor(cell.ColorGreen),
    445 446   ),
    446 447   },
    447  - cellCanvas: image.Rect(0, 0, MinCols, MinRows),
     448 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    448 449   update: func(d *Display) error {
    449 450   for _, s := range AllSegments() {
    450 451   if err := d.SetSegment(s); err != nil {
    skipped 37 lines
    488 489   },
    489 490   {
    490 491   desc: "clears the display",
    491  - cellCanvas: image.Rect(0, 0, MinCols, MinRows),
     492 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    492 493   update: func(d *Display) error {
    493 494   for _, s := range AllSegments() {
    494 495   if err := d.SetSegment(s); err != nil {
    skipped 6 lines
    501 502   },
    502 503   {
    503 504   desc: "clears the display and sets cell options",
    504  - cellCanvas: image.Rect(0, 0, MinCols, MinRows),
     505 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    505 506   update: func(d *Display) error {
    506 507   d.Clear(CellOpts(cell.FgColor(cell.ColorBlue)))
    507 508   return d.SetSegment(A1)
    skipped 9 lines
    517 518   },
    518 519   {
    519 520   desc: "clears some segments",
    520  - cellCanvas: image.Rect(0, 0, MinCols, MinRows),
     521 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    521 522   update: func(d *Display) error {
    522 523   for _, s := range AllSegments() {
    523 524   if err := d.SetSegment(s); err != nil {
    skipped 29 lines
    553 554   },
    554 555   {
    555 556   desc: "toggles some segments off",
    556  - cellCanvas: image.Rect(0, 0, MinCols, MinRows),
     557 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    557 558   update: func(d *Display) error {
    558 559   for _, s := range AllSegments() {
    559 560   if err := d.SetSegment(s); err != nil {
    skipped 29 lines
    589 590   },
    590 591   {
    591 592   desc: "toggles some segments on",
    592  - cellCanvas: image.Rect(0, 0, MinCols, MinRows),
     593 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    593 594   update: func(d *Display) error {
    594 595   for _, s := range []Segment{A1, A2, G1, G2, D1, D2, L} {
    595 596   if err := d.ToggleSegment(s); err != nil {
    skipped 23 lines
    619 620   },
    620 621   {
    621 622   desc: "set is idempotent",
    622  - cellCanvas: image.Rect(0, 0, MinCols, MinRows),
     623 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    623 624   update: func(d *Display) error {
    624 625   if err := d.SetSegment(A1); err != nil {
    625 626   return err
    skipped 11 lines
    637 638   },
    638 639   {
    639 640   desc: "clear is idempotent",
    640  - cellCanvas: image.Rect(0, 0, MinCols, MinRows),
     641 + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    641 642   update: func(d *Display) error {
    642 643   if err := d.SetSegment(A1); err != nil {
    643 644   return err
    skipped 6 lines
    650 651   },
    651 652   {
    652 653   desc: "segment width of two",
    653  - cellCanvas: image.Rect(0, 0, MinCols*2, MinRows*2),
     654 + cellCanvas: image.Rect(0, 0, segdisp.MinCols*2, segdisp.MinRows*2),
    654 655   update: func(d *Display) error {
    655 656   for _, s := range AllSegments() {
    656 657   if err := d.SetSegment(s); err != nil {
    skipped 33 lines
    690 691   },
    691 692   {
    692 693   desc: "segment width of three",
    693  - cellCanvas: image.Rect(0, 0, MinCols*3, MinRows*3),
     694 + cellCanvas: image.Rect(0, 0, segdisp.MinCols*3, segdisp.MinRows*3),
    694 695   update: func(d *Display) error {
    695 696   for _, s := range AllSegments() {
    696 697   if err := d.SetSegment(s); err != nil {
    skipped 33 lines
    730 731   },
    731 732   {
    732 733   desc: "segment with even width is changed to odd",
    733  - cellCanvas: image.Rect(0, 0, MinCols*4, MinRows*4),
     734 + cellCanvas: image.Rect(0, 0, segdisp.MinCols*4, segdisp.MinRows*4),
    734 735   update: func(d *Display) error {
    735 736   for _, s := range AllSegments() {
    736 737   if err := d.SetSegment(s); err != nil {
    skipped 33 lines
    770 771   },
    771 772   {
    772 773   desc: "segment with odd width and e√en peak to peak distance is changed to odd",
    773  - cellCanvas: image.Rect(0, 0, MinCols*7, MinRows*7),
     774 + cellCanvas: image.Rect(0, 0, segdisp.MinCols*7, segdisp.MinRows*7),
    774 775   update: func(d *Display) error {
    775 776   for _, s := range AllSegments() {
    776 777   if err := d.SetSegment(s); err != nil {
    skipped 115 lines
    892 893   }{
    893 894   {
    894 895   desc: "fails on unsupported character",
    895  - char: '.',
     896 + char: 'â',
    896 897   wantErr: true,
    897 898   },
    898 899   {
    skipped 683 lines
    1582 1583   }
    1583 1584   }
    1584 1585   
    1585  - ar := image.Rect(0, 0, MinCols, MinRows)
     1586 + ar := image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows)
    1586 1587   cvs, err := canvas.New(ar)
    1587 1588   if err != nil {
    1588 1589   t.Fatalf("canvas.New => unexpected error: %v", err)
    skipped 23 lines
    1612 1613   }
    1613 1614  }
    1614 1615   
    1615  -func TestRequired(t *testing.T) {
    1616  - tests := []struct {
    1617  - desc string
    1618  - cellArea image.Rectangle
    1619  - want image.Rectangle
    1620  - wantErr bool
    1621  - }{
    1622  - {
    1623  - desc: "fails when area isn't wide enough",
    1624  - cellArea: image.Rect(0, 0, MinCols-1, MinRows),
    1625  - wantErr: true,
    1626  - },
    1627  - {
    1628  - desc: "fails when area isn't tall enough",
    1629  - cellArea: image.Rect(0, 0, MinCols, MinRows-1),
    1630  - wantErr: true,
    1631  - },
    1632  - {
    1633  - desc: "returns same area when no adjustment needed",
    1634  - cellArea: image.Rect(0, 0, MinCols, MinRows),
    1635  - want: image.Rect(0, 0, MinCols, MinRows),
    1636  - },
    1637  - {
    1638  - desc: "adjusts width to aspect ratio",
    1639  - cellArea: image.Rect(0, 0, MinCols+100, MinRows),
    1640  - want: image.Rect(0, 0, MinCols, MinRows),
    1641  - },
    1642  - {
    1643  - desc: "adjusts height to aspect ratio",
    1644  - cellArea: image.Rect(0, 0, MinCols, MinRows+100),
    1645  - want: image.Rect(0, 0, MinCols, MinRows),
    1646  - },
    1647  - {
    1648  - desc: "adjusts larger area to aspect ratio",
    1649  - cellArea: image.Rect(0, 0, MinCols*2, MinRows*4),
    1650  - want: image.Rect(0, 0, 12, 10),
    1651  - },
    1652  - }
    1653  - 
    1654  - for _, tc := range tests {
    1655  - t.Run(tc.desc, func(t *testing.T) {
    1656  - got, err := Required(tc.cellArea)
    1657  - if (err != nil) != tc.wantErr {
    1658  - t.Errorf("Required => unexpected error: %v, wantErr: %v", err, tc.wantErr)
    1659  - }
    1660  - if err != nil {
    1661  - return
    1662  - }
    1663  - 
    1664  - if diff := pretty.Compare(tc.want, got); diff != "" {
    1665  - t.Errorf("Required => unexpected diff (-want, +got):\n%s", diff)
    1666  - }
    1667  - })
    1668  - }
    1669  -}
    1670  - 
    1671 1616  func TestAllSegments(t *testing.T) {
    1672 1617   want := []Segment{A1, A2, B, C, D1, D2, E, F, G1, G2, H, J, K, L, M, N}
    1673 1618   got := AllSegments()
    skipped 23 lines
    1697 1642   },
    1698 1643   {
    1699 1644   desc: "supports some chars in the string",
    1700  - str: " w.W :",
     1645 + str: " wâW :",
    1701 1646   wantRes: false,
    1702  - wantUnsupp: []rune{'.'},
     1647 + wantUnsupp: []rune{'â'},
    1703 1648   },
    1704 1649   {
    1705 1650   desc: "supports no chars in the string",
    1706  - str: ".",
     1651 + str: "â",
    1707 1652   wantRes: false,
    1708  - wantUnsupp: []rune{'.'},
     1653 + wantUnsupp: []rune{'â'},
    1709 1654   },
    1710 1655   }
    1711 1656   
    skipped 33 lines
    1745 1690   },
    1746 1691   {
    1747 1692   desc: "some characters are supported",
    1748  - str: " w.W:",
     1693 + str: " wâW:",
    1749 1694   want: " w W:",
    1750 1695   },
    1751 1696   {
    1752 1697   desc: "no characters are supported",
    1753  - str: ".",
     1698 + str: "â",
    1754 1699   want: " ",
    1755 1700   },
    1756 1701   }
    skipped 11 lines
  • ■ ■ ■ ■ ■ ■
    internal/wrap/wrap.go
    skipped 58 lines
    59 59  )
    60 60   
    61 61  // ValidText validates the provided text for wrapping.
    62  -// The text must not contain any control or space characters other
    63  -// than '\n' and ' '.
     62 +// The text must not be empty, contain any control or
     63 +// space characters other than '\n' and ' '.
    64 64  func ValidText(text string) error {
    65 65   if text == "" {
    66 66   return errors.New("the text cannot be empty")
    skipped 344 lines
  • ■ ■ ■ ■ ■ ■
    widgets/donut/donut.go
    skipped 166 lines
    167 167  // The text is only drawn if the radius of the donut "hole" is large enough to
    168 168  // accommodate it.
    169 169  // The mid point addresses coordinates in pixels on a braille canvas.
    170  -// The donutAr is the cell area for the donut itself.
    171  -func (d *Donut) drawText(cvs *canvas.Canvas, donutAr image.Rectangle, mid image.Point, holeR int) error {
     170 +func (d *Donut) drawText(cvs *canvas.Canvas, mid image.Point, holeR int) error {
    172 171   cells, first := availableCells(mid, holeR)
    173 172   t := d.progressText()
    174 173   needCells := runewidth.StringWidth(t)
    skipped 1 lines
    176 175   return nil
    177 176   }
    178 177   
    179  - if donutAr.Min.Y > 0 {
    180  - // donutAr is what the braille canvas is created from, mid is relative
    181  - // to it.
    182  - // donutAr might have non-zero Y coordinate if we are displaying a text
    183  - // label.
    184  - first.Y += donutAr.Min.Y
    185  - }
    186 178   ar := image.Rect(first.X, first.Y, first.X+cells+2, first.Y+1)
    187 179   start, err := alignfor.Text(ar, t, align.HorizontalCenter, align.VerticalMiddle)
    188 180   if err != nil {
    skipped 7 lines
    196 188   
    197 189  // drawLabel draws the text label in the area.
    198 190  func (d *Donut) drawLabel(cvs *canvas.Canvas, labelAr image.Rectangle) error {
    199  - start, err := alignfor.Text(labelAr, d.opts.label, d.opts.labelAlign, align.VerticalMiddle)
     191 + start, err := alignfor.Text(labelAr, d.opts.label, d.opts.labelAlign, align.VerticalBottom)
    200 192   if err != nil {
    201 193   return err
    202 194   }
    203  - if err := draw.Text(
     195 + return draw.Text(
    204 196   cvs, d.opts.label, start,
    205 197   draw.TextOverrunMode(draw.OverrunModeThreeDot),
    206 198   draw.TextMaxX(labelAr.Max.X),
    207 199   draw.TextCellOpts(d.opts.labelCellOpts...),
    208  - ); err != nil {
    209  - return err
    210  - }
    211  - return nil
     200 + )
    212 201  }
    213 202   
    214 203  // Draw draws the Donut widget onto the canvas.
    skipped 55 lines
    270 259   }
    271 260   
    272 261   if !d.opts.hideTextProgress {
    273  - if err := d.drawText(cvs, donutAr, mid, holeR); err != nil {
     262 + if err := d.drawText(cvs, mid, holeR); err != nil {
    274 263   return err
    275 264   }
    276 265   }
    skipped 33 lines
    310 299   }
    311 300  }
    312 301   
    313  -// donutAndLabel splits the canvas area into square area for the donut and an
     302 +// donutAndLabel splits the canvas area into an area for the donut and an
    314 303  // area under the donut for the text label.
    315 304  func donutAndLabel(cvsAr image.Rectangle) (donAr, labelAr image.Rectangle, err error) {
    316 305   height := cvsAr.Dy()
    317  - // One line for the text label at the bottom.
    318  - top, labelAr, err := area.HSplitCells(cvsAr, height-1)
    319  - if err != nil {
    320  - return image.ZR, image.ZR, err
    321  - }
    322  - 
    323  - // Remove one line from the top too so the donut area remains square.
    324  - // When using braille, this effectively removes 4 pixels from both the top
    325  - // and the bottom. See braille.RowMult.
    326  - donAr, err = area.Shrink(top, 1, 0, 0, 0)
     306 + // Two lines for the text label at the bottom.
     307 + // One for the text itself and one for visual space between the donut and
     308 + // the label.
     309 + donAr, labelAr, err = area.HSplitCells(cvsAr, height-2)
    327 310   if err != nil {
    328 311   return image.ZR, image.ZR, err
    329 312   }
    skipped 3 lines
  • ■ ■ ■ ■ ■ ■
    widgets/donut/donut_test.go
    skipped 337 lines
    338 338   c := testcanvas.MustNew(ft.Area())
    339 339   bc := testbraille.MustNew(ft.Area())
    340 340   
    341  - testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 5, draw.BrailleCircleFilled())
    342  - testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 3,
     341 + testdraw.MustBrailleCircle(bc, image.Point{6, 9}, 5, draw.BrailleCircleFilled())
     342 + testdraw.MustBrailleCircle(bc, image.Point{6, 9}, 3,
    343 343   draw.BrailleCircleFilled(),
    344 344   draw.BrailleCircleClearPixels(),
    345 345   )
    skipped 296 lines
    642 642   c := testcanvas.MustNew(ft.Area())
    643 643   bc := testbraille.MustNew(c.Area())
    644 644   
    645  - testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 6, draw.BrailleCircleFilled())
    646  - testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 5,
     645 + testdraw.MustBrailleCircle(bc, image.Point{6, 9}, 6, draw.BrailleCircleFilled())
     646 + testdraw.MustBrailleCircle(bc, image.Point{6, 9}, 5,
    647 647   draw.BrailleCircleFilled(),
    648 648   draw.BrailleCircleClearPixels(),
    649 649   )
    650 650   testbraille.MustCopyTo(bc, c)
    651 651   
    652  - testdraw.MustText(c, "100%", image.Point{2, 3})
     652 + testdraw.MustText(c, "100%", image.Point{2, 2})
    653 653   
    654 654   testdraw.MustText(c, "hi", image.Point{2, 6})
    655 655   
    skipped 16 lines
    672 672   c := testcanvas.MustNew(ft.Area())
    673 673   bc := testbraille.MustNew(c.Area())
    674 674   
    675  - testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 6, draw.BrailleCircleFilled())
    676  - testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 5,
     675 + testdraw.MustBrailleCircle(bc, image.Point{6, 9}, 6, draw.BrailleCircleFilled())
     676 + testdraw.MustBrailleCircle(bc, image.Point{6, 9}, 5,
    677 677   draw.BrailleCircleFilled(),
    678 678   draw.BrailleCircleClearPixels(),
    679 679   )
    680 680   testbraille.MustCopyTo(bc, c)
    681 681   
    682  - testdraw.MustText(c, "100%", image.Point{2, 3})
     682 + testdraw.MustText(c, "100%", image.Point{2, 2})
    683 683   
    684 684   testdraw.MustText(c, "hi", image.Point{2, 6})
    685 685   
    skipped 16 lines
    702 702   c := testcanvas.MustNew(ft.Area())
    703 703   bc := testbraille.MustNew(c.Area())
    704 704   
    705  - testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 6, draw.BrailleCircleFilled())
    706  - testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 5,
     705 + testdraw.MustBrailleCircle(bc, image.Point{6, 9}, 6, draw.BrailleCircleFilled())
     706 + testdraw.MustBrailleCircle(bc, image.Point{6, 9}, 5,
    707 707   draw.BrailleCircleFilled(),
    708 708   draw.BrailleCircleClearPixels(),
    709 709   )
    710 710   testbraille.MustCopyTo(bc, c)
    711 711   
    712  - testdraw.MustText(c, "100%", image.Point{2, 3})
     712 + testdraw.MustText(c, "100%", image.Point{2, 2})
    713 713   
    714 714   testdraw.MustText(c, "hi", image.Point{0, 6})
    715 715   
    skipped 16 lines
    732 732   c := testcanvas.MustNew(ft.Area())
    733 733   bc := testbraille.MustNew(c.Area())
    734 734   
    735  - testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 6, draw.BrailleCircleFilled())
    736  - testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 5,
     735 + testdraw.MustBrailleCircle(bc, image.Point{6, 9}, 6, draw.BrailleCircleFilled())
     736 + testdraw.MustBrailleCircle(bc, image.Point{6, 9}, 5,
    737 737   draw.BrailleCircleFilled(),
    738 738   draw.BrailleCircleClearPixels(),
    739 739   )
    740 740   testbraille.MustCopyTo(bc, c)
    741 741   
    742  - testdraw.MustText(c, "100%", image.Point{2, 3})
     742 + testdraw.MustText(c, "100%", image.Point{2, 2})
    743 743   
    744 744   testdraw.MustText(c, "hi", image.Point{5, 6})
    745 745   
    skipped 19 lines
    765 765   c := testcanvas.MustNew(ft.Area())
    766 766   bc := testbraille.MustNew(c.Area())
    767 767   
    768  - testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 6, draw.BrailleCircleFilled())
    769  - testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 5,
     768 + testdraw.MustBrailleCircle(bc, image.Point{6, 9}, 6, draw.BrailleCircleFilled())
     769 + testdraw.MustBrailleCircle(bc, image.Point{6, 9}, 5,
    770 770   draw.BrailleCircleFilled(),
    771 771   draw.BrailleCircleClearPixels(),
    772 772   )
    773 773   testbraille.MustCopyTo(bc, c)
    774 774   
    775  - testdraw.MustText(c, "100%", image.Point{2, 3})
     775 + testdraw.MustText(c, "100%", image.Point{2, 2})
    776 776   
    777 777   testdraw.MustText(
    778 778   c,
    skipped 25 lines
    804 804   c := testcanvas.MustNew(ft.Area())
    805 805   bc := testbraille.MustNew(c.Area())
    806 806   
    807  - testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 6, draw.BrailleCircleFilled())
    808  - testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 5,
     807 + testdraw.MustBrailleCircle(bc, image.Point{6, 9}, 6, draw.BrailleCircleFilled())
     808 + testdraw.MustBrailleCircle(bc, image.Point{6, 9}, 5,
    809 809   draw.BrailleCircleFilled(),
    810 810   draw.BrailleCircleClearPixels(),
    811 811   )
    812 812   testbraille.MustCopyTo(bc, c)
    813 813   
    814  - testdraw.MustText(c, "100%", image.Point{2, 3})
     814 + testdraw.MustText(c, "100%", image.Point{2, 2})
    815 815   
    816 816   testdraw.MustText(c, "hello …", image.Point{0, 6})
    817 817   
    skipped 102 lines
  • ■ ■ ■ ■ ■
    widgets/linechart/internal/axes/axes.go
    skipped 17 lines
    18 18  import (
    19 19   "fmt"
    20 20   "image"
     21 + 
     22 + "github.com/mum4k/termdash/internal/runewidth"
    21 23  )
    22 24   
    23 25  const (
    skipped 48 lines
    72 74   ReqXHeight int
    73 75   // ScaleMode determines how the Y axis scales.
    74 76   ScaleMode YScaleMode
     77 + // ValueFormatter is the formatter used to format numeric values to string representation.
     78 + ValueFormatter func(float64) string
    75 79  }
    76 80   
    77 81  // NewYDetails retrieves details about the Y axis required to draw it on a
    skipped 7 lines
    85 89   }
    86 90   
    87 91   graphHeight := cvsHeight - yp.ReqXHeight
    88  - scale, err := NewYScale(yp.Min, yp.Max, graphHeight, nonZeroDecimals, yp.ScaleMode)
     92 + scale, err := NewYScale(yp.Min, yp.Max, graphHeight, nonZeroDecimals, yp.ScaleMode, yp.ValueFormatter)
    89 93   if err != nil {
    90 94   return nil, err
    91 95   }
    skipped 34 lines
    126 130  func longestLabel(labels []*Label) int {
    127 131   var widest int
    128 132   for _, label := range labels {
    129  - if l := len(label.Value.Text()); l > widest {
     133 + if l := runewidth.StringWidth(label.Value.Text()); l > widest {
    130 134   widest = l
    131 135   }
    132 136   }
    skipped 105 lines
  • ■ ■ ■ ■ ■
    widgets/linechart/internal/axes/axes_test.go
    skipped 21 lines
    22 22   "github.com/kylelemons/godebug/pretty"
    23 23  )
    24 24   
     25 +var (
     26 + testValueFormatter = func(float64) string { return "test" }
     27 +)
     28 + 
    25 29  type updateY struct {
    26 30   minVal float64
    27 31   maxVal float64
    skipped 54 lines
    82 86   Width: 2,
    83 87   Start: image.Point{1, 0},
    84 88   End: image.Point{1, 2},
    85  - Scale: mustNewYScale(0, 3, 2, nonZeroDecimals, YScaleModeAnchored),
     89 + Scale: mustNewYScale(0, 3, 2, nonZeroDecimals, YScaleModeAnchored, nil),
    86 90   Labels: []*Label{
    87 91   {NewValue(0, nonZeroDecimals), image.Point{0, 1}},
    88 92   {NewValue(1.72, nonZeroDecimals), image.Point{0, 0}},
    skipped 14 lines
    103 107   Width: 2,
    104 108   Start: image.Point{1, 0},
    105 109   End: image.Point{1, 2},
    106  - Scale: mustNewYScale(0, 3, 2, nonZeroDecimals, YScaleModeAnchored),
     110 + Scale: mustNewYScale(0, 3, 2, nonZeroDecimals, YScaleModeAnchored, nil),
    107 111   Labels: []*Label{
    108 112   {NewValue(0, nonZeroDecimals), image.Point{0, 1}},
    109 113   {NewValue(1.72, nonZeroDecimals), image.Point{0, 0}},
    skipped 14 lines
    124 128   Width: 2,
    125 129   Start: image.Point{1, 0},
    126 130   End: image.Point{1, 2},
    127  - Scale: mustNewYScale(0, 3, 2, nonZeroDecimals, YScaleModeAnchored),
     131 + Scale: mustNewYScale(0, 3, 2, nonZeroDecimals, YScaleModeAnchored, nil),
    128 132   Labels: []*Label{
    129 133   {NewValue(0, nonZeroDecimals), image.Point{0, 1}},
    130 134   {NewValue(1.72, nonZeroDecimals), image.Point{0, 0}},
    skipped 14 lines
    145 149   Width: 2,
    146 150   Start: image.Point{1, 0},
    147 151   End: image.Point{1, 2},
    148  - Scale: mustNewYScale(1, 6, 2, nonZeroDecimals, YScaleModeAdaptive),
     152 + Scale: mustNewYScale(1, 6, 2, nonZeroDecimals, YScaleModeAdaptive, nil),
    149 153   Labels: []*Label{
    150 154   {NewValue(1, nonZeroDecimals), image.Point{0, 1}},
    151 155   {NewValue(3.88, nonZeroDecimals), image.Point{0, 0}},
    skipped 13 lines
    165 169   Width: 5,
    166 170   Start: image.Point{4, 0},
    167 171   End: image.Point{4, 2},
    168  - Scale: mustNewYScale(0, 3, 2, nonZeroDecimals, YScaleModeAnchored),
     172 + Scale: mustNewYScale(0, 3, 2, nonZeroDecimals, YScaleModeAnchored, nil),
    169 173   Labels: []*Label{
    170 174   {NewValue(0, nonZeroDecimals), image.Point{3, 1}},
    171 175   {NewValue(1.72, nonZeroDecimals), image.Point{0, 0}},
    skipped 13 lines
    185 189   Width: 5,
    186 190   Start: image.Point{4, 0},
    187 191   End: image.Point{4, 2},
    188  - Scale: mustNewYScale(0, 3, 2, nonZeroDecimals, YScaleModeAnchored),
     192 + Scale: mustNewYScale(0, 3, 2, nonZeroDecimals, YScaleModeAnchored, nil),
    189 193   Labels: []*Label{
    190 194   {NewValue(0, nonZeroDecimals), image.Point{3, 1}},
    191 195   {NewValue(1.72, nonZeroDecimals), image.Point{0, 0}},
     196 + },
     197 + },
     198 + },
     199 + {
     200 + desc: "success for formatted labels scale",
     201 + yp: &YProperties{
     202 + Min: 1,
     203 + Max: 3,
     204 + ReqXHeight: 2,
     205 + ScaleMode: YScaleModeAnchored,
     206 + ValueFormatter: testValueFormatter,
     207 + },
     208 + cvsAr: image.Rect(0, 0, 3, 4),
     209 + wantWidth: 2,
     210 + want: &YDetails{
     211 + Width: 2,
     212 + Start: image.Point{1, 0},
     213 + End: image.Point{1, 2},
     214 + Scale: mustNewYScale(0, 3, 2, nonZeroDecimals, YScaleModeAnchored, testValueFormatter),
     215 + Labels: []*Label{
     216 + {NewValue(0, nonZeroDecimals, ValueFormatter(testValueFormatter)), image.Point{0, 1}},
     217 + {NewValue(1.72, nonZeroDecimals, ValueFormatter(testValueFormatter)), image.Point{0, 0}},
    192 218   },
    193 219   },
    194 220   },
    skipped 309 lines
  • ■ ■ ■ ■
    widgets/linechart/internal/axes/label.go
    skipped 70 lines
    71 71   if min := 2; scale.GraphHeight < min {
    72 72   return nil, fmt.Errorf("cannot place labels on a canvas with height %d, minimum is %d", scale.GraphHeight, min)
    73 73   }
    74  - if min := 1; labelWidth < min {
     74 + if min := 0; labelWidth < min {
    75 75   return nil, fmt.Errorf("cannot place labels in label area width %d, minimum is %d", labelWidth, min)
    76 76   }
    77 77   
    skipped 190 lines
  • ■ ■ ■ ■ ■ ■
    widgets/linechart/internal/axes/label_test.go
    skipped 44 lines
    45 45   min: 0,
    46 46   max: 1,
    47 47   graphHeight: 2,
    48  - labelWidth: 0,
     48 + labelWidth: -1,
    49 49   wantErr: true,
    50 50   },
    51 51   {
    skipped 77 lines
    129 129   
    130 130   for _, tc := range tests {
    131 131   t.Run(tc.desc, func(t *testing.T) {
    132  - scale, err := NewYScale(tc.min, tc.max, tc.graphHeight, nonZeroDecimals, YScaleModeAnchored)
     132 + scale, err := NewYScale(tc.min, tc.max, tc.graphHeight, nonZeroDecimals, YScaleModeAnchored, nil)
    133 133   if err != nil {
    134 134   t.Fatalf("NewYScale => unexpected error: %v", err)
    135 135   }
    skipped 398 lines
  • ■ ■ ■ ■ ■
    widgets/linechart/internal/axes/scale.go
    skipped 65 lines
    66 66   GraphHeight int
    67 67   // brailleHeight is the height of the braille canvas based on the GraphHeight.
    68 68   brailleHeight int
     69 + 
     70 + // valueFormatter is the value formatter used for the labels
     71 + // represented by the values on the scale.
     72 + valueFormatter func(float64) string
    69 73  }
    70 74   
    71 75  // String implements fmt.Stringer.
    skipped 6 lines
    78 82  // calculated scale, see NewValue for details.
    79 83  // Max must be greater or equal to min. The graphHeight must be a positive
    80 84  // number.
    81  -func NewYScale(min, max float64, graphHeight, nonZeroDecimals int, mode YScaleMode) (*YScale, error) {
     85 +func NewYScale(min, max float64, graphHeight, nonZeroDecimals int, mode YScaleMode, valueFormatter func(float64) string) (*YScale, error) {
    82 86   if max < min {
    83 87   return nil, fmt.Errorf("max(%v) cannot be less than min(%v)", max, min)
    84 88   }
    skipped 29 lines
    114 118   diff := max - min
    115 119   step := NewValue(diff/float64(usablePixels), nonZeroDecimals)
    116 120   return &YScale{
    117  - Min: NewValue(min, nonZeroDecimals),
    118  - Max: NewValue(max, nonZeroDecimals),
    119  - Step: step,
    120  - GraphHeight: graphHeight,
    121  - brailleHeight: brailleHeight,
     121 + Min: yScaleNewValue(min, nonZeroDecimals, valueFormatter),
     122 + Max: yScaleNewValue(max, nonZeroDecimals, valueFormatter),
     123 + Step: step,
     124 + GraphHeight: graphHeight,
     125 + brailleHeight: brailleHeight,
     126 + valueFormatter: valueFormatter,
    122 127   }, nil
    123 128  }
    124 129   
    skipped 63 lines
    188 193   if err != nil {
    189 194   return nil, err
    190 195   }
    191  - return NewValue(v, ys.Min.NonZeroDecimals), nil
     196 + return yScaleNewValue(v, ys.Min.NonZeroDecimals, ys.valueFormatter), nil
     197 +}
     198 + 
     199 +// yScaleNewValue is a helper method to get new values for the y scale.
     200 +func yScaleNewValue(value float64, nonZeroDecimals int, valueFormatter func(float64) string) *Value {
     201 + opts := []ValueOption{}
     202 + if valueFormatter != nil {
     203 + opts = append(opts, ValueFormatter(valueFormatter))
     204 + }
     205 + 
     206 + return NewValue(value, nonZeroDecimals, opts...)
    192 207  }
    193 208   
    194 209  // XScale is the scale of the X axis.
    skipped 141 lines
  • ■ ■ ■ ■ ■ ■
    widgets/linechart/internal/axes/scale_test.go
    skipped 21 lines
    22 22  )
    23 23   
    24 24  // mustNewYScale returns a new YScale or panics.
    25  -func mustNewYScale(min, max float64, graphHeight, nonZeroDecimals int, mode YScaleMode) *YScale {
    26  - s, err := NewYScale(min, max, graphHeight, nonZeroDecimals, mode)
     25 +func mustNewYScale(min, max float64, graphHeight, nonZeroDecimals int, mode YScaleMode, valueFormatter func(float64) string) *YScale {
     26 + s, err := NewYScale(min, max, graphHeight, nonZeroDecimals, mode, valueFormatter)
    27 27   if err != nil {
    28 28   panic(err)
    29 29   }
    skipped 736 lines
    766 766   }
    767 767   
    768 768   for _, test := range tests {
    769  - scale, err := NewYScale(test.min, test.max, test.graphHeight, test.nonZeroDecimals, test.mode)
     769 + scale, err := NewYScale(test.min, test.max, test.graphHeight, test.nonZeroDecimals, test.mode, nil)
    770 770   if (err != nil) != test.wantErr {
    771 771   t.Errorf("NewYScale => unexpected error: %v, wantErr: %v", err, test.wantErr)
    772 772   }
    skipped 437 lines
  • ■ ■ ■ ■ ■
    widgets/linechart/internal/axes/value.go
    skipped 22 lines
    23 23   "github.com/mum4k/termdash/internal/numbers"
    24 24  )
    25 25   
     26 +// ValueOption is used to provide options to the NewValue function.
     27 +type ValueOption interface {
     28 + // set sets the provided option.
     29 + set(*valueOptions)
     30 +}
     31 + 
     32 +type valueOptions struct {
     33 + formatter func(v float64) string
     34 +}
     35 + 
     36 +// valueOption implements ValueOption.
     37 +type valueOption func(opts *valueOptions)
     38 + 
     39 +// set implements ValueOption.set.
     40 +func (vo valueOption) set(opts *valueOptions) {
     41 + vo(opts)
     42 +}
     43 + 
     44 +// ValueFormatter sets a custom formatter for the value.
     45 +func ValueFormatter(formatter func(float64) string) ValueOption {
     46 + return valueOption(func(opts *valueOptions) {
     47 + opts.formatter = formatter
     48 + })
     49 +}
     50 + 
    26 51  // Value represents one value.
    27 52  type Value struct {
    28 53   // Value is the original unmodified value.
    skipped 8 lines
    37 62   // a call to newValue.
    38 63   NonZeroDecimals int
    39 64   
     65 + // formatter will format value to a string representation of the value,
     66 + // if Formatter is not present it will fallback to default format.
     67 + formatter func(float64) string
    40 68   // text value if this value was constructed using NewTextValue.
    41 69   text string
    42 70  }
    skipped 5 lines
    48 76   
    49 77  // NewValue returns a new instance representing the provided value, rounding
    50 78  // the value up to the specified number of non-zero decimal places.
    51  -func NewValue(v float64, nonZeroDecimals int) *Value {
     79 +func NewValue(v float64, nonZeroDecimals int, opts ...ValueOption) *Value {
     80 + opt := &valueOptions{}
     81 + for _, o := range opts {
     82 + o.set(opt)
     83 + }
     84 + 
    52 85   r, zd := numbers.RoundToNonZeroPlaces(v, nonZeroDecimals)
    53 86   return &Value{
    54 87   Value: v,
    55 88   Rounded: r,
    56 89   ZeroDecimals: zd,
    57 90   NonZeroDecimals: nonZeroDecimals,
     91 + formatter: opt.formatter,
    58 92   }
    59 93  }
    60 94   
    skipped 11 lines
    72 106   if v.text != "" {
    73 107   return v.text
    74 108   }
    75  - if math.Ceil(v.Rounded) == v.Rounded {
    76  - return fmt.Sprintf("%.0f", v.Rounded)
     109 + 
     110 + if v.formatter != nil {
     111 + return v.formatter(v.Value)
    77 112   }
    78 113   
    79  - format := fmt.Sprintf("%%.%df", v.NonZeroDecimals+v.ZeroDecimals)
    80  - t := fmt.Sprintf(format, v.Rounded)
     114 + return defaultFormatter(v.Rounded, v.NonZeroDecimals, v.ZeroDecimals)
     115 +}
     116 + 
     117 +func defaultFormatter(value float64, nonZeroDecimals, zeroDecimals int) string {
     118 + if math.Ceil(value) == value {
     119 + return fmt.Sprintf("%.0f", value)
     120 + }
     121 + 
     122 + format := fmt.Sprintf("%%.%df", nonZeroDecimals+zeroDecimals)
     123 + t := fmt.Sprintf(format, value)
    81 124   if len(t) > 10 {
    82  - t = fmt.Sprintf("%.2e", v.Rounded)
     125 + t = fmt.Sprintf("%.2e", value)
    83 126   }
     127 + 
    84 128   return t
    85 129  }
    86 130   
  • ■ ■ ■ ■ ■ ■
    widgets/linechart/internal/axes/value_test.go
    skipped 21 lines
    22 22  )
    23 23   
    24 24  func TestValue(t *testing.T) {
     25 + formatter := func(float64) string { return "test" }
     26 + 
    25 27   tests := []struct {
    26 28   desc string
    27 29   float float64
    28 30   nonZeroDecimals int
     31 + formatter func(float64) string
    29 32   want *Value
    30 33   }{
    31 34   {
    skipped 29 lines
    61 64   NonZeroDecimals: 0,
    62 65   },
    63 66   },
     67 + {
     68 + desc: "formatter value when value formatter as option",
     69 + float: 1.01234,
     70 + nonZeroDecimals: 0,
     71 + formatter: formatter,
     72 + want: &Value{
     73 + Value: 1.01234,
     74 + Rounded: 1.01234,
     75 + ZeroDecimals: 1,
     76 + NonZeroDecimals: 0,
     77 + formatter: formatter,
     78 + },
     79 + },
    64 80   }
    65 81   
    66 82   for _, tc := range tests {
    67 83   t.Run(tc.desc, func(t *testing.T) {
    68  - got := NewValue(tc.float, tc.nonZeroDecimals)
     84 + got := NewValue(tc.float, tc.nonZeroDecimals, ValueFormatter(tc.formatter))
    69 85   if diff := pretty.Compare(tc.want, got); diff != "" {
    70 86   t.Errorf("NewValue => unexpected diff (-want, +got):\n%s", diff)
    71 87   }
    skipped 56 lines
  • ■ ■ ■ ■ ■ ■
    widgets/linechart/linechart.go
    skipped 274 lines
    275 275  func (lc *LineChart) axesDetails(cvs *canvas.Canvas) (*axes.XDetails, *axes.YDetails, error) {
    276 276   reqXHeight := axes.RequiredHeight(lc.maxXValue(), lc.xLabels, lc.opts.xLabelOrientation)
    277 277   yp := &axes.YProperties{
    278  - Min: lc.yMin,
    279  - Max: lc.yMax,
    280  - ReqXHeight: reqXHeight,
    281  - ScaleMode: lc.opts.yAxisMode,
     278 + Min: lc.yMin,
     279 + Max: lc.yMax,
     280 + ReqXHeight: reqXHeight,
     281 + ScaleMode: lc.opts.yAxisMode,
     282 + ValueFormatter: lc.opts.yAxisValueFormatter,
    282 283   }
    283 284   yd, err := axes.NewYDetails(cvs.Area(), yp)
    284 285   if err != nil {
    skipped 264 lines
  • ■ ■ ■ ■ ■ ■
    widgets/linechart/linechart_test.go
    skipped 14 lines
    15 15  package linechart
    16 16   
    17 17  import (
     18 + "fmt"
    18 19   "image"
    19 20   "math"
    20 21   "testing"
    skipped 1613 lines
    1634 1635   testdraw.MustBrailleLine(bc, image.Point{9, 5}, image.Point{10, 4})
    1635 1636   testdraw.MustBrailleLine(bc, image.Point{10, 4}, image.Point{10, 2})
    1636 1637   testdraw.MustBrailleLine(bc, image.Point{10, 2}, image.Point{11, 0})
     1638 + testbraille.MustCopyTo(bc, c)
     1639 + 
     1640 + testcanvas.MustApply(c, ft)
     1641 + return ft
     1642 + },
     1643 + },
     1644 + {
     1645 + desc: "custom Y-axis labels using a value formatter",
     1646 + canvas: image.Rect(0, 0, 20, 10),
     1647 + opts: []Option{
     1648 + YAxisFormattedValues(func(v float64) string {
     1649 + if v == 0 || math.IsNaN(v) {
     1650 + return "∅"
     1651 + }
     1652 + return fmt.Sprintf("%.1fs", v+10)
     1653 + }),
     1654 + },
     1655 + writes: func(lc *LineChart) error {
     1656 + return lc.Series("first", []float64{0, 100})
     1657 + },
     1658 + wantCapacity: 28,
     1659 + want: func(size image.Point) *faketerm.Terminal {
     1660 + ft := faketerm.MustNew(size)
     1661 + c := testcanvas.MustNew(ft.Area())
     1662 + 
     1663 + // Y and X axis.
     1664 + lines := []draw.HVLine{
     1665 + {Start: image.Point{5, 0}, End: image.Point{5, 8}},
     1666 + {Start: image.Point{5, 8}, End: image.Point{19, 8}},
     1667 + }
     1668 + testdraw.MustHVLines(c, lines)
     1669 + 
     1670 + // Value labels.
     1671 + testdraw.MustText(c, "∅", image.Point{4, 7})
     1672 + testdraw.MustText(c, "61.7s", image.Point{0, 3})
     1673 + testdraw.MustText(c, "0", image.Point{6, 9})
     1674 + testdraw.MustText(c, "1", image.Point{19, 9})
     1675 + 
     1676 + // Braille line.
     1677 + graphAr := image.Rect(6, 0, 20, 8)
     1678 + bc := testbraille.MustNew(graphAr)
     1679 + testdraw.MustBrailleLine(bc, image.Point{0, 31}, image.Point{26, 0})
     1680 + testbraille.MustCopyTo(bc, c)
     1681 + 
     1682 + testcanvas.MustApply(c, ft)
     1683 + return ft
     1684 + },
     1685 + },
     1686 + {
     1687 + desc: "custom Y-axis labels using a value formatter that returns empty labels",
     1688 + canvas: image.Rect(0, 0, 20, 10),
     1689 + opts: []Option{
     1690 + YAxisFormattedValues(func(v float64) string { return "" }),
     1691 + },
     1692 + writes: func(lc *LineChart) error {
     1693 + return lc.Series("first", []float64{0, 100})
     1694 + },
     1695 + wantCapacity: 38,
     1696 + want: func(size image.Point) *faketerm.Terminal {
     1697 + ft := faketerm.MustNew(size)
     1698 + c := testcanvas.MustNew(ft.Area())
     1699 + 
     1700 + // Y and X axis.
     1701 + lines := []draw.HVLine{
     1702 + {Start: image.Point{0, 0}, End: image.Point{0, 8}},
     1703 + {Start: image.Point{0, 8}, End: image.Point{19, 8}},
     1704 + }
     1705 + testdraw.MustHVLines(c, lines)
     1706 + 
     1707 + // Value labels.
     1708 + testdraw.MustText(c, "0", image.Point{1, 9})
     1709 + testdraw.MustText(c, "1", image.Point{19, 9})
     1710 + 
     1711 + // Braille line.
     1712 + graphAr := image.Rect(1, 0, 20, 8)
     1713 + bc := testbraille.MustNew(graphAr)
     1714 + testdraw.MustBrailleLine(bc, image.Point{0, 31}, image.Point{36, 0})
     1715 + testbraille.MustCopyTo(bc, c)
     1716 + 
     1717 + testcanvas.MustApply(c, ft)
     1718 + return ft
     1719 + },
     1720 + },
     1721 + {
     1722 + desc: "custom Y-axis labels using a value formatter that returns very long strings",
     1723 + canvas: image.Rect(0, 0, 20, 10),
     1724 + opts: []Option{
     1725 + YAxisFormattedValues(func(v float64) string {
     1726 + if v == 0 || math.IsNaN(v) {
     1727 + return "0"
     1728 + }
     1729 + return fmt.Sprintf("%.20f", v)
     1730 + }),
     1731 + },
     1732 + writes: func(lc *LineChart) error {
     1733 + return lc.Series("first", []float64{0, 100})
     1734 + },
     1735 + wantCapacity: 2,
     1736 + want: func(size image.Point) *faketerm.Terminal {
     1737 + ft := faketerm.MustNew(size)
     1738 + c := testcanvas.MustNew(ft.Area())
     1739 + 
     1740 + // Y and X axis.
     1741 + lines := []draw.HVLine{
     1742 + {Start: image.Point{18, 0}, End: image.Point{18, 8}},
     1743 + {Start: image.Point{18, 8}, End: image.Point{19, 8}},
     1744 + }
     1745 + testdraw.MustHVLines(c, lines)
     1746 + 
     1747 + // Value labels.
     1748 + testdraw.MustText(c, "0", image.Point{17, 7})
     1749 + testdraw.MustText(c, "51.67999999999999…", image.Point{0, 3})
     1750 + testdraw.MustText(c, "0", image.Point{19, 9})
     1751 + 
     1752 + // Braille line.
     1753 + graphAr := image.Rect(19, 0, 20, 8)
     1754 + bc := testbraille.MustNew(graphAr)
     1755 + testdraw.MustBrailleLine(bc, image.Point{0, 31}, image.Point{1, 0})
     1756 + testbraille.MustCopyTo(bc, c)
     1757 + 
     1758 + testcanvas.MustApply(c, ft)
     1759 + return ft
     1760 + },
     1761 + },
     1762 + {
     1763 + desc: "custom Y-axis labels using a value formatter with non printable strings (\\n)",
     1764 + canvas: image.Rect(0, 0, 20, 10),
     1765 + opts: []Option{
     1766 + YAxisFormattedValues(func(v float64) string { return "\n" }),
     1767 + },
     1768 + writes: func(lc *LineChart) error {
     1769 + return lc.Series("first", []float64{0, 100})
     1770 + },
     1771 + wantDrawErr: true,
     1772 + },
     1773 + {
     1774 + desc: "custom Y-axis labels using a value formatter with non printable strings (\\t)",
     1775 + canvas: image.Rect(0, 0, 20, 10),
     1776 + opts: []Option{
     1777 + YAxisFormattedValues(func(v float64) string { return "\ta" }),
     1778 + },
     1779 + writes: func(lc *LineChart) error {
     1780 + return lc.Series("first", []float64{0, 100})
     1781 + },
     1782 + wantDrawErr: true,
     1783 + },
     1784 + {
     1785 + desc: "custom Y-axis labels using a value formatter with non printable strings (control characters)",
     1786 + canvas: image.Rect(0, 0, 20, 10),
     1787 + opts: []Option{
     1788 + YAxisFormattedValues(func(v float64) string { return fmt.Sprintf("%ca", 0x007f) }),
     1789 + },
     1790 + writes: func(lc *LineChart) error {
     1791 + return lc.Series("first", []float64{0, 100})
     1792 + },
     1793 + wantDrawErr: true,
     1794 + },
     1795 + {
     1796 + desc: "custom Y-axis labels using a value formatter that returns unicode strings",
     1797 + canvas: image.Rect(0, 0, 20, 10),
     1798 + opts: []Option{
     1799 + YAxisFormattedValues(func(v float64) string {
     1800 + if v == 0 {
     1801 + return "abc"
     1802 + }
     1803 + return "世世世世"
     1804 + }),
     1805 + },
     1806 + writes: func(lc *LineChart) error {
     1807 + return lc.Series("first", []float64{0, 100})
     1808 + },
     1809 + wantCapacity: 22,
     1810 + want: func(size image.Point) *faketerm.Terminal {
     1811 + ft := faketerm.MustNew(size)
     1812 + c := testcanvas.MustNew(ft.Area())
     1813 + 
     1814 + // Y and X axis.
     1815 + lines := []draw.HVLine{
     1816 + {Start: image.Point{8, 0}, End: image.Point{8, 8}},
     1817 + {Start: image.Point{8, 8}, End: image.Point{19, 8}},
     1818 + }
     1819 + testdraw.MustHVLines(c, lines)
     1820 + 
     1821 + // Value labels.
     1822 + testdraw.MustText(c, "abc", image.Point{5, 7})
     1823 + testdraw.MustText(c, "世世世世", image.Point{0, 3})
     1824 + testdraw.MustText(c, "0", image.Point{9, 9})
     1825 + testdraw.MustText(c, "1", image.Point{19, 9})
     1826 + 
     1827 + // Braille line.
     1828 + graphAr := image.Rect(9, 0, 20, 8)
     1829 + bc := testbraille.MustNew(graphAr)
     1830 + testdraw.MustBrailleLine(bc, image.Point{0, 31}, image.Point{21, 0})
    1637 1831   testbraille.MustCopyTo(bc, c)
    1638 1832   
    1639 1833   testcanvas.MustApply(c, ft)
    skipped 168 lines
  • ■ ■ ■ ■ ■ ■
    widgets/linechart/options.go
    skipped 39 lines
    40 40   xAxisUnscaled bool
    41 41   yAxisMode axes.YScaleMode
    42 42   yAxisCustomScale *customScale
     43 + yAxisValueFormatter ValueFormatter
    43 44   zoomHightlightColor cell.Color
    44 45   zoomStepPercent int
    45 46  }
    skipped 149 lines
    195 196   })
    196 197  }
    197 198   
     199 +// YAxisFormattedValues sets a value formatter for the Y axis values.
     200 +// If a formatter is set, it will format the values with the desired
     201 +// ValueFormatter and will use the retuning string from the formatter
     202 +// instead of the numeric value to represent this value on the Y axis.
     203 +func YAxisFormattedValues(vfmt ValueFormatter) Option {
     204 + return option(func(opts *options) {
     205 + opts.yAxisValueFormatter = vfmt
     206 + })
     207 +}
     208 + 
     209 +// ValueFormatter will be used to format values onto string based
     210 +// representation.
     211 +// The received float64 value could be a math.NaN value.
     212 +type ValueFormatter func(value float64) string
     213 + 
  • ■ ■ ■ ■ ■ ■
    widgets/linechart/value_formatter.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 linechart
     16 + 
     17 +// value_formatter.go provides common implementations of ValueFormatter that can be
     18 +// used with the YAxisFormattedValues() LineChart option.
     19 + 
     20 +import (
     21 + "fmt"
     22 + "math"
     23 + "strings"
     24 + "time"
     25 +)
     26 + 
     27 +// durationSingleUnitPrettyFormat returns the pretty format in one single
     28 +// unit for a time.Duration, the different returned unit formats
     29 +// are: nanoseconds, microseconds, milliseconds, seconds, minutes
     30 +// hours, days.
     31 +func durationSingleUnitPrettyFormat(d time.Duration, decimals int) string {
     32 + // Check if the duration is less than 0.
     33 + prefix := ""
     34 + if d < 0 {
     35 + prefix = "-"
     36 + d = time.Duration(math.Abs(d.Seconds()) * float64(time.Second))
     37 + }
     38 + 
     39 + switch {
     40 + // Nanoseconds.
     41 + case d.Nanoseconds() < 1000:
     42 + dFmt := prefix + "%dns"
     43 + return fmt.Sprintf(dFmt, d.Nanoseconds())
     44 + // Microseconds.
     45 + case d.Seconds()*1000*1000 < 1000:
     46 + dFmt := prefix + suffixDecimalFormat(decimals, "µs")
     47 + return fmt.Sprintf(dFmt, d.Seconds()*1000*1000)
     48 + // Milliseconds.
     49 + case d.Seconds()*1000 < 1000:
     50 + dFmt := prefix + suffixDecimalFormat(decimals, "ms")
     51 + return fmt.Sprintf(dFmt, d.Seconds()*1000)
     52 + // Seconds.
     53 + case d.Seconds() < 60:
     54 + dFmt := prefix + suffixDecimalFormat(decimals, "s")
     55 + return fmt.Sprintf(dFmt, d.Seconds())
     56 + // Minutes.
     57 + case d.Minutes() < 60:
     58 + dFmt := prefix + suffixDecimalFormat(decimals, "m")
     59 + return fmt.Sprintf(dFmt, d.Minutes())
     60 + // Hours.
     61 + case d.Hours() < 24:
     62 + dFmt := prefix + suffixDecimalFormat(decimals, "h")
     63 + return fmt.Sprintf(dFmt, d.Hours())
     64 + // Days.
     65 + default:
     66 + dFmt := prefix + suffixDecimalFormat(decimals, "d")
     67 + return fmt.Sprintf(dFmt, d.Hours()/24)
     68 + }
     69 +}
     70 + 
     71 +func suffixDecimalFormat(decimals int, suffix string) string {
     72 + suffix = strings.Replace(suffix, "%", "%%", -1) // Safe `%` character for fmt.
     73 + return fmt.Sprintf("%%.%df%s", decimals, suffix)
     74 +}
     75 + 
     76 +// ValueFormatterSingleUnitDuration is a factory to create a custom duration
     77 +// in a single unit representation formatter based on a unit and the decimals
     78 +// to truncate.
     79 +// If the received decimal value is negative it will fallback to a 0 decimal
     80 +// value.
     81 +// The result value formatter handles NaN values, if the value formatter
     82 +// receives a NaN float64 it will return an empty string.
     83 +func ValueFormatterSingleUnitDuration(unit time.Duration, decimals int) ValueFormatter {
     84 + if decimals < 0 {
     85 + decimals = 0
     86 + }
     87 + 
     88 + return func(v float64) string {
     89 + if math.IsNaN(v) {
     90 + return ""
     91 + }
     92 + 
     93 + d := time.Duration(v * float64(unit))
     94 + return durationSingleUnitPrettyFormat(d, decimals)
     95 + }
     96 +}
     97 + 
     98 +// ValueFormatterSingleUnitSeconds is a formatter that will receive
     99 +// seconds unit in the float64 argument and will return a pretty
     100 +// format in one single unit without decimals, it doesn't round,
     101 +// it truncates.
     102 +// Received seconds that are NaN will be ignored and return an
     103 +// empty string.
     104 +func ValueFormatterSingleUnitSeconds(seconds float64) string {
     105 + f := ValueFormatterSingleUnitDuration(time.Second, 0)
     106 + return f(seconds)
     107 +}
     108 + 
     109 +// ValueFormatterRound is a formatter that will receive a float64
     110 +// value and will round to the nearest value without decimals.
     111 +func ValueFormatterRound(value float64) string {
     112 + f := ValueFormatterRoundWithSuffix("")
     113 + return f(value)
     114 +}
     115 + 
     116 +// ValueFormatterRoundWithSuffix is a factory that returns a formatter
     117 +// that will receive a float64 value and will round to the nearest value
     118 +// without decimals adding a suffix to the final value string representation.
     119 +func ValueFormatterRoundWithSuffix(suffix string) ValueFormatter {
     120 + return valueFormatterSuffixWithTransformer(0, suffix, math.Round)
     121 +}
     122 + 
     123 +// ValueFormatterSuffix is a factory that returns a formatter
     124 +// that will receive a float64 value and return a string representation with
     125 +// the desired number of decimal truncated and a suffix.
     126 +func ValueFormatterSuffix(decimals int, suffix string) ValueFormatter {
     127 + return valueFormatterSuffixWithTransformer(decimals, suffix, nil)
     128 +}
     129 + 
     130 +// valueFormatterSuffixWithTransformer is a factory that returns a formatter
     131 +// that will apply a tranform function to the received value before
     132 +// returning the decimal with suffix representation.
     133 +func valueFormatterSuffixWithTransformer(decimals int, suffix string, transformFunc func(float64) float64) ValueFormatter {
     134 + dFmt := suffixDecimalFormat(decimals, suffix)
     135 + return func(value float64) string {
     136 + if math.IsNaN(value) {
     137 + return ""
     138 + }
     139 + 
     140 + if transformFunc != nil {
     141 + value = transformFunc(value)
     142 + }
     143 + 
     144 + return fmt.Sprintf(dFmt, value)
     145 + }
     146 +}
     147 + 
  • ■ ■ ■ ■ ■ ■
    widgets/linechart/value_formatter_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 linechart
     16 + 
     17 +import (
     18 + "math"
     19 + "testing"
     20 + "time"
     21 + 
     22 + "github.com/kylelemons/godebug/pretty"
     23 +)
     24 + 
     25 +func TestFormatters(t *testing.T) {
     26 + tests := []struct {
     27 + desc string
     28 + value float64
     29 + formatter ValueFormatter
     30 + want string
     31 + }{
     32 + {
     33 + desc: "Pretty duration formatter handles zero values",
     34 + value: 0,
     35 + formatter: ValueFormatterSingleUnitSeconds,
     36 + want: "0ns",
     37 + },
     38 + {
     39 + desc: "Pretty duration formatter handles minus minute values",
     40 + value: -1500,
     41 + formatter: ValueFormatterSingleUnitSeconds,
     42 + want: "-25m",
     43 + },
     44 + {
     45 + desc: "Pretty duration formatter handles minus minute values",
     46 + value: -60,
     47 + formatter: ValueFormatterSingleUnitSeconds,
     48 + want: "-1m",
     49 + },
     50 + {
     51 + desc: "Pretty duration formatter handles nanoseconds",
     52 + value: 1.23e-7,
     53 + formatter: ValueFormatterSingleUnitSeconds,
     54 + want: "123ns",
     55 + },
     56 + {
     57 + desc: "Pretty duration formatter handles microseconds",
     58 + value: 1.23e-4,
     59 + formatter: ValueFormatterSingleUnitSeconds,
     60 + want: "123µs",
     61 + },
     62 + {
     63 + desc: "Pretty duration formatter handles milliseconds",
     64 + value: 0.123,
     65 + formatter: ValueFormatterSingleUnitSeconds,
     66 + want: "123ms",
     67 + },
     68 + {
     69 + desc: "Pretty duration formatter handles seconds",
     70 + value: 12,
     71 + formatter: ValueFormatterSingleUnitSeconds,
     72 + want: "12s",
     73 + },
     74 + {
     75 + desc: "Pretty duration formatter handles minutes",
     76 + value: 60,
     77 + formatter: ValueFormatterSingleUnitSeconds,
     78 + want: "1m",
     79 + },
     80 + {
     81 + desc: "Pretty duration formatter handles hours",
     82 + value: 2 * 60 * 60,
     83 + formatter: ValueFormatterSingleUnitSeconds,
     84 + want: "2h",
     85 + },
     86 + {
     87 + desc: "Pretty duration formatter handles days",
     88 + value: 5 * 24 * 60 * 60,
     89 + formatter: ValueFormatterSingleUnitSeconds,
     90 + want: "5d",
     91 + },
     92 + {
     93 + desc: "Pretty minus duration formatter handles days",
     94 + value: -5 * 24 * 60 * 60,
     95 + formatter: ValueFormatterSingleUnitSeconds,
     96 + want: "-5d",
     97 + },
     98 + {
     99 + desc: "Pretty custom minute formatter with decimals handles days",
     100 + value: 135,
     101 + formatter: ValueFormatterSingleUnitDuration(time.Minute, 2),
     102 + want: "2.25h",
     103 + },
     104 + {
     105 + desc: "Pretty custom millisecond formatter with decimals handles minutes",
     106 + value: 2525789,
     107 + formatter: ValueFormatterSingleUnitDuration(time.Millisecond, 4),
     108 + want: "42.0965m",
     109 + },
     110 + {
     111 + desc: "Pretty custom nanosecond formatter with decimals handles days",
     112 + value: 999999999999999,
     113 + formatter: ValueFormatterSingleUnitDuration(time.Nanosecond, 8),
     114 + want: "11.57407407d",
     115 + },
     116 + {
     117 + desc: "Pretty custom minus nanosecond formatter with decimals handles days",
     118 + value: -999999999999999,
     119 + formatter: ValueFormatterSingleUnitDuration(time.Nanosecond, 8),
     120 + want: "-11.57407407d",
     121 + },
     122 + {
     123 + desc: "Pretty custom minus nanosecond formatter without decimals handles microseconds",
     124 + value: -1500,
     125 + formatter: ValueFormatterSingleUnitDuration(time.Nanosecond, 1),
     126 + want: "-1.5µs",
     127 + },
     128 + {
     129 + desc: "Pretty custom millisecond formatter with negative decimals handles minutes",
     130 + value: 2525789,
     131 + formatter: ValueFormatterSingleUnitDuration(time.Millisecond, -4),
     132 + want: "42m",
     133 + },
     134 + {
     135 + desc: "Pretty Second duration formatter handles NaN values",
     136 + value: math.NaN(),
     137 + formatter: ValueFormatterSingleUnitSeconds,
     138 + want: "",
     139 + },
     140 + {
     141 + desc: "Pretty custom duration formatter handles NaN values",
     142 + value: math.NaN(),
     143 + formatter: ValueFormatterSingleUnitDuration(time.Nanosecond, 8),
     144 + want: "",
     145 + },
     146 + {
     147 + desc: "Round formatter handles NaN values",
     148 + value: math.NaN(),
     149 + formatter: ValueFormatterRound,
     150 + want: "",
     151 + },
     152 + {
     153 + desc: "Round formatter handles 0 values",
     154 + value: 0,
     155 + formatter: ValueFormatterRound,
     156 + want: "0",
     157 + },
     158 + {
     159 + desc: "Round formatter handles > x.5 values",
     160 + value: 96.7,
     161 + formatter: ValueFormatterRound,
     162 + want: "97",
     163 + },
     164 + {
     165 + desc: "Round formatter handles < x.5 values",
     166 + value: 1621.2,
     167 + formatter: ValueFormatterRound,
     168 + want: "1621",
     169 + },
     170 + {
     171 + desc: "Round formatter handles x.5 values",
     172 + value: 6.5,
     173 + formatter: ValueFormatterRound,
     174 + want: "7",
     175 + },
     176 + {
     177 + desc: "Round formatter handles minus > x.5 values",
     178 + value: -96.7,
     179 + formatter: ValueFormatterRound,
     180 + want: "-97",
     181 + },
     182 + {
     183 + desc: "Round formatter handles minus < x.5 values",
     184 + value: -1621.2,
     185 + formatter: ValueFormatterRound,
     186 + want: "-1621",
     187 + },
     188 + {
     189 + desc: "Round formatter handles minus x.5 values",
     190 + value: -6.5,
     191 + formatter: ValueFormatterRound,
     192 + want: "-7",
     193 + },
     194 + {
     195 + desc: "Round formatter handles values with suffix",
     196 + value: 96.7,
     197 + formatter: ValueFormatterRoundWithSuffix("km"),
     198 + want: "97km",
     199 + },
     200 + {
     201 + desc: "Suffix formatter handles values with decimals",
     202 + value: 11234567890.71234567890,
     203 + formatter: ValueFormatterSuffix(4, " reqps"),
     204 + want: "11234567890.7123 reqps",
     205 + },
     206 + {
     207 + desc: "Suffix formatter handles NaN values",
     208 + value: math.NaN(),
     209 + formatter: ValueFormatterSuffix(2, "test"),
     210 + want: "",
     211 + },
     212 + {
     213 + desc: "Suffix formatter handles 0 values",
     214 + value: 0,
     215 + formatter: ValueFormatterSuffix(2, "test"),
     216 + want: "0.00test",
     217 + },
     218 + {
     219 + desc: "Suffix formatters handles correctly percent suffix",
     220 + value: 96.78,
     221 + formatter: ValueFormatterSuffix(2, "%"),
     222 + want: "96.78%",
     223 + },
     224 + {
     225 + desc: "Round formatter handles values with percent suffix",
     226 + value: 96.7,
     227 + formatter: ValueFormatterRoundWithSuffix("%"),
     228 + want: "97%",
     229 + },
     230 + }
     231 + 
     232 + for _, tc := range tests {
     233 + t.Run(tc.desc, func(t *testing.T) {
     234 + got := tc.formatter(tc.value)
     235 + if diff := pretty.Compare(tc.want, got); diff != "" {
     236 + t.Errorf("formatter => unexpected diff (-want, +got):\n%s", diff)
     237 + }
     238 + })
     239 + }
     240 +}
     241 + 
  • ■ ■ ■ ■ ■ ■
    widgets/segmentdisplay/segment_area.go
    skipped 20 lines
    21 21   "fmt"
    22 22   "image"
    23 23   
    24  - "github.com/mum4k/termdash/internal/segdisp/sixteen"
     24 + "github.com/mum4k/termdash/internal/segdisp"
    25 25  )
    26 26   
    27 27  // segArea contains information about the area that will contain the segments.
    skipped 22 lines
    50 50  // newSegArea calculates the area for segments given available canvas area,
    51 51  // length of the text to be displayed and the size of gap between segments
    52 52  func newSegArea(cvsAr image.Rectangle, textLen, gapPercent int) (*segArea, error) {
    53  - segAr, err := sixteen.Required(cvsAr)
     53 + segAr, err := segdisp.Required(cvsAr)
    54 54   if err != nil {
    55 55   return nil, fmt.Errorf("sixteen.Required => %v", err)
    56 56   }
    skipped 43 lines
    100 100  // required for a single segment and the number of segments we can fit.
    101 101  func maximizeFit(cvsAr image.Rectangle, textLen, gapPercent int) (*segArea, error) {
    102 102   var bestSegAr *segArea
    103  - for height := cvsAr.Dy(); height >= sixteen.MinRows; height-- {
     103 + for height := cvsAr.Dy(); height >= segdisp.MinRows; height-- {
    104 104   cvsAr := image.Rect(cvsAr.Min.X, cvsAr.Min.Y, cvsAr.Max.X, cvsAr.Min.Y+height)
    105 105   segAr, err := newSegArea(cvsAr, textLen, gapPercent)
    106 106   if err != nil {
    skipped 11 lines
  • ■ ■ ■ ■ ■
    widgets/segmentdisplay/segmentdisplay.go
    skipped 25 lines
    26 26   "github.com/mum4k/termdash/internal/alignfor"
    27 27   "github.com/mum4k/termdash/internal/attrrange"
    28 28   "github.com/mum4k/termdash/internal/canvas"
     29 + "github.com/mum4k/termdash/internal/segdisp"
     30 + "github.com/mum4k/termdash/internal/segdisp/dotseg"
    29 31   "github.com/mum4k/termdash/internal/segdisp/sixteen"
    30 32   "github.com/mum4k/termdash/terminal/terminalapi"
    31 33   "github.com/mum4k/termdash/widgetapi"
    skipped 21 lines
    53 55   // lastCanFit is the number of segments that could fit the area the last
    54 56   // time Draw was called.
    55 57   lastCanFit int
     58 + 
     59 + // dotChars are characters that are drawn using the dot segment.
     60 + // All other characters are draws using the 16-segment display.
     61 + dotChars map[rune]bool
    56 62   
    57 63   // mu protects the widget.
    58 64   mu sync.Mutex
    skipped 11 lines
    70 76   if err := opt.validate(); err != nil {
    71 77   return nil, err
    72 78   }
     79 + 
     80 + dotChars := map[rune]bool{}
     81 + for _, r := range dotseg.SupportedChars() {
     82 + dotChars[r] = true
     83 + }
    73 84   return &SegmentDisplay{
    74 85   wOptsTracker: attrrange.NewTracker(),
    75 86   opts: opt,
     87 + dotChars: dotChars,
    76 88   }, nil
    77 89  }
    78 90   
    skipped 145 lines
    224 236   break
    225 237   }
    226 238   
    227  - disp := sixteen.New()
    228  - if err := disp.SetCharacter(c); err != nil {
    229  - return fmt.Errorf("disp.SetCharacter => %v", err)
    230  - }
    231  - 
    232 239   endX := startX + segAr.segment.Dx()
    233 240   ar := image.Rect(startX, aligned.Min.Y, endX, aligned.Max.Y)
    234 241   startX = endX
    skipped 15 lines
    250 257   optRange = or
    251 258   }
    252 259   wOpts := sd.givenWOpts[optRange.AttrIdx]
    253  - 
    254  - if err := disp.Draw(dCvs, sixteen.CellOpts(wOpts.cellOpts...)); err != nil {
    255  - return fmt.Errorf("disp.Draw => %v", err)
     260 + if err := sd.drawChar(dCvs, c, wOpts); err != nil {
     261 + return err
    256 262   }
    257 263   
    258 264   if err := dCvs.CopyTo(cvs); err != nil {
    skipped 3 lines
    262 268   return nil
    263 269  }
    264 270   
     271 +// drawChar draws a single character onto the provided canvas.
     272 +func (sd *SegmentDisplay) drawChar(dCvs *canvas.Canvas, c rune, wOpts *writeOptions) error {
     273 + if sd.dotChars[c] {
     274 + disp := dotseg.New()
     275 + if err := disp.SetCharacter(c); err != nil {
     276 + return fmt.Errorf("dotseg.Display.SetCharacter => %v", err)
     277 + }
     278 + if err := disp.Draw(dCvs, dotseg.CellOpts(wOpts.cellOpts...)); err != nil {
     279 + return fmt.Errorf("dotseg.Display..Draw => %v", err)
     280 + }
     281 + return nil
     282 + }
     283 + 
     284 + disp := sixteen.New()
     285 + if err := disp.SetCharacter(c); err != nil {
     286 + return fmt.Errorf("sixteen.Display.SetCharacter => %v", err)
     287 + }
     288 + if err := disp.Draw(dCvs, sixteen.CellOpts(wOpts.cellOpts...)); err != nil {
     289 + return fmt.Errorf("sixteen.Display.Draw => %v", err)
     290 + }
     291 + return nil
     292 +}
     293 + 
    265 294  // Keyboard input isn't supported on the SegmentDisplay widget.
    266 295  func (*SegmentDisplay) Keyboard(k *terminalapi.Keyboard) error {
    267 296   return errors.New("the SegmentDisplay widget doesn't support keyboard events")
    skipped 8 lines
    276 305  func (sd *SegmentDisplay) Options() widgetapi.Options {
    277 306   return widgetapi.Options{
    278 307   // The smallest supported size of a display segment.
    279  - MinimumSize: image.Point{sixteen.MinCols, sixteen.MinRows},
     308 + MinimumSize: image.Point{segdisp.MinCols, segdisp.MinRows},
    280 309   WantKeyboard: widgetapi.KeyScopeNone,
    281 310   WantMouse: widgetapi.MouseScopeNone,
    282 311   }
    skipped 2 lines
  • ■ ■ ■ ■ ■ ■
    widgets/segmentdisplay/segmentdisplay_test.go
    skipped 23 lines
    24 24   "github.com/mum4k/termdash/internal/canvas"
    25 25   "github.com/mum4k/termdash/internal/canvas/testcanvas"
    26 26   "github.com/mum4k/termdash/internal/faketerm"
     27 + "github.com/mum4k/termdash/internal/segdisp"
     28 + "github.com/mum4k/termdash/internal/segdisp/dotseg"
     29 + "github.com/mum4k/termdash/internal/segdisp/dotseg/testdotseg"
    27 30   "github.com/mum4k/termdash/internal/segdisp/sixteen"
    28 31   "github.com/mum4k/termdash/internal/segdisp/sixteen/testsixteen"
    29 32   "github.com/mum4k/termdash/terminal/terminalapi"
    skipped 1 lines
    31 34  )
    32 35   
    33 36  // mustDrawChar draws the provided character in the area of the canvas or panics.
    34  -func mustDrawChar(cvs *canvas.Canvas, char rune, ar image.Rectangle, opts ...sixteen.Option) {
    35  - d := sixteen.New()
    36  - testsixteen.MustSetCharacter(d, char)
     37 +func mustDrawChar(cvs *canvas.Canvas, char rune, ar image.Rectangle, cOpts ...cell.Option) {
    37 38   c := testcanvas.MustNew(ar)
    38  - testsixteen.MustDraw(d, c, opts...)
     39 + switch {
     40 + case char == '.' || char == ':':
     41 + d := dotseg.New()
     42 + testdotseg.MustSetCharacter(d, char)
     43 + testdotseg.MustDraw(d, c, dotseg.CellOpts(cOpts...))
     44 + 
     45 + default:
     46 + d := sixteen.New()
     47 + testsixteen.MustSetCharacter(d, char)
     48 + testsixteen.MustDraw(d, c, sixteen.CellOpts(cOpts...))
     49 + }
     50 + 
    39 51   testcanvas.MustCopyTo(c, cvs)
    40 52  }
    41 53   
    skipped 15 lines
    57 69   opts: []Option{
    58 70   GapPercent(-1),
    59 71   },
    60  - canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows),
     72 + canvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    61 73   wantNewErr: true,
    62 74   },
    63 75   {
    skipped 1 lines
    65 77   opts: []Option{
    66 78   GapPercent(101),
    67 79   },
    68  - canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows),
     80 + canvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    69 81   wantNewErr: true,
    70 82   },
    71 83   {
    72 84   desc: "write fails on invalid GapPercent (too low)",
    73  - canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows),
     85 + canvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    74 86   update: func(sd *SegmentDisplay) error {
    75 87   return sd.Write(
    76 88   []*TextChunk{NewChunk("1")},
    skipped 4 lines
    81 93   },
    82 94   {
    83 95   desc: "write fails on invalid GapPercent (too high)",
    84  - canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows),
     96 + canvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    85 97   update: func(sd *SegmentDisplay) error {
    86 98   return sd.Write(
    87 99   []*TextChunk{NewChunk("1")},
    skipped 4 lines
    92 104   },
    93 105   {
    94 106   desc: "fails on area too small for a segment",
    95  - canvas: image.Rect(0, 0, sixteen.MinCols-1, sixteen.MinRows),
     107 + canvas: image.Rect(0, 0, segdisp.MinCols-1, segdisp.MinRows),
    96 108   update: func(sd *SegmentDisplay) error {
    97 109   return sd.Write([]*TextChunk{NewChunk("1")})
    98 110   },
    skipped 1 lines
    100 112   },
    101 113   {
    102 114   desc: "write fails without chunks",
    103  - canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows),
     115 + canvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    104 116   update: func(sd *SegmentDisplay) error {
    105 117   return sd.Write(nil)
    106 118   },
    skipped 1 lines
    108 120   },
    109 121   {
    110 122   desc: "write fails with an empty chunk",
    111  - canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows),
     123 + canvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    112 124   update: func(sd *SegmentDisplay) error {
    113 125   return sd.Write([]*TextChunk{NewChunk("")})
    114 126   },
    skipped 1 lines
    116 128   },
    117 129   {
    118 130   desc: "write fails on unsupported characters when requested",
    119  - canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows),
     131 + canvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    120 132   update: func(sd *SegmentDisplay) error {
    121  - return sd.Write([]*TextChunk{NewChunk(".", WriteErrOnUnsupported())})
     133 + return sd.Write([]*TextChunk{NewChunk("", WriteErrOnUnsupported())})
    122 134   },
    123 135   wantUpdateErr: true,
    124 136   },
    125 137   {
    126 138   desc: "draws empty without text",
    127  - canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows),
     139 + canvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    128 140   wantCapacity: 1,
    129 141   },
    130 142   {
    skipped 1 lines
    132 144   opts: []Option{
    133 145   GapPercent(0),
    134 146   },
    135  - canvas: image.Rect(0, 0, sixteen.MinCols*3, sixteen.MinRows),
     147 + canvas: image.Rect(0, 0, segdisp.MinCols*3, segdisp.MinRows),
    136 148   update: func(sd *SegmentDisplay) error {
    137 149   return sd.Write([]*TextChunk{NewChunk("123")})
    138 150   },
    skipped 5 lines
    144 156   char rune
    145 157   area image.Rectangle
    146 158   }{
    147  - {'1', image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows)},
    148  - {'2', image.Rect(sixteen.MinCols, 0, sixteen.MinCols*2, sixteen.MinRows)},
    149  - {'3', image.Rect(sixteen.MinCols*2, 0, sixteen.MinCols*3, sixteen.MinRows)},
     159 + {'1', image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows)},
     160 + {'2', image.Rect(segdisp.MinCols, 0, segdisp.MinCols*2, segdisp.MinRows)},
     161 + {'3', image.Rect(segdisp.MinCols*2, 0, segdisp.MinCols*3, segdisp.MinRows)},
     162 + } {
     163 + mustDrawChar(cvs, tc.char, tc.area)
     164 + }
     165 + 
     166 + testcanvas.MustApply(cvs, ft)
     167 + return ft
     168 + },
     169 + wantCapacity: 3,
     170 + },
     171 + {
     172 + desc: "uses the dot segment for a colon",
     173 + opts: []Option{
     174 + GapPercent(0),
     175 + },
     176 + canvas: image.Rect(0, 0, segdisp.MinCols*3, segdisp.MinRows),
     177 + update: func(sd *SegmentDisplay) error {
     178 + return sd.Write([]*TextChunk{NewChunk("1:3")})
     179 + },
     180 + want: func(size image.Point) *faketerm.Terminal {
     181 + ft := faketerm.MustNew(size)
     182 + cvs := testcanvas.MustNew(ft.Area())
     183 + 
     184 + for _, tc := range []struct {
     185 + char rune
     186 + area image.Rectangle
     187 + }{
     188 + {'1', image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows)},
     189 + {':', image.Rect(segdisp.MinCols, 0, segdisp.MinCols*2, segdisp.MinRows)},
     190 + {'3', image.Rect(segdisp.MinCols*2, 0, segdisp.MinCols*3, segdisp.MinRows)},
     191 + } {
     192 + mustDrawChar(cvs, tc.char, tc.area)
     193 + }
     194 + 
     195 + testcanvas.MustApply(cvs, ft)
     196 + return ft
     197 + },
     198 + wantCapacity: 3,
     199 + },
     200 + {
     201 + desc: "uses the dot segment for a dot",
     202 + opts: []Option{
     203 + GapPercent(0),
     204 + },
     205 + canvas: image.Rect(0, 0, segdisp.MinCols*3, segdisp.MinRows),
     206 + update: func(sd *SegmentDisplay) error {
     207 + return sd.Write([]*TextChunk{NewChunk("1.3")})
     208 + },
     209 + want: func(size image.Point) *faketerm.Terminal {
     210 + ft := faketerm.MustNew(size)
     211 + cvs := testcanvas.MustNew(ft.Area())
     212 + 
     213 + for _, tc := range []struct {
     214 + char rune
     215 + area image.Rectangle
     216 + }{
     217 + {'1', image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows)},
     218 + {'.', image.Rect(segdisp.MinCols, 0, segdisp.MinCols*2, segdisp.MinRows)},
     219 + {'3', image.Rect(segdisp.MinCols*2, 0, segdisp.MinCols*3, segdisp.MinRows)},
    150 220   } {
    151 221   mustDrawChar(cvs, tc.char, tc.area)
    152 222   }
    skipped 8 lines
    161 231   opts: []Option{
    162 232   GapPercent(0),
    163 233   },
    164  - canvas: image.Rect(0, 0, sixteen.MinCols*2, sixteen.MinRows),
     234 + canvas: image.Rect(0, 0, segdisp.MinCols*2, segdisp.MinRows),
    165 235   update: func(sd *SegmentDisplay) error {
    166  - return sd.Write([]*TextChunk{NewChunk(".1")})
     236 + return sd.Write([]*TextChunk{NewChunk("1")})
    167 237   },
    168 238   want: func(size image.Point) *faketerm.Terminal {
    169 239   ft := faketerm.MustNew(size)
    170 240   cvs := testcanvas.MustNew(ft.Area())
    171 241   
    172  - mustDrawChar(cvs, '1', image.Rect(sixteen.MinCols, 0, sixteen.MinCols*2, sixteen.MinRows))
     242 + mustDrawChar(cvs, '1', image.Rect(segdisp.MinCols, 0, segdisp.MinCols*2, segdisp.MinRows))
    173 243   
    174 244   testcanvas.MustApply(cvs, ft)
    175 245   return ft
    skipped 5 lines
    181 251   opts: []Option{
    182 252   GapPercent(0),
    183 253   },
    184  - canvas: image.Rect(0, 0, sixteen.MinCols*2, sixteen.MinRows),
     254 + canvas: image.Rect(0, 0, segdisp.MinCols*2, segdisp.MinRows),
    185 255   update: func(sd *SegmentDisplay) error {
    186  - return sd.Write([]*TextChunk{NewChunk(".1", WriteSanitize())})
     256 + return sd.Write([]*TextChunk{NewChunk("1", WriteSanitize())})
    187 257   },
    188 258   want: func(size image.Point) *faketerm.Terminal {
    189 259   ft := faketerm.MustNew(size)
    190 260   cvs := testcanvas.MustNew(ft.Area())
    191 261   
    192  - mustDrawChar(cvs, '1', image.Rect(sixteen.MinCols, 0, sixteen.MinCols*2, sixteen.MinRows))
     262 + mustDrawChar(cvs, '1', image.Rect(segdisp.MinCols, 0, segdisp.MinCols*2, segdisp.MinRows))
    193 263   
    194 264   testcanvas.MustApply(cvs, ft)
    195 265   return ft
    skipped 2 lines
    198 268   },
    199 269   {
    200 270   desc: "aligns segment vertical middle by default",
    201  - canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows+2),
     271 + canvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows+2),
    202 272   update: func(sd *SegmentDisplay) error {
    203 273   return sd.Write([]*TextChunk{NewChunk("1")})
    204 274   },
    skipped 1 lines
    206 276   ft := faketerm.MustNew(size)
    207 277   cvs := testcanvas.MustNew(ft.Area())
    208 278   
    209  - mustDrawChar(cvs, '1', image.Rect(0, 1, sixteen.MinCols, sixteen.MinRows+1))
     279 + mustDrawChar(cvs, '1', image.Rect(0, 1, segdisp.MinCols, segdisp.MinRows+1))
    210 280   
    211 281   testcanvas.MustApply(cvs, ft)
    212 282   return ft
    skipped 2 lines
    215 285   },
    216 286   {
    217 287   desc: "subsequent calls to write overwrite previous text",
    218  - canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows+2),
     288 + canvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows+2),
    219 289   update: func(sd *SegmentDisplay) error {
    220 290   if err := sd.Write([]*TextChunk{NewChunk("123")}); err != nil {
    221 291   return err
    skipped 4 lines
    226 296   ft := faketerm.MustNew(size)
    227 297   cvs := testcanvas.MustNew(ft.Area())
    228 298   
    229  - mustDrawChar(cvs, '4', image.Rect(0, 1, sixteen.MinCols, sixteen.MinRows+1))
     299 + mustDrawChar(cvs, '4', image.Rect(0, 1, segdisp.MinCols, segdisp.MinRows+1))
    230 300   
    231 301   testcanvas.MustApply(cvs, ft)
    232 302   return ft
    skipped 5 lines
    238 308   opts: []Option{
    239 309   GapPercent(0),
    240 310   },
    241  - canvas: image.Rect(0, 0, sixteen.MinCols*2, sixteen.MinRows),
     311 + canvas: image.Rect(0, 0, segdisp.MinCols*2, segdisp.MinRows),
    242 312   update: func(sd *SegmentDisplay) error {
    243 313   return sd.Write(
    244 314   []*TextChunk{
    skipped 13 lines
    258 328   
    259 329   mustDrawChar(
    260 330   cvs, '1',
    261  - image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows),
    262  - sixteen.CellOpts(
    263  - cell.FgColor(cell.ColorRed),
    264  - cell.BgColor(cell.ColorBlue),
    265  - ),
     331 + image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
     332 + cell.FgColor(cell.ColorRed),
     333 + cell.BgColor(cell.ColorBlue),
    266 334   )
    267 335   mustDrawChar(
    268 336   cvs, '2',
    269  - image.Rect(sixteen.MinCols, 0, sixteen.MinCols*2, sixteen.MinRows),
    270  - sixteen.CellOpts(
    271  - cell.FgColor(cell.ColorGreen),
    272  - cell.BgColor(cell.ColorYellow),
    273  - ),
     337 + image.Rect(segdisp.MinCols, 0, segdisp.MinCols*2, segdisp.MinRows),
     338 + cell.FgColor(cell.ColorGreen),
     339 + cell.BgColor(cell.ColorYellow),
    274 340   )
    275 341   
    276 342   testcanvas.MustApply(cvs, ft)
    skipped 6 lines
    283 349   opts: []Option{
    284 350   MaximizeDisplayedText(),
    285 351   },
    286  - canvas: image.Rect(0, 0, sixteen.MinCols*3, sixteen.MinRows+2),
     352 + canvas: image.Rect(0, 0, segdisp.MinCols*3, segdisp.MinRows+2),
    287 353   update: func(sd *SegmentDisplay) error {
    288 354   if err := sd.Write([]*TextChunk{NewChunk("123")}); err != nil {
    289 355   return err
    skipped 9 lines
    299 365   GapPercent(0),
    300 366   MaximizeDisplayedText(),
    301 367   },
    302  - canvas: image.Rect(0, 0, sixteen.MinCols*3, sixteen.MinRows+2),
     368 + canvas: image.Rect(0, 0, segdisp.MinCols*3, segdisp.MinRows+2),
    303 369   update: func(sd *SegmentDisplay) error {
    304 370   if err := sd.Write([]*TextChunk{NewChunk("123")}); err != nil {
    305 371   return err
    skipped 8 lines
    314 380   opts: []Option{
    315 381   MaximizeSegmentHeight(),
    316 382   },
    317  - canvas: image.Rect(0, 0, sixteen.MinCols*3, sixteen.MinRows+2),
     383 + canvas: image.Rect(0, 0, segdisp.MinCols*3, segdisp.MinRows+2),
    318 384   update: func(sd *SegmentDisplay) error {
    319 385   if err := sd.Write([]*TextChunk{NewChunk("123")}); err != nil {
    320 386   return err
    skipped 9 lines
    330 396   GapPercent(0),
    331 397   MaximizeSegmentHeight(),
    332 398   },
    333  - canvas: image.Rect(0, 0, sixteen.MinCols*3, sixteen.MinRows+2),
     399 + canvas: image.Rect(0, 0, segdisp.MinCols*3, segdisp.MinRows+2),
    334 400   update: func(sd *SegmentDisplay) error {
    335 401   if err := sd.Write([]*TextChunk{NewChunk("123")}); err != nil {
    336 402   return err
    skipped 5 lines
    342 408   },
    343 409   {
    344 410   desc: "reset resets provided cell options",
    345  - canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows),
     411 + canvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows),
    346 412   update: func(sd *SegmentDisplay) error {
    347 413   if err := sd.Write(
    348 414   []*TextChunk{
    skipped 11 lines
    360 426   ft := faketerm.MustNew(size)
    361 427   cvs := testcanvas.MustNew(ft.Area())
    362 428   
    363  - mustDrawChar(cvs, '1', image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows))
     429 + mustDrawChar(cvs, '1', image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows))
    364 430   
    365 431   testcanvas.MustApply(cvs, ft)
    366 432   return ft
    skipped 5 lines
    372 438   opts: []Option{
    373 439   AlignVertical(align.VerticalMiddle),
    374 440   },
    375  - canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows+2),
     441 + canvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows+2),
    376 442   update: func(sd *SegmentDisplay) error {
    377 443   return sd.Write([]*TextChunk{NewChunk("1")})
    378 444   },
    skipped 1 lines
    380 446   ft := faketerm.MustNew(size)
    381 447   cvs := testcanvas.MustNew(ft.Area())
    382 448   
    383  - mustDrawChar(cvs, '1', image.Rect(0, 1, sixteen.MinCols, sixteen.MinRows+1))
     449 + mustDrawChar(cvs, '1', image.Rect(0, 1, segdisp.MinCols, segdisp.MinRows+1))
    384 450   
    385 451   testcanvas.MustApply(cvs, ft)
    386 452   return ft
    skipped 5 lines
    392 458   opts: []Option{
    393 459   AlignVertical(align.VerticalTop),
    394 460   },
    395  - canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows+2),
     461 + canvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows+2),
    396 462   update: func(sd *SegmentDisplay) error {
    397 463   return sd.Write([]*TextChunk{NewChunk("1")})
    398 464   },
    skipped 1 lines
    400 466   ft := faketerm.MustNew(size)
    401 467   cvs := testcanvas.MustNew(ft.Area())
    402 468   
    403  - mustDrawChar(cvs, '1', image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows))
     469 + mustDrawChar(cvs, '1', image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows))
    404 470   
    405 471   testcanvas.MustApply(cvs, ft)
    406 472   return ft
    skipped 5 lines
    412 478   opts: []Option{
    413 479   AlignVertical(align.VerticalBottom),
    414 480   },
    415  - canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows+2),
     481 + canvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows+2),
    416 482   update: func(sd *SegmentDisplay) error {
    417 483   return sd.Write(
    418 484   []*TextChunk{NewChunk("1")},
    skipped 4 lines
    423 489   ft := faketerm.MustNew(size)
    424 490   cvs := testcanvas.MustNew(ft.Area())
    425 491   
    426  - mustDrawChar(cvs, '1', image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows))
     492 + mustDrawChar(cvs, '1', image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows))
    427 493   
    428 494   testcanvas.MustApply(cvs, ft)
    429 495   return ft
    skipped 5 lines
    435 501   opts: []Option{
    436 502   AlignVertical(align.VerticalBottom),
    437 503   },
    438  - canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows+2),
     504 + canvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows+2),
    439 505   update: func(sd *SegmentDisplay) error {
    440 506   return sd.Write([]*TextChunk{NewChunk("1")})
    441 507   },
    skipped 1 lines
    443 509   ft := faketerm.MustNew(size)
    444 510   cvs := testcanvas.MustNew(ft.Area())
    445 511   
    446  - mustDrawChar(cvs, '1', image.Rect(0, 2, sixteen.MinCols, sixteen.MinRows+2))
     512 + mustDrawChar(cvs, '1', image.Rect(0, 2, segdisp.MinCols, segdisp.MinRows+2))
    447 513   
    448 514   testcanvas.MustApply(cvs, ft)
    449 515   return ft
    skipped 2 lines
    452 518   },
    453 519   {
    454 520   desc: "aligns segment horizontal center by default",
    455  - canvas: image.Rect(0, 0, sixteen.MinCols+2, sixteen.MinRows),
     521 + canvas: image.Rect(0, 0, segdisp.MinCols+2, segdisp.MinRows),
    456 522   update: func(sd *SegmentDisplay) error {
    457 523   return sd.Write([]*TextChunk{NewChunk("8")})
    458 524   },
    skipped 1 lines
    460 526   ft := faketerm.MustNew(size)
    461 527   cvs := testcanvas.MustNew(ft.Area())
    462 528   
    463  - mustDrawChar(cvs, '8', image.Rect(1, 0, sixteen.MinCols+1, sixteen.MinRows))
     529 + mustDrawChar(cvs, '8', image.Rect(1, 0, segdisp.MinCols+1, segdisp.MinRows))
    464 530   
    465 531   testcanvas.MustApply(cvs, ft)
    466 532   return ft
    skipped 5 lines
    472 538   opts: []Option{
    473 539   AlignHorizontal(align.HorizontalCenter),
    474 540   },
    475  - canvas: image.Rect(0, 0, sixteen.MinCols+2, sixteen.MinRows),
     541 + canvas: image.Rect(0, 0, segdisp.MinCols+2, segdisp.MinRows),
    476 542   update: func(sd *SegmentDisplay) error {
    477 543   return sd.Write([]*TextChunk{NewChunk("8")})
    478 544   },
    skipped 1 lines
    480 546   ft := faketerm.MustNew(size)
    481 547   cvs := testcanvas.MustNew(ft.Area())
    482 548   
    483  - mustDrawChar(cvs, '8', image.Rect(1, 0, sixteen.MinCols+1, sixteen.MinRows))
     549 + mustDrawChar(cvs, '8', image.Rect(1, 0, segdisp.MinCols+1, segdisp.MinRows))
    484 550   
    485 551   testcanvas.MustApply(cvs, ft)
    486 552   return ft
    skipped 5 lines
    492 558   opts: []Option{
    493 559   AlignHorizontal(align.HorizontalLeft),
    494 560   },
    495  - canvas: image.Rect(0, 0, sixteen.MinCols+2, sixteen.MinRows),
     561 + canvas: image.Rect(0, 0, segdisp.MinCols+2, segdisp.MinRows),
    496 562   update: func(sd *SegmentDisplay) error {
    497 563   return sd.Write([]*TextChunk{NewChunk("8")})
    498 564   },
    skipped 1 lines
    500 566   ft := faketerm.MustNew(size)
    501 567   cvs := testcanvas.MustNew(ft.Area())
    502 568   
    503  - mustDrawChar(cvs, '8', image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows))
     569 + mustDrawChar(cvs, '8', image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows))
    504 570   
    505 571   testcanvas.MustApply(cvs, ft)
    506 572   return ft
    skipped 5 lines
    512 578   opts: []Option{
    513 579   AlignHorizontal(align.HorizontalRight),
    514 580   },
    515  - canvas: image.Rect(0, 0, sixteen.MinCols+2, sixteen.MinRows),
     581 + canvas: image.Rect(0, 0, segdisp.MinCols+2, segdisp.MinRows),
    516 582   update: func(sd *SegmentDisplay) error {
    517 583   return sd.Write([]*TextChunk{NewChunk("8")})
    518 584   },
    skipped 1 lines
    520 586   ft := faketerm.MustNew(size)
    521 587   cvs := testcanvas.MustNew(ft.Area())
    522 588   
    523  - mustDrawChar(cvs, '8', image.Rect(2, 0, sixteen.MinCols+2, sixteen.MinRows))
     589 + mustDrawChar(cvs, '8', image.Rect(2, 0, segdisp.MinCols+2, segdisp.MinRows))
    524 590   
    525 591   testcanvas.MustApply(cvs, ft)
    526 592   return ft
    skipped 6 lines
    533 599   MaximizeSegmentHeight(),
    534 600   GapPercent(0),
    535 601   },
    536  - canvas: image.Rect(0, 0, sixteen.MinCols*2, sixteen.MinRows),
     602 + canvas: image.Rect(0, 0, segdisp.MinCols*2, segdisp.MinRows),
    537 603   update: func(sd *SegmentDisplay) error {
    538 604   return sd.Write([]*TextChunk{NewChunk("123")})
    539 605   },
    skipped 5 lines
    545 611   char rune
    546 612   area image.Rectangle
    547 613   }{
    548  - {'1', image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows)},
    549  - {'2', image.Rect(sixteen.MinCols, 0, sixteen.MinCols*2, sixteen.MinRows)},
     614 + {'1', image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows)},
     615 + {'2', image.Rect(segdisp.MinCols, 0, segdisp.MinCols*2, segdisp.MinRows)},
    550 616   } {
    551 617   mustDrawChar(cvs, tc.char, tc.area)
    552 618   }
    skipped 8 lines
    561 627   opts: []Option{
    562 628   GapPercent(0),
    563 629   },
    564  - canvas: image.Rect(0, 0, sixteen.MinCols*3, sixteen.MinRows*4),
     630 + canvas: image.Rect(0, 0, segdisp.MinCols*3, segdisp.MinRows*4),
    565 631   update: func(sd *SegmentDisplay) error {
    566 632   return sd.Write([]*TextChunk{NewChunk("123")})
    567 633   },
    skipped 22 lines
    590 656   opts: []Option{
    591 657   GapPercent(0),
    592 658   },
    593  - canvas: image.Rect(0, 0, sixteen.MinCols*3, sixteen.MinRows*4),
     659 + canvas: image.Rect(0, 0, segdisp.MinCols*3, segdisp.MinRows*4),
    594 660   update: func(sd *SegmentDisplay) error {
    595 661   return sd.Write([]*TextChunk{NewChunk("1234")})
    596 662   },
    skipped 23 lines
    620 686   MaximizeDisplayedText(),
    621 687   GapPercent(0),
    622 688   },
    623  - canvas: image.Rect(0, 0, sixteen.MinCols*3, sixteen.MinRows*4),
     689 + canvas: image.Rect(0, 0, segdisp.MinCols*3, segdisp.MinRows*4),
    624 690   update: func(sd *SegmentDisplay) error {
    625 691   return sd.Write([]*TextChunk{NewChunk("123")})
    626 692   },
    skipped 19 lines
    646 712   },
    647 713   {
    648 714   desc: "draws multiple segments with a gap by default",
    649  - canvas: image.Rect(0, 0, sixteen.MinCols*3+2, sixteen.MinRows),
     715 + canvas: image.Rect(0, 0, segdisp.MinCols*3+2, segdisp.MinRows),
    650 716   update: func(sd *SegmentDisplay) error {
    651 717   return sd.Write([]*TextChunk{NewChunk("123")})
    652 718   },
    skipped 5 lines
    658 724   char rune
    659 725   area image.Rectangle
    660 726   }{
    661  - {'1', image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows)},
    662  - {'2', image.Rect(sixteen.MinCols+1, 0, sixteen.MinCols*2+1, sixteen.MinRows)},
    663  - {'3', image.Rect(sixteen.MinCols*2+2, 0, sixteen.MinCols*3+2, sixteen.MinRows)},
     727 + {'1', image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows)},
     728 + {'2', image.Rect(segdisp.MinCols+1, 0, segdisp.MinCols*2+1, segdisp.MinRows)},
     729 + {'3', image.Rect(segdisp.MinCols*2+2, 0, segdisp.MinCols*3+2, segdisp.MinRows)},
    664 730   } {
    665 731   mustDrawChar(cvs, tc.char, tc.area)
    666 732   }
    skipped 8 lines
    675 741   opts: []Option{
    676 742   GapPercent(20),
    677 743   },
    678  - canvas: image.Rect(0, 0, sixteen.MinCols*3+2, sixteen.MinRows),
     744 + canvas: image.Rect(0, 0, segdisp.MinCols*3+2, segdisp.MinRows),
    679 745   update: func(sd *SegmentDisplay) error {
    680 746   return sd.Write([]*TextChunk{NewChunk("123")})
    681 747   },
    skipped 5 lines
    687 753   char rune
    688 754   area image.Rectangle
    689 755   }{
    690  - {'1', image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows)},
    691  - {'2', image.Rect(sixteen.MinCols+1, 0, sixteen.MinCols*2+1, sixteen.MinRows)},
    692  - {'3', image.Rect(sixteen.MinCols*2+2, 0, sixteen.MinCols*3+2, sixteen.MinRows)},
     756 + {'1', image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows)},
     757 + {'2', image.Rect(segdisp.MinCols+1, 0, segdisp.MinCols*2+1, segdisp.MinRows)},
     758 + {'3', image.Rect(segdisp.MinCols*2+2, 0, segdisp.MinCols*3+2, segdisp.MinRows)},
    693 759   } {
    694 760   mustDrawChar(cvs, tc.char, tc.area)
    695 761   }
    skipped 8 lines
    704 770   opts: []Option{
    705 771   GapPercent(40),
    706 772   },
    707  - canvas: image.Rect(0, 0, sixteen.MinCols*3+2, sixteen.MinRows),
     773 + canvas: image.Rect(0, 0, segdisp.MinCols*3+2, segdisp.MinRows),
    708 774   update: func(sd *SegmentDisplay) error {
    709 775   return sd.Write([]*TextChunk{NewChunk("123")})
    710 776   },
    skipped 21 lines
    732 798   opts: []Option{
    733 799   GapPercent(20),
    734 800   },
    735  - canvas: image.Rect(0, 0, sixteen.MinCols*3+2, sixteen.MinRows),
     801 + canvas: image.Rect(0, 0, segdisp.MinCols*3+2, segdisp.MinRows),
    736 802   update: func(sd *SegmentDisplay) error {
    737 803   return sd.Write([]*TextChunk{NewChunk("8888")})
    738 804   },
    skipped 5 lines
    744 810   char rune
    745 811   area image.Rectangle
    746 812   }{
    747  - {'8', image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows)},
    748  - {'8', image.Rect(sixteen.MinCols+1, 0, sixteen.MinCols*2+1, sixteen.MinRows)},
    749  - {'8', image.Rect(sixteen.MinCols*2+2, 0, sixteen.MinCols*3+2, sixteen.MinRows)},
     813 + {'8', image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows)},
     814 + {'8', image.Rect(segdisp.MinCols+1, 0, segdisp.MinCols*2+1, segdisp.MinRows)},
     815 + {'8', image.Rect(segdisp.MinCols*2+2, 0, segdisp.MinCols*3+2, segdisp.MinRows)},
    750 816   } {
    751 817   mustDrawChar(cvs, tc.char, tc.area)
    752 818   }
    skipped 8 lines
    761 827   opts: []Option{
    762 828   GapPercent(20),
    763 829   },
    764  - canvas: image.Rect(0, 0, sixteen.MinCols*4+2, sixteen.MinRows),
     830 + canvas: image.Rect(0, 0, segdisp.MinCols*4+2, segdisp.MinRows),
    765 831   update: func(sd *SegmentDisplay) error {
    766 832   return sd.Write([]*TextChunk{NewChunk("8888")})
    767 833   },
    skipped 23 lines
    791 857   MaximizeSegmentHeight(),
    792 858   GapPercent(20),
    793 859   },
    794  - canvas: image.Rect(0, 0, sixteen.MinCols*5, sixteen.MinRows*2),
     860 + canvas: image.Rect(0, 0, segdisp.MinCols*5, segdisp.MinRows*2),
    795 861   update: func(sd *SegmentDisplay) error {
    796 862   return sd.Write([]*TextChunk{NewChunk("123")})
    797 863   },
    skipped 21 lines
    819 885   opts: []Option{
    820 886   GapPercent(0),
    821 887   },
    822  - canvas: image.Rect(0, 0, sixteen.MinCols*3, sixteen.MinRows),
     888 + canvas: image.Rect(0, 0, segdisp.MinCols*3, segdisp.MinRows),
    823 889   update: func(sd *SegmentDisplay) error {
    824 890   chunks := []*TextChunk{NewChunk("123")}
    825 891   if err := sd.Write(chunks); err != nil {
    skipped 11 lines
    837 903   char rune
    838 904   area image.Rectangle
    839 905   }{
    840  - {'1', image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows)},
    841  - {'2', image.Rect(sixteen.MinCols, 0, sixteen.MinCols*2, sixteen.MinRows)},
    842  - {'3', image.Rect(sixteen.MinCols*2, 0, sixteen.MinCols*3, sixteen.MinRows)},
     906 + {'1', image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows)},
     907 + {'2', image.Rect(segdisp.MinCols, 0, segdisp.MinCols*2, segdisp.MinRows)},
     908 + {'3', image.Rect(segdisp.MinCols*2, 0, segdisp.MinCols*3, segdisp.MinRows)},
    843 909   } {
    844 910   mustDrawChar(cvs, tc.char, tc.area)
    845 911   }
    skipped 92 lines
    938 1004   }
    939 1005   got := sd.Options()
    940 1006   want := widgetapi.Options{
    941  - MinimumSize: image.Point{sixteen.MinCols, sixteen.MinRows},
     1007 + MinimumSize: image.Point{segdisp.MinCols, segdisp.MinRows},
    942 1008   WantKeyboard: widgetapi.KeyScopeNone,
    943 1009   WantMouse: widgetapi.MouseScopeNone,
    944 1010   }
    skipped 6 lines
  • ■ ■ ■ ■
    widgets/segmentdisplay/segmentdisplaydemo/segmentdisplaydemo.go
    skipped 42 lines
    43 43   
    44 44   spacer := " "
    45 45   if now.Second()%2 == 0 {
    46  - spacer = "_"
     46 + spacer = ":"
    47 47   }
    48 48   chunks := []*segmentdisplay.TextChunk{
    49 49   segmentdisplay.NewChunk(parts[0], segmentdisplay.WriteCellOpts(cell.FgColor(cell.ColorBlue))),
    skipped 114 lines
  • ■ ■ ■ ■
    widgets/text/scroll.go
    skipped 115 lines
    116 116   // If the user didn't scroll, just roll the content so that the last line
    117 117   // is visible.
    118 118   if st.scroll == 0 && st.scrollPage == 0 {
    119  - st.first = normalizeScroll(math.MaxUint32, lines, height)
     119 + st.first = normalizeScroll(math.MaxInt32, lines, height)
    120 120   return rollToEnd
    121 121   }
    122 122   
    skipped 44 lines
  • ■ ■ ■ ■ ■ ■
    widgets/textinput/textinput.go
    skipped 109 lines
    110 110   if err != nil {
    111 111   return err
    112 112   }
    113  - if err := draw.Text(
     113 + return draw.Text(
    114 114   cvs, ti.opts.label, start,
    115 115   draw.TextOverrunMode(draw.OverrunModeThreeDot),
    116 116   draw.TextMaxX(labelAr.Max.X),
    117 117   draw.TextCellOpts(ti.opts.labelCellOpts...),
    118  - ); err != nil {
    119  - return err
    120  - }
    121  - return nil
     118 + )
    122 119  }
    123 120   
    124 121  // drawField draws the text input field.
    skipped 6 lines
    131 128   text = hideText(text, ti.opts.hideTextWith)
    132 129   }
    133 130   
    134  - if err := draw.Text(
     131 + return draw.Text(
    135 132   cvs, text, ti.forField.Min,
    136 133   draw.TextMaxX(ti.forField.Max.X),
    137 134   draw.TextCellOpts(cell.FgColor(ti.opts.textColor)),
    138  - ); err != nil {
    139  - return err
    140  - }
    141  - return nil
     135 + )
    142 136  }
    143 137   
    144 138  // drawCursor draws the cursor within the text input field.
    skipped 248 lines
  • ■ ■ ■ ■ ■ ■
    widgets/textinput/textinputdemo/textinputdemo.go
    skipped 142 lines
    143 143   button.GlobalKey(keyboard.KeyEnter),
    144 144   button.FillColor(cell.ColorNumber(220)),
    145 145   )
     146 + if err != nil {
     147 + panic(err)
     148 + }
    146 149   clearB, err := button.New("Clear", func() error {
    147 150   input.ReadAndClear()
    148 151   updateText <- ""
    skipped 2 lines
    151 154   button.WidthFor("Submit"),
    152 155   button.FillColor(cell.ColorNumber(220)),
    153 156   )
     157 + if err != nil {
     158 + panic(err)
     159 + }
    154 160   quitB, err := button.New("Quit", func() error {
    155 161   cancel()
    156 162   return nil
    skipped 1 lines
    158 164   button.WidthFor("Submit"),
    159 165   button.FillColor(cell.ColorNumber(196)),
    160 166   )
     167 + if err != nil {
     168 + panic(err)
     169 + }
    161 170   
    162 171   builder := grid.New()
    163 172   builder.Add(
    skipped 59 lines
Please wait...
Page is in error, reload to recover