Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
module github.com/noborus/termhyo

go 1.23.0
go 1.24.0

require (
github.com/mattn/go-runewidth v0.0.16
golang.org/x/text v0.28.0
)

require github.com/rivo/uniseg v0.4.7 // indirect
require github.com/rivo/uniseg v0.4.7
5 changes: 0 additions & 5 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,7 +1,2 @@
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/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
14 changes: 7 additions & 7 deletions header_styles.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (

// ANSI escape sequences for text formatting.
const (
// Text formatting
// Text formatting.
AnsiReset = "\x1b[0m"
AnsiBold = "\x1b[1m"
AnsiDim = "\x1b[2m"
Expand All @@ -17,7 +17,7 @@ const (
AnsiReverse = "\x1b[7m"
AnsiStrike = "\x1b[9m"

// Foreground colors (text colors)
// Foreground colors (text colors).
AnsiBlack = "\x1b[30m"
AnsiRed = "\x1b[31m"
AnsiGreen = "\x1b[32m"
Expand All @@ -27,7 +27,7 @@ const (
AnsiCyan = "\x1b[36m"
AnsiWhite = "\x1b[37m"

// Bright foreground colors
// Bright foreground colors.
AnsiBrightBlack = "\x1b[90m"
AnsiBrightRed = "\x1b[91m"
AnsiBrightGreen = "\x1b[92m"
Expand All @@ -37,7 +37,7 @@ const (
AnsiBrightCyan = "\x1b[96m"
AnsiBrightWhite = "\x1b[97m"

// Background colors
// Background colors.
AnsiBgBlack = "\x1b[40m"
AnsiBgRed = "\x1b[41m"
AnsiBgGreen = "\x1b[42m"
Expand All @@ -47,7 +47,7 @@ const (
AnsiBgCyan = "\x1b[46m"
AnsiBgWhite = "\x1b[47m"

// Bright background colors
// Bright background colors.
AnsiBgBrightBlack = "\x1b[100m"
AnsiBgBrightRed = "\x1b[101m"
AnsiBgBrightGreen = "\x1b[102m"
Expand Down Expand Up @@ -115,7 +115,7 @@ func (hs HeaderStyle) ApplyStyle(text string) string {
}

var prefix string
var suffix string = AnsiReset
var suffix = AnsiReset

// Add custom prefix if specified
if hs.CustomPrefix != "" {
Expand Down Expand Up @@ -218,7 +218,7 @@ func (hs HeaderStyle) getSuffix() string {
return ""
}

var suffix string = AnsiReset
var suffix = AnsiReset

// Add custom suffix if specified
if hs.CustomSuffix != "" {
Expand Down
4 changes: 2 additions & 2 deletions renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ type Renderer interface {
type RenderMode int

const (
// BufferedMode: collect all rows before rendering (for auto-width)
// BufferedMode: collect all rows before rendering (for auto-width).
BufferedMode RenderMode = iota
// StreamingMode: render immediately (for fixed-width)
// StreamingMode: render immediately (for fixed-width).
StreamingMode
)

Expand Down
79 changes: 20 additions & 59 deletions width.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,15 @@ package termhyo
import (
"regexp"
"strings"
"unicode"

"github.com/mattn/go-runewidth"
"golang.org/x/text/unicode/norm"
"github.com/rivo/uniseg"
)

// ANSI escape sequence patterns.
var (
// ANSI color codes and other escape sequences
// ANSI color codes and other escape sequences.
ansiEscapeRegex = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`)
// Other control sequences (like \r, \n, \t etc.)
// Other control sequences (like \r, \n, \t etc.).
controlCharsRegex = regexp.MustCompile(`[\x00-\x1f\x7f]`)
)

Expand Down Expand Up @@ -44,10 +42,7 @@ func StringWidth(s string) int {
func stringWidth(s string) int {
// First remove escape sequences and control characters
cleaned := stripEscapeSequences(s)

// Normalize the string to handle combining characters properly
normalized := norm.NFC.String(cleaned)
return runewidth.StringWidth(normalized)
return uniseg.StringWidth(cleaned)
}

// truncateString truncates a string to fit within the specified display width.
Expand Down Expand Up @@ -89,73 +84,39 @@ func truncateWithEscapes(s string, maxWidth int) string {

var result strings.Builder
var currentWidth int
gr := uniseg.NewGraphemes(s)

// Process the string character by character, handling escape sequences
i := 0
runes := []rune(s)

for i < len(runes) {
r := runes[i]

// Check for ANSI escape sequence
if r == '\x1b' && i+1 < len(runes) && runes[i+1] == '[' {
// Find the end of the escape sequence
escapeStart := i
i += 2 // Skip \x1b[

for i < len(runes) && !isAlpha(runes[i]) {
i++
}
if i < len(runes) {
i++ // Include the final letter
}

// Add the entire escape sequence to result (doesn't count toward width)
result.WriteString(string(runes[escapeStart:i]))
for gr.Next() {
cluster := gr.Str()
// Detect ANSI escape sequence
if strings.HasPrefix(cluster, "\x1b[") {
result.WriteString(cluster)
continue
}

// Handle control characters
if r < 0x20 || r == 0x7f {
if r == ' ' || r == '\t' {
// Count spaces and tabs toward width
// Detect control characters
runes := []rune(cluster)
if len(runes) == 1 && (runes[0] < 0x20 || runes[0] == 0x7f) {
if runes[0] == ' ' || runes[0] == '\t' {
if currentWidth >= maxWidth {
break
}
result.WriteRune(r)
result.WriteString(cluster)
currentWidth++
}
// Skip other control characters
i++
continue
}

// Handle combining characters
if unicode.Is(unicode.Mn, r) || unicode.Is(unicode.Me, r) || unicode.Is(unicode.Mc, r) {
result.WriteRune(r)
i++
continue
}

// Regular character - check if it fits
charWidth := runewidth.RuneWidth(r)
if currentWidth+charWidth > maxWidth {
// Normal character
clusterWidth := uniseg.StringWidth(cluster)
if currentWidth+clusterWidth > maxWidth {
break
}

result.WriteRune(r)
currentWidth += charWidth
i++
result.WriteString(cluster)
currentWidth += clusterWidth
}

return result.String()
}

// isAlpha returns true if the rune is an ASCII alphabetic character.
func isAlpha(r rune) bool {
return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')
}

// padString pads a string to the specified display width with spaces.
// Correctly handles ANSI escape sequences when calculating padding.
func padString(s string, width int, align Alignment) string {
Expand Down