Skip to content

Commit

Permalink
caps: implement explicit width support
Browse files Browse the repository at this point in the history
Implement explicit width support based on the spec developed by kitty.
In the presence of both mode 2027 and explicit width, we opt for
explicit width

Reference: kovidgoyal/kitty#8226
  • Loading branch information
rockorager committed Feb 3, 2025
1 parent e892dbd commit 3e2bf52
Show file tree
Hide file tree
Showing 6 changed files with 47 additions and 27 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ most dank TUI library
| Synchronized Output (DEC 2026) |||||
| Unicode Core (DEC 2027) |||||
| Color Mode Updates (DEC 2031) |||||
| Explicit Width |||||
| Images (full/space) |||||
| Images (half block) |||||
| Images (quadrant) |||||
Expand Down
2 changes: 2 additions & 0 deletions quirks.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@ func (vx *Vaxis) applyQuirks() {
}
if os.Getenv("VAXIS_FORCE_WCWIDTH") != "" {
vx.caps.unicodeCore = false
vx.caps.explicitWidth = false
}
if os.Getenv("VAXIS_FORCE_UNICODE") != "" {
vx.caps.unicodeCore = true
}
if os.Getenv("VAXIS_FORCE_NOZWJ") != "" {
vx.caps.noZWJ = true
vx.caps.explicitWidth = false
}
if os.Getenv("VAXIS_DISABLE_NOZWJ") != "" {
vx.caps.noZWJ = false
Expand Down
25 changes: 13 additions & 12 deletions sequences.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,19 @@ const (
xtsmSixelGeom = "\x1b[?2;1;0S"

// Misc
clear = "\x1b[H\x1b[2J"
cup = "\x1B[%d;%dH"
osc8 = "\x1b]8;%s;%s\x1b\\"
osc11 = "\x1b]11;?\x07"
osc52put = "\x1b]52;c;%s\x1b\\"
osc52pop = "\x1b]52;c;?\x1b\\"
osc9notify = "\x1b]9;%s\x1b\\"
osc777notify = "\x1b]777;notify;%s;%s\x1b\\"
setTitle = "\x1b]2;%s\x1b\\"
getAppID = "\x1b]176;?\x1b\\"
setAppID = "\x1b]176;%s\x1b\\"
mouseShape = "\x1b]22;%s\x1b\\"
clear = "\x1b[H\x1b[2J"
cup = "\x1B[%d;%dH"
osc8 = "\x1b]8;%s;%s\x1b\\"
osc11 = "\x1b]11;?\x07"
osc52put = "\x1b]52;c;%s\x1b\\"
osc52pop = "\x1b]52;c;?\x1b\\"
osc9notify = "\x1b]9;%s\x1b\\"
osc777notify = "\x1b]777;notify;%s;%s\x1b\\"
setTitle = "\x1b]2;%s\x1b\\"
getAppID = "\x1b]176;?\x1b\\"
setAppID = "\x1b]176;%s\x1b\\"
mouseShape = "\x1b]22;%s\x1b\\"
explicitWidth = "\x1b]66;w=%d;%s\x1b\\"

// SGR
sgrReset = "\x1b[m"
Expand Down
2 changes: 1 addition & 1 deletion styled_string.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ func (vx *Vaxis) NewStyledString(s string, defaultStyle Style) *StyledString {
default:
grapheme, s, width, _ = uniseg.FirstGraphemeClusterInString(s, -1)
switch {
case vx.caps.unicodeCore:
case vx.caps.unicodeCore || vx.caps.explicitWidth:
// we're done
case vx.caps.noZWJ:
width = gwidth(grapheme, noZWJ)
Expand Down
32 changes: 26 additions & 6 deletions vaxis.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type capabilities struct {
osc11 bool
osc176 bool
inBandResize bool
explicitWidth bool
}

type cursorState struct {
Expand Down Expand Up @@ -710,9 +711,11 @@ outerNew:
next.Width = vx.characterWidth(next.Grapheme)
}

switch next.Width {
case 0:
switch {
case next.Width == 0:
_, _ = vx.tw.WriteString(" ")
case next.Width > 1 && vx.caps.explicitWidth:
_, _ = fmt.Fprintf(vx.tw, explicitWidth, next.Width, next.Grapheme)
default:
_, _ = vx.tw.WriteString(next.Grapheme)
}
Expand Down Expand Up @@ -1090,6 +1093,17 @@ func (vx *Vaxis) sendQueries() {
// Can the terminal report it's own size?
_, _ = vx.tw.WriteString(textAreaSize)

// Explici width query
_, _ = vx.tw.WriteString("\x1b[H")
_, _ = fmt.Fprintf(vx.tw, explicitWidth, 1, " ")
_, col := vx.CursorPosition()
if col == 1 {
log.Debug("[capability] explicit width supported")
vx.mu.Lock()
vx.caps.explicitWidth = true
vx.mu.Unlock()
}

// Query some terminfo capabilities
// Just another way to see if we have RGB support
_, _ = vx.tw.WriteString(xtgettcap("RGB"))
Expand Down Expand Up @@ -1121,8 +1135,9 @@ func (vx *Vaxis) enableModes() {
if vx.caps.sixels {
_, _ = vx.tw.WriteString(decset(sixelScrolling))
}
// Mode 2027, unicode segmentation (for correct emoji/wc widths)
if vx.caps.unicodeCore {
// Mode 2027, unicode segmentation (for correct emoji/wc widths). We
// only enable if we don't also have explicitWidth
if vx.caps.unicodeCore && !vx.caps.explicitWidth {
_, _ = vx.tw.WriteString(decset(unicodeCore))
}

Expand Down Expand Up @@ -1171,7 +1186,7 @@ func (vx *Vaxis) disableModes() {
if vx.caps.sixels {
_, _ = vx.tw.WriteString(decrst(sixelScrolling))
}
if vx.caps.unicodeCore {
if vx.caps.unicodeCore && !vx.caps.explicitWidth {
_, _ = vx.tw.WriteString(decrst(unicodeCore))
}
if vx.caps.colorThemeUpdates {
Expand Down Expand Up @@ -1442,10 +1457,11 @@ func (vx *Vaxis) advance(cell Cell) int {
// This call can be expensive, callers should consider caching the result for
// strings or characters which will need to be measured frequently
func (vx *Vaxis) RenderedWidth(s string) int {
if vx.caps.unicodeCore {
if vx.caps.unicodeCore || vx.caps.explicitWidth {
return gwidth(s, unicodeStd)
}
if vx.caps.noZWJ {
log.Debug("nozwj")
return gwidth(s, noZWJ)
}
return gwidth(s, wcwidth)
Expand Down Expand Up @@ -1502,6 +1518,10 @@ func (vx *Vaxis) CanUnicodeCore() bool {
return vx.caps.unicodeCore
}

func (vx *Vaxis) CanExplicitWidth() bool {
return vx.caps.explicitWidth
}

func (vx *Vaxis) nextGraphicID() uint64 {
vx.graphicsIDNext += 1
return vx.graphicsIDNext
Expand Down
12 changes: 4 additions & 8 deletions window.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ func (win Window) Print(segs ...Segment) (col int, row int) {
if row > rows {
return col, row
}
if !win.Vx.caps.unicodeCore {
if !win.Vx.caps.unicodeCore || !win.Vx.caps.explicitWidth {
// characterWidth will cache the result
char.Width = win.Vx.characterWidth(char.Grapheme)
}
Expand Down Expand Up @@ -203,7 +203,7 @@ func (win Window) PrintTruncate(row int, segs ...Segment) {
}
for _, seg := range segs {
for _, char := range Characters(seg.Text) {
if !win.Vx.caps.unicodeCore {
if !win.Vx.caps.unicodeCore || !win.Vx.caps.explicitWidth {
// characterWidth will cache the result
char.Width = win.Vx.characterWidth(char.Grapheme)
}
Expand Down Expand Up @@ -237,18 +237,14 @@ func (win Window) Println(row int, segs ...Segment) {
col := 0
for _, seg := range segs {
for _, char := range Characters(seg.Text) {
if !win.Vx.caps.unicodeCore {
if !win.Vx.caps.unicodeCore || !win.Vx.caps.explicitWidth {
// characterWidth will cache the result
char.Width = win.Vx.characterWidth(char.Grapheme)
}
w := char.Width
if col+w > cols {
return
}
if !win.Vx.caps.unicodeCore {
// characterWidth will cache the result
win.Vx.characterWidth(char.Grapheme)
}
cell := Cell{
Character: char,
Style: seg.Style,
Expand Down Expand Up @@ -277,7 +273,7 @@ func (win Window) Wrap(segs ...Segment) (col int, row int) {
chars := Characters(segment)
total := 0
for _, char := range chars {
if !win.Vx.caps.unicodeCore {
if !win.Vx.caps.unicodeCore || !win.Vx.caps.explicitWidth {
// characterWidth will cache the result
char.Width = win.Vx.characterWidth(char.Grapheme)
}
Expand Down

0 comments on commit 3e2bf52

Please sign in to comment.