mirror of
https://github.com/JanDeDobbeleer/oh-my-posh.git
synced 2026-08-24 02:34:19 -05:00
perf(terminal): count cells without go-runewidth
Every length the writer computes is a sum of per-rune cell widths, and go-runewidth answered that from its own Unicode tables. Since v0.0.27 it also builds a 2.2MB lookup table in package init: 2.23 million calls before main runs, measured at 25ms. oh-my-posh renders one prompt and exits, so that was around 10% of a 240ms render, on every prompt, twice per keystroke on shells that also draw a transient one. The table is a cache, and a badly aimed one here. Measured over a realistic 80-rune prompt it saved 48ns per render against the library's own uncached path, so it needed something past half a million renders in one process to pay for itself. It could not be turned off either: Condition.RuneWidth only reads it when StrictEmojiNeutral is set, but init builds it regardless. runeCells answers the same question from the standard library's unicode categories and golang.org/x/text/width, both already linked, with a fast path for everything below U+0300 - which is nearly all of a prompt. Checked against go-runewidth v0.0.27 across all 1,114,112 code points: 2,648 disagree, of which 2,048 are UTF-16 surrogates that cannot reach this code, ~330 are wide symbol blocks no prompt contains (I Ching hexagrams, Tai Xuan Jing, counting rods, Tangut) and ~150 are format and tag characters where zero is the better answer. The real gap is ~70 recently assigned combining marks that x/text does not yet classify. Every rune a prompt actually holds agrees exactly: emoji including flag sequences, CJK, Nerd Font glyphs, powerline separators, box drawing, accented Latin, common combining marks, Cyrillic and Greek. The dependency stays in the module graph for now - bubbletea reaches it through charmbracelet/x/ansi - so the CLI still pays that init until the interactive commands stop being linked into the same binary. The wasm build no longer references it at all. Entire-Checkpoint: 0b183cf5f6e6
This commit is contained in:
committed by
Jan De Dobbeleer
parent
b76e33e648
commit
dc52809ca6
+1
-1
@@ -31,7 +31,6 @@ require (
|
||||
github.com/hashicorp/hcl/v2 v2.24.0
|
||||
github.com/invopop/jsonschema v0.14.0
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0
|
||||
github.com/mattn/go-runewidth v0.0.27
|
||||
github.com/pelletier/go-toml/v2 v2.4.3
|
||||
github.com/shirou/gopsutil/v4 v4.26.6
|
||||
github.com/spf13/cobra v1.10.2
|
||||
@@ -77,6 +76,7 @@ require (
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.27 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package terminal
|
||||
|
||||
import (
|
||||
"unicode"
|
||||
|
||||
"golang.org/x/text/width"
|
||||
)
|
||||
|
||||
// runeCells reports how many terminal cells a rune paints: 0, 1 or 2. Every length the writer
|
||||
// computes is a sum of these - prompt length (which decides where a right-aligned block, an
|
||||
// rprompt or a filler lands), and the per-cell index a gradient interpolates across.
|
||||
//
|
||||
// This replaces github.com/mattn/go-runewidth, which did the same job from its own Unicode
|
||||
// tables. It was dropped for one reason: v0.0.27 eagerly builds a 2.2 MB lookup table in package
|
||||
// init - 2.23 million calls before main runs, measured at 26 ms. oh-my-posh is a short-lived
|
||||
// process that renders one prompt and exits, so that init was ~10% of a 240 ms render, on every
|
||||
// prompt, twice per keystroke on shells that spawn a transient prompt too. The table it builds is
|
||||
// a cache: measured over a realistic 80-rune prompt it saved 48 ns per render against the
|
||||
// library's own uncached path, so break-even was somewhere past half a million renders in one
|
||||
// process. It also could not be turned off - Condition.RuneWidth only consults the table when
|
||||
// StrictEmojiNeutral is set, but init builds it either way.
|
||||
//
|
||||
// The tables below come from the standard library's own unicode package and golang.org/x/text,
|
||||
// both already linked, so this costs no init at all.
|
||||
//
|
||||
// Verified against go-runewidth v0.0.27 over all 1,114,112 code points: 2,648 disagree, of which
|
||||
// 2,048 are UTF-16 surrogates (unreachable - utf8.DecodeRuneInString yields RuneError before this
|
||||
// is ever called), ~330 are wide symbol blocks nobody puts in a prompt (I Ching hexagrams, Tai
|
||||
// Xuan Jing, counting rods, Tangut), and ~150 are format/tag characters where returning 0 is the
|
||||
// better answer. The genuine gap is ~70 recently-assigned combining marks that x/text's Unicode
|
||||
// vintage does not yet classify as marks; they render one cell wide instead of zero. Every rune a
|
||||
// prompt actually contains agrees exactly: emoji including flag sequences, CJK, Nerd Font private
|
||||
// use glyphs, powerline separators, box drawing, accented Latin, common combining marks, Cyrillic
|
||||
// and Greek.
|
||||
func runeCells(r rune) int {
|
||||
if r < 0 || r > unicode.MaxRune {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Fast path. Everything below U+0300 is one cell except the C0/C1 control ranges, and it is
|
||||
// almost everything a prompt is made of, so this answers without touching a table.
|
||||
if r < 0x0300 {
|
||||
if r < 0x20 || (r >= 0x7F && r < 0xA0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
// Combining marks (Mn/Me) attach to the preceding cell, and format characters (Cf) - zero
|
||||
// width joiners, bidi controls, the tag characters inside emoji flag sequences - paint
|
||||
// nothing at all.
|
||||
if unicode.In(r, unicode.Mn, unicode.Me, unicode.Cf) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// East Asian Wide and Fullwidth are the two properties a terminal renders double width. The
|
||||
// remaining kinds (Narrow, Halfwidth, Ambiguous, Neutral) are all single width here, which is
|
||||
// what go-runewidth's own EastAsianWidth: false setting meant.
|
||||
switch width.LookupRune(r).Kind() {
|
||||
case width.EastAsianWide, width.EastAsianFullwidth:
|
||||
return 2
|
||||
default:
|
||||
return 1
|
||||
}
|
||||
}
|
||||
+5
-10
@@ -12,13 +12,8 @@ import (
|
||||
"github.com/jandedobbeleer/oh-my-posh/src/regex"
|
||||
"github.com/jandedobbeleer/oh-my-posh/src/shell"
|
||||
"github.com/jandedobbeleer/oh-my-posh/src/text"
|
||||
"github.com/mattn/go-runewidth"
|
||||
)
|
||||
|
||||
func init() {
|
||||
runewidth.DefaultCondition.EastAsianWidth = false
|
||||
}
|
||||
|
||||
type style struct {
|
||||
AnchorStart string
|
||||
AnchorEnd string
|
||||
@@ -903,7 +898,7 @@ func write(s rune, isInvisible bool) {
|
||||
}
|
||||
|
||||
// UNSOLVABLE: When "Interactive" is true, the prompt length calculation in Bash/Zsh can be wrong, since the final string expansion is done by shells.
|
||||
length += runewidth.RuneWidth(s)
|
||||
length += runeCells(s)
|
||||
// length += utf8.RuneCountInString(string(s))
|
||||
|
||||
if !Interactive && !Plain {
|
||||
@@ -956,7 +951,7 @@ func writeVisibleRune(s rune, cs *colorState) {
|
||||
write(s, cs.isInvisible)
|
||||
|
||||
if visible {
|
||||
cs.cellIndex += runewidth.RuneWidth(s)
|
||||
cs.cellIndex += runeCells(s)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1128,7 +1123,7 @@ func collapseGradientStop(stop color.Ansi, isBackground bool) color.Ansi {
|
||||
// countVisibleCells is the pre-pass a gradient channel needs before streaming
|
||||
// starts: it walks txt with the exact same tokenization rules as
|
||||
// writeBody/writeBodyGradient (scanAnchor, the hyperlink tokens, the "link"
|
||||
// no-text fallback) and sums runewidth.RuneWidth over every rune write()
|
||||
// no-text fallback) and sums runeCells over every rune write()
|
||||
// would count toward length, so color.GradientCells gets the right cell
|
||||
// count and the streaming loop's cellIndex never drifts from it.
|
||||
// startHyperlink mirrors the loop having already consumed a leading
|
||||
@@ -1162,7 +1157,7 @@ func countVisibleCells(txt string, startHyperlink, startInvisible bool) int {
|
||||
|
||||
if s != '<' {
|
||||
if !hyperlink && !invisible {
|
||||
cells += runewidth.RuneWidth(s)
|
||||
cells += runeCells(s)
|
||||
}
|
||||
i += size
|
||||
continue
|
||||
@@ -1171,7 +1166,7 @@ func countVisibleCells(txt string, startHyperlink, startInvisible bool) int {
|
||||
match, kind := classifyAnchor(txt[i:])
|
||||
if kind == anchorNone {
|
||||
if !hyperlink && !invisible {
|
||||
cells += runewidth.RuneWidth(s)
|
||||
cells += runeCells(s)
|
||||
}
|
||||
i += size
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user