diff --git a/CHANGELOG.md b/CHANGELOG.md index 089c2b5..e28f104 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 series are provided. - Lint issues in the codebase, and updated Travis configuration so that golint is executed on every run. +- Termdash now correctly starts in locales like zh_CN.UTF-8 where some of the + characters it uses internally can have ambiguous width. ## [0.6.1] - 12-Feb-2019 diff --git a/align/align.go b/align/align.go index 4be0aa3..aac220f 100644 --- a/align/align.go +++ b/align/align.go @@ -20,7 +20,7 @@ import ( "image" "strings" - runewidth "github.com/mattn/go-runewidth" + "github.com/mum4k/termdash/cell/runewidth" ) // Horizontal indicates the type of horizontal alignment. diff --git a/cell/cell.go b/cell/cell.go index 7d3bb96..d8e3f37 100644 --- a/cell/cell.go +++ b/cell/cell.go @@ -23,8 +23,8 @@ import ( "fmt" "image" - runewidth "github.com/mattn/go-runewidth" "github.com/mum4k/termdash/area" + "github.com/mum4k/termdash/cell/runewidth" ) // Option is used to provide options for cells on a 2-D terminal. diff --git a/cell/runewidth/runewidth.go b/cell/runewidth/runewidth.go new file mode 100644 index 0000000..4f2f63a --- /dev/null +++ b/cell/runewidth/runewidth.go @@ -0,0 +1,98 @@ +// Copyright 2019 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package runewidth is a wrapper over github.com/mattn/go-runewidth which +// gives different treatment to certain runes with ambiguous width. +package runewidth + +import runewidth "github.com/mattn/go-runewidth" + +// RuneWidth returns the number of cells needed to draw r. +// Background in http://www.unicode.org/reports/tr11/. +// +// Treats runes used internally by termdash as single-cell (half-width) runes +// regardless of the locale. I.e. runes that are used to draw lines, boxes, +// indicate resize or text trimming was needed and runes used by the braille +// canvas. +// +// This should be safe, since even in locales where these runes have ambiguous +// width, we still place all the character content around them so they should +// have be half-width. +func RuneWidth(r rune) int { + if inTable(r, exceptions) { + return 1 + } + return runewidth.RuneWidth(r) +} + +// StringWidth is like RuneWidth, but returns the number of cells occupied by +// all the runes in the string. +func StringWidth(s string) int { + var width int + for _, r := range []rune(s) { + width += RuneWidth(r) + } + return width +} + +// inTable determines if the rune falls within the table. +// Copied from github.com/mattn/go-runewidth/blob/master/runewidth.go. +func inTable(r rune, t table) bool { + // func (t table) IncludesRune(r rune) bool { + if r < t[0].first { + return false + } + + bot := 0 + top := len(t) - 1 + for top >= bot { + mid := (bot + top) >> 1 + + switch { + case t[mid].last < r: + bot = mid + 1 + case t[mid].first > r: + top = mid - 1 + default: + return true + } + } + + return false +} + +type interval struct { + first rune + last rune +} + +type table []interval + +// exceptions runes defined here are always considered to be half-width even if +// they might be ambiguous in some contexts. +var exceptions = table{ + // Characters used by termdash to indicate text trim or scroll. + {0x2026, 0x2026}, + {0x21c4, 0x21c4}, + {0x21e7, 0x21e7}, + {0x21e9, 0x21e9}, + + // Box drawing, used as line-styles. + // https://en.wikipedia.org/wiki/Box-drawing_character + {0x2500, 0x257F}, + + // Block elements used as sparks. + // https://en.wikipedia.org/wiki/Box-drawing_character + {0x2580, 0x258F}, +} diff --git a/cell/runewidth/runewidth_test.go b/cell/runewidth/runewidth_test.go new file mode 100644 index 0000000..ce3ee6b --- /dev/null +++ b/cell/runewidth/runewidth_test.go @@ -0,0 +1,166 @@ +// Copyright 2019 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runewidth + +import ( + "testing" + + runewidth "github.com/mattn/go-runewidth" +) + +func TestRuneWidth(t *testing.T) { + tests := []struct { + desc string + runes []rune + eastAsian bool + want int + }{ + { + desc: "ascii characters", + runes: []rune{'a', 'f', '#'}, + want: 1, + }, + { + desc: "non-printable characters from mattn/runewidth/runewidth_test", + runes: []rune{'\x00', '\x01', '\u0300', '\u2028', '\u2029'}, + want: 0, + }, + { + desc: "half-width runes from mattn/runewidth/runewidth_test", + runes: []rune{'セ', 'カ', 'イ', '☆'}, + want: 1, + }, + { + desc: "full-width runes from mattn/runewidth/runewidth_test", + runes: []rune{'世', '界'}, + want: 2, + }, + { + desc: "ambiguous so double-width in eastAsian from mattn/runewidth/runewidth_test", + runes: []rune{'☆'}, + eastAsian: true, + want: 2, + }, + { + desc: "braille runes", + runes: []rune{'⠀', '⠴', '⠷', '⣿'}, + want: 1, + }, + { + desc: "braille runes in eastAsian", + runes: []rune{'⠀', '⠴', '⠷', '⣿'}, + eastAsian: true, + want: 1, + }, + { + desc: "termdash special runes", + runes: []rune{'⇄', '…', '⇧', '⇩'}, + want: 1, + }, + { + desc: "termdash special runes in eastAsian", + runes: []rune{'⇄', '…', '⇧', '⇩'}, + eastAsian: true, + want: 1, + }, + { + desc: "termdash sparks", + runes: []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'}, + want: 1, + }, + { + desc: "termdash sparks in eastAsian", + runes: []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'}, + eastAsian: true, + want: 1, + }, + { + desc: "termdash line styles", + runes: []rune{'─', '═', '─', '┼', '╬', '┼'}, + want: 1, + }, + { + desc: "termdash line styles in eastAsian", + runes: []rune{'─', '═', '─', '┼', '╬', '┼'}, + eastAsian: true, + want: 1, + }, + } + + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + runewidth.DefaultCondition.EastAsianWidth = tc.eastAsian + defer func() { + runewidth.DefaultCondition.EastAsianWidth = false + }() + + for _, r := range tc.runes { + if got := RuneWidth(r); got != tc.want { + t.Errorf("RuneWidth(%c, %#x) => %v, want %v", r, r, got, tc.want) + } + } + }) + } +} + +func TestStringWidth(t *testing.T) { + tests := []struct { + desc string + str string + eastAsian bool + want int + }{ + { + desc: "ascii characters", + str: "hello", + want: 5, + }, + { + desc: "string from mattn/runewidth/runewidth_test", + str: "■㈱の世界①", + want: 10, + }, + { + desc: "string in eastAsian from mattn/runewidth/runewidth_test", + str: "■㈱の世界①", + eastAsian: true, + want: 12, + }, + { + desc: "string using termdash characters", + str: "⇄…⇧⇩", + want: 4, + }, + { + desc: "string in eastAsien using termdash characters", + str: "⇄…⇧⇩", + eastAsian: true, + want: 4, + }, + } + + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + runewidth.DefaultCondition.EastAsianWidth = tc.eastAsian + defer func() { + runewidth.DefaultCondition.EastAsianWidth = false + }() + + if got := StringWidth(tc.str); got != tc.want { + t.Errorf("StringWidth(%q) => %v, want %v", tc.str, got, tc.want) + } + }) + } +} diff --git a/draw/line_style.go b/draw/line_style.go index 0e914a7..21eb5fc 100644 --- a/draw/line_style.go +++ b/draw/line_style.go @@ -17,7 +17,7 @@ package draw import ( "fmt" - runewidth "github.com/mattn/go-runewidth" + "github.com/mum4k/termdash/cell/runewidth" ) // line_style.go contains the Unicode characters used for drawing lines of diff --git a/draw/text.go b/draw/text.go index 06f823d..cc92095 100644 --- a/draw/text.go +++ b/draw/text.go @@ -21,9 +21,9 @@ import ( "fmt" "image" - runewidth "github.com/mattn/go-runewidth" "github.com/mum4k/termdash/canvas" "github.com/mum4k/termdash/cell" + "github.com/mum4k/termdash/cell/runewidth" ) // OverrunMode represents diff --git a/widgets/donut/donut.go b/widgets/donut/donut.go index ef3a11a..3e851b2 100644 --- a/widgets/donut/donut.go +++ b/widgets/donut/donut.go @@ -22,10 +22,10 @@ import ( "image" "sync" - runewidth "github.com/mattn/go-runewidth" "github.com/mum4k/termdash/align" "github.com/mum4k/termdash/canvas" "github.com/mum4k/termdash/canvas/braille" + "github.com/mum4k/termdash/cell/runewidth" "github.com/mum4k/termdash/draw" "github.com/mum4k/termdash/numbers" "github.com/mum4k/termdash/terminalapi" diff --git a/widgets/gauge/gauge.go b/widgets/gauge/gauge.go index cafa8fa..53d8ef7 100644 --- a/widgets/gauge/gauge.go +++ b/widgets/gauge/gauge.go @@ -22,11 +22,11 @@ import ( "image" "sync" - runewidth "github.com/mattn/go-runewidth" "github.com/mum4k/termdash/align" "github.com/mum4k/termdash/area" "github.com/mum4k/termdash/canvas" "github.com/mum4k/termdash/cell" + "github.com/mum4k/termdash/cell/runewidth" "github.com/mum4k/termdash/draw" "github.com/mum4k/termdash/terminalapi" "github.com/mum4k/termdash/widgetapi" diff --git a/widgets/sparkline/sparks.go b/widgets/sparkline/sparks.go index f908163..8866ae4 100644 --- a/widgets/sparkline/sparks.go +++ b/widgets/sparkline/sparks.go @@ -20,7 +20,7 @@ package sparkline import ( "fmt" - runewidth "github.com/mattn/go-runewidth" + "github.com/mum4k/termdash/cell/runewidth" "github.com/mum4k/termdash/numbers" ) diff --git a/widgets/text/line_scanner.go b/widgets/text/line_scanner.go index a2bfee7..e2f65af 100644 --- a/widgets/text/line_scanner.go +++ b/widgets/text/line_scanner.go @@ -20,7 +20,7 @@ import ( "strings" "text/scanner" - runewidth "github.com/mattn/go-runewidth" + "github.com/mum4k/termdash/cell/runewidth" ) // wrapNeeded returns true if wrapping is needed for the rune at the horizontal diff --git a/widgets/text/line_trim.go b/widgets/text/line_trim.go index d019a07..46b199e 100644 --- a/widgets/text/line_trim.go +++ b/widgets/text/line_trim.go @@ -18,8 +18,8 @@ import ( "fmt" "image" - runewidth "github.com/mattn/go-runewidth" "github.com/mum4k/termdash/canvas" + "github.com/mum4k/termdash/cell/runewidth" ) // line_trim.go contains code that trims lines that are too long.