build(deps): bump github.com/olekukonko/tablewriter from 1.1.2 to 1.1.3

Bumps [github.com/olekukonko/tablewriter](https://github.com/olekukonko/tablewriter) from 1.1.2 to 1.1.3.
- [Release notes](https://github.com/olekukonko/tablewriter/releases)
- [Commits](https://github.com/olekukonko/tablewriter/compare/v1.1.2...v1.1.3)

---
updated-dependencies:
- dependency-name: github.com/olekukonko/tablewriter
  dependency-version: 1.1.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
This commit is contained in:
dependabot[bot]
2026-01-23 10:16:56 +01:00
committed by Ralf Haferkamp
parent 6455052fa6
commit 056039b624
22 changed files with 1186 additions and 690 deletions
+5
View File
@@ -667,3 +667,8 @@ func Inspect(values ...interface{}) {
o := NewInspector(defaultLogger)
o.Log(2, values...)
}
func Apply(opts ...Option) *Logger {
return defaultLogger.Apply(opts...)
}
+12 -8
View File
@@ -29,6 +29,7 @@ type Palette struct {
Info string // Color for Info level messages
Warn string // Color for Warn level messages
Error string // Color for Error level messages
Fatal string // Color for Fatal level messages
Title string // Color for dump titles (BEGIN/END separators)
}
@@ -47,10 +48,11 @@ var darkPalette = Palette{
Hex: "\033[38;5;156m", // Light green for hex values
Ascii: "\033[38;5;224m", // Light pink for ASCII values
Debug: "\033[36m", // Cyan for Debug level
Info: "\033[32m", // Green for Info level
Warn: "\033[33m", // Yellow for Warn level
Error: "\033[31m", // Red for Error level
Debug: "\033[36m", // Cyan for Debug level
Info: "\033[32m", // Green for Info level
Warn: "\033[33m", // Yellow for Warn level
Error: "\033[31m", // Standard red
Fatal: "\033[1;31m", // Bold red - stands out more
}
// lightPalette defines colors optimized for light terminal backgrounds.
@@ -68,10 +70,11 @@ var lightPalette = Palette{
Hex: "\033[38;5;156m", // Light green for hex values
Ascii: "\033[38;5;224m", // Light pink for ASCII values
Debug: "\033[36m", // Cyan for Debug level
Info: "\033[32m", // Green for Info level
Warn: "\033[33m", // Yellow for Warn level
Error: "\033[31m", // Red for Error level
Debug: "\033[36m", // Cyan for Debug level
Info: "\033[32m", // Green for Info level
Warn: "\033[33m", // Yellow for Warn level
Error: "\033[31m", // Standard red
Fatal: "\033[1;31m", // Bold red - stands out more
}
// ColorizedHandler is a handler that outputs log entries with ANSI color codes.
@@ -250,6 +253,7 @@ func (h *ColorizedHandler) formatLevel(b *strings.Builder, e *lx.Entry) {
lx.LevelInfo: h.palette.Info, // Green
lx.LevelWarn: h.palette.Warn, // Yellow
lx.LevelError: h.palette.Error, // Red
lx.LevelFatal: h.palette.Fatal, // Bold Red
}[e.Level]
b.WriteString(color)
+13
View File
@@ -3,6 +3,7 @@ package lh
import (
"errors"
"fmt"
"github.com/olekukonko/ll/lx"
)
@@ -30,6 +31,18 @@ func NewMultiHandler(h ...lx.Handler) *MultiHandler {
}
}
// Len returns the number of handlers in the MultiHandler.
func (h *MultiHandler) Len() int {
return len(h.Handlers)
}
// Append adds one or more lx.Handler instances to the MultiHandler's list of handlers.
func (h *MultiHandler) Append(handlers ...lx.Handler) {
for _, e := range handlers {
h.Handlers = append(h.Handlers, e)
}
}
// Handle implements the Handler interface, calling Handle on each handler in sequence.
// It collects any errors from handlers and combines them into a single error using errors.Join.
// If no errors occur, it returns nil. Thread-safe if the underlying handlers are thread-safe.
+3 -2
View File
@@ -2,8 +2,9 @@ package lh
import (
"context"
"github.com/olekukonko/ll/lx"
"log/slog"
"github.com/olekukonko/ll/lx"
)
// SlogHandler adapts a slog.Handler to implement lx.Handler.
@@ -81,7 +82,7 @@ func toSlogLevel(level lx.LevelType) slog.Level {
return slog.LevelInfo
case lx.LevelWarn:
return slog.LevelWarn
case lx.LevelError:
case lx.LevelError, lx.LevelFatal:
return slog.LevelError
default:
return slog.LevelInfo // Default for unknown levels
+66 -62
View File
@@ -39,6 +39,8 @@ type Logger struct {
stackBufferSize int // Buffer size for capturing stack traces
separator string // Separator for namespace paths (e.g., "/")
entries atomic.Int64 // Tracks total log entries sent to handler
fatalExits bool
fatalStack bool
}
// New creates a new Logger with the given namespace and optional configurations.
@@ -71,22 +73,71 @@ func New(namespace string, opts ...Option) *Logger {
return logger
}
// AddContext adds a key-value pair to the logger's context, modifying it directly.
// Unlike Context, it mutates the existing context. It is thread-safe using a write lock.
// Apply applies one or more functional options to the default/global logger.
// Useful for late configuration (e.g., after migration, attach VictoriaLogs handler,
// set level, add middleware, etc.) without changing existing New() calls.
//
// Example:
//
// logger := New("app").Enable()
// logger.AddContext("user", "alice")
// logger.Info("Action") // Output: [app] INFO: Action [user=alice]
func (l *Logger) AddContext(key string, value interface{}) *Logger {
// // In main() or init(), after setting up handler
// ll.Apply(
// ll.Handler(vlBatched),
// ll.Level(ll.LevelInfo),
// ll.Use(rateLimiterMiddleware),
// )
//
// Returns the default logger for chaining (if needed).
func (l *Logger) Apply(opts ...Option) *Logger {
l.mu.Lock()
defer l.mu.Unlock()
for _, opt := range opts {
if opt != nil {
opt(l)
}
}
return l
}
// AddContext adds one or more key-value pairs to the logger's persistent context.
// These fields will be included in **every** subsequent log message from this logger
// (and its child namespace loggers).
//
// It supports variadic key-value pairs (string key, any value).
// Non-string keys or uneven number of arguments will be safely ignored/logged.
//
// Returns the logger for chaining.
//
// Examples:
//
// logger.AddContext("user", "alice", "env", "prod")
// logger.AddContext("request_id", reqID, "trace_id", traceID)
// logger.AddContext("service", "payment") // single pair
func (l *Logger) AddContext(pairs ...any) *Logger {
l.mu.Lock()
defer l.mu.Unlock()
// Initialize context map if nil
// Lazy initialization of context map
if l.context == nil {
l.context = make(map[string]interface{})
}
l.context[key] = value
// Process key-value pairs
for i := 0; i < len(pairs)-1; i += 2 {
key, ok := pairs[i].(string)
if !ok {
l.Warnf("AddContext: non-string key at index %d: %v", i, pairs[i])
continue
}
value := pairs[i+1]
l.context[key] = value
}
// Optional: warn about uneven number of arguments
if len(pairs)%2 != 0 {
l.Warn("AddContext: uneven number of arguments, last value ignored")
}
return l
}
@@ -357,6 +408,7 @@ func (l *Logger) Output(values ...interface{}) {
l.output(2, values...)
}
// mark logs the caller's file and line number along with an optional custom name label for tracing execution flow.
func (l *Logger) output(skip int, values ...interface{}) {
if !l.shouldLog(lx.LevelInfo) {
return
@@ -536,8 +588,10 @@ func (l *Logger) Fatal(args ...any) {
os.Exit(1)
}
l.log(lx.LevelError, lx.ClassText, cat.Space(args...), nil, false)
os.Exit(1)
l.log(lx.LevelFatal, lx.ClassText, cat.Space(args...), nil, l.fatalStack)
if l.fatalExits {
os.Exit(1)
}
}
// Fatalf logs a formatted message at Error level with a stack trace and exits the program.
@@ -795,6 +849,7 @@ func (l *Logger) Mark(name ...string) {
l.mark(2, name...)
}
// mark logs the caller's file and line number along with an optional custom name label for tracing execution flow.
func (l *Logger) mark(skip int, names ...string) {
// Skip logging if Info level is not enabled
if !l.shouldLog(lx.LevelInfo) {
@@ -978,7 +1033,7 @@ func (l *Logger) Panic(args ...any) {
panic(msg)
}
l.log(lx.LevelError, lx.ClassText, msg, nil, true)
l.log(lx.LevelFatal, lx.ClassText, msg, nil, true)
panic(msg)
}
@@ -1459,54 +1514,3 @@ func (l *Logger) shouldLog(level lx.LevelType) bool {
return true
}
// WithHandler sets the handler for the logger as a functional option for configuring
// a new logger instance.
// Example:
//
// logger := New("app", WithHandler(lh.NewJSONHandler(os.Stdout)))
func WithHandler(handler lx.Handler) Option {
return func(l *Logger) {
l.handler = handler
}
}
// WithTimestamped returns an Option that configures timestamp settings for the logger's existing handler.
// It enables or disables timestamp logging and optionally sets the timestamp format if the handler
// supports the lx.Timestamper interface. If no handler is set, the function has no effect.
// Parameters:
//
// enable: Boolean to enable or disable timestamp logging
// format: Optional string(s) to specify the timestamp format
func WithTimestamped(enable bool, format ...string) Option {
return func(l *Logger) {
if l.handler != nil { // Check if a handler is set
// Verify if the handler supports the lx.Timestamper interface
if h, ok := l.handler.(lx.Timestamper); ok {
h.Timestamped(enable, format...) // Apply timestamp settings to the handler
}
}
}
}
// WithLevel sets the minimum log level for the logger as a functional option for
// configuring a new logger instance.
// Example:
//
// logger := New("app", WithLevel(lx.LevelWarn))
func WithLevel(level lx.LevelType) Option {
return func(l *Logger) {
l.level = level
}
}
// WithStyle sets the namespace formatting style for the logger as a functional option
// for configuring a new logger instance.
// Example:
//
// logger := New("app", WithStyle(lx.NestedPath))
func WithStyle(style lx.StyleType) Option {
return func(l *Logger) {
l.style = style
}
}
+6 -1
View File
@@ -36,6 +36,7 @@ const (
LevelInfo // Info level for general operational messages
LevelWarn // Warn level for warning conditions
LevelError // Error level for error conditions requiring attention
LevelFatal // Fatal level for critical error conditions
LevelDebug // None level for logs without a specific severity (e.g., raw output)
LevelUnknown // None level for logs without a specific severity (e.g., raw output)
)
@@ -45,7 +46,9 @@ const (
DebugString = "DEBUG"
InfoString = "INFO"
WarnString = "WARN"
WarningString = "WARNING"
ErrorString = "ERROR"
FatalString = "FATAL"
NoneString = "NONE"
UnknownString = "UNKNOWN"
@@ -98,6 +101,8 @@ func (l LevelType) String() string {
return WarnString
case LevelError:
return ErrorString
case LevelFatal:
return FatalString
case LevelNone:
return NoneString
default:
@@ -114,7 +119,7 @@ func LevelParse(s string) LevelType {
return LevelDebug
case InfoString:
return LevelInfo
case WarnString, "WARNING": // Allow both "WARN" and "WARNING"
case WarnString, WarningString: // Allow both "WARN" and "WARNING"
return LevelWarn
case ErrorString:
return LevelError
+67
View File
@@ -0,0 +1,67 @@
package ll
import "github.com/olekukonko/ll/lx"
// WithHandler sets the handler for the logger as a functional option for configuring
// a new logger instance.
// Example:
//
// logger := New("app", WithHandler(lh.NewJSONHandler(os.Stdout)))
func WithHandler(handler lx.Handler) Option {
return func(l *Logger) {
l.handler = handler
}
}
// WithTimestamped returns an Option that configures timestamp settings for the logger's existing handler.
// It enables or disables timestamp logging and optionally sets the timestamp format if the handler
// supports the lx.Timestamper interface. If no handler is set, the function has no effect.
// Parameters:
//
// enable: Boolean to enable or disable timestamp logging
// format: Optional string(s) to specify the timestamp format
func WithTimestamped(enable bool, format ...string) Option {
return func(l *Logger) {
if l.handler != nil { // Check if a handler is set
// Verify if the handler supports the lx.Timestamper interface
if h, ok := l.handler.(lx.Timestamper); ok {
h.Timestamped(enable, format...) // Apply timestamp settings to the handler
}
}
}
}
// WithLevel sets the minimum log level for the logger as a functional option for
// configuring a new logger instance.
// Example:
//
// logger := New("app", WithLevel(lx.LevelWarn))
func WithLevel(level lx.LevelType) Option {
return func(l *Logger) {
l.level = level
}
}
// WithStyle sets the namespace formatting style for the logger as a functional option
// for configuring a new logger instance.
// Example:
//
// logger := New("app", WithStyle(lx.NestedPath))
func WithStyle(style lx.StyleType) Option {
return func(l *Logger) {
l.style = style
}
}
// Functional options (can be passed to New() or applied later)
func WithFatalExits(enabled bool) Option {
return func(l *Logger) {
l.fatalExits = enabled
}
}
func WithFatalStack(enabled bool) Option {
return func(l *Logger) {
l.fatalStack = enabled
}
}
+3 -3
View File
@@ -28,7 +28,7 @@ go get github.com/olekukonko/tablewriter@v0.0.5
#### Latest Version
The latest stable version
```bash
go get github.com/olekukonko/tablewriter@v1.1.2
go get github.com/olekukonko/tablewriter@v1.1.3
```
**Warning:** Version `v1.0.0` contains missing functionality and should not be used.
@@ -62,7 +62,7 @@ func main() {
data := [][]string{
{"Package", "Version", "Status"},
{"tablewriter", "v0.0.5", "legacy"},
{"tablewriter", "v1.1.2", "latest"},
{"tablewriter", "v1.1.3", "latest"},
}
table := tablewriter.NewWriter(os.Stdout)
@@ -77,7 +77,7 @@ func main() {
│ PACKAGE │ VERSION │ STATUS │
├─────────────┼─────────┼────────┤
│ tablewriter │ v0.0.5 │ legacy │
│ tablewriter │ v1.1.2 │ latest │
│ tablewriter │ v1.1.3 │ latest │
└─────────────┴─────────┴────────┘
```
+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)
}
+150 -87
View File
@@ -1,3 +1,4 @@
// width.go
package twwidth
import (
@@ -21,6 +22,10 @@ const (
// 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.
@@ -36,12 +41,25 @@ var widthCache *twcache.LRU[string, int]
var ansi = Filter()
func init() {
// Initialize global options by detecting from the environment,
// which is the one key feature we get from go-runewidth.
isEastAsian := EastAsianDetect()
cond := runewidth.NewCondition()
cond.EastAsianWidth = isEastAsian
globalOptions = Options{
EastAsianWidth: cond.EastAsianWidth,
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.
//
// Note: In the standard EastAsian logic, isEastAsian will
// ALREADY be false for modern environments, so this boolean implies
// a specific "Forced On" scenario.
ForceNarrowBorders: isEastAsian && isModernEnvironment(),
}
widthCache = twcache.NewLRU[string, int](cacheCapacity)
}
@@ -55,6 +73,14 @@ func makeCacheKey(str string, eastAsianWidth bool) string {
return cachePrefix + str
}
// 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.
@@ -73,25 +99,15 @@ func Filter() *regexp.Regexp {
return regexp.MustCompile("(" + regCSI + "|" + regOSC + ")")
}
// SetOptions sets the global options for width calculation.
// This function is thread-safe.
func SetOptions(opts Options) {
// GetCacheStats returns current cache statistics
func GetCacheStats() (size, capacity int, hitRate float64) {
mu.Lock()
defer mu.Unlock()
if globalOptions.EastAsianWidth != opts.EastAsianWidth {
globalOptions = opts
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})
if widthCache == nil {
return 0, 0, 0
}
return widthCache.Len(), widthCache.Cap(), widthCache.HitRate()
}
// IsEastAsian returns the current East Asian width setting.
@@ -108,6 +124,22 @@ func IsEastAsian() bool {
return globalOptions.EastAsianWidth
}
// 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[string, int](capacity)
widthCache = newCache
}
// 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) {
@@ -120,55 +152,33 @@ func SetCondition(cond *runewidth.Condition) {
}
}
// 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.
// SetEastAsian enables or disables East Asian width handling globally.
// This function is thread-safe.
//
// Example:
//
// width := twdw.Width("Hello\x1b[31mWorld") // Returns 10
func Width(str string) int {
currentEA := IsEastAsian()
key := makeCacheKey(str, currentEA)
// twdw.SetEastAsian(true) // Enable East Asian width handling
func SetEastAsian(enable bool) {
SetOptions(Options{EastAsianWidth: enable})
}
if w, found := widthCache.Get(key); found {
return w
// 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()
}
opts := displaywidth.Options{EastAsianWidth: currentEA}
stripped := ansi.ReplaceAllLiteralString(str, "")
calculatedWidth := opts.String(stripped)
widthCache.Add(key, calculatedWidth)
return calculatedWidth
}
// 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 {
dwOpts := displaywidth.Options{EastAsianWidth: opts.EastAsianWidth}
stripped := ansi.ReplaceAllLiteralString(str, "")
return dwOpts.String(stripped)
}
// 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.
return WidthWithOptions(str, Options{EastAsianWidth: IsEastAsian()})
}
// 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)
}
// Truncate shortens a string to fit within a specified visual width, optionally
@@ -235,11 +245,13 @@ func Truncate(s string, maxWidth int, suffix ...string) string {
// Case 4: String needs truncation (sDisplayWidth > maxWidth).
// maxWidth is the total budget for the final string (content + suffix).
currentGlobalEastAsianWidth := IsEastAsian()
mu.Lock()
currentOpts := globalOptions
mu.Unlock()
// Special case for EastAsian true: if only suffix fits, return suffix.
// Special case for EastAsianDetect true: if only suffix fits, return suffix.
// This was derived from previous test behavior.
if len(suffixStr) > 0 && currentGlobalEastAsianWidth {
if len(suffixStr) > 0 && currentOpts.EastAsianWidth {
provisionalContentWidth := maxWidth - suffixDisplayWidth
if provisionalContentWidth == 0 { // Exactly enough space for suffix only
return suffixStr
@@ -271,8 +283,6 @@ func Truncate(s string, maxWidth int, suffix ...string) string {
inAnsiSequence := false
ansiWrittenToContent := false
dwOpts := displaywidth.Options{EastAsianWidth: currentGlobalEastAsianWidth}
for _, r := range s {
if r == '\x1b' {
inAnsiSequence = true
@@ -305,7 +315,7 @@ func Truncate(s string, maxWidth int, suffix ...string) string {
ansiSeqBuf.Reset()
}
} else { // Normal character
runeDisplayWidth := dwOpts.Rune(r)
runeDisplayWidth := calculateRunewidth(r, currentOpts)
if targetContentForIteration == 0 { // No budget for content at all
break
}
@@ -342,28 +352,81 @@ func Truncate(s string, maxWidth int, suffix ...string) string {
return result
}
// 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
// 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 {
return 1
}
newCache := twcache.NewLRU[string, int](capacity)
widthCache = newCache
}
// GetCacheStats returns current cache statistics
func GetCacheStats() (size, capacity int, hitRate float64) {
mu.Lock()
defer mu.Unlock()
currentOpts := globalOptions
mu.Unlock()
if widthCache == nil {
return 0, 0, 0
key := makeCacheKey(str, currentOpts.EastAsianWidth)
// Check Cache (Optimization)
if w, found := widthCache.Get(key); found {
return w
}
return widthCache.Len(), widthCache.Cap(), widthCache.HitRate()
stripped := ansi.ReplaceAllLiteralString(str, "")
calculatedWidth := 0
for _, r := range stripped {
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 stripped {
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.
func calculateRunewidth(r rune, opts Options) int {
if opts.ForceNarrowBorders && isBoxDrawingChar(r) {
return 1
}
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
}
-23
View File
@@ -4,7 +4,6 @@ import (
"io"
"strings"
"github.com/fatih/color"
"github.com/olekukonko/ll"
"github.com/olekukonko/ll/lh"
"github.com/olekukonko/tablewriter/pkg/twwidth"
@@ -24,28 +23,6 @@ type ColorizedConfig struct {
Symbols tw.Symbols // Symbols for table drawing (e.g., corners, lines)
}
// Colors is a slice of color attributes for use with fatih/color, such as color.FgWhite or color.Bold.
type Colors []color.Attribute
// Tint defines foreground and background color settings for table elements, with optional per-column overrides.
type Tint struct {
FG Colors // Foreground color attributes
BG Colors // Background color attributes
Columns []Tint // Per-column color settings
}
// Apply applies the Tint's foreground and background colors to the given text, returning the text unchanged if no colors are set.
func (t Tint) Apply(text string) string {
if len(t.FG) == 0 && len(t.BG) == 0 {
return text
}
// Combine foreground and background colors
combinedColors := append(t.FG, t.BG...)
// Create a color function and apply it to the text
c := color.New(combinedColors...).SprintFunc()
return c(text)
}
// Colorized renders colored ASCII tables with customizable borders, colors, and alignments.
type Colorized struct {
config ColorizedConfig // Renderer configuration
+25
View File
@@ -0,0 +1,25 @@
package renderer
import "github.com/fatih/color"
// Colors is a slice of color attributes for use with fatih/color, such as color.FgWhite or color.Bold.
type Colors []color.Attribute
// Tint defines foreground and background color settings for table elements, with optional per-column overrides.
type Tint struct {
FG Colors // Foreground color attributes
BG Colors // Background color attributes
Columns []Tint // Per-column color settings
}
// Apply applies the Tint's foreground and background colors to the given text, returning the text unchanged if no colors are set.
func (t Tint) Apply(text string) string {
if len(t.FG) == 0 && len(t.BG) == 0 {
return text
}
// Combine foreground and background colors
combinedColors := append(t.FG, t.BG...)
// Create a color function and apply it to the text
c := color.New(combinedColors...).SprintFunc()
return c(text)
}
+20 -5
View File
@@ -4,7 +4,6 @@ import (
"bytes"
"io"
"math"
"os"
"reflect"
"runtime"
"strings"
@@ -419,7 +418,6 @@ func (t *Table) Options(opts ...Option) *Table {
}
// force debugging mode if set
// This should be move away form WithDebug
if t.config.Debug {
t.logger.Enable()
t.logger.Resume()
@@ -434,11 +432,28 @@ func (t *Table) Options(opts ...Option) *Table {
goArch := runtime.GOARCH
numCPU := runtime.NumCPU()
t.logger.Infof("Environment: LC_CTYPE=%s, LANG=%s, TERM=%s", os.Getenv("LC_CTYPE"), os.Getenv("LANG"), os.Getenv("TERM"))
t.logger.Infof("Go Runtime: Version=%s, OS=%s, Arch=%s, CPUs=%d", goVersion, goOS, goArch, numCPU)
// Use the new struct-based info.
// No type assertions or magic strings needed.
info := twwidth.Debugging()
t.logger.Infof("Go Runtime: Version=%s, OS=%s, Arch=%s, CPUs=%d",
goVersion, goOS, goArch, numCPU)
t.logger.Infof("Environment: LC_CTYPE=%s, LANG=%s, TERM=%s, TERM_PROGRAM=%s",
info.Raw.LC_CTYPE,
info.Raw.LANG,
info.Raw.TERM,
info.Raw.TERM_PROGRAM,
)
t.logger.Infof("East Asian Detection: Auto=%v, Mode=%s, ModernEnv=%v, CJKLocale=%v",
info.AutoUseEastAsian,
info.DetectionMode,
info.Derived.IsModernEnv,
info.Derived.IsCJKLocale,
)
// send logger to renderer
// this will overwrite the default logger
t.renderer.Logger(t.logger)
return t
}
+9 -13
View File
@@ -991,7 +991,7 @@ func (t *Table) calculateContentMaxWidth(colIdx int, config tw.CellConfig, padLe
constraintTotalCellWidth := 0
hasConstraint := false
// 1. Check new Widths.PerColumn (highest priority)
// Check new Widths.PerColumn (highest priority)
if t.config.Widths.Constrained() {
if colWidth, ok := t.config.Widths.PerColumn.OK(colIdx); ok && colWidth > 0 {
@@ -1001,7 +1001,7 @@ func (t *Table) calculateContentMaxWidth(colIdx int, config tw.CellConfig, padLe
colIdx, constraintTotalCellWidth)
}
// 2. Check new Widths.Global
// Check new Widths.Global
if !hasConstraint && t.config.Widths.Global > 0 {
constraintTotalCellWidth = t.config.Widths.Global
hasConstraint = true
@@ -1009,7 +1009,7 @@ func (t *Table) calculateContentMaxWidth(colIdx int, config tw.CellConfig, padLe
}
}
// 3. Fall back to legacy ColMaxWidths.PerColumn (backward compatibility)
// Fall back to legacy ColMaxWidths.PerColumn (backward compatibility)
if !hasConstraint && config.ColMaxWidths.PerColumn != nil {
if colMax, ok := config.ColMaxWidths.PerColumn.OK(colIdx); ok && colMax > 0 {
constraintTotalCellWidth = colMax
@@ -1019,7 +1019,7 @@ func (t *Table) calculateContentMaxWidth(colIdx int, config tw.CellConfig, padLe
}
}
// 4. Fall back to legacy ColMaxWidths.Global
// Fall back to legacy ColMaxWidths.Global
if !hasConstraint && config.ColMaxWidths.Global > 0 {
constraintTotalCellWidth = config.ColMaxWidths.Global
hasConstraint = true
@@ -1027,7 +1027,7 @@ func (t *Table) calculateContentMaxWidth(colIdx int, config tw.CellConfig, padLe
constraintTotalCellWidth)
}
// 5. Fall back to table MaxWidth if auto-wrapping
// Fall back to table MaxWidth if auto-wrapping
if !hasConstraint && t.config.MaxWidth > 0 && config.Formatting.AutoWrap != tw.WrapNone {
constraintTotalCellWidth = t.config.MaxWidth
hasConstraint = true
@@ -1217,14 +1217,10 @@ func (t *Table) convertToString(value interface{}) string {
// convertItemToCells is responsible for converting a single input item (which could be
// a struct, a basic type, or an item implementing Stringer/Formatter) into a slice
// of strings, where each string represents a cell for the table row.
// zoo.go
// convertItemToCells is responsible for converting a single input item into a slice of strings.
// It now uses the unified struct parser for structs.
func (t *Table) convertItemToCells(item interface{}) ([]string, error) {
t.logger.Debugf("convertItemToCells: Converting item of type %T", item)
// 1. User-defined table-wide stringer (t.stringer) takes highest precedence.
// User-defined table-wide stringer (t.stringer) takes highest precedence.
if t.stringer != nil {
res, err := t.convertToStringer(item)
if err == nil {
@@ -1234,13 +1230,13 @@ func (t *Table) convertItemToCells(item interface{}) ([]string, error) {
t.logger.Warnf("convertItemToCells: Custom table stringer was set but incompatible for type %T: %v. Will attempt other methods.", item, err)
}
// 2. Handle untyped nil directly.
// Handle untyped nil directly.
if item == nil {
t.logger.Debugf("convertItemToCells: Item is untyped nil. Returning single empty cell.")
return []string{""}, nil
}
// 3. Use the new unified struct parser. It handles pointers and embedding.
// Use the new unified struct parser. It handles pointers and embedding.
// We only care about the values it returns.
_, values := t.extractFieldsAndValuesFromStruct(item)
if values != nil {
@@ -1248,7 +1244,7 @@ func (t *Table) convertItemToCells(item interface{}) ([]string, error) {
return values, nil
}
// 4. Fallback for any other single item (e.g., basic types, or types that implement Stringer/Formatter).
// Fallback for any other single item (e.g., basic types, or types that implement Stringer/Formatter).
// This code path is now for non-struct types.
if formatter, ok := item.(tw.Formatter); ok {
t.logger.Debugf("convertItemToCells: Item (non-struct, type %T) is tw.Formatter. Using Format().", item)