Skip to content

Commit

Permalink
feat: add mode 2027 grapheme clustering stubs (#1105)
Browse files Browse the repository at this point in the history
* feat: add mode 2027 grapheme clustering stubs

This adds the necessary stubs to make mode 2027 (grapheme clustering)
work on Bubble Tea with renderers that support it.

* fix: screen grapheme clustering tests
  • Loading branch information
aymanbagabas authored Aug 23, 2024
1 parent 833cc78 commit b2e983a
Show file tree
Hide file tree
Showing 11 changed files with 100 additions and 24 deletions.
2 changes: 1 addition & 1 deletion examples/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymanbagabas/go-udiff v0.2.0 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/charmbracelet/x/ansi v0.2.2 // indirect
github.com/charmbracelet/x/ansi v0.2.3 // indirect
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b // indirect
github.com/charmbracelet/x/term v0.2.0 // indirect
github.com/charmbracelet/x/windows v0.2.0 // indirect
Expand Down
8 changes: 4 additions & 4 deletions examples/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ github.com/charmbracelet/glamour v0.8.0 h1:tPrjL3aRcQbn++7t18wOpgLyl8wrOHUEDS7IZ
github.com/charmbracelet/glamour v0.8.0/go.mod h1:ViRgmKkf3u5S7uakt2czJ272WSg2ZenlYEZXT2x7Bjw=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw=
github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY=
github.com/charmbracelet/x/ansi v0.2.2 h1:BC7xzaVpfWIYZRNE8NhO9zo8KA4eGUL6L/JWXDh3GF0=
github.com/charmbracelet/x/ansi v0.2.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/charmbracelet/lipgloss v0.12.1 h1:/gmzszl+pedQpjCOH+wFkZr/N90Snz40J/NR7A0zQcs=
github.com/charmbracelet/lipgloss v0.12.1/go.mod h1:V2CiwIuhx9S1S1ZlADfOj9HmxeMAORuz5izHb0zGbB8=
github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY=
github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q=
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/teatest v0.0.0-20240521184646-23081fb03b28 h1:sOWKNRjt8uOEVgPiJVIJCse1+mUDM2F/vYY6W0Go640=
Expand Down
2 changes: 1 addition & 1 deletion examples/simple/testdata/TestApp.golden
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[?25l[?2004hHi. This program will exit in 10 seconds.
[?25l[?2004h[?2027h[?2027$pHi. This program will exit in 10 seconds.

To quit sooner press ctrl-c, or press ctrl-z to suspend...
Hi. This program will exit in 9 seconds.
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ module github.com/charmbracelet/bubbletea
go 1.18

require (
github.com/charmbracelet/x/ansi v0.2.3
github.com/charmbracelet/lipgloss v0.13.0
github.com/charmbracelet/x/ansi v0.2.2
github.com/charmbracelet/x/term v0.2.0
github.com/charmbracelet/x/windows v0.2.0
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY=
github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw=
github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY=
github.com/charmbracelet/x/ansi v0.2.2 h1:BC7xzaVpfWIYZRNE8NhO9zo8KA4eGUL6L/JWXDh3GF0=
github.com/charmbracelet/x/ansi v0.2.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0=
github.com/charmbracelet/x/windows v0.2.0 h1:ilXA1GJjTNkgOm94CLPeSz7rar54jtFatdmoiONPuEw=
Expand Down
15 changes: 15 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -327,3 +327,18 @@ func _WithWindowsInputMode() ProgramOption { //nolint:unused
p.win32Input = true
}
}

// WithoutGraphemeClustering disables grapheme clustering. This is useful if you
// want to disable grapheme clustering for your program.
//
// Grapheme clustering is a character width calculation method that accurately
// calculates the width of wide characters in a terminal. This is useful for
// properly rendering double width characters such as emojis and CJK
// characters.
//
// See https://mitchellh.com/writing/grapheme-clusters-in-terminals
func WithoutGraphemeClustering() ProgramOption {
return func(p *Program) {
p.startupOptions |= withoutGraphemeClustering
}
}
5 changes: 3 additions & 2 deletions renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ type repaintMsg struct{}

// Terminal modes used by SetMode and Mode in Bubble Tea.
const (
altScreenMode = 1049
hideCursor = 25
graphemeClustering = 2027
altScreenMode = 1049
hideCursor = 25
)
21 changes: 21 additions & 0 deletions screen.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,27 @@ func DisableBracketedPaste() Msg {
// disableBracketedPasteMsg with DisableBracketedPaste.
type disableBracketedPasteMsg struct{}

// enableGraphemeClusteringMsg is an internal message that signals that
// grapheme clustering should be enabled.
type enableGraphemeClusteringMsg struct{}

// EnableGraphemeClustering is a special command that tells the Bubble Tea
// program to enable grapheme clustering. This is enabled by default.
func EnableGraphemeClustering() Msg {
return enableGraphemeClusteringMsg{}
}

// disableGraphemeClusteringMsg is an internal message that signals that
// grapheme clustering should be disabled.
type disableGraphemeClusteringMsg struct{}

// DisableGraphemeClustering is a special command that tells the Bubble Tea
// program to disable grapheme clustering. This mode will be disabled
// automatically when the program quits.
func DisableGraphemeClustering() Msg {
return disableGraphemeClusteringMsg{}
}

// enableReportFocusMsg is an internal message that signals that focus
// reporting should be enabled.
type enableReportFocusMsg struct{}
Expand Down
26 changes: 13 additions & 13 deletions screen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,67 +15,67 @@ func TestClearMsg(t *testing.T) {
{
name: "clear_screen",
cmds: []Cmd{ClearScreen},
expected: "\x1b[?25l\x1b[?2004h\x1b[2J\x1b[1;1H\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h",
expected: "\x1b[?25l\x1b[?2004h\x1b[?2027h\x1b[?2027$p\x1b[2J\x1b[1;1H\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h",
},
{
name: "altscreen",
cmds: []Cmd{EnterAltScreen, ExitAltScreen},
expected: "\x1b[?25l\x1b[?2004h\x1b[?1049h\x1b[2J\x1b[1;1H\x1b[?25l\x1b[?1049l\x1b[?25l\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h",
expected: "\x1b[?25l\x1b[?2004h\x1b[?2027h\x1b[?2027$p\x1b[?1049h\x1b[2J\x1b[1;1H\x1b[?25l\x1b[?1049l\x1b[?25l\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h",
},
{
name: "altscreen_autoexit",
cmds: []Cmd{EnterAltScreen},
expected: "\x1b[?25l\x1b[?2004h\x1b[?1049h\x1b[2J\x1b[1;1H\x1b[?25l\rsuccess\r\n\x1b[2;0H\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1049l\x1b[?25h",
expected: "\x1b[?25l\x1b[?2004h\x1b[?2027h\x1b[?2027$p\x1b[?1049h\x1b[2J\x1b[1;1H\x1b[?25l\rsuccess\r\n\x1b[2;0H\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1049l\x1b[?25h",
},
{
name: "mouse_cellmotion",
cmds: []Cmd{EnableMouseCellMotion},
expected: "\x1b[?25l\x1b[?2004h\x1b[?1002h\x1b[?1006h\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
expected: "\x1b[?25l\x1b[?2004h\x1b[?2027h\x1b[?2027$p\x1b[?1002h\x1b[?1006h\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
},
{
name: "mouse_allmotion",
cmds: []Cmd{EnableMouseAllMotion},
expected: "\x1b[?25l\x1b[?2004h\x1b[?1003h\x1b[?1006h\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
expected: "\x1b[?25l\x1b[?2004h\x1b[?2027h\x1b[?2027$p\x1b[?1003h\x1b[?1006h\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
},
{
name: "mouse_disable",
cmds: []Cmd{EnableMouseAllMotion, DisableMouse},
expected: "\x1b[?25l\x1b[?2004h\x1b[?1003h\x1b[?1006h\x1b[?1002l\x1b[?1003l\x1b[?1006l\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h",
expected: "\x1b[?25l\x1b[?2004h\x1b[?2027h\x1b[?2027$p\x1b[?1003h\x1b[?1006h\x1b[?1002l\x1b[?1003l\x1b[?1006l\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h",
},
{
name: "cursor_hide",
cmds: []Cmd{HideCursor},
expected: "\x1b[?25l\x1b[?2004h\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h",
expected: "\x1b[?25l\x1b[?2004h\x1b[?2027h\x1b[?2027$p\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h",
},
{
name: "cursor_hideshow",
cmds: []Cmd{HideCursor, ShowCursor},
expected: "\x1b[?25l\x1b[?2004h\x1b[?25h\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l",
expected: "\x1b[?25l\x1b[?2004h\x1b[?2027h\x1b[?2027$p\x1b[?25h\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l",
},
{
name: "bp_stop_start",
cmds: []Cmd{DisableBracketedPaste, EnableBracketedPaste},
expected: "\x1b[?25l\x1b[?2004h\x1b[?2004l\x1b[?2004h\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h",
expected: "\x1b[?25l\x1b[?2004h\x1b[?2027h\x1b[?2027$p\x1b[?2004l\x1b[?2004h\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h",
},
{
name: "read_set_clipboard",
cmds: []Cmd{ReadClipboard, SetClipboard("success")},
expected: "\x1b[?25l\x1b[?2004h\x1b]52;c;?\a\x1b]52;c;c3VjY2Vzcw==\a\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h",
expected: "\x1b[?25l\x1b[?2004h\x1b[?2027h\x1b[?2027$p\x1b]52;c;?\a\x1b]52;c;c3VjY2Vzcw==\a\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h",
},
{
name: "bg_fg_cur_color",
cmds: []Cmd{ForegroundColor, BackgroundColor, CursorColor},
expected: "\x1b[?25l\x1b[?2004h\x1b]10;?\a\x1b]11;?\a\x1b]12;?\a\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h",
expected: "\x1b[?25l\x1b[?2004h\x1b[?2027h\x1b[?2027$p\x1b]10;?\a\x1b]11;?\a\x1b]12;?\a\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h",
},
{
name: "bg_set_color",
cmds: []Cmd{SetBackgroundColor(color.RGBA{255, 255, 255, 255})},
expected: "\x1b[?25l\x1b[?2004h\x1b]11;#ffffff\a\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h",
expected: "\x1b[?25l\x1b[?2004h\x1b[?2027h\x1b[?2027$p\x1b]11;#ffffff\a\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h",
},
{
name: "kitty_start",
cmds: []Cmd{disableKittyKeyboard, enableKittyKeyboard(3)},
expected: "\x1b[?25l\x1b[?2004h\x1b[>u\x1b[>3u\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[>0u",
expected: "\x1b[?25l\x1b[?2004h\x1b[?2027h\x1b[?2027$p\x1b[>u\x1b[>3u\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[>0u",
},
}

Expand Down
36 changes: 36 additions & 0 deletions tea.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ const (
withKittyKeyboard
withModifyOtherKeys
withWindowsInputMode
withoutGraphemeClustering
)

// channelHandlers manages the series of channels returned by various processes.
Expand Down Expand Up @@ -174,6 +175,8 @@ type Program struct {

bpActive bool // was the bracketed paste mode active before releasing the terminal?

graphemeClustering bool // whether grapheme clustering is enabled

cursorHidden bool // the cursor visibility state

mouseEnabled bool // whether mouse reporting is enabled
Expand Down Expand Up @@ -390,6 +393,16 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
p.suspend()
}

case ReportModeMsg:
switch msg.Mode {
case graphemeClustering:
// 1 means mode is set (see DECRPM).
p.graphemeClustering = msg.Value == 1
if p.graphemeClustering {
p.renderer.SetMode(graphemeClustering, true)
}
}

case clearScreenMsg:
p.renderer.ClearScreen()

Expand Down Expand Up @@ -428,6 +441,19 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
p.execute(ansi.DisableBracketedPaste)
p.bpActive = false

case enableGraphemeClusteringMsg:
p.execute(ansi.EnableGraphemeClustering)
p.execute(ansi.RequestGraphemeClustering)
// We store the state of grapheme clustering after we enable it
// and get a response in the eventLoop.

case disableGraphemeClusteringMsg:
if p.graphemeClustering {
// We only disable grapheme clustering if it was enabled.
p.execute(ansi.DisableGraphemeClustering)
p.renderer.SetMode(graphemeClustering, false)
}

case enableReportFocusMsg:
p.execute(ansi.EnableReportFocus)
p.reportFocus = true
Expand Down Expand Up @@ -701,6 +727,12 @@ func (p *Program) Run() (Model, error) {
p.execute(ansi.EnableBracketedPaste)
p.bpActive = true
}
if p.startupOptions&withoutGraphemeClustering == 0 {
p.execute(ansi.EnableGraphemeClustering)
p.execute(ansi.RequestGraphemeClustering)
// We store the state of grapheme clustering after we query it and get
// a response in the eventLoop.
}
if p.startupOptions&withMouseCellMotion != 0 {
p.execute(ansi.EnableMouseCellMotion)
p.execute(ansi.EnableMouseSgrExt)
Expand Down Expand Up @@ -868,6 +900,7 @@ func (p *Program) ReleaseTerminal() error {

if p.renderer != nil {
p.stopRenderer(false)
// TODO: store these values when they're set in the eventLoop and [Run].
p.altScreenWasActive = p.renderer.Mode(altScreenMode)
p.cursorHidden = p.renderer.Mode(hideCursor)
}
Expand Down Expand Up @@ -925,6 +958,9 @@ func (p *Program) RestoreTerminal() error {
p.execute(ansi.EnableMouseSgrExt)
}
}
if p.graphemeClustering {
p.execute(ansi.EnableGraphemeClustering)
}

// If the output is a terminal, it may have been resized while another
// process was at the foreground, in which case we may not have received
Expand Down
3 changes: 3 additions & 0 deletions tty.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ func (p *Program) restoreTerminalState() error {
if p.reportFocus {
p.execute(ansi.DisableReportFocus)
}
if p.graphemeClustering {
p.execute(ansi.DisableGraphemeClustering)
}

if p.renderer != nil {
if p.renderer.Mode(altScreenMode) {
Expand Down

0 comments on commit b2e983a

Please sign in to comment.