Projects STRLCPY termdash Commits c861ecef
🤬
Showing first 79 files as there are too many
  • ■ ■ ■ ■ ■
    .travis.yml
    skipped 5 lines
    6 6   - stable
    7 7  script:
    8 8   - go get -t ./...
     9 + - go get -u golang.org/x/lint/golint
    9 10   - go test ./...
    10 11   - go test -race ./...
    11 12   - go vet ./...
    12 13   - diff -u <(echo -n) <(gofmt -d -s .)
    13  - - diff -u <(echo -n) <(./scripts/autogen_licences.sh .)
     14 + - diff -u <(echo -n) <(./internal/scripts/autogen_licences.sh .)
     15 + - diff -u <(echo -n) <(golint ./...)
    14 16  after_success:
    15 17   - ./scripts/coverage.sh
    16 18   
  • ■ ■ ■ ■ ■ ■
    CHANGELOG.md
    skipped 6 lines
    7 7   
    8 8  ## [Unreleased]
    9 9   
     10 +## [0.7.0] - 24-Feb-2019
     11 + 
     12 +### Added
     13 + 
     14 +#### New widgets
     15 + 
     16 +- The Button widget.
     17 + 
     18 +#### Improvements to documentation
     19 + 
     20 +- Clearly marked the public API surface by moving private packages into
     21 + internal directory.
     22 +- Started a GitHub wiki for Termdash.
     23 + 
     24 +#### Improvements to the LineChart widget
     25 + 
     26 +- The LineChart widget can display X axis labels in vertical orientation.
     27 +- The LineChart widget allows the user to specify a custom scale for the Y
     28 + axis.
     29 +- The LineChart widget now has an option that disables scaling of the X axis.
     30 + Useful for applications that want to continuously feed data and make them
     31 + "roll" through the linechart.
     32 +- The LineChart widget now has a method that returns the observed capacity of
     33 + the LineChart the last time Draw was called.
     34 +- The LineChart widget now supports zoom of the content triggered by mouse
     35 + events.
     36 + 
     37 +#### Improvements to the Text widget
     38 + 
     39 +- The Text widget now has a Write option that atomically replaces the entire
     40 + text content.
     41 + 
     42 + 
     43 +#### Improvements to the infrastructure
     44 + 
     45 +- A function that draws text vertically.
     46 +- A non-blocking event distribution system that can throttle repetitive events.
     47 +- Generalized mouse button FSM for use in widgets that need to track mouse
     48 + button clicks.
     49 + 
     50 +### Changed
     51 + 
     52 +- Termbox is now initialized in 256 color mode by default.
     53 +- The infrastructure now uses the non-blocking event distribution system to
     54 + distribute events to subscribers. Each widget is now an individual
     55 + subscriber.
     56 +- The infrastructure now throttles event driven screen redraw rather than
     57 + redrawing for each input event.
     58 +- Widgets can now specify the scope at which they want to receive keyboard and
     59 + mouse events.
     60 + 
     61 +#### Breaking API changes
     62 + 
     63 +##### High impact
     64 + 
     65 +- The constructors of all the widgets now also return an error so that they
     66 + can validate the options. This is a breaking change for the following
     67 + widgets: BarChart, Gauge, LineChart, SparkLine, Text. The callers will have
     68 + to handle the returned error.
     69 + 
     70 +##### Low impact
     71 + 
     72 +- The container package no longer exports separate methods to receive Keyboard
     73 + and Mouse events which were replaced by a Subscribe method for the event
     74 + distribution system. This shouldn't affect users as the removed methods
     75 + aren't needed by container users.
     76 +- The widgetapi.Options struct now uses an enum instead of a boolean when
     77 + widget specifies if it wants keyboard or mouse events. This only impacts
     78 + development of new widgets.
     79 + 
     80 +### Fixed
     81 + 
     82 +- The LineChart widget now correctly determines the Y axis scale when multiple
     83 + series are provided.
     84 +- Lint issues in the codebase, and updated Travis configuration so that golint
     85 + is executed on every run.
     86 +- Termdash now correctly starts in locales like zh_CN.UTF-8 where some of the
     87 + characters it uses internally can have ambiguous width.
     88 + 
    10 89  ## [0.6.1] - 12-Feb-2019
    11 90   
    12 91  ### Fixed
    skipped 76 lines
    89 168  - The Gauge widget.
    90 169  - The Text widget.
    91 170   
    92  -[Unreleased]: https://github.com/mum4k/termdash/compare/v0.6.1...devel
     171 +[Unreleased]: https://github.com/mum4k/termdash/compare/v0.7.0...devel
     172 +[0.7.0]: https://github.com/mum4k/termdash/compare/v0.6.1...v0.7.0
    93 173  [0.6.1]: https://github.com/mum4k/termdash/compare/v0.6.0...v0.6.1
    94 174  [0.6.0]: https://github.com/mum4k/termdash/compare/v0.5.0...v0.6.0
    95 175  [0.5.0]: https://github.com/mum4k/termdash/compare/v0.4.0...v0.5.0
    skipped 4 lines
  • ■ ■ ■ ■ ■ ■
    README.md
    1 1  [![Doc Status](https://godoc.org/github.com/mum4k/termdash?status.png)](https://godoc.org/github.com/mum4k/termdash)
    2 2  [![Build Status](https://travis-ci.org/mum4k/termdash.svg?branch=master)](https://travis-ci.org/mum4k/termdash)
     3 +[![Sourcegraph](https://sourcegraph.com/github.com/mum4k/termdash/-/badge.svg)](https://sourcegraph.com/github.com/mum4k/termdash?badge)
    3 4  [![Coverage Status](https://coveralls.io/repos/github/mum4k/termdash/badge.svg?branch=master)](https://coveralls.io/github/mum4k/termdash?branch=master)
    4 5  [![Go Report Card](https://goreportcard.com/badge/github.com/mum4k/termdash)](https://goreportcard.com/report/github.com/mum4k/termdash)
    5 6  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/mum4k/termdash/blob/master/LICENSE)
    skipped 1 lines
    7 8   
    8 9  # termdash
    9 10   
    10  -[<img src="./images/termdashdemo_0_6_0.gif" alt="termdashdemo" type="image/gif">](termdashdemo/termdashdemo.go)
     11 +[<img src="./doc/images/termdashdemo_0_7_0.gif" alt="termdashdemo" type="image/gif">](termdashdemo/termdashdemo.go)
    11 12   
    12  -This project implements a cross-platform customizable terminal based dashboard.
     13 +Termdash is a cross-platform customizable terminal based dashboard.
    13 14  The feature set is inspired by the
    14 15  [gizak/termui](http://github.com/gizak/termui) project, which in turn was
    15  -inspired by a javascript based
     16 +inspired by
    16 17  [yaronn/blessed-contrib](http://github.com/yaronn/blessed-contrib).
    17 18   
    18 19  This rewrite focuses on code readability, maintainability and testability, see
    skipped 1 lines
    20 21  [requirements](doc/requirements.md). See the [high-level design](doc/hld.md)
    21 22  for more details.
    22 23   
     24 +# Public API and status
     25 + 
     26 +The public API surface is documented in the
     27 +[wiki](http://github.com/mum4k/termdash/wiki).
     28 + 
     29 +Private packages can be identified by the presence of the **/internal/**
     30 +directory in their import path. Stability of the private packages isn't
     31 +guaranteed and changes won't be backward compatible.
     32 + 
     33 +There might still be breaking changes to the public API, at least until the
     34 +project reaches version 1.0.0. Any breaking changes will be published in the
     35 +[changelog](CHANGELOG.md).
     36 + 
    23 37  # Current feature set
    24 38   
    25 39  - Full support for terminal window resizing throughout the infrastructure.
    skipped 5 lines
    31 45  - UTF-8 for all text elements.
    32 46  - Drawing primitives (Go functions) for widget development with character and
    33 47   sub-character resolution.
    34  - 
    35  -See the [changelog](CHANGELOG.md) for more details.
    36 48   
    37 49  # Installation
    38 50   
    skipped 15 lines
    54 66   
    55 67  # Documentation
    56 68   
    57  -Code documentation can be viewed in
    58  -[godoc](https://godoc.org/github.com/mum4k/termdash).
     69 +Please refer to the [Termdash wiki](http://github.com/mum4k/termdash/wiki) for
     70 +all documentation and resources.
     71 + 
     72 +# Implemented Widgets
    59 73   
    60  -Project documentation is available in the [doc](doc/) directory.
     74 +## The Button
     75 + 
     76 +Allows users to interact with the application, each button press runs a callback function.
     77 +Run the
     78 +[buttondemo](widgets/button/buttondemo/buttondemo.go).
     79 + 
     80 +```go
     81 +go run github.com/mum4k/termdash/widgets/button/buttondemo/buttondemo.go
     82 +```
    61 83   
    62  -## Implemented Widgets
     84 +[<img src="./doc/images/buttondemo.gif" alt="buttondemo" type="image/gif" width="50%">](widgets/button/buttondemo/buttondemo.go)
    63 85   
    64  -### The Gauge
     86 +## The Gauge
    65 87   
    66 88  Displays the progress of an operation. Run the
    67 89  [gaugedemo](widgets/gauge/gaugedemo/gaugedemo.go).
    skipped 2 lines
    70 92  go run github.com/mum4k/termdash/widgets/gauge/gaugedemo/gaugedemo.go
    71 93  ```
    72 94   
    73  -[<img src="./images/gaugedemo.gif" alt="gaugedemo" type="image/gif">](widgets/gauge/gaugedemo/gaugedemo.go)
     95 +[<img src="./doc/images/gaugedemo.gif" alt="gaugedemo" type="image/gif">](widgets/gauge/gaugedemo/gaugedemo.go)
    74 96   
    75  -### The Donut
     97 +## The Donut
    76 98   
    77 99  Visualizes progress of an operation as a partial or a complete donut. Run the
    78 100  [donutdemo](widgets/donut/donutdemo/donutdemo.go).
    skipped 2 lines
    81 103  go run github.com/mum4k/termdash/widgets/donut/donutdemo/donutdemo.go
    82 104  ```
    83 105   
    84  -[<img src="./images/donutdemo.gif" alt="donutdemo" type="image/gif">](widgets/donut/donutdemo/donutdemo.go)
     106 +[<img src="./doc/images/donutdemo.gif" alt="donutdemo" type="image/gif">](widgets/donut/donutdemo/donutdemo.go)
    85 107   
    86  -### The Text
     108 +## The Text
    87 109   
    88 110  Displays text content, supports trimming and scrolling of content. Run the
    89 111  [textdemo](widgets/text/textdemo/textdemo.go).
    skipped 2 lines
    92 114  go run github.com/mum4k/termdash/widgets/text/textdemo/textdemo.go
    93 115  ```
    94 116   
    95  -[<img src="./images/textdemo.gif" alt="textdemo" type="image/gif">](widgets/text/textdemo/textdemo.go)
     117 +[<img src="./doc/images/textdemo.gif" alt="textdemo" type="image/gif">](widgets/text/textdemo/textdemo.go)
    96 118   
    97  -### The SparkLine
     119 +## The SparkLine
    98 120   
    99 121  Draws a graph showing a series of values as vertical bars. The bars can have
    100 122  sub-cell height. Run the
    skipped 3 lines
    104 126  go run github.com/mum4k/termdash/widgets/sparkline/sparklinedemo/sparklinedemo.go
    105 127  ```
    106 128   
    107  -[<img src="./images/sparklinedemo.gif" alt="sparklinedemo" type="image/gif" width="50%">](widgets/sparkline/sparklinedemo/sparklinedemo.go)
     129 +[<img src="./doc/images/sparklinedemo.gif" alt="sparklinedemo" type="image/gif" width="50%">](widgets/sparkline/sparklinedemo/sparklinedemo.go)
    108 130   
    109  -### The BarChart
     131 +## The BarChart
    110 132   
    111 133  Displays multiple bars showing relative ratios of values. Run the
    112 134  [barchartdemo](widgets/barchart/barchartdemo/barchartdemo.go).
    skipped 2 lines
    115 137  go run github.com/mum4k/termdash/widgets/barchart/barchartdemo/barchartdemo.go
    116 138  ```
    117 139   
    118  -[<img src="./images/barchartdemo.gif" alt="barchartdemo" type="image/gif" width="50%">](widgets/barchart/barchartdemo/barchartdemo.go)
     140 +[<img src="./doc/images/barchartdemo.gif" alt="barchartdemo" type="image/gif" width="50%">](widgets/barchart/barchartdemo/barchartdemo.go)
    119 141   
    120  -### The LineChart
     142 +## The LineChart
    121 143   
    122  -Displays series of values on a line chart. Run the
     144 +Displays series of values on a line chart, supports zoom triggered by mouse
     145 +events. Run the
    123 146  [linechartdemo](widgets/linechart/linechartdemo/linechartdemo.go).
    124 147   
    125 148  ```go
    126 149  go run github.com/mum4k/termdash/widgets/linechart/linechartdemo/linechartdemo.go
    127 150  ```
    128 151   
    129  -[<img src="./images/linechartdemo.gif" alt="linechartdemo" type="image/gif" width="70%">](widgets/linechart/linechartdemo/linechartdemo.go)
     152 +[<img src="./doc/images/linechartdemo.gif" alt="linechartdemo" type="image/gif" width="70%">](widgets/linechart/linechartdemo/linechartdemo.go)
    130 153   
    131  -### The SegmentDisplay
     154 +## The SegmentDisplay
    132 155   
    133 156  Displays text by simulating a 16-segment display. Run the
    134 157  [segmentdisplaydemo](widgets/segmentdisplay/segmentdisplaydemo/segmentdisplaydemo.go).
    skipped 2 lines
    137 160  go run github.com/mum4k/termdash/widgets/segmentdisplay/segmentdisplaydemo/segmentdisplaydemo.go
    138 161  ```
    139 162   
    140  -[<img src="./images/segmentdisplaydemo.gif" alt="segmentdisplaydemo" type="image/gif">](widgets/segmentdisplay/segmentdisplaydemo/segmentdisplaydemo.go)
     163 +[<img src="./doc/images/segmentdisplaydemo.gif" alt="segmentdisplaydemo" type="image/gif">](widgets/segmentdisplay/segmentdisplaydemo/segmentdisplaydemo.go)
    141 164   
    142 165  # Contributing
    143 166   
    skipped 13 lines
    157 180  development](doc/widget_development.md) section.
    158 181   
    159 182   
    160  -## Disclaimer
     183 +# Disclaimer
    161 184   
    162 185  This is not an official Google product.
    163 186   
  • ■ ■ ■ ■ ■ ■
    canvas/canvas_test.go
    1  -// Copyright 2018 Google Inc.
    2  -//
    3  -// Licensed under the Apache License, Version 2.0 (the "License");
    4  -// you may not use this file except in compliance with the License.
    5  -// You may obtain a copy of the License at
    6  -//
    7  -// http://www.apache.org/licenses/LICENSE-2.0
    8  -//
    9  -// Unless required by applicable law or agreed to in writing, software
    10  -// distributed under the License is distributed on an "AS IS" BASIS,
    11  -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  -// See the License for the specific language governing permissions and
    13  -// limitations under the License.
    14  - 
    15  -package canvas
    16  - 
    17  -import (
    18  - "image"
    19  - "testing"
    20  - 
    21  - "github.com/kylelemons/godebug/pretty"
    22  - "github.com/mum4k/termdash/area"
    23  - "github.com/mum4k/termdash/cell"
    24  - "github.com/mum4k/termdash/terminal/faketerm"
    25  -)
    26  - 
    27  -func TestNew(t *testing.T) {
    28  - tests := []struct {
    29  - desc string
    30  - area image.Rectangle
    31  - wantSize image.Point
    32  - wantArea image.Rectangle
    33  - wantErr bool
    34  - }{
    35  - {
    36  - desc: "area min has negative X",
    37  - area: image.Rect(-1, 0, 0, 0),
    38  - wantErr: true,
    39  - },
    40  - {
    41  - desc: "area min has negative Y",
    42  - area: image.Rect(0, -1, 0, 0),
    43  - wantErr: true,
    44  - },
    45  - {
    46  - desc: "area max has negative X",
    47  - area: image.Rect(0, 0, -1, 0),
    48  - wantErr: true,
    49  - },
    50  - {
    51  - desc: "area max has negative Y",
    52  - area: image.Rect(0, 0, 0, -1),
    53  - wantErr: true,
    54  - },
    55  - {
    56  - desc: "zero area is invalid",
    57  - area: image.Rect(0, 0, 0, 0),
    58  - wantErr: true,
    59  - },
    60  - {
    61  - desc: "smallest valid size",
    62  - area: image.Rect(0, 0, 1, 1),
    63  - wantSize: image.Point{1, 1},
    64  - wantArea: image.Rect(0, 0, 1, 1),
    65  - },
    66  - {
    67  - desc: "rectangular canvas 3 by 4",
    68  - area: image.Rect(0, 0, 3, 4),
    69  - wantSize: image.Point{3, 4},
    70  - wantArea: image.Rect(0, 0, 3, 4),
    71  - },
    72  - {
    73  - desc: "non-zero based area",
    74  - area: image.Rect(1, 1, 2, 3),
    75  - wantSize: image.Point{1, 2},
    76  - wantArea: image.Rect(0, 0, 1, 2),
    77  - },
    78  - }
    79  - 
    80  - for _, tc := range tests {
    81  - t.Run(tc.desc, func(t *testing.T) {
    82  - c, err := New(tc.area)
    83  - if (err != nil) != tc.wantErr {
    84  - t.Errorf("New => unexpected error: %v, wantErr: %v", err, tc.wantErr)
    85  - }
    86  - if err != nil {
    87  - return
    88  - }
    89  - 
    90  - gotSize := c.Size()
    91  - if diff := pretty.Compare(tc.wantSize, gotSize); diff != "" {
    92  - t.Errorf("Size => unexpected diff (-want, +got):\n%s", diff)
    93  - }
    94  - 
    95  - gotArea := c.Area()
    96  - if diff := pretty.Compare(tc.wantArea, gotArea); diff != "" {
    97  - t.Errorf("Area => unexpected diff (-want, +got):\n%s", diff)
    98  - }
    99  - })
    100  - }
    101  -}
    102  - 
    103  -func TestSetCellAndApply(t *testing.T) {
    104  - tests := []struct {
    105  - desc string
    106  - termSize image.Point
    107  - canvasArea image.Rectangle
    108  - point image.Point
    109  - r rune
    110  - opts []cell.Option
    111  - want cell.Buffer // Expected back buffer in the fake terminal.
    112  - wantCells int
    113  - wantSetCellErr bool
    114  - wantApplyErr bool
    115  - }{
    116  - {
    117  - desc: "setting cell outside the designated area",
    118  - termSize: image.Point{2, 2},
    119  - canvasArea: image.Rect(0, 0, 1, 1),
    120  - point: image.Point{0, 2},
    121  - wantSetCellErr: true,
    122  - },
    123  - {
    124  - desc: "sets a top-left corner cell",
    125  - termSize: image.Point{3, 3},
    126  - canvasArea: image.Rect(1, 1, 3, 3),
    127  - point: image.Point{0, 0},
    128  - r: 'X',
    129  - wantCells: 1,
    130  - want: cell.Buffer{
    131  - {
    132  - cell.New(0),
    133  - cell.New(0),
    134  - cell.New(0),
    135  - },
    136  - {
    137  - cell.New(0),
    138  - cell.New('X'),
    139  - cell.New(0),
    140  - },
    141  - {
    142  - cell.New(0),
    143  - cell.New(0),
    144  - cell.New(0),
    145  - },
    146  - },
    147  - },
    148  - {
    149  - desc: "sets a full-width rune in the top-left corner cell",
    150  - termSize: image.Point{3, 3},
    151  - canvasArea: image.Rect(1, 1, 3, 3),
    152  - point: image.Point{0, 0},
    153  - r: '界',
    154  - wantCells: 2,
    155  - want: cell.Buffer{
    156  - {
    157  - cell.New(0),
    158  - cell.New(0),
    159  - cell.New(0),
    160  - },
    161  - {
    162  - cell.New(0),
    163  - cell.New('界'),
    164  - cell.New(0),
    165  - },
    166  - {
    167  - cell.New(0),
    168  - cell.New(0),
    169  - cell.New(0),
    170  - },
    171  - },
    172  - },
    173  - {
    174  - desc: "not enough space for a full-width rune",
    175  - termSize: image.Point{3, 3},
    176  - canvasArea: image.Rect(1, 1, 3, 3),
    177  - point: image.Point{1, 0},
    178  - r: '界',
    179  - wantSetCellErr: true,
    180  - },
    181  - {
    182  - desc: "sets a top-right corner cell",
    183  - termSize: image.Point{3, 3},
    184  - canvasArea: image.Rect(1, 1, 3, 3),
    185  - point: image.Point{1, 0},
    186  - r: 'X',
    187  - wantCells: 1,
    188  - want: cell.Buffer{
    189  - {
    190  - cell.New(0),
    191  - cell.New(0),
    192  - cell.New(0),
    193  - },
    194  - {
    195  - cell.New(0),
    196  - cell.New(0),
    197  - cell.New(0),
    198  - },
    199  - {
    200  - cell.New(0),
    201  - cell.New('X'),
    202  - cell.New(0),
    203  - },
    204  - },
    205  - },
    206  - {
    207  - desc: "sets a bottom-left corner cell",
    208  - termSize: image.Point{3, 3},
    209  - canvasArea: image.Rect(1, 1, 3, 3),
    210  - point: image.Point{0, 1},
    211  - r: 'X',
    212  - wantCells: 1,
    213  - want: cell.Buffer{
    214  - {
    215  - cell.New(0),
    216  - cell.New(0),
    217  - cell.New(0),
    218  - },
    219  - {
    220  - cell.New(0),
    221  - cell.New(0),
    222  - cell.New('X'),
    223  - },
    224  - {
    225  - cell.New(0),
    226  - cell.New(0),
    227  - cell.New(0),
    228  - },
    229  - },
    230  - },
    231  - {
    232  - desc: "sets a bottom-right corner cell",
    233  - termSize: image.Point{3, 3},
    234  - canvasArea: image.Rect(1, 1, 3, 3),
    235  - point: image.Point{1, 1},
    236  - r: 'Z',
    237  - wantCells: 1,
    238  - want: cell.Buffer{
    239  - {
    240  - cell.New(0),
    241  - cell.New(0),
    242  - cell.New(0),
    243  - },
    244  - {
    245  - cell.New(0),
    246  - cell.New(0),
    247  - cell.New(0),
    248  - },
    249  - {
    250  - cell.New(0),
    251  - cell.New(0),
    252  - cell.New('Z'),
    253  - },
    254  - },
    255  - },
    256  - {
    257  - desc: "sets cell options",
    258  - termSize: image.Point{3, 3},
    259  - canvasArea: image.Rect(1, 1, 3, 3),
    260  - point: image.Point{1, 1},
    261  - r: 'A',
    262  - opts: []cell.Option{
    263  - cell.BgColor(cell.ColorRed),
    264  - },
    265  - wantCells: 1,
    266  - want: cell.Buffer{
    267  - {
    268  - cell.New(0),
    269  - cell.New(0),
    270  - cell.New(0),
    271  - },
    272  - {
    273  - cell.New(0),
    274  - cell.New(0),
    275  - cell.New(0),
    276  - },
    277  - {
    278  - cell.New(0),
    279  - cell.New(0),
    280  - cell.New('A', cell.BgColor(cell.ColorRed)),
    281  - },
    282  - },
    283  - },
    284  - {
    285  - desc: "canvas size equals terminal size",
    286  - termSize: image.Point{1, 1},
    287  - canvasArea: image.Rect(0, 0, 1, 1),
    288  - point: image.Point{0, 0},
    289  - r: 'A',
    290  - wantCells: 1,
    291  - want: cell.Buffer{
    292  - {
    293  - cell.New('A'),
    294  - },
    295  - },
    296  - },
    297  - {
    298  - desc: "terminal too small for the area",
    299  - termSize: image.Point{1, 1},
    300  - canvasArea: image.Rect(0, 0, 2, 2),
    301  - point: image.Point{0, 0},
    302  - r: 'A',
    303  - wantCells: 1,
    304  - wantApplyErr: true,
    305  - },
    306  - }
    307  - 
    308  - for _, tc := range tests {
    309  - t.Run(tc.desc, func(t *testing.T) {
    310  - c, err := New(tc.canvasArea)
    311  - if err != nil {
    312  - t.Fatalf("New => unexpected error: %v", err)
    313  - }
    314  - 
    315  - gotCells, err := c.SetCell(tc.point, tc.r, tc.opts...)
    316  - if (err != nil) != tc.wantSetCellErr {
    317  - t.Errorf("SetCell => unexpected error: %v, wantSetCellErr: %v", err, tc.wantSetCellErr)
    318  - }
    319  - if err != nil {
    320  - return
    321  - }
    322  - 
    323  - if gotCells != tc.wantCells {
    324  - t.Errorf("SetCell => unexpected number of cells %d, want %d", gotCells, tc.wantCells)
    325  - }
    326  - 
    327  - ft, err := faketerm.New(tc.termSize)
    328  - if err != nil {
    329  - t.Fatalf("faketerm.New => unexpected error: %v", err)
    330  - }
    331  - err = c.Apply(ft)
    332  - if (err != nil) != tc.wantApplyErr {
    333  - t.Errorf("Apply => unexpected error: %v, wantApplyErr: %v", err, tc.wantApplyErr)
    334  - }
    335  - if err != nil {
    336  - return
    337  - }
    338  - 
    339  - got := ft.BackBuffer()
    340  - if diff := pretty.Compare(tc.want, got); diff != "" {
    341  - t.Errorf("faketerm.BackBuffer => unexpected diff (-want, +got):\n%s", diff)
    342  - }
    343  - })
    344  - }
    345  -}
    346  - 
    347  -func TestClear(t *testing.T) {
    348  - c, err := New(image.Rect(1, 1, 3, 3))
    349  - if err != nil {
    350  - t.Fatalf("New => unexpected error: %v", err)
    351  - }
    352  - 
    353  - if _, err := c.SetCell(image.Point{0, 0}, 'X'); err != nil {
    354  - t.Fatalf("SetCell => unexpected error: %v", err)
    355  - }
    356  - 
    357  - ft, err := faketerm.New(image.Point{3, 3})
    358  - if err != nil {
    359  - t.Fatalf("faketerm.New => unexpected error: %v", err)
    360  - }
    361  - // Set one cell outside of the canvas on the terminal.
    362  - if err := ft.SetCell(image.Point{0, 0}, 'A'); err != nil {
    363  - t.Fatalf("faketerm.SetCell => unexpected error: %v", err)
    364  - }
    365  - 
    366  - if err := c.Apply(ft); err != nil {
    367  - t.Fatalf("Apply => unexpected error: %v", err)
    368  - }
    369  - 
    370  - want := cell.Buffer{
    371  - {
    372  - cell.New('A'),
    373  - cell.New(0),
    374  - cell.New(0),
    375  - },
    376  - {
    377  - cell.New(0),
    378  - cell.New('X'),
    379  - cell.New(0),
    380  - },
    381  - {
    382  - cell.New(0),
    383  - cell.New(0),
    384  - cell.New(0),
    385  - },
    386  - }
    387  - got := ft.BackBuffer()
    388  - if diff := pretty.Compare(want, got); diff != "" {
    389  - t.Errorf("faketerm.BackBuffer before Clear => unexpected diff (-want, +got):\n%s", diff)
    390  - }
    391  - 
    392  - // Call Clear(), Apply() and verify that only the area belonging to the
    393  - // canvas was cleared.
    394  - if err := c.Clear(); err != nil {
    395  - t.Fatalf("Clear => unexpected error: %v", err)
    396  - }
    397  - if err := c.Apply(ft); err != nil {
    398  - t.Fatalf("Apply => unexpected error: %v", err)
    399  - }
    400  - 
    401  - want = cell.Buffer{
    402  - {
    403  - cell.New('A'),
    404  - cell.New(0),
    405  - cell.New(0),
    406  - },
    407  - {
    408  - cell.New(0),
    409  - cell.New(0),
    410  - cell.New(0),
    411  - },
    412  - {
    413  - cell.New(0),
    414  - cell.New(0),
    415  - cell.New(0),
    416  - },
    417  - }
    418  - 
    419  - got = ft.BackBuffer()
    420  - if diff := pretty.Compare(want, got); diff != "" {
    421  - t.Errorf("faketerm.BackBuffer after Clear => unexpected diff (-want, +got):\n%s", diff)
    422  - }
    423  -}
    424  - 
    425  -// TestApplyFullWidthRunes verifies that when applying a full-width rune to the
    426  -// terminal, canvas doesn't touch the neighbor cell that holds the remaining
    427  -// part of the full-width rune.
    428  -func TestApplyFullWidthRunes(t *testing.T) {
    429  - ar := image.Rect(0, 0, 3, 3)
    430  - c, err := New(ar)
    431  - if err != nil {
    432  - t.Fatalf("New => unexpected error: %v", err)
    433  - }
    434  - 
    435  - fullP := image.Point{0, 0}
    436  - if _, err := c.SetCell(fullP, '界'); err != nil {
    437  - t.Fatalf("SetCell => unexpected error: %v", err)
    438  - }
    439  - 
    440  - ft, err := faketerm.New(area.Size(ar))
    441  - if err != nil {
    442  - t.Fatalf("faketerm.New => unexpected error: %v", err)
    443  - }
    444  - partP := image.Point{1, 0}
    445  - if err := ft.SetCell(partP, 'A'); err != nil {
    446  - t.Fatalf("faketerm.SetCell => unexpected error: %v", err)
    447  - }
    448  - 
    449  - if err := c.Apply(ft); err != nil {
    450  - t.Fatalf("Apply => unexpected error: %v", err)
    451  - }
    452  - 
    453  - want, err := cell.NewBuffer(area.Size(ar))
    454  - if err != nil {
    455  - t.Fatalf("NewBuffer => unexpected error: %v", err)
    456  - }
    457  - want[fullP.X][fullP.Y].Rune = '界'
    458  - want[partP.X][partP.Y].Rune = 'A'
    459  - 
    460  - got := ft.BackBuffer()
    461  - if diff := pretty.Compare(want, got); diff != "" {
    462  - t.Errorf("faketerm.BackBuffer => unexpected diff (-want, +got):\n%s", diff)
    463  - }
    464  -}
    465  - 
    466  -func TestCell(t *testing.T) {
    467  - tests := []struct {
    468  - desc string
    469  - cvs func() (*Canvas, error)
    470  - point image.Point
    471  - want *cell.Cell
    472  - wantErr bool
    473  - }{
    474  - {
    475  - desc: "requested point falls outside of the canvas",
    476  - cvs: func() (*Canvas, error) {
    477  - cvs, err := New(image.Rect(0, 0, 1, 1))
    478  - if err != nil {
    479  - return nil, err
    480  - }
    481  - return cvs, nil
    482  - },
    483  - point: image.Point{1, 1},
    484  - wantErr: true,
    485  - },
    486  - {
    487  - desc: "returns the cell",
    488  - cvs: func() (*Canvas, error) {
    489  - cvs, err := New(image.Rect(0, 0, 2, 2))
    490  - if err != nil {
    491  - return nil, err
    492  - }
    493  - if _, err := cvs.SetCell(
    494  - image.Point{1, 1}, 'A',
    495  - cell.FgColor(cell.ColorRed),
    496  - cell.BgColor(cell.ColorBlue),
    497  - ); err != nil {
    498  - return nil, err
    499  - }
    500  - return cvs, nil
    501  - },
    502  - point: image.Point{1, 1},
    503  - want: &cell.Cell{
    504  - Rune: 'A',
    505  - Opts: cell.NewOptions(
    506  - cell.FgColor(cell.ColorRed),
    507  - cell.BgColor(cell.ColorBlue),
    508  - ),
    509  - },
    510  - },
    511  - }
    512  - 
    513  - for _, tc := range tests {
    514  - t.Run(tc.desc, func(t *testing.T) {
    515  - cvs, err := tc.cvs()
    516  - if err != nil {
    517  - t.Fatalf("tc.cvs => unexpected error: %v", err)
    518  - }
    519  - 
    520  - got, err := cvs.Cell(tc.point)
    521  - if (err != nil) != tc.wantErr {
    522  - t.Errorf("Cell => unexpected error: %v, wantErr: %v", err, tc.wantErr)
    523  - }
    524  - if err != nil {
    525  - return
    526  - }
    527  - 
    528  - if diff := pretty.Compare(tc.want, got); diff != "" {
    529  - t.Errorf("Cell => unexpected diff (-want, +got):\n%s", diff)
    530  - }
    531  - })
    532  - }
    533  -}
    534  - 
    535  -// mustNew creates a new Canvas or panics.
    536  -func mustNew(ar image.Rectangle) *Canvas {
    537  - c, err := New(ar)
    538  - if err != nil {
    539  - panic(err)
    540  - }
    541  - return c
    542  -}
    543  - 
    544  -// mustFill fills the canvas with the specified runes or panics.
    545  -func mustFill(c *Canvas, r rune) {
    546  - ar := c.Area()
    547  - for col := 0; col < ar.Max.X; col++ {
    548  - for row := 0; row < ar.Max.Y; row++ {
    549  - if _, err := c.SetCell(image.Point{col, row}, r); err != nil {
    550  - panic(err)
    551  - }
    552  - }
    553  - }
    554  -}
    555  - 
    556  -// mustSetCell sets cell at the specified point of the canvas or panics.
    557  -func mustSetCell(c *Canvas, p image.Point, r rune, opts ...cell.Option) {
    558  - if _, err := c.SetCell(p, r, opts...); err != nil {
    559  - panic(err)
    560  - }
    561  -}
    562  - 
    563  -func TestCopyTo(t *testing.T) {
    564  - tests := []struct {
    565  - desc string
    566  - src *Canvas
    567  - dst *Canvas
    568  - want *Canvas
    569  - wantErr bool
    570  - }{
    571  - {
    572  - desc: "fails when the canvas doesn't fit",
    573  - src: func() *Canvas {
    574  - c := mustNew(image.Rect(0, 0, 3, 3))
    575  - mustFill(c, 'X')
    576  - return c
    577  - }(),
    578  - dst: mustNew(image.Rect(0, 0, 2, 2)),
    579  - want: mustNew(image.Rect(0, 0, 3, 3)),
    580  - wantErr: true,
    581  - },
    582  - {
    583  - desc: "fails when the area lies outside of the destination canvas",
    584  - src: func() *Canvas {
    585  - c := mustNew(image.Rect(3, 3, 4, 4))
    586  - mustFill(c, 'X')
    587  - return c
    588  - }(),
    589  - dst: mustNew(image.Rect(0, 0, 3, 3)),
    590  - want: mustNew(image.Rect(0, 0, 3, 3)),
    591  - wantErr: true,
    592  - },
    593  - {
    594  - desc: "copies zero based same size canvases",
    595  - src: func() *Canvas {
    596  - c := mustNew(image.Rect(0, 0, 3, 3))
    597  - mustFill(c, 'X')
    598  - return c
    599  - }(),
    600  - dst: mustNew(image.Rect(0, 0, 3, 3)),
    601  - want: func() *Canvas {
    602  - c := mustNew(image.Rect(0, 0, 3, 3))
    603  - mustSetCell(c, image.Point{0, 0}, 'X')
    604  - mustSetCell(c, image.Point{1, 0}, 'X')
    605  - mustSetCell(c, image.Point{2, 0}, 'X')
    606  - 
    607  - mustSetCell(c, image.Point{0, 1}, 'X')
    608  - mustSetCell(c, image.Point{1, 1}, 'X')
    609  - mustSetCell(c, image.Point{2, 1}, 'X')
    610  - 
    611  - mustSetCell(c, image.Point{0, 2}, 'X')
    612  - mustSetCell(c, image.Point{1, 2}, 'X')
    613  - mustSetCell(c, image.Point{2, 2}, 'X')
    614  - return c
    615  - }(),
    616  - },
    617  - {
    618  - desc: "copies smaller canvas with an offset",
    619  - src: func() *Canvas {
    620  - c := mustNew(image.Rect(1, 1, 2, 2))
    621  - mustFill(c, 'X')
    622  - return c
    623  - }(),
    624  - dst: mustNew(image.Rect(0, 0, 3, 3)),
    625  - want: func() *Canvas {
    626  - c := mustNew(image.Rect(0, 0, 3, 3))
    627  - mustSetCell(c, image.Point{1, 1}, 'X')
    628  - return c
    629  - }(),
    630  - },
    631  - {
    632  - desc: "copies smaller canvas with an offset into a canvas with offset from terminal",
    633  - src: func() *Canvas {
    634  - c := mustNew(image.Rect(1, 1, 2, 2))
    635  - mustFill(c, 'X')
    636  - return c
    637  - }(),
    638  - dst: mustNew(image.Rect(3, 3, 6, 6)),
    639  - want: func() *Canvas {
    640  - c := mustNew(image.Rect(3, 3, 6, 6))
    641  - mustSetCell(c, image.Point{1, 1}, 'X')
    642  - return c
    643  - }(),
    644  - },
    645  - {
    646  - desc: "copies cell options",
    647  - src: func() *Canvas {
    648  - c := mustNew(image.Rect(0, 0, 1, 1))
    649  - mustSetCell(c, image.Point{0, 0}, 'X',
    650  - cell.FgColor(cell.ColorRed),
    651  - cell.BgColor(cell.ColorBlue),
    652  - )
    653  - return c
    654  - }(),
    655  - dst: mustNew(image.Rect(0, 0, 3, 1)),
    656  - want: func() *Canvas {
    657  - c := mustNew(image.Rect(0, 0, 3, 1))
    658  - mustSetCell(c, image.Point{0, 0}, 'X',
    659  - cell.FgColor(cell.ColorRed),
    660  - cell.BgColor(cell.ColorBlue),
    661  - )
    662  - return c
    663  - }(),
    664  - },
    665  - {
    666  - desc: "copies cells with full-width runes",
    667  - src: func() *Canvas {
    668  - c := mustNew(image.Rect(0, 0, 3, 3))
    669  - mustSetCell(c, image.Point{0, 0}, '界')
    670  - mustSetCell(c, image.Point{1, 1}, '界')
    671  - return c
    672  - }(),
    673  - dst: mustNew(image.Rect(0, 0, 3, 3)),
    674  - want: func() *Canvas {
    675  - c := mustNew(image.Rect(0, 0, 3, 3))
    676  - mustSetCell(c, image.Point{0, 0}, '界')
    677  - mustSetCell(c, image.Point{1, 1}, '界')
    678  - return c
    679  - }(),
    680  - },
    681  - }
    682  - 
    683  - for _, tc := range tests {
    684  - t.Run(tc.desc, func(t *testing.T) {
    685  - err := tc.src.CopyTo(tc.dst)
    686  - if (err != nil) != tc.wantErr {
    687  - t.Errorf("CopyTo => unexpected error: %v, wantErr: %v", err, tc.wantErr)
    688  - }
    689  - if err != nil {
    690  - return
    691  - }
    692  - 
    693  - ftSize := image.Point{10, 10}
    694  - got, err := faketerm.New(ftSize)
    695  - if err != nil {
    696  - t.Fatalf("faketerm.New(tc.dst.Size()) => unexpected error: %v", err)
    697  - }
    698  - if err := tc.dst.Apply(got); err != nil {
    699  - t.Fatalf("tc.dst.Apply => unexpected error: %v", err)
    700  - }
    701  - 
    702  - want, err := faketerm.New(ftSize)
    703  - if err != nil {
    704  - t.Fatalf("faketerm.New(tc.want.Size()) => unexpected error: %v", err)
    705  - }
    706  - 
    707  - if err := tc.want.Apply(want); err != nil {
    708  - t.Fatalf("tc.want.Apply => unexpected error: %v", err)
    709  - }
    710  - 
    711  - if diff := faketerm.Diff(want, got); diff != "" {
    712  - t.Errorf("CopyTo => %v", diff)
    713  - }
    714  - })
    715  - }
    716  -}
    717  - 
  • ■ ■ ■ ■ ■
    container/container.go
    skipped 23 lines
    24 24  import (
    25 25   "fmt"
    26 26   "image"
     27 + "sync"
    27 28   
    28  - "github.com/mum4k/termdash/align"
    29  - "github.com/mum4k/termdash/area"
    30  - "github.com/mum4k/termdash/draw"
    31  - "github.com/mum4k/termdash/terminalapi"
     29 + "github.com/mum4k/termdash/internal/align"
     30 + "github.com/mum4k/termdash/internal/area"
     31 + "github.com/mum4k/termdash/internal/draw"
     32 + "github.com/mum4k/termdash/internal/event"
     33 + "github.com/mum4k/termdash/internal/terminalapi"
     34 + "github.com/mum4k/termdash/internal/widgetapi"
    32 35  )
    33 36   
    34 37  // Container wraps either sub containers or widgets and positions them on the
    35 38  // terminal.
    36  -// This is not thread-safe.
     39 +// This is thread-safe.
    37 40  type Container struct {
    38 41   // parent is the parent container, nil if this is the root container.
    39 42   parent *Container
    skipped 14 lines
    54 57   
    55 58   // opts are the options provided to the container.
    56 59   opts *options
     60 + 
     61 + // mu protects the container tree.
     62 + // All containers in the tree share the same lock.
     63 + mu *sync.Mutex
    57 64  }
    58 65   
    59 66  // String represents the container metadata in a human readable format.
    skipped 11 lines
    71 78   // The root container has access to the entire terminal.
    72 79   area: image.Rect(0, 0, size.X, size.Y),
    73 80   opts: newOptions( /* parent = */ nil),
     81 + mu: &sync.Mutex{},
    74 82   }
    75 83   
    76 84   // Initially the root is focused.
    skipped 12 lines
    89 97   focusTracker: parent.focusTracker,
    90 98   area: area,
    91 99   opts: newOptions(parent.opts),
     100 + mu: parent.mu,
    92 101   }
    93 102  }
    94 103   
    skipped 77 lines
    172 181   
    173 182  // Draw draws this container and all of its sub containers.
    174 183  func (c *Container) Draw() error {
     184 + c.mu.Lock()
     185 + defer c.mu.Unlock()
    175 186   return drawTree(c)
    176 187  }
    177 188   
    178  -// Keyboard is used to forward a keyboard event to the container.
    179  -// Keyboard events are forwarded to the widget in the currently focused
    180  -// container, assuming that the widget registered for keyboard events.
    181  -func (c *Container) Keyboard(k *terminalapi.Keyboard) error {
    182  - w := c.focusTracker.active().opts.widget
    183  - if w == nil || !w.Options().WantKeyboard {
     189 +// updateFocus processes the mouse event and determines if it changes the
     190 +// focused container.
     191 +func (c *Container) updateFocus(m *terminalapi.Mouse) {
     192 + c.mu.Lock()
     193 + defer c.mu.Unlock()
     194 + 
     195 + target := pointCont(c, m.Position)
     196 + if target == nil { // Ignore mouse clicks where no containers are.
     197 + return
     198 + }
     199 + c.focusTracker.mouse(target, m)
     200 +}
     201 + 
     202 +// keyboardToWidget forwards the keyboard event to the widget unconditionally.
     203 +func (c *Container) keyboardToWidget(k *terminalapi.Keyboard, scope widgetapi.KeyScope) error {
     204 + c.mu.Lock()
     205 + defer c.mu.Unlock()
     206 + 
     207 + if scope == widgetapi.KeyScopeFocused && !c.focusTracker.isActive(c) {
    184 208   return nil
    185 209   }
    186  - return w.Keyboard(k)
     210 + return c.opts.widget.Keyboard(k)
    187 211  }
    188 212   
    189  -// Mouse is used to forward a mouse event to the container.
    190  -// Container uses mouse events to track and change which is the active
    191  -// (focused) container.
    192  -//
    193  -// If the container that receives the mouse click contains a widget that
    194  -// registered for mouse events, the mouse event is further forwarded to that
    195  -// widget. Only mouse events that fall within the widget's canvas are forwarded
    196  -// and the coordinates are adjusted relative to the widget's canvas.
    197  -func (c *Container) Mouse(m *terminalapi.Mouse) error {
    198  - c.focusTracker.mouse(m)
     213 +// mouseToWidget forwards the mouse event to the widget.
     214 +func (c *Container) mouseToWidget(m *terminalapi.Mouse, scope widgetapi.MouseScope) error {
     215 + c.mu.Lock()
     216 + defer c.mu.Unlock()
    199 217   
    200 218   target := pointCont(c, m.Position)
    201 219   if target == nil { // Ignore mouse clicks where no containers are.
    202 220   return nil
    203 221   }
    204  - w := target.opts.widget
    205  - if w == nil || !w.Options().WantMouse {
    206  - return nil
    207  - }
    208 222   
    209 223   // Ignore clicks falling outside of the container.
    210  - if !m.Position.In(target.usable()) {
     224 + if scope != widgetapi.MouseScopeGlobal && !m.Position.In(c.area) {
    211 225   return nil
    212 226   }
    213 227   
    214 228   // Ignore clicks falling outside of the widget's canvas.
    215  - wa, err := target.widgetArea()
     229 + wa, err := c.widgetArea()
    216 230   if err != nil {
    217 231   return err
    218 232   }
    219  - if !m.Position.In(wa) {
     233 + if scope == widgetapi.MouseScopeWidget && !m.Position.In(wa) {
    220 234   return nil
    221 235   }
    222 236   
    skipped 1 lines
    224 238   // based, even though the widget might not be in the top left corner on the
    225 239   // terminal.
    226 240   offset := wa.Min
    227  - wm := &terminalapi.Mouse{
    228  - Position: m.Position.Sub(offset),
    229  - Button: m.Button,
     241 + var wm *terminalapi.Mouse
     242 + if m.Position.In(wa) {
     243 + wm = &terminalapi.Mouse{
     244 + Position: m.Position.Sub(offset),
     245 + Button: m.Button,
     246 + }
     247 + } else {
     248 + wm = &terminalapi.Mouse{
     249 + Position: image.Point{-1, -1},
     250 + Button: m.Button,
     251 + }
    230 252   }
    231  - return w.Mouse(wm)
     253 + return c.opts.widget.Mouse(wm)
     254 +}
     255 + 
     256 +// Subscribe tells the container to subscribe itself and widgets to the
     257 +// provided event distribution system.
     258 +// This method is private to termdash, stability isn't guaranteed and changes
     259 +// won't be backward compatible.
     260 +func (c *Container) Subscribe(eds *event.DistributionSystem) {
     261 + c.mu.Lock()
     262 + defer c.mu.Unlock()
     263 + 
     264 + // maxReps is the maximum number of repetitive events towards widgets
     265 + // before we throttle them.
     266 + const maxReps = 10
     267 + 
     268 + root := rootCont(c)
     269 + // Subscriber the container itself in order to track keyboard focus.
     270 + eds.Subscribe([]terminalapi.Event{&terminalapi.Mouse{}}, func(ev terminalapi.Event) {
     271 + root.updateFocus(ev.(*terminalapi.Mouse))
     272 + }, event.MaxRepetitive(0)) // One event is enough to change the focus.
     273 + 
     274 + // Subscribe any widgets that specify Keyboard or Mouse in their options.
     275 + var errStr string
     276 + preOrder(root, &errStr, visitFunc(func(c *Container) error {
     277 + if c.hasWidget() {
     278 + wOpt := c.opts.widget.Options()
     279 + switch scope := wOpt.WantKeyboard; scope {
     280 + case widgetapi.KeyScopeNone:
     281 + // Widget doesn't want any keyboard events.
     282 + 
     283 + default:
     284 + eds.Subscribe([]terminalapi.Event{&terminalapi.Keyboard{}}, func(ev terminalapi.Event) {
     285 + if err := c.keyboardToWidget(ev.(*terminalapi.Keyboard), scope); err != nil {
     286 + eds.Event(terminalapi.NewErrorf("failed to send global keyboard event %v to widget %T: %v", ev, c.opts.widget, err))
     287 + }
     288 + }, event.MaxRepetitive(maxReps))
     289 + }
     290 + 
     291 + switch scope := wOpt.WantMouse; scope {
     292 + case widgetapi.MouseScopeNone:
     293 + // Widget doesn't want any mouse events.
     294 + 
     295 + default:
     296 + eds.Subscribe([]terminalapi.Event{&terminalapi.Mouse{}}, func(ev terminalapi.Event) {
     297 + if err := c.mouseToWidget(ev.(*terminalapi.Mouse), scope); err != nil {
     298 + eds.Event(terminalapi.NewErrorf("failed to send mouse event %v to widget %T: %v", ev, c.opts.widget, err))
     299 + }
     300 + }, event.MaxRepetitive(maxReps))
     301 + }
     302 + }
     303 + return nil
     304 + }))
    232 305  }
    233 306   
  • ■ ■ ■ ■ ■
    container/container_test.go
    skipped 14 lines
    15 15  package container
    16 16   
    17 17  import (
     18 + "fmt"
    18 19   "image"
     20 + "sync"
    19 21   "testing"
     22 + "time"
    20 23   
    21  - "github.com/mum4k/termdash/align"
    22  - "github.com/mum4k/termdash/canvas/testcanvas"
    23  - "github.com/mum4k/termdash/cell"
    24  - "github.com/mum4k/termdash/draw"
    25  - "github.com/mum4k/termdash/draw/testdraw"
    26  - "github.com/mum4k/termdash/keyboard"
    27  - "github.com/mum4k/termdash/mouse"
    28  - "github.com/mum4k/termdash/terminal/faketerm"
    29  - "github.com/mum4k/termdash/terminalapi"
    30  - "github.com/mum4k/termdash/widgetapi"
     24 + "github.com/mum4k/termdash/internal/align"
     25 + "github.com/mum4k/termdash/internal/canvas/testcanvas"
     26 + "github.com/mum4k/termdash/internal/cell"
     27 + "github.com/mum4k/termdash/internal/draw"
     28 + "github.com/mum4k/termdash/internal/draw/testdraw"
     29 + "github.com/mum4k/termdash/internal/event"
     30 + "github.com/mum4k/termdash/internal/event/testevent"
     31 + "github.com/mum4k/termdash/internal/keyboard"
     32 + "github.com/mum4k/termdash/internal/mouse"
     33 + "github.com/mum4k/termdash/internal/terminal/faketerm"
     34 + "github.com/mum4k/termdash/internal/terminalapi"
     35 + "github.com/mum4k/termdash/internal/widgetapi"
    31 36   "github.com/mum4k/termdash/widgets/fakewidget"
    32 37  )
    33 38   
    skipped 489 lines
    523 528   
    524 529  }
    525 530   
     531 +// eventGroup is a group of events to be delivered with synchronization.
     532 +// I.e. the test execution waits until the specified number is processed before
     533 +// proceeding with test execution.
     534 +type eventGroup struct {
     535 + events []terminalapi.Event
     536 + wantProcessed int
     537 +}
     538 + 
     539 +// errorHandler just stores the last error received.
     540 +type errorHandler struct {
     541 + err error
     542 + mu sync.Mutex
     543 +}
     544 + 
     545 +func (eh *errorHandler) get() error {
     546 + eh.mu.Lock()
     547 + defer eh.mu.Unlock()
     548 + return eh.err
     549 +}
     550 + 
     551 +func (eh *errorHandler) handle(err error) {
     552 + eh.mu.Lock()
     553 + defer eh.mu.Unlock()
     554 + eh.err = err
     555 +}
     556 + 
    526 557  func TestKeyboard(t *testing.T) {
    527 558   tests := []struct {
    528  - desc string
    529  - termSize image.Point
    530  - container func(ft *faketerm.Terminal) (*Container, error)
    531  - events []terminalapi.Event
    532  - want func(size image.Point) *faketerm.Terminal
    533  - wantErr bool
     559 + desc string
     560 + termSize image.Point
     561 + container func(ft *faketerm.Terminal) (*Container, error)
     562 + eventGroups []*eventGroup
     563 + want func(size image.Point) *faketerm.Terminal
     564 + wantErr bool
    534 565   }{
    535 566   {
    536 567   desc: "event not forwarded if container has no widget",
    skipped 1 lines
    538 569   container: func(ft *faketerm.Terminal) (*Container, error) {
    539 570   return New(ft)
    540 571   },
    541  - events: []terminalapi.Event{
    542  - &terminalapi.Keyboard{Key: keyboard.KeyEnter},
     572 + eventGroups: []*eventGroup{
     573 + {
     574 + events: []terminalapi.Event{
     575 + &terminalapi.Keyboard{Key: keyboard.KeyEnter},
     576 + },
     577 + wantProcessed: 0,
     578 + },
    543 579   },
    544 580   want: func(size image.Point) *faketerm.Terminal {
    545 581   return faketerm.MustNew(size)
    skipped 7 lines
    553 589   ft,
    554 590   SplitVertical(
    555 591   Left(
    556  - PlaceWidget(fakewidget.New(widgetapi.Options{WantKeyboard: true})),
     592 + PlaceWidget(fakewidget.New(widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused})),
    557 593   ),
    558 594   Right(
    559 595   SplitHorizontal(
    560 596   Top(
    561  - PlaceWidget(fakewidget.New(widgetapi.Options{WantKeyboard: true})),
     597 + PlaceWidget(fakewidget.New(widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused})),
    562 598   ),
    563 599   Bottom(
    564  - PlaceWidget(fakewidget.New(widgetapi.Options{WantKeyboard: true})),
     600 + PlaceWidget(fakewidget.New(widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused})),
    565 601   ),
    566 602   ),
    567 603   ),
    568 604   ),
    569 605   )
    570 606   },
    571  - events: []terminalapi.Event{
    572  - &terminalapi.Mouse{Position: image.Point{39, 19}, Button: mouse.ButtonLeft},
    573  - &terminalapi.Mouse{Position: image.Point{39, 19}, Button: mouse.ButtonRelease},
    574  - &terminalapi.Keyboard{Key: keyboard.KeyEnter},
     607 + eventGroups: []*eventGroup{
     608 + // Move focus to the target container.
     609 + {
     610 + events: []terminalapi.Event{
     611 + &terminalapi.Mouse{Position: image.Point{39, 19}, Button: mouse.ButtonLeft},
     612 + &terminalapi.Mouse{Position: image.Point{39, 19}, Button: mouse.ButtonRelease},
     613 + },
     614 + wantProcessed: 2,
     615 + },
     616 + // Send the keyboard event.
     617 + {
     618 + events: []terminalapi.Event{
     619 + &terminalapi.Keyboard{Key: keyboard.KeyEnter},
     620 + },
     621 + wantProcessed: 5,
     622 + },
    575 623   },
     624 + 
    576 625   want: func(size image.Point) *faketerm.Terminal {
    577 626   ft := faketerm.MustNew(size)
    578 627   
    skipped 1 lines
    580 629   fakewidget.MustDraw(
    581 630   ft,
    582 631   testcanvas.MustNew(image.Rect(0, 0, 20, 20)),
    583  - widgetapi.Options{WantKeyboard: true},
     632 + widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused},
     633 + )
     634 + fakewidget.MustDraw(
     635 + ft,
     636 + testcanvas.MustNew(image.Rect(20, 0, 40, 10)),
     637 + widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused},
     638 + )
     639 + 
     640 + // The focused widget receives the key.
     641 + fakewidget.MustDraw(
     642 + ft,
     643 + testcanvas.MustNew(image.Rect(20, 10, 40, 20)),
     644 + widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused},
     645 + &terminalapi.Keyboard{Key: keyboard.KeyEnter},
     646 + )
     647 + return ft
     648 + },
     649 + },
     650 + {
     651 + desc: "event forwarded to all widgets that requested global key scope",
     652 + termSize: image.Point{40, 20},
     653 + container: func(ft *faketerm.Terminal) (*Container, error) {
     654 + return New(
     655 + ft,
     656 + SplitVertical(
     657 + Left(
     658 + PlaceWidget(fakewidget.New(widgetapi.Options{WantKeyboard: widgetapi.KeyScopeGlobal})),
     659 + ),
     660 + Right(
     661 + SplitHorizontal(
     662 + Top(
     663 + PlaceWidget(fakewidget.New(widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused})),
     664 + ),
     665 + Bottom(
     666 + PlaceWidget(fakewidget.New(widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused})),
     667 + ),
     668 + ),
     669 + ),
     670 + ),
     671 + )
     672 + },
     673 + eventGroups: []*eventGroup{
     674 + // Move focus to the target container.
     675 + {
     676 + events: []terminalapi.Event{
     677 + &terminalapi.Mouse{Position: image.Point{39, 19}, Button: mouse.ButtonLeft},
     678 + &terminalapi.Mouse{Position: image.Point{39, 19}, Button: mouse.ButtonRelease},
     679 + },
     680 + wantProcessed: 2,
     681 + },
     682 + // Send the keyboard event.
     683 + {
     684 + events: []terminalapi.Event{
     685 + &terminalapi.Keyboard{Key: keyboard.KeyEnter},
     686 + },
     687 + wantProcessed: 5,
     688 + },
     689 + },
     690 + 
     691 + want: func(size image.Point) *faketerm.Terminal {
     692 + ft := faketerm.MustNew(size)
     693 + 
     694 + // Widget that isn't focused, but registered for global
     695 + // keyboard events.
     696 + fakewidget.MustDraw(
     697 + ft,
     698 + testcanvas.MustNew(image.Rect(0, 0, 20, 20)),
     699 + widgetapi.Options{WantKeyboard: widgetapi.KeyScopeGlobal},
     700 + &terminalapi.Keyboard{Key: keyboard.KeyEnter},
    584 701   )
     702 + 
     703 + // Widget that isn't focused and only wants focused events.
    585 704   fakewidget.MustDraw(
    586 705   ft,
    587 706   testcanvas.MustNew(image.Rect(20, 0, 40, 10)),
    588  - widgetapi.Options{WantKeyboard: true},
     707 + widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused},
    589 708   )
    590 709   
    591 710   // The focused widget receives the key.
    592 711   fakewidget.MustDraw(
    593 712   ft,
    594 713   testcanvas.MustNew(image.Rect(20, 10, 40, 20)),
    595  - widgetapi.Options{WantKeyboard: true},
     714 + widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused},
    596 715   &terminalapi.Keyboard{Key: keyboard.KeyEnter},
    597 716   )
    598 717   return ft
    skipped 5 lines
    604 723   container: func(ft *faketerm.Terminal) (*Container, error) {
    605 724   return New(
    606 725   ft,
    607  - PlaceWidget(fakewidget.New(widgetapi.Options{WantKeyboard: false})),
     726 + PlaceWidget(fakewidget.New(widgetapi.Options{WantKeyboard: widgetapi.KeyScopeNone})),
    608 727   )
    609 728   },
    610  - events: []terminalapi.Event{
    611  - &terminalapi.Keyboard{Key: keyboard.KeyEnter},
     729 + eventGroups: []*eventGroup{
     730 + {
     731 + events: []terminalapi.Event{
     732 + &terminalapi.Keyboard{Key: keyboard.KeyEnter},
     733 + },
     734 + wantProcessed: 0,
     735 + },
    612 736   },
    613 737   want: func(size image.Point) *faketerm.Terminal {
    614 738   ft := faketerm.MustNew(size)
    skipped 12 lines
    627 751   container: func(ft *faketerm.Terminal) (*Container, error) {
    628 752   return New(
    629 753   ft,
    630  - PlaceWidget(fakewidget.New(widgetapi.Options{WantKeyboard: true})),
     754 + PlaceWidget(fakewidget.New(widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused})),
    631 755   )
    632 756   },
    633  - events: []terminalapi.Event{
    634  - &terminalapi.Keyboard{Key: keyboard.KeyEsc},
     757 + eventGroups: []*eventGroup{
     758 + {
     759 + events: []terminalapi.Event{
     760 + &terminalapi.Keyboard{Key: keyboard.KeyEsc},
     761 + },
     762 + wantProcessed: 2,
     763 + },
    635 764   },
    636 765   want: func(size image.Point) *faketerm.Terminal {
    637 766   ft := faketerm.MustNew(size)
    skipped 20 lines
    658 787   if err != nil {
    659 788   t.Fatalf("tc.container => unexpected error: %v", err)
    660 789   }
    661  - for _, ev := range tc.events {
    662  - switch e := ev.(type) {
    663  - case *terminalapi.Mouse:
    664  - if err := c.Mouse(e); err != nil {
    665  - t.Fatalf("Mouse => unexpected error: %v", err)
    666  - }
    667 790   
    668  - case *terminalapi.Keyboard:
    669  - err := c.Keyboard(e)
    670  - if (err != nil) != tc.wantErr {
    671  - t.Fatalf("Keyboard => unexpected error: %v, wantErr: %v", err, tc.wantErr)
    672  - }
     791 + eds := event.NewDistributionSystem()
     792 + eh := &errorHandler{}
     793 + // Subscribe to receive errors.
     794 + eds.Subscribe([]terminalapi.Event{terminalapi.NewError("")}, func(ev terminalapi.Event) {
     795 + eh.handle(ev.(*terminalapi.Error).Error())
     796 + })
    673 797   
    674  - default:
    675  - t.Fatalf("Unsupported event %T.", e)
     798 + c.Subscribe(eds)
     799 + for _, eg := range tc.eventGroups {
     800 + for _, ev := range eg.events {
     801 + eds.Event(ev)
     802 + }
     803 + if err := testevent.WaitFor(5*time.Second, func() error {
     804 + if got, want := eds.Processed(), eg.wantProcessed; got != want {
     805 + return fmt.Errorf("the event distribution system processed %d events, want %d", got, want)
     806 + }
     807 + return nil
     808 + }); err != nil {
     809 + t.Fatalf("testevent.WaitFor => %v", err)
    676 810   }
    677 811   }
    678 812   
    skipped 3 lines
    682 816   
    683 817   if diff := faketerm.Diff(tc.want(tc.termSize), got); diff != "" {
    684 818   t.Errorf("Draw => %v", diff)
     819 + }
     820 + 
     821 + if err := eh.get(); (err != nil) != tc.wantErr {
     822 + t.Errorf("errorHandler => unexpected error %v, wantErr: %v", err, tc.wantErr)
    685 823   }
    686 824   })
    687 825   }
    skipped 1 lines
    689 827   
    690 828  func TestMouse(t *testing.T) {
    691 829   tests := []struct {
    692  - desc string
    693  - termSize image.Point
    694  - container func(ft *faketerm.Terminal) (*Container, error)
    695  - events []terminalapi.Event
    696  - want func(size image.Point) *faketerm.Terminal
    697  - wantErr bool
     830 + desc string
     831 + termSize image.Point
     832 + container func(ft *faketerm.Terminal) (*Container, error)
     833 + events []terminalapi.Event
     834 + want func(size image.Point) *faketerm.Terminal
     835 + wantProcessed int
     836 + wantErr bool
    698 837   }{
    699 838   {
    700 839   desc: "mouse click outside of the terminal is ignored",
    skipped 1 lines
    702 841   container: func(ft *faketerm.Terminal) (*Container, error) {
    703 842   return New(
    704 843   ft,
    705  - PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: true})),
     844 + PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget})),
    706 845   )
    707 846   },
    708 847   events: []terminalapi.Event{
    skipped 10 lines
    719 858   )
    720 859   return ft
    721 860   },
     861 + wantProcessed: 4,
    722 862   },
    723 863   {
    724 864   desc: "event not forwarded if container has no widget",
    skipped 8 lines
    733 873   want: func(size image.Point) *faketerm.Terminal {
    734 874   return faketerm.MustNew(size)
    735 875   },
     876 + wantProcessed: 2,
    736 877   },
    737 878   {
    738 879   desc: "event forwarded to container at that point",
    skipped 3 lines
    742 883   ft,
    743 884   SplitVertical(
    744 885   Left(
    745  - PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: true})),
     886 + PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget})),
    746 887   ),
    747 888   Right(
    748 889   SplitHorizontal(
    749 890   Top(
    750  - PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: true})),
     891 + PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget})),
    751 892   ),
    752 893   Bottom(
    753  - PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: true})),
     894 + PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget})),
    754 895   ),
    755 896   ),
    756 897   ),
    skipped 15 lines
    772 913   fakewidget.MustDraw(
    773 914   ft,
    774 915   testcanvas.MustNew(image.Rect(25, 10, 50, 20)),
    775  - widgetapi.Options{WantMouse: true},
     916 + widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget},
    776 917   &terminalapi.Keyboard{},
    777 918   )
    778 919   
    skipped 1 lines
    780 921   fakewidget.MustDraw(
    781 922   ft,
    782 923   testcanvas.MustNew(image.Rect(25, 0, 50, 10)),
    783  - widgetapi.Options{WantMouse: true},
     924 + widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget},
    784 925   &terminalapi.Mouse{Position: image.Point{24, 9}, Button: mouse.ButtonLeft},
    785 926   &terminalapi.Mouse{Position: image.Point{24, 9}, Button: mouse.ButtonRelease},
    786 927   )
    787 928   return ft
    788 929   },
     930 + wantProcessed: 8,
    789 931   },
    790 932   {
    791 933   desc: "event not forwarded if the widget didn't request it",
    skipped 1 lines
    793 935   container: func(ft *faketerm.Terminal) (*Container, error) {
    794 936   return New(
    795 937   ft,
    796  - PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: false})),
     938 + PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: widgetapi.MouseScopeNone})),
    797 939   )
    798 940   },
    799 941   events: []terminalapi.Event{
    skipped 9 lines
    809 951   )
    810 952   return ft
    811 953   },
     954 + wantProcessed: 1,
    812 955   },
    813 956   {
    814  - desc: "event not forwarded if it falls on the container's border",
     957 + desc: "MouseScopeWidget, event not forwarded if it falls on the container's border",
    815 958   termSize: image.Point{20, 20},
    816 959   container: func(ft *faketerm.Terminal) (*Container, error) {
    817 960   return New(
    818 961   ft,
    819 962   Border(draw.LineStyleLight),
    820 963   PlaceWidget(
    821  - fakewidget.New(widgetapi.Options{WantMouse: true}),
     964 + fakewidget.New(widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget}),
    822 965   ),
    823 966   )
    824 967   },
    skipped 18 lines
    843 986   )
    844 987   return ft
    845 988   },
     989 + wantProcessed: 2,
    846 990   },
    847 991   {
    848  - desc: "event not forwarded if it falls outside of widget's canvas",
     992 + desc: "MouseScopeContainer, event forwarded if it falls on the container's border",
     993 + termSize: image.Point{21, 20},
     994 + container: func(ft *faketerm.Terminal) (*Container, error) {
     995 + return New(
     996 + ft,
     997 + Border(draw.LineStyleLight),
     998 + PlaceWidget(
     999 + fakewidget.New(widgetapi.Options{WantMouse: widgetapi.MouseScopeContainer}),
     1000 + ),
     1001 + )
     1002 + },
     1003 + events: []terminalapi.Event{
     1004 + &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
     1005 + },
     1006 + want: func(size image.Point) *faketerm.Terminal {
     1007 + ft := faketerm.MustNew(size)
     1008 + 
     1009 + cvs := testcanvas.MustNew(ft.Area())
     1010 + testdraw.MustBorder(
     1011 + cvs,
     1012 + ft.Area(),
     1013 + draw.BorderCellOpts(cell.FgColor(cell.ColorYellow)),
     1014 + )
     1015 + testcanvas.MustApply(cvs, ft)
     1016 + 
     1017 + fakewidget.MustDraw(
     1018 + ft,
     1019 + testcanvas.MustNew(image.Rect(1, 1, 20, 19)),
     1020 + widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget},
     1021 + &terminalapi.Mouse{Position: image.Point{-1, -1}, Button: mouse.ButtonLeft},
     1022 + )
     1023 + return ft
     1024 + },
     1025 + wantProcessed: 2,
     1026 + },
     1027 + {
     1028 + desc: "MouseScopeGlobal, event forwarded if it falls on the container's border",
     1029 + termSize: image.Point{21, 20},
     1030 + container: func(ft *faketerm.Terminal) (*Container, error) {
     1031 + return New(
     1032 + ft,
     1033 + Border(draw.LineStyleLight),
     1034 + PlaceWidget(
     1035 + fakewidget.New(widgetapi.Options{WantMouse: widgetapi.MouseScopeGlobal}),
     1036 + ),
     1037 + )
     1038 + },
     1039 + events: []terminalapi.Event{
     1040 + &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
     1041 + },
     1042 + want: func(size image.Point) *faketerm.Terminal {
     1043 + ft := faketerm.MustNew(size)
     1044 + 
     1045 + cvs := testcanvas.MustNew(ft.Area())
     1046 + testdraw.MustBorder(
     1047 + cvs,
     1048 + ft.Area(),
     1049 + draw.BorderCellOpts(cell.FgColor(cell.ColorYellow)),
     1050 + )
     1051 + testcanvas.MustApply(cvs, ft)
     1052 + 
     1053 + fakewidget.MustDraw(
     1054 + ft,
     1055 + testcanvas.MustNew(image.Rect(1, 1, 20, 19)),
     1056 + widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget},
     1057 + &terminalapi.Mouse{Position: image.Point{-1, -1}, Button: mouse.ButtonLeft},
     1058 + )
     1059 + return ft
     1060 + },
     1061 + wantProcessed: 2,
     1062 + },
     1063 + {
     1064 + desc: "MouseScopeWidget event not forwarded if it falls outside of widget's canvas",
    849 1065   termSize: image.Point{20, 20},
    850 1066   container: func(ft *faketerm.Terminal) (*Container, error) {
    851 1067   return New(
    852 1068   ft,
    853 1069   PlaceWidget(
    854 1070   fakewidget.New(widgetapi.Options{
    855  - WantMouse: true,
     1071 + WantMouse: widgetapi.MouseScopeWidget,
    856 1072   Ratio: image.Point{2, 1},
    857 1073   }),
    858 1074   ),
    skipped 14 lines
    873 1089   )
    874 1090   return ft
    875 1091   },
     1092 + wantProcessed: 2,
    876 1093   },
    877 1094   {
    878  - desc: "mouse poisition adjusted relative to widget's canvas, vertical offset",
     1095 + desc: "MouseScopeContainer event forwarded if it falls outside of widget's canvas",
    879 1096   termSize: image.Point{20, 20},
    880 1097   container: func(ft *faketerm.Terminal) (*Container, error) {
    881 1098   return New(
    882 1099   ft,
    883 1100   PlaceWidget(
    884 1101   fakewidget.New(widgetapi.Options{
    885  - WantMouse: true,
     1102 + WantMouse: widgetapi.MouseScopeContainer,
     1103 + Ratio: image.Point{2, 1},
     1104 + }),
     1105 + ),
     1106 + AlignVertical(align.VerticalMiddle),
     1107 + AlignHorizontal(align.HorizontalCenter),
     1108 + )
     1109 + },
     1110 + events: []terminalapi.Event{
     1111 + &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
     1112 + },
     1113 + want: func(size image.Point) *faketerm.Terminal {
     1114 + ft := faketerm.MustNew(size)
     1115 + 
     1116 + fakewidget.MustDraw(
     1117 + ft,
     1118 + testcanvas.MustNew(image.Rect(0, 5, 20, 15)),
     1119 + widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget},
     1120 + &terminalapi.Mouse{Position: image.Point{-1, -1}, Button: mouse.ButtonLeft},
     1121 + )
     1122 + return ft
     1123 + },
     1124 + wantProcessed: 2,
     1125 + },
     1126 + {
     1127 + desc: "MouseScopeGlobal event forwarded if it falls outside of widget's canvas",
     1128 + termSize: image.Point{20, 20},
     1129 + container: func(ft *faketerm.Terminal) (*Container, error) {
     1130 + return New(
     1131 + ft,
     1132 + PlaceWidget(
     1133 + fakewidget.New(widgetapi.Options{
     1134 + WantMouse: widgetapi.MouseScopeGlobal,
     1135 + Ratio: image.Point{2, 1},
     1136 + }),
     1137 + ),
     1138 + AlignVertical(align.VerticalMiddle),
     1139 + AlignHorizontal(align.HorizontalCenter),
     1140 + )
     1141 + },
     1142 + events: []terminalapi.Event{
     1143 + &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
     1144 + },
     1145 + want: func(size image.Point) *faketerm.Terminal {
     1146 + ft := faketerm.MustNew(size)
     1147 + 
     1148 + fakewidget.MustDraw(
     1149 + ft,
     1150 + testcanvas.MustNew(image.Rect(0, 5, 20, 15)),
     1151 + widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget},
     1152 + &terminalapi.Mouse{Position: image.Point{-1, -1}, Button: mouse.ButtonLeft},
     1153 + )
     1154 + return ft
     1155 + },
     1156 + wantProcessed: 2,
     1157 + },
     1158 + {
     1159 + desc: "MouseScopeWidget event not forwarded if it falls to another container",
     1160 + termSize: image.Point{20, 20},
     1161 + container: func(ft *faketerm.Terminal) (*Container, error) {
     1162 + return New(
     1163 + ft,
     1164 + SplitHorizontal(
     1165 + Top(),
     1166 + Bottom(
     1167 + PlaceWidget(
     1168 + fakewidget.New(widgetapi.Options{
     1169 + WantMouse: widgetapi.MouseScopeWidget,
     1170 + Ratio: image.Point{2, 1},
     1171 + }),
     1172 + ),
     1173 + AlignVertical(align.VerticalMiddle),
     1174 + AlignHorizontal(align.HorizontalCenter),
     1175 + ),
     1176 + ),
     1177 + )
     1178 + },
     1179 + events: []terminalapi.Event{
     1180 + &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
     1181 + },
     1182 + want: func(size image.Point) *faketerm.Terminal {
     1183 + ft := faketerm.MustNew(size)
     1184 + 
     1185 + fakewidget.MustDraw(
     1186 + ft,
     1187 + testcanvas.MustNew(image.Rect(0, 10, 20, 20)),
     1188 + widgetapi.Options{},
     1189 + )
     1190 + return ft
     1191 + },
     1192 + wantProcessed: 2,
     1193 + },
     1194 + {
     1195 + desc: "MouseScopeContainer event not forwarded if it falls to another container",
     1196 + termSize: image.Point{20, 20},
     1197 + container: func(ft *faketerm.Terminal) (*Container, error) {
     1198 + return New(
     1199 + ft,
     1200 + SplitHorizontal(
     1201 + Top(),
     1202 + Bottom(
     1203 + PlaceWidget(
     1204 + fakewidget.New(widgetapi.Options{
     1205 + WantMouse: widgetapi.MouseScopeContainer,
     1206 + Ratio: image.Point{2, 1},
     1207 + }),
     1208 + ),
     1209 + AlignVertical(align.VerticalMiddle),
     1210 + AlignHorizontal(align.HorizontalCenter),
     1211 + ),
     1212 + ),
     1213 + )
     1214 + },
     1215 + events: []terminalapi.Event{
     1216 + &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
     1217 + },
     1218 + want: func(size image.Point) *faketerm.Terminal {
     1219 + ft := faketerm.MustNew(size)
     1220 + 
     1221 + fakewidget.MustDraw(
     1222 + ft,
     1223 + testcanvas.MustNew(image.Rect(0, 10, 20, 20)),
     1224 + widgetapi.Options{},
     1225 + )
     1226 + return ft
     1227 + },
     1228 + wantProcessed: 2,
     1229 + },
     1230 + {
     1231 + desc: "MouseScopeGlobal event forwarded if it falls to another container",
     1232 + termSize: image.Point{20, 20},
     1233 + container: func(ft *faketerm.Terminal) (*Container, error) {
     1234 + return New(
     1235 + ft,
     1236 + SplitHorizontal(
     1237 + Top(),
     1238 + Bottom(
     1239 + PlaceWidget(
     1240 + fakewidget.New(widgetapi.Options{
     1241 + WantMouse: widgetapi.MouseScopeGlobal,
     1242 + Ratio: image.Point{2, 1},
     1243 + }),
     1244 + ),
     1245 + AlignVertical(align.VerticalMiddle),
     1246 + AlignHorizontal(align.HorizontalCenter),
     1247 + ),
     1248 + ),
     1249 + )
     1250 + },
     1251 + events: []terminalapi.Event{
     1252 + &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
     1253 + },
     1254 + want: func(size image.Point) *faketerm.Terminal {
     1255 + ft := faketerm.MustNew(size)
     1256 + 
     1257 + fakewidget.MustDraw(
     1258 + ft,
     1259 + testcanvas.MustNew(image.Rect(0, 10, 20, 20)),
     1260 + widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget},
     1261 + &terminalapi.Mouse{Position: image.Point{-1, -1}, Button: mouse.ButtonLeft},
     1262 + )
     1263 + return ft
     1264 + },
     1265 + wantProcessed: 2,
     1266 + },
     1267 + {
     1268 + desc: "mouse position adjusted relative to widget's canvas, vertical offset",
     1269 + termSize: image.Point{20, 20},
     1270 + container: func(ft *faketerm.Terminal) (*Container, error) {
     1271 + return New(
     1272 + ft,
     1273 + PlaceWidget(
     1274 + fakewidget.New(widgetapi.Options{
     1275 + WantMouse: widgetapi.MouseScopeWidget,
    886 1276   Ratio: image.Point{2, 1},
    887 1277   }),
    888 1278   ),
    skipped 10 lines
    899 1289   fakewidget.MustDraw(
    900 1290   ft,
    901 1291   testcanvas.MustNew(image.Rect(0, 5, 20, 15)),
    902  - widgetapi.Options{WantMouse: true},
     1292 + widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget},
    903 1293   &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
    904 1294   )
    905 1295   return ft
    906 1296   },
     1297 + wantProcessed: 2,
    907 1298   },
    908 1299   {
    909 1300   desc: "mouse poisition adjusted relative to widget's canvas, horizontal offset",
    skipped 3 lines
    913 1304   ft,
    914 1305   PlaceWidget(
    915 1306   fakewidget.New(widgetapi.Options{
    916  - WantMouse: true,
     1307 + WantMouse: widgetapi.MouseScopeWidget,
    917 1308   Ratio: image.Point{9, 10},
    918 1309   }),
    919 1310   ),
    skipped 10 lines
    930 1321   fakewidget.MustDraw(
    931 1322   ft,
    932 1323   testcanvas.MustNew(image.Rect(6, 0, 24, 20)),
    933  - widgetapi.Options{WantMouse: true},
     1324 + widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget},
    934 1325   &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
    935 1326   )
    936 1327   return ft
    937 1328   },
     1329 + wantProcessed: 2,
    938 1330   },
    939 1331   {
    940 1332   desc: "widget returns an error when processing the event",
    skipped 1 lines
    942 1334   container: func(ft *faketerm.Terminal) (*Container, error) {
    943 1335   return New(
    944 1336   ft,
    945  - PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: true})),
     1337 + PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget})),
    946 1338   )
    947 1339   },
    948 1340   events: []terminalapi.Event{
    skipped 9 lines
    958 1350   )
    959 1351   return ft
    960 1352   },
    961  - wantErr: true,
     1353 + wantProcessed: 3,
     1354 + wantErr: true,
    962 1355   },
    963 1356   }
    964 1357   
    skipped 8 lines
    973 1366   if err != nil {
    974 1367   t.Fatalf("tc.container => unexpected error: %v", err)
    975 1368   }
     1369 + 
     1370 + eds := event.NewDistributionSystem()
     1371 + eh := &errorHandler{}
     1372 + // Subscribe to receive errors.
     1373 + eds.Subscribe([]terminalapi.Event{terminalapi.NewError("")}, func(ev terminalapi.Event) {
     1374 + eh.handle(ev.(*terminalapi.Error).Error())
     1375 + })
     1376 + c.Subscribe(eds)
    976 1377   for _, ev := range tc.events {
    977  - switch e := ev.(type) {
    978  - case *terminalapi.Mouse:
    979  - err := c.Mouse(e)
    980  - if (err != nil) != tc.wantErr {
    981  - t.Fatalf("Mouse => unexpected error: %v, wantErr: %v", err, tc.wantErr)
    982  - }
    983  - 
    984  - case *terminalapi.Keyboard:
    985  - if err := c.Keyboard(e); err != nil {
    986  - t.Fatalf("Keyboard => unexpected error: %v", err)
    987  - }
    988  - 
    989  - default:
    990  - t.Fatalf("Unsupported event %T.", e)
     1378 + eds.Event(ev)
     1379 + }
     1380 + if err := testevent.WaitFor(5*time.Second, func() error {
     1381 + if got, want := eds.Processed(), tc.wantProcessed; got != want {
     1382 + return fmt.Errorf("the event distribution system processed %d events, want %d", got, want)
    991 1383   }
     1384 + return nil
     1385 + }); err != nil {
     1386 + t.Fatalf("testevent.WaitFor => %v", err)
    992 1387   }
    993 1388   
    994 1389   if err := c.Draw(); err != nil {
    skipped 2 lines
    997 1392   
    998 1393   if diff := faketerm.Diff(tc.want(tc.termSize), got); diff != "" {
    999 1394   t.Errorf("Draw => %v", diff)
     1395 + }
     1396 + 
     1397 + if err := eh.get(); (err != nil) != tc.wantErr {
     1398 + t.Errorf("errorHandler => unexpected error %v, wantErr: %v", err, tc.wantErr)
    1000 1399   }
    1001 1400   })
    1002 1401   }
    skipped 2 lines
  • ■ ■ ■ ■ ■ ■
    container/draw.go
    skipped 20 lines
    21 21   "fmt"
    22 22   "image"
    23 23   
    24  - "github.com/mum4k/termdash/area"
    25  - "github.com/mum4k/termdash/canvas"
    26  - "github.com/mum4k/termdash/cell"
    27  - "github.com/mum4k/termdash/draw"
     24 + "github.com/mum4k/termdash/internal/area"
     25 + "github.com/mum4k/termdash/internal/canvas"
     26 + "github.com/mum4k/termdash/internal/cell"
     27 + "github.com/mum4k/termdash/internal/draw"
    28 28  )
    29 29   
    30 30  // drawTree draws this container and all of its sub containers.
    skipped 129 lines
  • ■ ■ ■ ■ ■ ■
    container/draw_test.go
    skipped 17 lines
    18 18   "image"
    19 19   "testing"
    20 20   
    21  - "github.com/mum4k/termdash/align"
    22  - "github.com/mum4k/termdash/canvas/testcanvas"
    23  - "github.com/mum4k/termdash/cell"
    24  - "github.com/mum4k/termdash/draw"
    25  - "github.com/mum4k/termdash/draw/testdraw"
    26  - "github.com/mum4k/termdash/terminal/faketerm"
    27  - "github.com/mum4k/termdash/widgetapi"
     21 + "github.com/mum4k/termdash/internal/align"
     22 + "github.com/mum4k/termdash/internal/canvas/testcanvas"
     23 + "github.com/mum4k/termdash/internal/cell"
     24 + "github.com/mum4k/termdash/internal/draw"
     25 + "github.com/mum4k/termdash/internal/draw/testdraw"
     26 + "github.com/mum4k/termdash/internal/terminal/faketerm"
     27 + "github.com/mum4k/termdash/internal/widgetapi"
    28 28   "github.com/mum4k/termdash/widgets/fakewidget"
    29 29  )
    30 30   
    skipped 764 lines
  • ■ ■ ■ ■ ■
    container/focus.go
    skipped 18 lines
    19 19  import (
    20 20   "image"
    21 21   
    22  - "github.com/mum4k/termdash/terminalapi"
     22 + "github.com/mum4k/termdash/internal/mouse"
     23 + "github.com/mum4k/termdash/internal/mouse/button"
     24 + "github.com/mum4k/termdash/internal/terminalapi"
    23 25  )
    24 26   
    25 27  // pointCont finds the top-most (on the screen) container whose area contains
    skipped 24 lines
    50 52   // a mouse click and now waiting for a release or a timeout.
    51 53   candidate *Container
    52 54   
    53  - // mouseFSM is a state machine tracking mouse clicks in containers and
     55 + // buttonFSM is a state machine tracking mouse clicks in containers and
    54 56   // moving focus from one container to the next.
    55  - mouseFSM mouseStateFn
     57 + buttonFSM *button.FSM
    56 58  }
    57 59   
    58 60  // newFocusTracker returns a new focus tracker with focus set at the provided
    skipped 1 lines
    60 62  func newFocusTracker(c *Container) *focusTracker {
    61 63   return &focusTracker{
    62 64   container: c,
    63  - mouseFSM: mouseWantLeftButton,
     65 + // Mouse FSM tracking clicks inside the entire area for the root
     66 + // container.
     67 + buttonFSM: button.NewFSM(mouse.ButtonLeft, c.area),
    64 68   }
    65 69  }
    66 70   
    skipped 9 lines
    76 80   
    77 81  // mouse identifies mouse events that change the focused container and track
    78 82  // the focused container in the tree.
    79  -func (ft *focusTracker) mouse(m *terminalapi.Mouse) {
    80  - ft.mouseFSM = ft.mouseFSM(ft, m)
     83 +// The argument c is the container onto which the mouse event landed.
     84 +func (ft *focusTracker) mouse(target *Container, m *terminalapi.Mouse) {
     85 + clicked, bs := ft.buttonFSM.Event(m)
     86 + switch {
     87 + case bs == button.Down:
     88 + ft.candidate = target
     89 + case bs == button.Up && clicked:
     90 + if target == ft.candidate {
     91 + ft.container = target
     92 + }
     93 + }
    81 94  }
    82 95   
  • ■ ■ ■ ■ ■ ■
    container/focus_test.go
    skipped 14 lines
    15 15  package container
    16 16   
    17 17  import (
     18 + "fmt"
    18 19   "image"
    19 20   "testing"
     21 + "time"
    20 22   
    21  - "github.com/mum4k/termdash/cell"
    22  - "github.com/mum4k/termdash/draw"
    23  - "github.com/mum4k/termdash/mouse"
    24  - "github.com/mum4k/termdash/terminal/faketerm"
    25  - "github.com/mum4k/termdash/terminalapi"
     23 + "github.com/mum4k/termdash/internal/cell"
     24 + "github.com/mum4k/termdash/internal/draw"
     25 + "github.com/mum4k/termdash/internal/event"
     26 + "github.com/mum4k/termdash/internal/event/testevent"
     27 + "github.com/mum4k/termdash/internal/mouse"
     28 + "github.com/mum4k/termdash/internal/terminal/faketerm"
     29 + "github.com/mum4k/termdash/internal/terminalapi"
    26 30  )
    27 31   
    28 32  // pointCase is a test case for the pointCont function.
    skipped 261 lines
    290 294   tests := []struct {
    291 295   desc string
    292 296   // Can be either the mouse event or a time.Duration to pause for.
    293  - events []*terminalapi.Mouse
    294  - wantFocused contLoc
     297 + events []*terminalapi.Mouse
     298 + wantFocused contLoc
     299 + wantProcessed int
    295 300   }{
    296 301   {
    297 302   desc: "initially the root is focused",
    skipped 5 lines
    303 308   {Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
    304 309   {Position: image.Point{1, 1}, Button: mouse.ButtonRelease},
    305 310   },
    306  - wantFocused: contLocLeft,
     311 + wantFocused: contLocLeft,
     312 + wantProcessed: 2,
    307 313   },
    308 314   {
    309 315   desc: "click and release moves focus to the right",
    skipped 1 lines
    311 317   {Position: image.Point{5, 5}, Button: mouse.ButtonLeft},
    312 318   {Position: image.Point{6, 6}, Button: mouse.ButtonRelease},
    313 319   },
    314  - wantFocused: contLocRight,
     320 + wantFocused: contLocRight,
     321 + wantProcessed: 2,
    315 322   },
    316 323   {
    317 324   desc: "click in the same container is a no-op",
    skipped 3 lines
    321 328   {Position: insideRight, Button: mouse.ButtonLeft},
    322 329   {Position: insideRight, Button: mouse.ButtonRelease},
    323 330   },
    324  - wantFocused: contLocRight,
     331 + wantFocused: contLocRight,
     332 + wantProcessed: 4,
    325 333   },
    326 334   {
    327 335   desc: "click in the same container and release never happens",
    skipped 2 lines
    330 338   {Position: insideLeft, Button: mouse.ButtonLeft},
    331 339   {Position: insideLeft, Button: mouse.ButtonRelease},
    332 340   },
    333  - wantFocused: contLocLeft,
     341 + wantFocused: contLocLeft,
     342 + wantProcessed: 3,
    334 343   },
    335 344   {
    336 345   desc: "click in the same container, release elsewhere",
    skipped 1 lines
    338 347   {Position: insideRight, Button: mouse.ButtonLeft},
    339 348   {Position: insideLeft, Button: mouse.ButtonRelease},
    340 349   },
    341  - wantFocused: contLocRoot,
     350 + wantFocused: contLocRoot,
     351 + wantProcessed: 2,
    342 352   },
    343 353   {
    344 354   desc: "other buttons are ignored",
    skipped 5 lines
    350 360   {Position: insideLeft, Button: mouse.ButtonWheelUp},
    351 361   {Position: insideLeft, Button: mouse.ButtonWheelDown},
    352 362   },
    353  - wantFocused: contLocRoot,
     363 + wantFocused: contLocRoot,
     364 + wantProcessed: 6,
    354 365   },
    355 366   {
    356 367   desc: "moving mouse with pressed button and then releasing moves focus",
    skipped 2 lines
    359 370   {Position: image.Point{1, 1}, Button: mouse.ButtonLeft},
    360 371   {Position: image.Point{2, 2}, Button: mouse.ButtonRelease},
    361 372   },
    362  - wantFocused: contLocLeft,
     373 + wantFocused: contLocLeft,
     374 + wantProcessed: 3,
    363 375   },
    364 376   {
    365 377   desc: "click ignored if followed by another click of the same button elsewhere",
    skipped 1 lines
    367 379   {Position: insideRight, Button: mouse.ButtonLeft},
    368 380   {Position: insideLeft, Button: mouse.ButtonLeft},
    369 381   {Position: insideRight, Button: mouse.ButtonRelease},
    370  - {Position: insideRight, Button: mouse.ButtonRelease},
    371 382   },
    372  - wantFocused: contLocRoot,
     383 + wantFocused: contLocRoot,
     384 + wantProcessed: 3,
    373 385   },
    374 386   {
    375 387   desc: "click ignored if followed by another click of a different button",
    skipped 1 lines
    377 389   {Position: insideRight, Button: mouse.ButtonLeft},
    378 390   {Position: insideRight, Button: mouse.ButtonMiddle},
    379 391   {Position: insideRight, Button: mouse.ButtonRelease},
    380  - {Position: insideRight, Button: mouse.ButtonRelease},
    381 392   },
    382  - wantFocused: contLocRoot,
     393 + wantFocused: contLocRoot,
     394 + wantProcessed: 3,
    383 395   },
    384 396   }
    385 397   
    skipped 10 lines
    396 408   t.Fatalf("New => unexpected error: %v", err)
    397 409   }
    398 410   
     411 + eds := event.NewDistributionSystem()
     412 + root.Subscribe(eds)
    399 413   for _, ev := range tc.events {
    400  - root.Mouse(ev)
     414 + eds.Event(ev)
     415 + }
     416 + if err := testevent.WaitFor(5*time.Second, func() error {
     417 + if got, want := eds.Processed(), tc.wantProcessed; got != want {
     418 + return fmt.Errorf("the event distribution system processed %d events, want %d", got, want)
     419 + }
     420 + return nil
     421 + }); err != nil {
     422 + t.Fatalf("testevent.WaitFor => %v", err)
    401 423   }
    402 424   
    403 425   var wantFocused *Container
    skipped 23 lines
  • ■ ■ ■ ■ ■ ■
    container/mouse_fsm.go
    1  -// Copyright 2018 Google Inc.
    2  -//
    3  -// Licensed under the Apache License, Version 2.0 (the "License");
    4  -// you may not use this file except in compliance with the License.
    5  -// You may obtain a copy of the License at
    6  -//
    7  -// http://www.apache.org/licenses/LICENSE-2.0
    8  -//
    9  -// Unless required by applicable law or agreed to in writing, software
    10  -// distributed under the License is distributed on an "AS IS" BASIS,
    11  -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  -// See the License for the specific language governing permissions and
    13  -// limitations under the License.
    14  - 
    15  -package container
    16  - 
    17  -// mouse_fsm.go implements a state machine that tracks mouse clicks in regards
    18  -// to changing which container is focused.
    19  - 
    20  -import (
    21  - "github.com/mum4k/termdash/mouse"
    22  - "github.com/mum4k/termdash/terminalapi"
    23  -)
    24  - 
    25  -// mouseStateFn is a single state in the focus tracking state machine.
    26  -// Returns the next state.
    27  -type mouseStateFn func(ft *focusTracker, m *terminalapi.Mouse) mouseStateFn
    28  - 
    29  -// nextForLeftClick determines the next state for a left mouse click.
    30  -func nextForLeftClick(ft *focusTracker, m *terminalapi.Mouse) mouseStateFn {
    31  - // The click isn't in any known container.
    32  - if ft.candidate = pointCont(ft.container, m.Position); ft.candidate == nil {
    33  - return mouseWantLeftButton
    34  - }
    35  - return mouseWantRelease
    36  -}
    37  - 
    38  -// mouseWantLeftButton is the initial state, expecting a left button click inside a container.
    39  -func mouseWantLeftButton(ft *focusTracker, m *terminalapi.Mouse) mouseStateFn {
    40  - if m.Button != mouse.ButtonLeft {
    41  - return mouseWantLeftButton
    42  - }
    43  - return nextForLeftClick(ft, m)
    44  -}
    45  - 
    46  -// mouseWantRelease waits for a mouse button release in the same container as
    47  -// the click or a timeout or other left mouse button click.
    48  -func mouseWantRelease(ft *focusTracker, m *terminalapi.Mouse) mouseStateFn {
    49  - switch m.Button {
    50  - case mouse.ButtonLeft:
    51  - return nextForLeftClick(ft, m)
    52  - 
    53  - case mouse.ButtonRelease:
    54  - // Process the release.
    55  - default:
    56  - return mouseWantLeftButton
    57  - }
    58  - 
    59  - // The release happened in another container.
    60  - if ft.candidate != pointCont(ft.container, m.Position) {
    61  - return mouseWantLeftButton
    62  - }
    63  - 
    64  - ft.container = ft.candidate
    65  - ft.candidate = nil
    66  - return mouseWantLeftButton
    67  -}
    68  - 
  • ■ ■ ■ ■ ■ ■
    container/options.go
    skipped 18 lines
    19 19  import (
    20 20   "fmt"
    21 21   
    22  - "github.com/mum4k/termdash/align"
    23  - "github.com/mum4k/termdash/cell"
    24  - "github.com/mum4k/termdash/draw"
    25  - "github.com/mum4k/termdash/widgetapi"
     22 + "github.com/mum4k/termdash/internal/align"
     23 + "github.com/mum4k/termdash/internal/cell"
     24 + "github.com/mum4k/termdash/internal/draw"
     25 + "github.com/mum4k/termdash/internal/widgetapi"
    26 26  )
    27 27   
    28 28  // applyOptions applies the options to the container.
    skipped 378 lines
  • ■ ■ ■ ■ ■ ■
    container/traversal_test.go
    skipped 19 lines
    20 20   "reflect"
    21 21   "testing"
    22 22   
    23  - "github.com/mum4k/termdash/cell"
    24  - "github.com/mum4k/termdash/terminal/faketerm"
     23 + "github.com/mum4k/termdash/internal/cell"
     24 + "github.com/mum4k/termdash/internal/terminal/faketerm"
    25 25  )
    26 26   
    27 27  func TestRoot(t *testing.T) {
    skipped 141 lines
  • ■ ■ ■ ■ ■ ■
    doc/hld.md
    skipped 35 lines
    36 36  directly.
    37 37   
    38 38  The **infrastructure layer** is responsible for container management, tracking
    39  -of keyboard and mouse focus and handling external events like resizing of the
    40  -terminal. The infrastructure layer also decides when to flush the buffer and
    41  -refresh the screen. I.e. The widgets update content of a back buffer and the
    42  -infrastructure decides when it is synchronized to the terminal.
     39 +of keyboard and mouse focus and distribution and handling of external events
     40 +like resizing of the terminal. The infrastructure layer also decides when to
     41 +flush the buffer and refresh the screen. I.e. The widgets update content of a
     42 +back buffer and the infrastructure decides when it is synchronized to the
     43 +terminal.
    43 44   
    44 45  The **widgets layer** contains the implementations of individual widgets. Each
    45 46  widget receives a canvas from the container on which it presents its content to
    skipped 4 lines
    50 51  with the container API when placing the widgets onto the dashboard.
    51 52   
    52 53  <p align="center">
    53  - <img src="hld.png" width="50%">
     54 + <img src="images/hld.png" width="50%">
    54 55  </p>
    55 56   
    56 57  ## Detailed design
    skipped 8 lines
    65 66   canvas.
    66 67  - Flush the content of the back buffer to the output.
    67 68  - Manipulate the cursor position and visibility.
    68  -- Read input events (keyboard, mouse, terminal resize, etc...).
    69  - 
    70  -The terminal buffers input events until they are read by the client. The buffer
    71  -is bound, if the client isn't picking up events fast enough, new events are
    72  -dropped and a message is logged.
     69 +- Allow the infrastructure to read input events (keyboard, mouse, terminal
     70 + resize, etc...).
    73 71   
    74 72  ### Infrastructure
    75 73   
    76 74  The infrastructure handles terminal setup, input events and manages containers.
    77 75   
    78  -#### Keyboard and mouse input
     76 +#### Input events
     77 + 
     78 +The infrastructure regularly polls events from the terminal layer and feeds
     79 +them into the event distribution system (EDS). The EDS fulfills the following
     80 +functions:
     81 + 
     82 +- Allow subscribers to specify the type of events they want to receive.
     83 +- Distributeis events in a non-blocking manner, i.e. a single slow subscriber
     84 + cannot slow down other subscribers.
     85 +- Events to each subscriber are throttled, if a subscriber builds a long tail
     86 + of unprocessed input events, the EDS selectively drops repetitive events
     87 + towards the subscriber and eventually implements a tail-drop strategy.
     88 + 
     89 +The infrastructure itself is an input event subscriber and processes resize and
     90 +error events. The infrastructure panics on error events by default, unless an
     91 +error handler is provided by the user. Each widget that registers for keyboard
     92 +or mouse events is also an event subscriber. Any errors that happen while
     93 +processing an input event are send back to the EDS in the form of an Error
     94 +event and are processed by the infrastructure.
    79 95   
    80  -The raw keyboard and mouse events received from the terminal are pre-processed
    81  -by the infrastructure. The pre-processing involves recognizing keyboard
    82  -shortcuts (i.e. Key combination). The infrastructure recognizes globally
    83  -configurable keyboard shortcuts that are processed by the infrastructure. All
    84  -other keyboard and mouse events are forwarded to the currently focused widget.
    85 96   
    86  -#### Input focus
     97 +#### Input keyboard focus
    87 98   
    88  -The infrastructure tracks focus. Only the focused widget receives keyboard and
    89  -mouse events. Focus can be changed using mouse or global keyboard shortcuts.
    90  -The focused widget is highlighted on the dashboard.
     99 +The infrastructure tracks focus. Only the focused widget receives keyboard
     100 +events. Focus can be changed using mouse or keyboard shortcuts. The focused
     101 +widget is highlighted on the dashboard.
    91 102   
    92 103  #### Containers
    93 104   
    skipped 11 lines
    105 116   
    106 117  #### Flushing the terminal
    107 118   
    108  -All widgets indirectly write to the back buffer of the terminal implementation. The changes
    109  -to the back buffer only become visible when the infrastructure flushes its content.
     119 +All widgets indirectly write to the back buffer of the terminal implementation.
     120 +The changes to the back buffer only become visible when the infrastructure
     121 +flushes its content.
    110 122   
    111 123  #### Terminal resizing
    112 124   
    skipped 20 lines
    133 145  Each widget receives a canvas from the parent container, the widget can draw
    134 146  anything on the canvas as long as it stays within the limits. Helper libraries
    135 147  are developed that allow placement and drawing of common elements like lines or
    136  -geometrical shapes.
     148 +geometric shapes.
    137 149   
    138 150  ## APIs
    139 151   
    skipped 45 lines
    185 197   
    186 198  ## Testing plan
    187 199   
    188  -Unit test helpers are provided with the terminal dashboard library, these include:
     200 +Unit test helpers are provided with the terminal dashboard library, these
     201 +include:
    189 202   
    190 203  - A fake implementation of the terminal API.
    191 204  - Unit test comparison helpers to verify the content of the fake terminal.
    192  -- Visualization tools to display differences between the expected and the actual.
     205 +- Visualization tools to display differences between the expected and the
     206 + actual.
    193 207   
    194 208  ## Document history
    195 209   
    196 210  Date | Author | Description
    197  -------------|--------|---------------
     211 +------------|--------|-----------------------------------
    198 212  24-Mar-2018 | mum4k | Initial draft.
     213 +20-Feb-2019 | mum4k | Added notes on event distribution.
    199 214   
  • images/barchartdemo.gif doc/images/barchartdemo.gif
  • doc/images/buttondemo.gif
  • images/donutdemo.gif doc/images/donutdemo.gif
  • images/gaugedemo.gif doc/images/gaugedemo.gif
  • doc/hld.graffle doc/images/hld.graffle
    Binary file.
  • doc/hld.png doc/images/hld.png
  • doc/images/linechartdemo.gif
  • images/segmentdisplaydemo.gif doc/images/segmentdisplaydemo.gif
  • images/sparklinedemo.gif doc/images/sparklinedemo.gif
  • doc/images/termdashdemo_0_7_0.gif
  • images/textdemo.gif doc/images/textdemo.gif
  • ■ ■ ■ ■ ■ ■
    eventqueue/eventqueue.go
    1  -// Copyright 2018 Google Inc.
    2  -//
    3  -// Licensed under the Apache License, Version 2.0 (the "License");
    4  -// you may not use this file except in compliance with the License.
    5  -// You may obtain a copy of the License at
    6  -//
    7  -// http://www.apache.org/licenses/LICENSE-2.0
    8  -//
    9  -// Unless required by applicable law or agreed to in writing, software
    10  -// distributed under the License is distributed on an "AS IS" BASIS,
    11  -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  -// See the License for the specific language governing permissions and
    13  -// limitations under the License.
    14  - 
    15  -// Package eventqueue provides an unboud FIFO queue of events.
    16  -package eventqueue
    17  - 
    18  -import (
    19  - "context"
    20  - "sync"
    21  - "time"
    22  - 
    23  - "github.com/mum4k/termdash/terminalapi"
    24  -)
    25  - 
    26  -// node is a single data item on the queue.
    27  -type node struct {
    28  - next *node
    29  - event terminalapi.Event
    30  -}
    31  - 
    32  -// Unbound is an unbound FIFO queue of terminal events.
    33  -// Unbound must not be copied, pass it by reference only.
    34  -// This implementation is thread-safe.
    35  -type Unbound struct {
    36  - first *node
    37  - last *node
    38  - // mu protects first and last.
    39  - mu sync.Mutex
    40  - 
    41  - // cond is used to notify any callers waiting on a call to Pull().
    42  - cond *sync.Cond
    43  - 
    44  - // condMU protects cond.
    45  - condMU sync.RWMutex
    46  - 
    47  - // done is closed when the queue isn't needed anymore.
    48  - done chan struct{}
    49  -}
    50  - 
    51  -// New returns a new Unbound queue of terminal events.
    52  -// Call Close() when done with the queue.
    53  -func New() *Unbound {
    54  - u := &Unbound{
    55  - done: make(chan (struct{})),
    56  - }
    57  - u.cond = sync.NewCond(&u.condMU)
    58  - go u.wake() // Stops when Close() is called.
    59  - return u
    60  -}
    61  - 
    62  -// wake periodically wakes up all goroutines waiting at Pull() so they can
    63  -// check if the context expired.
    64  -func (u *Unbound) wake() {
    65  - const spinTime = 250 * time.Millisecond
    66  - t := time.NewTicker(spinTime)
    67  - defer t.Stop()
    68  - for {
    69  - select {
    70  - case <-t.C:
    71  - u.cond.Broadcast()
    72  - case <-u.done:
    73  - return
    74  - }
    75  - }
    76  -}
    77  - 
    78  -// Empty determines if the queue is empty.
    79  -func (u *Unbound) Empty() bool {
    80  - u.mu.Lock()
    81  - defer u.mu.Unlock()
    82  - return u.empty()
    83  -}
    84  - 
    85  -// empty determines if the queue is empty.
    86  -func (u *Unbound) empty() bool {
    87  - return u.first == nil
    88  -}
    89  - 
    90  -// Push pushes an event onto the queue.
    91  -func (u *Unbound) Push(e terminalapi.Event) {
    92  - u.mu.Lock()
    93  - defer u.mu.Unlock()
    94  - 
    95  - n := &node{
    96  - event: e,
    97  - }
    98  - if u.empty() {
    99  - u.first = n
    100  - u.last = n
    101  - } else {
    102  - u.last.next = n
    103  - u.last = n
    104  - }
    105  - u.cond.Signal()
    106  -}
    107  - 
    108  -// Pop pops an event from the queue. Returns nil if the queue is empty.
    109  -func (u *Unbound) Pop() terminalapi.Event {
    110  - u.mu.Lock()
    111  - defer u.mu.Unlock()
    112  - 
    113  - if u.empty() {
    114  - return nil
    115  - }
    116  - 
    117  - n := u.first
    118  - u.first = u.first.next
    119  - 
    120  - if u.empty() {
    121  - u.last = nil
    122  - }
    123  - return n.event
    124  -}
    125  - 
    126  -// Pull is like Pop(), but blocks until an item is available or the context
    127  -// expires.
    128  -func (u *Unbound) Pull(ctx context.Context) (terminalapi.Event, error) {
    129  - if e := u.Pop(); e != nil {
    130  - return e, nil
    131  - }
    132  - 
    133  - u.cond.L.Lock()
    134  - defer u.cond.L.Unlock()
    135  - for {
    136  - select {
    137  - case <-ctx.Done():
    138  - return nil, ctx.Err()
    139  - default:
    140  - }
    141  - 
    142  - if e := u.Pop(); e != nil {
    143  - return e, nil
    144  - }
    145  - u.cond.Wait()
    146  - }
    147  -}
    148  - 
    149  -// Close should be called when the queue isn't needed anymore.
    150  -func (u *Unbound) Close() {
    151  - close(u.done)
    152  -}
    153  - 
  • ■ ■ ■ ■ ■ ■
    eventqueue/eventqueue_test.go
    1  -// Copyright 2018 Google Inc.
    2  -//
    3  -// Licensed under the Apache License, Version 2.0 (the "License");
    4  -// you may not use this file except in compliance with the License.
    5  -// You may obtain a copy of the License at
    6  -//
    7  -// http://www.apache.org/licenses/LICENSE-2.0
    8  -//
    9  -// Unless required by applicable law or agreed to in writing, software
    10  -// distributed under the License is distributed on an "AS IS" BASIS,
    11  -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  -// See the License for the specific language governing permissions and
    13  -// limitations under the License.
    14  - 
    15  -package eventqueue
    16  - 
    17  -import (
    18  - "context"
    19  - "testing"
    20  - "time"
    21  - 
    22  - "github.com/kylelemons/godebug/pretty"
    23  - "github.com/mum4k/termdash/terminalapi"
    24  -)
    25  - 
    26  -func TestQueue(t *testing.T) {
    27  - tests := []struct {
    28  - desc string
    29  - pushes []terminalapi.Event
    30  - wantEmpty bool // Checked after pushes and before pops.
    31  - wantPops []terminalapi.Event
    32  - }{
    33  - {
    34  - desc: "empty queue returns nil",
    35  - wantEmpty: true,
    36  - wantPops: []terminalapi.Event{
    37  - nil,
    38  - },
    39  - },
    40  - {
    41  - desc: "queue is FIFO",
    42  - pushes: []terminalapi.Event{
    43  - terminalapi.NewError("error1"),
    44  - terminalapi.NewError("error2"),
    45  - terminalapi.NewError("error3"),
    46  - },
    47  - wantEmpty: false,
    48  - wantPops: []terminalapi.Event{
    49  - terminalapi.NewError("error1"),
    50  - terminalapi.NewError("error2"),
    51  - terminalapi.NewError("error3"),
    52  - nil,
    53  - },
    54  - },
    55  - }
    56  - 
    57  - for _, tc := range tests {
    58  - t.Run(tc.desc, func(t *testing.T) {
    59  - q := New()
    60  - defer q.Close()
    61  - for _, ev := range tc.pushes {
    62  - q.Push(ev)
    63  - }
    64  - 
    65  - gotEmpty := q.Empty()
    66  - if gotEmpty != tc.wantEmpty {
    67  - t.Errorf("Empty => got %v, want %v", gotEmpty, tc.wantEmpty)
    68  - }
    69  - 
    70  - for i, want := range tc.wantPops {
    71  - got := q.Pop()
    72  - if diff := pretty.Compare(want, got); diff != "" {
    73  - t.Errorf("Pop[%d] => unexpected diff (-want, +got):\n%s", i, diff)
    74  - }
    75  - }
    76  - })
    77  - }
    78  -}
    79  - 
    80  -func TestPullEventAvailable(t *testing.T) {
    81  - q := New()
    82  - defer q.Close()
    83  - want := terminalapi.NewError("error event")
    84  - q.Push(want)
    85  - 
    86  - ctx := context.Background()
    87  - got, err := q.Pull(ctx)
    88  - if err != nil {
    89  - t.Fatalf("Pull => unexpected error: %v", err)
    90  - }
    91  - if diff := pretty.Compare(want, got); diff != "" {
    92  - t.Errorf("Pull => unexpected diff (-want, +got):\n%s", diff)
    93  - }
    94  -}
    95  - 
    96  -func TestPullBlocksUntilAvailable(t *testing.T) {
    97  - q := New()
    98  - defer q.Close()
    99  - want := terminalapi.NewError("error event")
    100  - 
    101  - ch := make(chan struct{})
    102  - go func() {
    103  - <-ch
    104  - q.Push(want)
    105  - }()
    106  - 
    107  - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond)
    108  - defer cancel()
    109  - 
    110  - if _, err := q.Pull(ctx); err == nil {
    111  - t.Fatal("Pull => expected timeout error, got nil")
    112  - }
    113  - 
    114  - close(ch)
    115  - got, err := q.Pull(context.Background())
    116  - if err != nil {
    117  - t.Fatalf("Pull => unexpected error: %v", err)
    118  - }
    119  - 
    120  - if diff := pretty.Compare(want, got); diff != "" {
    121  - t.Errorf("Pull => unexpected diff (-want, +got):\n%s", diff)
    122  - }
    123  -}
    124  - 
  • images/linechartdemo.gif
  • images/termdashdemo_0_6_0.gif
  • ■ ■ ■ ■ ■
    internal/README.md
     1 +# Internal termdash libraries
     2 + 
     3 +The packages under this directory are private to termdash. Stability of the
     4 +private packages isn't guaranteed and changes won't be backward compatible.
     5 + 
  • ■ ■ ■ ■
    align/align.go internal/align/align.go
    skipped 19 lines
    20 20   "image"
    21 21   "strings"
    22 22   
    23  - runewidth "github.com/mattn/go-runewidth"
     23 + "github.com/mum4k/termdash/internal/cell/runewidth"
    24 24  )
    25 25   
    26 26  // Horizontal indicates the type of horizontal alignment.
    skipped 148 lines
  • align/align_test.go internal/align/align_test.go
    Content is identical
  • ■ ■ ■ ■ ■
    area/area.go internal/area/area.go
    skipped 18 lines
    19 19   "fmt"
    20 20   "image"
    21 21   
    22  - "github.com/mum4k/termdash/numbers"
     22 + "github.com/mum4k/termdash/internal/numbers"
    23 23  )
    24 24   
    25 25  // Size returns the size of the provided area.
    skipped 70 lines
    96 96   )
    97 97  }
    98 98   
    99  -// findGCF finds the greatest common factor of two integers.
    100  -func findGCF(a, b int) int {
    101  - if a == 0 || b == 0 {
    102  - return 0
    103  - }
    104  - 
    105  - // https://en.wikipedia.org/wiki/Euclidean_algorithm
    106  - for {
    107  - rem := a % b
    108  - a = b
    109  - b = rem
    110  - 
    111  - if b == 0 {
    112  - break
    113  - }
    114  - }
    115  - return a
    116  -}
    117  - 
    118  -// simplifyRatio simplifies the given ratio.
    119  -func simplifyRatio(ratio image.Point) image.Point {
    120  - gcf := findGCF(ratio.X, ratio.Y)
    121  - if gcf == 0 {
    122  - return image.ZP
    123  - }
    124  - return image.Point{
    125  - X: ratio.X / gcf,
    126  - Y: ratio.Y / gcf,
    127  - }
    128  -}
    129  - 
    130 99  // WithRatio returns the largest area that has the requested ratio but is
    131 100  // either equal or smaller than the provided area. Returns zero area if the
    132 101  // area or the ratio are zero, or if there is no such area.
    133 102  func WithRatio(area image.Rectangle, ratio image.Point) image.Rectangle {
    134  - ratio = simplifyRatio(ratio)
     103 + ratio = numbers.SimplifyRatio(ratio)
    135 104   if area == image.ZR || ratio == image.ZP {
    136 105   return image.ZR
    137 106   }
    skipped 18 lines
  • ■ ■ ■ ■ ■ ■
    area/area_test.go internal/area/area_test.go
    skipped 14 lines
    15 15  package area
    16 16   
    17 17  import (
    18  - "fmt"
    19 18   "image"
    20 19   "testing"
    21 20   
    skipped 294 lines
    316 315   got := ExcludeBorder(tc.area)
    317 316   if diff := pretty.Compare(tc.want, got); diff != "" {
    318 317   t.Errorf("ExcludeBorder => unexpected diff (-want, +got):\n%s", diff)
    319  - }
    320  - })
    321  - }
    322  -}
    323  - 
    324  -func TestFindGCF(t *testing.T) {
    325  - tests := []struct {
    326  - a int
    327  - b int
    328  - want int
    329  - }{
    330  - {0, 0, 0},
    331  - {0, 1, 0},
    332  - {1, 0, 0},
    333  - {1, 1, 1},
    334  - {2, 2, 2},
    335  - {50, 35, 5},
    336  - {16, 88, 8},
    337  - }
    338  - 
    339  - for _, tc := range tests {
    340  - t.Run(fmt.Sprintf("findGCF(%d,%d)", tc.a, tc.b), func(t *testing.T) {
    341  - if got := findGCF(tc.a, tc.b); got != tc.want {
    342  - t.Errorf("findGCF(%d,%d) => got %v, want %v", tc.a, tc.b, got, tc.want)
    343 318   }
    344 319   })
    345 320   }
    skipped 81 lines
  • attrrange/attrrange.go internal/attrrange/attrrange.go
    Content is identical
  • ■ ■ ■ ■
    attrrange/attrrange_test.go internal/attrrange/attrrange_test.go
    skipped 18 lines
    19 19   "testing"
    20 20   
    21 21   "github.com/kylelemons/godebug/pretty"
    22  - "github.com/mum4k/termdash/cell"
     22 + "github.com/mum4k/termdash/internal/cell"
    23 23  )
    24 24   
    25 25  func Example() {
    skipped 141 lines
  • ■ ■ ■ ■ ■
    canvas/braille/braille.go internal/canvas/braille/braille.go
    skipped 44 lines
    45 45   "fmt"
    46 46   "image"
    47 47   
    48  - "github.com/mum4k/termdash/canvas"
    49  - "github.com/mum4k/termdash/cell"
    50  - "github.com/mum4k/termdash/terminalapi"
     48 + "github.com/mum4k/termdash/internal/canvas"
     49 + "github.com/mum4k/termdash/internal/cell"
     50 + "github.com/mum4k/termdash/internal/terminalapi"
    51 51  )
    52 52   
    53 53  const (
    skipped 54 lines
    108 108  func (c *Canvas) Size() image.Point {
    109 109   s := c.regular.Size()
    110 110   return image.Point{s.X * ColMult, s.Y * RowMult}
     111 +}
     112 + 
     113 +// CellArea returns the area of the underlying cell canvas in cells.
     114 +func (c *Canvas) CellArea() image.Rectangle {
     115 + return c.regular.Area()
    111 116  }
    112 117   
    113 118  // Area returns the area of the braille canvas in pixels.
    skipped 72 lines
    186 191   if err != nil {
    187 192   return err
    188 193   }
    189  - cell, err := c.regular.Cell(cp)
     194 + curCell, err := c.regular.Cell(cp)
    190 195   if err != nil {
    191 196   return err
    192 197   }
    193 198   
    194  - if isBraille(cell.Rune) && pixelSet(cell.Rune, p) {
     199 + if isBraille(curCell.Rune) && pixelSet(curCell.Rune, p) {
    195 200   return c.ClearPixel(p, opts...)
    196 201   }
    197 202   return c.SetPixel(p, opts...)
     203 +}
     204 + 
     205 +// SetCellOpts sets options on the specified cell of the braille canvas without
     206 +// modifying the content of the cell.
     207 +// Sets the default cell options if no options are provided.
     208 +// This method is idempotent.
     209 +func (c *Canvas) SetCellOpts(cellPoint image.Point, opts ...cell.Option) error {
     210 + curCell, err := c.regular.Cell(cellPoint)
     211 + if err != nil {
     212 + return err
     213 + }
     214 + 
     215 + if len(opts) == 0 {
     216 + // Set the default options.
     217 + opts = []cell.Option{
     218 + cell.FgColor(cell.ColorDefault),
     219 + cell.BgColor(cell.ColorDefault),
     220 + }
     221 + }
     222 + if _, err := c.regular.SetCell(cellPoint, curCell.Rune, opts...); err != nil {
     223 + return err
     224 + }
     225 + return nil
     226 +}
     227 + 
     228 +// SetAreaCellOpts is like SetCellOpts, but sets the specified options on all
     229 +// the cells within the provided area.
     230 +func (c *Canvas) SetAreaCellOpts(cellArea image.Rectangle, opts ...cell.Option) error {
     231 + haveArea := c.regular.Area()
     232 + if !cellArea.In(haveArea) {
     233 + return fmt.Errorf("unable to set cell options in area %v, it must fit inside the available cell area is %v", cellArea, haveArea)
     234 + }
     235 + for col := cellArea.Min.X; col < cellArea.Max.X; col++ {
     236 + for row := cellArea.Min.Y; row < cellArea.Max.Y; row++ {
     237 + if err := c.SetCellOpts(image.Point{col, row}, opts...); err != nil {
     238 + return err
     239 + }
     240 + }
     241 + }
     242 + return nil
    198 243  }
    199 244   
    200 245  // Apply applies the canvas to the corresponding area of the terminal.
    skipped 40 lines
  • ■ ■ ■ ■ ■
    canvas/braille/braille_test.go internal/canvas/braille/braille_test.go
    skipped 18 lines
    19 19   "testing"
    20 20   
    21 21   "github.com/kylelemons/godebug/pretty"
    22  - "github.com/mum4k/termdash/area"
    23  - "github.com/mum4k/termdash/canvas"
    24  - "github.com/mum4k/termdash/canvas/testcanvas"
    25  - "github.com/mum4k/termdash/cell"
    26  - "github.com/mum4k/termdash/terminal/faketerm"
     22 + "github.com/mum4k/termdash/internal/area"
     23 + "github.com/mum4k/termdash/internal/canvas"
     24 + "github.com/mum4k/termdash/internal/canvas/testcanvas"
     25 + "github.com/mum4k/termdash/internal/cell"
     26 + "github.com/mum4k/termdash/internal/terminal/faketerm"
    27 27  )
    28 28   
    29 29  func Example_copiedToCanvas() {
    skipped 44 lines
    74 74   
    75 75  func TestNew(t *testing.T) {
    76 76   tests := []struct {
    77  - desc string
    78  - ar image.Rectangle
    79  - wantSize image.Point
    80  - wantArea image.Rectangle
    81  - wantErr bool
     77 + desc string
     78 + ar image.Rectangle
     79 + wantSize image.Point
     80 + wantArea image.Rectangle
     81 + wantCellArea image.Rectangle
     82 + wantErr bool
    82 83   }{
    83 84   {
    84 85   desc: "fails on a negative area",
    skipped 1 lines
    86 87   wantErr: true,
    87 88   },
    88 89   {
    89  - desc: "braille from zero-based single-cell area",
    90  - ar: image.Rect(0, 0, 1, 1),
    91  - wantSize: image.Point{2, 4},
    92  - wantArea: image.Rect(0, 0, 2, 4),
     90 + desc: "braille from zero-based single-cell area",
     91 + ar: image.Rect(0, 0, 1, 1),
     92 + wantSize: image.Point{2, 4},
     93 + wantArea: image.Rect(0, 0, 2, 4),
     94 + wantCellArea: image.Rect(0, 0, 1, 1),
    93 95   },
    94 96   {
    95  - desc: "braille from non-zero-based single-cell area",
    96  - ar: image.Rect(3, 3, 4, 4),
    97  - wantSize: image.Point{2, 4},
    98  - wantArea: image.Rect(0, 0, 2, 4),
     97 + desc: "braille from non-zero-based single-cell area",
     98 + ar: image.Rect(3, 3, 4, 4),
     99 + wantSize: image.Point{2, 4},
     100 + wantArea: image.Rect(0, 0, 2, 4),
     101 + wantCellArea: image.Rect(0, 0, 1, 1),
    99 102   },
    100 103   {
    101  - desc: "braille from zero-based multi-cell area",
    102  - ar: image.Rect(0, 0, 3, 3),
    103  - wantSize: image.Point{6, 12},
    104  - wantArea: image.Rect(0, 0, 6, 12),
     104 + desc: "braille from zero-based multi-cell area",
     105 + ar: image.Rect(0, 0, 3, 3),
     106 + wantSize: image.Point{6, 12},
     107 + wantArea: image.Rect(0, 0, 6, 12),
     108 + wantCellArea: image.Rect(0, 0, 3, 3),
    105 109   },
    106 110   {
    107  - desc: "braille from non-zero-based multi-cell area",
    108  - ar: image.Rect(6, 6, 9, 9),
    109  - wantSize: image.Point{6, 12},
    110  - wantArea: image.Rect(0, 0, 6, 12),
     111 + desc: "braille from non-zero-based multi-cell area",
     112 + ar: image.Rect(6, 6, 9, 9),
     113 + wantSize: image.Point{6, 12},
     114 + wantArea: image.Rect(0, 0, 6, 12),
     115 + wantCellArea: image.Rect(0, 0, 3, 3),
    111 116   },
    112 117   {
    113  - desc: "braille from non-zero-based multi-cell rectangular area",
    114  - ar: image.Rect(6, 6, 9, 10),
    115  - wantSize: image.Point{6, 16},
    116  - wantArea: image.Rect(0, 0, 6, 16),
     118 + desc: "braille from non-zero-based multi-cell rectangular area",
     119 + ar: image.Rect(6, 6, 9, 10),
     120 + wantSize: image.Point{6, 16},
     121 + wantArea: image.Rect(0, 0, 6, 16),
     122 + wantCellArea: image.Rect(0, 0, 3, 4),
    117 123   },
    118 124   }
    119 125   
    skipped 15 lines
    135 141   gotArea := got.Area()
    136 142   if diff := pretty.Compare(tc.wantArea, gotArea); diff != "" {
    137 143   t.Errorf("Area => unexpected diff (-want, +got):\n%s", diff)
     144 + }
     145 + 
     146 + gotCellArea := got.CellArea()
     147 + if diff := pretty.Compare(tc.wantCellArea, gotCellArea); diff != "" {
     148 + t.Errorf("CellArea => unexpected diff (-want, +got):\n%s", diff)
    138 149   }
    139 150   })
    140 151   }
    skipped 27 lines
    168 179   wantErr: true,
    169 180   want: func(size image.Point) *faketerm.Terminal {
    170 181   return faketerm.MustNew(size)
     182 + },
     183 + },
     184 + {
     185 + desc: "SetCellOptions fails on a cell outside of the braille canvas",
     186 + ar: image.Rect(0, 0, 1, 1),
     187 + pixelOps: func(c *Canvas) error {
     188 + return c.SetCellOpts(image.Point{0, -1})
     189 + },
     190 + wantErr: true,
     191 + want: func(size image.Point) *faketerm.Terminal {
     192 + return faketerm.MustNew(size)
     193 + },
     194 + },
     195 + {
     196 + desc: "SetCellOptions sets options on cell with no options",
     197 + ar: image.Rect(0, 0, 1, 1),
     198 + pixelOps: func(c *Canvas) error {
     199 + return c.SetCellOpts(image.Point{0, 0}, cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue))
     200 + },
     201 + want: func(size image.Point) *faketerm.Terminal {
     202 + ft := faketerm.MustNew(size)
     203 + cvs := testcanvas.MustNew(ft.Area())
     204 + 
     205 + c := testcanvas.MustCell(cvs, image.Point{0, 0})
     206 + testcanvas.MustSetCell(cvs, image.Point{0, 0}, c.Rune, cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue))
     207 + 
     208 + testcanvas.MustApply(cvs, ft)
     209 + return ft
     210 + },
     211 + },
     212 + {
     213 + desc: "SetCellOptions preserves the cell rune",
     214 + ar: image.Rect(0, 0, 1, 1),
     215 + pixelOps: func(c *Canvas) error {
     216 + if err := c.SetPixel(image.Point{0, 0}); err != nil {
     217 + return err
     218 + }
     219 + return c.SetCellOpts(image.Point{0, 0}, cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue))
     220 + },
     221 + want: func(size image.Point) *faketerm.Terminal {
     222 + ft := faketerm.MustNew(size)
     223 + cvs := testcanvas.MustNew(ft.Area())
     224 + 
     225 + testcanvas.MustSetCell(cvs, image.Point{0, 0}, '⠁', cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue))
     226 + 
     227 + testcanvas.MustApply(cvs, ft)
     228 + return ft
     229 + },
     230 + },
     231 + {
     232 + desc: "SetCellOptions overwrites options set previously",
     233 + ar: image.Rect(0, 0, 1, 1),
     234 + pixelOps: func(c *Canvas) error {
     235 + if err := c.SetPixel(image.Point{0, 0}, cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue)); err != nil {
     236 + return err
     237 + }
     238 + return c.SetCellOpts(image.Point{0, 0}, cell.FgColor(cell.ColorGreen), cell.BgColor(cell.ColorYellow))
     239 + },
     240 + want: func(size image.Point) *faketerm.Terminal {
     241 + ft := faketerm.MustNew(size)
     242 + cvs := testcanvas.MustNew(ft.Area())
     243 + 
     244 + testcanvas.MustSetCell(cvs, image.Point{0, 0}, '⠁', cell.FgColor(cell.ColorGreen), cell.BgColor(cell.ColorYellow))
     245 + 
     246 + testcanvas.MustApply(cvs, ft)
     247 + return ft
     248 + },
     249 + },
     250 + {
     251 + desc: "SetCellOptions sets default options when no options provided",
     252 + ar: image.Rect(0, 0, 1, 1),
     253 + pixelOps: func(c *Canvas) error {
     254 + if err := c.SetPixel(image.Point{0, 0}, cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue)); err != nil {
     255 + return err
     256 + }
     257 + return c.SetCellOpts(image.Point{0, 0})
     258 + },
     259 + want: func(size image.Point) *faketerm.Terminal {
     260 + ft := faketerm.MustNew(size)
     261 + cvs := testcanvas.MustNew(ft.Area())
     262 + 
     263 + testcanvas.MustSetCell(cvs, image.Point{0, 0}, '⠁')
     264 + 
     265 + testcanvas.MustApply(cvs, ft)
     266 + return ft
     267 + },
     268 + },
     269 + {
     270 + desc: "SetCellOptions is idempotent",
     271 + ar: image.Rect(0, 0, 1, 1),
     272 + pixelOps: func(c *Canvas) error {
     273 + if err := c.SetCellOpts(image.Point{0, 0}, cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue)); err != nil {
     274 + return err
     275 + }
     276 + return c.SetCellOpts(image.Point{0, 0}, cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue))
     277 + },
     278 + want: func(size image.Point) *faketerm.Terminal {
     279 + ft := faketerm.MustNew(size)
     280 + cvs := testcanvas.MustNew(ft.Area())
     281 + 
     282 + c := testcanvas.MustCell(cvs, image.Point{0, 0})
     283 + testcanvas.MustSetCell(cvs, image.Point{0, 0}, c.Rune, cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue))
     284 + 
     285 + testcanvas.MustApply(cvs, ft)
     286 + return ft
     287 + },
     288 + },
     289 + {
     290 + desc: "SetAreaCellOptions fails on area too large",
     291 + ar: image.Rect(0, 0, 1, 1),
     292 + pixelOps: func(c *Canvas) error {
     293 + return c.SetAreaCellOpts(image.Rect(0, 0, 2, 2), cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue))
     294 + },
     295 + wantErr: true,
     296 + },
     297 + {
     298 + desc: "SetAreaCellOptions sets the cell options in full area",
     299 + ar: image.Rect(0, 0, 1, 1),
     300 + pixelOps: func(c *Canvas) error {
     301 + return c.SetAreaCellOpts(image.Rect(0, 0, 1, 1), cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue))
     302 + },
     303 + want: func(size image.Point) *faketerm.Terminal {
     304 + ft := faketerm.MustNew(size)
     305 + cvs := testcanvas.MustNew(ft.Area())
     306 + 
     307 + for _, p := range []image.Point{
     308 + {0, 0},
     309 + } {
     310 + c := testcanvas.MustCell(cvs, p)
     311 + testcanvas.MustSetCell(cvs, p, c.Rune, cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue))
     312 + }
     313 + testcanvas.MustApply(cvs, ft)
     314 + return ft
     315 + },
     316 + },
     317 + {
     318 + desc: "SetAreaCellOptions sets the cell options in a sub-area",
     319 + ar: image.Rect(0, 0, 3, 3),
     320 + pixelOps: func(c *Canvas) error {
     321 + return c.SetAreaCellOpts(image.Rect(0, 0, 2, 2), cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue))
     322 + },
     323 + want: func(size image.Point) *faketerm.Terminal {
     324 + ft := faketerm.MustNew(size)
     325 + cvs := testcanvas.MustNew(ft.Area())
     326 + 
     327 + for _, p := range []image.Point{
     328 + {0, 0},
     329 + {0, 1},
     330 + {1, 0},
     331 + {1, 1},
     332 + } {
     333 + c := testcanvas.MustCell(cvs, p)
     334 + testcanvas.MustSetCell(cvs, p, c.Rune, cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue))
     335 + }
     336 + testcanvas.MustApply(cvs, ft)
     337 + return ft
    171 338   },
    172 339   },
    173 340   {
    skipped 653 lines
  • ■ ■ ■ ■ ■
    canvas/braille/testbraille/testbraille.go internal/canvas/braille/testbraille/testbraille.go
    skipped 18 lines
    19 19   "fmt"
    20 20   "image"
    21 21   
    22  - "github.com/mum4k/termdash/canvas"
    23  - "github.com/mum4k/termdash/canvas/braille"
    24  - "github.com/mum4k/termdash/cell"
    25  - "github.com/mum4k/termdash/terminal/faketerm"
     22 + "github.com/mum4k/termdash/internal/canvas"
     23 + "github.com/mum4k/termdash/internal/canvas/braille"
     24 + "github.com/mum4k/termdash/internal/cell"
     25 + "github.com/mum4k/termdash/internal/terminal/faketerm"
    26 26  )
    27 27   
    28 28  // MustNew returns a new canvas or panics.
    skipped 33 lines
    62 62   }
    63 63  }
    64 64   
     65 +// MustSetCellOpts sets the cell options or panics.
     66 +func MustSetCellOpts(bc *braille.Canvas, cellPoint image.Point, opts ...cell.Option) {
     67 + if err := bc.SetCellOpts(cellPoint, opts...); err != nil {
     68 + panic(fmt.Sprintf("bc.SetCellOpts => unexpected error: %v", err))
     69 + }
     70 +}
     71 + 
     72 +// MustSetAreaCellOpts sets the cell options in the area or panics.
     73 +func MustSetAreaCellOpts(bc *braille.Canvas, cellArea image.Rectangle, opts ...cell.Option) {
     74 + if err := bc.SetAreaCellOpts(cellArea, opts...); err != nil {
     75 + panic(fmt.Sprintf("bc.SetAreaCellOpts => unexpected error: %v", err))
     76 + }
     77 +}
     78 + 
  • ■ ■ ■ ■ ■ ■
    canvas/canvas.go internal/canvas/canvas.go
    skipped 18 lines
    19 19   "fmt"
    20 20   "image"
    21 21   
    22  - "github.com/mum4k/termdash/area"
    23  - "github.com/mum4k/termdash/cell"
    24  - "github.com/mum4k/termdash/terminalapi"
     22 + "github.com/mum4k/termdash/internal/area"
     23 + "github.com/mum4k/termdash/internal/cell"
     24 + "github.com/mum4k/termdash/internal/cell/runewidth"
     25 + "github.com/mum4k/termdash/internal/terminalapi"
    25 26  )
    26 27   
    27 28  // Canvas is where a widget draws its output for display on the terminal.
    skipped 64 lines
    92 93   }
    93 94   
    94 95   return c.buffer[p.X][p.Y].Copy(), nil
     96 +}
     97 + 
     98 +// SetCellOpts sets options on the specified cell of the canvas without
     99 +// modifying the content of the cell.
     100 +// Sets the default cell options if no options are provided.
     101 +// This method is idempotent.
     102 +func (c *Canvas) SetCellOpts(p image.Point, opts ...cell.Option) error {
     103 + curCell, err := c.Cell(p)
     104 + if err != nil {
     105 + return err
     106 + }
     107 + 
     108 + if len(opts) == 0 {
     109 + // Set the default options.
     110 + opts = []cell.Option{
     111 + cell.FgColor(cell.ColorDefault),
     112 + cell.BgColor(cell.ColorDefault),
     113 + }
     114 + }
     115 + if _, err := c.SetCell(p, curCell.Rune, opts...); err != nil {
     116 + return err
     117 + }
     118 + return nil
     119 +}
     120 + 
     121 +// SetAreaCells is like SetCell, but sets the specified rune and options on all
     122 +// the cells within the provided area.
     123 +// This method is idempotent.
     124 +func (c *Canvas) SetAreaCells(cellArea image.Rectangle, r rune, opts ...cell.Option) error {
     125 + haveArea := c.Area()
     126 + if !cellArea.In(haveArea) {
     127 + return fmt.Errorf("unable to set cell runes in area %v, it must fit inside the available cell area is %v", cellArea, haveArea)
     128 + }
     129 + 
     130 + rw := runewidth.RuneWidth(r)
     131 + for row := cellArea.Min.Y; row < cellArea.Max.Y; row++ {
     132 + for col := cellArea.Min.X; col < cellArea.Max.X; {
     133 + p := image.Point{col, row}
     134 + if col+rw > cellArea.Max.X {
     135 + break
     136 + }
     137 + cells, err := c.SetCell(p, r, opts...)
     138 + if err != nil {
     139 + return err
     140 + }
     141 + col += cells
     142 + }
     143 + }
     144 + return nil
     145 +}
     146 + 
     147 +// SetAreaCellOpts is like SetCellOpts, but sets the specified options on all
     148 +// the cells within the provided area.
     149 +func (c *Canvas) SetAreaCellOpts(cellArea image.Rectangle, opts ...cell.Option) error {
     150 + haveArea := c.Area()
     151 + if !cellArea.In(haveArea) {
     152 + return fmt.Errorf("unable to set cell options in area %v, it must fit inside the available cell area is %v", cellArea, haveArea)
     153 + }
     154 + for col := cellArea.Min.X; col < cellArea.Max.X; col++ {
     155 + for row := cellArea.Min.Y; row < cellArea.Max.Y; row++ {
     156 + if err := c.SetCellOpts(image.Point{col, row}, opts...); err != nil {
     157 + return err
     158 + }
     159 + }
     160 + }
     161 + return nil
    95 162  }
    96 163   
    97 164  // setCellFunc is a function that sets cell content on a terminal or a canvas.
    skipped 83 lines
  • ■ ■ ■ ■ ■ ■
    internal/canvas/canvas_test.go
     1 +// Copyright 2018 Google Inc.
     2 +//
     3 +// Licensed under the Apache License, Version 2.0 (the "License");
     4 +// you may not use this file except in compliance with the License.
     5 +// You may obtain a copy of the License at
     6 +//
     7 +// http://www.apache.org/licenses/LICENSE-2.0
     8 +//
     9 +// Unless required by applicable law or agreed to in writing, software
     10 +// distributed under the License is distributed on an "AS IS" BASIS,
     11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 +// See the License for the specific language governing permissions and
     13 +// limitations under the License.
     14 + 
     15 +package canvas
     16 + 
     17 +import (
     18 + "image"
     19 + "testing"
     20 + 
     21 + "github.com/kylelemons/godebug/pretty"
     22 + "github.com/mum4k/termdash/internal/area"
     23 + "github.com/mum4k/termdash/internal/cell"
     24 + "github.com/mum4k/termdash/internal/terminal/faketerm"
     25 +)
     26 + 
     27 +func TestNew(t *testing.T) {
     28 + tests := []struct {
     29 + desc string
     30 + area image.Rectangle
     31 + wantSize image.Point
     32 + wantArea image.Rectangle
     33 + wantErr bool
     34 + }{
     35 + {
     36 + desc: "area min has negative X",
     37 + area: image.Rect(-1, 0, 0, 0),
     38 + wantErr: true,
     39 + },
     40 + {
     41 + desc: "area min has negative Y",
     42 + area: image.Rect(0, -1, 0, 0),
     43 + wantErr: true,
     44 + },
     45 + {
     46 + desc: "area max has negative X",
     47 + area: image.Rect(0, 0, -1, 0),
     48 + wantErr: true,
     49 + },
     50 + {
     51 + desc: "area max has negative Y",
     52 + area: image.Rect(0, 0, 0, -1),
     53 + wantErr: true,
     54 + },
     55 + {
     56 + desc: "zero area is invalid",
     57 + area: image.Rect(0, 0, 0, 0),
     58 + wantErr: true,
     59 + },
     60 + {
     61 + desc: "smallest valid size",
     62 + area: image.Rect(0, 0, 1, 1),
     63 + wantSize: image.Point{1, 1},
     64 + wantArea: image.Rect(0, 0, 1, 1),
     65 + },
     66 + {
     67 + desc: "rectangular canvas 3 by 4",
     68 + area: image.Rect(0, 0, 3, 4),
     69 + wantSize: image.Point{3, 4},
     70 + wantArea: image.Rect(0, 0, 3, 4),
     71 + },
     72 + {
     73 + desc: "non-zero based area",
     74 + area: image.Rect(1, 1, 2, 3),
     75 + wantSize: image.Point{1, 2},
     76 + wantArea: image.Rect(0, 0, 1, 2),
     77 + },
     78 + }
     79 + 
     80 + for _, tc := range tests {
     81 + t.Run(tc.desc, func(t *testing.T) {
     82 + c, err := New(tc.area)
     83 + if (err != nil) != tc.wantErr {
     84 + t.Errorf("New => unexpected error: %v, wantErr: %v", err, tc.wantErr)
     85 + }
     86 + if err != nil {
     87 + return
     88 + }
     89 + 
     90 + gotSize := c.Size()
     91 + if diff := pretty.Compare(tc.wantSize, gotSize); diff != "" {
     92 + t.Errorf("Size => unexpected diff (-want, +got):\n%s", diff)
     93 + }
     94 + 
     95 + gotArea := c.Area()
     96 + if diff := pretty.Compare(tc.wantArea, gotArea); diff != "" {
     97 + t.Errorf("Area => unexpected diff (-want, +got):\n%s", diff)
     98 + }
     99 + })
     100 + }
     101 +}
     102 + 
     103 +func TestCanvas(t *testing.T) {
     104 + tests := []struct {
     105 + desc string
     106 + canvas image.Rectangle
     107 + ops func(*Canvas) error
     108 + want func(size image.Point) (*faketerm.Terminal, error)
     109 + wantErr bool
     110 + }{
     111 + {
     112 + desc: "SetCellOpts fails on a point outside of the canvas",
     113 + canvas: image.Rect(0, 0, 1, 1),
     114 + ops: func(cvs *Canvas) error {
     115 + return cvs.SetCellOpts(image.Point{1, 1})
     116 + },
     117 + wantErr: true,
     118 + },
     119 + {
     120 + desc: "SetCellOpts sets options on a cell with no options",
     121 + canvas: image.Rect(0, 0, 2, 2),
     122 + ops: func(cvs *Canvas) error {
     123 + return cvs.SetCellOpts(image.Point{0, 1}, cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue))
     124 + },
     125 + want: func(size image.Point) (*faketerm.Terminal, error) {
     126 + ft := faketerm.MustNew(size)
     127 + cvs, err := New(ft.Area())
     128 + if err != nil {
     129 + return nil, err
     130 + }
     131 + 
     132 + c, err := cvs.Cell(image.Point{0, 1})
     133 + if err != nil {
     134 + return nil, err
     135 + }
     136 + if _, err := cvs.SetCell(image.Point{0, 1}, c.Rune, cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue)); err != nil {
     137 + return nil, err
     138 + }
     139 + 
     140 + if err := cvs.Apply(ft); err != nil {
     141 + return nil, err
     142 + }
     143 + return ft, nil
     144 + },
     145 + },
     146 + {
     147 + desc: "SetCellOpts preserves cell rune",
     148 + canvas: image.Rect(0, 0, 2, 2),
     149 + ops: func(cvs *Canvas) error {
     150 + if _, err := cvs.SetCell(image.Point{0, 1}, 'X'); err != nil {
     151 + return err
     152 + }
     153 + return cvs.SetCellOpts(image.Point{0, 1}, cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue))
     154 + },
     155 + want: func(size image.Point) (*faketerm.Terminal, error) {
     156 + ft := faketerm.MustNew(size)
     157 + cvs, err := New(ft.Area())
     158 + if err != nil {
     159 + return nil, err
     160 + }
     161 + 
     162 + if _, err := cvs.SetCell(image.Point{0, 1}, 'X', cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue)); err != nil {
     163 + return nil, err
     164 + }
     165 + 
     166 + if err := cvs.Apply(ft); err != nil {
     167 + return nil, err
     168 + }
     169 + return ft, nil
     170 + },
     171 + },
     172 + {
     173 + desc: "SetCellOpts overwrites options set previously",
     174 + canvas: image.Rect(0, 0, 2, 2),
     175 + ops: func(cvs *Canvas) error {
     176 + if _, err := cvs.SetCell(image.Point{0, 1}, 'X', cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue)); err != nil {
     177 + return err
     178 + }
     179 + return cvs.SetCellOpts(image.Point{0, 1}, cell.FgColor(cell.ColorGreen), cell.BgColor(cell.ColorYellow))
     180 + },
     181 + want: func(size image.Point) (*faketerm.Terminal, error) {
     182 + ft := faketerm.MustNew(size)
     183 + cvs, err := New(ft.Area())
     184 + if err != nil {
     185 + return nil, err
     186 + }
     187 + 
     188 + if _, err := cvs.SetCell(image.Point{0, 1}, 'X', cell.FgColor(cell.ColorGreen), cell.BgColor(cell.ColorYellow)); err != nil {
     189 + return nil, err
     190 + }
     191 + 
     192 + if err := cvs.Apply(ft); err != nil {
     193 + return nil, err
     194 + }
     195 + return ft, nil
     196 + },
     197 + },
     198 + {
     199 + desc: "SetCellOpts sets default options when no options provided",
     200 + canvas: image.Rect(0, 0, 2, 2),
     201 + ops: func(cvs *Canvas) error {
     202 + if _, err := cvs.SetCell(image.Point{0, 1}, 'X', cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue)); err != nil {
     203 + return err
     204 + }
     205 + return cvs.SetCellOpts(image.Point{0, 1})
     206 + },
     207 + want: func(size image.Point) (*faketerm.Terminal, error) {
     208 + ft := faketerm.MustNew(size)
     209 + cvs, err := New(ft.Area())
     210 + if err != nil {
     211 + return nil, err
     212 + }
     213 + 
     214 + if _, err := cvs.SetCell(image.Point{0, 1}, 'X'); err != nil {
     215 + return nil, err
     216 + }
     217 + 
     218 + if err := cvs.Apply(ft); err != nil {
     219 + return nil, err
     220 + }
     221 + return ft, nil
     222 + },
     223 + },
     224 + {
     225 + desc: "SetCellOpts is idempotent",
     226 + canvas: image.Rect(0, 0, 2, 2),
     227 + ops: func(cvs *Canvas) error {
     228 + if _, err := cvs.SetCell(image.Point{0, 1}, 'X'); err != nil {
     229 + return err
     230 + }
     231 + if err := cvs.SetCellOpts(image.Point{0, 1}, cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue)); err != nil {
     232 + return err
     233 + }
     234 + return cvs.SetCellOpts(image.Point{0, 1}, cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue))
     235 + },
     236 + want: func(size image.Point) (*faketerm.Terminal, error) {
     237 + ft := faketerm.MustNew(size)
     238 + cvs, err := New(ft.Area())
     239 + if err != nil {
     240 + return nil, err
     241 + }
     242 + 
     243 + if _, err := cvs.SetCell(image.Point{0, 1}, 'X', cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue)); err != nil {
     244 + return nil, err
     245 + }
     246 + 
     247 + if err := cvs.Apply(ft); err != nil {
     248 + return nil, err
     249 + }
     250 + return ft, nil
     251 + },
     252 + },
     253 + {
     254 + desc: "SetAreaCellOpts fails on area too large",
     255 + canvas: image.Rect(0, 0, 1, 1),
     256 + ops: func(cvs *Canvas) error {
     257 + return cvs.SetAreaCellOpts(image.Rect(0, 0, 2, 2), cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue))
     258 + },
     259 + wantErr: true,
     260 + },
     261 + {
     262 + desc: "SetAreaCellOpts sets options in the full canvas",
     263 + canvas: image.Rect(0, 0, 1, 1),
     264 + ops: func(cvs *Canvas) error {
     265 + return cvs.SetAreaCellOpts(image.Rect(0, 0, 1, 1), cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue))
     266 + },
     267 + want: func(size image.Point) (*faketerm.Terminal, error) {
     268 + ft := faketerm.MustNew(size)
     269 + cvs, err := New(ft.Area())
     270 + if err != nil {
     271 + return nil, err
     272 + }
     273 + 
     274 + for _, p := range []image.Point{
     275 + {0, 0},
     276 + } {
     277 + c, err := cvs.Cell(p)
     278 + if err != nil {
     279 + return nil, err
     280 + }
     281 + if _, err := cvs.SetCell(p, c.Rune, cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue)); err != nil {
     282 + return nil, err
     283 + }
     284 + }
     285 + 
     286 + if err := cvs.Apply(ft); err != nil {
     287 + return nil, err
     288 + }
     289 + return ft, nil
     290 + },
     291 + },
     292 + {
     293 + desc: "SetAreaCellOpts sets options in a sub-area",
     294 + canvas: image.Rect(0, 0, 3, 3),
     295 + ops: func(cvs *Canvas) error {
     296 + return cvs.SetAreaCellOpts(image.Rect(0, 0, 2, 2), cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue))
     297 + },
     298 + want: func(size image.Point) (*faketerm.Terminal, error) {
     299 + ft := faketerm.MustNew(size)
     300 + cvs, err := New(ft.Area())
     301 + if err != nil {
     302 + return nil, err
     303 + }
     304 + 
     305 + for _, p := range []image.Point{
     306 + {0, 0},
     307 + {0, 1},
     308 + {1, 0},
     309 + {1, 1},
     310 + } {
     311 + c, err := cvs.Cell(p)
     312 + if err != nil {
     313 + return nil, err
     314 + }
     315 + if _, err := cvs.SetCell(p, c.Rune, cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue)); err != nil {
     316 + return nil, err
     317 + }
     318 + }
     319 + 
     320 + if err := cvs.Apply(ft); err != nil {
     321 + return nil, err
     322 + }
     323 + return ft, nil
     324 + },
     325 + },
     326 + {
     327 + desc: "SetAreaCells sets cells in the full canvas",
     328 + canvas: image.Rect(0, 0, 1, 1),
     329 + ops: func(cvs *Canvas) error {
     330 + return cvs.SetAreaCells(image.Rect(0, 0, 1, 1), 'r')
     331 + },
     332 + want: func(size image.Point) (*faketerm.Terminal, error) {
     333 + ft := faketerm.MustNew(size)
     334 + cvs, err := New(ft.Area())
     335 + if err != nil {
     336 + return nil, err
     337 + }
     338 + 
     339 + if _, err := cvs.SetCell(image.Point{0, 0}, 'r'); err != nil {
     340 + return nil, err
     341 + }
     342 + 
     343 + if err := cvs.Apply(ft); err != nil {
     344 + return nil, err
     345 + }
     346 + return ft, nil
     347 + },
     348 + },
     349 + {
     350 + desc: "SetAreaCells is idempotent",
     351 + canvas: image.Rect(0, 0, 1, 1),
     352 + ops: func(cvs *Canvas) error {
     353 + if err := cvs.SetAreaCells(image.Rect(0, 0, 1, 1), 'r'); err != nil {
     354 + return err
     355 + }
     356 + return cvs.SetAreaCells(image.Rect(0, 0, 1, 1), 'r')
     357 + },
     358 + want: func(size image.Point) (*faketerm.Terminal, error) {
     359 + ft := faketerm.MustNew(size)
     360 + cvs, err := New(ft.Area())
     361 + if err != nil {
     362 + return nil, err
     363 + }
     364 + 
     365 + if _, err := cvs.SetCell(image.Point{0, 0}, 'r'); err != nil {
     366 + return nil, err
     367 + }
     368 + 
     369 + if err := cvs.Apply(ft); err != nil {
     370 + return nil, err
     371 + }
     372 + return ft, nil
     373 + },
     374 + },
     375 + {
     376 + desc: "SetAreaCells fails on area too large",
     377 + canvas: image.Rect(0, 0, 1, 1),
     378 + ops: func(cvs *Canvas) error {
     379 + return cvs.SetAreaCells(image.Rect(0, 0, 2, 2), 'r', cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue))
     380 + },
     381 + wantErr: true,
     382 + },
     383 + {
     384 + desc: "SetAreaCells sets cell options",
     385 + canvas: image.Rect(0, 0, 1, 1),
     386 + ops: func(cvs *Canvas) error {
     387 + return cvs.SetAreaCells(image.Rect(0, 0, 1, 1), 'r', cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue))
     388 + },
     389 + want: func(size image.Point) (*faketerm.Terminal, error) {
     390 + ft := faketerm.MustNew(size)
     391 + cvs, err := New(ft.Area())
     392 + if err != nil {
     393 + return nil, err
     394 + }
     395 + 
     396 + if _, err := cvs.SetCell(image.Point{0, 0}, 'r', cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue)); err != nil {
     397 + return nil, err
     398 + }
     399 + 
     400 + if err := cvs.Apply(ft); err != nil {
     401 + return nil, err
     402 + }
     403 + return ft, nil
     404 + },
     405 + },
     406 + {
     407 + desc: "SetAreaCells sets cell in a sub-area",
     408 + canvas: image.Rect(0, 0, 3, 3),
     409 + ops: func(cvs *Canvas) error {
     410 + return cvs.SetAreaCells(image.Rect(0, 0, 2, 2), 'p')
     411 + },
     412 + want: func(size image.Point) (*faketerm.Terminal, error) {
     413 + ft := faketerm.MustNew(size)
     414 + cvs, err := New(ft.Area())
     415 + if err != nil {
     416 + return nil, err
     417 + }
     418 + 
     419 + for _, p := range []image.Point{
     420 + {0, 0},
     421 + {0, 1},
     422 + {1, 0},
     423 + {1, 1},
     424 + } {
     425 + if _, err := cvs.SetCell(p, 'p'); err != nil {
     426 + return nil, err
     427 + }
     428 + }
     429 + 
     430 + if err := cvs.Apply(ft); err != nil {
     431 + return nil, err
     432 + }
     433 + return ft, nil
     434 + },
     435 + },
     436 + {
     437 + desc: "SetAreaCells sets full-width runes that fit",
     438 + canvas: image.Rect(0, 0, 3, 3),
     439 + ops: func(cvs *Canvas) error {
     440 + return cvs.SetAreaCells(image.Rect(0, 0, 2, 2), '世')
     441 + },
     442 + want: func(size image.Point) (*faketerm.Terminal, error) {
     443 + ft := faketerm.MustNew(size)
     444 + cvs, err := New(ft.Area())
     445 + if err != nil {
     446 + return nil, err
     447 + }
     448 + 
     449 + for _, p := range []image.Point{
     450 + {0, 0},
     451 + {0, 1},
     452 + } {
     453 + if _, err := cvs.SetCell(p, '世'); err != nil {
     454 + return nil, err
     455 + }
     456 + }
     457 + 
     458 + if err := cvs.Apply(ft); err != nil {
     459 + return nil, err
     460 + }
     461 + return ft, nil
     462 + },
     463 + },
     464 + {
     465 + desc: "SetAreaCells sets full-width runes that will leave a gap at the end of each row",
     466 + canvas: image.Rect(0, 0, 3, 3),
     467 + ops: func(cvs *Canvas) error {
     468 + return cvs.SetAreaCells(image.Rect(0, 0, 3, 3), '世')
     469 + },
     470 + want: func(size image.Point) (*faketerm.Terminal, error) {
     471 + ft := faketerm.MustNew(size)
     472 + cvs, err := New(ft.Area())
     473 + if err != nil {
     474 + return nil, err
     475 + }
     476 + 
     477 + for _, p := range []image.Point{
     478 + {0, 0},
     479 + {0, 1},
     480 + {0, 2},
     481 + } {
     482 + if _, err := cvs.SetCell(p, '世'); err != nil {
     483 + return nil, err
     484 + }
     485 + }
     486 + 
     487 + if err := cvs.Apply(ft); err != nil {
     488 + return nil, err
     489 + }
     490 + return ft, nil
     491 + },
     492 + },
     493 + }
     494 + 
     495 + for _, tc := range tests {
     496 + t.Run(tc.desc, func(t *testing.T) {
     497 + cvs, err := New(tc.canvas)
     498 + if err != nil {
     499 + t.Fatalf("New => unexpected error: %v", err)
     500 + }
     501 + 
     502 + if tc.ops != nil {
     503 + err := tc.ops(cvs)
     504 + if (err != nil) != tc.wantErr {
     505 + t.Errorf("tc.ops => unexpected error: %v, wantErr: %v", err, tc.wantErr)
     506 + }
     507 + if err != nil {
     508 + return
     509 + }
     510 + }
     511 + 
     512 + size := cvs.Size()
     513 + got, err := faketerm.New(size)
     514 + if err != nil {
     515 + t.Fatalf("faketerm.New => unexpected error: %v", err)
     516 + }
     517 + if err := cvs.Apply(got); err != nil {
     518 + t.Fatalf("cvs.Apply => %v", err)
     519 + }
     520 + 
     521 + var want *faketerm.Terminal
     522 + if tc.want != nil {
     523 + want, err = tc.want(size)
     524 + if err != nil {
     525 + t.Fatalf("tc.want => unexpected error: %v", err)
     526 + }
     527 + } else {
     528 + w, err := faketerm.New(size)
     529 + if err != nil {
     530 + t.Fatalf("faketerm.New => unexpected error: %v", err)
     531 + }
     532 + want = w
     533 + }
     534 + 
     535 + if diff := faketerm.Diff(want, got); diff != "" {
     536 + t.Errorf("cvs.SetCellOpts => %v", diff)
     537 + }
     538 + })
     539 + }
     540 +}
     541 + 
     542 +func TestSetCellAndApply(t *testing.T) {
     543 + tests := []struct {
     544 + desc string
     545 + termSize image.Point
     546 + canvasArea image.Rectangle
     547 + point image.Point
     548 + r rune
     549 + opts []cell.Option
     550 + want cell.Buffer // Expected back buffer in the fake terminal.
     551 + wantCells int
     552 + wantSetCellErr bool
     553 + wantApplyErr bool
     554 + }{
     555 + {
     556 + desc: "setting cell outside the designated area",
     557 + termSize: image.Point{2, 2},
     558 + canvasArea: image.Rect(0, 0, 1, 1),
     559 + point: image.Point{0, 2},
     560 + wantSetCellErr: true,
     561 + },
     562 + {
     563 + desc: "sets a top-left corner cell",
     564 + termSize: image.Point{3, 3},
     565 + canvasArea: image.Rect(1, 1, 3, 3),
     566 + point: image.Point{0, 0},
     567 + r: 'X',
     568 + wantCells: 1,
     569 + want: cell.Buffer{
     570 + {
     571 + cell.New(0),
     572 + cell.New(0),
     573 + cell.New(0),
     574 + },
     575 + {
     576 + cell.New(0),
     577 + cell.New('X'),
     578 + cell.New(0),
     579 + },
     580 + {
     581 + cell.New(0),
     582 + cell.New(0),
     583 + cell.New(0),
     584 + },
     585 + },
     586 + },
     587 + {
     588 + desc: "sets a full-width rune in the top-left corner cell",
     589 + termSize: image.Point{3, 3},
     590 + canvasArea: image.Rect(1, 1, 3, 3),
     591 + point: image.Point{0, 0},
     592 + r: '界',
     593 + wantCells: 2,
     594 + want: cell.Buffer{
     595 + {
     596 + cell.New(0),
     597 + cell.New(0),
     598 + cell.New(0),
     599 + },
     600 + {
     601 + cell.New(0),
     602 + cell.New('界'),
     603 + cell.New(0),
     604 + },
     605 + {
     606 + cell.New(0),
     607 + cell.New(0),
     608 + cell.New(0),
     609 + },
     610 + },
     611 + },
     612 + {
     613 + desc: "not enough space for a full-width rune",
     614 + termSize: image.Point{3, 3},
     615 + canvasArea: image.Rect(1, 1, 3, 3),
     616 + point: image.Point{1, 0},
     617 + r: '界',
     618 + wantSetCellErr: true,
     619 + },
     620 + {
     621 + desc: "sets a top-right corner cell",
     622 + termSize: image.Point{3, 3},
     623 + canvasArea: image.Rect(1, 1, 3, 3),
     624 + point: image.Point{1, 0},
     625 + r: 'X',
     626 + wantCells: 1,
     627 + want: cell.Buffer{
     628 + {
     629 + cell.New(0),
     630 + cell.New(0),
     631 + cell.New(0),
     632 + },
     633 + {
     634 + cell.New(0),
     635 + cell.New(0),
     636 + cell.New(0),
     637 + },
     638 + {
     639 + cell.New(0),
     640 + cell.New('X'),
     641 + cell.New(0),
     642 + },
     643 + },
     644 + },
     645 + {
     646 + desc: "sets a bottom-left corner cell",
     647 + termSize: image.Point{3, 3},
     648 + canvasArea: image.Rect(1, 1, 3, 3),
     649 + point: image.Point{0, 1},
     650 + r: 'X',
     651 + wantCells: 1,
     652 + want: cell.Buffer{
     653 + {
     654 + cell.New(0),
     655 + cell.New(0),
     656 + cell.New(0),
     657 + },
     658 + {
     659 + cell.New(0),
     660 + cell.New(0),
     661 + cell.New('X'),
     662 + },
     663 + {
     664 + cell.New(0),
     665 + cell.New(0),
     666 + cell.New(0),
     667 + },
     668 + },
     669 + },
     670 + {
     671 + desc: "sets a bottom-right corner cell",
     672 + termSize: image.Point{3, 3},
     673 + canvasArea: image.Rect(1, 1, 3, 3),
     674 + point: image.Point{1, 1},
     675 + r: 'Z',
     676 + wantCells: 1,
     677 + want: cell.Buffer{
     678 + {
     679 + cell.New(0),
     680 + cell.New(0),
     681 + cell.New(0),
     682 + },
     683 + {
     684 + cell.New(0),
     685 + cell.New(0),
     686 + cell.New(0),
     687 + },
     688 + {
     689 + cell.New(0),
     690 + cell.New(0),
     691 + cell.New('Z'),
     692 + },
     693 + },
     694 + },
     695 + {
     696 + desc: "sets cell options",
     697 + termSize: image.Point{3, 3},
     698 + canvasArea: image.Rect(1, 1, 3, 3),
     699 + point: image.Point{1, 1},
     700 + r: 'A',
     701 + opts: []cell.Option{
     702 + cell.BgColor(cell.ColorRed),
     703 + },
     704 + wantCells: 1,
     705 + want: cell.Buffer{
     706 + {
     707 + cell.New(0),
     708 + cell.New(0),
     709 + cell.New(0),
     710 + },
     711 + {
     712 + cell.New(0),
     713 + cell.New(0),
     714 + cell.New(0),
     715 + },
     716 + {
     717 + cell.New(0),
     718 + cell.New(0),
     719 + cell.New('A', cell.BgColor(cell.ColorRed)),
     720 + },
     721 + },
     722 + },
     723 + {
     724 + desc: "canvas size equals terminal size",
     725 + termSize: image.Point{1, 1},
     726 + canvasArea: image.Rect(0, 0, 1, 1),
     727 + point: image.Point{0, 0},
     728 + r: 'A',
     729 + wantCells: 1,
     730 + want: cell.Buffer{
     731 + {
     732 + cell.New('A'),
     733 + },
     734 + },
     735 + },
     736 + {
     737 + desc: "terminal too small for the area",
     738 + termSize: image.Point{1, 1},
     739 + canvasArea: image.Rect(0, 0, 2, 2),
     740 + point: image.Point{0, 0},
     741 + r: 'A',
     742 + wantCells: 1,
     743 + wantApplyErr: true,
     744 + },
     745 + }
     746 + 
     747 + for _, tc := range tests {
     748 + t.Run(tc.desc, func(t *testing.T) {
     749 + c, err := New(tc.canvasArea)
     750 + if err != nil {
     751 + t.Fatalf("New => unexpected error: %v", err)
     752 + }
     753 + 
     754 + gotCells, err := c.SetCell(tc.point, tc.r, tc.opts...)
     755 + if (err != nil) != tc.wantSetCellErr {
     756 + t.Errorf("SetCell => unexpected error: %v, wantSetCellErr: %v", err, tc.wantSetCellErr)
     757 + }
     758 + if err != nil {
     759 + return
     760 + }
     761 + 
     762 + if gotCells != tc.wantCells {
     763 + t.Errorf("SetCell => unexpected number of cells %d, want %d", gotCells, tc.wantCells)
     764 + }
     765 + 
     766 + ft, err := faketerm.New(tc.termSize)
     767 + if err != nil {
     768 + t.Fatalf("faketerm.New => unexpected error: %v", err)
     769 + }
     770 + err = c.Apply(ft)
     771 + if (err != nil) != tc.wantApplyErr {
     772 + t.Errorf("Apply => unexpected error: %v, wantApplyErr: %v", err, tc.wantApplyErr)
     773 + }
     774 + if err != nil {
     775 + return
     776 + }
     777 + 
     778 + got := ft.BackBuffer()
     779 + if diff := pretty.Compare(tc.want, got); diff != "" {
     780 + t.Errorf("faketerm.BackBuffer => unexpected diff (-want, +got):\n%s", diff)
     781 + }
     782 + })
     783 + }
     784 +}
     785 + 
     786 +func TestClear(t *testing.T) {
     787 + c, err := New(image.Rect(1, 1, 3, 3))
     788 + if err != nil {
     789 + t.Fatalf("New => unexpected error: %v", err)
     790 + }
     791 + 
     792 + if _, err := c.SetCell(image.Point{0, 0}, 'X'); err != nil {
     793 + t.Fatalf("SetCell => unexpected error: %v", err)
     794 + }
     795 + 
     796 + ft, err := faketerm.New(image.Point{3, 3})
     797 + if err != nil {
     798 + t.Fatalf("faketerm.New => unexpected error: %v", err)
     799 + }
     800 + // Set one cell outside of the canvas on the terminal.
     801 + if err := ft.SetCell(image.Point{0, 0}, 'A'); err != nil {
     802 + t.Fatalf("faketerm.SetCell => unexpected error: %v", err)
     803 + }
     804 + 
     805 + if err := c.Apply(ft); err != nil {
     806 + t.Fatalf("Apply => unexpected error: %v", err)
     807 + }
     808 + 
     809 + want := cell.Buffer{
     810 + {
     811 + cell.New('A'),
     812 + cell.New(0),
     813 + cell.New(0),
     814 + },
     815 + {
     816 + cell.New(0),
     817 + cell.New('X'),
     818 + cell.New(0),
     819 + },
     820 + {
     821 + cell.New(0),
     822 + cell.New(0),
     823 + cell.New(0),
     824 + },
     825 + }
     826 + got := ft.BackBuffer()
     827 + if diff := pretty.Compare(want, got); diff != "" {
     828 + t.Errorf("faketerm.BackBuffer before Clear => unexpected diff (-want, +got):\n%s", diff)
     829 + }
     830 + 
     831 + // Call Clear(), Apply() and verify that only the area belonging to the
     832 + // canvas was cleared.
     833 + if err := c.Clear(); err != nil {
     834 + t.Fatalf("Clear => unexpected error: %v", err)
     835 + }
     836 + if err := c.Apply(ft); err != nil {
     837 + t.Fatalf("Apply => unexpected error: %v", err)
     838 + }
     839 + 
     840 + want = cell.Buffer{
     841 + {
     842 + cell.New('A'),
     843 + cell.New(0),
     844 + cell.New(0),
     845 + },
     846 + {
     847 + cell.New(0),
     848 + cell.New(0),
     849 + cell.New(0),
     850 + },
     851 + {
     852 + cell.New(0),
     853 + cell.New(0),
     854 + cell.New(0),
     855 + },
     856 + }
     857 + 
     858 + got = ft.BackBuffer()
     859 + if diff := pretty.Compare(want, got); diff != "" {
     860 + t.Errorf("faketerm.BackBuffer after Clear => unexpected diff (-want, +got):\n%s", diff)
     861 + }
     862 +}
     863 + 
     864 +// TestApplyFullWidthRunes verifies that when applying a full-width rune to the
     865 +// terminal, canvas doesn't touch the neighbor cell that holds the remaining
     866 +// part of the full-width rune.
     867 +func TestApplyFullWidthRunes(t *testing.T) {
     868 + ar := image.Rect(0, 0, 3, 3)
     869 + c, err := New(ar)
     870 + if err != nil {
     871 + t.Fatalf("New => unexpected error: %v", err)
     872 + }
     873 + 
     874 + fullP := image.Point{0, 0}
     875 + if _, err := c.SetCell(fullP, '界'); err != nil {
     876 + t.Fatalf("SetCell => unexpected error: %v", err)
     877 + }
     878 + 
     879 + ft, err := faketerm.New(area.Size(ar))
     880 + if err != nil {
     881 + t.Fatalf("faketerm.New => unexpected error: %v", err)
     882 + }
     883 + partP := image.Point{1, 0}
     884 + if err := ft.SetCell(partP, 'A'); err != nil {
     885 + t.Fatalf("faketerm.SetCell => unexpected error: %v", err)
     886 + }
     887 + 
     888 + if err := c.Apply(ft); err != nil {
     889 + t.Fatalf("Apply => unexpected error: %v", err)
     890 + }
     891 + 
     892 + want, err := cell.NewBuffer(area.Size(ar))
     893 + if err != nil {
     894 + t.Fatalf("NewBuffer => unexpected error: %v", err)
     895 + }
     896 + want[fullP.X][fullP.Y].Rune = '界'
     897 + want[partP.X][partP.Y].Rune = 'A'
     898 + 
     899 + got := ft.BackBuffer()
     900 + if diff := pretty.Compare(want, got); diff != "" {
     901 + t.Errorf("faketerm.BackBuffer => unexpected diff (-want, +got):\n%s", diff)
     902 + }
     903 +}
     904 + 
     905 +func TestCell(t *testing.T) {
     906 + tests := []struct {
     907 + desc string
     908 + cvs func() (*Canvas, error)
     909 + point image.Point
     910 + want *cell.Cell
     911 + wantErr bool
     912 + }{
     913 + {
     914 + desc: "requested point falls outside of the canvas",
     915 + cvs: func() (*Canvas, error) {
     916 + cvs, err := New(image.Rect(0, 0, 1, 1))
     917 + if err != nil {
     918 + return nil, err
     919 + }
     920 + return cvs, nil
     921 + },
     922 + point: image.Point{1, 1},
     923 + wantErr: true,
     924 + },
     925 + {
     926 + desc: "returns the cell",
     927 + cvs: func() (*Canvas, error) {
     928 + cvs, err := New(image.Rect(0, 0, 2, 2))
     929 + if err != nil {
     930 + return nil, err
     931 + }
     932 + if _, err := cvs.SetCell(
     933 + image.Point{1, 1}, 'A',
     934 + cell.FgColor(cell.ColorRed),
     935 + cell.BgColor(cell.ColorBlue),
     936 + ); err != nil {
     937 + return nil, err
     938 + }
     939 + return cvs, nil
     940 + },
     941 + point: image.Point{1, 1},
     942 + want: &cell.Cell{
     943 + Rune: 'A',
     944 + Opts: cell.NewOptions(
     945 + cell.FgColor(cell.ColorRed),
     946 + cell.BgColor(cell.ColorBlue),
     947 + ),
     948 + },
     949 + },
     950 + }
     951 + 
     952 + for _, tc := range tests {
     953 + t.Run(tc.desc, func(t *testing.T) {
     954 + cvs, err := tc.cvs()
     955 + if err != nil {
     956 + t.Fatalf("tc.cvs => unexpected error: %v", err)
     957 + }
     958 + 
     959 + got, err := cvs.Cell(tc.point)
     960 + if (err != nil) != tc.wantErr {
     961 + t.Errorf("Cell => unexpected error: %v, wantErr: %v", err, tc.wantErr)
     962 + }
     963 + if err != nil {
     964 + return
     965 + }
     966 + 
     967 + if diff := pretty.Compare(tc.want, got); diff != "" {
     968 + t.Errorf("Cell => unexpected diff (-want, +got):\n%s", diff)
     969 + }
     970 + })
     971 + }
     972 +}
     973 + 
     974 +// mustNew creates a new Canvas or panics.
     975 +func mustNew(ar image.Rectangle) *Canvas {
     976 + c, err := New(ar)
     977 + if err != nil {
     978 + panic(err)
     979 + }
     980 + return c
     981 +}
     982 + 
     983 +// mustFill fills the canvas with the specified runes or panics.
     984 +func mustFill(c *Canvas, r rune) {
     985 + ar := c.Area()
     986 + for col := 0; col < ar.Max.X; col++ {
     987 + for row := 0; row < ar.Max.Y; row++ {
     988 + if _, err := c.SetCell(image.Point{col, row}, r); err != nil {
     989 + panic(err)
     990 + }
     991 + }
     992 + }
     993 +}
     994 + 
     995 +// mustSetCell sets cell at the specified point of the canvas or panics.
     996 +func mustSetCell(c *Canvas, p image.Point, r rune, opts ...cell.Option) {
     997 + if _, err := c.SetCell(p, r, opts...); err != nil {
     998 + panic(err)
     999 + }
     1000 +}
     1001 + 
     1002 +func TestCopyTo(t *testing.T) {
     1003 + tests := []struct {
     1004 + desc string
     1005 + src *Canvas
     1006 + dst *Canvas
     1007 + want *Canvas
     1008 + wantErr bool
     1009 + }{
     1010 + {
     1011 + desc: "fails when the canvas doesn't fit",
     1012 + src: func() *Canvas {
     1013 + c := mustNew(image.Rect(0, 0, 3, 3))
     1014 + mustFill(c, 'X')
     1015 + return c
     1016 + }(),
     1017 + dst: mustNew(image.Rect(0, 0, 2, 2)),
     1018 + want: mustNew(image.Rect(0, 0, 3, 3)),
     1019 + wantErr: true,
     1020 + },
     1021 + {
     1022 + desc: "fails when the area lies outside of the destination canvas",
     1023 + src: func() *Canvas {
     1024 + c := mustNew(image.Rect(3, 3, 4, 4))
     1025 + mustFill(c, 'X')
     1026 + return c
     1027 + }(),
     1028 + dst: mustNew(image.Rect(0, 0, 3, 3)),
     1029 + want: mustNew(image.Rect(0, 0, 3, 3)),
     1030 + wantErr: true,
     1031 + },
     1032 + {
     1033 + desc: "copies zero based same size canvases",
     1034 + src: func() *Canvas {
     1035 + c := mustNew(image.Rect(0, 0, 3, 3))
     1036 + mustFill(c, 'X')
     1037 + return c
     1038 + }(),
     1039 + dst: mustNew(image.Rect(0, 0, 3, 3)),
     1040 + want: func() *Canvas {
     1041 + c := mustNew(image.Rect(0, 0, 3, 3))
     1042 + mustSetCell(c, image.Point{0, 0}, 'X')
     1043 + mustSetCell(c, image.Point{1, 0}, 'X')
     1044 + mustSetCell(c, image.Point{2, 0}, 'X')
     1045 + 
     1046 + mustSetCell(c, image.Point{0, 1}, 'X')
     1047 + mustSetCell(c, image.Point{1, 1}, 'X')
     1048 + mustSetCell(c, image.Point{2, 1}, 'X')
     1049 + 
     1050 + mustSetCell(c, image.Point{0, 2}, 'X')
     1051 + mustSetCell(c, image.Point{1, 2}, 'X')
     1052 + mustSetCell(c, image.Point{2, 2}, 'X')
     1053 + return c
     1054 + }(),
     1055 + },
     1056 + {
     1057 + desc: "copies smaller canvas with an offset",
     1058 + src: func() *Canvas {
     1059 + c := mustNew(image.Rect(1, 1, 2, 2))
     1060 + mustFill(c, 'X')
     1061 + return c
     1062 + }(),
     1063 + dst: mustNew(image.Rect(0, 0, 3, 3)),
     1064 + want: func() *Canvas {
     1065 + c := mustNew(image.Rect(0, 0, 3, 3))
     1066 + mustSetCell(c, image.Point{1, 1}, 'X')
     1067 + return c
     1068 + }(),
     1069 + },
     1070 + {
     1071 + desc: "copies smaller canvas with an offset into a canvas with offset from terminal",
     1072 + src: func() *Canvas {
     1073 + c := mustNew(image.Rect(1, 1, 2, 2))
     1074 + mustFill(c, 'X')
     1075 + return c
     1076 + }(),
     1077 + dst: mustNew(image.Rect(3, 3, 6, 6)),
     1078 + want: func() *Canvas {
     1079 + c := mustNew(image.Rect(3, 3, 6, 6))
     1080 + mustSetCell(c, image.Point{1, 1}, 'X')
     1081 + return c
     1082 + }(),
     1083 + },
     1084 + {
     1085 + desc: "copies cell options",
     1086 + src: func() *Canvas {
     1087 + c := mustNew(image.Rect(0, 0, 1, 1))
     1088 + mustSetCell(c, image.Point{0, 0}, 'X',
     1089 + cell.FgColor(cell.ColorRed),
     1090 + cell.BgColor(cell.ColorBlue),
     1091 + )
     1092 + return c
     1093 + }(),
     1094 + dst: mustNew(image.Rect(0, 0, 3, 1)),
     1095 + want: func() *Canvas {
     1096 + c := mustNew(image.Rect(0, 0, 3, 1))
     1097 + mustSetCell(c, image.Point{0, 0}, 'X',
     1098 + cell.FgColor(cell.ColorRed),
     1099 + cell.BgColor(cell.ColorBlue),
     1100 + )
     1101 + return c
     1102 + }(),
     1103 + },
     1104 + {
     1105 + desc: "copies cells with full-width runes",
     1106 + src: func() *Canvas {
     1107 + c := mustNew(image.Rect(0, 0, 3, 3))
     1108 + mustSetCell(c, image.Point{0, 0}, '界')
     1109 + mustSetCell(c, image.Point{1, 1}, '界')
     1110 + return c
     1111 + }(),
     1112 + dst: mustNew(image.Rect(0, 0, 3, 3)),
     1113 + want: func() *Canvas {
     1114 + c := mustNew(image.Rect(0, 0, 3, 3))
     1115 + mustSetCell(c, image.Point{0, 0}, '界')
     1116 + mustSetCell(c, image.Point{1, 1}, '界')
     1117 + return c
     1118 + }(),
     1119 + },
     1120 + }
     1121 + 
     1122 + for _, tc := range tests {
     1123 + t.Run(tc.desc, func(t *testing.T) {
     1124 + err := tc.src.CopyTo(tc.dst)
     1125 + if (err != nil) != tc.wantErr {
     1126 + t.Errorf("CopyTo => unexpected error: %v, wantErr: %v", err, tc.wantErr)
     1127 + }
     1128 + if err != nil {
     1129 + return
     1130 + }
     1131 + 
     1132 + ftSize := image.Point{10, 10}
     1133 + got, err := faketerm.New(ftSize)
     1134 + if err != nil {
     1135 + t.Fatalf("faketerm.New(tc.dst.Size()) => unexpected error: %v", err)
     1136 + }
     1137 + if err := tc.dst.Apply(got); err != nil {
     1138 + t.Fatalf("tc.dst.Apply => unexpected error: %v", err)
     1139 + }
     1140 + 
     1141 + want, err := faketerm.New(ftSize)
     1142 + if err != nil {
     1143 + t.Fatalf("faketerm.New(tc.want.Size()) => unexpected error: %v", err)
     1144 + }
     1145 + 
     1146 + if err := tc.want.Apply(want); err != nil {
     1147 + t.Fatalf("tc.want.Apply => unexpected error: %v", err)
     1148 + }
     1149 + 
     1150 + if diff := faketerm.Diff(want, got); diff != "" {
     1151 + t.Errorf("CopyTo => %v", diff)
     1152 + }
     1153 + })
     1154 + }
     1155 +}
     1156 + 
  • ■ ■ ■ ■ ■
    canvas/testcanvas/testcanvas.go internal/canvas/testcanvas/testcanvas.go
    skipped 18 lines
    19 19   "fmt"
    20 20   "image"
    21 21   
    22  - "github.com/mum4k/termdash/canvas"
    23  - "github.com/mum4k/termdash/cell"
    24  - "github.com/mum4k/termdash/terminal/faketerm"
     22 + "github.com/mum4k/termdash/internal/canvas"
     23 + "github.com/mum4k/termdash/internal/cell"
     24 + "github.com/mum4k/termdash/internal/terminal/faketerm"
    25 25  )
    26 26   
    27 27  // MustNew returns a new canvas or panics.
    skipped 21 lines
    49 49   panic(fmt.Sprintf("canvas.SetCell => unexpected error: %v", err))
    50 50   }
    51 51   return cells
     52 +}
     53 + 
     54 +// MustSetAreaCells sets the cells in the area or panics.
     55 +func MustSetAreaCells(c *canvas.Canvas, cellArea image.Rectangle, r rune, opts ...cell.Option) {
     56 + if err := c.SetAreaCells(cellArea, r, opts...); err != nil {
     57 + panic(fmt.Sprintf("canvas.SetAreaCells => unexpected error: %v", err))
     58 + }
     59 +}
     60 + 
     61 +// MustCell returns the cell or panics.
     62 +func MustCell(c *canvas.Canvas, p image.Point) *cell.Cell {
     63 + cell, err := c.Cell(p)
     64 + if err != nil {
     65 + panic(fmt.Sprintf("canvas.Cell => unexpected error: %v", err))
     66 + }
     67 + return cell
    52 68  }
    53 69   
    54 70  // MustCopyTo copies the content of the source canvas onto the destination
    skipped 7 lines
  • ■ ■ ■ ■ ■ ■
    cell/cell.go internal/cell/cell.go
    skipped 22 lines
    23 23   "fmt"
    24 24   "image"
    25 25   
    26  - runewidth "github.com/mattn/go-runewidth"
    27  - "github.com/mum4k/termdash/area"
     26 + "github.com/mum4k/termdash/internal/area"
     27 + "github.com/mum4k/termdash/internal/cell/runewidth"
    28 28  )
    29 29   
    30 30  // Option is used to provide options for cells on a 2-D terminal.
    skipped 191 lines
  • cell/cell_test.go internal/cell/cell_test.go
    Content is identical
  • cell/color.go internal/cell/color.go
    Content is identical
  • cell/color_test.go internal/cell/color_test.go
    Content is identical
  • ■ ■ ■ ■ ■ ■
    internal/cell/runewidth/runewidth.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 runewidth is a wrapper over github.com/mattn/go-runewidth which
     16 +// gives different treatment to certain runes with ambiguous width.
     17 +package runewidth
     18 + 
     19 +import runewidth "github.com/mattn/go-runewidth"
     20 + 
     21 +// RuneWidth returns the number of cells needed to draw r.
     22 +// Background in http://www.unicode.org/reports/tr11/.
     23 +//
     24 +// Treats runes used internally by termdash as single-cell (half-width) runes
     25 +// regardless of the locale. I.e. runes that are used to draw lines, boxes,
     26 +// indicate resize or text trimming was needed and runes used by the braille
     27 +// canvas.
     28 +//
     29 +// This should be safe, since even in locales where these runes have ambiguous
     30 +// width, we still place all the character content around them so they should
     31 +// have be half-width.
     32 +func RuneWidth(r rune) int {
     33 + if inTable(r, exceptions) {
     34 + return 1
     35 + }
     36 + return runewidth.RuneWidth(r)
     37 +}
     38 + 
     39 +// StringWidth is like RuneWidth, but returns the number of cells occupied by
     40 +// all the runes in the string.
     41 +func StringWidth(s string) int {
     42 + var width int
     43 + for _, r := range []rune(s) {
     44 + width += RuneWidth(r)
     45 + }
     46 + return width
     47 +}
     48 + 
     49 +// inTable determines if the rune falls within the table.
     50 +// Copied from github.com/mattn/go-runewidth/blob/master/runewidth.go.
     51 +func inTable(r rune, t table) bool {
     52 + // func (t table) IncludesRune(r rune) bool {
     53 + if r < t[0].first {
     54 + return false
     55 + }
     56 + 
     57 + bot := 0
     58 + top := len(t) - 1
     59 + for top >= bot {
     60 + mid := (bot + top) >> 1
     61 + 
     62 + switch {
     63 + case t[mid].last < r:
     64 + bot = mid + 1
     65 + case t[mid].first > r:
     66 + top = mid - 1
     67 + default:
     68 + return true
     69 + }
     70 + }
     71 + 
     72 + return false
     73 +}
     74 + 
     75 +type interval struct {
     76 + first rune
     77 + last rune
     78 +}
     79 + 
     80 +type table []interval
     81 + 
     82 +// exceptions runes defined here are always considered to be half-width even if
     83 +// they might be ambiguous in some contexts.
     84 +var exceptions = table{
     85 + // Characters used by termdash to indicate text trim or scroll.
     86 + {0x2026, 0x2026},
     87 + {0x21c4, 0x21c4},
     88 + {0x21e7, 0x21e7},
     89 + {0x21e9, 0x21e9},
     90 + 
     91 + // Box drawing, used as line-styles.
     92 + // https://en.wikipedia.org/wiki/Box-drawing_character
     93 + {0x2500, 0x257F},
     94 + 
     95 + // Block elements used as sparks.
     96 + // https://en.wikipedia.org/wiki/Box-drawing_character
     97 + {0x2580, 0x258F},
     98 +}
     99 + 
  • ■ ■ ■ ■ ■ ■
    internal/cell/runewidth/runewidth_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 runewidth
     16 + 
     17 +import (
     18 + "testing"
     19 + 
     20 + runewidth "github.com/mattn/go-runewidth"
     21 +)
     22 + 
     23 +func TestRuneWidth(t *testing.T) {
     24 + tests := []struct {
     25 + desc string
     26 + runes []rune
     27 + eastAsian bool
     28 + want int
     29 + }{
     30 + {
     31 + desc: "ascii characters",
     32 + runes: []rune{'a', 'f', '#'},
     33 + want: 1,
     34 + },
     35 + {
     36 + desc: "non-printable characters from mattn/runewidth/runewidth_test",
     37 + runes: []rune{'\x00', '\x01', '\u0300', '\u2028', '\u2029'},
     38 + want: 0,
     39 + },
     40 + {
     41 + desc: "half-width runes from mattn/runewidth/runewidth_test",
     42 + runes: []rune{'セ', 'カ', 'イ', '☆'},
     43 + want: 1,
     44 + },
     45 + {
     46 + desc: "full-width runes from mattn/runewidth/runewidth_test",
     47 + runes: []rune{'世', '界'},
     48 + want: 2,
     49 + },
     50 + {
     51 + desc: "ambiguous so double-width in eastAsian from mattn/runewidth/runewidth_test",
     52 + runes: []rune{'☆'},
     53 + eastAsian: true,
     54 + want: 2,
     55 + },
     56 + {
     57 + desc: "braille runes",
     58 + runes: []rune{'⠀', '⠴', '⠷', '⣿'},
     59 + want: 1,
     60 + },
     61 + {
     62 + desc: "braille runes in eastAsian",
     63 + runes: []rune{'⠀', '⠴', '⠷', '⣿'},
     64 + eastAsian: true,
     65 + want: 1,
     66 + },
     67 + {
     68 + desc: "termdash special runes",
     69 + runes: []rune{'⇄', '…', '⇧', '⇩'},
     70 + want: 1,
     71 + },
     72 + {
     73 + desc: "termdash special runes in eastAsian",
     74 + runes: []rune{'⇄', '…', '⇧', '⇩'},
     75 + eastAsian: true,
     76 + want: 1,
     77 + },
     78 + {
     79 + desc: "termdash sparks",
     80 + runes: []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'},
     81 + want: 1,
     82 + },
     83 + {
     84 + desc: "termdash sparks in eastAsian",
     85 + runes: []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'},
     86 + eastAsian: true,
     87 + want: 1,
     88 + },
     89 + {
     90 + desc: "termdash line styles",
     91 + runes: []rune{'─', '═', '─', '┼', '╬', '┼'},
     92 + want: 1,
     93 + },
     94 + {
     95 + desc: "termdash line styles in eastAsian",
     96 + runes: []rune{'─', '═', '─', '┼', '╬', '┼'},
     97 + eastAsian: true,
     98 + want: 1,
     99 + },
     100 + }
     101 + 
     102 + for _, tc := range tests {
     103 + t.Run(tc.desc, func(t *testing.T) {
     104 + runewidth.DefaultCondition.EastAsianWidth = tc.eastAsian
     105 + defer func() {
     106 + runewidth.DefaultCondition.EastAsianWidth = false
     107 + }()
     108 + 
     109 + for _, r := range tc.runes {
     110 + if got := RuneWidth(r); got != tc.want {
     111 + t.Errorf("RuneWidth(%c, %#x) => %v, want %v", r, r, got, tc.want)
     112 + }
     113 + }
     114 + })
     115 + }
     116 +}
     117 + 
     118 +func TestStringWidth(t *testing.T) {
     119 + tests := []struct {
     120 + desc string
     121 + str string
     122 + eastAsian bool
     123 + want int
     124 + }{
     125 + {
     126 + desc: "ascii characters",
     127 + str: "hello",
     128 + want: 5,
     129 + },
     130 + {
     131 + desc: "string from mattn/runewidth/runewidth_test",
     132 + str: "■㈱の世界①",
     133 + want: 10,
     134 + },
     135 + {
     136 + desc: "string in eastAsian from mattn/runewidth/runewidth_test",
     137 + str: "■㈱の世界①",
     138 + eastAsian: true,
     139 + want: 12,
     140 + },
     141 + {
     142 + desc: "string using termdash characters",
     143 + str: "⇄…⇧⇩",
     144 + want: 4,
     145 + },
     146 + {
     147 + desc: "string in eastAsien using termdash characters",
     148 + str: "⇄…⇧⇩",
     149 + eastAsian: true,
     150 + want: 4,
     151 + },
     152 + }
     153 + 
     154 + for _, tc := range tests {
     155 + t.Run(tc.desc, func(t *testing.T) {
     156 + runewidth.DefaultCondition.EastAsianWidth = tc.eastAsian
     157 + defer func() {
     158 + runewidth.DefaultCondition.EastAsianWidth = false
     159 + }()
     160 + 
     161 + if got := StringWidth(tc.str); got != tc.want {
     162 + t.Errorf("StringWidth(%q) => %v, want %v", tc.str, got, tc.want)
     163 + }
     164 + })
     165 + }
     166 +}
     167 + 
  • ■ ■ ■ ■ ■ ■
    draw/border.go internal/draw/border.go
    skipped 19 lines
    20 20   "fmt"
    21 21   "image"
    22 22   
    23  - "github.com/mum4k/termdash/align"
    24  - "github.com/mum4k/termdash/canvas"
    25  - "github.com/mum4k/termdash/cell"
     23 + "github.com/mum4k/termdash/internal/align"
     24 + "github.com/mum4k/termdash/internal/canvas"
     25 + "github.com/mum4k/termdash/internal/cell"
    26 26  )
    27 27   
    28 28  // BorderOption is used to provide options to Border().
    skipped 153 lines
  • ■ ■ ■ ■ ■ ■
    draw/border_test.go internal/draw/border_test.go
    skipped 17 lines
    18 18   "image"
    19 19   "testing"
    20 20   
    21  - "github.com/mum4k/termdash/align"
    22  - "github.com/mum4k/termdash/canvas"
    23  - "github.com/mum4k/termdash/canvas/testcanvas"
    24  - "github.com/mum4k/termdash/cell"
    25  - "github.com/mum4k/termdash/terminal/faketerm"
     21 + "github.com/mum4k/termdash/internal/align"
     22 + "github.com/mum4k/termdash/internal/canvas"
     23 + "github.com/mum4k/termdash/internal/canvas/testcanvas"
     24 + "github.com/mum4k/termdash/internal/cell"
     25 + "github.com/mum4k/termdash/internal/terminal/faketerm"
    26 26  )
    27 27   
    28 28  func TestBorder(t *testing.T) {
    skipped 475 lines
  • ■ ■ ■ ■ ■ ■
    draw/braille_circle.go internal/draw/braille_circle.go
    skipped 19 lines
    20 20   "fmt"
    21 21   "image"
    22 22   
    23  - "github.com/mum4k/termdash/canvas/braille"
    24  - "github.com/mum4k/termdash/cell"
    25  - "github.com/mum4k/termdash/trig"
     23 + "github.com/mum4k/termdash/internal/canvas/braille"
     24 + "github.com/mum4k/termdash/internal/cell"
     25 + "github.com/mum4k/termdash/internal/numbers/trig"
    26 26  )
    27 27   
    28 28  // BrailleCircleOption is used to provide options to BrailleCircle.
    skipped 236 lines
  • ■ ■ ■ ■ ■ ■
    draw/braille_circle_test.go internal/draw/braille_circle_test.go
    skipped 17 lines
    18 18   "image"
    19 19   "testing"
    20 20   
    21  - "github.com/mum4k/termdash/area"
    22  - "github.com/mum4k/termdash/canvas/braille"
    23  - "github.com/mum4k/termdash/canvas/braille/testbraille"
    24  - "github.com/mum4k/termdash/cell"
    25  - "github.com/mum4k/termdash/terminal/faketerm"
     21 + "github.com/mum4k/termdash/internal/area"
     22 + "github.com/mum4k/termdash/internal/canvas/braille"
     23 + "github.com/mum4k/termdash/internal/canvas/braille/testbraille"
     24 + "github.com/mum4k/termdash/internal/cell"
     25 + "github.com/mum4k/termdash/internal/terminal/faketerm"
    26 26  )
    27 27   
    28 28  // mustBrailleLine draws the braille line or panics.
    skipped 1164 lines
  • ■ ■ ■ ■ ■ ■
    draw/braille_fill.go internal/draw/braille_fill.go
    skipped 19 lines
    20 20   "fmt"
    21 21   "image"
    22 22   
    23  - "github.com/mum4k/termdash/canvas/braille"
    24  - "github.com/mum4k/termdash/cell"
     23 + "github.com/mum4k/termdash/internal/canvas/braille"
     24 + "github.com/mum4k/termdash/internal/cell"
    25 25  )
    26 26   
    27 27  // BrailleFillOption is used to provide options to BrailleFill.
    skipped 134 lines
  • ■ ■ ■ ■ ■ ■
    draw/braille_fill_test.go internal/draw/braille_fill_test.go
    skipped 17 lines
    18 18   "image"
    19 19   "testing"
    20 20   
    21  - "github.com/mum4k/termdash/area"
    22  - "github.com/mum4k/termdash/canvas/braille"
    23  - "github.com/mum4k/termdash/canvas/braille/testbraille"
    24  - "github.com/mum4k/termdash/cell"
    25  - "github.com/mum4k/termdash/terminal/faketerm"
     21 + "github.com/mum4k/termdash/internal/area"
     22 + "github.com/mum4k/termdash/internal/canvas/braille"
     23 + "github.com/mum4k/termdash/internal/canvas/braille/testbraille"
     24 + "github.com/mum4k/termdash/internal/cell"
     25 + "github.com/mum4k/termdash/internal/terminal/faketerm"
    26 26  )
    27 27   
    28 28  func TestBrailleFill(t *testing.T) {
    skipped 243 lines
  • ■ ■ ■ ■ ■ ■
    draw/braille_line.go internal/draw/braille_line.go
    skipped 19 lines
    20 20   "fmt"
    21 21   "image"
    22 22   
    23  - "github.com/mum4k/termdash/canvas/braille"
    24  - "github.com/mum4k/termdash/cell"
    25  - "github.com/mum4k/termdash/numbers"
     23 + "github.com/mum4k/termdash/internal/canvas/braille"
     24 + "github.com/mum4k/termdash/internal/cell"
     25 + "github.com/mum4k/termdash/internal/numbers"
    26 26  )
    27 27   
    28 28  // braillePixelChange represents an action on a pixel on the braille canvas.
    skipped 177 lines
  • ■ ■ ■ ■ ■ ■
    draw/braille_line_test.go internal/draw/braille_line_test.go
    skipped 17 lines
    18 18   "image"
    19 19   "testing"
    20 20   
    21  - "github.com/mum4k/termdash/area"
    22  - "github.com/mum4k/termdash/canvas/braille"
    23  - "github.com/mum4k/termdash/canvas/braille/testbraille"
    24  - "github.com/mum4k/termdash/cell"
    25  - "github.com/mum4k/termdash/terminal/faketerm"
     21 + "github.com/mum4k/termdash/internal/area"
     22 + "github.com/mum4k/termdash/internal/canvas/braille"
     23 + "github.com/mum4k/termdash/internal/canvas/braille/testbraille"
     24 + "github.com/mum4k/termdash/internal/cell"
     25 + "github.com/mum4k/termdash/internal/terminal/faketerm"
    26 26  )
    27 27   
    28 28  func TestBrailleLine(t *testing.T) {
    skipped 430 lines
  • draw/draw.go internal/draw/draw.go
    Content is identical
  • ■ ■ ■ ■ ■ ■
    draw/hv_line.go internal/draw/hv_line.go
    skipped 19 lines
    20 20   "fmt"
    21 21   "image"
    22 22   
    23  - "github.com/mum4k/termdash/canvas"
    24  - "github.com/mum4k/termdash/cell"
     23 + "github.com/mum4k/termdash/internal/canvas"
     24 + "github.com/mum4k/termdash/internal/cell"
    25 25  )
    26 26   
    27 27  // HVLineOption is used to provide options to HVLine().
    skipped 180 lines
  • draw/hv_line_graph.go internal/draw/hv_line_graph.go
    Content is identical
  • ■ ■ ■ ■
    draw/hv_line_graph_test.go internal/draw/hv_line_graph_test.go
    skipped 19 lines
    20 20   "testing"
    21 21   
    22 22   "github.com/kylelemons/godebug/pretty"
    23  - "github.com/mum4k/termdash/canvas"
     23 + "github.com/mum4k/termdash/internal/canvas"
    24 24  )
    25 25   
    26 26  func TestMultiEdgeNodes(t *testing.T) {
    skipped 350 lines
  • ■ ■ ■ ■ ■ ■
    draw/hv_line_test.go internal/draw/hv_line_test.go
    skipped 17 lines
    18 18   "image"
    19 19   "testing"
    20 20   
    21  - "github.com/mum4k/termdash/canvas"
    22  - "github.com/mum4k/termdash/canvas/testcanvas"
    23  - "github.com/mum4k/termdash/cell"
    24  - "github.com/mum4k/termdash/terminal/faketerm"
     21 + "github.com/mum4k/termdash/internal/canvas"
     22 + "github.com/mum4k/termdash/internal/canvas/testcanvas"
     23 + "github.com/mum4k/termdash/internal/cell"
     24 + "github.com/mum4k/termdash/internal/terminal/faketerm"
    25 25  )
    26 26   
    27 27  func TestHVLines(t *testing.T) {
    skipped 647 lines
  • ■ ■ ■ ■
    draw/line_style.go internal/draw/line_style.go
    skipped 16 lines
    17 17  import (
    18 18   "fmt"
    19 19   
    20  - runewidth "github.com/mattn/go-runewidth"
     20 + "github.com/mum4k/termdash/internal/cell/runewidth"
    21 21  )
    22 22   
    23 23  // line_style.go contains the Unicode characters used for drawing lines of
    skipped 132 lines
  • ■ ■ ■ ■ ■ ■
    draw/rectangle.go internal/draw/rectangle.go
    skipped 19 lines
    20 20   "fmt"
    21 21   "image"
    22 22   
    23  - "github.com/mum4k/termdash/canvas"
    24  - "github.com/mum4k/termdash/cell"
     23 + "github.com/mum4k/termdash/internal/canvas"
     24 + "github.com/mum4k/termdash/internal/cell"
    25 25  )
    26 26   
    27 27  // RectangleOption is used to provide options to the Rectangle function.
    skipped 67 lines
  • ■ ■ ■ ■ ■ ■
    draw/rectangle_test.go internal/draw/rectangle_test.go
    skipped 17 lines
    18 18   "image"
    19 19   "testing"
    20 20   
    21  - "github.com/mum4k/termdash/canvas"
    22  - "github.com/mum4k/termdash/canvas/testcanvas"
    23  - "github.com/mum4k/termdash/cell"
    24  - "github.com/mum4k/termdash/terminal/faketerm"
     21 + "github.com/mum4k/termdash/internal/canvas"
     22 + "github.com/mum4k/termdash/internal/canvas/testcanvas"
     23 + "github.com/mum4k/termdash/internal/cell"
     24 + "github.com/mum4k/termdash/internal/terminal/faketerm"
    25 25  )
    26 26   
    27 27  func TestRectangle(t *testing.T) {
    skipped 134 lines
  • ■ ■ ■ ■ ■ ■
    draw/segdisp/segment/segment.go internal/draw/segdisp/segment/segment.go
    skipped 18 lines
    19 19   "fmt"
    20 20   "image"
    21 21   
    22  - "github.com/mum4k/termdash/canvas/braille"
    23  - "github.com/mum4k/termdash/cell"
    24  - "github.com/mum4k/termdash/draw"
     22 + "github.com/mum4k/termdash/internal/canvas/braille"
     23 + "github.com/mum4k/termdash/internal/cell"
     24 + "github.com/mum4k/termdash/internal/draw"
    25 25  )
    26 26   
    27 27  // Type identifies the type of the segment that is drawn.
    skipped 446 lines
  • ■ ■ ■ ■ ■ ■
    draw/segdisp/segment/segment_test.go internal/draw/segdisp/segment/segment_test.go
    skipped 18 lines
    19 19   "image"
    20 20   "testing"
    21 21   
    22  - "github.com/mum4k/termdash/area"
    23  - "github.com/mum4k/termdash/canvas/braille"
    24  - "github.com/mum4k/termdash/canvas/braille/testbraille"
    25  - "github.com/mum4k/termdash/cell"
    26  - "github.com/mum4k/termdash/draw"
    27  - "github.com/mum4k/termdash/draw/testdraw"
    28  - "github.com/mum4k/termdash/terminal/faketerm"
     22 + "github.com/mum4k/termdash/internal/area"
     23 + "github.com/mum4k/termdash/internal/canvas/braille"
     24 + "github.com/mum4k/termdash/internal/canvas/braille/testbraille"
     25 + "github.com/mum4k/termdash/internal/cell"
     26 + "github.com/mum4k/termdash/internal/draw"
     27 + "github.com/mum4k/termdash/internal/draw/testdraw"
     28 + "github.com/mum4k/termdash/internal/terminal/faketerm"
    29 29  )
    30 30   
    31 31  func TestHV(t *testing.T) {
    skipped 1730 lines
  • ■ ■ ■ ■ ■ ■
    draw/segdisp/segment/testsegment/testsegment.go internal/draw/segdisp/segment/testsegment/testsegment.go
    skipped 18 lines
    19 19   "fmt"
    20 20   "image"
    21 21   
    22  - "github.com/mum4k/termdash/canvas/braille"
    23  - "github.com/mum4k/termdash/draw/segdisp/segment"
     22 + "github.com/mum4k/termdash/internal/canvas/braille"
     23 + "github.com/mum4k/termdash/internal/draw/segdisp/segment"
    24 24  )
    25 25   
    26 26  // MustHV draws the segment or panics.
    skipped 13 lines
  • ■ ■ ■ ■ ■ ■
    draw/segdisp/sixteen/attributes.go internal/draw/segdisp/sixteen/attributes.go
    skipped 21 lines
    22 22   "image"
    23 23   "math"
    24 24   
    25  - "github.com/mum4k/termdash/draw/segdisp/segment"
    26  - "github.com/mum4k/termdash/numbers"
     25 + "github.com/mum4k/termdash/internal/draw/segdisp/segment"
     26 + "github.com/mum4k/termdash/internal/numbers"
    27 27  )
    28 28   
    29 29  // hvSegType maps horizontal and vertical segments to their type.
    skipped 272 lines
  • draw/segdisp/sixteen/doc/16-Segment-ASCII-All.jpg internal/draw/segdisp/sixteen/doc/16-Segment-ASCII-All.jpg
  • draw/segdisp/sixteen/doc/segment_placement.graffle internal/draw/segdisp/sixteen/doc/segment_placement.graffle
    Binary file.
  • draw/segdisp/sixteen/doc/segment_placement.svg internal/draw/segdisp/sixteen/doc/segment_placement.svg
  • ■ ■ ■ ■ ■
    draw/segdisp/sixteen/sixteen.go internal/draw/segdisp/sixteen/sixteen.go
    skipped 44 lines
    45 45   "image"
    46 46   "math"
    47 47   
    48  - "github.com/mum4k/termdash/area"
    49  - "github.com/mum4k/termdash/canvas"
    50  - "github.com/mum4k/termdash/canvas/braille"
    51  - "github.com/mum4k/termdash/cell"
    52  - "github.com/mum4k/termdash/draw/segdisp/segment"
     48 + "github.com/mum4k/termdash/internal/area"
     49 + "github.com/mum4k/termdash/internal/canvas"
     50 + "github.com/mum4k/termdash/internal/canvas/braille"
     51 + "github.com/mum4k/termdash/internal/cell"
     52 + "github.com/mum4k/termdash/internal/draw/segdisp/segment"
    53 53  )
    54 54   
    55 55  // Segment represents a single segment in the display.
    skipped 30 lines
    86 86  const (
    87 87   segmentUnknown Segment = iota
    88 88   
     89 + // A1 is a segment, see the diagram above.
    89 90   A1
     91 + // A2 is a segment, see the diagram above.
    90 92   A2
     93 + // B is a segment, see the diagram above.
    91 94   B
     95 + // C is a segment, see the diagram above.
    92 96   C
     97 + // D1 is a segment, see the diagram above.
    93 98   D1
     99 + // D2 is a segment, see the diagram above.
    94 100   D2
     101 + // E is a segment, see the diagram above.
    95 102   E
     103 + // F is a segment, see the diagram above.
    96 104   F
     105 + // G1 is a segment, see the diagram above.
    97 106   G1
     107 + // G2 is a segment, see the diagram above.
    98 108   G2
     109 + // H is a segment, see the diagram above.
    99 110   H
     111 + // J is a segment, see the diagram above.
    100 112   J
     113 + // K is a segment, see the diagram above.
    101 114   K
     115 + // L is a segment, see the diagram above.
    102 116   L
     117 + // M is a segment, see the diagram above.
    103 118   M
     119 + // N is a segment, see the diagram above.
    104 120   N
    105 121   
    106 122   segmentMax // Used for validation.
    skipped 233 lines
    340 356   return nil
    341 357  }
    342 358   
    343  -// Character sets all the segments that are needed to display the provided character.
     359 +// SetCharacter sets all the segments that are needed to display the provided
     360 +// character.
    344 361  // The display only supports a subset of ASCII characters, use SupportsChars()
    345 362  // or Sanitize() to ensure the provided character is supported.
    346 363  // Doesn't clear the display of segments set previously.
    skipped 88 lines
    435 452   return bc.CopyTo(cvs)
    436 453  }
    437 454   
    438  -// Required, when given an area of cells, returns either an area of the same
     455 +// Required when given an area of cells, returns either an area of the same
    439 456  // size or a smaller area that is required to draw one display.
    440 457  // Returns a smaller area when the provided area didn't have the required
    441 458  // aspect ratio.
    skipped 32 lines
  • ■ ■ ■ ■ ■ ■
    draw/segdisp/sixteen/sixteen_test.go internal/draw/segdisp/sixteen/sixteen_test.go
    skipped 19 lines
    20 20   "testing"
    21 21   
    22 22   "github.com/kylelemons/godebug/pretty"
    23  - "github.com/mum4k/termdash/area"
    24  - "github.com/mum4k/termdash/canvas"
    25  - "github.com/mum4k/termdash/canvas/braille/testbraille"
    26  - "github.com/mum4k/termdash/canvas/testcanvas"
    27  - "github.com/mum4k/termdash/cell"
    28  - "github.com/mum4k/termdash/draw/segdisp/segment"
    29  - "github.com/mum4k/termdash/draw/segdisp/segment/testsegment"
    30  - "github.com/mum4k/termdash/terminal/faketerm"
     23 + "github.com/mum4k/termdash/internal/area"
     24 + "github.com/mum4k/termdash/internal/canvas"
     25 + "github.com/mum4k/termdash/internal/canvas/braille/testbraille"
     26 + "github.com/mum4k/termdash/internal/canvas/testcanvas"
     27 + "github.com/mum4k/termdash/internal/cell"
     28 + "github.com/mum4k/termdash/internal/draw/segdisp/segment"
     29 + "github.com/mum4k/termdash/internal/draw/segdisp/segment/testsegment"
     30 + "github.com/mum4k/termdash/internal/terminal/faketerm"
    31 31  )
    32 32   
    33 33  func TestDraw(t *testing.T) {
    skipped 1734 lines
  • ■ ■ ■ ■ ■ ■
    draw/segdisp/sixteen/testsixteen/testsixteen.go internal/draw/segdisp/sixteen/testsixteen/testsixteen.go
    skipped 17 lines
    18 18  import (
    19 19   "fmt"
    20 20   
    21  - "github.com/mum4k/termdash/canvas"
    22  - "github.com/mum4k/termdash/draw/segdisp/sixteen"
     21 + "github.com/mum4k/termdash/internal/canvas"
     22 + "github.com/mum4k/termdash/internal/draw/segdisp/sixteen"
    23 23  )
    24 24   
    25 25  // MustSetCharacter sets the character on the display or panics.
    skipped 13 lines
  • ■ ■ ■ ■ ■
    draw/testdraw/testdraw.go internal/draw/testdraw/testdraw.go
    skipped 18 lines
    19 19   "fmt"
    20 20   "image"
    21 21   
    22  - "github.com/mum4k/termdash/canvas"
    23  - "github.com/mum4k/termdash/canvas/braille"
    24  - "github.com/mum4k/termdash/draw"
     22 + "github.com/mum4k/termdash/internal/canvas"
     23 + "github.com/mum4k/termdash/internal/canvas/braille"
     24 + "github.com/mum4k/termdash/internal/draw"
    25 25  )
    26 26   
    27 27  // MustBorder draws border on the canvas or panics.
    skipped 7 lines
    35 35  func MustText(c *canvas.Canvas, text string, start image.Point, opts ...draw.TextOption) {
    36 36   if err := draw.Text(c, text, start, opts...); err != nil {
    37 37   panic(fmt.Sprintf("draw.Text => unexpected error: %v", err))
     38 + }
     39 +}
     40 + 
     41 +// MustVerticalText draws the vertical text on the canvas or panics.
     42 +func MustVerticalText(c *canvas.Canvas, text string, start image.Point, opts ...draw.VerticalTextOption) {
     43 + if err := draw.VerticalText(c, text, start, opts...); err != nil {
     44 + panic(fmt.Sprintf("draw.VerticalText => unexpected error: %v", err))
    38 45   }
    39 46  }
    40 47   
    skipped 35 lines
  • ■ ■ ■ ■ ■ ■
    draw/text.go internal/draw/text.go
    skipped 20 lines
    21 21   "fmt"
    22 22   "image"
    23 23   
    24  - runewidth "github.com/mattn/go-runewidth"
    25  - "github.com/mum4k/termdash/canvas"
    26  - "github.com/mum4k/termdash/cell"
     24 + "github.com/mum4k/termdash/internal/canvas"
     25 + "github.com/mum4k/termdash/internal/cell"
     26 + "github.com/mum4k/termdash/internal/cell/runewidth"
    27 27  )
    28 28   
    29 29  // OverrunMode represents
    skipped 167 lines
  • ■ ■ ■ ■ ■ ■
    draw/text_test.go internal/draw/text_test.go
    skipped 17 lines
    18 18   "image"
    19 19   "testing"
    20 20   
    21  - "github.com/mum4k/termdash/canvas"
    22  - "github.com/mum4k/termdash/canvas/testcanvas"
    23  - "github.com/mum4k/termdash/cell"
    24  - "github.com/mum4k/termdash/terminal/faketerm"
     21 + "github.com/mum4k/termdash/internal/canvas"
     22 + "github.com/mum4k/termdash/internal/canvas/testcanvas"
     23 + "github.com/mum4k/termdash/internal/cell"
     24 + "github.com/mum4k/termdash/internal/terminal/faketerm"
    25 25  )
    26 26   
    27 27  func TestTrimText(t *testing.T) {
    skipped 630 lines
  • ■ ■ ■ ■ ■ ■
    internal/draw/vertical_text.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 draw
     16 + 
     17 +// vertical_text.go contains code that prints UTF-8 encoded strings on the
     18 +// canvas in vertical columns instead of lines.
     19 + 
     20 +import (
     21 + "fmt"
     22 + "image"
     23 + 
     24 + "github.com/mum4k/termdash/internal/canvas"
     25 + "github.com/mum4k/termdash/internal/cell"
     26 +)
     27 + 
     28 +// VerticalTextOption is used to provide options to Text().
     29 +type VerticalTextOption interface {
     30 + // set sets the provided option.
     31 + set(*verticalTextOptions)
     32 +}
     33 + 
     34 +// verticalTextOptions stores the provided options.
     35 +type verticalTextOptions struct {
     36 + cellOpts []cell.Option
     37 + maxY int
     38 + overrunMode OverrunMode
     39 +}
     40 + 
     41 +// verticalTextOption implements VerticalTextOption.
     42 +type verticalTextOption func(*verticalTextOptions)
     43 + 
     44 +// set implements VerticalTextOption.set.
     45 +func (vto verticalTextOption) set(vtOpts *verticalTextOptions) {
     46 + vto(vtOpts)
     47 +}
     48 + 
     49 +// VerticalTextCellOpts sets options on the cells that contain the text.
     50 +func VerticalTextCellOpts(opts ...cell.Option) VerticalTextOption {
     51 + return verticalTextOption(func(vtOpts *verticalTextOptions) {
     52 + vtOpts.cellOpts = opts
     53 + })
     54 +}
     55 + 
     56 +// VerticalTextMaxY sets a limit on the Y coordinate (row) of the drawn text.
     57 +// The Y coordinate of all cells used by the vertical text must be within
     58 +// start.Y <= Y < VerticalTextMaxY.
     59 +// If not provided, the height of the canvas is used as VerticalTextMaxY.
     60 +func VerticalTextMaxY(y int) VerticalTextOption {
     61 + return verticalTextOption(func(vtOpts *verticalTextOptions) {
     62 + vtOpts.maxY = y
     63 + })
     64 +}
     65 + 
     66 +// VerticalTextOverrunMode indicates what to do with text that overruns the
     67 +// VerticalTextMaxY() or the width of the canvas if VerticalTextMaxY() isn't
     68 +// specified.
     69 +// Defaults to OverrunModeStrict.
     70 +func VerticalTextOverrunMode(om OverrunMode) VerticalTextOption {
     71 + return verticalTextOption(func(vtOpts *verticalTextOptions) {
     72 + vtOpts.overrunMode = om
     73 + })
     74 +}
     75 + 
     76 +// VerticalText prints the provided text on the canvas starting at the provided point.
     77 +// The text is printed in a vertical orientation, i.e:
     78 +// H
     79 +// e
     80 +// l
     81 +// l
     82 +// o
     83 +func VerticalText(c *canvas.Canvas, text string, start image.Point, opts ...VerticalTextOption) error {
     84 + ar := c.Area()
     85 + if !start.In(ar) {
     86 + return fmt.Errorf("the requested start point %v falls outside of the provided canvas %v", start, ar)
     87 + }
     88 + 
     89 + opt := &verticalTextOptions{}
     90 + for _, o := range opts {
     91 + o.set(opt)
     92 + }
     93 + 
     94 + if opt.maxY < 0 || opt.maxY > ar.Max.Y {
     95 + return fmt.Errorf("invalid VerticalTextMaxY(%v), must be a positive number that is <= canvas.width %v", opt.maxY, ar.Dy())
     96 + }
     97 + 
     98 + var wantMaxY int
     99 + if opt.maxY == 0 {
     100 + wantMaxY = ar.Max.Y
     101 + } else {
     102 + wantMaxY = opt.maxY
     103 + }
     104 + 
     105 + maxCells := wantMaxY - start.Y
     106 + trimmed, err := TrimText(text, maxCells, opt.overrunMode)
     107 + if err != nil {
     108 + return err
     109 + }
     110 + 
     111 + cur := start
     112 + for _, r := range trimmed {
     113 + cells, err := c.SetCell(cur, r, opt.cellOpts...)
     114 + if err != nil {
     115 + return err
     116 + }
     117 + cur = image.Point{cur.X, cur.Y + cells}
     118 + }
     119 + return nil
     120 +}
     121 + 
  • ■ ■ ■ ■ ■ ■
    internal/draw/vertical_text_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 draw
     16 + 
     17 +import (
     18 + "image"
     19 + "testing"
     20 + 
     21 + "github.com/mum4k/termdash/internal/canvas"
     22 + "github.com/mum4k/termdash/internal/canvas/testcanvas"
     23 + "github.com/mum4k/termdash/internal/cell"
     24 + "github.com/mum4k/termdash/internal/terminal/faketerm"
     25 +)
     26 + 
     27 +func TestVerticalText(t *testing.T) {
     28 + tests := []struct {
     29 + desc string
     30 + canvas image.Rectangle
     31 + text string
     32 + start image.Point
     33 + opts []VerticalTextOption
     34 + want func(size image.Point) *faketerm.Terminal
     35 + wantErr bool
     36 + }{
     37 + {
     38 + desc: "start falls outside of the canvas",
     39 + canvas: image.Rect(0, 0, 2, 2),
     40 + start: image.Point{2, 2},
     41 + want: func(size image.Point) *faketerm.Terminal {
     42 + return faketerm.MustNew(size)
     43 + },
     44 + wantErr: true,
     45 + },
     46 + {
     47 + desc: "unsupported overrun mode specified",
     48 + canvas: image.Rect(0, 0, 1, 1),
     49 + text: "ab",
     50 + start: image.Point{0, 0},
     51 + opts: []VerticalTextOption{
     52 + VerticalTextOverrunMode(OverrunMode(-1)),
     53 + },
     54 + want: func(size image.Point) *faketerm.Terminal {
     55 + return faketerm.MustNew(size)
     56 + },
     57 + wantErr: true,
     58 + },
     59 + {
     60 + desc: "zero text",
     61 + canvas: image.Rect(0, 0, 1, 1),
     62 + text: "",
     63 + start: image.Point{0, 0},
     64 + want: func(size image.Point) *faketerm.Terminal {
     65 + return faketerm.MustNew(size)
     66 + },
     67 + },
     68 + {
     69 + desc: "text falls outside of the canvas on OverrunModeStrict",
     70 + canvas: image.Rect(0, 0, 1, 1),
     71 + text: "ab",
     72 + start: image.Point{0, 0},
     73 + want: func(size image.Point) *faketerm.Terminal {
     74 + return faketerm.MustNew(size)
     75 + },
     76 + wantErr: true,
     77 + },
     78 + {
     79 + desc: "text falls outside of the canvas because the rune is full-width on OverrunModeStrict",
     80 + canvas: image.Rect(0, 0, 1, 1),
     81 + text: "界",
     82 + start: image.Point{0, 0},
     83 + want: func(size image.Point) *faketerm.Terminal {
     84 + return faketerm.MustNew(size)
     85 + },
     86 + wantErr: true,
     87 + },
     88 + {
     89 + desc: "text falls outside of the canvas on OverrunModeTrim",
     90 + canvas: image.Rect(0, 0, 1, 1),
     91 + text: "ab",
     92 + start: image.Point{0, 0},
     93 + opts: []VerticalTextOption{
     94 + VerticalTextOverrunMode(OverrunModeTrim),
     95 + },
     96 + want: func(size image.Point) *faketerm.Terminal {
     97 + ft := faketerm.MustNew(size)
     98 + c := testcanvas.MustNew(ft.Area())
     99 + 
     100 + testcanvas.MustSetCell(c, image.Point{0, 0}, 'a')
     101 + testcanvas.MustApply(c, ft)
     102 + return ft
     103 + },
     104 + },
     105 + {
     106 + desc: "text falls outside of the canvas because the rune is full-width on OverrunModeTrim",
     107 + canvas: image.Rect(0, 0, 1, 1),
     108 + text: "界",
     109 + start: image.Point{0, 0},
     110 + opts: []VerticalTextOption{
     111 + VerticalTextOverrunMode(OverrunModeTrim),
     112 + },
     113 + want: func(size image.Point) *faketerm.Terminal {
     114 + ft := faketerm.MustNew(size)
     115 + c := testcanvas.MustNew(ft.Area())
     116 + testcanvas.MustApply(c, ft)
     117 + return ft
     118 + },
     119 + },
     120 + {
     121 + desc: "OverrunModeTrim trims longer text",
     122 + canvas: image.Rect(0, 0, 1, 2),
     123 + text: "abcdef",
     124 + start: image.Point{0, 0},
     125 + opts: []VerticalTextOption{
     126 + VerticalTextOverrunMode(OverrunModeTrim),
     127 + },
     128 + want: func(size image.Point) *faketerm.Terminal {
     129 + ft := faketerm.MustNew(size)
     130 + c := testcanvas.MustNew(ft.Area())
     131 + 
     132 + testcanvas.MustSetCell(c, image.Point{0, 0}, 'a')
     133 + testcanvas.MustSetCell(c, image.Point{0, 1}, 'b')
     134 + testcanvas.MustApply(c, ft)
     135 + return ft
     136 + },
     137 + },
     138 + {
     139 + desc: "OverrunModeTrim trims longer text with full-width runes, trim falls before the rune",
     140 + canvas: image.Rect(0, 0, 1, 2),
     141 + text: "ab界",
     142 + start: image.Point{0, 0},
     143 + opts: []VerticalTextOption{
     144 + VerticalTextOverrunMode(OverrunModeTrim),
     145 + },
     146 + want: func(size image.Point) *faketerm.Terminal {
     147 + ft := faketerm.MustNew(size)
     148 + c := testcanvas.MustNew(ft.Area())
     149 + 
     150 + testcanvas.MustSetCell(c, image.Point{0, 0}, 'a')
     151 + testcanvas.MustSetCell(c, image.Point{0, 1}, 'b')
     152 + testcanvas.MustApply(c, ft)
     153 + return ft
     154 + },
     155 + },
     156 + {
     157 + desc: "OverrunModeTrim trims longer text with full-width runes, trim falls on the rune",
     158 + canvas: image.Rect(0, 0, 1, 2),
     159 + text: "a界",
     160 + start: image.Point{0, 0},
     161 + opts: []VerticalTextOption{
     162 + VerticalTextOverrunMode(OverrunModeTrim),
     163 + },
     164 + want: func(size image.Point) *faketerm.Terminal {
     165 + ft := faketerm.MustNew(size)
     166 + c := testcanvas.MustNew(ft.Area())
     167 + 
     168 + testcanvas.MustSetCell(c, image.Point{0, 0}, 'a')
     169 + testcanvas.MustApply(c, ft)
     170 + return ft
     171 + },
     172 + },
     173 + {
     174 + desc: "text falls outside of the canvas on OverrunModeThreeDot",
     175 + canvas: image.Rect(0, 0, 1, 1),
     176 + text: "ab",
     177 + start: image.Point{0, 0},
     178 + opts: []VerticalTextOption{
     179 + VerticalTextOverrunMode(OverrunModeThreeDot),
     180 + },
     181 + want: func(size image.Point) *faketerm.Terminal {
     182 + ft := faketerm.MustNew(size)
     183 + c := testcanvas.MustNew(ft.Area())
     184 + 
     185 + testcanvas.MustSetCell(c, image.Point{0, 0}, '…')
     186 + testcanvas.MustApply(c, ft)
     187 + return ft
     188 + },
     189 + },
     190 + {
     191 + desc: "text falls outside of the canvas because the rune is full-width on OverrunModeThreeDot",
     192 + canvas: image.Rect(0, 0, 1, 1),
     193 + text: "界",
     194 + start: image.Point{0, 0},
     195 + opts: []VerticalTextOption{
     196 + VerticalTextOverrunMode(OverrunModeThreeDot),
     197 + },
     198 + want: func(size image.Point) *faketerm.Terminal {
     199 + ft := faketerm.MustNew(size)
     200 + c := testcanvas.MustNew(ft.Area())
     201 + 
     202 + testcanvas.MustSetCell(c, image.Point{0, 0}, '…')
     203 + testcanvas.MustApply(c, ft)
     204 + return ft
     205 + },
     206 + },
     207 + {
     208 + desc: "OverrunModeThreeDot trims longer text",
     209 + canvas: image.Rect(0, 0, 1, 2),
     210 + text: "abcdef",
     211 + start: image.Point{0, 0},
     212 + opts: []VerticalTextOption{
     213 + VerticalTextOverrunMode(OverrunModeThreeDot),
     214 + },
     215 + want: func(size image.Point) *faketerm.Terminal {
     216 + ft := faketerm.MustNew(size)
     217 + c := testcanvas.MustNew(ft.Area())
     218 + 
     219 + testcanvas.MustSetCell(c, image.Point{0, 0}, 'a')
     220 + testcanvas.MustSetCell(c, image.Point{0, 1}, '…')
     221 + testcanvas.MustApply(c, ft)
     222 + return ft
     223 + },
     224 + },
     225 + {
     226 + desc: "OverrunModeThreeDot trims longer text with full-width runes, trim falls before the rune",
     227 + canvas: image.Rect(0, 0, 1, 2),
     228 + text: "ab界",
     229 + start: image.Point{0, 0},
     230 + opts: []VerticalTextOption{
     231 + VerticalTextOverrunMode(OverrunModeThreeDot),
     232 + },
     233 + want: func(size image.Point) *faketerm.Terminal {
     234 + ft := faketerm.MustNew(size)
     235 + c := testcanvas.MustNew(ft.Area())
     236 + 
     237 + testcanvas.MustSetCell(c, image.Point{0, 0}, 'a')
     238 + testcanvas.MustSetCell(c, image.Point{0, 1}, '…')
     239 + testcanvas.MustApply(c, ft)
     240 + return ft
     241 + },
     242 + },
     243 + {
     244 + desc: "OverrunModeThreeDot trims longer text with full-width runes, trim falls on the rune",
     245 + canvas: image.Rect(0, 0, 1, 2),
     246 + text: "a界",
     247 + start: image.Point{0, 0},
     248 + opts: []VerticalTextOption{
     249 + VerticalTextOverrunMode(OverrunModeThreeDot),
     250 + },
     251 + want: func(size image.Point) *faketerm.Terminal {
     252 + ft := faketerm.MustNew(size)
     253 + c := testcanvas.MustNew(ft.Area())
     254 + 
     255 + testcanvas.MustSetCell(c, image.Point{0, 0}, 'a')
     256 + testcanvas.MustSetCell(c, image.Point{0, 1}, '…')
     257 + testcanvas.MustApply(c, ft)
     258 + return ft
     259 + },
     260 + },
     261 + {
     262 + desc: "requested MaxY is negative",
     263 + canvas: image.Rect(0, 0, 1, 1),
     264 + text: "",
     265 + start: image.Point{0, 0},
     266 + opts: []VerticalTextOption{
     267 + VerticalTextMaxY(-1),
     268 + },
     269 + want: func(size image.Point) *faketerm.Terminal {
     270 + return faketerm.MustNew(size)
     271 + },
     272 + wantErr: true,
     273 + },
     274 + {
     275 + desc: "requested MaxY is greater than canvas height",
     276 + canvas: image.Rect(0, 0, 1, 1),
     277 + text: "",
     278 + start: image.Point{0, 0},
     279 + opts: []VerticalTextOption{
     280 + VerticalTextMaxY(2),
     281 + },
     282 + want: func(size image.Point) *faketerm.Terminal {
     283 + return faketerm.MustNew(size)
     284 + },
     285 + wantErr: true,
     286 + },
     287 + {
     288 + desc: "text falls outside of requested MaxY",
     289 + canvas: image.Rect(0, 0, 2, 3),
     290 + text: "ab",
     291 + start: image.Point{1, 1},
     292 + opts: []VerticalTextOption{
     293 + VerticalTextMaxY(2),
     294 + },
     295 + want: func(size image.Point) *faketerm.Terminal {
     296 + return faketerm.MustNew(size)
     297 + },
     298 + wantErr: true,
     299 + },
     300 + {
     301 + desc: "text is empty, nothing to do",
     302 + canvas: image.Rect(0, 0, 1, 1),
     303 + text: "",
     304 + start: image.Point{0, 0},
     305 + want: func(size image.Point) *faketerm.Terminal {
     306 + return faketerm.MustNew(size)
     307 + },
     308 + },
     309 + {
     310 + desc: "draws text",
     311 + canvas: image.Rect(0, 0, 2, 3),
     312 + text: "ab",
     313 + start: image.Point{1, 1},
     314 + want: func(size image.Point) *faketerm.Terminal {
     315 + ft := faketerm.MustNew(size)
     316 + c := testcanvas.MustNew(ft.Area())
     317 + 
     318 + testcanvas.MustSetCell(c, image.Point{1, 1}, 'a')
     319 + testcanvas.MustSetCell(c, image.Point{1, 2}, 'b')
     320 + testcanvas.MustApply(c, ft)
     321 + return ft
     322 + },
     323 + },
     324 + {
     325 + desc: "draws text with cell options",
     326 + canvas: image.Rect(0, 0, 2, 3),
     327 + text: "ab",
     328 + start: image.Point{1, 1},
     329 + opts: []VerticalTextOption{
     330 + VerticalTextCellOpts(cell.FgColor(cell.ColorRed)),
     331 + },
     332 + want: func(size image.Point) *faketerm.Terminal {
     333 + ft := faketerm.MustNew(size)
     334 + c := testcanvas.MustNew(ft.Area())
     335 + 
     336 + testcanvas.MustSetCell(c, image.Point{1, 1}, 'a', cell.FgColor(cell.ColorRed))
     337 + testcanvas.MustSetCell(c, image.Point{1, 2}, 'b', cell.FgColor(cell.ColorRed))
     338 + testcanvas.MustApply(c, ft)
     339 + return ft
     340 + },
     341 + },
     342 + {
     343 + desc: "draws a half-width unicode character",
     344 + canvas: image.Rect(0, 0, 1, 1),
     345 + text: "⇄",
     346 + start: image.Point{0, 0},
     347 + want: func(size image.Point) *faketerm.Terminal {
     348 + ft := faketerm.MustNew(size)
     349 + c := testcanvas.MustNew(ft.Area())
     350 + 
     351 + testcanvas.MustSetCell(c, image.Point{0, 0}, '⇄')
     352 + testcanvas.MustApply(c, ft)
     353 + return ft
     354 + },
     355 + },
     356 + {
     357 + desc: "draws multiple half-width unicode characters",
     358 + canvas: image.Rect(0, 0, 3, 3),
     359 + text: "⇄࿃°",
     360 + start: image.Point{0, 0},
     361 + want: func(size image.Point) *faketerm.Terminal {
     362 + ft := faketerm.MustNew(size)
     363 + c := testcanvas.MustNew(ft.Area())
     364 + 
     365 + testcanvas.MustSetCell(c, image.Point{0, 0}, '⇄')
     366 + testcanvas.MustSetCell(c, image.Point{0, 1}, '࿃')
     367 + testcanvas.MustSetCell(c, image.Point{0, 2}, '°')
     368 + testcanvas.MustApply(c, ft)
     369 + return ft
     370 + },
     371 + },
     372 + {
     373 + desc: "draws multiple full-width unicode characters",
     374 + canvas: image.Rect(0, 0, 3, 10),
     375 + text: "你好,世界",
     376 + start: image.Point{0, 0},
     377 + want: func(size image.Point) *faketerm.Terminal {
     378 + ft := faketerm.MustNew(size)
     379 + c := testcanvas.MustNew(ft.Area())
     380 + 
     381 + testcanvas.MustSetCell(c, image.Point{0, 0}, '你')
     382 + testcanvas.MustSetCell(c, image.Point{0, 2}, '好')
     383 + testcanvas.MustSetCell(c, image.Point{0, 4}, ',')
     384 + testcanvas.MustSetCell(c, image.Point{0, 6}, '世')
     385 + testcanvas.MustSetCell(c, image.Point{0, 8}, '界')
     386 + testcanvas.MustApply(c, ft)
     387 + return ft
     388 + },
     389 + },
     390 + }
     391 + 
     392 + for _, tc := range tests {
     393 + t.Run(tc.desc, func(t *testing.T) {
     394 + c, err := canvas.New(tc.canvas)
     395 + if err != nil {
     396 + t.Fatalf("canvas.New => unexpected error: %v", err)
     397 + }
     398 + 
     399 + err = VerticalText(c, tc.text, tc.start, tc.opts...)
     400 + if (err != nil) != tc.wantErr {
     401 + t.Errorf("VerticalText => unexpected error: %v, wantErr: %v", err, tc.wantErr)
     402 + }
     403 + if err != nil {
     404 + return
     405 + }
     406 + 
     407 + got, err := faketerm.New(c.Size())
     408 + if err != nil {
     409 + t.Fatalf("faketerm.New => unexpected error: %v", err)
     410 + }
     411 + 
     412 + if err := c.Apply(got); err != nil {
     413 + t.Fatalf("Apply => unexpected error: %v", err)
     414 + }
     415 + 
     416 + if diff := faketerm.Diff(tc.want(c.Size()), got); diff != "" {
     417 + t.Errorf("VerticalText => %v", diff)
     418 + }
     419 + })
     420 + }
     421 +}
     422 + 
Please wait...
Page is in error, reload to recover