refactor: drop x/text cases, language and message packages

Three call sites pulled in x/text's casing and message-formatting
machinery (with its plural and number-format tables) for ASCII-only
work: title-casing segment names and taskwarrior command keys, and
comma-grouping a number in the execution time segment. Replace them
with text.Title and a local thousands-separator helper, both verified
against the exact x/text output for the inputs these paths handle,
including the compatibility-sensitive segment template names
(nix-shell -> Nix-Shell, ui5tooling -> Ui5tooling).

x/text itself stays: the terminal writer's rune width tables use
x/text/width by design.

Shrinks the stripped linux/amd64 binary by 545 kB.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qiyvpiy5jR2tyzwZ3zUki5
This commit is contained in:
Claude
2026-07-31 07:40:24 +02:00
committed by Jan De Dobbeleer
parent 09cface00b
commit 229932c68e
6 changed files with 135 additions and 23 deletions
+14 -15
View File
@@ -18,10 +18,9 @@ import (
runjobs "github.com/jandedobbeleer/oh-my-posh/src/runtime/jobs"
"github.com/jandedobbeleer/oh-my-posh/src/segments/options"
"github.com/jandedobbeleer/oh-my-posh/src/template"
"github.com/jandedobbeleer/oh-my-posh/src/text"
"go.yaml.in/yaml/v3"
c "golang.org/x/text/cases"
"golang.org/x/text/language"
)
type SegmentStyle string
@@ -178,7 +177,7 @@ func (segment *Segment) Name() string {
name := segment.Alias
if name == "" {
name = c.Title(language.English).String(string(segment.Type))
name = text.Title(string(segment.Type))
}
segment.name = name
@@ -325,11 +324,11 @@ func (segment *Segment) Render(index int, force bool) bool {
segment.setIndex(index)
text := segment.string()
rendered := segment.string()
// Only update Enabled if segment is NOT pending (avoid race with Execute goroutine)
if !segment.Pending {
segment.Enabled = segment.Force || strings.ContainsFunc(text, func(r rune) bool { return r != ' ' })
segment.Enabled = segment.Force || strings.ContainsFunc(rendered, func(r rune) bool { return r != ' ' })
if !segment.Enabled {
template.Cache.RemoveSegmentData(segment.Name())
@@ -337,7 +336,7 @@ func (segment *Segment) Render(index int, force bool) bool {
}
}
segment.SetText(text)
segment.SetText(rendered)
segment.setCache()
// We do this to make `.Text` available for a cross-segment reference in an extra prompt.
@@ -359,12 +358,12 @@ func (segment *Segment) renderFallback(index int) bool {
return false
}
text, err := template.RenderTrusted(segment.FallbackTemplate, segment.writer)
rendered, err := template.RenderTrusted(segment.FallbackTemplate, segment.writer)
if err != nil {
text = err.Error()
rendered = err.Error()
}
if !strings.ContainsFunc(text, func(r rune) bool { return r != ' ' }) {
if !strings.ContainsFunc(rendered, func(r rune) bool { return r != ' ' }) {
return false
}
@@ -375,7 +374,7 @@ func (segment *Segment) renderFallback(index int) bool {
segment.setIndex(index)
segment.Enabled = true
segment.SetText(text)
segment.SetText(rendered)
// Intentionally skip setCache(): the writer is zero/partially hydrated
// here, and caching it would make restoreCache() later resurrect a
@@ -394,13 +393,13 @@ func (segment *Segment) Text() string {
return segment.writer.Text()
}
func (segment *Segment) SetText(text string) {
func (segment *Segment) SetText(value string) {
if segment.writer == nil {
segment.text = text
segment.text = value
return
}
segment.writer.SetText(text)
segment.writer.SetText(value)
}
func (segment *Segment) setIndex(index int) {
@@ -814,12 +813,12 @@ func (segment *Segment) string() string {
segment.Template = segment.writer.Template()
}
text, err := template.RenderTrusted(segment.Template, context)
rendered, err := template.RenderTrusted(segment.Template, context)
if err != nil {
return err.Error()
}
return text
return rendered
}
func (segment *Segment) shouldIncludeFolder() bool {
+31 -4
View File
@@ -3,10 +3,9 @@ package segments
import (
"fmt"
"strconv"
"strings"
"github.com/jandedobbeleer/oh-my-posh/src/segments/options"
lang "golang.org/x/text/language"
"golang.org/x/text/message"
)
type Executiontime struct {
@@ -177,6 +176,35 @@ func (t *Executiontime) formatDurationHouston() string {
return result
}
// groupThousands renders n with comma thousand separators, matching what
// x/text/message's English printer produced for %d.
func groupThousands(n int64) string {
s := strconv.FormatInt(n, 10)
start := 0
if s[0] == '-' {
start = 1
}
if len(s)-start <= 3 {
return s
}
var sb strings.Builder
first := start + (len(s)-start)%3
if first == start {
first = start + 3
}
sb.WriteString(s[:first])
for i := first; i < len(s); i += 3 {
sb.WriteByte(',')
sb.WriteString(s[i : i+3])
}
return sb.String()
}
func (t *Executiontime) formatDurationAmarillo() string {
// wholeNumber represents the value to the left of the decimal point (seconds)
wholeNumber := t.Ms / second
@@ -184,8 +212,7 @@ func (t *Executiontime) formatDurationAmarillo() string {
decimalNumber := float64(t.Ms%second) / second
// format wholeNumber as a string with thousands separators
printer := message.NewPrinter(lang.English)
result := printer.Sprintf("%d", wholeNumber)
result := groupThousands(wholeNumber)
if decimalNumber > 0 {
// format decimalNumber as a string with truncated trailing zeros
+20
View File
@@ -418,3 +418,23 @@ func TestExecutionTimeFormatISO8601Ms(t *testing.T) {
assert.Equal(t, tc.Expected, output, "Input: %s", tc.Input)
}
}
func TestGroupThousands(t *testing.T) {
cases := map[int64]string{
0: "0",
1: "1",
999: "999",
1000: "1,000",
12345: "12,345",
123456: "123,456",
1234567: "1,234,567",
1000000000: "1,000,000,000",
-999: "-999",
-1000: "-1,000",
-1234567: "-1,234,567",
}
for input, expected := range cases {
assert.Equal(t, expected, groupThousands(input))
}
}
+2 -4
View File
@@ -3,11 +3,9 @@ package segments
import (
"strings"
c "golang.org/x/text/cases"
"golang.org/x/text/language"
"github.com/jandedobbeleer/oh-my-posh/src/log"
"github.com/jandedobbeleer/oh-my-posh/src/segments/options"
"github.com/jandedobbeleer/oh-my-posh/src/text"
)
const (
@@ -45,7 +43,7 @@ func (t *Taskwarrior) Enabled() bool {
t.Commands = make(map[string]string, len(configuredCommands))
for name, args := range configuredCommands {
key := c.Title(language.English).String(name)
key := text.Title(name)
t.Commands[key] = t.runCommand(cmd, args)
}
+39
View File
@@ -0,0 +1,39 @@
package text
import (
"strings"
"unicode"
)
// Title uppercases the first letter of each word and lowercases every other
// letter. Words break on any character other than a letter, digit or
// underscore, and digits never count as a word's first letter - matching how
// x/text's cases.Title(language.English) handled the ASCII identifiers this
// replaces it for: "nix-shell" -> "Nix-Shell", "ui5tooling" -> "Ui5tooling",
// "foo_bar" -> "Foo_bar".
func Title(s string) string {
var sb strings.Builder
sb.Grow(len(s))
first := true
for _, r := range s {
switch {
case unicode.IsLetter(r):
if first {
sb.WriteRune(unicode.ToUpper(r))
first = false
continue
}
sb.WriteRune(unicode.ToLower(r))
case unicode.IsDigit(r) || r == '_':
sb.WriteRune(r)
default:
sb.WriteRune(r)
first = true
}
}
return sb.String()
}
+29
View File
@@ -0,0 +1,29 @@
package text
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestTitle(t *testing.T) {
// expected values are what x/text's cases.Title(language.English)
// produced for the same input before it was replaced
cases := map[string]string{
"git": "Git",
"nix-shell": "Nix-Shell",
"ui5tooling": "Ui5tooling",
"GIT": "Git",
"foo_bar": "Foo_bar",
"os": "Os",
"context": "Context",
"Nice2see": "Nice2see",
"hello world": "Hello World",
"a-1b": "A-1B",
"": "",
}
for input, expected := range cases {
assert.Equal(t, expected, Title(input), input)
}
}