v0.1.0 initial commit

This commit is contained in:
2026-07-12 18:41:05 -04:00
commit aa22225c61
69 changed files with 10971 additions and 0 deletions
+192
View File
@@ -0,0 +1,192 @@
package tui
import (
"unicode/utf8"
)
// --- Length calculation ---
// RuneLen returns rune count, used as display width under the package-wide assumption of 1 cell per rune
// Wide (CJK), emoji, and combining characters are not handled
func RuneLen(s string) int {
return utf8.RuneCountInString(s)
}
// --- Truncation ---
// Truncate truncates string with … suffix if exceeds maxLen
func Truncate(s string, maxLen int) string {
if maxLen <= 0 {
return ""
}
if utf8.RuneCountInString(s) <= maxLen {
return s
}
// Boundary-safe truncation for UTF-8
count := 0
for i := range s {
if count == maxLen-1 {
return s[:i] + "…"
}
count++
}
return s
}
// TruncateLeft truncates with … prefix, keeps end of string
func TruncateLeft(s string, maxLen int) string {
if maxLen <= 0 {
return ""
}
runes := []rune(s)
if len(runes) <= maxLen {
return s
}
if maxLen <= 1 {
return "…"
}
return "…" + string(runes[len(runes)-maxLen+1:])
}
// TruncateMiddle keeps start and end, … in middle
func TruncateMiddle(s string, maxLen int) string {
if maxLen <= 0 {
return ""
}
runes := []rune(s)
if len(runes) <= maxLen {
return s
}
if maxLen <= 3 {
return Truncate(s, maxLen)
}
// Split remaining space between start and end
// Favor start slightly: (maxLen-1)/2 for start, rest for end
startLen := (maxLen - 1) / 2
endLen := maxLen - 1 - startLen
return string(runes[:startLen]) + "…" + string(runes[len(runes)-endLen:])
}
// --- Padding ---
// PadRight pads string with spaces to width
func PadRight(s string, width int) string {
runes := []rune(s)
if len(runes) >= width {
return s
}
result := make([]rune, width)
copy(result, runes)
for i := len(runes); i < width; i++ {
result[i] = ' '
}
return string(result)
}
// PadLeft left-pads string with spaces to width
func PadLeft(s string, width int) string {
runes := []rune(s)
if len(runes) >= width {
return s
}
result := make([]rune, width)
padding := width - len(runes)
for i := 0; i < padding; i++ {
result[i] = ' '
}
copy(result[padding:], runes)
return string(result)
}
// PadCenter centers string within width
func PadCenter(s string, width int) string {
runes := []rune(s)
if len(runes) >= width {
return s
}
result := make([]rune, width)
leftPad := (width - len(runes)) / 2
for i := range result {
result[i] = ' '
}
copy(result[leftPad:], runes)
return string(result)
}
// --- Text wrapping ---
// WrapText wraps text at word boundaries to fit width
// Returns slice of lines, each no longer than width
func WrapText(s string, width int) []string {
if width <= 0 {
return nil
}
runes := []rune(s)
if len(runes) == 0 {
return []string{""}
}
var lines []string
lineStart := 0
lastSpace := -1
for i := 0; i <= len(runes); i++ {
// Check if we need to wrap
if i-lineStart >= width || i == len(runes) {
if i == len(runes) {
// End of string
if lineStart < len(runes) {
lines = append(lines, string(runes[lineStart:]))
}
break
}
// Need to wrap
wrapAt := i
if lastSpace > lineStart {
// Wrap at last space
wrapAt = lastSpace
}
lines = append(lines, string(runes[lineStart:wrapAt]))
// Skip space at wrap point
if wrapAt < len(runes) && runes[wrapAt] == ' ' {
lineStart = wrapAt + 1
} else {
lineStart = wrapAt
}
lastSpace = -1
}
// Track spaces for word wrapping
if i < len(runes) && runes[i] == ' ' {
lastSpace = i
}
}
if len(lines) == 0 {
lines = []string{""}
}
return lines
}
// --- Repetition ---
// RepeatRune returns a string of n repeated runes, for string repeat use strings.Repeat
func RepeatRune(r rune, n int) string {
if n <= 0 {
return ""
}
runes := make([]rune, n)
for i := range runes {
runes[i] = r
}
return string(runes)
}