├── .github ├── dependabot.yml └── workflows │ └── go.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── align ├── align.go └── align_test.go ├── cell ├── cell.go ├── cell_test.go ├── color.go └── color_test.go ├── container ├── container.go ├── container_test.go ├── draw.go ├── draw_test.go ├── focus.go ├── focus_test.go ├── grid │ ├── grid.go │ └── grid_test.go ├── options.go ├── traversal.go └── traversal_test.go ├── doc ├── design_goals.md ├── design_guidelines.md ├── hld.md ├── images │ ├── barchartdemo.gif │ ├── buttondemo.gif │ ├── donutdemo.gif │ ├── formdemo.gif │ ├── gaugedemo.gif │ ├── hld.graffle │ ├── hld.png │ ├── linechartdemo.gif │ ├── segmentdisplaydemo.gif │ ├── sparklinedemo.gif │ ├── termdash.png │ ├── termdashdemo_0_8_0.gif │ ├── termdashdemo_0_9_0.gif │ ├── textdemo.gif │ └── textinputdemo.gif ├── requirements.md └── widget_development.md ├── go.mod ├── go.sum ├── keyboard ├── keyboard.go └── keyboard_test.go ├── linestyle ├── linestyle.go └── linestyle_test.go ├── mouse ├── mouse.go └── mouse_test.go ├── private ├── README.md ├── alignfor │ ├── alignfor.go │ └── alignfor_test.go ├── area │ ├── area.go │ └── area_test.go ├── attrrange │ ├── attrrange.go │ └── attrrange_test.go ├── button │ ├── button.go │ └── button_test.go ├── canvas │ ├── braille │ │ ├── braille.go │ │ ├── braille_test.go │ │ └── testbraille │ │ │ └── testbraille.go │ ├── buffer │ │ ├── buffer.go │ │ └── buffer_test.go │ ├── canvas.go │ ├── canvas_test.go │ └── testcanvas │ │ └── testcanvas.go ├── draw │ ├── border.go │ ├── border_test.go │ ├── braille_circle.go │ ├── braille_circle_test.go │ ├── braille_fill.go │ ├── braille_fill_test.go │ ├── braille_line.go │ ├── braille_line_test.go │ ├── draw.go │ ├── hv_line.go │ ├── hv_line_graph.go │ ├── hv_line_graph_test.go │ ├── hv_line_test.go │ ├── line_style.go │ ├── rectangle.go │ ├── rectangle_test.go │ ├── testdraw │ │ └── testdraw.go │ ├── text.go │ ├── text_test.go │ ├── vertical_text.go │ └── vertical_text_test.go ├── event │ ├── event.go │ ├── event_test.go │ ├── eventqueue │ │ ├── eventqueue.go │ │ └── eventqueue_test.go │ └── testevent │ │ └── testevent.go ├── faketerm │ ├── diff.go │ ├── diff_test.go │ └── faketerm.go ├── fakewidget │ ├── fakewidget.go │ └── fakewidget_test.go ├── numbers │ ├── numbers.go │ ├── numbers_test.go │ └── trig │ │ ├── trig.go │ │ └── trig_test.go ├── runewidth │ ├── runewidth.go │ └── runewidth_test.go ├── scripts │ └── autogen_licences.sh ├── segdisp │ ├── dotseg │ │ ├── attributes.go │ │ ├── attributes_test.go │ │ ├── dotseg.go │ │ ├── dotseg_test.go │ │ └── testdotseg │ │ │ └── testdotseg.go │ ├── segdisp.go │ ├── segdisp_test.go │ ├── segment │ │ ├── segment.go │ │ ├── segment_test.go │ │ └── testsegment │ │ │ └── testsegment.go │ └── sixteen │ │ ├── attributes.go │ │ ├── doc │ │ ├── 16-Segment-ASCII-All.jpg │ │ ├── segment_placement.graffle │ │ └── segment_placement.svg │ │ ├── sixteen.go │ │ ├── sixteen_test.go │ │ └── testsixteen │ │ └── testsixteen.go └── wrap │ ├── wrap.go │ └── wrap_test.go ├── termdash.go ├── termdash_test.go ├── termdashdemo └── termdashdemo.go ├── terminal ├── tcell │ ├── cell_options.go │ ├── cell_options_test.go │ ├── event.go │ ├── event_test.go │ ├── tcell.go │ └── tcell_test.go ├── termbox │ ├── cell_options.go │ ├── cell_options_test.go │ ├── color_mode.go │ ├── event.go │ ├── event_test.go │ ├── termbox.go │ └── termbox_test.go └── terminalapi │ ├── color_mode.go │ ├── event.go │ └── terminalapi.go ├── widgetapi └── widgetapi.go └── widgets ├── barchart ├── barchart.go ├── barchart_test.go ├── barchartdemo │ └── barchartdemo.go └── options.go ├── button ├── button.go ├── button_test.go ├── buttondemo │ └── buttondemo.go ├── options.go └── text_options.go ├── donut ├── circle.go ├── circle_test.go ├── donut.go ├── donut_test.go ├── donutdemo │ └── donutdemo.go └── options.go ├── gauge ├── gauge.go ├── gauge_test.go ├── gaugedemo │ └── gaugedemo.go └── options.go ├── heatmap ├── heatmap.go ├── heatmap_test.go ├── heatmapdemo │ └── heatmapdemo.go ├── internal │ ├── README.md │ └── axes │ │ ├── axes.go │ │ ├── axes_test.go │ │ ├── label.go │ │ └── label_test.go └── options.go ├── linechart ├── internal │ ├── README.md │ ├── axes │ │ ├── axes.go │ │ ├── axes_test.go │ │ ├── label.go │ │ ├── label_test.go │ │ ├── scale.go │ │ ├── scale_test.go │ │ ├── value.go │ │ └── value_test.go │ └── zoom │ │ ├── zoom.go │ │ └── zoom_test.go ├── linechart.go ├── linechart_test.go ├── linechartdemo │ └── linechartdemo.go ├── options.go ├── value_formatter.go └── value_formatter_test.go ├── segmentdisplay ├── options.go ├── segment_area.go ├── segmentdisplay.go ├── segmentdisplay_test.go ├── segmentdisplaydemo │ └── segmentdisplaydemo.go └── write_options.go ├── sparkline ├── options.go ├── sparkline.go ├── sparkline_test.go ├── sparklinedemo │ └── sparklinedemo.go ├── sparks.go └── sparks_test.go ├── text ├── line_trim.go ├── line_trim_test.go ├── options.go ├── scroll.go ├── scroll_test.go ├── text.go ├── text_test.go ├── textdemo │ └── textdemo.go └── write_options.go └── textinput ├── editor.go ├── editor_test.go ├── formdemo └── formdemo.go ├── options.go ├── textinput.go ├── textinput_test.go └── textinputdemo └── textinputdemo.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "gomod" 7 | directory: "/" 8 | target-branch: "devel" 9 | schedule: 10 | interval: "weekly" 11 | - package-ecosystem: github-actions 12 | directory: / 13 | schedule: 14 | interval: "weekly" 15 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will builds and tests Termdash. 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | push: 8 | branches: [ "master", "devel" ] 9 | pull_request: 10 | branches: [ "master", "devel" ] 11 | 12 | jobs: 13 | 14 | build: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | go-version: [ '1.20', '1.21', 'stable' ] 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: Setup Go ${{ matrix.go-version }} 24 | uses: actions/setup-go@v5 25 | with: 26 | go-version: ${{ matrix.go-version }} 27 | 28 | - name: Install dependencies 29 | run: | 30 | go install golang.org/x/tools/cmd/cover@latest 31 | go install github.com/mattn/goveralls@latest 32 | go install golang.org/x/lint/golint@latest 33 | go get -t ./... 34 | 35 | - name: Test 36 | run: go test -v -covermode=count -coverprofile=coverage.out ./... 37 | 38 | - name: Test Race 39 | run: CGO_ENABLED=1 go test -race ./... 40 | 41 | - name: Format 42 | run: diff -u <(echo -n) <(gofmt -d -s .) 43 | 44 | - name: Lint 45 | run: diff -u <(echo -n) <(golint ./...) 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Exclude MacOS attribute files. 2 | .DS_Store 3 | 4 | # Exclude IDE files. 5 | .idea/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Fork and merge into the "devel" branch 7 | 8 | All development in termdash repository must happen in the [devel 9 | branch](https://github.com/mum4k/termdash/tree/devel). The devel branch is 10 | merged into the master branch during release of each new version. 11 | 12 | When you fork the termdash repository, be sure to checkout the devel branch. 13 | When you are creating a pull request, be sure to pull back into the devel 14 | branch. 15 | 16 | ## Contributor License Agreement 17 | 18 | Contributions to this project must be accompanied by a Contributor License 19 | Agreement. You (or your employer) retain the copyright to your contribution; 20 | this simply gives us permission to use and redistribute your contributions as 21 | part of the project. Head over to to see 22 | your current agreements on file or to sign a new one. 23 | 24 | You generally only need to submit a CLA once, so if you've already submitted one 25 | (even if it was for a different project), you probably don't need to do it 26 | again. 27 | 28 | ## Code reviews 29 | 30 | All submissions, including submissions by project members, require review. We 31 | use GitHub pull requests for this purpose. Consult 32 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 33 | information on using pull requests. 34 | 35 | ## Community Guidelines 36 | 37 | This project follows [Google's Open Source Community 38 | Guidelines](https://opensource.google.com/conduct/). 39 | -------------------------------------------------------------------------------- /align/align.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package align defines constants representing types of alignment. 16 | package align 17 | 18 | // Horizontal indicates the type of horizontal alignment. 19 | type Horizontal int 20 | 21 | // String implements fmt.Stringer() 22 | func (h Horizontal) String() string { 23 | if n, ok := horizontalNames[h]; ok { 24 | return n 25 | } 26 | return "HorizontalUnknown" 27 | } 28 | 29 | // horizontalNames maps Horizontal values to human readable names. 30 | var horizontalNames = map[Horizontal]string{ 31 | HorizontalLeft: "HorizontalLeft", 32 | HorizontalCenter: "HorizontalCenter", 33 | HorizontalRight: "HorizontalRight", 34 | } 35 | 36 | const ( 37 | // HorizontalLeft is left alignment along the horizontal axis. 38 | HorizontalLeft Horizontal = iota 39 | // HorizontalCenter is center alignment along the horizontal axis. 40 | HorizontalCenter 41 | // HorizontalRight is right alignment along the horizontal axis. 42 | HorizontalRight 43 | ) 44 | 45 | // Vertical indicates the type of vertical alignment. 46 | type Vertical int 47 | 48 | // String implements fmt.Stringer() 49 | func (v Vertical) String() string { 50 | if n, ok := verticalNames[v]; ok { 51 | return n 52 | } 53 | return "VerticalUnknown" 54 | } 55 | 56 | // verticalNames maps Vertical values to human readable names. 57 | var verticalNames = map[Vertical]string{ 58 | VerticalTop: "VerticalTop", 59 | VerticalMiddle: "VerticalMiddle", 60 | VerticalBottom: "VerticalBottom", 61 | } 62 | 63 | const ( 64 | // VerticalTop is top alignment along the vertical axis. 65 | VerticalTop Vertical = iota 66 | // VerticalMiddle is middle alignment along the vertical axis. 67 | VerticalMiddle 68 | // VerticalBottom is bottom alignment along the vertical axis. 69 | VerticalBottom 70 | ) 71 | -------------------------------------------------------------------------------- /align/align_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 align 16 | 17 | import "testing" 18 | 19 | func TestHorizontal(t *testing.T) { 20 | tests := []struct { 21 | desc string 22 | align Horizontal 23 | want string 24 | }{ 25 | { 26 | desc: "unknown", 27 | align: Horizontal(-1), 28 | want: "HorizontalUnknown", 29 | }, 30 | { 31 | desc: "left", 32 | align: HorizontalLeft, 33 | want: "HorizontalLeft", 34 | }, 35 | { 36 | desc: "center", 37 | align: HorizontalCenter, 38 | want: "HorizontalCenter", 39 | }, 40 | { 41 | desc: "right", 42 | align: HorizontalRight, 43 | want: "HorizontalRight", 44 | }, 45 | } 46 | 47 | for _, tc := range tests { 48 | t.Run(tc.desc, func(t *testing.T) { 49 | if got := tc.align.String(); got != tc.want { 50 | t.Errorf("String => %q, want %q", got, tc.want) 51 | } 52 | }) 53 | } 54 | } 55 | 56 | func TestVertical(t *testing.T) { 57 | tests := []struct { 58 | desc string 59 | align Vertical 60 | want string 61 | }{ 62 | { 63 | desc: "unknown", 64 | align: Vertical(-1), 65 | want: "VerticalUnknown", 66 | }, 67 | { 68 | desc: "top", 69 | align: VerticalTop, 70 | want: "VerticalTop", 71 | }, 72 | { 73 | desc: "middle", 74 | align: VerticalMiddle, 75 | want: "VerticalMiddle", 76 | }, 77 | { 78 | desc: "bottom", 79 | align: VerticalBottom, 80 | want: "VerticalBottom", 81 | }, 82 | } 83 | 84 | for _, tc := range tests { 85 | t.Run(tc.desc, func(t *testing.T) { 86 | if got := tc.align.String(); got != tc.want { 87 | t.Errorf("String => %q, want %q", got, tc.want) 88 | } 89 | }) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /cell/cell.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 cell implements cell options and attributes. 16 | package cell 17 | 18 | // Option is used to provide options for cells on a 2-D terminal. 19 | type Option interface { 20 | // Set sets the provided option. 21 | Set(*Options) 22 | } 23 | 24 | // Options stores the provided options. 25 | type Options struct { 26 | FgColor Color 27 | BgColor Color 28 | Bold bool 29 | Italic bool 30 | Underline bool 31 | Strikethrough bool 32 | Inverse bool 33 | Blink bool 34 | Dim bool 35 | } 36 | 37 | // Set allows existing options to be passed as an option. 38 | func (o *Options) Set(other *Options) { 39 | *other = *o 40 | } 41 | 42 | // NewOptions returns a new Options instance after applying the provided options. 43 | func NewOptions(opts ...Option) *Options { 44 | o := &Options{} 45 | for _, opt := range opts { 46 | opt.Set(o) 47 | } 48 | return o 49 | } 50 | 51 | // option implements Option. 52 | type option func(*Options) 53 | 54 | // Set implements Option.set. 55 | func (co option) Set(opts *Options) { 56 | co(opts) 57 | } 58 | 59 | // FgColor sets the foreground color of the cell. 60 | func FgColor(color Color) Option { 61 | return option(func(co *Options) { 62 | co.FgColor = color 63 | }) 64 | } 65 | 66 | // BgColor sets the background color of the cell. 67 | func BgColor(color Color) Option { 68 | return option(func(co *Options) { 69 | co.BgColor = color 70 | }) 71 | } 72 | 73 | // Bold makes cell's text bold. 74 | func Bold() Option { 75 | return option(func(co *Options) { 76 | co.Bold = true 77 | }) 78 | } 79 | 80 | // Italic makes cell's text italic. Only works when using the tcell backend. 81 | func Italic() Option { 82 | return option(func(co *Options) { 83 | co.Italic = true 84 | }) 85 | } 86 | 87 | // Underline makes cell's text underlined. 88 | func Underline() Option { 89 | return option(func(co *Options) { 90 | co.Underline = true 91 | }) 92 | } 93 | 94 | // Strikethrough strikes through the cell's text. Only works when using the tcell backend. 95 | func Strikethrough() Option { 96 | return option(func(co *Options) { 97 | co.Strikethrough = true 98 | }) 99 | } 100 | 101 | // Inverse inverts the colors of the cell's text. 102 | func Inverse() Option { 103 | return option(func(co *Options) { 104 | co.Inverse = true 105 | }) 106 | } 107 | 108 | // Blink makes the cell's text blink. Only works when using the tcell backend. 109 | func Blink() Option { 110 | return option(func(co *Options) { 111 | co.Blink = true 112 | }) 113 | } 114 | 115 | // Dim makes the cell foreground color dim. Only works when using the tcell backend. 116 | func Dim() Option { 117 | return option(func(co *Options) { 118 | co.Dim = true 119 | }) 120 | } 121 | -------------------------------------------------------------------------------- /cell/cell_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 cell 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/kylelemons/godebug/pretty" 21 | ) 22 | 23 | func TestNewOptions(t *testing.T) { 24 | tests := []struct { 25 | desc string 26 | opts []Option 27 | want *Options 28 | }{ 29 | { 30 | 31 | desc: "no provided options", 32 | want: &Options{}, 33 | }, 34 | { 35 | desc: "setting foreground color", 36 | opts: []Option{ 37 | FgColor(ColorBlack), 38 | }, 39 | want: &Options{ 40 | FgColor: ColorBlack, 41 | }, 42 | }, 43 | { 44 | desc: "setting background color", 45 | opts: []Option{ 46 | BgColor(ColorRed), 47 | }, 48 | want: &Options{ 49 | BgColor: ColorRed, 50 | }, 51 | }, 52 | { 53 | desc: "setting multiple options", 54 | opts: []Option{ 55 | FgColor(ColorCyan), 56 | BgColor(ColorMagenta), 57 | }, 58 | want: &Options{ 59 | FgColor: ColorCyan, 60 | BgColor: ColorMagenta, 61 | }, 62 | }, 63 | { 64 | desc: "setting options by passing the options struct", 65 | opts: []Option{ 66 | &Options{ 67 | FgColor: ColorCyan, 68 | BgColor: ColorMagenta, 69 | }, 70 | }, 71 | want: &Options{ 72 | FgColor: ColorCyan, 73 | BgColor: ColorMagenta, 74 | }, 75 | }, 76 | { 77 | desc: "setting font attributes", 78 | opts: []Option{ 79 | Bold(), 80 | Italic(), 81 | Underline(), 82 | Strikethrough(), 83 | Inverse(), 84 | Blink(), 85 | Dim(), 86 | }, 87 | want: &Options{ 88 | Bold: true, 89 | Italic: true, 90 | Underline: true, 91 | Strikethrough: true, 92 | Inverse: true, 93 | Blink: true, 94 | Dim: true, 95 | }, 96 | }, 97 | } 98 | 99 | for _, tc := range tests { 100 | t.Run(tc.desc, func(t *testing.T) { 101 | got := NewOptions(tc.opts...) 102 | if diff := pretty.Compare(tc.want, got); diff != "" { 103 | t.Errorf("NewOptions => unexpected diff (-want, +got):\n%s", diff) 104 | } 105 | }) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /cell/color.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 cell 16 | 17 | import ( 18 | "fmt" 19 | ) 20 | 21 | // color.go defines constants for cell colors. 22 | 23 | // Color is the color of a cell. 24 | type Color int 25 | 26 | // String implements fmt.Stringer() 27 | func (cc Color) String() string { 28 | if n, ok := colorNames[cc]; ok { 29 | return n 30 | } 31 | return fmt.Sprintf("Color:%d", cc) 32 | } 33 | 34 | // colorNames maps Color values to human readable names. 35 | var colorNames = map[Color]string{ 36 | ColorDefault: "ColorDefault", 37 | ColorBlack: "ColorBlack", 38 | ColorRed: "ColorRed", 39 | ColorGreen: "ColorGreen", 40 | ColorYellow: "ColorYellow", 41 | ColorBlue: "ColorBlue", 42 | ColorMagenta: "ColorMagenta", 43 | ColorCyan: "ColorCyan", 44 | ColorWhite: "ColorWhite", 45 | } 46 | 47 | // The supported terminal colors. 48 | const ( 49 | ColorDefault Color = iota 50 | 51 | // The 16 Xterm colors. 52 | // See https://jonasjacek.github.io/colors/ 53 | ColorBlack 54 | ColorMaroon 55 | ColorGreen 56 | ColorOlive 57 | ColorNavy 58 | ColorPurple 59 | ColorTeal 60 | ColorSilver 61 | ColorGray 62 | ColorRed 63 | ColorLime 64 | ColorYellow 65 | ColorBlue 66 | ColorFuchsia 67 | ColorAqua 68 | ColorWhite 69 | ) 70 | 71 | // Colors defined for backward compatibility with termbox-go. 72 | const ( 73 | ColorMagenta Color = ColorPurple 74 | ColorCyan Color = ColorTeal 75 | ) 76 | 77 | // ColorNumber sets a color using its number. 78 | // Make sure your terminal is set to a terminalapi.ColorMode that supports the 79 | // target color. The provided value must be in the range 0-255. 80 | // Larger or smaller values will be reset to the default color. 81 | // 82 | // For reference on these colors see the Xterm number in: 83 | // https://jonasjacek.github.io/colors/ 84 | func ColorNumber(n int) Color { 85 | if n < 0 || n > 255 { 86 | return ColorDefault 87 | } 88 | return Color(n + 1) // Colors are off-by-one due to ColorDefault being zero. 89 | } 90 | 91 | // ColorRGB6 sets a color using the 6x6x6 terminal color. 92 | // Make sure your terminal is set to the terminalapi.ColorMode256 mode. 93 | // The provided values (r, g, b) must be in the range 0-5. 94 | // Larger or smaller values will be reset to the default color. 95 | // 96 | // For reference on these colors see: 97 | // https://superuser.com/questions/783656/whats-the-deal-with-terminal-colors 98 | func ColorRGB6(r, g, b int) Color { 99 | for _, c := range []int{r, g, b} { 100 | if c < 0 || c > 5 { 101 | return ColorDefault 102 | } 103 | } 104 | // Explanation: 105 | // https://stackoverflow.com/questions/27159322/rgb-values-of-the-colors-in-the-ansi-extended-colors-index-17-255 106 | return Color(0x10 + 36*r + 6*g + b + 1) // Colors are off-by-one due to ColorDefault being zero. 107 | } 108 | 109 | // ColorRGB24 sets a color using the 24 bit web color scheme. 110 | // Make sure your terminal is set to the terminalapi.ColorMode256 mode. 111 | // The provided values (r, g, b) must be in the range 0-255. 112 | // Larger or smaller values will be reset to the default color. 113 | // 114 | // For reference on these colors see the RGB column in: 115 | // https://jonasjacek.github.io/colors/ 116 | func ColorRGB24(r, g, b int) Color { 117 | for _, c := range []int{r, g, b} { 118 | if c < 0 || c > 255 { 119 | return ColorDefault 120 | } 121 | } 122 | return ColorRGB6(r/51, g/51, b/51) 123 | } 124 | -------------------------------------------------------------------------------- /cell/color_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 cell 16 | 17 | import ( 18 | "fmt" 19 | "testing" 20 | ) 21 | 22 | func TestColorNumber(t *testing.T) { 23 | tests := []struct { 24 | desc string 25 | number int 26 | want Color 27 | }{ 28 | { 29 | desc: "default when too small", 30 | number: -1, 31 | want: ColorDefault, 32 | }, 33 | { 34 | desc: "default when too large", 35 | number: 256, 36 | want: ColorDefault, 37 | }, 38 | { 39 | desc: "translates system color", 40 | number: 0, 41 | want: ColorBlack, 42 | }, 43 | { 44 | desc: "adds one to the value", 45 | number: 42, 46 | want: Color(43), 47 | }, 48 | } 49 | 50 | for _, tc := range tests { 51 | t.Run(tc.desc, func(t *testing.T) { 52 | t.Logf(fmt.Sprintf("color: %v", tc.want)) 53 | got := ColorNumber(tc.number) 54 | if got != tc.want { 55 | t.Errorf("ColorNumber(%v) => %v, want %v", tc.number, got, tc.want) 56 | } 57 | }) 58 | } 59 | } 60 | 61 | func TestColorRGB6(t *testing.T) { 62 | tests := []struct { 63 | desc string 64 | r, g, b int 65 | want Color 66 | }{ 67 | { 68 | desc: "default when r too small", 69 | r: -1, 70 | g: 0, 71 | b: 0, 72 | want: ColorDefault, 73 | }, 74 | { 75 | desc: "default when r too large", 76 | r: 6, 77 | g: 0, 78 | b: 0, 79 | want: ColorDefault, 80 | }, 81 | { 82 | desc: "default when g too small", 83 | r: 0, 84 | g: -1, 85 | b: 0, 86 | want: ColorDefault, 87 | }, 88 | { 89 | desc: "default when g too large", 90 | r: 0, 91 | g: 6, 92 | b: 0, 93 | want: ColorDefault, 94 | }, 95 | { 96 | desc: "default when b too small", 97 | r: 0, 98 | g: 0, 99 | b: -1, 100 | want: ColorDefault, 101 | }, 102 | { 103 | desc: "default when b too large", 104 | r: 0, 105 | g: 0, 106 | b: 6, 107 | want: ColorDefault, 108 | }, 109 | { 110 | desc: "translates black", 111 | r: 0, 112 | g: 0, 113 | b: 0, 114 | want: Color(17), 115 | }, 116 | { 117 | desc: "adds one to value", 118 | r: 2, 119 | g: 1, 120 | b: 3, 121 | want: Color(98), 122 | }, 123 | } 124 | 125 | for _, tc := range tests { 126 | t.Run(tc.desc, func(t *testing.T) { 127 | got := ColorRGB6(tc.r, tc.g, tc.b) 128 | if got != tc.want { 129 | t.Errorf("ColorRGB6(%v, %v, %v) => %v, want %v", tc.r, tc.g, tc.b, got, tc.want) 130 | } 131 | }) 132 | } 133 | } 134 | 135 | func TestColorRGB24(t *testing.T) { 136 | tests := []struct { 137 | desc string 138 | r, g, b int 139 | want Color 140 | }{ 141 | { 142 | desc: "default when r too small", 143 | r: -1, 144 | g: 0, 145 | b: 0, 146 | want: ColorDefault, 147 | }, 148 | { 149 | desc: "default when r too large", 150 | r: 256, 151 | g: 0, 152 | b: 0, 153 | want: ColorDefault, 154 | }, 155 | { 156 | desc: "default when g too small", 157 | r: 0, 158 | g: -1, 159 | b: 0, 160 | want: ColorDefault, 161 | }, 162 | { 163 | desc: "default when g too large", 164 | r: 0, 165 | g: 256, 166 | b: 0, 167 | want: ColorDefault, 168 | }, 169 | { 170 | desc: "default when b too small", 171 | r: 0, 172 | g: 0, 173 | b: -1, 174 | want: ColorDefault, 175 | }, 176 | { 177 | desc: "default when b too large", 178 | r: 0, 179 | g: 0, 180 | b: 256, 181 | want: ColorDefault, 182 | }, 183 | { 184 | desc: "translates black", 185 | r: 0, 186 | g: 0, 187 | b: 0, 188 | want: Color(17), 189 | }, 190 | { 191 | desc: "adds one to value", 192 | r: 95, 193 | g: 255, 194 | b: 135, 195 | want: Color(85), 196 | }, 197 | } 198 | 199 | for _, tc := range tests { 200 | t.Run(tc.desc, func(t *testing.T) { 201 | got := ColorRGB24(tc.r, tc.g, tc.b) 202 | if got != tc.want { 203 | t.Errorf("ColorRGB24(%v, %v, %v) => %v, want %v", tc.r, tc.g, tc.b, got, tc.want) 204 | } 205 | }) 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /container/traversal.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 | import ( 18 | "errors" 19 | "fmt" 20 | ) 21 | 22 | // traversal.go provides functions that navigate the container tree. 23 | 24 | // rootCont returns the root container. 25 | func rootCont(c *Container) *Container { 26 | for p := c.parent; p != nil; p = c.parent { 27 | c = p 28 | } 29 | return c 30 | } 31 | 32 | // visitFunc is executed during traversals when node is visited. 33 | // If the visit function returns an error, the traversal terminates and the 34 | // errStr is set to the text of the returned error. 35 | type visitFunc func(*Container) error 36 | 37 | // preOrder performs pre-order DFS traversal on the container tree. 38 | func preOrder(c *Container, errStr *string, visit visitFunc) { 39 | if c == nil || *errStr != "" { 40 | return 41 | } 42 | 43 | if err := visit(c); err != nil { 44 | *errStr = err.Error() 45 | return 46 | } 47 | preOrder(c.first, errStr, visit) 48 | preOrder(c.second, errStr, visit) 49 | } 50 | 51 | // postOrder performs post-order DFS traversal on the container tree. 52 | func postOrder(c *Container, errStr *string, visit visitFunc) { 53 | if c == nil || *errStr != "" { 54 | return 55 | } 56 | 57 | postOrder(c.first, errStr, visit) 58 | postOrder(c.second, errStr, visit) 59 | if err := visit(c); err != nil { 60 | *errStr = err.Error() 61 | return 62 | } 63 | } 64 | 65 | // findID finds container with the provided ID. 66 | // Returns an error of there is no container with the specified ID. 67 | func findID(root *Container, id string) (*Container, error) { 68 | if id == "" { 69 | return nil, errors.New("the container ID must not be empty") 70 | } 71 | 72 | var ( 73 | errStr string 74 | cont *Container 75 | ) 76 | preOrder(root, &errStr, visitFunc(func(c *Container) error { 77 | if c.opts.id == id { 78 | cont = c 79 | } 80 | return nil 81 | })) 82 | if cont == nil { 83 | return nil, fmt.Errorf("cannot find container with ID %q", id) 84 | } 85 | return cont, nil 86 | } 87 | -------------------------------------------------------------------------------- /doc/design_goals.md: -------------------------------------------------------------------------------- 1 | # Design goals 2 | 3 | This effort is focused on good software design and maintainability. By good 4 | design I mean: 5 | 6 | 1. Write readable, well documented code. 7 | 1. Only beautiful, simple APIs, no exposed concurrency, channels, internals, etc. 8 | 1. Follow [Effective Go](http://golang.org/doc/effective_go.html). 9 | 1. Provide an infrastructure that allows development of individual dashboard 10 | components in separation. 11 | 1. The infrastructure must enforce consistency in how the dashboard components 12 | are implemented. 13 | 1. Focus on maintainability, the infrastructure and dashboard components must 14 | have good test coverage, the repository must have CI/CD enabled. 15 | -------------------------------------------------------------------------------- /doc/design_guidelines.md: -------------------------------------------------------------------------------- 1 | # Design guidelines 2 | 3 | ## Don't clutter the widget code with drawing primitives 4 | 5 | The widget implementations should contain high level code only. Low level 6 | drawing primitives should be in separate packages. That way the widgets remain 7 | easy to understand, enhance and test. 8 | 9 | E.g. the **gauge** widget contains code that calculates the size of the 10 | rectangle that needs to be drawn. It doesn't contain code that draws the 11 | rectangle itself as that belongs into the **draw** package. 12 | 13 | ## Provide test helpers for all functions in the draw package 14 | 15 | To simplify unit tests of widgets, a test helper should be provided to all 16 | functions in the **draw** package. 17 | 18 | E.g. a function called **Rectangle()** that draws a rectangle should come with 19 | a helper caller **MustRectangle()**. Tests of a widget that uses 20 | **Rectangle()** can just specify the expected rectangle by calling 21 | **MustRectangle()** on the test canvas. 22 | -------------------------------------------------------------------------------- /doc/images/barchartdemo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mum4k/termdash/102df20a882547528c83047688595acd2a82c7ef/doc/images/barchartdemo.gif -------------------------------------------------------------------------------- /doc/images/buttondemo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mum4k/termdash/102df20a882547528c83047688595acd2a82c7ef/doc/images/buttondemo.gif -------------------------------------------------------------------------------- /doc/images/donutdemo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mum4k/termdash/102df20a882547528c83047688595acd2a82c7ef/doc/images/donutdemo.gif -------------------------------------------------------------------------------- /doc/images/formdemo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mum4k/termdash/102df20a882547528c83047688595acd2a82c7ef/doc/images/formdemo.gif -------------------------------------------------------------------------------- /doc/images/gaugedemo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mum4k/termdash/102df20a882547528c83047688595acd2a82c7ef/doc/images/gaugedemo.gif -------------------------------------------------------------------------------- /doc/images/hld.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mum4k/termdash/102df20a882547528c83047688595acd2a82c7ef/doc/images/hld.graffle -------------------------------------------------------------------------------- /doc/images/hld.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mum4k/termdash/102df20a882547528c83047688595acd2a82c7ef/doc/images/hld.png -------------------------------------------------------------------------------- /doc/images/linechartdemo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mum4k/termdash/102df20a882547528c83047688595acd2a82c7ef/doc/images/linechartdemo.gif -------------------------------------------------------------------------------- /doc/images/segmentdisplaydemo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mum4k/termdash/102df20a882547528c83047688595acd2a82c7ef/doc/images/segmentdisplaydemo.gif -------------------------------------------------------------------------------- /doc/images/sparklinedemo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mum4k/termdash/102df20a882547528c83047688595acd2a82c7ef/doc/images/sparklinedemo.gif -------------------------------------------------------------------------------- /doc/images/termdash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mum4k/termdash/102df20a882547528c83047688595acd2a82c7ef/doc/images/termdash.png -------------------------------------------------------------------------------- /doc/images/termdashdemo_0_8_0.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mum4k/termdash/102df20a882547528c83047688595acd2a82c7ef/doc/images/termdashdemo_0_8_0.gif -------------------------------------------------------------------------------- /doc/images/termdashdemo_0_9_0.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mum4k/termdash/102df20a882547528c83047688595acd2a82c7ef/doc/images/termdashdemo_0_9_0.gif -------------------------------------------------------------------------------- /doc/images/textdemo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mum4k/termdash/102df20a882547528c83047688595acd2a82c7ef/doc/images/textdemo.gif -------------------------------------------------------------------------------- /doc/images/textinputdemo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mum4k/termdash/102df20a882547528c83047688595acd2a82c7ef/doc/images/textinputdemo.gif -------------------------------------------------------------------------------- /doc/requirements.md: -------------------------------------------------------------------------------- 1 | # Requirements 2 | 3 | 1. Native support of the UTF-8 encoding. 4 | 1. Simple container management to position the widgets and set their size. 5 | 1. Mouse and keyboard input. 6 | 1. Cross-platform terminal based output. 7 | 1. Unit testing framework for simple and readable tests of dashboard elements. 8 | 1. Tooling to streamline addition of new widgets. 9 | 1. Apache-2.0 licence for the project. 10 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mum4k/termdash 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/gdamore/tcell/v2 v2.7.4 7 | github.com/kylelemons/godebug v1.1.0 8 | github.com/mattn/go-runewidth v0.0.15 9 | github.com/nsf/termbox-go v1.1.1 10 | ) 11 | 12 | require ( 13 | github.com/gdamore/encoding v1.0.0 // indirect 14 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 15 | github.com/rivo/uniseg v0.4.3 // indirect 16 | golang.org/x/sys v0.17.0 // indirect 17 | golang.org/x/term v0.17.0 // indirect 18 | golang.org/x/text v0.14.0 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= 2 | github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= 3 | github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU= 4 | github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg= 5 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 6 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 7 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 8 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 9 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 10 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 11 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 12 | github.com/nsf/termbox-go v1.1.1 h1:nksUPLCb73Q++DwbYUBEglYBRPZyoXJdrj5L+TkjyZY= 13 | github.com/nsf/termbox-go v1.1.1/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo= 14 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 15 | github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= 16 | github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 17 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 18 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 19 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 20 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 21 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 22 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 23 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 24 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 25 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 26 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 27 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 28 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 29 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 30 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 31 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 32 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 33 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 34 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 35 | golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= 36 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 37 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 38 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 39 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 40 | golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= 41 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 42 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 43 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 44 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 45 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 46 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 47 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 48 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 49 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 50 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 51 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 52 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 53 | -------------------------------------------------------------------------------- /keyboard/keyboard.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 keyboard defines well known keyboard keys and shortcuts. 16 | package keyboard 17 | 18 | // Key represents a single button on the keyboard. 19 | // Printable characters are set to their ASCII/Unicode rune value. 20 | // Non-printable (control) characters are equal to one of the constants defined 21 | // below. 22 | type Key rune 23 | 24 | // String implements fmt.Stringer() 25 | func (b Key) String() string { 26 | if n, ok := buttonNames[b]; ok { 27 | return n 28 | } else if b >= 0 { 29 | return string(b) 30 | } 31 | return "KeyUnknown" 32 | } 33 | 34 | // buttonNames maps Key values to human readable names. 35 | var buttonNames = map[Key]string{ 36 | KeyF1: "KeyF1", 37 | KeyF2: "KeyF2", 38 | KeyF3: "KeyF3", 39 | KeyF4: "KeyF4", 40 | KeyF5: "KeyF5", 41 | KeyF6: "KeyF6", 42 | KeyF7: "KeyF7", 43 | KeyF8: "KeyF8", 44 | KeyF9: "KeyF9", 45 | KeyF10: "KeyF10", 46 | KeyF11: "KeyF11", 47 | KeyF12: "KeyF12", 48 | KeyInsert: "KeyInsert", 49 | KeyDelete: "KeyDelete", 50 | KeyHome: "KeyHome", 51 | KeyEnd: "KeyEnd", 52 | KeyPgUp: "KeyPgUp", 53 | KeyPgDn: "KeyPgDn", 54 | KeyArrowUp: "KeyArrowUp", 55 | KeyArrowDown: "KeyArrowDown", 56 | KeyArrowLeft: "KeyArrowLeft", 57 | KeyArrowRight: "KeyArrowRight", 58 | KeyCtrlTilde: "KeyCtrlTilde", 59 | KeyCtrlA: "KeyCtrlA", 60 | KeyCtrlB: "KeyCtrlB", 61 | KeyCtrlC: "KeyCtrlC", 62 | KeyCtrlD: "KeyCtrlD", 63 | KeyCtrlE: "KeyCtrlE", 64 | KeyCtrlF: "KeyCtrlF", 65 | KeyCtrlG: "KeyCtrlG", 66 | KeyBackspace: "KeyBackspace", 67 | KeyTab: "KeyTab", 68 | KeyBacktab: "KeyBacktab", 69 | KeyCtrlJ: "KeyCtrlJ", 70 | KeyCtrlK: "KeyCtrlK", 71 | KeyCtrlL: "KeyCtrlL", 72 | KeyEnter: "KeyEnter", 73 | KeyCtrlN: "KeyCtrlN", 74 | KeyCtrlO: "KeyCtrlO", 75 | KeyCtrlP: "KeyCtrlP", 76 | KeyCtrlQ: "KeyCtrlQ", 77 | KeyCtrlR: "KeyCtrlR", 78 | KeyCtrlS: "KeyCtrlS", 79 | KeyCtrlT: "KeyCtrlT", 80 | KeyCtrlU: "KeyCtrlU", 81 | KeyCtrlV: "KeyCtrlV", 82 | KeyCtrlW: "KeyCtrlW", 83 | KeyCtrlX: "KeyCtrlX", 84 | KeyCtrlY: "KeyCtrlY", 85 | KeyCtrlZ: "KeyCtrlZ", 86 | KeyEsc: "KeyEsc", 87 | KeyCtrl4: "KeyCtrl4", 88 | KeyCtrl5: "KeyCtrl5", 89 | KeyCtrl6: "KeyCtrl6", 90 | KeyCtrl7: "KeyCtrl7", 91 | KeySpace: "KeySpace", 92 | KeyBackspace2: "KeyBackspace2", 93 | } 94 | 95 | // Printable characters, but worth having constants for them. 96 | const ( 97 | KeySpace = ' ' 98 | ) 99 | 100 | // Negative values for non-printable characters. 101 | const ( 102 | KeyF1 Key = -(iota + 1) 103 | KeyF2 104 | KeyF3 105 | KeyF4 106 | KeyF5 107 | KeyF6 108 | KeyF7 109 | KeyF8 110 | KeyF9 111 | KeyF10 112 | KeyF11 113 | KeyF12 114 | KeyInsert 115 | KeyDelete 116 | KeyHome 117 | KeyEnd 118 | KeyPgUp 119 | KeyPgDn 120 | KeyArrowUp 121 | KeyArrowDown 122 | KeyArrowLeft 123 | KeyArrowRight 124 | KeyCtrlTilde 125 | KeyCtrlA 126 | KeyCtrlB 127 | KeyCtrlC 128 | KeyCtrlD 129 | KeyCtrlE 130 | KeyCtrlF 131 | KeyCtrlG 132 | KeyBackspace 133 | KeyTab 134 | KeyBacktab 135 | KeyCtrlJ 136 | KeyCtrlK 137 | KeyCtrlL 138 | KeyEnter 139 | KeyCtrlN 140 | KeyCtrlO 141 | KeyCtrlP 142 | KeyCtrlQ 143 | KeyCtrlR 144 | KeyCtrlS 145 | KeyCtrlT 146 | KeyCtrlU 147 | KeyCtrlV 148 | KeyCtrlW 149 | KeyCtrlX 150 | KeyCtrlY 151 | KeyCtrlZ 152 | KeyEsc 153 | KeyCtrl4 154 | KeyCtrl5 155 | KeyCtrl6 156 | KeyCtrl7 157 | KeyBackspace2 158 | ) 159 | 160 | // Keys declared as duplicates by termbox. 161 | const ( 162 | KeyCtrl2 Key = KeyCtrlTilde 163 | KeyCtrlSpace Key = KeyCtrlTilde 164 | KeyCtrlH Key = KeyBackspace 165 | KeyCtrlI Key = KeyTab 166 | KeyCtrlM Key = KeyEnter 167 | KeyCtrlLsqBracket Key = KeyEsc 168 | KeyCtrl3 Key = KeyEsc 169 | KeyCtrlBackslash Key = KeyCtrl4 170 | KeyCtrlRsqBracket Key = KeyCtrl5 171 | KeyCtrlSlash Key = KeyCtrl7 172 | KeyCtrlUnderscore Key = KeyCtrl7 173 | KeyCtrl8 Key = KeyBackspace2 174 | ) 175 | -------------------------------------------------------------------------------- /keyboard/keyboard_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package keyboard 16 | 17 | import "testing" 18 | 19 | func TestString(t *testing.T) { 20 | tests := []struct { 21 | desc string 22 | key Key 23 | want string 24 | }{ 25 | { 26 | desc: "unknown", 27 | key: Key(-1000), 28 | want: "KeyUnknown", 29 | }, 30 | { 31 | desc: "defined value", 32 | key: KeyEnter, 33 | want: "KeyEnter", 34 | }, 35 | { 36 | desc: "standard key", 37 | key: 'a', 38 | want: "a", 39 | }, 40 | } 41 | 42 | for _, tc := range tests { 43 | t.Run(tc.desc, func(t *testing.T) { 44 | if got := tc.key.String(); got != tc.want { 45 | t.Errorf("String => %q, want %q", got, tc.want) 46 | } 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /linestyle/linestyle.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 linestyle defines various line styles. 16 | package linestyle 17 | 18 | // LineStyle defines the supported line styles. 19 | type LineStyle int 20 | 21 | // String implements fmt.Stringer() 22 | func (ls LineStyle) String() string { 23 | if n, ok := lineStyleNames[ls]; ok { 24 | return n 25 | } 26 | return "LineStyleUnknown" 27 | } 28 | 29 | // lineStyleNames maps LineStyle values to human readable names. 30 | var lineStyleNames = map[LineStyle]string{ 31 | None: "LineStyleNone", 32 | Light: "LineStyleLight", 33 | Double: "LineStyleDouble", 34 | Round: "LineStyleRound", 35 | } 36 | 37 | // Supported line styles. 38 | // See https://en.wikipedia.org/wiki/Box-drawing_character. 39 | const ( 40 | // None indicates that no line should be present. 41 | None LineStyle = iota 42 | 43 | // Light is line style using the '─' characters. 44 | Light 45 | 46 | // Double is line style using the '═' characters. 47 | Double 48 | 49 | // Round is line style using the rounded corners '╭' characters. 50 | Round 51 | ) 52 | -------------------------------------------------------------------------------- /linestyle/linestyle_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 linestyle 16 | 17 | import "testing" 18 | 19 | func TestLineStyleName(t *testing.T) { 20 | tests := []struct { 21 | desc string 22 | ls LineStyle 23 | want string 24 | }{ 25 | { 26 | desc: "unknown", 27 | ls: LineStyle(-1), 28 | want: "LineStyleUnknown", 29 | }, 30 | { 31 | desc: "none", 32 | ls: None, 33 | want: "LineStyleNone", 34 | }, 35 | { 36 | desc: "light", 37 | ls: Light, 38 | want: "LineStyleLight", 39 | }, 40 | { 41 | desc: "double", 42 | ls: Double, 43 | want: "LineStyleDouble", 44 | }, 45 | { 46 | desc: "round", 47 | ls: Round, 48 | want: "LineStyleRound", 49 | }, 50 | } 51 | 52 | for _, tc := range tests { 53 | t.Run(tc.desc, func(t *testing.T) { 54 | if got := tc.ls.String(); got != tc.want { 55 | t.Errorf("String => %q, want %q", got, tc.want) 56 | } 57 | 58 | }) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /mouse/mouse.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 mouse defines known mouse buttons. 16 | package mouse 17 | 18 | // Button represents a mouse button. 19 | type Button int 20 | 21 | // String implements fmt.Stringer() 22 | func (b Button) String() string { 23 | if n, ok := buttonNames[b]; ok { 24 | return n 25 | } 26 | return "ButtonUnknown" 27 | } 28 | 29 | // buttonNames maps Button values to human readable names. 30 | var buttonNames = map[Button]string{ 31 | ButtonLeft: "ButtonLeft", 32 | ButtonRight: "ButtonRight", 33 | ButtonMiddle: "ButtonMiddle", 34 | ButtonRelease: "ButtonRelease", 35 | ButtonWheelUp: "ButtonWheelUp", 36 | ButtonWheelDown: "ButtonWheelDown", 37 | } 38 | 39 | // Buttons recognized on the mouse. 40 | const ( 41 | buttonUnknown Button = iota 42 | ButtonLeft 43 | ButtonRight 44 | ButtonMiddle 45 | ButtonRelease 46 | ButtonWheelUp 47 | ButtonWheelDown 48 | ) 49 | -------------------------------------------------------------------------------- /mouse/mouse_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package mouse 16 | 17 | import "testing" 18 | 19 | func TestString(t *testing.T) { 20 | tests := []struct { 21 | desc string 22 | button Button 23 | want string 24 | }{ 25 | { 26 | desc: "unknown", 27 | button: Button(-1000), 28 | want: "ButtonUnknown", 29 | }, 30 | { 31 | desc: "defined value", 32 | button: ButtonLeft, 33 | want: "ButtonLeft", 34 | }, 35 | } 36 | 37 | for _, tc := range tests { 38 | t.Run(tc.desc, func(t *testing.T) { 39 | if got := tc.button.String(); got != tc.want { 40 | t.Errorf("String => %q, want %q", got, tc.want) 41 | } 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /private/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 | -------------------------------------------------------------------------------- /private/alignfor/alignfor.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package alignfor provides functions that align elements. 16 | package alignfor 17 | 18 | import ( 19 | "fmt" 20 | "image" 21 | "strings" 22 | 23 | "github.com/mum4k/termdash/align" 24 | "github.com/mum4k/termdash/private/runewidth" 25 | "github.com/mum4k/termdash/private/wrap" 26 | ) 27 | 28 | // hAlign aligns the given area in the rectangle horizontally. 29 | func hAlign(rect image.Rectangle, ar image.Rectangle, h align.Horizontal) (image.Rectangle, error) { 30 | gap := rect.Dx() - ar.Dx() 31 | switch h { 32 | case align.HorizontalRight: 33 | // Use gap from above. 34 | case align.HorizontalCenter: 35 | gap /= 2 36 | case align.HorizontalLeft: 37 | gap = 0 38 | default: 39 | return image.ZR, fmt.Errorf("unsupported horizontal alignment %v", h) 40 | } 41 | 42 | return image.Rect( 43 | rect.Min.X+gap, 44 | ar.Min.Y, 45 | rect.Min.X+gap+ar.Dx(), 46 | ar.Max.Y, 47 | ), nil 48 | } 49 | 50 | // vAlign aligns the given area in the rectangle vertically. 51 | func vAlign(rect image.Rectangle, ar image.Rectangle, v align.Vertical) (image.Rectangle, error) { 52 | gap := rect.Dy() - ar.Dy() 53 | switch v { 54 | case align.VerticalBottom: 55 | // Use gap from above. 56 | case align.VerticalMiddle: 57 | gap /= 2 58 | case align.VerticalTop: 59 | gap = 0 60 | default: 61 | return image.ZR, fmt.Errorf("unsupported vertical alignment %v", v) 62 | } 63 | 64 | return image.Rect( 65 | ar.Min.X, 66 | rect.Min.Y+gap, 67 | ar.Max.X, 68 | rect.Min.Y+gap+ar.Dy(), 69 | ), nil 70 | } 71 | 72 | // Rectangle aligns the area within the rectangle returning the 73 | // aligned area. The area must fall within the rectangle. 74 | func Rectangle(rect image.Rectangle, ar image.Rectangle, h align.Horizontal, v align.Vertical) (image.Rectangle, error) { 75 | if !ar.In(rect) { 76 | return image.ZR, fmt.Errorf("cannot align area %v inside rectangle %v, the area falls outside of the rectangle", ar, rect) 77 | } 78 | 79 | aligned, err := hAlign(rect, ar, h) 80 | if err != nil { 81 | return image.ZR, err 82 | } 83 | aligned, err = vAlign(rect, aligned, v) 84 | if err != nil { 85 | return image.ZR, err 86 | } 87 | return aligned, nil 88 | } 89 | 90 | // Text aligns the text within the given rectangle, returns the start point for the text. 91 | // For the purposes of the alignment this assumes that text will be trimmed if 92 | // it overruns the rectangle. 93 | // This only supports a single line of text, the text must not contain non-printable characters, 94 | // allows empty text. 95 | func Text(rect image.Rectangle, text string, h align.Horizontal, v align.Vertical) (image.Point, error) { 96 | if strings.ContainsRune(text, '\n') { 97 | return image.ZP, fmt.Errorf("the provided text contains a newline character: %q", text) 98 | } 99 | 100 | if text != "" { 101 | if err := wrap.ValidText(text); err != nil { 102 | return image.ZP, fmt.Errorf("the provided text contains non printable character(s): %s", err) 103 | } 104 | } 105 | 106 | cells := runewidth.StringWidth(text) 107 | var textLen int 108 | if cells < rect.Dx() { 109 | textLen = cells 110 | } else { 111 | textLen = rect.Dx() 112 | } 113 | 114 | textRect := image.Rect( 115 | rect.Min.X, 116 | rect.Min.Y, 117 | // For the purposes of aligning the text, assume that it will be 118 | // trimmed to the available space. 119 | rect.Min.X+textLen, 120 | rect.Min.Y+1, 121 | ) 122 | 123 | aligned, err := Rectangle(rect, textRect, h, v) 124 | if err != nil { 125 | return image.ZP, err 126 | } 127 | return image.Point{aligned.Min.X, aligned.Min.Y}, nil 128 | } 129 | -------------------------------------------------------------------------------- /private/attrrange/attrrange.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 attrrange simplifies tracking of attributes that apply to a range of 16 | // items. 17 | // Refer to the examples in the test file for details on usage. 18 | package attrrange 19 | 20 | import ( 21 | "fmt" 22 | "sort" 23 | ) 24 | 25 | // AttrRange is a range of items that share the same attributes. 26 | type AttrRange struct { 27 | // Low is the first position where these attributes apply. 28 | Low int 29 | 30 | // High is the end of the range. The attributes apply to all items in range 31 | // Low <= b < high. 32 | High int 33 | 34 | // AttrIdx is the index of the attributes that apply to this range. 35 | AttrIdx int 36 | } 37 | 38 | // newAttrRange returns a new AttrRange instance. 39 | func newAttrRange(low, high, attrIdx int) *AttrRange { 40 | return &AttrRange{ 41 | Low: low, 42 | High: high, 43 | AttrIdx: attrIdx, 44 | } 45 | } 46 | 47 | // Tracker tracks attributes that apply to a range of items. 48 | // This object is not thread safe. 49 | type Tracker struct { 50 | // ranges maps low indices of ranges to the attribute ranges. 51 | ranges map[int]*AttrRange 52 | } 53 | 54 | // NewTracker returns a new tracker of ranges that share the same attributes. 55 | func NewTracker() *Tracker { 56 | return &Tracker{ 57 | ranges: map[int]*AttrRange{}, 58 | } 59 | } 60 | 61 | // Add adds a new range of items that share attributes with the specified 62 | // index. 63 | // The low position of the range must not overlap with low position of any 64 | // existing range. 65 | func (t *Tracker) Add(low, high, attrIdx int) error { 66 | ar := newAttrRange(low, high, attrIdx) 67 | if ar, ok := t.ranges[low]; ok { 68 | return fmt.Errorf("already have range starting on low:%d, existing:%+v", low, ar) 69 | } 70 | t.ranges[low] = ar 71 | return nil 72 | } 73 | 74 | // ForPosition returns attribute index that apply to the specified position. 75 | // Returns ErrNotFound when the requested position wasn't found in any of the 76 | // known ranges. 77 | func (t *Tracker) ForPosition(pos int) (*AttrRange, error) { 78 | if ar, ok := t.ranges[pos]; ok { 79 | return ar, nil 80 | } 81 | 82 | var keys []int 83 | for k := range t.ranges { 84 | keys = append(keys, k) 85 | } 86 | sort.Ints(keys) 87 | 88 | var res *AttrRange 89 | for _, k := range keys { 90 | ar := t.ranges[k] 91 | if ar.Low > pos { 92 | break 93 | } 94 | if ar.High > pos { 95 | res = ar 96 | } 97 | } 98 | 99 | if res == nil { 100 | return nil, fmt.Errorf("did not find attribute range for position %d", pos) 101 | } 102 | return res, nil 103 | } 104 | -------------------------------------------------------------------------------- /private/attrrange/attrrange_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 attrrange 16 | 17 | import ( 18 | "log" 19 | "testing" 20 | 21 | "github.com/kylelemons/godebug/pretty" 22 | "github.com/mum4k/termdash/cell" 23 | ) 24 | 25 | func Example() { 26 | // Caller has a slice of some attributes, like a cell color that applies 27 | // to a portion of text. 28 | attrs := []cell.Color{cell.ColorRed, cell.ColorBlue} 29 | redIdx := 0 30 | blueIdx := 1 31 | 32 | // This is the text the colors apply to. 33 | const text = "HelloWorld" 34 | 35 | // Assuming that we want the word "Hello" in red and the word "World" in 36 | // green, we can set our ranges as follows: 37 | tr := NewTracker() 38 | if err := tr.Add(0, len("Hello"), redIdx); err != nil { 39 | panic(err) 40 | } 41 | if err := tr.Add(len("Hello")+1, len(text), blueIdx); err != nil { 42 | panic(err) 43 | } 44 | 45 | // Now to get the index into attrs (i.e. the color) for a particular 46 | // character, we can do: 47 | for i, c := range text { 48 | ar, err := tr.ForPosition(i) 49 | if err != nil { 50 | panic(err) 51 | } 52 | log.Printf("character at text[%d] = %q, color index %d = %v, range low:%d, high:%d", i, c, ar.AttrIdx, attrs[ar.AttrIdx], ar.Low, ar.High) 53 | } 54 | } 55 | 56 | func TestForPosition(t *testing.T) { 57 | tests := []struct { 58 | desc string 59 | // if not nil, called before calling ForPosition. 60 | // Can add ranges. 61 | update func(*Tracker) error 62 | pos int 63 | want *AttrRange 64 | wantErr bool 65 | wantUpdateErr bool 66 | }{ 67 | { 68 | desc: "fails when no ranges given", 69 | pos: 0, 70 | wantErr: true, 71 | }, 72 | { 73 | desc: "fails to add a duplicate", 74 | update: func(tr *Tracker) error { 75 | if err := tr.Add(2, 5, 40); err != nil { 76 | return err 77 | } 78 | return tr.Add(2, 3, 41) 79 | }, 80 | wantUpdateErr: true, 81 | }, 82 | { 83 | desc: "fails when multiple given ranges, position falls before them", 84 | update: func(tr *Tracker) error { 85 | if err := tr.Add(2, 5, 40); err != nil { 86 | return err 87 | } 88 | return tr.Add(5, 10, 41) 89 | }, 90 | pos: 1, 91 | wantErr: true, 92 | }, 93 | { 94 | desc: "multiple given options, position falls on the lower", 95 | update: func(tr *Tracker) error { 96 | if err := tr.Add(2, 5, 40); err != nil { 97 | return err 98 | } 99 | return tr.Add(5, 10, 41) 100 | }, 101 | pos: 2, 102 | want: newAttrRange(2, 5, 40), 103 | }, 104 | { 105 | desc: "multiple given options, position falls between them", 106 | update: func(tr *Tracker) error { 107 | if err := tr.Add(2, 5, 40); err != nil { 108 | return err 109 | } 110 | return tr.Add(5, 10, 41) 111 | }, 112 | pos: 4, 113 | want: newAttrRange(2, 5, 40), 114 | }, 115 | { 116 | desc: "multiple given options, position falls on the higher", 117 | update: func(tr *Tracker) error { 118 | if err := tr.Add(2, 5, 40); err != nil { 119 | return err 120 | } 121 | return tr.Add(5, 10, 41) 122 | }, 123 | pos: 5, 124 | want: newAttrRange(5, 10, 41), 125 | }, 126 | { 127 | desc: "multiple given options, position falls after them", 128 | update: func(tr *Tracker) error { 129 | if err := tr.Add(2, 5, 40); err != nil { 130 | return err 131 | } 132 | return tr.Add(5, 10, 41) 133 | }, 134 | pos: 10, 135 | wantErr: true, 136 | }, 137 | } 138 | 139 | for _, tc := range tests { 140 | t.Run(tc.desc, func(t *testing.T) { 141 | tr := NewTracker() 142 | if tc.update != nil { 143 | err := tc.update(tr) 144 | if (err != nil) != tc.wantUpdateErr { 145 | t.Errorf("tc.update => unexpected error:%v, wantUpdateErr:%v", err, tc.wantUpdateErr) 146 | } 147 | if err != nil { 148 | return 149 | } 150 | } 151 | 152 | got, err := tr.ForPosition(tc.pos) 153 | if (err != nil) != tc.wantErr { 154 | t.Errorf("ForPosition => unexpected error:%v, wantErr:%v", err, tc.wantErr) 155 | } 156 | if err != nil { 157 | return 158 | } 159 | 160 | if diff := pretty.Compare(tc.want, got); diff != "" { 161 | t.Errorf("ForPosition => unexpected diff (-want, +got):\n%s", diff) 162 | } 163 | }) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /private/button/button.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 button implements a state machine that tracks mouse button clicks. 16 | package button 17 | 18 | import ( 19 | "image" 20 | 21 | "github.com/mum4k/termdash/mouse" 22 | "github.com/mum4k/termdash/terminal/terminalapi" 23 | ) 24 | 25 | // State represents the state of the mouse button. 26 | type State int 27 | 28 | // String implements fmt.Stringer() 29 | func (s State) String() string { 30 | if n, ok := stateNames[s]; ok { 31 | return n 32 | } 33 | return "StateUnknown" 34 | } 35 | 36 | // stateNames maps State values to human readable names. 37 | var stateNames = map[State]string{ 38 | Up: "StateUp", 39 | Down: "StateDown", 40 | } 41 | 42 | const ( 43 | // Up is the default idle state of the mouse button. 44 | Up State = iota 45 | 46 | // Down is a state where the mouse button is pressed down and held. 47 | Down 48 | ) 49 | 50 | // FSM implements a finite-state machine that tracks mouse clicks within an 51 | // area. 52 | // 53 | // Simplifies tracking of mouse button clicks, i.e. when the caller wants to 54 | // perform an action only if both the button press and release happen within 55 | // the specified area. 56 | // 57 | // This object is not thread-safe. 58 | type FSM struct { 59 | // button is the mouse button whose state this FSM tracks. 60 | button mouse.Button 61 | 62 | // area is the area provided to NewFSM. 63 | area image.Rectangle 64 | 65 | // state is the current state of the FSM. 66 | state stateFn 67 | } 68 | 69 | // NewFSM creates a new FSM instance that tracks the state of the specified 70 | // mouse button through button events that fall within the provided area. 71 | func NewFSM(button mouse.Button, area image.Rectangle) *FSM { 72 | return &FSM{ 73 | button: button, 74 | area: area, 75 | state: wantPress, 76 | } 77 | } 78 | 79 | // Event is used to forward mouse events to the state machine. 80 | // Only events related to the button specified on a call to NewFSM are 81 | // processed. 82 | // 83 | // Returns a bool indicating if an action guarded by the button should be 84 | // performed and the state of the button after the provided event. 85 | // The bool is true if the button click should take an effect, i.e. if the 86 | // FSM saw both the button click and its release. 87 | func (fsm *FSM) Event(m *terminalapi.Mouse) (bool, State) { 88 | clicked, bs, next := fsm.state(fsm, m) 89 | fsm.state = next 90 | return clicked, bs 91 | } 92 | 93 | // UpdateArea informs FSM of an area change. 94 | // This method is idempotent. 95 | func (fsm *FSM) UpdateArea(area image.Rectangle) { 96 | fsm.area = area 97 | } 98 | 99 | // stateFn is a single state in the state machine. 100 | // Returns bool indicating if a click happened, the state of the button and the 101 | // next state of the FSM. 102 | type stateFn func(fsm *FSM, m *terminalapi.Mouse) (bool, State, stateFn) 103 | 104 | // wantPress is the initial state, expecting a button press inside the area. 105 | func wantPress(fsm *FSM, m *terminalapi.Mouse) (bool, State, stateFn) { 106 | if m.Button != fsm.button || !m.Position.In(fsm.area) { 107 | return false, Up, wantPress 108 | } 109 | return false, Down, wantRelease 110 | } 111 | 112 | // wantRelease waits for a mouse button release in the same area as 113 | // the press. 114 | func wantRelease(fsm *FSM, m *terminalapi.Mouse) (bool, State, stateFn) { 115 | switch m.Button { 116 | case fsm.button: 117 | if m.Position.In(fsm.area) { 118 | // Remain in the same state, since termbox reports move of mouse with 119 | // button held down as a series of clicks, one per position. 120 | return false, Down, wantRelease 121 | } 122 | return false, Up, wantPress 123 | 124 | case mouse.ButtonRelease: 125 | if m.Position.In(fsm.area) { 126 | // Seen both press and release, report a click. 127 | return true, Up, wantPress 128 | } 129 | // Release the button even if the release event happened outside of the area. 130 | return false, Up, wantPress 131 | 132 | default: 133 | return false, Up, wantPress 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /private/canvas/braille/testbraille/testbraille.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 testbraille provides helpers for tests that use the braille package. 16 | package testbraille 17 | 18 | import ( 19 | "fmt" 20 | "image" 21 | 22 | "github.com/mum4k/termdash/cell" 23 | "github.com/mum4k/termdash/private/canvas" 24 | "github.com/mum4k/termdash/private/canvas/braille" 25 | "github.com/mum4k/termdash/private/faketerm" 26 | ) 27 | 28 | // MustNew returns a new canvas or panics. 29 | func MustNew(area image.Rectangle) *braille.Canvas { 30 | cvs, err := braille.New(area) 31 | if err != nil { 32 | panic(fmt.Sprintf("braille.New => unexpected error: %v", err)) 33 | } 34 | return cvs 35 | } 36 | 37 | // MustApply applies the canvas on the terminal or panics. 38 | func MustApply(bc *braille.Canvas, t *faketerm.Terminal) { 39 | if err := bc.Apply(t); err != nil { 40 | panic(fmt.Sprintf("braille.Apply => unexpected error: %v", err)) 41 | } 42 | } 43 | 44 | // MustSetPixel sets the specified pixel or panics. 45 | func MustSetPixel(bc *braille.Canvas, p image.Point, opts ...cell.Option) { 46 | if err := bc.SetPixel(p, opts...); err != nil { 47 | panic(fmt.Sprintf("braille.SetPixel => unexpected error: %v", err)) 48 | } 49 | } 50 | 51 | // MustClearPixel clears the specified pixel or panics. 52 | func MustClearPixel(bc *braille.Canvas, p image.Point, opts ...cell.Option) { 53 | if err := bc.ClearPixel(p, opts...); err != nil { 54 | panic(fmt.Sprintf("braille.ClearPixel => unexpected error: %v", err)) 55 | } 56 | } 57 | 58 | // MustCopyTo copies the braille canvas onto the provided canvas or panics. 59 | func MustCopyTo(bc *braille.Canvas, dst *canvas.Canvas) { 60 | if err := bc.CopyTo(dst); err != nil { 61 | panic(fmt.Sprintf("bc.CopyTo => unexpected error: %v", err)) 62 | } 63 | } 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 | -------------------------------------------------------------------------------- /private/canvas/testcanvas/testcanvas.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 testcanvas provides helpers for tests that use the canvas package. 16 | package testcanvas 17 | 18 | import ( 19 | "fmt" 20 | "image" 21 | 22 | "github.com/mum4k/termdash/cell" 23 | "github.com/mum4k/termdash/private/canvas" 24 | "github.com/mum4k/termdash/private/canvas/buffer" 25 | "github.com/mum4k/termdash/private/faketerm" 26 | ) 27 | 28 | // MustNew returns a new canvas or panics. 29 | func MustNew(area image.Rectangle) *canvas.Canvas { 30 | cvs, err := canvas.New(area) 31 | if err != nil { 32 | panic(fmt.Sprintf("canvas.New => unexpected error: %v", err)) 33 | } 34 | return cvs 35 | } 36 | 37 | // MustApply applies the canvas on the terminal or panics. 38 | func MustApply(c *canvas.Canvas, t *faketerm.Terminal) { 39 | if err := c.Apply(t); err != nil { 40 | panic(fmt.Sprintf("canvas.Apply => unexpected error: %v", err)) 41 | } 42 | } 43 | 44 | // MustSetCell sets the cell value or panics. Returns the number of cells the 45 | // rune occupies, wide runes can occupy multiple cells when printed on the 46 | // terminal. See http://www.unicode.org/reports/tr11/. 47 | func MustSetCell(c *canvas.Canvas, p image.Point, r rune, opts ...cell.Option) int { 48 | cells, err := c.SetCell(p, r, opts...) 49 | if err != nil { 50 | panic(fmt.Sprintf("canvas.SetCell => unexpected error: %v", err)) 51 | } 52 | return cells 53 | } 54 | 55 | // MustSetAreaCells sets the cells in the area or panics. 56 | func MustSetAreaCells(c *canvas.Canvas, cellArea image.Rectangle, r rune, opts ...cell.Option) { 57 | if err := c.SetAreaCells(cellArea, r, opts...); err != nil { 58 | panic(fmt.Sprintf("canvas.SetAreaCells => unexpected error: %v", err)) 59 | } 60 | } 61 | 62 | // MustCell returns the cell or panics. 63 | func MustCell(c *canvas.Canvas, p image.Point) *buffer.Cell { 64 | cell, err := c.Cell(p) 65 | if err != nil { 66 | panic(fmt.Sprintf("canvas.Cell => unexpected error: %v", err)) 67 | } 68 | return cell 69 | } 70 | 71 | // MustCopyTo copies the content of the source canvas onto the destination 72 | // canvas or panics. 73 | func MustCopyTo(src, dst *canvas.Canvas) { 74 | if err := src.CopyTo(dst); err != nil { 75 | panic(fmt.Sprintf("canvas.CopyTo => unexpected error: %v", err)) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /private/draw/draw.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 draw provides functions that draw lines, shapes, etc on 2-D terminal 16 | // like canvases. 17 | package draw 18 | -------------------------------------------------------------------------------- /private/draw/line_style.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 draw 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/mum4k/termdash/linestyle" 21 | "github.com/mum4k/termdash/private/runewidth" 22 | ) 23 | 24 | // line_style.go contains the Unicode characters used for drawing lines of 25 | // different styles. 26 | 27 | // lineStyleChars maps the line styles to the corresponding component characters. 28 | // Source: http://en.wikipedia.org/wiki/Box-drawing_character. 29 | var lineStyleChars = map[linestyle.LineStyle]map[linePart]rune{ 30 | linestyle.Light: { 31 | hLine: '─', 32 | vLine: '│', 33 | topLeftCorner: '┌', 34 | topRightCorner: '┐', 35 | bottomLeftCorner: '└', 36 | bottomRightCorner: '┘', 37 | hAndUp: '┴', 38 | hAndDown: '┬', 39 | vAndLeft: '┤', 40 | vAndRight: '├', 41 | vAndH: '┼', 42 | }, 43 | linestyle.Double: { 44 | hLine: '═', 45 | vLine: '║', 46 | topLeftCorner: '╔', 47 | topRightCorner: '╗', 48 | bottomLeftCorner: '╚', 49 | bottomRightCorner: '╝', 50 | hAndUp: '╩', 51 | hAndDown: '╦', 52 | vAndLeft: '╣', 53 | vAndRight: '╠', 54 | vAndH: '╬', 55 | }, 56 | linestyle.Round: { 57 | hLine: '─', 58 | vLine: '│', 59 | topLeftCorner: '╭', 60 | topRightCorner: '╮', 61 | bottomLeftCorner: '╰', 62 | bottomRightCorner: '╯', 63 | hAndUp: '┴', 64 | hAndDown: '┬', 65 | vAndLeft: '┤', 66 | vAndRight: '├', 67 | vAndH: '┼', 68 | }, 69 | } 70 | 71 | // init verifies that all line parts are half-width runes (occupy only one 72 | // cell). 73 | func init() { 74 | for ls, parts := range lineStyleChars { 75 | for part, r := range parts { 76 | if got := runewidth.RuneWidth(r); got > 1 { 77 | panic(fmt.Errorf("line style %v line part %v is a rune %c with width %v, all parts must be half-width runes (width of one)", ls, part, r, got)) 78 | } 79 | } 80 | } 81 | } 82 | 83 | // lineParts returns the line component characters for the provided line style. 84 | func lineParts(ls linestyle.LineStyle) (map[linePart]rune, error) { 85 | parts, ok := lineStyleChars[ls] 86 | if !ok { 87 | return nil, fmt.Errorf("unsupported line style %d", ls) 88 | } 89 | return parts, nil 90 | } 91 | 92 | // linePart identifies individual line parts. 93 | type linePart int 94 | 95 | // String implements fmt.Stringer() 96 | func (lp linePart) String() string { 97 | if n, ok := linePartNames[lp]; ok { 98 | return n 99 | } 100 | return "linePartUnknown" 101 | } 102 | 103 | // linePartNames maps linePart values to human readable names. 104 | var linePartNames = map[linePart]string{ 105 | vLine: "linePartVLine", 106 | topLeftCorner: "linePartTopLeftCorner", 107 | topRightCorner: "linePartTopRightCorner", 108 | bottomLeftCorner: "linePartBottomLeftCorner", 109 | bottomRightCorner: "linePartBottomRightCorner", 110 | hAndUp: "linePartHAndUp", 111 | hAndDown: "linePartHAndDown", 112 | vAndLeft: "linePartVAndLeft", 113 | vAndRight: "linePartVAndRight", 114 | vAndH: "linePartVAndH", 115 | } 116 | 117 | const ( 118 | hLine linePart = iota 119 | vLine 120 | topLeftCorner 121 | topRightCorner 122 | bottomLeftCorner 123 | bottomRightCorner 124 | hAndUp 125 | hAndDown 126 | vAndLeft 127 | vAndRight 128 | vAndH 129 | ) 130 | -------------------------------------------------------------------------------- /private/draw/rectangle.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 draw 16 | 17 | // rectangle.go draws a rectangle. 18 | 19 | import ( 20 | "fmt" 21 | "image" 22 | 23 | "github.com/mum4k/termdash/cell" 24 | "github.com/mum4k/termdash/private/canvas" 25 | ) 26 | 27 | // RectangleOption is used to provide options to the Rectangle function. 28 | type RectangleOption interface { 29 | // set sets the provided option. 30 | set(*rectOptions) 31 | } 32 | 33 | // rectOptions stores the provided options. 34 | type rectOptions struct { 35 | cellOpts []cell.Option 36 | char rune 37 | } 38 | 39 | // rectOption implements RectangleOption. 40 | type rectOption func(rOpts *rectOptions) 41 | 42 | // set implements RectangleOption.set. 43 | func (ro rectOption) set(rOpts *rectOptions) { 44 | ro(rOpts) 45 | } 46 | 47 | // RectCellOpts sets options on the cells that create the rectangle. 48 | func RectCellOpts(opts ...cell.Option) RectangleOption { 49 | return rectOption(func(rOpts *rectOptions) { 50 | rOpts.cellOpts = append(rOpts.cellOpts, opts...) 51 | }) 52 | } 53 | 54 | // DefaultRectChar is the default value for the RectChar option. 55 | const DefaultRectChar = ' ' 56 | 57 | // RectChar sets the character used in each of the cells of the rectangle. 58 | func RectChar(c rune) RectangleOption { 59 | return rectOption(func(rOpts *rectOptions) { 60 | rOpts.char = c 61 | }) 62 | } 63 | 64 | // Rectangle draws a filled rectangle on the canvas. 65 | func Rectangle(c *canvas.Canvas, r image.Rectangle, opts ...RectangleOption) error { 66 | opt := &rectOptions{ 67 | char: DefaultRectChar, 68 | } 69 | for _, o := range opts { 70 | o.set(opt) 71 | } 72 | 73 | if ar := c.Area(); !r.In(ar) { 74 | return fmt.Errorf("the requested rectangle %v doesn't fit the canvas area %v", r, ar) 75 | } 76 | 77 | if r.Dx() < 1 || r.Dy() < 1 { 78 | return fmt.Errorf("the rectangle must be at least 1x1 cell, got %v", r) 79 | } 80 | 81 | for col := r.Min.X; col < r.Max.X; col++ { 82 | for row := r.Min.Y; row < r.Max.Y; row++ { 83 | cells, err := c.SetCell(image.Point{col, row}, opt.char, opt.cellOpts...) 84 | if err != nil { 85 | return err 86 | } 87 | if cells != 1 { 88 | return fmt.Errorf("invalid rectangle character %q, this character occupies %d cells, the implementation only supports half-width runes that occupy exactly one cell", opt.char, cells) 89 | } 90 | } 91 | } 92 | return nil 93 | } 94 | -------------------------------------------------------------------------------- /private/draw/rectangle_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 draw 16 | 17 | import ( 18 | "image" 19 | "testing" 20 | 21 | "github.com/mum4k/termdash/cell" 22 | "github.com/mum4k/termdash/private/canvas" 23 | "github.com/mum4k/termdash/private/canvas/testcanvas" 24 | "github.com/mum4k/termdash/private/faketerm" 25 | ) 26 | 27 | func TestRectangle(t *testing.T) { 28 | tests := []struct { 29 | desc string 30 | canvas image.Rectangle 31 | rect image.Rectangle 32 | opts []RectangleOption 33 | want func(size image.Point) *faketerm.Terminal 34 | wantErr bool 35 | }{ 36 | { 37 | desc: "draws a 1x1 rectangle", 38 | canvas: image.Rect(0, 0, 2, 2), 39 | rect: image.Rect(0, 0, 1, 1), 40 | opts: []RectangleOption{ 41 | RectChar('x'), 42 | }, 43 | want: func(size image.Point) *faketerm.Terminal { 44 | ft := faketerm.MustNew(size) 45 | c := testcanvas.MustNew(ft.Area()) 46 | 47 | testcanvas.MustSetCell(c, image.Point{0, 0}, 'x') 48 | testcanvas.MustApply(c, ft) 49 | return ft 50 | }, 51 | }, 52 | { 53 | desc: "fails when the rectangle character occupies multiple cells", 54 | canvas: image.Rect(0, 0, 2, 2), 55 | rect: image.Rect(0, 0, 1, 1), 56 | opts: []RectangleOption{ 57 | RectChar('界'), 58 | }, 59 | wantErr: true, 60 | }, 61 | { 62 | desc: "sets cell options", 63 | canvas: image.Rect(0, 0, 2, 2), 64 | rect: image.Rect(0, 0, 1, 1), 65 | opts: []RectangleOption{ 66 | RectChar('x'), 67 | RectCellOpts( 68 | cell.FgColor(cell.ColorBlue), 69 | cell.BgColor(cell.ColorRed), 70 | ), 71 | }, 72 | want: func(size image.Point) *faketerm.Terminal { 73 | ft := faketerm.MustNew(size) 74 | c := testcanvas.MustNew(ft.Area()) 75 | 76 | testcanvas.MustSetCell( 77 | c, 78 | image.Point{0, 0}, 79 | 'x', 80 | cell.FgColor(cell.ColorBlue), 81 | cell.BgColor(cell.ColorRed), 82 | ) 83 | testcanvas.MustApply(c, ft) 84 | return ft 85 | }, 86 | }, 87 | { 88 | desc: "draws a larger rectangle", 89 | canvas: image.Rect(0, 0, 10, 10), 90 | rect: image.Rect(0, 0, 3, 2), 91 | opts: []RectangleOption{ 92 | RectChar('o'), 93 | }, 94 | want: func(size image.Point) *faketerm.Terminal { 95 | ft := faketerm.MustNew(size) 96 | c := testcanvas.MustNew(ft.Area()) 97 | 98 | testcanvas.MustSetCell(c, image.Point{0, 0}, 'o') 99 | testcanvas.MustSetCell(c, image.Point{1, 0}, 'o') 100 | testcanvas.MustSetCell(c, image.Point{2, 0}, 'o') 101 | testcanvas.MustSetCell(c, image.Point{0, 1}, 'o') 102 | testcanvas.MustSetCell(c, image.Point{1, 1}, 'o') 103 | testcanvas.MustSetCell(c, image.Point{2, 1}, 'o') 104 | testcanvas.MustApply(c, ft) 105 | return ft 106 | }, 107 | }, 108 | { 109 | desc: "rectangle not in the corner of the canvas", 110 | canvas: image.Rect(0, 0, 10, 10), 111 | rect: image.Rect(2, 1, 4, 4), 112 | opts: []RectangleOption{ 113 | RectChar('o'), 114 | }, 115 | want: func(size image.Point) *faketerm.Terminal { 116 | ft := faketerm.MustNew(size) 117 | c := testcanvas.MustNew(ft.Area()) 118 | 119 | testcanvas.MustSetCell(c, image.Point{2, 1}, 'o') 120 | testcanvas.MustSetCell(c, image.Point{3, 1}, 'o') 121 | testcanvas.MustSetCell(c, image.Point{2, 2}, 'o') 122 | testcanvas.MustSetCell(c, image.Point{3, 2}, 'o') 123 | testcanvas.MustSetCell(c, image.Point{2, 3}, 'o') 124 | testcanvas.MustSetCell(c, image.Point{3, 3}, 'o') 125 | testcanvas.MustApply(c, ft) 126 | return ft 127 | }, 128 | }, 129 | } 130 | 131 | for _, tc := range tests { 132 | t.Run(tc.desc, func(t *testing.T) { 133 | c, err := canvas.New(tc.canvas) 134 | if err != nil { 135 | t.Fatalf("canvas.New => unexpected error: %v", err) 136 | } 137 | 138 | err = Rectangle(c, tc.rect, tc.opts...) 139 | if (err != nil) != tc.wantErr { 140 | t.Errorf("Rectangle => unexpected error: %v, wantErr: %v", err, tc.wantErr) 141 | } 142 | if err != nil { 143 | return 144 | } 145 | 146 | got, err := faketerm.New(c.Size()) 147 | if err != nil { 148 | t.Fatalf("faketerm.New => unexpected error: %v", err) 149 | } 150 | 151 | if err := c.Apply(got); err != nil { 152 | t.Fatalf("Apply => unexpected error: %v", err) 153 | } 154 | 155 | if diff := faketerm.Diff(tc.want(c.Size()), got); diff != "" { 156 | t.Errorf("Rectangle => %v", diff) 157 | } 158 | }) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /private/draw/testdraw/testdraw.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 testdraw provides helpers for tests that use the draw package. 16 | package testdraw 17 | 18 | import ( 19 | "fmt" 20 | "image" 21 | 22 | "github.com/mum4k/termdash/private/canvas" 23 | "github.com/mum4k/termdash/private/canvas/braille" 24 | "github.com/mum4k/termdash/private/draw" 25 | ) 26 | 27 | // MustBorder draws border on the canvas or panics. 28 | func MustBorder(c *canvas.Canvas, border image.Rectangle, opts ...draw.BorderOption) { 29 | if err := draw.Border(c, border, opts...); err != nil { 30 | panic(fmt.Sprintf("draw.Border => unexpected error: %v", err)) 31 | } 32 | } 33 | 34 | // MustText draws the text on the canvas or panics. 35 | func MustText(c *canvas.Canvas, text string, start image.Point, opts ...draw.TextOption) { 36 | if err := draw.Text(c, text, start, opts...); err != nil { 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)) 45 | } 46 | } 47 | 48 | // MustRectangle draws the rectangle on the canvas or panics. 49 | func MustRectangle(c *canvas.Canvas, r image.Rectangle, opts ...draw.RectangleOption) { 50 | if err := draw.Rectangle(c, r, opts...); err != nil { 51 | panic(fmt.Sprintf("draw.Rectangle => unexpected error: %v", err)) 52 | } 53 | } 54 | 55 | // MustHVLines draws the vertical / horizontal lines or panics. 56 | func MustHVLines(c *canvas.Canvas, lines []draw.HVLine, opts ...draw.HVLineOption) { 57 | if err := draw.HVLines(c, lines, opts...); err != nil { 58 | panic(fmt.Sprintf("draw.HVLines => unexpected error: %v", err)) 59 | } 60 | } 61 | 62 | // MustBrailleLine draws the braille line or panics. 63 | func MustBrailleLine(bc *braille.Canvas, start, end image.Point, opts ...draw.BrailleLineOption) { 64 | if err := draw.BrailleLine(bc, start, end, opts...); err != nil { 65 | panic(fmt.Sprintf("draw.BrailleLine => unexpected error: %v", err)) 66 | } 67 | } 68 | 69 | // MustBrailleCircle draws the braille circle or panics. 70 | func MustBrailleCircle(bc *braille.Canvas, mid image.Point, radius int, opts ...draw.BrailleCircleOption) { 71 | if err := draw.BrailleCircle(bc, mid, radius, opts...); err != nil { 72 | panic(fmt.Sprintf("draw.BrailleCircle => unexpected error: %v", err)) 73 | } 74 | } 75 | 76 | // MustResizeNeeded draws the character or panics. 77 | func MustResizeNeeded(cvs *canvas.Canvas) { 78 | if err := draw.ResizeNeeded(cvs); err != nil { 79 | panic(fmt.Sprintf("draw.ResizeNeeded => unexpected error: %v", err)) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /private/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/cell" 25 | "github.com/mum4k/termdash/private/canvas" 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 | // 79 | // H 80 | // e 81 | // l 82 | // l 83 | // o 84 | func VerticalText(c *canvas.Canvas, text string, start image.Point, opts ...VerticalTextOption) error { 85 | ar := c.Area() 86 | if !start.In(ar) { 87 | return fmt.Errorf("the requested start point %v falls outside of the provided canvas %v", start, ar) 88 | } 89 | 90 | opt := &verticalTextOptions{} 91 | for _, o := range opts { 92 | o.set(opt) 93 | } 94 | 95 | if opt.maxY < 0 || opt.maxY > ar.Max.Y { 96 | return fmt.Errorf("invalid VerticalTextMaxY(%v), must be a positive number that is <= canvas.width %v", opt.maxY, ar.Dy()) 97 | } 98 | 99 | var wantMaxY int 100 | if opt.maxY == 0 { 101 | wantMaxY = ar.Max.Y 102 | } else { 103 | wantMaxY = opt.maxY 104 | } 105 | 106 | maxCells := wantMaxY - start.Y 107 | trimmed, err := TrimText(text, maxCells, opt.overrunMode) 108 | if err != nil { 109 | return err 110 | } 111 | 112 | cur := start 113 | for _, r := range trimmed { 114 | cells, err := c.SetCell(cur, r, opt.cellOpts...) 115 | if err != nil { 116 | return err 117 | } 118 | cur = image.Point{cur.X, cur.Y + cells} 119 | } 120 | return nil 121 | } 122 | -------------------------------------------------------------------------------- /private/event/testevent/testevent.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 testevent provides utilities for tests that deal with concurrent 16 | // events. 17 | package testevent 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "time" 23 | ) 24 | 25 | // WaitFor waits until the provided function returns a nil error or the timeout. 26 | // If the function doesn't return a nil error before the timeout expires, 27 | // returns the last returned error. 28 | func WaitFor(timeout time.Duration, fn func() error) error { 29 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 30 | defer cancel() 31 | 32 | var err error 33 | for { 34 | tick := time.NewTimer(5 * time.Millisecond) 35 | select { 36 | case <-tick.C: 37 | if err = fn(); err != nil { 38 | continue 39 | } 40 | return nil 41 | 42 | case <-ctx.Done(): 43 | return fmt.Errorf("timeout expired, error: %v", err) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /private/faketerm/diff.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 faketerm 16 | 17 | // diff.go provides functions that highlight differences between fake terminals. 18 | 19 | import ( 20 | "fmt" 21 | "image" 22 | "reflect" 23 | "strings" 24 | 25 | "github.com/kylelemons/godebug/pretty" 26 | "github.com/mum4k/termdash/cell" 27 | ) 28 | 29 | // optDiff is used to display differences in cell options. 30 | type optDiff struct { 31 | // point indicates the cell with the differing options. 32 | point image.Point 33 | 34 | got *cell.Options 35 | want *cell.Options 36 | } 37 | 38 | // Diff compares the two terminals, returning an empty string if there is not 39 | // difference. If a difference is found, returns a human readable description 40 | // of the differences. 41 | func Diff(want, got *Terminal) string { 42 | if reflect.DeepEqual(want.BackBuffer(), got.BackBuffer()) { 43 | return "" 44 | } 45 | 46 | var b strings.Builder 47 | b.WriteString("found differences between the two fake terminals.\n") 48 | b.WriteString(" got:\n") 49 | b.WriteString(got.String()) 50 | b.WriteString(" want:\n") 51 | b.WriteString(want.String()) 52 | b.WriteString(" diff (unexpected cells highlighted with rune '࿃')\n") 53 | b.WriteString(" note - this excludes cell options:\n") 54 | 55 | size := got.Size() 56 | var optDiffs []*optDiff 57 | cellsDiffer := false 58 | for row := 0; row < size.Y; row++ { 59 | for col := 0; col < size.X; col++ { 60 | p := image.Point{col, row} 61 | partial, err := got.BackBuffer().IsPartial(p) 62 | if err != nil { 63 | panic(fmt.Errorf("unable to determine if point %v is a partial rune: %v", p, err)) 64 | } 65 | 66 | gotCell := got.BackBuffer()[col][row] 67 | wantCell := want.BackBuffer()[col][row] 68 | r := gotCell.Rune 69 | if r != wantCell.Rune { 70 | r = '࿃' 71 | cellsDiffer = true 72 | } else if r == 0 && !partial { 73 | r = ' ' 74 | } 75 | b.WriteRune(r) 76 | 77 | if !reflect.DeepEqual(gotCell.Opts, wantCell.Opts) { 78 | optDiffs = append(optDiffs, &optDiff{ 79 | point: image.Point{col, row}, 80 | got: gotCell.Opts, 81 | want: wantCell.Opts, 82 | }) 83 | } 84 | } 85 | b.WriteRune('\n') 86 | } 87 | 88 | if len(optDiffs) > 0 { 89 | b.WriteString(" Found differences in options on some of the cells:\n") 90 | for _, od := range optDiffs { 91 | if diff := pretty.Compare(od.want, od.got); diff != "" { 92 | b.WriteString(fmt.Sprintf("cell %v, diff (-want +got):\n%s\n", od.point, diff)) 93 | } 94 | } 95 | } 96 | 97 | if cellsDiffer { 98 | b.WriteString(" Found differences in some of the cell runes:\n") 99 | for row := 0; row < size.Y; row++ { 100 | for col := 0; col < size.X; col++ { 101 | got := got.BackBuffer()[col][row].Rune 102 | want := want.BackBuffer()[col][row].Rune 103 | if got == want { 104 | continue 105 | } 106 | b.WriteString(fmt.Sprintf(" cell(%v, %v) => got '%c' (rune %d), want '%c' (rune %d)\n", col, row, got, got, want, want)) 107 | } 108 | } 109 | } 110 | return b.String() 111 | } 112 | -------------------------------------------------------------------------------- /private/faketerm/diff_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 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 faketerm 16 | 17 | import ( 18 | "image" 19 | "testing" 20 | 21 | "github.com/mum4k/termdash/cell" 22 | ) 23 | 24 | func TestDiff(t *testing.T) { 25 | tests := []struct { 26 | desc string 27 | term1 *Terminal 28 | term2 *Terminal 29 | wantDiff bool 30 | }{ 31 | { 32 | desc: "no diff on equal terminals", 33 | term1: func() *Terminal { 34 | t := MustNew(image.Point{2, 2}) 35 | t.SetCell(image.Point{0, 0}, 'a') 36 | return t 37 | }(), 38 | term2: func() *Terminal { 39 | t := MustNew(image.Point{2, 2}) 40 | t.SetCell(image.Point{0, 0}, 'a') 41 | return t 42 | }(), 43 | wantDiff: false, 44 | }, 45 | { 46 | desc: "reports diff on when cell runes differ", 47 | term1: func() *Terminal { 48 | t := MustNew(image.Point{2, 2}) 49 | t.SetCell(image.Point{0, 0}, 'a') 50 | return t 51 | }(), 52 | term2: func() *Terminal { 53 | t := MustNew(image.Point{2, 2}) 54 | t.SetCell(image.Point{1, 1}, 'a') 55 | return t 56 | }(), 57 | wantDiff: true, 58 | }, 59 | { 60 | desc: "reports diff on when cell options differ", 61 | term1: func() *Terminal { 62 | t := MustNew(image.Point{2, 2}) 63 | t.SetCell(image.Point{0, 0}, 'a', cell.Bold()) 64 | return t 65 | }(), 66 | term2: func() *Terminal { 67 | t := MustNew(image.Point{2, 2}) 68 | t.SetCell(image.Point{0, 0}, 'a') 69 | return t 70 | }(), 71 | wantDiff: true, 72 | }, 73 | } 74 | 75 | for _, tc := range tests { 76 | t.Run(tc.desc, func(t *testing.T) { 77 | gotDiff := Diff(tc.term1, tc.term2) 78 | if (gotDiff != "") != tc.wantDiff { 79 | t.Errorf("Diff -> unexpected diff while wantDiff:%v, the diff:\n%s", tc.wantDiff, gotDiff) 80 | } 81 | }) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /private/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 | // Option is used to provide options. 22 | type Option interface { 23 | // set sets the provided option. 24 | set(*options) 25 | } 26 | 27 | // options stores the provided options. 28 | type options struct { 29 | runeWidths map[rune]int 30 | } 31 | 32 | // newOptions create a new instance of options. 33 | func newOptions() *options { 34 | return &options{ 35 | runeWidths: map[rune]int{}, 36 | } 37 | } 38 | 39 | // option implements Option. 40 | type option func(*options) 41 | 42 | // set implements Option.set. 43 | func (o option) set(opts *options) { 44 | o(opts) 45 | } 46 | 47 | // CountAsWidth overrides the default behavior, counting the specified runes as 48 | // the specified width. Can be provided multiple times to specify an override 49 | // for multiple runes. 50 | func CountAsWidth(r rune, width int) Option { 51 | return option(func(opts *options) { 52 | opts.runeWidths[r] = width 53 | }) 54 | } 55 | 56 | // RuneWidth returns the number of cells needed to draw r. 57 | // Background in http://www.unicode.org/reports/tr11/. 58 | // 59 | // Treats runes used internally by termdash as single-cell (half-width) runes 60 | // regardless of the locale. I.e. runes that are used to draw lines, boxes, 61 | // indicate resize or text trimming was needed and runes used by the braille 62 | // canvas. 63 | // 64 | // This should be safe, since even in locales where these runes have ambiguous 65 | // width, we still place all the character content around them so they should 66 | // have be half-width. 67 | func RuneWidth(r rune, opts ...Option) int { 68 | o := newOptions() 69 | for _, opt := range opts { 70 | opt.set(o) 71 | } 72 | 73 | if w, ok := o.runeWidths[r]; ok { 74 | return w 75 | } 76 | 77 | if inTable(r, exceptions) { 78 | return 1 79 | } 80 | return runewidth.RuneWidth(r) 81 | } 82 | 83 | // StringWidth is like RuneWidth, but returns the number of cells occupied by 84 | // all the runes in the string. 85 | func StringWidth(s string, opts ...Option) int { 86 | var width int 87 | for _, r := range []rune(s) { 88 | width += RuneWidth(r, opts...) 89 | } 90 | return width 91 | } 92 | 93 | // inTable determines if the rune falls within the table. 94 | // Copied from github.com/mattn/go-runewidth/blob/master/runewidth.go. 95 | func inTable(r rune, t table) bool { 96 | // func (t table) IncludesRune(r rune) bool { 97 | if r < t[0].first { 98 | return false 99 | } 100 | 101 | bot := 0 102 | top := len(t) - 1 103 | for top >= bot { 104 | mid := (bot + top) >> 1 105 | 106 | switch { 107 | case t[mid].last < r: 108 | bot = mid + 1 109 | case t[mid].first > r: 110 | top = mid - 1 111 | default: 112 | return true 113 | } 114 | } 115 | 116 | return false 117 | } 118 | 119 | type interval struct { 120 | first rune 121 | last rune 122 | } 123 | 124 | type table []interval 125 | 126 | // exceptions runes defined here are always considered to be half-width even if 127 | // they might be ambiguous in some contexts. 128 | var exceptions = table{ 129 | // Characters used by termdash to indicate text trim or scroll. 130 | {0x2026, 0x2026}, 131 | {0x21c4, 0x21c4}, 132 | {0x21e7, 0x21e7}, 133 | {0x21e9, 0x21e9}, 134 | 135 | // Box drawing, used as line-styles. 136 | // https://en.wikipedia.org/wiki/Box-drawing_character 137 | {0x2500, 0x257F}, 138 | 139 | // Block elements used as sparks. 140 | // https://en.wikipedia.org/wiki/Box-drawing_character 141 | {0x2580, 0x258F}, 142 | } 143 | -------------------------------------------------------------------------------- /private/scripts/autogen_licences.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | # 3 | # Copyright 2018 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | BIN_DIR="${HOME}/bin" 18 | INSTALL_DIR="${BIN_DIR}/autogen" 19 | AUTOGEN="${INSTALL_DIR}/autogen" 20 | 21 | if [ "$#" -eq 0 ]; then 22 | echo "Usage $0 [WRITE]" 23 | echo 24 | echo -n "Starts searching at for all Go files and adds licences " 25 | echo "by editing them in place." 26 | echo "Doesn't make any changes unless WRITE is the second argument." 27 | exit 1 28 | fi 29 | 30 | DIRECTORY="$1" 31 | 32 | WRITE="" 33 | if [ "$#" -ge 2 ]; then 34 | WRITE="$2" 35 | fi 36 | 37 | if [ ! -d "${BIN_DIR}" ]; then 38 | echo "Directory ${BIN_DIR} doesn't exist." 39 | exit 1 40 | fi 41 | 42 | 43 | 44 | if [ ! -d "${INSTALL_DIR}" ]; then 45 | git clone https://github.com/mbrukman/autogen.git "${BIN_DIR}/autogen" 46 | if [ $? -ne 0 ]; then 47 | echo "Failed to run git clone." 48 | exit 1 49 | fi 50 | fi 51 | 52 | if [ "${WRITE}" == "WRITE" ]; then 53 | DRY_RUN="" 54 | else 55 | DRY_RUN="echo " 56 | fi 57 | 58 | ADD_LICENCE="${DRY_RUN}${AUTOGEN} -i --no-top-level-comment" 59 | FIND_FILES="find ${DIRECTORY} -type f -name \*.go" 60 | LICENCE="Licensed under the Apache License" 61 | 62 | MISSING=0 63 | 64 | for FILE in `eval ${FIND_FILES}`; do 65 | if ! grep -q "${LICENCE}" "${FILE}"; then 66 | MISSING=1 67 | eval "${ADD_LICENCE} ${FILE}" 68 | fi 69 | done 70 | 71 | if [[ ! -z "$DRY_RUN" ]] && [ $MISSING -eq 1 ]; then 72 | echo -e "\nFound files with missing licences. To fix, run the commands above." 73 | echo "Or just execute:" 74 | echo "$0 . WRITE" 75 | fi 76 | -------------------------------------------------------------------------------- /private/segdisp/dotseg/attributes.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package dotseg 16 | 17 | // attributes.go calculates attributes needed when determining placement of 18 | // segments. 19 | 20 | import ( 21 | "fmt" 22 | "image" 23 | "math" 24 | 25 | "github.com/mum4k/termdash/align" 26 | "github.com/mum4k/termdash/private/alignfor" 27 | "github.com/mum4k/termdash/private/area" 28 | "github.com/mum4k/termdash/private/segdisp" 29 | "github.com/mum4k/termdash/private/segdisp/sixteen" 30 | ) 31 | 32 | // attributes contains attributes needed to draw the segment display. 33 | // Refer to doc/segment_placement.svg for a visual aid and explanation of the 34 | // usage of the square roots. 35 | type attributes struct { 36 | // bcAr is the area the attributes were created for. 37 | bcAr image.Rectangle 38 | 39 | // segSize is the width of a vertical or height of a horizontal segment. 40 | segSize int 41 | 42 | // sixteen are attributes of a 16-segment display when placed on the same 43 | // area. 44 | sixteen *sixteen.Attributes 45 | } 46 | 47 | // newAttributes calculates attributes needed to place the segments for the 48 | // provided pixel area. 49 | func newAttributes(bcAr image.Rectangle) *attributes { 50 | segSize := segdisp.SegmentSize(bcAr) 51 | return &attributes{ 52 | bcAr: bcAr, 53 | segSize: segSize, 54 | sixteen: sixteen.NewAttributes(bcAr), 55 | } 56 | } 57 | 58 | // segArea returns the area for the specified segment. 59 | func (a *attributes) segArea(seg Segment) (image.Rectangle, error) { 60 | // Dots have double width of normal segments to fill more space in the 61 | // segment display. 62 | segSize := a.segSize * 2 63 | 64 | // An area representing the dot which gets aligned and moved into position 65 | // below. 66 | dotAr := image.Rect( 67 | a.bcAr.Min.X, 68 | a.bcAr.Min.Y, 69 | a.bcAr.Min.X+segSize, 70 | a.bcAr.Min.Y+segSize, 71 | ) 72 | mid, err := alignfor.Rectangle(a.bcAr, dotAr, align.HorizontalCenter, align.VerticalMiddle) 73 | if err != nil { 74 | return image.ZR, err 75 | } 76 | 77 | // moveBySize is the multiplier of segment size to determine by how many 78 | // pixels to move D1 and D2 up and down from the center. 79 | const moveBySize = 1.5 80 | moveBy := int(math.Round(moveBySize * float64(segSize))) 81 | switch seg { 82 | case D1: 83 | moved, err := area.MoveUp(mid, moveBy) 84 | if err != nil { 85 | return image.ZR, err 86 | } 87 | return moved, nil 88 | 89 | case D2: 90 | moved, err := area.MoveDown(mid, moveBy) 91 | if err != nil { 92 | return image.ZR, err 93 | } 94 | return moved, nil 95 | 96 | case D3: 97 | // Align at the middle of the bottom. 98 | bot, err := alignfor.Rectangle(a.bcAr, dotAr, align.HorizontalCenter, align.VerticalBottom) 99 | if err != nil { 100 | return image.ZR, err 101 | } 102 | 103 | // Shift up to where the sixteen segment actually places its bottom 104 | // segments. 105 | diff := bot.Min.Y - a.sixteen.VertBotY 106 | // Shift further up by one segment size, since the dots have double width. 107 | diff += a.segSize 108 | moved, err := area.MoveUp(bot, diff) 109 | if err != nil { 110 | return image.ZR, err 111 | } 112 | return moved, nil 113 | 114 | default: 115 | return image.ZR, fmt.Errorf("cannot calculate area for %v(%d)", seg, seg) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /private/segdisp/dotseg/attributes_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package dotseg 16 | 17 | import ( 18 | "image" 19 | "testing" 20 | 21 | "github.com/kylelemons/godebug/pretty" 22 | ) 23 | 24 | func TestAttributes(t *testing.T) { 25 | tests := []struct { 26 | desc string 27 | brailleAr image.Rectangle 28 | seg Segment 29 | want image.Rectangle 30 | wantErr bool 31 | }{ 32 | { 33 | desc: "fails on unsupported segment", 34 | brailleAr: image.Rect(0, 0, 1, 1), 35 | seg: Segment(-1), 36 | wantErr: true, 37 | }, 38 | } 39 | 40 | for _, tc := range tests { 41 | t.Run(tc.desc, func(t *testing.T) { 42 | attr := newAttributes(tc.brailleAr) 43 | got, err := attr.segArea(tc.seg) 44 | if (err != nil) != tc.wantErr { 45 | t.Errorf("segArea => unexpected error: %v, wantErr: %v", err, tc.wantErr) 46 | } 47 | if err != nil { 48 | return 49 | } 50 | 51 | if diff := pretty.Compare(tc.want, got); diff != "" { 52 | t.Errorf("segArea => unexpected diff (-want, +got):\n%s", diff) 53 | } 54 | }) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /private/segdisp/dotseg/testdotseg/testdotseg.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package testdotseg provides helpers for tests that use the dotseg package. 16 | package testdotseg 17 | 18 | import ( 19 | "fmt" 20 | 21 | "github.com/mum4k/termdash/private/canvas" 22 | "github.com/mum4k/termdash/private/segdisp/dotseg" 23 | ) 24 | 25 | // MustSetCharacter sets the character on the display or panics. 26 | func MustSetCharacter(d *dotseg.Display, c rune) { 27 | if err := d.SetCharacter(c); err != nil { 28 | panic(fmt.Errorf("dotseg.Display.SetCharacter => unexpected error: %v", err)) 29 | } 30 | } 31 | 32 | // MustDraw draws the display onto the canvas or panics. 33 | func MustDraw(d *dotseg.Display, cvs *canvas.Canvas, opts ...dotseg.Option) { 34 | if err := d.Draw(cvs, opts...); err != nil { 35 | panic(fmt.Errorf("dotseg.Display.Draw => unexpected error: %v", err)) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /private/segdisp/segdisp.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package segdisp provides utilities used by all segment display types. 16 | package segdisp 17 | 18 | import ( 19 | "fmt" 20 | "image" 21 | "math" 22 | 23 | "github.com/mum4k/termdash/private/area" 24 | "github.com/mum4k/termdash/private/canvas" 25 | "github.com/mum4k/termdash/private/canvas/braille" 26 | ) 27 | 28 | // Minimum valid size of a cell canvas in order to draw a segment display. 29 | const ( 30 | // MinCols is the smallest valid amount of columns in a cell area. 31 | MinCols = 6 32 | // MinRowPixels is the smallest valid amount of rows in a cell area. 33 | MinRows = 5 34 | ) 35 | 36 | // aspectRatio is the desired aspect ratio of a single segment display. 37 | var aspectRatio = image.Point{3, 5} 38 | 39 | // Required when given an area of cells, returns either an area of the same 40 | // size or a smaller area that is required to draw one segment display (i.e. 41 | // one character). 42 | // Returns a smaller area when the provided area didn't have the required 43 | // aspect ratio. 44 | // Returns an error if the area is too small to draw a segment display, i.e. 45 | // smaller than MinCols x MinRows. 46 | func Required(cellArea image.Rectangle) (image.Rectangle, error) { 47 | if cols, rows := cellArea.Dx(), cellArea.Dy(); cols < MinCols || rows < MinRows { 48 | return image.ZR, fmt.Errorf("cell area %v is too small to draw the segment display, has %dx%d cells, need at least %dx%d cells", 49 | cellArea, cols, rows, MinCols, MinRows) 50 | } 51 | 52 | bcAr := image.Rect(cellArea.Min.X, cellArea.Min.Y, cellArea.Max.X*braille.ColMult, cellArea.Max.Y*braille.RowMult) 53 | bcArAdj := area.WithRatio(bcAr, aspectRatio) 54 | 55 | needCols := int(math.Ceil(float64(bcArAdj.Dx()) / braille.ColMult)) 56 | needRows := int(math.Ceil(float64(bcArAdj.Dy()) / braille.RowMult)) 57 | needAr := image.Rect(cellArea.Min.X, cellArea.Min.Y, cellArea.Min.X+needCols, cellArea.Min.Y+needRows) 58 | return needAr, nil 59 | } 60 | 61 | // ToBraille converts the canvas into a braille canvas and returns a pixel area 62 | // with aspect ratio adjusted for the segment display. 63 | func ToBraille(cvs *canvas.Canvas) (*braille.Canvas, image.Rectangle, error) { 64 | ar, err := Required(cvs.Area()) 65 | if err != nil { 66 | return nil, image.ZR, fmt.Errorf("Required => %v", err) 67 | } 68 | 69 | bc, err := braille.New(ar) 70 | if err != nil { 71 | return nil, image.ZR, fmt.Errorf("braille.New => %v", err) 72 | } 73 | return bc, area.WithRatio(bc.Area(), aspectRatio), nil 74 | } 75 | 76 | // SegmentSize given an area for the display segment determines the size of 77 | // individual segments, i.e. the width of a vertical or the height of a 78 | // horizontal segment. 79 | func SegmentSize(ar image.Rectangle) int { 80 | // widthPerc is the relative width of a segment to the width of the canvas. 81 | const widthPerc = 9 82 | s := int(math.Round(float64(ar.Dx()) * widthPerc / 100)) 83 | if s > 3 && s%2 == 0 { 84 | // Segments with odd number of pixels in their width/height look 85 | // better, since the spike at the top of their slopes has only one 86 | // pixel. 87 | s++ 88 | } 89 | return s 90 | } 91 | -------------------------------------------------------------------------------- /private/segdisp/segdisp_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package segdisp 16 | 17 | import ( 18 | "image" 19 | "testing" 20 | 21 | "github.com/kylelemons/godebug/pretty" 22 | "github.com/mum4k/termdash/private/canvas" 23 | "github.com/mum4k/termdash/private/canvas/braille" 24 | "github.com/mum4k/termdash/private/canvas/braille/testbraille" 25 | ) 26 | 27 | func TestRequired(t *testing.T) { 28 | tests := []struct { 29 | desc string 30 | cellArea image.Rectangle 31 | want image.Rectangle 32 | wantErr bool 33 | }{ 34 | { 35 | desc: "fails when area isn't wide enough", 36 | cellArea: image.Rect(0, 0, MinCols-1, MinRows), 37 | wantErr: true, 38 | }, 39 | { 40 | desc: "fails when area isn't tall enough", 41 | cellArea: image.Rect(0, 0, MinCols, MinRows-1), 42 | wantErr: true, 43 | }, 44 | { 45 | desc: "returns same area when no adjustment needed", 46 | cellArea: image.Rect(0, 0, MinCols, MinRows), 47 | want: image.Rect(0, 0, MinCols, MinRows), 48 | }, 49 | { 50 | desc: "adjusts width to aspect ratio", 51 | cellArea: image.Rect(0, 0, MinCols+100, MinRows), 52 | want: image.Rect(0, 0, MinCols, MinRows), 53 | }, 54 | { 55 | desc: "adjusts height to aspect ratio", 56 | cellArea: image.Rect(0, 0, MinCols, MinRows+100), 57 | want: image.Rect(0, 0, MinCols, MinRows), 58 | }, 59 | { 60 | desc: "adjusts larger area to aspect ratio", 61 | cellArea: image.Rect(0, 0, MinCols*2, MinRows*4), 62 | want: image.Rect(0, 0, 12, 10), 63 | }, 64 | } 65 | 66 | for _, tc := range tests { 67 | t.Run(tc.desc, func(t *testing.T) { 68 | got, err := Required(tc.cellArea) 69 | if (err != nil) != tc.wantErr { 70 | t.Errorf("Required => unexpected error: %v, wantErr: %v", err, tc.wantErr) 71 | } 72 | if err != nil { 73 | return 74 | } 75 | 76 | if diff := pretty.Compare(tc.want, got); diff != "" { 77 | t.Errorf("Required => unexpected diff (-want, +got):\n%s", diff) 78 | } 79 | }) 80 | } 81 | } 82 | 83 | func TestToBraille(t *testing.T) { 84 | tests := []struct { 85 | desc string 86 | cellArea image.Rectangle 87 | wantBC *braille.Canvas 88 | wantAr image.Rectangle 89 | wantErr bool 90 | }{ 91 | { 92 | desc: "fails when area isn't wide enough", 93 | cellArea: image.Rect(0, 0, MinCols-1, MinRows), 94 | wantErr: true, 95 | }, 96 | { 97 | desc: "canvas creates braille with the desired aspect ratio", 98 | cellArea: image.Rect(0, 0, MinCols, MinRows), 99 | wantBC: testbraille.MustNew(image.Rect(0, 0, MinCols, MinRows)), 100 | wantAr: image.Rect(0, 0, MinCols*braille.ColMult, MinRows*braille.RowMult), 101 | }, 102 | } 103 | 104 | for _, tc := range tests { 105 | t.Run(tc.desc, func(t *testing.T) { 106 | cvs, err := canvas.New(tc.cellArea) 107 | if err != nil { 108 | t.Fatalf("canvas.New => unexpected error: %v", err) 109 | } 110 | 111 | gotBC, gotAr, err := ToBraille(cvs) 112 | if (err != nil) != tc.wantErr { 113 | t.Errorf("ToBraille => unexpected error: %v, wantErr: %v", err, tc.wantErr) 114 | } 115 | if err != nil { 116 | return 117 | } 118 | 119 | if diff := pretty.Compare(tc.wantBC, gotBC); diff != "" { 120 | t.Errorf("ToBraille => unexpected braille canvas, diff (-want, +got):\n%s", diff) 121 | } 122 | if diff := pretty.Compare(tc.wantAr, gotAr); diff != "" { 123 | t.Errorf("ToBraille => unexpected area, diff (-want, +got):\n%s", diff) 124 | } 125 | }) 126 | } 127 | } 128 | 129 | func TestSegmentSize(t *testing.T) { 130 | tests := []struct { 131 | desc string 132 | ar image.Rectangle 133 | want int 134 | }{ 135 | { 136 | desc: "zero area", 137 | ar: image.ZR, 138 | want: 0, 139 | }, 140 | { 141 | desc: "smallest segment size", 142 | ar: image.Rect(0, 0, 15, 1), 143 | want: 1, 144 | }, 145 | { 146 | desc: "allows even size of two", 147 | ar: image.Rect(0, 0, 22, 1), 148 | want: 2, 149 | }, 150 | { 151 | desc: "lands on even width, corrected to odd", 152 | ar: image.Rect(0, 0, 44, 1), 153 | want: 5, 154 | }, 155 | { 156 | desc: "lands on odd width", 157 | ar: image.Rect(0, 0, 55, 1), 158 | want: 5, 159 | }, 160 | } 161 | 162 | for _, tc := range tests { 163 | t.Run(tc.desc, func(t *testing.T) { 164 | got := SegmentSize(tc.ar) 165 | if got != tc.want { 166 | t.Errorf("SegmentSize => %d, want %d", got, tc.want) 167 | } 168 | }) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /private/segdisp/segment/testsegment/testsegment.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 testsegment provides helpers for tests that use the segment package. 16 | package testsegment 17 | 18 | import ( 19 | "fmt" 20 | "image" 21 | 22 | "github.com/mum4k/termdash/private/canvas/braille" 23 | "github.com/mum4k/termdash/private/segdisp/segment" 24 | ) 25 | 26 | // MustHV draws the segment or panics. 27 | func MustHV(bc *braille.Canvas, ar image.Rectangle, st segment.Type, opts ...segment.Option) { 28 | if err := segment.HV(bc, ar, st, opts...); err != nil { 29 | panic(fmt.Sprintf("segment.HV => unexpected error: %v", err)) 30 | } 31 | } 32 | 33 | // MustDiagonal draws the segment or panics. 34 | func MustDiagonal(bc *braille.Canvas, ar image.Rectangle, width int, dt segment.DiagonalType, opts ...segment.DiagonalOption) { 35 | if err := segment.Diagonal(bc, ar, width, dt, opts...); err != nil { 36 | panic(fmt.Sprintf("segment.Diagonal => unexpected error: %v", err)) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /private/segdisp/sixteen/doc/16-Segment-ASCII-All.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mum4k/termdash/102df20a882547528c83047688595acd2a82c7ef/private/segdisp/sixteen/doc/16-Segment-ASCII-All.jpg -------------------------------------------------------------------------------- /private/segdisp/sixteen/doc/segment_placement.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mum4k/termdash/102df20a882547528c83047688595acd2a82c7ef/private/segdisp/sixteen/doc/segment_placement.graffle -------------------------------------------------------------------------------- /private/segdisp/sixteen/testsixteen/testsixteen.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 testsixteen provides helpers for tests that use the sixteen package. 16 | package testsixteen 17 | 18 | import ( 19 | "fmt" 20 | 21 | "github.com/mum4k/termdash/private/canvas" 22 | "github.com/mum4k/termdash/private/segdisp/sixteen" 23 | ) 24 | 25 | // MustSetCharacter sets the character on the display or panics. 26 | func MustSetCharacter(d *sixteen.Display, c rune) { 27 | if err := d.SetCharacter(c); err != nil { 28 | panic(fmt.Errorf("sixteen.Display.SetCharacter => unexpected error: %v", err)) 29 | } 30 | } 31 | 32 | // MustDraw draws the display onto the canvas or panics. 33 | func MustDraw(d *sixteen.Display, cvs *canvas.Canvas, opts ...sixteen.Option) { 34 | if err := d.Draw(cvs, opts...); err != nil { 35 | panic(fmt.Errorf("sixteen.Display.Draw => unexpected error: %v", err)) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /terminal/tcell/cell_options.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 tcell 16 | 17 | import ( 18 | tcell "github.com/gdamore/tcell/v2" 19 | "github.com/mum4k/termdash/cell" 20 | "github.com/mum4k/termdash/terminal/terminalapi" 21 | ) 22 | 23 | // cellColor converts termdash cell color to the tcell format. 24 | func cellColor(c cell.Color) tcell.Color { 25 | if c == cell.ColorDefault { 26 | return tcell.ColorDefault 27 | } 28 | // Subtract one, because cell.ColorBlack has value one instead of zero. 29 | // Zero is used for cell.ColorDefault instead. 30 | return tcell.Color(c-1) + tcell.ColorValid 31 | } 32 | 33 | // colorToMode adjusts the color to the color mode. 34 | func colorToMode(c cell.Color, colorMode terminalapi.ColorMode) cell.Color { 35 | if c == cell.ColorDefault { 36 | return c 37 | } 38 | switch colorMode { 39 | case terminalapi.ColorModeNormal: 40 | c %= 16 + 1 // Add one for cell.ColorDefault. 41 | case terminalapi.ColorMode256: 42 | c %= 256 + 1 // Add one for cell.ColorDefault. 43 | case terminalapi.ColorMode216: 44 | if c <= 216 { // Add one for cell.ColorDefault. 45 | return c + 16 46 | } 47 | c = c%216 + 16 48 | case terminalapi.ColorModeGrayscale: 49 | if c <= 24 { // Add one for cell.ColorDefault. 50 | return c + 232 51 | } 52 | c = c%24 + 232 53 | default: 54 | c = cell.ColorDefault 55 | } 56 | return c 57 | } 58 | 59 | // cellOptsToStyle converts termdash cell color to the tcell format. 60 | func cellOptsToStyle(opts *cell.Options, colorMode terminalapi.ColorMode) tcell.Style { 61 | st := tcell.StyleDefault 62 | 63 | fg := cellColor(colorToMode(opts.FgColor, colorMode)) 64 | bg := cellColor(colorToMode(opts.BgColor, colorMode)) 65 | 66 | st = st.Foreground(fg). 67 | Background(bg). 68 | Bold(opts.Bold). 69 | Italic(opts.Italic). 70 | Underline(opts.Underline). 71 | StrikeThrough(opts.Strikethrough). 72 | Reverse(opts.Inverse). 73 | Blink(opts.Blink). 74 | Dim(opts.Dim) 75 | return st 76 | } 77 | -------------------------------------------------------------------------------- /terminal/tcell/tcell_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 tcell 16 | 17 | import ( 18 | "testing" 19 | 20 | tcell "github.com/gdamore/tcell/v2" 21 | "github.com/kylelemons/godebug/pretty" 22 | "github.com/mum4k/termdash/cell" 23 | "github.com/mum4k/termdash/terminal/terminalapi" 24 | ) 25 | 26 | func TestNewTerminalColorMode(t *testing.T) { 27 | tests := []struct { 28 | desc string 29 | opts []Option 30 | want *Terminal 31 | }{ 32 | { 33 | desc: "default options", 34 | want: &Terminal{ 35 | colorMode: terminalapi.ColorMode256, 36 | }, 37 | }, 38 | { 39 | desc: "sets color mode", 40 | opts: []Option{ 41 | ColorMode(terminalapi.ColorModeNormal), 42 | }, 43 | want: &Terminal{ 44 | colorMode: terminalapi.ColorModeNormal, 45 | }, 46 | }, 47 | } 48 | 49 | tcellNewScreen = func() (tcell.Screen, error) { return nil, nil } 50 | for _, tc := range tests { 51 | t.Run(tc.desc, func(t *testing.T) { 52 | got, err := newTerminal(tc.opts...) 53 | if err != nil { 54 | t.Errorf("newTerminal => unexpected error:\n%v", err) 55 | return 56 | } 57 | 58 | // Ignore these fields. 59 | got.screen = nil 60 | got.events = nil 61 | got.done = nil 62 | got.clearStyle = nil 63 | 64 | if diff := pretty.Compare(tc.want, got); diff != "" { 65 | t.Errorf("newTerminal => unexpected diff (-want, +got):\n%s", diff) 66 | } 67 | }) 68 | } 69 | } 70 | 71 | func TestNewTerminalClearStyle(t *testing.T) { 72 | tests := []struct { 73 | desc string 74 | opts []Option 75 | want *Terminal 76 | }{ 77 | { 78 | desc: "default options", 79 | want: &Terminal{ 80 | colorMode: terminalapi.ColorMode256, 81 | clearStyle: &cell.Options{ 82 | FgColor: cell.ColorDefault, 83 | BgColor: cell.ColorDefault, 84 | }, 85 | }, 86 | }, 87 | { 88 | desc: "sets clear style", 89 | opts: []Option{ 90 | ClearStyle(cell.ColorRed, cell.ColorBlue), 91 | }, 92 | want: &Terminal{ 93 | colorMode: terminalapi.ColorMode256, 94 | clearStyle: &cell.Options{ 95 | FgColor: cell.ColorRed, 96 | BgColor: cell.ColorBlue, 97 | }, 98 | }, 99 | }, 100 | } 101 | 102 | tcellNewScreen = func() (tcell.Screen, error) { return nil, nil } 103 | for _, tc := range tests { 104 | t.Run(tc.desc, func(t *testing.T) { 105 | got, err := newTerminal(tc.opts...) 106 | if err != nil { 107 | t.Errorf("newTerminal => unexpected error:\n%v", err) 108 | return 109 | } 110 | 111 | // Ignore these fields. 112 | got.screen = nil 113 | got.events = nil 114 | got.done = nil 115 | 116 | if diff := pretty.Compare(tc.want, got); diff != "" { 117 | t.Errorf("newTerminal => unexpected diff (-want, +got):\n%s", diff) 118 | } 119 | }) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /terminal/termbox/cell_options.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 termbox 16 | 17 | // cell_options.go converts termdash cell options to the termbox format. 18 | 19 | import ( 20 | "errors" 21 | 22 | "github.com/mum4k/termdash/cell" 23 | tbx "github.com/nsf/termbox-go" 24 | ) 25 | 26 | // cellColor converts termdash cell color to the termbox format. 27 | func cellColor(c cell.Color) tbx.Attribute { 28 | // Special cases for backward compatibility after we have aligned the 29 | // definition of the first 16 colors with Xterm and tcell. 30 | // This ensures that users that run with termbox-go don't experience any 31 | // change in colors. 32 | switch c { 33 | case cell.ColorRed: 34 | return tbx.Attribute(cell.ColorMaroon) 35 | case cell.ColorYellow: 36 | return tbx.Attribute(cell.ColorOlive) 37 | case cell.ColorBlue: 38 | return tbx.Attribute(cell.ColorNavy) 39 | case cell.ColorWhite: 40 | return tbx.Attribute(cell.ColorSilver) 41 | default: 42 | return tbx.Attribute(c) 43 | } 44 | } 45 | 46 | // cellOptsToFg converts the cell options to the termbox foreground attribute. 47 | func cellOptsToFg(opts *cell.Options) (tbx.Attribute, error) { 48 | a := cellColor(opts.FgColor) 49 | if opts.Bold { 50 | a |= tbx.AttrBold 51 | } 52 | // Termbox doesn't have an italics attribute 53 | if opts.Italic { 54 | return 0, errors.New("Termbox: Unsupported attribute: Italic") 55 | } 56 | if opts.Underline { 57 | a |= tbx.AttrUnderline 58 | } 59 | // Termbox doesn't have a strikethrough attribute 60 | if opts.Strikethrough { 61 | return 0, errors.New("Termbox: Unsupported attribute: Strikethrough") 62 | } 63 | if opts.Inverse { 64 | a |= tbx.AttrReverse 65 | } 66 | // Termbox doesn't have a blink attribute 67 | if opts.Blink { 68 | return 0, errors.New("Termbox: Unsupported attribute: Blink") 69 | } 70 | 71 | if opts.Dim { 72 | return 0, errors.New("Termbox: Unsupported attribute: Dim") 73 | } 74 | 75 | return a, nil 76 | } 77 | 78 | // cellOptsToBg converts the cell options to the termbox background attribute. 79 | func cellOptsToBg(opts *cell.Options) tbx.Attribute { 80 | return cellColor(opts.BgColor) 81 | } 82 | -------------------------------------------------------------------------------- /terminal/termbox/cell_options_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 termbox 16 | 17 | import ( 18 | "fmt" 19 | "testing" 20 | 21 | "github.com/mum4k/termdash/cell" 22 | tbx "github.com/nsf/termbox-go" 23 | ) 24 | 25 | func TestCellColor(t *testing.T) { 26 | tests := []struct { 27 | color cell.Color 28 | want tbx.Attribute 29 | }{ 30 | {cell.ColorDefault, tbx.ColorDefault}, 31 | {cell.ColorBlack, tbx.ColorBlack}, 32 | {cell.ColorRed, tbx.ColorRed}, 33 | {cell.ColorGreen, tbx.ColorGreen}, 34 | {cell.ColorYellow, tbx.ColorYellow}, 35 | {cell.ColorBlue, tbx.ColorBlue}, 36 | {cell.ColorMagenta, tbx.ColorMagenta}, 37 | {cell.ColorCyan, tbx.ColorCyan}, 38 | {cell.ColorWhite, tbx.ColorWhite}, 39 | {cell.Color(42), tbx.Attribute(42)}, 40 | } 41 | 42 | for _, tc := range tests { 43 | t.Run(tc.color.String(), func(t *testing.T) { 44 | got := cellColor(tc.color) 45 | if got != tc.want { 46 | t.Errorf("cellColor(%v) => got %v, want %v", tc.color, got, tc.want) 47 | } 48 | 49 | }) 50 | } 51 | } 52 | 53 | func TestCellFontModifier(t *testing.T) { 54 | tests := []struct { 55 | opt cell.Options 56 | want tbx.Attribute 57 | wantErr bool 58 | }{ 59 | {cell.Options{Bold: true}, tbx.AttrBold, false}, 60 | {cell.Options{Underline: true}, tbx.AttrUnderline, false}, 61 | {cell.Options{Italic: true}, 0, true}, 62 | {cell.Options{Strikethrough: true}, 0, true}, 63 | {cell.Options{Inverse: true}, tbx.AttrReverse, false}, 64 | {cell.Options{Blink: true}, 0, true}, 65 | {cell.Options{Dim: true}, 0, true}, 66 | } 67 | 68 | for _, tc := range tests { 69 | t.Run(fmt.Sprintf("%v", tc.opt), func(t *testing.T) { 70 | got, err := cellOptsToFg(&tc.opt) 71 | if (err != nil) != tc.wantErr { 72 | t.Errorf("cellOptsToFg(%v) => unexpected error: %v, wantErr: %v", tc.opt, err, tc.wantErr) 73 | } 74 | if err != nil { 75 | return 76 | } 77 | if got != tc.want { 78 | t.Errorf("cellOptsToFg(%v) => got %v, want %v", tc.opt, got, tc.want) 79 | } 80 | }) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /terminal/termbox/color_mode.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 termbox 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/mum4k/termdash/terminal/terminalapi" 21 | tbx "github.com/nsf/termbox-go" 22 | ) 23 | 24 | // colorMode converts termdash color modes to the termbox format. 25 | func colorMode(cm terminalapi.ColorMode) (tbx.OutputMode, error) { 26 | switch cm { 27 | case terminalapi.ColorModeNormal: 28 | return tbx.OutputNormal, nil 29 | case terminalapi.ColorMode256: 30 | return tbx.Output256, nil 31 | case terminalapi.ColorMode216: 32 | return tbx.Output216, nil 33 | case terminalapi.ColorModeGrayscale: 34 | return tbx.OutputGrayscale, nil 35 | default: 36 | return -1, fmt.Errorf("don't know how to convert color mode %v to the termbox format", cm) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /terminal/termbox/termbox_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 termbox 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/kylelemons/godebug/pretty" 21 | "github.com/mum4k/termdash/terminal/terminalapi" 22 | ) 23 | 24 | func TestNewTerminal(t *testing.T) { 25 | tests := []struct { 26 | desc string 27 | opts []Option 28 | want *Terminal 29 | }{ 30 | { 31 | desc: "default options", 32 | want: &Terminal{ 33 | colorMode: terminalapi.ColorMode256, 34 | }, 35 | }, 36 | { 37 | desc: "sets color mode", 38 | opts: []Option{ 39 | ColorMode(terminalapi.ColorModeNormal), 40 | }, 41 | want: &Terminal{ 42 | colorMode: terminalapi.ColorModeNormal, 43 | }, 44 | }, 45 | } 46 | 47 | for _, tc := range tests { 48 | t.Run(tc.desc, func(t *testing.T) { 49 | got := newTerminal(tc.opts...) 50 | 51 | // Ignore these fields. 52 | got.events = nil 53 | got.done = nil 54 | 55 | if diff := pretty.Compare(tc.want, got); diff != "" { 56 | t.Errorf("newTerminal => unexpected diff (-want, +got):\n%s", diff) 57 | } 58 | }) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /terminal/terminalapi/color_mode.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 terminalapi 16 | 17 | // color_mode.go defines the terminal color modes. 18 | 19 | // ColorMode represents a color mode of a terminal. 20 | type ColorMode int 21 | 22 | // String implements fmt.Stringer() 23 | func (cm ColorMode) String() string { 24 | if n, ok := colorModeNames[cm]; ok { 25 | return n 26 | } 27 | return "ColorModeUnknown" 28 | } 29 | 30 | // colorModeNames maps ColorMode values to human readable names. 31 | var colorModeNames = map[ColorMode]string{ 32 | ColorModeNormal: "ColorModeNormal", 33 | ColorMode256: "ColorMode256", 34 | ColorMode216: "ColorMode216", 35 | ColorModeGrayscale: "ColorModeGrayscale", 36 | } 37 | 38 | // Supported color modes. 39 | const ( 40 | // ColorModeNormal supports 16 Xterm colors. 41 | // These are defined as constants in the cell package. 42 | ColorModeNormal ColorMode = iota 43 | 44 | // ColorMode256 enables using any of the 256 terminal colors. 45 | // 0-7: the 8 Xterm colors accessible in ColorModeNormal. 46 | // 8-15: the 8 "bright" Xterm colors. 47 | // 16-231: the 216 different terminal colors. 48 | // 232-255: the 24 different shades of grey. 49 | ColorMode256 50 | 51 | // ColorMode216 supports only the third range of the ColorMode256, i.e the 52 | // 216 different terminal colors. However in this mode the colors are zero 53 | // based, so the caller doesn't need to provide an offset. 54 | ColorMode216 55 | 56 | // ColorModeGrayscale supports only the fourth range of the ColorMode256, 57 | // i.e the 24 different shades of grey. However in this mode the colors are 58 | // zero based, so the caller doesn't need to provide an offset. 59 | ColorModeGrayscale 60 | ) 61 | -------------------------------------------------------------------------------- /terminal/terminalapi/event.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 terminalapi 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "image" 21 | 22 | "github.com/mum4k/termdash/keyboard" 23 | "github.com/mum4k/termdash/mouse" 24 | ) 25 | 26 | // event.go defines events that can be received through the terminal API. 27 | 28 | // Event represents an input event. 29 | type Event interface { 30 | isEvent() 31 | } 32 | 33 | // Keyboard is the event used when a key is pressed. 34 | // Implements terminalapi.Event. 35 | type Keyboard struct { 36 | // Key is the pressed key. 37 | Key keyboard.Key 38 | } 39 | 40 | func (*Keyboard) isEvent() {} 41 | 42 | // String implements fmt.Stringer. 43 | func (k Keyboard) String() string { 44 | return fmt.Sprintf("Keyboard{Key: %v}", k.Key) 45 | } 46 | 47 | // Resize is the event used when the terminal was resized. 48 | // Implements terminalapi.Event. 49 | type Resize struct { 50 | // Size is the new size of the terminal. 51 | Size image.Point 52 | } 53 | 54 | func (*Resize) isEvent() {} 55 | 56 | // String implements fmt.Stringer. 57 | func (r Resize) String() string { 58 | return fmt.Sprintf("Resize{Size: %v}", r.Size) 59 | } 60 | 61 | // Mouse is the event used when the mouse is moved or a mouse button is 62 | // pressed. 63 | // Implements terminalapi.Event. 64 | type Mouse struct { 65 | // Position of the mouse on the terminal. 66 | Position image.Point 67 | // Button identifies the pressed button if any. 68 | Button mouse.Button 69 | } 70 | 71 | func (*Mouse) isEvent() {} 72 | 73 | // String implements fmt.Stringer. 74 | func (m Mouse) String() string { 75 | return fmt.Sprintf("Mouse{Position: %v, Button: %v}", m.Position, m.Button) 76 | } 77 | 78 | // Error is an event indicating an error while processing input. 79 | type Error string 80 | 81 | // NewError returns a new Error event. 82 | func NewError(e string) *Error { 83 | err := Error(e) 84 | return &err 85 | } 86 | 87 | // NewErrorf returns a new Error event, arguments are similar to fmt.Sprintf. 88 | func NewErrorf(format string, args ...interface{}) *Error { 89 | err := Error(fmt.Sprintf(format, args...)) 90 | return &err 91 | } 92 | 93 | func (*Error) isEvent() {} 94 | 95 | // Error returns the error that occurred. 96 | func (e *Error) Error() error { 97 | if e == nil || *e == "" { 98 | return nil 99 | } 100 | return errors.New(string(*e)) 101 | } 102 | 103 | // String implements fmt.Stringer. 104 | func (e Error) String() string { 105 | return string(e) 106 | } 107 | -------------------------------------------------------------------------------- /terminal/terminalapi/terminalapi.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 terminalapi defines the API of all terminal implementations. 16 | package terminalapi 17 | 18 | import ( 19 | "context" 20 | "image" 21 | 22 | "github.com/mum4k/termdash/cell" 23 | ) 24 | 25 | // Terminal abstracts an implementation of a 2-D terminal. 26 | // A terminal consists of a number of cells. 27 | type Terminal interface { 28 | // Size returns the terminal width and height in cells. 29 | Size() image.Point 30 | 31 | // Clear clears the content of the internal back buffer, resetting all 32 | // cells to their default content and attributes. Sets the provided options 33 | // on all the cell. 34 | Clear(opts ...cell.Option) error 35 | // Flush flushes the internal back buffer to the terminal. 36 | Flush() error 37 | 38 | // SetCursor sets the position of the cursor. 39 | SetCursor(p image.Point) 40 | // HideCursos hides the cursor. 41 | HideCursor() 42 | 43 | // SetCell sets the value of the specified cell to the provided rune. 44 | // Use the options to specify which attributes to modify, if an attribute 45 | // option isn't specified, the attribute retains its previous value. 46 | SetCell(p image.Point, r rune, opts ...cell.Option) error 47 | 48 | // Event waits for the next event and returns it. 49 | // This call blocks until the next event or cancellation of the context. 50 | // Returns nil when the context gets canceled. 51 | Event(ctx context.Context) Event 52 | 53 | // Close closes the underlying terminal implementation and should be called when 54 | // the terminal isn't required anymore to return the screen to a sane state. 55 | Close() 56 | } 57 | -------------------------------------------------------------------------------- /widgets/barchart/barchartdemo/barchartdemo.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 | // Binary barchartdemo displays a couple of BarChart widgets. 16 | // Exist when 'q' is pressed. 17 | package main 18 | 19 | import ( 20 | "context" 21 | "math/rand" 22 | "time" 23 | 24 | "github.com/mum4k/termdash" 25 | "github.com/mum4k/termdash/cell" 26 | "github.com/mum4k/termdash/container" 27 | "github.com/mum4k/termdash/linestyle" 28 | "github.com/mum4k/termdash/terminal/tcell" 29 | "github.com/mum4k/termdash/terminal/terminalapi" 30 | "github.com/mum4k/termdash/widgets/barchart" 31 | ) 32 | 33 | // playBarChart continuously changes the displayed values on the bar chart once every delay. 34 | // Exits when the context expires. 35 | func playBarChart(ctx context.Context, bc *barchart.BarChart, delay time.Duration) { 36 | const max = 100 37 | 38 | ticker := time.NewTicker(delay) 39 | defer ticker.Stop() 40 | for { 41 | select { 42 | case <-ticker.C: 43 | var values []int 44 | for i := 0; i < bc.ValueCapacity(); i++ { 45 | values = append(values, int(rand.Int31n(max+1))) 46 | } 47 | 48 | if err := bc.Values(values, max); err != nil { 49 | panic(err) 50 | } 51 | 52 | case <-ctx.Done(): 53 | return 54 | } 55 | } 56 | } 57 | 58 | func main() { 59 | t, err := tcell.New() 60 | if err != nil { 61 | panic(err) 62 | } 63 | defer t.Close() 64 | 65 | ctx, cancel := context.WithCancel(context.Background()) 66 | bc, err := barchart.New( 67 | barchart.BarColors([]cell.Color{ 68 | cell.ColorBlue, 69 | cell.ColorRed, 70 | cell.ColorYellow, 71 | cell.ColorBlue, 72 | cell.ColorGreen, 73 | cell.ColorRed, 74 | }), 75 | barchart.ValueColors([]cell.Color{ 76 | cell.ColorRed, 77 | cell.ColorYellow, 78 | cell.ColorNumber(33), 79 | cell.ColorGreen, 80 | cell.ColorRed, 81 | cell.ColorNumber(33), 82 | }), 83 | barchart.ShowValues(), 84 | barchart.BarWidth(8), 85 | barchart.Labels([]string{ 86 | "CPU1", 87 | "", 88 | "CPU3", 89 | }), 90 | ) 91 | if err != nil { 92 | panic(err) 93 | } 94 | go playBarChart(ctx, bc, 1*time.Second) 95 | 96 | c, err := container.New( 97 | t, 98 | container.Border(linestyle.Light), 99 | container.BorderTitle("PRESS Q TO QUIT"), 100 | container.PlaceWidget(bc), 101 | ) 102 | if err != nil { 103 | panic(err) 104 | } 105 | 106 | quitter := func(k *terminalapi.Keyboard) { 107 | if k.Key == 'q' || k.Key == 'Q' { 108 | cancel() 109 | } 110 | } 111 | 112 | if err := termdash.Run(ctx, t, c, termdash.KeyboardSubscriber(quitter)); err != nil { 113 | panic(err) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /widgets/button/buttondemo/buttondemo.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 | // Binary buttondemo shows the functionality of a button widget. 16 | package main 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | "time" 22 | 23 | "github.com/mum4k/termdash" 24 | "github.com/mum4k/termdash/align" 25 | "github.com/mum4k/termdash/cell" 26 | "github.com/mum4k/termdash/container" 27 | "github.com/mum4k/termdash/linestyle" 28 | "github.com/mum4k/termdash/terminal/tcell" 29 | "github.com/mum4k/termdash/terminal/terminalapi" 30 | "github.com/mum4k/termdash/widgets/button" 31 | "github.com/mum4k/termdash/widgets/segmentdisplay" 32 | ) 33 | 34 | func main() { 35 | t, err := tcell.New() 36 | if err != nil { 37 | panic(err) 38 | } 39 | defer t.Close() 40 | 41 | ctx, cancel := context.WithCancel(context.Background()) 42 | 43 | val := 0 44 | display, err := segmentdisplay.New() 45 | if err != nil { 46 | panic(err) 47 | } 48 | if err := display.Write([]*segmentdisplay.TextChunk{ 49 | segmentdisplay.NewChunk(fmt.Sprintf("%d", val)), 50 | }); err != nil { 51 | panic(err) 52 | } 53 | 54 | addB, err := button.New("(a)dd", func() error { 55 | val++ 56 | return display.Write([]*segmentdisplay.TextChunk{ 57 | segmentdisplay.NewChunk(fmt.Sprintf("%d", val)), 58 | }) 59 | }, 60 | button.GlobalKey('a'), 61 | button.WidthFor("(s)ubtract"), 62 | ) 63 | if err != nil { 64 | panic(err) 65 | } 66 | 67 | subB, err := button.New("(s)ubtract", func() error { 68 | val-- 69 | return display.Write([]*segmentdisplay.TextChunk{ 70 | segmentdisplay.NewChunk(fmt.Sprintf("%d", val)), 71 | }) 72 | }, 73 | button.FillColor(cell.ColorNumber(220)), 74 | button.GlobalKey('s'), 75 | ) 76 | if err != nil { 77 | panic(err) 78 | } 79 | 80 | c, err := container.New( 81 | t, 82 | container.Border(linestyle.Light), 83 | container.BorderTitle("PRESS Q TO QUIT"), 84 | container.SplitHorizontal( 85 | container.Top( 86 | container.PlaceWidget(display), 87 | ), 88 | container.Bottom( 89 | container.SplitVertical( 90 | container.Left( 91 | container.PlaceWidget(addB), 92 | container.AlignHorizontal(align.HorizontalRight), 93 | ), 94 | container.Right( 95 | container.PlaceWidget(subB), 96 | container.AlignHorizontal(align.HorizontalLeft), 97 | ), 98 | ), 99 | ), 100 | container.SplitPercent(60), 101 | ), 102 | ) 103 | if err != nil { 104 | panic(err) 105 | } 106 | 107 | quitter := func(k *terminalapi.Keyboard) { 108 | if k.Key == 'q' || k.Key == 'Q' { 109 | cancel() 110 | } 111 | } 112 | 113 | if err := termdash.Run(ctx, t, c, termdash.KeyboardSubscriber(quitter), termdash.RedrawInterval(100*time.Millisecond)); err != nil { 114 | panic(err) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /widgets/button/text_options.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 button 16 | 17 | // text_options.go contains options used for the text displayed by the button. 18 | 19 | import "github.com/mum4k/termdash/cell" 20 | 21 | // TextOption is used to provide options to NewChunk(). 22 | type TextOption interface { 23 | // set sets the provided option. 24 | set(*textOptions) 25 | } 26 | 27 | // textOptions stores the provided options. 28 | type textOptions struct { 29 | cellOpts []cell.Option 30 | focusedCellOpts []cell.Option 31 | pressedCellOpts []cell.Option 32 | } 33 | 34 | // setDefaultFgColor configures a default color for text if one isn't specified 35 | // in the text options. 36 | func (to *textOptions) setDefaultFgColor(c cell.Color) { 37 | to.cellOpts = append( 38 | []cell.Option{cell.FgColor(c)}, 39 | to.cellOpts..., 40 | ) 41 | } 42 | 43 | // newTextOptions returns new textOptions instance. 44 | func newTextOptions(tOpts ...TextOption) *textOptions { 45 | to := &textOptions{} 46 | for _, o := range tOpts { 47 | o.set(to) 48 | } 49 | return to 50 | } 51 | 52 | // textOption implements TextOption. 53 | type textOption func(*textOptions) 54 | 55 | // set implements TextOption.set. 56 | func (to textOption) set(tOpts *textOptions) { 57 | to(tOpts) 58 | } 59 | 60 | // TextCellOpts sets options on the cells that contain the button text. 61 | // If not specified, all cells will just have their foreground color set to the 62 | // value of TextColor(). 63 | func TextCellOpts(opts ...cell.Option) TextOption { 64 | return textOption(func(tOpts *textOptions) { 65 | tOpts.cellOpts = opts 66 | }) 67 | } 68 | 69 | // FocusedTextCellOpts sets options on the cells that contain the button text 70 | // when the widget's container is focused. 71 | // If not specified, TextCellOpts will be used instead. 72 | func FocusedTextCellOpts(opts ...cell.Option) TextOption { 73 | return textOption(func(tOpts *textOptions) { 74 | tOpts.focusedCellOpts = opts 75 | }) 76 | } 77 | 78 | // PressedTextCellOpts sets options on the cells that contain the button text 79 | // when it is pressed. 80 | // If not specified, TextCellOpts will be used instead. 81 | func PressedTextCellOpts(opts ...cell.Option) TextOption { 82 | return textOption(func(tOpts *textOptions) { 83 | tOpts.pressedCellOpts = opts 84 | }) 85 | } 86 | -------------------------------------------------------------------------------- /widgets/donut/circle.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 donut 16 | 17 | // circle.go assists in calculation of points and angles on a circle. 18 | 19 | import ( 20 | "image" 21 | "math" 22 | 23 | "github.com/mum4k/termdash/private/canvas/braille" 24 | ) 25 | 26 | // startEndAngles given progress indicators and the desired start angle and 27 | // direction, returns the starting and the ending angle of the partial circle 28 | // that represents this progress. 29 | func startEndAngles(current, total, startAngle, direction int) (start, end int) { 30 | const fullCircle = 360 31 | if total == 0 { 32 | return startAngle, startAngle 33 | } 34 | 35 | mult := float64(current) / float64(total) 36 | angleSize := math.Round(float64(360) * mult) 37 | 38 | if angleSize == fullCircle { 39 | return 0, fullCircle 40 | } 41 | end = startAngle + int(math.Round(float64(direction)*angleSize)) 42 | 43 | if end < 0 { 44 | end += fullCircle 45 | if startAngle == 0 { 46 | startAngle = fullCircle 47 | } 48 | return end, startAngle 49 | } 50 | 51 | if end < startAngle { 52 | return end, startAngle 53 | } 54 | if end > fullCircle { 55 | end = end % fullCircle 56 | } 57 | return startAngle, end 58 | } 59 | 60 | // midAndRadius given an area of a braille canvas, determines the mid point in 61 | // pixels and radius to draw the largest circle that fits. 62 | // The circle's mid point is always positioned on the {0,1} pixel in the chosen 63 | // cell so that any text inside of it can be visually centered. 64 | func midAndRadius(ar image.Rectangle) (image.Point, int) { 65 | mid := image.Point{ar.Dx() / 2, ar.Dy() / 2} 66 | if mid.X%2 != 0 { 67 | mid.X-- 68 | } 69 | switch mid.Y % 4 { 70 | case 0: 71 | mid.Y++ 72 | case 1: 73 | case 2: 74 | mid.Y-- 75 | case 3: 76 | mid.Y -= 2 77 | 78 | } 79 | 80 | // Calculate radius based on the smaller axis. 81 | var radius int 82 | if ar.Dx() < ar.Dy() { 83 | if mid.X < ar.Dx()/2 { 84 | radius = mid.X 85 | } else { 86 | radius = ar.Dx() - mid.X - 1 87 | } 88 | } else { 89 | if mid.Y < ar.Dy()/2 { 90 | radius = mid.Y 91 | } else { 92 | radius = ar.Dy() - mid.Y - 1 93 | } 94 | } 95 | return mid, radius 96 | } 97 | 98 | // availableCells given a radius returns the number of cells that are available 99 | // within the circle and the coordinates of the first cell. 100 | // These coordinates are for a normal (non-braille) canvas. 101 | // That is the cells that do not contain any of the circle points. This is 102 | // important since normal characters and braille characters cannot share the 103 | // same cell. 104 | func availableCells(mid image.Point, radius int) (int, image.Point) { 105 | if radius < 3 { 106 | return 0, image.Point{0, 0} 107 | } 108 | // Pixels available for the text only. 109 | // Subtract one for the circle itself. 110 | pixels := radius*2 - 1 111 | 112 | startPixel := image.Point{mid.X - pixels/2, mid.Y} 113 | startCell := image.Point{ 114 | startPixel.X / braille.ColMult, 115 | mid.Y / braille.RowMult, 116 | } 117 | return pixels / braille.ColMult, startCell 118 | } 119 | -------------------------------------------------------------------------------- /widgets/donut/donutdemo/donutdemo.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 | // Binary donutdemo displays a couple of Donut widgets. 16 | // Exist when 'q' is pressed. 17 | package main 18 | 19 | import ( 20 | "context" 21 | "time" 22 | 23 | "github.com/mum4k/termdash" 24 | "github.com/mum4k/termdash/cell" 25 | "github.com/mum4k/termdash/container" 26 | "github.com/mum4k/termdash/linestyle" 27 | "github.com/mum4k/termdash/terminal/tcell" 28 | "github.com/mum4k/termdash/terminal/terminalapi" 29 | "github.com/mum4k/termdash/widgets/donut" 30 | ) 31 | 32 | // playType indicates how to play a donut. 33 | type playType int 34 | 35 | const ( 36 | playTypePercent playType = iota 37 | playTypeAbsolute 38 | ) 39 | 40 | // playDonut continuously changes the displayed percent value on the donut by the 41 | // step once every delay. Exits when the context expires. 42 | func playDonut(ctx context.Context, d *donut.Donut, start, step int, delay time.Duration, pt playType) { 43 | progress := start 44 | mult := 1 45 | 46 | ticker := time.NewTicker(delay) 47 | defer ticker.Stop() 48 | for { 49 | select { 50 | case <-ticker.C: 51 | switch pt { 52 | case playTypePercent: 53 | if err := d.Percent(progress); err != nil { 54 | panic(err) 55 | } 56 | case playTypeAbsolute: 57 | if err := d.Absolute(progress, 100); err != nil { 58 | panic(err) 59 | } 60 | } 61 | 62 | progress += step * mult 63 | if progress > 100 || 100-progress < step { 64 | progress = 100 65 | } else if progress < 0 || progress < step { 66 | progress = 0 67 | } 68 | 69 | if progress == 100 { 70 | mult = -1 71 | } else if progress == 0 { 72 | mult = 1 73 | } 74 | 75 | case <-ctx.Done(): 76 | return 77 | } 78 | } 79 | } 80 | 81 | func main() { 82 | t, err := tcell.New() 83 | if err != nil { 84 | panic(err) 85 | } 86 | defer t.Close() 87 | 88 | ctx, cancel := context.WithCancel(context.Background()) 89 | green, err := donut.New( 90 | donut.CellOpts(cell.FgColor(cell.ColorGreen)), 91 | donut.Label("text label", cell.FgColor(cell.ColorGreen)), 92 | ) 93 | if err != nil { 94 | panic(err) 95 | } 96 | go playDonut(ctx, green, 0, 1, 250*time.Millisecond, playTypePercent) 97 | 98 | blue, err := donut.New(donut.CellOpts(cell.FgColor(cell.ColorNumber(33)))) 99 | if err != nil { 100 | panic(err) 101 | } 102 | go playDonut(ctx, blue, 25, 1, 500*time.Millisecond, playTypePercent) 103 | 104 | yellow, err := donut.New(donut.CellOpts(cell.FgColor(cell.ColorYellow))) 105 | if err != nil { 106 | panic(err) 107 | } 108 | go playDonut(ctx, yellow, 50, 1, 1*time.Second, playTypeAbsolute) 109 | 110 | red, err := donut.New(donut.CellOpts(cell.FgColor(cell.ColorRed))) 111 | if err != nil { 112 | panic(err) 113 | } 114 | go playDonut(ctx, red, 75, 1, 2*time.Second, playTypeAbsolute) 115 | 116 | c, err := container.New( 117 | t, 118 | container.Border(linestyle.Light), 119 | container.BorderTitle("PRESS Q TO QUIT"), 120 | container.SplitVertical( 121 | container.Left( 122 | container.SplitVertical( 123 | container.Left(container.PlaceWidget(green)), 124 | container.Right(container.PlaceWidget(blue)), 125 | ), 126 | ), 127 | container.Right( 128 | container.SplitVertical( 129 | container.Left(container.PlaceWidget(yellow)), 130 | container.Right(container.PlaceWidget(red)), 131 | ), 132 | ), 133 | ), 134 | ) 135 | if err != nil { 136 | panic(err) 137 | } 138 | 139 | quitter := func(k *terminalapi.Keyboard) { 140 | if k.Key == 'q' || k.Key == 'Q' { 141 | cancel() 142 | } 143 | } 144 | 145 | if err := termdash.Run(ctx, t, c, termdash.KeyboardSubscriber(quitter), termdash.RedrawInterval(1*time.Second)); err != nil { 146 | panic(err) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /widgets/heatmap/heatmap_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 heatmap 16 | -------------------------------------------------------------------------------- /widgets/heatmap/heatmapdemo/heatmapdemo.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 | // Binary heatmapdemo displays a heatmap widget. 16 | // Exist when 'q' is pressed. 17 | package main 18 | 19 | import ( 20 | "context" 21 | "github.com/mum4k/termdash" 22 | "github.com/mum4k/termdash/container" 23 | "github.com/mum4k/termdash/linestyle" 24 | "github.com/mum4k/termdash/terminal/tcell" 25 | "github.com/mum4k/termdash/terminal/terminalapi" 26 | "github.com/mum4k/termdash/widgets/heatmap" 27 | ) 28 | 29 | func main() { 30 | t, err := tcell.New() 31 | if err != nil { 32 | panic(err) 33 | } 34 | defer t.Close() 35 | 36 | hp, err := heatmap.New() 37 | if err != nil { 38 | panic(err) 39 | } 40 | 41 | // TODO: set heatmap's data 42 | 43 | c, err := container.New( 44 | t, 45 | container.Border(linestyle.Light), 46 | container.BorderTitle("PRESS Q TO QUIT"), 47 | container.PlaceWidget(hp), 48 | ) 49 | if err != nil { 50 | panic(err) 51 | } 52 | 53 | ctx, cancel := context.WithCancel(context.Background()) 54 | 55 | quitter := func(k *terminalapi.Keyboard) { 56 | if k.Key == 'q' || k.Key == 'Q' { 57 | cancel() 58 | } 59 | } 60 | 61 | if err := termdash.Run(ctx, t, c, termdash.KeyboardSubscriber(quitter)); err != nil { 62 | panic(err) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /widgets/heatmap/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 | -------------------------------------------------------------------------------- /widgets/heatmap/internal/axes/axes.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 axes calculates the required layout and draws the X and Y axes of a heat map. 16 | package axes 17 | 18 | import ( 19 | "errors" 20 | "image" 21 | 22 | "github.com/mum4k/termdash/private/runewidth" 23 | ) 24 | 25 | // axisWidth is width of an axis. 26 | const axisWidth = 1 27 | 28 | // YDetails contain information about the Y axis 29 | // that will NOT be drawn onto the canvas, but will take up space. 30 | type YDetails struct { 31 | // Width in character cells of the Y axis and its character labels. 32 | Width int 33 | 34 | // Start is the point where the Y axis starts. 35 | // The Y coordinate of Start is less than the Y coordinate of End. 36 | Start image.Point 37 | 38 | // End is the point where the Y axis ends. 39 | End image.Point 40 | 41 | // Labels are the labels for values on the Y axis in an increasing order. 42 | Labels []*Label 43 | } 44 | 45 | // RequiredWidth calculates the minimum width required 46 | // in order to draw the Y axis and its labels. 47 | // The parameter ls is the longest string in yLabels. 48 | func RequiredWidth(ls string) int { 49 | return runewidth.StringWidth(ls) + axisWidth 50 | } 51 | 52 | // NewYDetails retrieves details about the Y axis required 53 | // to draw it on a canvas of the provided area. 54 | func NewYDetails(labels []string) (*YDetails, error) { 55 | return nil, errors.New("not implemented") 56 | } 57 | 58 | // LongestString returns the length of the longest string in the string array. 59 | func LongestString(strings []string) int { 60 | var widest int 61 | for _, s := range strings { 62 | if l := runewidth.StringWidth(s); l > widest { 63 | widest = l 64 | } 65 | } 66 | return widest 67 | } 68 | 69 | // XDetails contain information about the X axis 70 | // that will NOT be drawn onto the canvas. 71 | type XDetails struct { 72 | // Start is the point where the X axis starts. 73 | // Both coordinates of Start are less than End. 74 | Start image.Point 75 | // End is the point where the X axis ends. 76 | End image.Point 77 | 78 | // Labels are the labels for values on the X axis in an increasing order. 79 | Labels []*Label 80 | } 81 | 82 | // NewXDetails retrieves details about the X axis required to draw it on a canvas 83 | // of the provided area. 84 | // The yEnd is the point where the Y axis ends. 85 | func NewXDetails(cvsAr image.Rectangle, yEnd image.Point, labels []string, cellWidth int) (*XDetails, error) { 86 | return nil, errors.New("not implemented") 87 | } 88 | -------------------------------------------------------------------------------- /widgets/heatmap/internal/axes/axes_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 axes 16 | -------------------------------------------------------------------------------- /widgets/heatmap/internal/axes/label.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 axes 16 | 17 | // label.go contains code that calculates the positions of labels on the axes. 18 | 19 | import ( 20 | "errors" 21 | "image" 22 | ) 23 | 24 | // Label is one text label on an axis. 25 | type Label struct { 26 | // Label content. 27 | Text string 28 | 29 | // Position of the label within the canvas. 30 | Pos image.Point 31 | } 32 | 33 | // yLabels returns labels that should be placed next to the cells. 34 | // The labelWidth is the width of the area from the left-most side of the 35 | // canvas until the Y axis (not including the Y axis). This is the area where 36 | // the labels will be placed and aligned. 37 | // Labels are returned with Y coordinates in ascending order. 38 | // Y coordinates grow down. 39 | func yLabels(graphHeight, labelWidth int, labels []string) ([]*Label, error) { 40 | return nil, errors.New("not implemented") 41 | } 42 | 43 | // rowLabel returns one label for the specified row. 44 | // The row is the Y coordinate of the row, Y coordinates grow down. 45 | func rowLabel(row int, label string, labelWidth int) (*Label, error) { 46 | return nil, errors.New("not implemented") 47 | } 48 | 49 | // xLabels returns labels that should be placed under the cells. 50 | // Labels are returned with X coordinates in ascending order. 51 | // X coordinates grow right. 52 | func xLabels(yEnd image.Point, graphWidth int, labels []string, cellWidth int) ([]*Label, error) { 53 | return nil, errors.New("not implemented") 54 | } 55 | 56 | // paddedLabelLength calculates the length of the padded X label and 57 | // the column index corresponding to the label. 58 | // For example, the longest X label's length is 5, like '12:34', and the cell's width is 3. 59 | // So in order to better display, every three columns of cells will display a X label, 60 | // the X label belongs to the middle column of the three columns, 61 | // and the padded length is 3*3 (cellWidth multiplies the number of columns), which is 9. 62 | func paddedLabelLength(graphWidth, longest, cellWidth int) (l, index int) { 63 | return 64 | } 65 | -------------------------------------------------------------------------------- /widgets/heatmap/internal/axes/label_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 axes 16 | -------------------------------------------------------------------------------- /widgets/heatmap/options.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 heatmap 16 | 17 | import ( 18 | "errors" 19 | "github.com/mum4k/termdash/cell" 20 | ) 21 | 22 | // options.go contains configurable options for HeatMap. 23 | 24 | // Option is used to provide options. 25 | type Option interface { 26 | // set sets the provided option. 27 | set(*options) 28 | } 29 | 30 | // options stores the provided options. 31 | type options struct { 32 | // The default value is 3 33 | cellWidth int 34 | xLabelCellOpts []cell.Option 35 | yLabelCellOpts []cell.Option 36 | } 37 | 38 | // validate validates the provided options. 39 | func (o *options) validate() error { 40 | return errors.New("not implemented") 41 | } 42 | 43 | // newOptions returns a new options instance. 44 | func newOptions(opts ...Option) *options { 45 | opt := &options{ 46 | cellWidth: 3, 47 | } 48 | for _, o := range opts { 49 | o.set(opt) 50 | } 51 | return opt 52 | } 53 | 54 | // option implements Option. 55 | type option func(*options) 56 | 57 | // set implements Option.set. 58 | func (o option) set(opts *options) { 59 | o(opts) 60 | } 61 | 62 | // CellWidth set the width of cells (or grids) in the heat map, not the terminal cell. 63 | // The default height of each cell (grid) is 1 and the width is 3. 64 | func CellWidth(w int) Option { 65 | return option(func(opts *options) { 66 | opts.cellWidth = w 67 | }) 68 | } 69 | 70 | // XLabelCellOpts set the cell options for the labels on the X axis. 71 | func XLabelCellOpts(co ...cell.Option) Option { 72 | return option(func(opts *options) { 73 | opts.xLabelCellOpts = co 74 | }) 75 | } 76 | 77 | // YLabelCellOpts set the cell options for the labels on the Y axis. 78 | func YLabelCellOpts(co ...cell.Option) Option { 79 | return option(func(opts *options) { 80 | opts.yLabelCellOpts = co 81 | }) 82 | } 83 | -------------------------------------------------------------------------------- /widgets/linechart/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 | -------------------------------------------------------------------------------- /widgets/linechart/internal/axes/value.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 axes 16 | 17 | // value.go contains code dealing with values on the line chart. 18 | 19 | import ( 20 | "fmt" 21 | "math" 22 | 23 | "github.com/mum4k/termdash/private/numbers" 24 | ) 25 | 26 | // ValueOption is used to provide options to the NewValue function. 27 | type ValueOption interface { 28 | // set sets the provided option. 29 | set(*valueOptions) 30 | } 31 | 32 | type valueOptions struct { 33 | formatter func(v float64) string 34 | } 35 | 36 | // valueOption implements ValueOption. 37 | type valueOption func(opts *valueOptions) 38 | 39 | // set implements ValueOption.set. 40 | func (vo valueOption) set(opts *valueOptions) { 41 | vo(opts) 42 | } 43 | 44 | // ValueFormatter sets a custom formatter for the value. 45 | func ValueFormatter(formatter func(float64) string) ValueOption { 46 | return valueOption(func(opts *valueOptions) { 47 | opts.formatter = formatter 48 | }) 49 | } 50 | 51 | // Value represents one value. 52 | type Value struct { 53 | // Value is the original unmodified value. 54 | Value float64 55 | // Rounded is the value rounded up to the nonZeroPlaces number of non-zero 56 | // decimal places. 57 | Rounded float64 58 | // ZeroDecimals indicates how many decimal places in Rounded have a value 59 | // of zero. 60 | ZeroDecimals int 61 | // NonZeroDecimals indicates the rounding precision used, it is provided on 62 | // a call to newValue. 63 | NonZeroDecimals int 64 | 65 | // formatter will format value to a string representation of the value, 66 | // if Formatter is not present it will fallback to default format. 67 | formatter func(float64) string 68 | // text value if this value was constructed using NewTextValue. 69 | text string 70 | } 71 | 72 | // String implements fmt.Stringer. 73 | func (v *Value) String() string { 74 | return fmt.Sprintf("Value{Round(%v) => %v}", v.Value, v.Rounded) 75 | } 76 | 77 | // NewValue returns a new instance representing the provided value, rounding 78 | // the value up to the specified number of non-zero decimal places. 79 | func NewValue(v float64, nonZeroDecimals int, opts ...ValueOption) *Value { 80 | opt := &valueOptions{} 81 | for _, o := range opts { 82 | o.set(opt) 83 | } 84 | 85 | r, zd := numbers.RoundToNonZeroPlaces(v, nonZeroDecimals) 86 | return &Value{ 87 | Value: v, 88 | Rounded: r, 89 | ZeroDecimals: zd, 90 | NonZeroDecimals: nonZeroDecimals, 91 | formatter: opt.formatter, 92 | } 93 | } 94 | 95 | // NewTextValue constructs a value out of the provided text. 96 | func NewTextValue(text string) *Value { 97 | return &Value{ 98 | Value: math.NaN(), 99 | Rounded: math.NaN(), 100 | text: text, 101 | } 102 | } 103 | 104 | // Text returns textual representation of the value. 105 | func (v *Value) Text() string { 106 | if v.text != "" { 107 | return v.text 108 | } 109 | 110 | if v.formatter != nil { 111 | return v.formatter(v.Value) 112 | } 113 | 114 | return defaultFormatter(v.Rounded, v.NonZeroDecimals, v.ZeroDecimals) 115 | } 116 | 117 | func defaultFormatter(value float64, nonZeroDecimals, zeroDecimals int) string { 118 | if math.Ceil(value) == value { 119 | return fmt.Sprintf("%.0f", value) 120 | } 121 | 122 | format := fmt.Sprintf("%%.%df", nonZeroDecimals+zeroDecimals) 123 | t := fmt.Sprintf(format, value) 124 | if len(t) > 10 { 125 | t = fmt.Sprintf("%.2e", value) 126 | } 127 | 128 | return t 129 | } 130 | -------------------------------------------------------------------------------- /widgets/linechart/internal/axes/value_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 axes 16 | 17 | import ( 18 | "fmt" 19 | "testing" 20 | 21 | "github.com/kylelemons/godebug/pretty" 22 | ) 23 | 24 | func TestValue(t *testing.T) { 25 | formatter := func(float64) string { return "test" } 26 | 27 | tests := []struct { 28 | desc string 29 | float float64 30 | nonZeroDecimals int 31 | formatter func(float64) string 32 | want *Value 33 | }{ 34 | { 35 | desc: "handles zeroes", 36 | float: 0, 37 | nonZeroDecimals: 0, 38 | want: &Value{ 39 | Value: 0, 40 | Rounded: 0, 41 | ZeroDecimals: 0, 42 | NonZeroDecimals: 0, 43 | }, 44 | }, 45 | { 46 | desc: "rounds to requested precision", 47 | float: 1.01234, 48 | nonZeroDecimals: 2, 49 | want: &Value{ 50 | Value: 1.01234, 51 | Rounded: 1.013, 52 | ZeroDecimals: 1, 53 | NonZeroDecimals: 2, 54 | }, 55 | }, 56 | { 57 | desc: "no rounding when not requested", 58 | float: 1.01234, 59 | nonZeroDecimals: 0, 60 | want: &Value{ 61 | Value: 1.01234, 62 | Rounded: 1.01234, 63 | ZeroDecimals: 1, 64 | NonZeroDecimals: 0, 65 | }, 66 | }, 67 | { 68 | desc: "formatter value when value formatter as option", 69 | float: 1.01234, 70 | nonZeroDecimals: 0, 71 | formatter: formatter, 72 | want: &Value{ 73 | Value: 1.01234, 74 | Rounded: 1.01234, 75 | ZeroDecimals: 1, 76 | NonZeroDecimals: 0, 77 | formatter: formatter, 78 | }, 79 | }, 80 | } 81 | 82 | for _, tc := range tests { 83 | t.Run(tc.desc, func(t *testing.T) { 84 | got := NewValue(tc.float, tc.nonZeroDecimals, ValueFormatter(tc.formatter)) 85 | if diff := pretty.Compare(tc.want, got); diff != "" { 86 | t.Errorf("NewValue => unexpected diff (-want, +got):\n%s", diff) 87 | } 88 | }) 89 | } 90 | } 91 | 92 | func TestText(t *testing.T) { 93 | tests := []struct { 94 | value float64 95 | nonZeroDecimals int 96 | wantRounded float64 97 | wantText string 98 | }{ 99 | {0, 2, 0, "0"}, 100 | {10, 2, 10, "10"}, 101 | {-10, 2, -10, "-10"}, 102 | {0.5, 2, 0.5, "0.50"}, 103 | {-0.5, 2, -0.5, "-0.50"}, 104 | {100.5, 2, 100.5, "100.50"}, 105 | {-100.5, 2, -100.5, "-100.50"}, 106 | {0.12345, 1, 0.2, "0.2"}, 107 | {0.12345, 2, 0.13, "0.13"}, 108 | {0.123, 4, 0.123, "0.1230"}, 109 | {-0.12345, 2, -0.12, "-0.12"}, 110 | {999.12345, 2, 999.13, "999.13"}, 111 | {-999.12345, 2, -999.12, "-999.12"}, 112 | {999.00012345, 2, 999.00013, "999.00013"}, 113 | {-999.00012345, 2, -999.00012, "-999.00012"}, 114 | {100000.1, 2, 100000.1, "100000.10"}, 115 | {1000000.1, 2, 1000000.1, "1.00e+06"}, 116 | } 117 | 118 | for _, tc := range tests { 119 | t.Run(fmt.Sprintf("%v_%v", tc.value, tc.nonZeroDecimals), func(t *testing.T) { 120 | v := NewValue(tc.value, tc.nonZeroDecimals) 121 | gotRounded := v.Rounded 122 | if gotRounded != tc.wantRounded { 123 | t.Errorf("newValue(%v, %v).Rounded => got %v, want %v", tc.value, tc.nonZeroDecimals, gotRounded, tc.wantRounded) 124 | } 125 | 126 | gotText := v.Text() 127 | if gotText != tc.wantText { 128 | t.Errorf("newValue(%v, %v).Text => got %q, want %q", tc.value, tc.nonZeroDecimals, gotText, tc.wantText) 129 | } 130 | 131 | }) 132 | } 133 | } 134 | 135 | func TestNewTextValue(t *testing.T) { 136 | const want = "foo" 137 | v := NewTextValue(want) 138 | got := v.Text() 139 | if got != want { 140 | t.Errorf("v.Text => got %q, want %q", got, want) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /widgets/linechart/linechartdemo/linechartdemo.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 | // Binary linechartdemo displays a linechart widget. 16 | // Exist when 'q' is pressed. 17 | package main 18 | 19 | import ( 20 | "context" 21 | "math" 22 | "time" 23 | 24 | "github.com/mum4k/termdash" 25 | "github.com/mum4k/termdash/cell" 26 | "github.com/mum4k/termdash/container" 27 | "github.com/mum4k/termdash/linestyle" 28 | "github.com/mum4k/termdash/terminal/tcell" 29 | "github.com/mum4k/termdash/terminal/terminalapi" 30 | "github.com/mum4k/termdash/widgets/linechart" 31 | ) 32 | 33 | // sineInputs generates values from -1 to 1 for display on the line chart. 34 | func sineInputs() []float64 { 35 | var res []float64 36 | 37 | for i := 0; i < 200; i++ { 38 | v := math.Sin(float64(i) / 100 * math.Pi) 39 | res = append(res, v) 40 | } 41 | return res 42 | } 43 | 44 | // playLineChart continuously adds values to the LineChart, once every delay. 45 | // Exits when the context expires. 46 | func playLineChart(ctx context.Context, lc *linechart.LineChart, delay time.Duration) { 47 | inputs := sineInputs() 48 | ticker := time.NewTicker(delay) 49 | defer ticker.Stop() 50 | for i := 0; ; { 51 | select { 52 | case <-ticker.C: 53 | i = (i + 1) % len(inputs) 54 | rotated := append(inputs[i:], inputs[:i]...) 55 | if err := lc.Series("first", rotated, 56 | linechart.SeriesCellOpts(cell.FgColor(cell.ColorNumber(33))), 57 | linechart.SeriesXLabels(map[int]string{ 58 | 0: "zero", 59 | }), 60 | ); err != nil { 61 | panic(err) 62 | } 63 | 64 | i2 := (i + 100) % len(inputs) 65 | rotated2 := append(inputs[i2:], inputs[:i2]...) 66 | if err := lc.Series("second", rotated2, linechart.SeriesCellOpts(cell.FgColor(cell.ColorWhite))); err != nil { 67 | panic(err) 68 | } 69 | 70 | case <-ctx.Done(): 71 | return 72 | } 73 | } 74 | } 75 | 76 | func main() { 77 | t, err := tcell.New() 78 | if err != nil { 79 | panic(err) 80 | } 81 | defer t.Close() 82 | 83 | const redrawInterval = 250 * time.Millisecond 84 | ctx, cancel := context.WithCancel(context.Background()) 85 | lc, err := linechart.New( 86 | linechart.AxesCellOpts(cell.FgColor(cell.ColorRed)), 87 | linechart.YLabelCellOpts(cell.FgColor(cell.ColorGreen)), 88 | linechart.XLabelCellOpts(cell.FgColor(cell.ColorCyan)), 89 | ) 90 | if err != nil { 91 | panic(err) 92 | } 93 | go playLineChart(ctx, lc, redrawInterval/3) 94 | c, err := container.New( 95 | t, 96 | container.Border(linestyle.Light), 97 | container.BorderTitle("PRESS Q TO QUIT"), 98 | container.PlaceWidget(lc), 99 | ) 100 | if err != nil { 101 | panic(err) 102 | } 103 | 104 | quitter := func(k *terminalapi.Keyboard) { 105 | if k.Key == 'q' || k.Key == 'Q' { 106 | cancel() 107 | } 108 | } 109 | 110 | if err := termdash.Run(ctx, t, c, termdash.KeyboardSubscriber(quitter), termdash.RedrawInterval(redrawInterval)); err != nil { 111 | panic(err) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /widgets/segmentdisplay/options.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 segmentdisplay 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/mum4k/termdash/align" 21 | ) 22 | 23 | // options.go contains configurable options for SegmentDisplay. 24 | 25 | // Option is used to provide options. 26 | type Option interface { 27 | // set sets the provided option. 28 | set(*options) 29 | } 30 | 31 | // option implements Option. 32 | type option func(*options) 33 | 34 | // set implements Option.set. 35 | func (o option) set(opts *options) { 36 | o(opts) 37 | } 38 | 39 | // options holds the provided options. 40 | type options struct { 41 | hAlign align.Horizontal 42 | vAlign align.Vertical 43 | maximizeSegSize bool 44 | gapPercent int 45 | } 46 | 47 | // validate validates the provided options. 48 | func (o *options) validate() error { 49 | if min, max := 0, 100; o.gapPercent < min || o.gapPercent > max { 50 | return fmt.Errorf("invalid GapPercent %d, must be %d <= value <= %d", o.gapPercent, min, max) 51 | } 52 | return nil 53 | } 54 | 55 | // newOptions returns options with the default values set. 56 | func newOptions() *options { 57 | return &options{ 58 | hAlign: align.HorizontalCenter, 59 | vAlign: align.VerticalMiddle, 60 | gapPercent: DefaultGapPercent, 61 | } 62 | } 63 | 64 | // AlignHorizontal sets the horizontal alignment for the individual display 65 | // segments. Defaults to alignment in the center. 66 | func AlignHorizontal(h align.Horizontal) Option { 67 | return option(func(opts *options) { 68 | opts.hAlign = h 69 | }) 70 | } 71 | 72 | // AlignVertical sets the vertical alignment for the individual display 73 | // segments. Defaults to alignment in the middle 74 | func AlignVertical(v align.Vertical) Option { 75 | return option(func(opts *options) { 76 | opts.vAlign = v 77 | }) 78 | } 79 | 80 | // MaximizeSegmentHeight tells the widget to maximize the height of the 81 | // individual display segments. 82 | // When this option is set and the user has provided more text than we can fit 83 | // on the canvas, the widget will prefer to maximize height of individual 84 | // characters which will result in earlier trimming of the text. 85 | func MaximizeSegmentHeight() Option { 86 | return option(func(opts *options) { 87 | opts.maximizeSegSize = true 88 | }) 89 | } 90 | 91 | // MaximizeDisplayedText tells the widget to maximize the amount of characters 92 | // that are displayed. 93 | // When this option is set and the user has provided more text than we can fit 94 | // on the canvas, the widget will prefer to decrease the height of individual 95 | // characters and fit more of them on the canvas. 96 | // This is the default behavior. 97 | func MaximizeDisplayedText() Option { 98 | return option(func(opts *options) { 99 | opts.maximizeSegSize = false 100 | }) 101 | } 102 | 103 | // DefaultGapPercent is the default value for the GapPercent option. 104 | const DefaultGapPercent = 20 105 | 106 | // GapPercent sets the size of the horizontal gap between individual segments 107 | // (characters) expressed as a percentage of the segment height. 108 | func GapPercent(perc int) Option { 109 | return option(func(opts *options) { 110 | opts.gapPercent = perc 111 | }) 112 | } 113 | -------------------------------------------------------------------------------- /widgets/segmentdisplay/segment_area.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 segmentdisplay 16 | 17 | // segment_area.go contains code that determines how many segments we can fit 18 | // in the canvas. 19 | 20 | import ( 21 | "fmt" 22 | "image" 23 | 24 | "github.com/mum4k/termdash/private/segdisp" 25 | ) 26 | 27 | // segArea contains information about the area that will contain the segments. 28 | type segArea struct { 29 | // segment is the area for one segment. 30 | segment image.Rectangle 31 | // canFit is the number of segments we can fit on the canvas. 32 | canFit int 33 | // gapPixels is the size of gaps between segments in pixels. 34 | gapPixels int 35 | // gaps is the number of gaps that will be drawn. 36 | gaps int 37 | } 38 | 39 | // needArea returns the complete area required for all the segments that we can 40 | // fit and any gaps. 41 | func (sa *segArea) needArea() image.Rectangle { 42 | return image.Rect( 43 | 0, 44 | 0, 45 | sa.segment.Dx()*sa.canFit+sa.gaps*sa.gapPixels, 46 | sa.segment.Dy(), 47 | ) 48 | } 49 | 50 | // newSegArea calculates the area for segments given available canvas area, 51 | // length of the text to be displayed and the size of gap between segments 52 | func newSegArea(cvsAr image.Rectangle, textLen, gapPercent int) (*segArea, error) { 53 | segAr, err := segdisp.Required(cvsAr) 54 | if err != nil { 55 | return nil, fmt.Errorf("sixteen.Required => %v", err) 56 | } 57 | gapPixels := segAr.Dy() * gapPercent / 100 58 | 59 | var ( 60 | gaps int 61 | canFit int 62 | taken int 63 | ) 64 | for i := 0; ; { 65 | taken += segAr.Dx() 66 | 67 | if taken > cvsAr.Dx() { 68 | break 69 | } 70 | canFit++ 71 | 72 | // Don't insert gaps after the last segment in the text or the last 73 | // segment we can fit. 74 | if gapPixels == 0 || i == textLen-1 { 75 | continue 76 | } 77 | 78 | remaining := cvsAr.Dx() - taken 79 | // Only insert gaps if we can still fit one more segment with the gap. 80 | if remaining >= gapPixels+segAr.Dx() { 81 | taken += gapPixels 82 | gaps++ 83 | } else { 84 | // Gap is needed but doesn't fit together with the next segment. 85 | // So insert neither. 86 | break 87 | } 88 | i++ 89 | } 90 | return &segArea{ 91 | segment: segAr, 92 | canFit: canFit, 93 | gapPixels: gapPixels, 94 | gaps: gaps, 95 | }, nil 96 | } 97 | 98 | // maximizeFit finds the largest individual segment size that enables us to fit 99 | // the most characters onto a canvas with the provided area. Returns the area 100 | // required for a single segment and the number of segments we can fit. 101 | func maximizeFit(cvsAr image.Rectangle, textLen, gapPercent int) (*segArea, error) { 102 | var bestSegAr *segArea 103 | for height := cvsAr.Dy(); height >= segdisp.MinRows; height-- { 104 | cvsAr := image.Rect(cvsAr.Min.X, cvsAr.Min.Y, cvsAr.Max.X, cvsAr.Min.Y+height) 105 | segAr, err := newSegArea(cvsAr, textLen, gapPercent) 106 | if err != nil { 107 | return nil, err 108 | } 109 | 110 | if textLen > 0 && segAr.canFit >= textLen { 111 | return segAr, nil 112 | } 113 | bestSegAr = segAr 114 | } 115 | return bestSegAr, nil 116 | } 117 | -------------------------------------------------------------------------------- /widgets/segmentdisplay/segmentdisplaydemo/segmentdisplaydemo.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 | // Binary segmentdisplaydemo shows the functionality of a segment display. 16 | package main 17 | 18 | import ( 19 | "context" 20 | "strings" 21 | "time" 22 | 23 | "github.com/mum4k/termdash" 24 | "github.com/mum4k/termdash/cell" 25 | "github.com/mum4k/termdash/container" 26 | "github.com/mum4k/termdash/linestyle" 27 | "github.com/mum4k/termdash/terminal/tcell" 28 | "github.com/mum4k/termdash/terminal/terminalapi" 29 | "github.com/mum4k/termdash/widgets/segmentdisplay" 30 | ) 31 | 32 | // clock displays the current time on the segment display. 33 | // Exists when the context expires. 34 | func clock(ctx context.Context, sd *segmentdisplay.SegmentDisplay) { 35 | ticker := time.NewTicker(1 * time.Second) 36 | defer ticker.Stop() 37 | for { 38 | select { 39 | case <-ticker.C: 40 | now := time.Now() 41 | nowStr := now.Format("15 04") 42 | parts := strings.Split(nowStr, " ") 43 | 44 | spacer := " " 45 | if now.Second()%2 == 0 { 46 | spacer = ":" 47 | } 48 | chunks := []*segmentdisplay.TextChunk{ 49 | segmentdisplay.NewChunk(parts[0], segmentdisplay.WriteCellOpts(cell.FgColor(cell.ColorNumber(33)))), 50 | segmentdisplay.NewChunk(spacer), 51 | segmentdisplay.NewChunk(parts[1], segmentdisplay.WriteCellOpts(cell.FgColor(cell.ColorRed))), 52 | } 53 | if err := sd.Write(chunks); err != nil { 54 | panic(err) 55 | } 56 | 57 | case <-ctx.Done(): 58 | return 59 | } 60 | } 61 | } 62 | 63 | // rotate returns a new slice with inputs rotated by step. 64 | // I.e. for a step of one: 65 | // 66 | // inputs[0] -> inputs[len(inputs)-1] 67 | // inputs[1] -> inputs[0] 68 | // 69 | // And so on. 70 | func rotate(inputs []rune, step int) []rune { 71 | return append(inputs[step:], inputs[:step]...) 72 | } 73 | 74 | // rollText rolls a text across the segment display. 75 | // Exists when the context expires. 76 | func rollText(ctx context.Context, sd *segmentdisplay.SegmentDisplay) { 77 | const text = "Termdash" 78 | colors := map[rune]cell.Color{ 79 | 'T': cell.ColorNumber(33), 80 | 'e': cell.ColorRed, 81 | 'r': cell.ColorYellow, 82 | 'm': cell.ColorNumber(33), 83 | 'd': cell.ColorGreen, 84 | 'a': cell.ColorRed, 85 | 's': cell.ColorGreen, 86 | 'h': cell.ColorRed, 87 | } 88 | 89 | var state []rune 90 | for i := 0; i < len(text); i++ { 91 | state = append(state, ' ') 92 | } 93 | state = append(state, []rune(text)...) 94 | ticker := time.NewTicker(1 * time.Second) 95 | defer ticker.Stop() 96 | for { 97 | select { 98 | case <-ticker.C: 99 | var chunks []*segmentdisplay.TextChunk 100 | for i := 0; i < len(text); i++ { 101 | chunks = append(chunks, segmentdisplay.NewChunk( 102 | string(state[i]), 103 | segmentdisplay.WriteCellOpts(cell.FgColor(colors[state[i]])), 104 | )) 105 | } 106 | if err := sd.Write(chunks); err != nil { 107 | panic(err) 108 | } 109 | state = rotate(state, 1) 110 | 111 | case <-ctx.Done(): 112 | return 113 | } 114 | } 115 | } 116 | 117 | func main() { 118 | t, err := tcell.New() 119 | if err != nil { 120 | panic(err) 121 | } 122 | defer t.Close() 123 | 124 | ctx, cancel := context.WithCancel(context.Background()) 125 | clockSD, err := segmentdisplay.New() 126 | if err != nil { 127 | panic(err) 128 | } 129 | go clock(ctx, clockSD) 130 | 131 | rollingSD, err := segmentdisplay.New() 132 | if err != nil { 133 | panic(err) 134 | } 135 | go rollText(ctx, rollingSD) 136 | 137 | c, err := container.New( 138 | t, 139 | container.Border(linestyle.Light), 140 | container.BorderTitle("PRESS Q TO QUIT"), 141 | container.SplitHorizontal( 142 | container.Top( 143 | container.PlaceWidget(rollingSD), 144 | ), 145 | container.Bottom( 146 | container.PlaceWidget(clockSD), 147 | ), 148 | container.SplitPercent(40), 149 | ), 150 | ) 151 | if err != nil { 152 | panic(err) 153 | } 154 | 155 | quitter := func(k *terminalapi.Keyboard) { 156 | if k.Key == 'q' || k.Key == 'Q' { 157 | cancel() 158 | } 159 | } 160 | 161 | if err := termdash.Run(ctx, t, c, termdash.KeyboardSubscriber(quitter), termdash.RedrawInterval(1*time.Second)); err != nil { 162 | panic(err) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /widgets/segmentdisplay/write_options.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 segmentdisplay 16 | 17 | // write_options.go contains options used when writing content to the widget. 18 | 19 | import "github.com/mum4k/termdash/cell" 20 | 21 | // WriteOption is used to provide options to Write(). 22 | type WriteOption interface { 23 | // set sets the provided option. 24 | set(*writeOptions) 25 | } 26 | 27 | // writeOptions stores the provided options. 28 | type writeOptions struct { 29 | cellOpts []cell.Option 30 | errOnUnsupported bool 31 | } 32 | 33 | // newWriteOptions returns new writeOptions instance. 34 | func newWriteOptions(wOpts ...WriteOption) *writeOptions { 35 | wo := &writeOptions{} 36 | for _, o := range wOpts { 37 | o.set(wo) 38 | } 39 | return wo 40 | } 41 | 42 | // writeOption implements WriteOption. 43 | type writeOption func(*writeOptions) 44 | 45 | // set implements WriteOption.set. 46 | func (wo writeOption) set(wOpts *writeOptions) { 47 | wo(wOpts) 48 | } 49 | 50 | // WriteCellOpts sets options on the cells that contain the text. 51 | func WriteCellOpts(opts ...cell.Option) WriteOption { 52 | return writeOption(func(wOpts *writeOptions) { 53 | wOpts.cellOpts = opts 54 | }) 55 | } 56 | 57 | // WriteSanitize instructs Write to sanitize the text, replacing all characters 58 | // the display doesn't support with a space ' ' character. 59 | // This is the default behavior. 60 | func WriteSanitize(opts ...cell.Option) WriteOption { 61 | return writeOption(func(wOpts *writeOptions) { 62 | wOpts.errOnUnsupported = false 63 | }) 64 | } 65 | 66 | // WriteErrOnUnsupported instructs Write to return an error when the text 67 | // contains a character the display doesn't support. 68 | // The default behavior is to sanitize the text, see WriteSanitize(). 69 | func WriteErrOnUnsupported(opts ...cell.Option) WriteOption { 70 | return writeOption(func(wOpts *writeOptions) { 71 | wOpts.errOnUnsupported = true 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /widgets/sparkline/options.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 sparkline 16 | 17 | // options.go contains configurable options for SparkLine. 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/mum4k/termdash/cell" 23 | ) 24 | 25 | // Option is used to provide options. 26 | type Option interface { 27 | // set sets the provided option. 28 | set(*options) 29 | } 30 | 31 | // option implements Option. 32 | type option func(*options) 33 | 34 | // set implements Option.set. 35 | func (o option) set(opts *options) { 36 | o(opts) 37 | } 38 | 39 | // options holds the provided options. 40 | type options struct { 41 | label string 42 | labelCellOpts []cell.Option 43 | height int 44 | color cell.Color 45 | } 46 | 47 | // newOptions returns options with the default values set. 48 | func newOptions() *options { 49 | return &options{ 50 | color: DefaultColor, 51 | } 52 | } 53 | 54 | // validate validates the provided options. 55 | func (o *options) validate() error { 56 | if got, min := o.height, 0; got < min { 57 | return fmt.Errorf("invalid Height %d, must be %d <= Height", got, min) 58 | } 59 | return nil 60 | } 61 | 62 | // Label adds a label above the SparkLine. 63 | func Label(text string, cOpts ...cell.Option) Option { 64 | return option(func(opts *options) { 65 | opts.label = text 66 | opts.labelCellOpts = cOpts 67 | }) 68 | } 69 | 70 | // Height sets a fixed height for the SparkLine. 71 | // If not provided or set to zero, the SparkLine takes all the available 72 | // vertical space in the container. Must be a positive or zero integer. 73 | func Height(h int) Option { 74 | return option(func(opts *options) { 75 | opts.height = h 76 | }) 77 | } 78 | 79 | // DefaultColor is the default value for the Color option. 80 | const DefaultColor = cell.ColorGreen 81 | 82 | // Color sets the color of the SparkLine. 83 | // Defaults to DefaultColor if not set. 84 | func Color(c cell.Color) Option { 85 | return option(func(opts *options) { 86 | opts.color = c 87 | }) 88 | } 89 | -------------------------------------------------------------------------------- /widgets/sparkline/sparklinedemo/sparklinedemo.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 | // Binary sparklinedemo displays a couple of SparkLine widgets. 16 | // Exist when 'q' is pressed. 17 | package main 18 | 19 | import ( 20 | "context" 21 | "math/rand" 22 | "time" 23 | 24 | "github.com/mum4k/termdash" 25 | "github.com/mum4k/termdash/cell" 26 | "github.com/mum4k/termdash/container" 27 | "github.com/mum4k/termdash/linestyle" 28 | "github.com/mum4k/termdash/terminal/tcell" 29 | "github.com/mum4k/termdash/terminal/terminalapi" 30 | "github.com/mum4k/termdash/widgets/sparkline" 31 | ) 32 | 33 | // playSparkLine continuously adds values to the SparkLine, once every delay. 34 | // Exits when the context expires. 35 | func playSparkLine(ctx context.Context, sl *sparkline.SparkLine, delay time.Duration) { 36 | const max = 100 37 | 38 | ticker := time.NewTicker(delay) 39 | defer ticker.Stop() 40 | for { 41 | select { 42 | case <-ticker.C: 43 | v := int(rand.Int31n(max + 1)) 44 | if err := sl.Add([]int{v}); err != nil { 45 | panic(err) 46 | } 47 | 48 | case <-ctx.Done(): 49 | return 50 | } 51 | } 52 | } 53 | 54 | // fillSparkLine continuously fills the SparkLine up to its capacity with 55 | // random values. 56 | func fillSparkLine(ctx context.Context, sl *sparkline.SparkLine, delay time.Duration) { 57 | const max = 100 58 | 59 | ticker := time.NewTicker(delay) 60 | defer ticker.Stop() 61 | for { 62 | select { 63 | case <-ticker.C: 64 | var values []int 65 | for i := 0; i < sl.ValueCapacity(); i++ { 66 | values = append(values, int(rand.Int31n(max+1))) 67 | } 68 | if err := sl.Add(values); err != nil { 69 | panic(err) 70 | } 71 | 72 | case <-ctx.Done(): 73 | return 74 | } 75 | } 76 | } 77 | 78 | func main() { 79 | t, err := tcell.New() 80 | if err != nil { 81 | panic(err) 82 | } 83 | defer t.Close() 84 | 85 | ctx, cancel := context.WithCancel(context.Background()) 86 | green, err := sparkline.New( 87 | sparkline.Label("Green SparkLine", cell.FgColor(cell.ColorNumber(33))), 88 | sparkline.Color(cell.ColorGreen), 89 | ) 90 | if err != nil { 91 | panic(err) 92 | } 93 | go playSparkLine(ctx, green, 250*time.Millisecond) 94 | red, err := sparkline.New( 95 | sparkline.Label("Red SparkLine", cell.FgColor(cell.ColorNumber(33))), 96 | sparkline.Color(cell.ColorRed), 97 | ) 98 | if err != nil { 99 | panic(err) 100 | } 101 | go playSparkLine(ctx, red, 500*time.Millisecond) 102 | yellow, err := sparkline.New( 103 | sparkline.Label("Yellow SparkLine", cell.FgColor(cell.ColorGreen)), 104 | sparkline.Color(cell.ColorYellow), 105 | ) 106 | if err != nil { 107 | panic(err) 108 | } 109 | go fillSparkLine(ctx, yellow, 1*time.Second) 110 | 111 | c, err := container.New( 112 | t, 113 | container.Border(linestyle.Light), 114 | container.BorderTitle("PRESS Q TO QUIT"), 115 | container.SplitVertical( 116 | container.Left( 117 | container.SplitHorizontal( 118 | container.Top(), 119 | container.Bottom( 120 | container.Border(linestyle.Light), 121 | container.BorderTitle("SparkLine group"), 122 | container.SplitHorizontal( 123 | container.Top( 124 | container.PlaceWidget(green), 125 | ), 126 | container.Bottom( 127 | container.PlaceWidget(red), 128 | ), 129 | ), 130 | ), 131 | ), 132 | ), 133 | container.Right( 134 | container.Border(linestyle.Light), 135 | container.PlaceWidget(yellow), 136 | ), 137 | ), 138 | ) 139 | if err != nil { 140 | panic(err) 141 | } 142 | 143 | quitter := func(k *terminalapi.Keyboard) { 144 | if k.Key == 'q' || k.Key == 'Q' { 145 | cancel() 146 | } 147 | } 148 | 149 | if err := termdash.Run(ctx, t, c, termdash.KeyboardSubscriber(quitter)); err != nil { 150 | panic(err) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /widgets/sparkline/sparks.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 sparkline 16 | 17 | // sparks.go contains code that determines which characters should be used to 18 | // represent a value on the SparkLine. 19 | 20 | import ( 21 | "fmt" 22 | "math" 23 | 24 | "github.com/mum4k/termdash/private/runewidth" 25 | ) 26 | 27 | // sparks are the characters used to draw the SparkLine. 28 | var sparks = []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'} 29 | 30 | // visibleMax determines the maximum visible data point given the canvas width. 31 | // Returns a slice that contains only visible data points and the maximum value 32 | // among them. 33 | func visibleMax(data []int, width int) ([]int, int) { 34 | if width <= 0 || len(data) == 0 { 35 | return nil, 0 36 | } 37 | 38 | if width < len(data) { 39 | data = data[len(data)-width:] 40 | } 41 | 42 | var max int 43 | for _, v := range data { 44 | if v > max { 45 | max = v 46 | } 47 | } 48 | return data, max 49 | } 50 | 51 | // blocks represents the building blocks that display one value on a SparkLine. 52 | // I.e. one vertical bar. 53 | type blocks struct { 54 | // full is the number of fully populated blocks. 55 | full int 56 | 57 | // partSpark is the spark character from sparks that should be used in the 58 | // topmost block. Equals to zero if no partial block should be displayed. 59 | partSpark rune 60 | } 61 | 62 | // toBlocks determines the number of full and partial vertical blocks required 63 | // to represent the provided value given the specified max visible value and 64 | // number of vertical cells available to the SparkLine. 65 | func toBlocks(value, max, vertCells int) blocks { 66 | if value <= 0 || max <= 0 || vertCells <= 0 { 67 | return blocks{} 68 | } 69 | 70 | // How many of the smallest spark elements fit into a cell. 71 | cellSparks := len(sparks) 72 | 73 | // Scale is how much of the max does one smallest spark element represent, 74 | // given the vertical cells that will be used to represent the value. 75 | scale := float64(cellSparks) * float64(vertCells) / float64(max) 76 | 77 | // How many smallest spark elements are needed to represent the value. 78 | elements := int(math.Round(float64(value) * scale)) 79 | 80 | b := blocks{ 81 | full: elements / cellSparks, 82 | } 83 | 84 | part := elements % cellSparks 85 | if part > 0 { 86 | b.partSpark = sparks[part-1] 87 | } 88 | return b 89 | } 90 | 91 | // init ensures that all spark characters are half-width runes. 92 | // The SparkLine widget assumes that each value can be represented in a column 93 | // that has a width of one cell. 94 | func init() { 95 | for i, s := range sparks { 96 | if got := runewidth.RuneWidth(s); got > 1 { 97 | panic(fmt.Sprintf("all sparks must be half-width runes (width of one), spark[%d] has width %d", i, got)) 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /widgets/text/line_trim.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 text 16 | 17 | import ( 18 | "fmt" 19 | "image" 20 | 21 | "github.com/mum4k/termdash/private/canvas" 22 | "github.com/mum4k/termdash/private/runewidth" 23 | "github.com/mum4k/termdash/private/wrap" 24 | ) 25 | 26 | // line_trim.go contains code that trims lines that are too long. 27 | 28 | type trimResult struct { 29 | // trimmed is set to true if the current and the following runes on this 30 | // line are trimmed. 31 | trimmed bool 32 | 33 | // curPoint is the updated current point the drawing should continue on. 34 | curPoint image.Point 35 | } 36 | 37 | // drawTrimChar draws the horizontal ellipsis '…' character as the last 38 | // character in the canvas on the specified line. 39 | func drawTrimChar(cvs *canvas.Canvas, line int) error { 40 | lastPoint := image.Point{cvs.Area().Dx() - 1, line} 41 | // If the penultimate cell contains a full-width rune, we need to clear it 42 | // first. Otherwise the trim char would cover just half of it. 43 | if width := cvs.Area().Dx(); width > 1 { 44 | penUlt := image.Point{width - 2, line} 45 | prev, err := cvs.Cell(penUlt) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | if runewidth.RuneWidth(prev.Rune) == 2 { 51 | if _, err := cvs.SetCell(penUlt, 0); err != nil { 52 | return err 53 | } 54 | } 55 | } 56 | 57 | cells, err := cvs.SetCell(lastPoint, '…') 58 | if err != nil { 59 | return err 60 | } 61 | if cells != 1 { 62 | panic(fmt.Errorf("invalid trim character, it occupies %d cells, the implementation only supports scroll markers that occupy exactly one cell", cells)) 63 | } 64 | return nil 65 | } 66 | 67 | // lineTrim determines if the current line needs to be trimmed. The cvs is the 68 | // canvas assigned to the widget, the curPoint is the current point the widget 69 | // is going to place the curRune at. If line trimming is needed, this function 70 | // replaces the last character with the horizontal ellipsis '…' character. 71 | func lineTrim(cvs *canvas.Canvas, curPoint image.Point, curRune rune, opts *options) (*trimResult, error) { 72 | if opts.wrapMode == wrap.AtRunes { 73 | // Don't trim if the widget is configured to wrap lines. 74 | return &trimResult{ 75 | trimmed: false, 76 | curPoint: curPoint, 77 | }, nil 78 | } 79 | 80 | // Newline characters are never trimmed, they start the next line. 81 | if curRune == '\n' { 82 | return &trimResult{ 83 | trimmed: false, 84 | curPoint: curPoint, 85 | }, nil 86 | } 87 | 88 | width := cvs.Area().Dx() 89 | rw := runewidth.RuneWidth(curRune) 90 | switch { 91 | case rw == 1: 92 | if curPoint.X == width { 93 | if err := drawTrimChar(cvs, curPoint.Y); err != nil { 94 | return nil, err 95 | } 96 | } 97 | 98 | case rw == 2: 99 | if curPoint.X == width || curPoint.X == width-1 { 100 | if err := drawTrimChar(cvs, curPoint.Y); err != nil { 101 | return nil, err 102 | } 103 | } 104 | 105 | default: 106 | return nil, fmt.Errorf("unable to decide line trimming at position %v for rune %q which has an unsupported width %d", curPoint, curRune, rw) 107 | } 108 | 109 | trimmed := curPoint.X > width-rw 110 | if trimmed { 111 | curPoint = image.Point{curPoint.X + rw, curPoint.Y} 112 | } 113 | return &trimResult{ 114 | trimmed: trimmed, 115 | curPoint: curPoint, 116 | }, nil 117 | } 118 | -------------------------------------------------------------------------------- /widgets/text/write_options.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 text 16 | 17 | // write_options.go contains options used when writing content to the Text widget. 18 | 19 | import ( 20 | "github.com/mum4k/termdash/cell" 21 | ) 22 | 23 | // WriteOption is used to provide options to Write(). 24 | type WriteOption interface { 25 | // set sets the provided option. 26 | set(*writeOptions) 27 | } 28 | 29 | // writeOptions stores the provided options. 30 | type writeOptions struct { 31 | cellOpts *cell.Options 32 | replace bool 33 | } 34 | 35 | // newWriteOptions returns new writeOptions instance. 36 | func newWriteOptions(wOpts ...WriteOption) *writeOptions { 37 | wo := &writeOptions{ 38 | cellOpts: cell.NewOptions(), 39 | } 40 | for _, o := range wOpts { 41 | o.set(wo) 42 | } 43 | return wo 44 | } 45 | 46 | // writeOption implements WriteOption. 47 | type writeOption func(*writeOptions) 48 | 49 | // set implements WriteOption.set. 50 | func (wo writeOption) set(wOpts *writeOptions) { 51 | wo(wOpts) 52 | } 53 | 54 | // WriteCellOpts sets options on the cells that contain the text. 55 | func WriteCellOpts(opts ...cell.Option) WriteOption { 56 | return writeOption(func(wOpts *writeOptions) { 57 | wOpts.cellOpts = cell.NewOptions(opts...) 58 | }) 59 | } 60 | 61 | // WriteReplace instructs the text widget to replace the entire text content on 62 | // this write instead of appending. 63 | func WriteReplace() WriteOption { 64 | return writeOption(func(wOpts *writeOptions) { 65 | wOpts.replace = true 66 | }) 67 | } 68 | --------------------------------------------------------------------------------