Skip to content

Commit

Permalink
✨ Add terminal package
Browse files Browse the repository at this point in the history
As part of the upcoming dependency upgrade, there will be changes to
the supported Unicode version. This change will cause some emojis
(grapheme clusters) to report different string widths than previously.
This will impact the layout especially when there are borders.

The terminal package will have the role of handling compatibility
issues between the terminal and packages used to determine string
width.

The first functionality added to this package is to allow the
overriding of the width of specific grapheme clusters (a selection
using variant select 16). This will provide compatibility for any
terminal that does not currently support Unicode 14 or above.

For overriding of grapheme cluster width, a fork of `uniseg` was
necessary to allow for an replacement map.

While the width calculcation changes are applied when the UI is
configured, no packages are currently using the `uniseg` functions so
none of the overrides apply. Once the dependency upgrade is completed,
this will provide the same results as the previous string width
functions.
  • Loading branch information
mikelorant committed Mar 6, 2024
1 parent 1b726c8 commit 32f6bad
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 4 deletions.
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ require (
github.com/muesli/gamut v0.3.1
github.com/muesli/reflow v0.3.0
github.com/muesli/termenv v0.14.0
github.com/rivo/uniseg v0.4.7
github.com/spf13/cobra v1.6.1
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.1
Expand All @@ -33,6 +34,7 @@ replace (
github.com/charmbracelet/bubbles => github.com/mikelorant/bubbles v0.0.0-20221206230145-c5687de7af43
github.com/charmbracelet/lipgloss => github.com/mikelorant/lipgloss v0.0.0-20230212060525-24ffefde7d62
github.com/muesli/reflow => github.com/mikelorant/reflow v0.0.0-20230112022445-408368584af4
github.com/rivo/uniseg => github.com/mikelorant/uniseg v0.0.0-20240303073632-27c86650ffa5
)

require (
Expand Down Expand Up @@ -67,7 +69,6 @@ require (
github.com/nightlyone/lockfile v1.0.0 // indirect
github.com/pjbgf/sha1cd v0.2.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.3 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
github.com/sahilm/fuzzy v0.1.0 // indirect
github.com/sergi/go-diff v1.3.1 // indirect
Expand Down
5 changes: 2 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ github.com/mikelorant/lipgloss v0.0.0-20230212060525-24ffefde7d62 h1:2qFaTxCkNSX
github.com/mikelorant/lipgloss v0.0.0-20230212060525-24ffefde7d62/go.mod h1:epOnQm/VM6q8Z5R+TR+EYI1ZgEULOU7eUR8DapUVFMc=
github.com/mikelorant/reflow v0.0.0-20230112022445-408368584af4 h1:yIT0phMB7rOQ7ZZ3KaTlRv/xi/mhvFApL/+7S0U6Hl0=
github.com/mikelorant/reflow v0.0.0-20230112022445-408368584af4/go.mod h1:mEMWZ0nzoGlTCHkXp5ljOWhHi1tjvtDGh7wuT1Thhsk=
github.com/mikelorant/uniseg v0.0.0-20240303073632-27c86650ffa5 h1:CRvVC4vrkEti+XI0DdksOAspVl0/9xG4BLF/bemiKfM=
github.com/mikelorant/uniseg v0.0.0-20240303073632-27c86650ffa5/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a h1:jlDOeO5TU0pYlbc/y6PFguab5IjANI0Knrpg3u/ton4=
github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
Expand All @@ -161,9 +163,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw=
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
Expand Down
68 changes: 68 additions & 0 deletions internal/terminal/terminal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package terminal

import (
"github.com/mikelorant/committed/internal/config"

"github.com/rivo/uniseg"
)

type graphemes struct {
codepoints []rune
width int
}

func Set(c config.Compatibility) {
uniseg.GraphemeClusterWidthOverrides = overrideGraphemeClusterWidth(c)
}

func Clear() {
uniseg.GraphemeClusterWidthOverrides = nil
}

func overrideGraphemeClusterWidth(c config.Compatibility) map[string]int {
gs := make([]graphemes, 0)

switch c {
case config.CompatibilityTtyd:
case config.CompatibilityKitty:
default:
gs = append(gs, overrideVS16()...)
}

return overrides(gs)
}

// Grapheme clusters using variant selector 16
// had their widths changed as part of Unicode 14.
// Unicode < 14 = 1.
// Unicode >= 14 = 2.
// Required for:
// - macOS Terminal (2.12.7)
// - iTerm2 (3.4.23)
// - VSCode (1.87.0)
// - Alacritty (0.13.1)
// - WezTerm (20240203)
func overrideVS16() []graphemes {
return []graphemes{
{codepoints: []rune{0x203c, 0xfe0f}, width: 1}, // ‼️
{codepoints: []rune{0x21a9, 0xfe0f}, width: 1}, // ↩️
{codepoints: []rune{0x2601, 0xfe0f}, width: 1}, // ☁️
{codepoints: []rune{0x267b, 0xfe0f}, width: 1}, // ♻️
{codepoints: []rune{0x2697, 0xfe0f}, width: 1}, // ⚗️
{codepoints: []rune{0x2699, 0xfe0f}, width: 1}, // ⚙️
{codepoints: []rune{0x26b0, 0xfe0f}, width: 1}, // ⚰️
{codepoints: []rune{0x270f, 0xfe0f}, width: 1}, // ✏️
{codepoints: []rune{0x2b06, 0xfe0f}, width: 1}, // ⬆️
{codepoints: []rune{0x2b07, 0xfe0f}, width: 1}, // ⬇️
}
}

func overrides(gs []graphemes) map[string]int {
overrides := make(map[string]int, len(gs))
for _, g := range gs {
key := string(g.codepoints)
overrides[key] = g.width
}

return overrides
}
146 changes: 146 additions & 0 deletions internal/terminal/terminal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package terminal_test

import (
"testing"

"github.com/mikelorant/committed/internal/config"
"github.com/mikelorant/committed/internal/terminal"

"github.com/rivo/uniseg"
"github.com/stretchr/testify/assert"
)

func TestSet(t *testing.T) {
tests := []struct {
name string
compat config.Compatibility
value string
width int
}{
{
name: "default empty",
width: 0,
},
{
name: "default simple",
value: "test",
width: 4,
},
{
name: "default single emoji without override",
value: "❤️",
width: 2,
},
{
name: "default single emoji with override",
value: "⬆️",
width: 1,
},
{
name: "default multiple emojis without override",
value: "❤️❤️",
width: 4,
},
{
name: "default multiple emojis with override",
value: "⬆️⬆️",
width: 2,
},
{
name: "default mixed emojis",
value: "⬆️❤️",
width: 3,
},
{
name: "default multiple mixed emojis",
value: "⬆️❤️❤️⬆️",
width: 6,
},
{
name: "ttyd empty",
compat: config.CompatibilityTtyd,
value: "",
width: 0,
},
{
name: "ttyd simple",
compat: config.CompatibilityTtyd,
value: "test",
width: 4,
},
{
name: "ttyd multiple emojis",
compat: config.CompatibilityTtyd,
value: "⬆️❤️",
width: 4,
},
{
name: "kitty empty",
compat: config.CompatibilityKitty,
value: "",
width: 0,
},
{
name: "ttyd simple",
compat: config.CompatibilityKitty,
value: "test",
width: 4,
},
{
name: "ttyd multiple emojis",
compat: config.CompatibilityKitty,
value: "⬆️❤️",
width: 4,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
terminal.Set(tt.compat)

width := uniseg.StringWidth(tt.value)
assert.Equal(t, tt.width, width)

terminal.Clear()
})
}
}

func TestClear(t *testing.T) {
tests := []struct {
name string
value string
width int
orWidth int
}{
{
name: "default without override",
value: "❤️",
width: 2,
orWidth: 2,
},
{
name: "default with override",
value: "⬆️",
width: 2,
orWidth: 1,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
width := uniseg.StringWidth(tt.value)
assert.Equal(t, tt.width, width)

terminal.Set(config.CompatibilityDefault)

width = uniseg.StringWidth(tt.value)
assert.Equal(t, tt.orWidth, width)

terminal.Clear()

width = uniseg.StringWidth(tt.value)
assert.Equal(t, tt.width, width)
})
}
}
3 changes: 3 additions & 0 deletions internal/ui/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/mikelorant/committed/internal/commit"
"github.com/mikelorant/committed/internal/config"
"github.com/mikelorant/committed/internal/emoji"
"github.com/mikelorant/committed/internal/terminal"
"github.com/mikelorant/committed/internal/ui/body"
"github.com/mikelorant/committed/internal/ui/colour"
"github.com/mikelorant/committed/internal/ui/footer"
Expand Down Expand Up @@ -494,6 +495,8 @@ func (m *Model) resetCursor() {
}

func (m *Model) setCompatibility() {
terminal.Set(m.state.Config.View.Compatibility)

switch m.state.Config.View.Compatibility {
case config.CompatibilityTtyd:
os.Setenv("LIPGLOSS_TERMINAL", "ttyd")
Expand Down

0 comments on commit 32f6bad

Please sign in to comment.