├── .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 |
--------------------------------------------------------------------------------