diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 38025667..4bb9c829 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -13,6 +13,10 @@ updates: commit-message: prefix: "chore" include: "scope" + groups: + all: + patterns: + - "*" - package-ecosystem: "github-actions" directory: "/" @@ -26,6 +30,10 @@ updates: commit-message: prefix: "chore" include: "scope" + groups: + all: + patterns: + - "*" - package-ecosystem: "docker" directory: "/" @@ -37,8 +45,12 @@ updates: labels: - "dependencies" commit-message: - prefix: "feat" + prefix: "chore" include: "scope" + groups: + all: + patterns: + - "*" - package-ecosystem: "gomod" directory: "/example" @@ -52,3 +64,7 @@ updates: commit-message: prefix: "chore" include: "scope" + groups: + all: + patterns: + - "*" diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index ab232146..83eeaf6e 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -12,12 +12,12 @@ jobs: GO111MODULE: "on" steps: - name: Install Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version: ${{ matrix.go-version }} - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - run: | git config --global url."https://${{ secrets.PERSONAL_ACCESS_TOKEN }}@github.com/charmbracelet".insteadOf "https://github.com/charmbracelet" diff --git a/.golangci.yml b/.golangci.yml index 7c0a115e..929cb0ac 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -7,7 +7,6 @@ linters: - exhaustive - goconst - godot - - godox - gomoddirectives - goprintffuncname - gosec @@ -27,13 +26,13 @@ linters: - whitespace - wrapcheck exclusions: + rules: + - text: '(slog|log)\.\w+' + linters: + - noctx generated: lax presets: - common-false-positives - paths: - - third_party$ - - builtin$ - - examples$ issues: max-issues-per-linter: 0 max-same-issues: 0 @@ -43,7 +42,3 @@ formatters: - goimports exclusions: generated: lax - paths: - - third_party$ - - builtin$ - - examples$ diff --git a/LICENSE b/LICENSE index 6f5b1fa6..3658b33e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021-2023 Charmbracelet, Inc +Copyright (c) 2021-2025 Charmbracelet, Inc Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index cee2371c..741fe69f 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,20 @@ # Lip Gloss

- Lip Gloss title treatment
+ + + + + +
Latest Release GoDoc Build Status - phorm.ai

Style definitions for nice terminal layouts. Built with TUIs in mind. -![Lip Gloss example](https://github.com/user-attachments/assets/7950b1c1-e0e3-427e-8e7d-6f7f6ad17ca7) +![Lip Gloss example](https://github.com/user-attachments/assets/92560e60-d70e-4ce0-b39e-a60bb933356b) Lip Gloss takes an expressive, declarative approach to terminal rendering. Users familiar with CSS will feel at home with Lip Gloss. @@ -806,7 +810,7 @@ We’d love to hear your thoughts on this project. Feel free to drop us a note! Part of [Charm](https://charm.sh). -The Charm logo +The Charm logo Charm热爱开源 • Charm loves open source diff --git a/borders.go b/borders.go index b36f8743..a8d3e904 100644 --- a/borders.go +++ b/borders.go @@ -2,6 +2,7 @@ package lipgloss import ( "strings" + "unicode/utf8" "github.com/charmbracelet/x/ansi" "github.com/muesli/termenv" @@ -485,6 +486,6 @@ func getFirstRuneAsString(str string) string { if str == "" { return str } - r := []rune(str) - return string(r[0]) + _, size := utf8.DecodeRuneInString(str) + return str[:size] } diff --git a/borders_test.go b/borders_test.go index 44b95d0c..c2fb5c14 100644 --- a/borders_test.go +++ b/borders_test.go @@ -1,6 +1,8 @@ package lipgloss -import "testing" +import ( + "testing" +) func TestStyle_GetBorderSizes(t *testing.T) { tests := []struct { @@ -94,3 +96,80 @@ func TestStyle_GetBorderSizes(t *testing.T) { }) } } + +// Old implementation using rune slice conversion +func getFirstRuneAsStringOld(str string) string { + if str == "" { + return str + } + r := []rune(str) + return string(r[0]) +} + +func TestGetFirstRuneAsString(t *testing.T) { + tests := []struct { + name string + input string + want string + }{ + {"Empty", "", ""}, + {"SingleASCII", "A", "A"}, + {"SingleUnicode", "世", "世"}, + {"ASCIIString", "Hello", "H"}, + {"UnicodeString", "你好世界", "你"}, + {"MixedASCIIFirst", "Hello世界", "H"}, + {"MixedUnicodeFirst", "世界Hello", "世"}, + {"Emoji", "😀Happy", "😀"}, + {"MultiByteFirst", "ñoño", "ñ"}, + {"LongString", "The quick brown fox jumps over the lazy dog", "T"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := getFirstRuneAsString(tt.input) + if got != tt.want { + t.Errorf("getFirstRuneAsString(%q) = %q, want %q", tt.input, got, tt.want) + } + + // Verify new implementation matches old implementation + old := getFirstRuneAsStringOld(tt.input) + if got != old { + t.Errorf("getFirstRuneAsString(%q) = %q, but old implementation returns %q", tt.input, got, old) + } + }) + } +} + +func BenchmarkGetFirstRuneAsString(b *testing.B) { + testCases := []struct { + name string + str string + }{ + {"ASCII", "Hello, World!"}, + {"Unicode", "你好世界"}, + {"Single", "A"}, + {"Empty", ""}, + } + + b.Run("Old", func(b *testing.B) { + for _, tc := range testCases { + b.Run(tc.name, func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = getFirstRuneAsStringOld(tc.str) + } + }) + } + }) + + b.Run("New", func(b *testing.B) { + for _, tc := range testCases { + b.Run(tc.name, func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = getFirstRuneAsString(tc.str) + } + }) + } + }) +} diff --git a/examples/go.mod b/examples/go.mod index 4f8dcb6e..eea207a8 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -13,34 +13,38 @@ require ( github.com/creack/pty v1.1.21 github.com/lucasb-eyer/go-colorful v1.2.0 github.com/muesli/gamut v0.3.1 - github.com/muesli/termenv v0.15.2 - golang.org/x/term v0.27.0 + github.com/muesli/termenv v0.16.0 + golang.org/x/term v0.29.0 ) require ( github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/bubbletea v0.25.0 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/charmbracelet/keygen v0.5.0 // indirect github.com/charmbracelet/log v0.4.0 // indirect - github.com/charmbracelet/x/ansi v0.6.0 // indirect + github.com/charmbracelet/x/ansi v0.8.0 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13 // indirect github.com/charmbracelet/x/errors v0.0.0-20240117030013-d31dba354651 // indirect github.com/charmbracelet/x/exp/term v0.0.0-20240328150354-ab9afc214dfd // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/clusters v0.0.0-20200529215643-2700303c1762 // indirect github.com/muesli/kmeans v0.3.1 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect - golang.org/x/crypto v0.31.0 // indirect + golang.org/x/crypto v0.35.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect ) diff --git a/examples/go.sum b/examples/go.sum index 5959d361..e90ef4a7 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -5,6 +5,8 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= github.com/charmbracelet/keygen v0.5.0 h1:XY0fsoYiCSM9axkrU+2ziE6u6YjJulo/b9Dghnw6MZc= github.com/charmbracelet/keygen v0.5.0/go.mod h1:DfvCgLHxZ9rJxdK0DGw3C/LkV4SgdGbnliHcObV3L+8= github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM= @@ -13,13 +15,17 @@ github.com/charmbracelet/ssh v0.0.0-20240401141849-854cddfa2917 h1:NZKjJ7d/pzk/A github.com/charmbracelet/ssh v0.0.0-20240401141849-854cddfa2917/go.mod h1:8/Ve8iGRRIGFM1kepYfRF2pEOF5Y3TEZYoJaA54228U= github.com/charmbracelet/wish v1.4.0 h1:pL1uVP/YuYgJheHEj98teZ/n6pMYnmlZq/fcHvomrfc= github.com/charmbracelet/wish v1.4.0/go.mod h1:ew4/MjJVfW/akEO9KmrQHQv1F7bQRGscRMrA+KtovTk= -github.com/charmbracelet/x/ansi v0.6.0 h1:qOznutrb93gx9oMiGf7caF7bqqubh6YIM0SWKyA08pA= -github.com/charmbracelet/x/ansi v0.6.0/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q= +github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= +github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= +github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= github.com/charmbracelet/x/errors v0.0.0-20240117030013-d31dba354651 h1:3RXpZWGWTOeVXCTv0Dnzxdv/MhNUkBfEcbaTY0zrTQI= github.com/charmbracelet/x/errors v0.0.0-20240117030013-d31dba354651/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= github.com/charmbracelet/x/exp/term v0.0.0-20240328150354-ab9afc214dfd h1:HqBjkSFXXfW4IgX3TMKipWoPEN08T3Pi4SA/3DLss/U= github.com/charmbracelet/x/exp/term v0.0.0-20240328150354-ab9afc214dfd/go.mod h1:6GZ13FjIP6eOCqWU4lqgveGnYxQo9c3qBzHPeFu4HBE= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= @@ -35,8 +41,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= @@ -50,8 +56,8 @@ github.com/muesli/kmeans v0.3.1 h1:KshLQ8wAETfLWOJKMuDCVYHnafddSa1kwGh/IypGIzY= github.com/muesli/kmeans v0.3.1/go.mod h1:8/OvJW7cHc1BpRf8URb43m+vR105DDe+Kj1WcFXYDqc= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= -github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -59,22 +65,24 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= +golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/go.mod b/go.mod index 09792957..2f60edad 100644 --- a/go.mod +++ b/go.mod @@ -4,13 +4,13 @@ retract v0.7.0 // v0.7.0 introduces a bug that causes some apps to freeze. retract v0.11.1 // v0.11.1 uses a broken version of x/ansi StringWidth that causes some lines to wrap incorrectly. -go 1.18 +go 1.24.0 require ( - github.com/aymanbagabas/go-udiff v0.2.0 - github.com/charmbracelet/x/ansi v0.8.0 + github.com/aymanbagabas/go-udiff v0.3.1 + github.com/charmbracelet/x/ansi v0.10.2 github.com/charmbracelet/x/cellbuf v0.0.13 - github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a + github.com/charmbracelet/x/exp/golden v0.0.0-20250609102027-b60490452b30 github.com/muesli/termenv v0.16.0 github.com/rivo/uniseg v0.4.7 ) @@ -19,9 +19,9 @@ require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/charmbracelet/x/term v0.2.1 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mattn/go-runewidth v0.0.17 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/sys v0.30.0 // indirect ) diff --git a/go.sum b/go.sum index 3858e705..cf41b749 100644 --- a/go.sum +++ b/go.sum @@ -1,23 +1,23 @@ 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/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= -github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= +github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= -github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= -github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/ansi v0.10.2 h1:ith2ArZS0CJG30cIUfID1LXN7ZFXRCww6RUvAPA+Pzw= +github.com/charmbracelet/x/ansi v0.10.2/go.mod h1:HbLdJjQH4UH4AqA2HpRWuWNluRE6zxJH/yteYEYCFa8= github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= -github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= -github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/exp/golden v0.0.0-20250609102027-b60490452b30 h1:lF42GCGfbMxx4SOYkjChVoUDexdM/hQ4DWnAHcJ/6K0= +github.com/charmbracelet/x/exp/golden v0.0.0-20250609102027-b60490452b30/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.17 h1:78v8ZlW0bP43XfmAfPsdXcoNCelfMHsDmd/pkENfrjQ= +github.com/mattn/go-runewidth v0.0.17/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -26,6 +26,7 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/style.go b/style.go index 59fa3ab2..7e962a4f 100644 --- a/style.go +++ b/style.go @@ -565,20 +565,6 @@ func pad(str string, n int, style *termenv.Style) string { return b.String() } -func max(a, b int) int { //nolint:unparam,predeclared - if a > b { - return a - } - return b -} - -func min(a, b int) int { //nolint:predeclared - if a < b { - return a - } - return b -} - func abs(a int) int { if a < 0 { return -a diff --git a/table/resizing.go b/table/resizing.go index 731e571c..30b0a010 100644 --- a/table/resizing.go +++ b/table/resizing.go @@ -212,7 +212,7 @@ func (r *resizer) expandTableWidth() (colWidths, rowHeights []int) { colWidths[shorterColumnIndex]++ } - rowHeights = r.expandRowHeigths(colWidths) + rowHeights = r.expandRowHeights(colWidths) return } @@ -288,11 +288,11 @@ func (r *resizer) shrinkTableWidth() (colWidths, rowHeights []int) { shrinkToMedian() shrinkBiggestColumns(false) - return colWidths, r.expandRowHeigths(colWidths) + return colWidths, r.expandRowHeights(colWidths) } -// expandRowHeigths expands the row heights. -func (r *resizer) expandRowHeigths(colWidths []int) (rowHeights []int) { +// expandRowHeights expands the row heights. +func (r *resizer) expandRowHeights(colWidths []int) (rowHeights []int) { rowHeights = r.defaultRowHeights() if !r.wrap { return rowHeights diff --git a/table/table.go b/table/table.go index b17c223f..ad17fcfb 100644 --- a/table/table.go +++ b/table/table.go @@ -347,16 +347,25 @@ func (t *Table) constructBottomBorder() string { // constructHeaders constructs the headers for the table given it's current // header configuration and data. func (t *Table) constructHeaders() string { + height := t.heights[HeaderRow+1] + var s strings.Builder if t.borderLeft { s.WriteString(t.borderStyle.Render(t.border.Left)) } for i, header := range t.headers { - s.WriteString(t.style(HeaderRow, i). - MaxHeight(1). - Width(t.widths[i]). + cellStyle := t.style(HeaderRow, i) + + if !t.wrap { + header = t.truncateCell(header, HeaderRow, i) + } + + s.WriteString(cellStyle. + Height(height - cellStyle.GetVerticalMargins()). + MaxHeight(height). + Width(t.widths[i] - cellStyle.GetHorizontalMargins()). MaxWidth(t.widths[i]). - Render(t.truncateCell(header, -1, i))) + Render(t.truncateCell(header, HeaderRow, i))) if i < len(t.headers)-1 && t.borderColumn { s.WriteString(t.borderStyle.Render(t.border.Left)) } @@ -469,7 +478,7 @@ func (t *Table) constructRow(index int, isOverflow bool) string { s.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, cells...) + "\n") - if t.borderRow && index < t.data.Rows()-1 { + if t.borderRow && index < t.data.Rows()-1 && !isOverflow { s.WriteString(t.borderStyle.Render(t.border.MiddleLeft)) for i := 0; i < len(t.widths); i++ { s.WriteString(t.borderStyle.Render(strings.Repeat(t.border.Bottom, t.widths[i]))) diff --git a/table/table_test.go b/table/table_test.go index 75cc86fe..3957c5d1 100644 --- a/table/table_test.go +++ b/table/table_test.go @@ -92,6 +92,23 @@ func TestTableNoStyleFunc(t *testing.T) { golden.RequireEqual(t, []byte(table.String())) } +func TestTableMarginAndRightAlignment(t *testing.T) { + table := New(). + Border(lipgloss.NormalBorder()). + StyleFunc(func(row, col int) lipgloss.Style { + return lipgloss.NewStyle().Margin(0, 1).Align(lipgloss.Right) + }). + Headers("LANGUAGE", "FORMAL", "INFORMAL"). + Row("Arabic", "أهلين", "أهلا"). + Row("Chinese", "Nǐn hǎo", "Nǐ hǎo"). + Row("French", "Bonjour", "Salut"). + Row("Japanese", "こんにちは", "やあ"). + Row("Russian", "Zdravstvuyte", "Privet"). + Row("Spanish", "Hola", "¿Qué tal?") + + golden.RequireEqual(t, []byte(table.String())) +} + func TestTableOffset(t *testing.T) { table := New(). Border(lipgloss.NormalBorder()). @@ -319,14 +336,28 @@ func TestTableRowSeparators(t *testing.T) { {"Spanish", "Hola", "¿Qué tal?"}, } - table := New(). - Border(lipgloss.NormalBorder()). - StyleFunc(TableStyle). - BorderRow(true). - Headers("LANGUAGE", "FORMAL", "INFORMAL"). - Rows(rows...) + t.Run("no overflow", func(t *testing.T) { + table := New(). + Border(lipgloss.NormalBorder()). + StyleFunc(TableStyle). + BorderRow(true). + Headers("LANGUAGE", "FORMAL", "INFORMAL"). + Rows(rows...) - golden.RequireEqual(t, []byte(table.String())) + golden.RequireEqual(t, []byte(table.String())) + }) + + t.Run("with overflow", func(t *testing.T) { + table := New(). + Border(lipgloss.NormalBorder()). + StyleFunc(TableStyle). + BorderRow(true). + Headers("LANGUAGE", "FORMAL", "INFORMAL"). + Rows(rows...). + Height(8) + + golden.RequireEqual(t, []byte(table.String())) + }) } func TestTableHeights(t *testing.T) { diff --git a/table/testdata/TestTableMarginAndRightAlignment.golden b/table/testdata/TestTableMarginAndRightAlignment.golden new file mode 100644 index 00000000..f00102da --- /dev/null +++ b/table/testdata/TestTableMarginAndRightAlignment.golden @@ -0,0 +1,10 @@ +┌──────────┬──────────────┬───────────┐ +│ LANGUAGE │ FORMAL │ INFORMAL │ +├──────────┼──────────────┼───────────┤ +│ Arabic │ أهلين │ أهلا │ +│ Chinese │ Nǐn hǎo │ Nǐ hǎo │ +│ French │ Bonjour │ Salut │ +│ Japanese │ こんにちは │ やあ │ +│ Russian │ Zdravstvuyte │ Privet │ +│ Spanish │ Hola │ ¿Qué tal? │ +└──────────┴──────────────┴───────────┘ \ No newline at end of file diff --git a/table/testdata/TestTableRowSeparators.golden b/table/testdata/TestTableRowSeparators/no_overflow.golden similarity index 100% rename from table/testdata/TestTableRowSeparators.golden rename to table/testdata/TestTableRowSeparators/no_overflow.golden diff --git a/table/testdata/TestTableRowSeparators/with_overflow.golden b/table/testdata/TestTableRowSeparators/with_overflow.golden new file mode 100644 index 00000000..e0c519c9 --- /dev/null +++ b/table/testdata/TestTableRowSeparators/with_overflow.golden @@ -0,0 +1,11 @@ +┌──────────┬──────────────┬───────────┐ +│ LANGUAGE │ FORMAL │ INFORMAL │ +├──────────┼──────────────┼───────────┤ +│ Chinese │ Nǐn hǎo │ Nǐ hǎo │ +├──────────┼──────────────┼───────────┤ +│ French │ Bonjour │ Salut │ +├──────────┼──────────────┼───────────┤ +│ Japanese │ こんにちは │ やあ │ +├──────────┼──────────────┼───────────┤ +│ … │ … │ … │ +└──────────┴──────────────┴───────────┘ \ No newline at end of file diff --git a/table/util.go b/table/util.go index 74bcdffe..d139ca28 100644 --- a/table/util.go +++ b/table/util.go @@ -12,22 +12,6 @@ func btoi(b bool) int { return 0 } -// max returns the greater of two integers. -func max(a, b int) int { //nolint:predeclared - if a > b { - return a - } - return b -} - -// min returns the smaller of two integers. -func min(a, b int) int { //nolint:predeclared - if a < b { - return a - } - return b -} - // sum returns the sum of all integers in a slice. func sum(n []int) int { var sum int diff --git a/tree/renderer.go b/tree/renderer.go index fea96fae..d6d4593f 100644 --- a/tree/renderer.go +++ b/tree/renderer.go @@ -138,10 +138,3 @@ func (r *renderer) render(node Node, root bool, prefix string) string { } return strings.Join(strs, "\n") } - -func max(a, b int) int { - if a > b { - return a - } - return b -}