Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
package twcache
// Cache defines a generic interface for a key-value storage with type constraints on keys and values.
// The keys must be of a type that supports comparison.
// Add inserts a new key-value pair, potentially evicting an item if necessary.
// Get retrieves a value associated with the given key, returning a boolean to indicate if the key was found.
// Purge clears all items from the cache.
type Cache[K comparable, V any] interface {
Add(key K, value V) (evicted bool)
Get(key K) (value V, ok bool)
Purge()
}
+289
View File
@@ -0,0 +1,289 @@
package twcache
import (
"sync"
"sync/atomic"
)
// EvictCallback is a function called when an entry is evicted.
// This includes evictions during Purge or Resize operations.
type EvictCallback[K comparable, V any] func(key K, value V)
// LRU is a thread-safe, generic LRU cache with a fixed size.
// It has zero dependencies, high performance, and full features.
type LRU[K comparable, V any] struct {
size int
items map[K]*entry[K, V]
head *entry[K, V] // Most Recently Used
tail *entry[K, V] // Least Recently Used
onEvict EvictCallback[K, V]
mu sync.Mutex
hits atomic.Int64
misses atomic.Int64
}
// entry represents a single item in the LRU linked list.
// It holds the key, value, and pointers to prev/next entries.
type entry[K comparable, V any] struct {
key K
value V
prev *entry[K, V]
next *entry[K, V]
}
// NewLRU creates a new LRU cache with the given size.
// Returns nil if size <= 0, acting as a disabled cache.
// Caps size at 100,000 for reasonableness.
func NewLRU[K comparable, V any](size int) *LRU[K, V] {
return NewLRUEvict[K, V](size, nil)
}
// NewLRUEvict creates a new LRU cache with an eviction callback.
// The callback is optional and called on evictions.
// Returns nil if size <= 0.
func NewLRUEvict[K comparable, V any](size int, onEvict EvictCallback[K, V]) *LRU[K, V] {
if size <= 0 {
return nil // nil = disabled cache (fast path in hot code)
}
if size > 100_000 {
size = 100_000 // reasonable upper bound
}
return &LRU[K, V]{
size: size,
items: make(map[K]*entry[K, V], size),
onEvict: onEvict,
}
}
// GetOrCompute retrieves a value or computes it if missing.
// Ensures no double computation under concurrency.
// Ideal for expensive computations like twwidth.
func (c *LRU[K, V]) GetOrCompute(key K, compute func() V) V {
if c == nil || c.size <= 0 {
return compute()
}
c.mu.Lock()
if e, ok := c.items[key]; ok {
c.moveToFront(e)
c.hits.Add(1)
c.mu.Unlock()
return e.value
}
c.misses.Add(1)
value := compute() // expensive work only on real miss
// Double-check: someone might have added it while computing
if e, ok := c.items[key]; ok {
e.value = value
c.moveToFront(e)
c.mu.Unlock()
return value
}
// Evict if needed
if len(c.items) >= c.size {
c.removeOldest()
}
e := &entry[K, V]{key: key, value: value}
c.addToFront(e)
c.items[key] = e
c.mu.Unlock()
return value
}
// Get retrieves a value by key if it exists.
// Returns the value and true if found, else zero and false.
// Updates the entry to most recently used.
func (c *LRU[K, V]) Get(key K) (V, bool) {
if c == nil || c.size <= 0 {
var zero V
return zero, false
}
c.mu.Lock()
defer c.mu.Unlock()
e, ok := c.items[key]
if !ok {
c.misses.Add(1)
var zero V
return zero, false
}
c.hits.Add(1)
c.moveToFront(e)
return e.value, true
}
// Add inserts or updates a key-value pair.
// Evicts the oldest if cache is full.
// Returns true if an eviction occurred.
func (c *LRU[K, V]) Add(key K, value V) (evicted bool) {
if c == nil || c.size <= 0 {
return false
}
c.mu.Lock()
defer c.mu.Unlock()
if e, ok := c.items[key]; ok {
e.value = value
c.moveToFront(e)
return false
}
if len(c.items) >= c.size {
c.removeOldest()
evicted = true
}
e := &entry[K, V]{key: key, value: value}
c.addToFront(e)
c.items[key] = e
return evicted
}
// Remove deletes a key from the cache.
// Returns true if the key was found and removed.
func (c *LRU[K, V]) Remove(key K) bool {
if c == nil || c.size <= 0 {
return false
}
c.mu.Lock()
defer c.mu.Unlock()
e, ok := c.items[key]
if !ok {
return false
}
c.removeNode(e)
delete(c.items, key)
return true
}
// Purge clears all entries from the cache.
// Calls onEvict for each entry if set.
// Resets hit/miss counters.
func (c *LRU[K, V]) Purge() {
if c == nil || c.size <= 0 {
return
}
c.mu.Lock()
if c.onEvict != nil {
for key, e := range c.items {
c.onEvict(key, e.value)
}
}
c.items = make(map[K]*entry[K, V], c.size)
c.head = nil
c.tail = nil
c.hits.Store(0)
c.misses.Store(0)
c.mu.Unlock()
}
// Len returns the current number of items in the cache.
func (c *LRU[K, V]) Len() int {
if c == nil || c.size <= 0 {
return 0
}
c.mu.Lock()
n := len(c.items)
c.mu.Unlock()
return n
}
// Cap returns the maximum capacity of the cache.
func (c *LRU[K, V]) Cap() int {
if c == nil {
return 0
}
return c.size
}
// HitRate returns the cache hit ratio (0.0 to 1.0).
// Based on hits / (hits + misses).
func (c *LRU[K, V]) HitRate() float64 {
h := c.hits.Load()
m := c.misses.Load()
total := h + m
if total == 0 {
return 0.0
}
return float64(h) / float64(total)
}
// RemoveOldest removes and returns the least recently used item.
// Returns key, value, and true if an item was removed.
// Calls onEvict if set.
func (c *LRU[K, V]) RemoveOldest() (key K, value V, ok bool) {
if c == nil || c.size <= 0 {
return
}
c.mu.Lock()
defer c.mu.Unlock()
if c.tail == nil {
return
}
key = c.tail.key
value = c.tail.value
c.removeOldest()
return key, value, true
}
// moveToFront moves an entry to the front (MRU position).
func (c *LRU[K, V]) moveToFront(e *entry[K, V]) {
if c.head == e {
return
}
c.removeNode(e)
c.addToFront(e)
}
// addToFront adds an entry to the front of the list.
func (c *LRU[K, V]) addToFront(e *entry[K, V]) {
e.prev = nil
e.next = c.head
if c.head != nil {
c.head.prev = e
}
c.head = e
if c.tail == nil {
c.tail = e
}
}
// removeNode removes an entry from the linked list.
func (c *LRU[K, V]) removeNode(e *entry[K, V]) {
if e.prev != nil {
e.prev.next = e.next
} else {
c.head = e.next
}
if e.next != nil {
e.next.prev = e.prev
} else {
c.tail = e.prev
}
e.prev = nil
e.next = nil
}
// removeOldest removes the tail entry (LRU).
// Calls onEvict if set and deletes from map.
func (c *LRU[K, V]) removeOldest() {
if c.tail == nil {
return
}
e := c.tail
if c.onEvict != nil {
c.onEvict(e.key, e.value)
}
c.removeNode(e)
delete(c.items, e.key)
}
+229
View File
@@ -0,0 +1,229 @@
// Copyright 2014 Oleku Konko All rights reserved.
// Use of this source code is governed by a MIT
// license that can be found in the LICENSE file.
// This module is a Table Writer API for the Go Programming Language.
// The protocols were written in pure Go and works on windows and unix systems
package twwarp
import (
"math"
"strings"
"unicode"
"github.com/clipperhouse/uax29/v2/graphemes"
"github.com/olekukonko/tablewriter/pkg/twwidth"
)
const (
nl = "\n"
sp = " "
)
const defaultPenalty = 1e5
func SplitWords(s string) []string {
words := make([]string, 0, len(s)/5)
var wordBegin int
wordPending := false
for i, c := range s {
if unicode.IsSpace(c) {
if wordPending {
words = append(words, s[wordBegin:i])
wordPending = false
}
continue
}
if !wordPending {
wordBegin = i
wordPending = true
}
}
if wordPending {
words = append(words, s[wordBegin:])
}
return words
}
// WrapString wraps s into a paragraph of lines of length lim, with minimal
// raggedness.
func WrapString(s string, lim int) ([]string, int) {
if s == sp {
return []string{sp}, lim
}
words := SplitWords(s)
if len(words) == 0 {
return []string{""}, lim
}
var lines []string
max := 0
for _, v := range words {
max = twwidth.Width(v)
if max > lim {
lim = max
}
}
for _, line := range WrapWords(words, 1, lim, defaultPenalty) {
lines = append(lines, strings.Join(line, sp))
}
return lines, lim
}
// WrapStringWithSpaces wraps a string into lines of a specified display width while preserving
// leading and trailing spaces. It splits the input string into words, condenses internal multiple
// spaces to a single space, and wraps the content to fit within the given width limit, measured
// using Unicode-aware display width. The function is used in the logging library to format log
// messages for consistent output. It returns the wrapped lines as a slice of strings and the
// adjusted width limit, which may increase if a single word exceeds the input limit. Thread-safe
// as it does not modify shared state.
func WrapStringWithSpaces(s string, lim int) ([]string, int) {
if len(s) == 0 {
return []string{""}, lim
}
if strings.TrimSpace(s) == "" { // All spaces
if twwidth.Width(s) <= lim {
return []string{s}, twwidth.Width(s)
}
// For very long all-space strings, "wrap" by truncating to the limit.
if lim > 0 {
substring, _ := stringToDisplayWidth(s, lim)
return []string{substring}, lim
}
return []string{""}, lim
}
var leadingSpaces, trailingSpaces, coreContent string
firstNonSpace := strings.IndexFunc(s, func(r rune) bool { return !unicode.IsSpace(r) })
leadingSpaces = s[:firstNonSpace]
lastNonSpace := strings.LastIndexFunc(s, func(r rune) bool { return !unicode.IsSpace(r) })
trailingSpaces = s[lastNonSpace+1:]
coreContent = s[firstNonSpace : lastNonSpace+1]
if coreContent == "" {
return []string{leadingSpaces + trailingSpaces}, lim
}
words := SplitWords(coreContent)
if len(words) == 0 {
return []string{leadingSpaces + trailingSpaces}, lim
}
var lines []string
currentLim := lim
maxCoreWordWidth := 0
for _, v := range words {
w := twwidth.Width(v)
if w > maxCoreWordWidth {
maxCoreWordWidth = w
}
}
if maxCoreWordWidth > currentLim {
currentLim = maxCoreWordWidth
}
wrappedWordLines := WrapWords(words, 1, currentLim, defaultPenalty)
for i, lineWords := range wrappedWordLines {
joinedLine := strings.Join(lineWords, sp)
finalLine := leadingSpaces + joinedLine
if i == len(wrappedWordLines)-1 { // Last line
finalLine += trailingSpaces
}
lines = append(lines, finalLine)
}
return lines, currentLim
}
// stringToDisplayWidth returns a substring of s that has a display width
// as close as possible to, but not exceeding, targetWidth.
// It returns the substring and its actual display width.
func stringToDisplayWidth(s string, targetWidth int) (substring string, actualWidth int) {
if targetWidth <= 0 {
return "", 0
}
var currentWidth int
var endIndex int // Tracks the byte index in the original string
g := graphemes.FromString(s)
for g.Next() {
grapheme := g.Value()
graphemeWidth := twwidth.Width(grapheme)
if currentWidth+graphemeWidth > targetWidth {
break
}
currentWidth += graphemeWidth
endIndex = g.End()
}
return s[:endIndex], currentWidth
}
// WrapWords is the low-level line-breaking algorithm, useful if you need more
// control over the details of the text wrapping process. For most uses,
// WrapString will be sufficient and more convenient.
//
// WrapWords splits a list of words into lines with minimal "raggedness",
// treating each rune as one unit, accounting for spc units between adjacent
// words on each line, and attempting to limit lines to lim units. Raggedness
// is the total error over all lines, where error is the square of the
// difference of the length of the line and lim. Too-long lines (which only
// happen when a single word is longer than lim units) have pen penalty units
// added to the error.
func WrapWords(words []string, spc, lim, pen int) [][]string {
n := len(words)
if n == 0 {
return nil
}
lengths := make([]int, n)
for i := 0; i < n; i++ {
lengths[i] = twwidth.Width(words[i])
}
nbrk := make([]int, n)
cost := make([]int, n)
for i := range cost {
cost[i] = math.MaxInt32
}
remainderLen := lengths[n-1] // Uses updated lengths
for i := n - 1; i >= 0; i-- {
if i < n-1 {
remainderLen += spc + lengths[i]
}
if remainderLen <= lim {
cost[i] = 0
nbrk[i] = n
continue
}
phraseLen := lengths[i]
for j := i + 1; j < n; j++ {
if j > i+1 {
phraseLen += spc + lengths[j-1]
}
d := lim - phraseLen
c := d*d + cost[j]
if phraseLen > lim {
c += pen // too-long lines get a worse penalty
}
if c < cost[i] {
cost[i] = c
nbrk[i] = j
}
}
}
var lines [][]string
i := 0
for i < n {
lines = append(lines, words[i:nbrk[i]])
i = nbrk[i]
}
return lines
}
// getLines decomposes a multiline string into a slice of strings.
func getLines(s string) []string {
return strings.Split(s, nl)
}
+26
View File
@@ -0,0 +1,26 @@
package twwidth
import "github.com/olekukonko/tablewriter/pkg/twcache"
// widthCache stores memoized results of Width calculations to improve performance.
var widthCache *twcache.LRU[cacheKey, int]
type cacheKey struct {
eastAsian bool
str string
}
// SetCacheCapacity changes the cache size dynamically
// If capacity <= 0, disables caching entirely
func SetCacheCapacity(capacity int) {
mu.Lock()
defer mu.Unlock()
if capacity <= 0 {
widthCache = nil // nil = fully disabled
return
}
newCache := twcache.NewLRU[cacheKey, int](capacity)
widthCache = newCache
}
+424
View File
@@ -0,0 +1,424 @@
/*
Package twwidth provides intelligent East Asian width detection.
In 2025/2026, most modern terminal emulators (VSCode, Windows Terminal, iTerm2,
Alacritty) and modern monospace fonts (Hack, Fira Code, Cascadia Code) treat
box-drawing characters as Single Width, regardless of the underlying OS Locale.
Detection Logic (in order of priority):
- RUNEWIDTH_EASTASIAN environment variable (explicit user override)
- Force Legacy Mode (programmatic override for backward compatibility)
- Modern environment detection (VSCode, Windows Terminal, etc. -> Narrow)
- Locale-based detection (CJK locales in traditional terminals -> Wide)
This prioritization ensures that:
- Users can always override behavior using RUNEWIDTH_EASTASIAN
- Modern development environments work correctly by default
- Traditional CJK terminals maintain compatibility via locale checks
Examples:
// Force narrow borders (for Hack font in zh_CN)
RUNEWIDTH_EASTASIAN=0 go run .
// Force wide borders (for legacy CJK terminals)
RUNEWIDTH_EASTASIAN=1 go run .
*/
package twwidth
import (
"os"
"runtime"
"strings"
"sync"
)
// Environment Variable Constants
const (
EnvLCAll = "LC_ALL"
EnvLCCtype = "LC_CTYPE"
EnvLang = "LANG"
EnvRuneWidthEastAsian = "RUNEWIDTH_EASTASIAN"
EnvTerm = "TERM"
EnvTermProgram = "TERM_PROGRAM"
EnvTermProgramWsl = "TERM_PROGRAM_WSL"
EnvWTProfile = "WT_PROFILE_ID" // Windows Terminal
EnvConEmuANSI = "ConEmuANSI" // ConEmu
EnvAlacritty = "ALACRITTY_LOG" // Alacritty
EnvVTEVersion = "VTE_VERSION" // GNOME/VTE
)
const (
overwriteOn = "override_on"
overwriteOff = "override_off"
envModern = "modern_env"
envCjk = "locale_cjk"
envAscii = "default_ascii"
)
// CJK Language Codes (Prefixes)
// Covers ISO 639-1 (2-letter) and common full names used in some systems.
var cjkPrefixes = []string{
"zh", "ja", "ko", // Standard: Chinese, Japanese, Korean
"chi", "zho", // ISO 639-2/B and T for Chinese
"jpn", "kor", // ISO 639-2 for Japanese, Korean
"chinese", "japanese", "korean", // Full names (rare but possible in some legacy systems)
}
// CJK Region Codes
// Checks for specific regions that imply CJK font usage (e.g., en_HK).
var cjkRegions = map[string]bool{
"cn": true, // China
"tw": true, // Taiwan
"hk": true, // Hong Kong
"mo": true, // Macau
"jp": true, // Japan
"kr": true, // South Korea
"kp": true, // North Korea
"sg": true, // Singapore (Often uses CJK fonts)
}
// Modern environments that should use narrow borders (1-width box chars)
var modernEnvironments = map[string]bool{
// Terminal programs
"vscode": true, "visual studio code": true,
"iterm.app": true, "iterm2": true,
"windows terminal": true, "windowsterminal": true,
"alacritty": true, "kitty": true,
"hyper": true, "tabby": true, "terminus": true, "fluentterminal": true,
"warp": true, "ghostty": true, "rio": true,
"jetbrains-jediterm": true,
// Terminal types (TERM signatures)
"xterm-kitty": true, "xterm-ghostty": true, "wezterm": true,
}
var (
eastAsianOnce sync.Once
eastAsianVal bool
// Legacy override control
// Renamed to cfgMu to avoid conflict with width.go's mu
cfgMu sync.RWMutex
forceLegacyEastAsian = false
)
type Enviroment struct {
GOOS string `json:"goos"`
LC_ALL string `json:"lc_all"`
LC_CTYPE string `json:"lc_ctype"`
LANG string `json:"lang"`
RUNEWIDTH_EASTASIAN string `json:"runewidth_eastasian"`
TERM string `json:"term"`
TERM_PROGRAM string `json:"term_program"`
}
// State captures the calculated internal state.
type State struct {
NormalizedLocale string `json:"normalized_locale"`
IsCJKLocale bool `json:"is_cjk_locale"`
IsModernEnv bool `json:"is_modern_env"`
LegacyOverrideMode bool `json:"legacy_override_mode"`
}
// Detection aggregates all debug information regarding East Asian width detection.
type Detection struct {
AutoUseEastAsian bool `json:"auto_use_east_asian"`
DetectionMode string `json:"detection_mode"`
Raw Enviroment `json:"raw"`
Derived State `json:"derived"`
}
// EastAsianForceLegacy forces the detection logic to ignore modern environment checks.
// It relies solely on Locale detection. This is useful for applications that need
// strict backward compatibility.
//
// Note: This does NOT override RUNEWIDTH_EASTASIAN. User environment variables take precedence.
// This should be called before the first table render.
func EastAsianForceLegacy(force bool) {
cfgMu.Lock()
defer cfgMu.Unlock()
forceLegacyEastAsian = force
}
// EastAsianDetect checks the environment variables to determine if
// East Asian width calculations should be enabled.
func EastAsianDetect() bool {
eastAsianOnce.Do(func() {
eastAsianVal = detectEastAsian()
})
return eastAsianVal
}
// EastAsianConservative is a stricter version that only defaults to Narrow
// if the terminal is definitely known to be modern (e.g. VSCode, iTerm2).
// It avoids heuristics like checking "xterm" in the TERM variable.
func EastAsianConservative() bool {
// Check overrides first
if val, found := checkOverrides(); found {
return val
}
// Stricter modern environment detection
if isConservativeModernEnvironment() {
return false
}
// Fall back to locale
return checkLocale()
}
// EastAsianMode returns the decision path used for the current environment.
// Useful for debugging why a specific width was chosen.
func EastAsianMode() string {
// Check override
if val, found := checkOverrides(); found {
if val {
return overwriteOn
}
return overwriteOff
}
cfgMu.RLock()
legacy := forceLegacyEastAsian
cfgMu.RUnlock()
if legacy {
if checkLocale() {
return envCjk
}
return envAscii
}
if isModernEnvironment() {
return envModern
}
if checkLocale() {
return envCjk
}
return envAscii
}
// Debugging returns detailed information about the detection decision.
// Useful for users to include in Github issues.
func Debugging() Detection {
locale := getNormalizedLocale()
cfgMu.RLock()
legacy := forceLegacyEastAsian
cfgMu.RUnlock()
return Detection{
AutoUseEastAsian: EastAsianDetect(),
DetectionMode: EastAsianMode(),
Raw: Enviroment{
GOOS: runtime.GOOS,
LC_ALL: os.Getenv(EnvLCAll),
LC_CTYPE: os.Getenv(EnvLCCtype),
LANG: os.Getenv(EnvLang),
RUNEWIDTH_EASTASIAN: os.Getenv(EnvRuneWidthEastAsian),
TERM: os.Getenv(EnvTerm),
TERM_PROGRAM: os.Getenv(EnvTermProgram),
},
Derived: State{
NormalizedLocale: locale,
IsCJKLocale: isCJKLocale(locale),
IsModernEnv: isModernEnvironment(),
LegacyOverrideMode: legacy,
},
}
}
// detectEastAsian evaluates the environment and locale settings to determine if East Asian width rules should apply.
func detectEastAsian() bool {
// User Override check (Highest Priority)
if val, found := checkOverrides(); found {
return val
}
// Force Legacy Mode check
cfgMu.RLock()
isLegacy := forceLegacyEastAsian
cfgMu.RUnlock()
if isLegacy {
// Legacy mode ignores modern environment checks,
// relying solely on locale.
return checkLocale()
}
// Modern Environment Detection
// If modern, we assume Single Width (return false)
if isModernEnvironment() {
return false
}
// 4. Locale Fallback
return checkLocale()
}
// checkOverrides looks for RUNEWIDTH_EASTASIAN
func checkOverrides() (bool, bool) {
if rw := os.Getenv(EnvRuneWidthEastAsian); rw != "" {
rw = strings.ToLower(rw)
if rw == "0" || rw == "off" || rw == "false" || rw == "no" {
return false, true
}
if rw == "1" || rw == "on" || rw == "true" || rw == "yes" {
return true, true
}
}
return false, false
}
// checkLocale performs the string analysis on LANG/LC_ALL
func checkLocale() bool {
locale := getNormalizedLocale()
if locale == "" {
return false
}
return isCJKLocale(locale)
}
// isModernEnvironment performs comprehensive checks for modern terminal capabilities.
func isModernEnvironment() bool {
// Check TERM_PROGRAM (Most reliable)
if termProg := os.Getenv(EnvTermProgram); termProg != "" {
termProgLower := strings.ToLower(termProg)
if modernEnvironments[termProgLower] {
return true
}
}
// Check WSL specific variable
if os.Getenv(EnvTermProgramWsl) != "" {
return true
}
// Windows Specifics
if runtime.GOOS == "windows" {
// Windows Terminal
if os.Getenv(EnvWTProfile) != "" {
return true
}
// ConEmu/Cmder
if os.Getenv(EnvConEmuANSI) == "ON" {
return true
}
// Modern Windows console (Windows 10+) check via TERM
if term := os.Getenv(EnvTerm); term != "" {
termLower := strings.ToLower(term)
if strings.Contains(termLower, "xterm") ||
strings.Contains(termLower, "vt") {
return true
}
}
}
// VTE-based terminals (GNOME Terminal, Tilix, etc.)
if os.Getenv(EnvVTEVersion) != "" {
return true
}
// Check for Alacritty specifically
if os.Getenv(EnvAlacritty) != "" {
return true
}
// Check TERM for modern terminal signatures
if term := os.Getenv(EnvTerm); term != "" {
termLower := strings.ToLower(term)
// Specific modern terminals often put their name in TERM
if modernEnvironments[termLower] {
return true
}
// Heuristics for standard modern-capable descriptors
if strings.Contains(termLower, "xterm") && !strings.Contains(termLower, "xterm-mono") {
return true
}
if strings.Contains(termLower, "screen") ||
strings.Contains(termLower, "tmux") {
return true
}
}
return false
}
// isConservativeModernEnvironment performs strict checks only for known modern terminals.
func isConservativeModernEnvironment() bool {
termProg := strings.ToLower(os.Getenv(EnvTermProgram))
// Allow-list of definitely modern terminals
switch termProg {
case "vscode", "visual studio code":
return true
case "iterm.app", "iterm2":
return true
case "windows terminal", "windowsterminal":
return true
case "alacritty", "wezterm", "kitty", "ghostty":
return true
case "warp", "tabby", "hyper":
return true
}
// Windows Terminal via specific Env
if os.Getenv(EnvWTProfile) != "" {
return true
}
return false
}
// isCJKLocale determines if a given locale string corresponds to a CJK (Chinese, Japanese, Korean) language or region.
func isCJKLocale(locale string) bool {
// Check Language Prefix
for _, prefix := range cjkPrefixes {
if strings.HasPrefix(locale, prefix) {
return true
}
}
// Check Regions
parts := strings.Split(locale, "_")
if len(parts) > 1 {
for _, part := range parts[1:] {
if cjkRegions[part] {
return true
}
}
}
return false
}
// getNormalizedLocale returns the normalized locale by inspecting environment variables LC_ALL, LC_CTYPE, and LANG.
func getNormalizedLocale() string {
var locale string
if loc := os.Getenv(EnvLCAll); loc != "" {
locale = loc
} else if loc := os.Getenv(EnvLCCtype); loc != "" {
locale = loc
} else if loc := os.Getenv(EnvLang); loc != "" {
locale = loc
}
// Fast fail for empty or standard C/POSIX locales
if locale == "" || locale == "C" || locale == "POSIX" {
return ""
}
// Strip encoding and modifiers
if idx := strings.IndexByte(locale, '.'); idx != -1 {
locale = locale[:idx]
}
if idx := strings.IndexByte(locale, '@'); idx != -1 {
locale = locale[:idx]
}
return strings.ToLower(locale)
}
+288
View File
@@ -0,0 +1,288 @@
package twwidth
import (
"bufio"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
)
type Tab rune
const (
TabWidthDefault = 8
TabString Tab = '\t'
)
// IsTab returns true if t equals the default tab.
func (t Tab) IsTab() bool {
return t == TabString
}
func (t Tab) Byte() byte {
return byte(t)
}
func (t Tab) Rune() rune {
return rune(t)
}
func (t Tab) String() string {
return string(t)
}
// IsTab returns true if r is a tab rune.
func IsTab(r rune) bool {
return r == TabString.Rune()
}
type Tabinal struct {
once sync.Once
width int
mu sync.RWMutex
}
func (t *Tabinal) String() string {
return TabString.String()
}
// Size returns the current tab width, default if unset.
func (t *Tabinal) Size() int {
t.once.Do(t.init)
t.mu.RLock()
w := t.width
t.mu.RUnlock()
if w <= 0 {
return TabWidthDefault
}
return w
}
// SetWidth sets the tab width if valid (132).
func (t *Tabinal) SetWidth(w int) {
if w <= 0 || w > 32 {
return
}
t.mu.Lock()
t.width = w
t.mu.Unlock()
}
func (t *Tabinal) init() {
w := t.detect()
t.mu.Lock()
t.width = w
t.mu.Unlock()
}
// detect determines tab width using env, editorconfig, project, or term.
func (t *Tabinal) detect() int {
if w := envInt("TABWIDTH"); w > 0 {
return clamp(w)
}
if w := envInt("TS"); w > 0 {
return clamp(w)
}
if w := envInt("VIM_TABSTOP"); w > 0 {
return clamp(w)
}
if w := editorConfigTabWidth(); w > 0 {
return w
}
if w := projectHeuristic(); w > 0 {
return w
}
if w := termHeuristic(); w > 0 {
return w
}
return 0
}
func editorConfigTabWidth() int {
dir, err := os.Getwd()
if err != nil {
return 0
}
for dir != "" && dir != "/" && dir != "." {
path := filepath.Join(dir, ".editorconfig")
if w := parseEditorConfig(path); w > 0 {
return clamp(w)
}
parent := filepath.Dir(dir)
if parent == dir {
break
}
dir = parent
}
return 0
}
// parseEditorConfig reads tab_width or indent_size from a file.
func parseEditorConfig(path string) int {
f, err := os.Open(path)
if err != nil {
return 0
}
defer f.Close()
scanner := bufio.NewScanner(f)
inMatch := false
globalWidth := 0
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") {
continue
}
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
pattern := line[1 : len(line)-1]
inMatch = pattern == "*"
knownExts := []string{".go", ".py", ".js", ".ts", ".java", ".rs"}
for _, ext := range knownExts {
if strings.Contains(pattern, ext) {
inMatch = true
break
}
}
continue
}
if !inMatch && globalWidth == 0 {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
val := strings.TrimSpace(parts[1])
switch key {
case "tab_width":
if w, err := strconv.Atoi(val); err == nil && w > 0 {
if inMatch {
return clamp(w)
}
if globalWidth == 0 {
globalWidth = w
}
}
case "indent_size":
if val == "tab" {
continue
}
if w, err := strconv.Atoi(val); err == nil && w > 0 {
if inMatch {
return clamp(w)
}
if globalWidth == 0 {
globalWidth = w
}
}
}
}
return globalWidth
}
// projectHeuristic returns 4 for known project types.
func projectHeuristic() int {
dir, err := os.Getwd()
if err != nil {
return 0
}
indicators := []string{
"go.mod", "go.sum",
"package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml",
"setup.py", "pyproject.toml", "requirements.txt", "Pipfile",
"pom.xml", "build.gradle", "build.gradle.kts",
"Cargo.toml",
"composer.json",
}
for _, indicator := range indicators {
if _, err := os.Stat(filepath.Join(dir, indicator)); err == nil {
return 4
}
}
patterns := []string{"*.go", "*.py", "*.js", "*.ts", "*.java", "*.rs"}
for _, pattern := range patterns {
if matches, _ := filepath.Glob(filepath.Join(dir, pattern)); len(matches) > 0 {
return 4
}
}
return 0
}
// termHeuristic returns a default width based on the TERM variable.
func termHeuristic() int {
termEnv := strings.ToLower(os.Getenv("TERM"))
if termEnv == "" {
return 0
}
if strings.Contains(termEnv, "vt52") {
return 2
}
if strings.Contains(termEnv, "xterm") ||
strings.Contains(termEnv, "screen") ||
strings.Contains(termEnv, "tmux") ||
strings.Contains(termEnv, "linux") ||
strings.Contains(termEnv, "ansi") ||
strings.Contains(termEnv, "rxvt") {
return TabWidthDefault
}
return 0
}
func clamp(w int) int {
if w <= 0 {
return 0
}
if w > 32 {
return 32
}
return w
}
var (
globalTab *Tabinal
globalTabOnce sync.Once
)
// TabInstance returns the singleton Tabinal.
func TabInstance() *Tabinal {
globalTabOnce.Do(func() {
globalTab = &Tabinal{}
})
return globalTab
}
// TabWidth returns the detected global tab width.
func TabWidth() int {
return TabInstance().Size()
}
// SetTabWidth sets the global tab width.
func SetTabWidth(w int) {
TabInstance().SetWidth(w)
}
func envInt(k string) int {
v := os.Getenv(k)
w, _ := strconv.Atoi(v)
return w
}
+419
View File
@@ -0,0 +1,419 @@
package twwidth
import (
"bytes"
"regexp"
"strings"
"sync"
"github.com/clipperhouse/displaywidth"
"github.com/mattn/go-runewidth"
"github.com/olekukonko/tablewriter/pkg/twcache"
)
const (
cacheCapacity = 8192
cachePrefix = "0:"
cacheEastAsianPrefix = "1:"
)
// Options allows for configuring width calculation on a per-call basis.
type Options struct {
EastAsianWidth bool
// Explicitly force box drawing chars to be narrow
// regardless of EastAsianWidth setting.
ForceNarrowBorders bool
}
// globalOptions holds the global displaywidth configuration, including East Asian width settings.
var globalOptions Options
// mu protects access to globalOptions for thread safety.
var mu sync.Mutex
// ansi is a compiled regular expression for stripping ANSI escape codes from strings.
var ansi = Filter()
func init() {
isEastAsian := EastAsianDetect()
cond := runewidth.NewCondition()
cond.EastAsianWidth = isEastAsian
globalOptions = Options{
EastAsianWidth: isEastAsian,
// Auto-enable ForceNarrowBorders for edge cases.
// If EastAsianWidth is ON (e.g. forced via Env Var), but we detect
// a modern environment, we might technically want to narrow borders
// while keeping text wide.
ForceNarrowBorders: isEastAsian && isModernEnvironment(),
}
widthCache = twcache.NewLRU[cacheKey, int](cacheCapacity)
}
// Display calculates the visual width of a string using a specific runewidth.Condition.
// Deprecated: use WidthWithOptions with the new twwidth.Options struct instead.
// This function is kept for backward compatibility.
func Display(cond *runewidth.Condition, str string) int {
opts := Options{EastAsianWidth: cond.EastAsianWidth}
return WidthWithOptions(str, opts)
}
// Filter compiles and returns a regular expression for matching ANSI escape sequences,
// including CSI (Control Sequence Introducer) and OSC (Operating System Command) sequences.
// The returned regex can be used to strip ANSI codes from strings.
func Filter() *regexp.Regexp {
regESC := "\x1b" // ASCII escape character
regBEL := "\x07" // ASCII bell character
// ANSI string terminator: either ESC+\ or BEL
regST := "(" + regexp.QuoteMeta(regESC+"\\") + "|" + regexp.QuoteMeta(regBEL) + ")"
// Control Sequence Introducer (CSI): ESC[ followed by parameters and a final byte
regCSI := regexp.QuoteMeta(regESC+"[") + "[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]"
// Operating System Command (OSC): ESC] followed by arbitrary content until a terminator
regOSC := regexp.QuoteMeta(regESC+"]") + ".*?" + regST
// Combine CSI and OSC patterns into a single regex
return regexp.MustCompile("(" + regCSI + "|" + regOSC + ")")
}
// GetCacheStats returns current cache statistics
func GetCacheStats() (size, capacity int, hitRate float64) {
mu.Lock()
defer mu.Unlock()
if widthCache == nil {
return 0, 0, 0
}
return widthCache.Len(), widthCache.Cap(), widthCache.HitRate()
}
// IsEastAsian returns the current East Asian width setting.
// This function is thread-safe.
//
// Example:
//
// if twdw.IsEastAsian() {
// // Handle East Asian width characters
// }
func IsEastAsian() bool {
mu.Lock()
defer mu.Unlock()
return globalOptions.EastAsianWidth
}
// SetCondition sets the global East Asian width setting based on a runewidth.Condition.
// Deprecated: use SetOptions with the new twwidth.Options struct instead.
// This function is kept for backward compatibility.
func SetCondition(cond *runewidth.Condition) {
mu.Lock()
defer mu.Unlock()
newEastAsianWidth := cond.EastAsianWidth
if globalOptions.EastAsianWidth != newEastAsianWidth {
globalOptions.EastAsianWidth = newEastAsianWidth
widthCache.Purge()
}
}
// SetEastAsian enables or disables East Asian width handling globally.
// This function is thread-safe.
//
// Example:
//
// twdw.SetEastAsian(true) // Enable East Asian width handling
func SetEastAsian(enable bool) {
SetOptions(Options{EastAsianWidth: enable})
}
// SetForceNarrow to preserve the new flag, or create a new setter
func SetForceNarrow(enable bool) {
mu.Lock()
defer mu.Unlock()
globalOptions.ForceNarrowBorders = enable
widthCache.Purge() // Clear cache because widths might change
}
// SetOptions sets the global options for width calculation.
// This function is thread-safe.
func SetOptions(opts Options) {
mu.Lock()
defer mu.Unlock()
if globalOptions.EastAsianWidth != opts.EastAsianWidth || globalOptions.ForceNarrowBorders != opts.ForceNarrowBorders {
globalOptions = opts
widthCache.Purge()
}
}
// Truncate shortens a string to fit within a specified visual width, optionally
// appending a suffix (e.g., "..."). It preserves ANSI escape sequences and adds
// a reset sequence (\x1b[0m) if needed to prevent formatting bleed. The function
// respects the global East Asian width setting and is thread-safe.
//
// If maxWidth is negative, an empty string is returned. If maxWidth is zero and
// a suffix is provided, the suffix is returned. If the string's visual width is
// less than or equal to maxWidth, the string (and suffix, if provided and fits)
// is returned unchanged.
//
// Example:
//
// s := twdw.Truncate("Hello\x1b[31mWorld", 5, "...") // Returns "Hello..."
// s = twdw.Truncate("Hello", 10) // Returns "Hello"
func Truncate(s string, maxWidth int, suffix ...string) string {
if maxWidth < 0 {
return ""
}
suffixStr := strings.Join(suffix, "")
sDisplayWidth := Width(s) // Uses global cached Width
suffixDisplayWidth := Width(suffixStr) // Uses global cached Width
// Case 1: Original string is visually empty.
if sDisplayWidth == 0 {
// If suffix is provided and fits within maxWidth (or if maxWidth is generous)
if len(suffixStr) > 0 && suffixDisplayWidth <= maxWidth {
return suffixStr
}
// If s has ANSI codes (len(s)>0) but maxWidth is 0, can't display them.
if maxWidth == 0 && len(s) > 0 {
return ""
}
return s // Returns "" or original ANSI codes
}
// Case 2: maxWidth is 0, but string has content. Cannot display anything.
if maxWidth == 0 {
return ""
}
// Case 3: String fits completely or fits with suffix.
// Here, maxWidth is the total budget for the line.
if sDisplayWidth <= maxWidth {
// If the string contains ANSI, we must ensure it ends with a reset
// to prevent bleeding, even if we don't truncate.
safeS := s
if strings.Contains(s, "\x1b") && !strings.HasSuffix(s, "\x1b[0m") {
safeS += "\x1b[0m"
}
if len(suffixStr) == 0 { // No suffix.
return safeS
}
// Suffix is provided. Check if s + suffix fits.
if sDisplayWidth+suffixDisplayWidth <= maxWidth {
return safeS + suffixStr
}
// s fits, but s + suffix is too long. Return s (with reset if needed).
return safeS
}
// Case 4: String needs truncation (sDisplayWidth > maxWidth).
// maxWidth is the total budget for the final string (content + suffix).
mu.Lock()
currentOpts := globalOptions
mu.Unlock()
// Special case for EastAsianDetect true: if only suffix fits, return suffix.
// This was derived from previous test behavior.
if len(suffixStr) > 0 && currentOpts.EastAsianWidth {
provisionalContentWidth := maxWidth - suffixDisplayWidth
if provisionalContentWidth == 0 { // Exactly enough space for suffix only
return suffixStr
}
}
// Calculate the budget for the content part, reserving space for the suffix.
targetContentForIteration := maxWidth
if len(suffixStr) > 0 {
targetContentForIteration -= suffixDisplayWidth
}
// If content budget is negative, means not even suffix fits (or no suffix and no space).
// However, if only suffix fits, it should be handled.
if targetContentForIteration < 0 {
// Can we still fit just the suffix?
if len(suffixStr) > 0 && suffixDisplayWidth <= maxWidth {
if strings.Contains(s, "\x1b[") {
return "\x1b[0m" + suffixStr
}
return suffixStr
}
return "" // Cannot fit anything.
}
var contentBuf bytes.Buffer
var currentContentDisplayWidth int
var ansiSeqBuf bytes.Buffer
inAnsiSequence := false
ansiWrittenToContent := false
for _, r := range s {
if r == '\x1b' {
inAnsiSequence = true
ansiSeqBuf.Reset()
ansiSeqBuf.WriteRune(r)
} else if inAnsiSequence {
ansiSeqBuf.WriteRune(r)
seqBytes := ansiSeqBuf.Bytes()
seqLen := len(seqBytes)
terminated := false
if seqLen >= 2 {
introducer := seqBytes[1]
switch introducer {
case '[':
if seqLen >= 3 && r >= 0x40 && r <= 0x7E {
terminated = true
}
case ']':
if r == '\x07' {
terminated = true
} else if seqLen > 1 && seqBytes[seqLen-2] == '\x1b' && r == '\\' { // Check for ST: \x1b\
terminated = true
}
}
}
if terminated {
inAnsiSequence = false
contentBuf.Write(ansiSeqBuf.Bytes())
ansiWrittenToContent = true
ansiSeqBuf.Reset()
}
} else { // Normal character
runeDisplayWidth := calculateRunewidth(r, currentOpts)
if targetContentForIteration == 0 { // No budget for content at all
break
}
if currentContentDisplayWidth+runeDisplayWidth > targetContentForIteration {
break
}
contentBuf.WriteRune(r)
currentContentDisplayWidth += runeDisplayWidth
}
}
result := contentBuf.String()
// Determine if we need to append a reset sequence to prevent color bleeding.
// This is needed if we wrote any ANSI codes or if the input had active codes
// that we might have cut off or left open.
needsReset := false
if (ansiWrittenToContent || (inAnsiSequence && strings.Contains(s, "\x1b["))) && (currentContentDisplayWidth > 0 || ansiWrittenToContent) {
if !strings.HasSuffix(result, "\x1b[0m") {
needsReset = true
}
} else if currentContentDisplayWidth > 0 && strings.Contains(result, "\x1b[") && !strings.HasSuffix(result, "\x1b[0m") && strings.Contains(s, "\x1b[") {
needsReset = true
}
if needsReset {
result += "\x1b[0m"
}
// Suffix is added if provided.
if len(suffixStr) > 0 {
result += suffixStr
}
return result
}
// Width calculates the visual width of a string using the global cache for performance.
// It excludes ANSI escape sequences and accounts for the global East Asian width setting.
// This function is thread-safe.
//
// Example:
//
// width := twdw.Width("Hello\x1b[31mWorld") // Returns 10
func Width(str string) int {
// Fast path ASCII (Optimization)
if len(str) == 1 && str[0] < 0x80 {
// Treat tab as special case even in fast path
if IsTab(rune(str[0])) {
return TabWidth()
}
return 1
}
mu.Lock()
currentOpts := globalOptions
mu.Unlock()
key := cacheKey{
eastAsian: currentOpts.EastAsianWidth,
str: str,
}
// Check Cache (Optimization)
if w, found := widthCache.Get(key); found {
return w
}
//stripped := ansi.ReplaceAllLiteralString(str, "")
calculatedWidth := 0
for _, r := range strip(str) {
calculatedWidth += calculateRunewidth(r, currentOpts)
}
// Store in Cache
widthCache.Add(key, calculatedWidth)
return calculatedWidth
}
// WidthNoCache calculates the visual width of a string without using the global cache.
//
// Example:
//
// width := twdw.WidthNoCache("Hello\x1b[31mWorld") // Returns 10
func WidthNoCache(str string) int {
// This function's behavior is equivalent to a one-shot calculation
// using the current global options. The WidthWithOptions function
// does not interact with the cache, thus fulfilling the requirement.
mu.Lock()
opts := globalOptions
mu.Unlock()
return WidthWithOptions(str, opts)
}
// WidthWithOptions calculates the visual width of a string with specific options,
// bypassing the global settings and cache. This is useful for one-shot calculations
// where global state is not desired.
func WidthWithOptions(str string, opts Options) int {
// stripped := ansi.ReplaceAllLiteralString(str, "")
calculatedWidth := 0
for _, r := range strip(str) {
calculatedWidth += calculateRunewidth(r, opts)
}
return calculatedWidth
}
// calculateRunewidth calculates the width of a single rune based on the provided options.
// It applies narrow overrides for box drawing characters if configured and handles Tabs.
func calculateRunewidth(r rune, opts Options) int {
if opts.ForceNarrowBorders && isBoxDrawingChar(r) {
return 1
}
// Explicitly handle Tabinal to ensure tables have enough space
// when TrimTab is Off.
if IsTab(r) {
return TabWidth()
}
dwOpts := displaywidth.Options{EastAsianWidth: opts.EastAsianWidth}
return dwOpts.Rune(r)
}
// isBoxDrawingChar checks if a rune is within the Unicode Box Drawing range.
func isBoxDrawingChar(r rune) bool {
return r >= 0x2500 && r <= 0x257F
}
func strip(s string) string {
if strings.IndexByte(s, '\x1b') == -1 {
return s
}
return ansi.ReplaceAllLiteralString(s, "")
}