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

Bumps [github.com/olekukonko/tablewriter](https://github.com/olekukonko/tablewriter) from 1.1.1 to 1.1.2.
- [Commits](https://github.com/olekukonko/tablewriter/compare/v1.1.1...v1.1.2)

---
updated-dependencies:
- dependency-name: github.com/olekukonko/tablewriter
  dependency-version: 1.1.2
  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-14 17:03:18 +01:00
committed by Ralf Haferkamp
parent e9bd5c4058
commit 063217c3e6
26 changed files with 2363 additions and 1836 deletions
+7
View File
@@ -657,6 +657,13 @@ func Mark(names ...string) {
// It is similar to Dbg but formats the output as JSON for better readability. It is thread-safe and respects
// the loggers configuration (e.g., enabled, level, suspend, handler, middleware).
func Output(values ...interface{}) {
defaultLogger.output(2, values...)
}
// Inspect logs one or more values in a **developer-friendly, deeply introspective format** at Info level.
// It includes the caller file and line number, and reveals **all fields** — including:
func Inspect(values ...interface{}) {
o := NewInspector(defaultLogger)
o.Log(2, values...)
}
+1 -1
View File
@@ -79,7 +79,7 @@ func (o *Inspector) Log(skip int, values ...interface{}) {
}
// Construct log message with file, line, and JSON data
msg := fmt.Sprintf("[%s:%d] DUMP: %s", shortFile, line, string(jsonData))
msg := fmt.Sprintf("[%s:%d] INSPECT: %s", shortFile, line, string(jsonData))
o.logger.log(lx.LevelInfo, lx.ClassText, msg, nil, false)
}
}
+51 -10
View File
@@ -350,17 +350,58 @@ func (l *Logger) Dump(values ...interface{}) {
}
}
// Output logs data in a human-readable JSON format at Info level, including caller file and line information.
// It is similar to Dbg but formats the output as JSON for better readability. It is thread-safe and respects
// the logger's configuration (e.g., enabled, level, suspend, handler, middleware).
// Example:
//
// logger := New("app").Enable()
// x := map[string]int{"key": 42}
// logger.Output(x) // Output: [app] INFO: [file.go:123] JSON: {"key": 42}
//
// Logger method to provide access to Output functionality
// Output logs each value as pretty-printed JSON for REST debugging.
// Each value is logged on its own line with [file:line] and a blank line after the header.
// Ideal for inspecting outgoing/incoming REST payloads.
func (l *Logger) Output(values ...interface{}) {
l.output(2, values...)
}
func (l *Logger) output(skip int, values ...interface{}) {
if !l.shouldLog(lx.LevelInfo) {
return
}
_, file, line, ok := runtime.Caller(skip)
if !ok {
return
}
shortFile := file
if idx := strings.LastIndex(file, "/"); idx >= 0 {
shortFile = file[idx+1:]
}
header := fmt.Sprintf("[%s:%d] JSON:\n", shortFile, line)
for _, v := range values {
// Always pretty-print with indent
b, err := json.MarshalIndent(v, " ", " ")
if err != nil {
b, _ = json.MarshalIndent(map[string]any{
"value": fmt.Sprintf("%+v", v),
"error": err.Error(),
}, " ", " ")
}
l.log(lx.LevelInfo, lx.ClassText, header+string(b), nil, false)
}
}
// Inspect logs one or more values in a **developer-friendly, deeply introspective format** at Info level.
// It includes the caller file and line number, and reveals **all fields** — including:
//
// - Private (unexported) fields → prefixed with `(field)`
// - Embedded structs (inlined)
// - Pointers and nil values → shown as `*(field)` or `nil`
// - Full struct nesting and type information
//
// This method uses `NewInspector` under the hood, which performs **full reflection-based traversal**.
// It is **not** meant for production logging or REST APIs — use `Output` for that.
//
// Ideal for:
// - Debugging complex internal state
// - Inspecting structs with private fields
// - Understanding struct embedding and pointer behavior
func (l *Logger) Inspect(values ...interface{}) {
o := NewInspector(l)
o.Log(2, values...)
}
+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.1
go get github.com/olekukonko/tablewriter@v1.1.2
```
**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.1", "latest"},
{"tablewriter", "v1.1.2", "latest"},
}
table := tablewriter.NewWriter(os.Stdout)
@@ -77,7 +77,7 @@ func main() {
│ PACKAGE │ VERSION │ STATUS │
├─────────────┼─────────┼────────┤
│ tablewriter │ v0.0.5 │ legacy │
│ tablewriter │ v1.1.1 │ latest │
│ tablewriter │ v1.1.2 │ latest │
└─────────────┴─────────┴────────┘
```
+15
View File
@@ -1,6 +1,8 @@
package tablewriter
import (
"github.com/mattn/go-runewidth"
"github.com/olekukonko/tablewriter/pkg/twwidth"
"github.com/olekukonko/tablewriter/tw"
)
@@ -218,3 +220,16 @@ func WithTableMax(width int) Option {
}
}
}
// Deprecated: use WithEastAsian instead.
// WithCondition provides a way to set a custom global runewidth.Condition
// that will be used for all subsequent display width calculations by the twwidth (twdw) package.
//
// The runewidth.Condition object allows for more fine-grained control over how rune widths
// are determined, beyond just toggling EastAsianWidth. This could include settings for
// ambiguous width characters or other future properties of runewidth.Condition.
func WithCondition(cond *runewidth.Condition) Option {
return func(target *Table) {
twwidth.SetCondition(cond)
}
}
+43 -24
View File
@@ -3,8 +3,8 @@ package tablewriter
import (
"reflect"
"github.com/mattn/go-runewidth"
"github.com/olekukonko/ll"
"github.com/olekukonko/tablewriter/pkg/twcache"
"github.com/olekukonko/tablewriter/pkg/twwidth"
"github.com/olekukonko/tablewriter/tw"
)
@@ -471,22 +471,48 @@ func WithStreaming(c tw.StreamConfig) Option {
func WithStringer(stringer interface{}) Option {
return func(t *Table) {
t.stringer = stringer
t.stringerCacheMu.Lock()
t.stringerCache = make(map[reflect.Type]reflect.Value)
t.stringerCacheMu.Unlock()
t.stringerCache = twcache.NewLRU[reflect.Type, reflect.Value](tw.DefaultCacheStringCapacity)
if t.logger != nil {
t.logger.Debug("Stringer updated, cache cleared")
}
}
}
// WithStringerCache enables caching for the stringer function.
// Logs the change if debugging is enabled.
// WithStringerCache enables the default LRU caching for the stringer function.
// It initializes the cache with a default capacity if one does not already exist.
func WithStringerCache() Option {
return func(t *Table) {
t.stringerCacheEnabled = true
// Initialize default cache if strictly necessary (nil),
// or if you want to ensure the default implementation is used.
if t.stringerCache == nil {
// NewLRU returns (Instance, error). We ignore the error here assuming capacity > 0.
cache := twcache.NewLRU[reflect.Type, reflect.Value](tw.DefaultCacheStringCapacity)
t.stringerCache = cache
}
if t.logger != nil {
t.logger.Debug("Option: WithStringerCache enabled")
t.logger.Debug("Option: WithStringerCache enabled (Default LRU)")
}
}
}
// WithStringerCacheCustom enables caching for the stringer function using a specific implementation.
// Passing nil disables caching entirely.
func WithStringerCacheCustom(cache twcache.Cache[reflect.Type, reflect.Value]) Option {
return func(t *Table) {
if cache == nil {
t.stringerCache = nil
if t.logger != nil {
t.logger.Debug("Option: WithStringerCacheCustom called with nil (Caching Disabled)")
}
return
}
// Set the custom cache and enable the flag
t.stringerCache = cache
if t.logger != nil {
t.logger.Debug("Option: WithStringerCacheCustom enabled")
}
}
}
@@ -629,27 +655,20 @@ func WithRendition(rendition tw.Rendition) Option {
}
// WithEastAsian configures the global East Asian width calculation setting.
// - enable=true: Enables East Asian width calculations. CJK and ambiguous characters
// - state=tw.On: Enables East Asian width calculations. CJK and ambiguous characters
// are typically measured as double width.
// - enable=false: Disables East Asian width calculations. Characters are generally
// - state=tw.Off: Disables East Asian width calculations. Characters are generally
// measured as single width, subject to Unicode standards.
//
// This setting affects all subsequent display width calculations using the twdw package.
func WithEastAsian(enable bool) Option {
func WithEastAsian(state tw.State) Option {
return func(target *Table) {
twwidth.SetEastAsian(enable)
}
}
// WithCondition provides a way to set a custom global runewidth.Condition
// that will be used for all subsequent display width calculations by the twwidth (twdw) package.
//
// The runewidth.Condition object allows for more fine-grained control over how rune widths
// are determined, beyond just toggling EastAsianWidth. This could include settings for
// ambiguous width characters or other future properties of runewidth.Condition.
func WithCondition(cond *runewidth.Condition) Option {
return func(target *Table) {
twwidth.SetCondition(cond)
if state.Enabled() {
twwidth.SetEastAsian(true)
}
if state.Disabled() {
twwidth.SetEastAsian(false)
}
}
}
+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)
}
+134 -116
View File
@@ -8,42 +8,53 @@ import (
"github.com/clipperhouse/displaywidth"
"github.com/mattn/go-runewidth"
"github.com/olekukonko/tablewriter/pkg/twcache"
)
// globalOptions holds the global displaywidth configuration, including East Asian width settings.
var globalOptions displaywidth.Options
const (
cacheCapacity = 8192
// mu protects access to condition and widthCache for thread safety.
cachePrefix = "0:"
cacheEastAsianPrefix = "1:"
)
// Options allows for configuring width calculation on a per-call basis.
type Options struct {
EastAsianWidth 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
// widthCache stores memoized results of Width calculations to improve performance.
var widthCache *twcache.LRU[string, int]
// ansi is a compiled regular expression for stripping ANSI escape codes from strings.
var ansi = Filter()
func init() {
globalOptions = newOptions()
widthCache = make(map[cacheKey]int)
}
func newOptions() displaywidth.Options {
// go-runewidth has default logic based on env variables and locale,
// we want to keep that compatibility
// Initialize global options by detecting from the environment,
// which is the one key feature we get from go-runewidth.
cond := runewidth.NewCondition()
options := displaywidth.Options{
EastAsianWidth: cond.EastAsianWidth,
StrictEmojiNeutral: cond.StrictEmojiNeutral,
globalOptions = Options{
EastAsianWidth: cond.EastAsianWidth,
}
return options
widthCache = twcache.NewLRU[string, int](cacheCapacity)
}
// cacheKey is used as a key for memoizing string width results in widthCache.
type cacheKey struct {
str string // Input string
eastAsianWidth bool // East Asian width setting
// makeCacheKey generates a string key for the LRU cache from the input string
// and the current East Asian width setting.
// Prefix "0:" for false, "1:" for true.
func makeCacheKey(str string, eastAsianWidth bool) string {
if eastAsianWidth {
return cacheEastAsianPrefix + str
}
return cachePrefix + str
}
// widthCache stores memoized results of Width calculations to improve performance.
var widthCache map[cacheKey]int
// 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.
@@ -62,20 +73,25 @@ func Filter() *regexp.Regexp {
return regexp.MustCompile("(" + regCSI + "|" + regOSC + ")")
}
// SetEastAsian enables or disables East Asian width handling for width calculations.
// When the setting changes, the width cache is cleared to ensure accuracy.
// 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 = 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) {
mu.Lock()
defer mu.Unlock()
if globalOptions.EastAsianWidth != enable {
globalOptions.EastAsianWidth = enable
widthCache = make(map[cacheKey]int) // Clear cache on setting change
}
SetOptions(Options{EastAsianWidth: enable})
}
// IsEastAsian returns the current East Asian width setting.
@@ -92,85 +108,67 @@ func IsEastAsian() bool {
return globalOptions.EastAsianWidth
}
// SetCondition updates the global runewidth.Condition used for width calculations.
// This method is kept for backward compatibility. The condition is converted to
// displaywidth.Options internally for better performance.
// 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()
widthCache = make(map[cacheKey]int) // Clear cache on setting change
globalOptions = conditionToOptions(cond)
}
// Convert runewidth.Condition to displaywidth.Options
func conditionToOptions(cond *runewidth.Condition) displaywidth.Options {
return displaywidth.Options{
EastAsianWidth: cond.EastAsianWidth,
StrictEmojiNeutral: cond.StrictEmojiNeutral,
newEastAsianWidth := cond.EastAsianWidth
if globalOptions.EastAsianWidth != newEastAsianWidth {
globalOptions.EastAsianWidth = newEastAsianWidth
widthCache.Purge()
}
}
// Width calculates the visual width of a string, excluding ANSI escape sequences,
// using the go-runewidth package for accurate Unicode handling. It accounts for the
// current East Asian width setting and caches results for performance.
// 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 {
mu.Lock()
key := cacheKey{str: str, eastAsianWidth: globalOptions.EastAsianWidth}
if w, found := widthCache[key]; found {
mu.Unlock()
currentEA := IsEastAsian()
key := makeCacheKey(str, currentEA)
if w, found := widthCache.Get(key); found {
return w
}
mu.Unlock()
options := newOptions()
options.EastAsianWidth = key.eastAsianWidth
opts := displaywidth.Options{EastAsianWidth: currentEA}
stripped := ansi.ReplaceAllLiteralString(str, "")
calculatedWidth := options.String(stripped)
mu.Lock()
widthCache[key] = calculatedWidth
mu.Unlock()
calculatedWidth := opts.String(stripped)
widthCache.Add(key, calculatedWidth)
return calculatedWidth
}
// WidthNoCache calculates the visual width of a string without using or
// updating the global cache. It uses the current global East Asian width setting.
// This function is intended for internal use (e.g., benchmarking) and is thread-safe.
// 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 {
mu.Lock()
currentEA := globalOptions.EastAsianWidth
mu.Unlock()
options := newOptions()
options.EastAsianWidth = currentEA
stripped := ansi.ReplaceAllLiteralString(str, "")
return options.String(stripped)
// 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()})
}
// Display calculates the visual width of a string, excluding ANSI escape sequences,
// using the provided runewidth condition. Unlike Width, it does not use caching
// and is intended for cases where a specific condition is required.
// This function is thread-safe with respect to the provided condition.
//
// Example:
//
// cond := runewidth.NewCondition()
// width := twdw.Display(cond, "Hello\x1b[31mWorld") // Returns 10
// 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 {
options := conditionToOptions(cond)
return options.String(ansi.ReplaceAllLiteralString(str, ""))
opts := Options{EastAsianWidth: cond.EastAsianWidth}
return WidthWithOptions(str, opts)
}
// Truncate shortens a string to fit within a specified visual width, optionally
@@ -217,31 +215,34 @@ func Truncate(s string, maxWidth int, suffix ...string) string {
// 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 s
return safeS
}
// Suffix is provided. Check if s + suffix fits.
if sDisplayWidth+suffixDisplayWidth <= maxWidth {
return s + suffixStr
return safeS + suffixStr
}
// s fits, but s + suffix is too long. Return s.
return s
// 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).
// Capture the global EastAsianWidth setting once for consistent use
mu.Lock()
currentGlobalEastAsianWidth := globalOptions.EastAsianWidth
mu.Unlock()
currentGlobalEastAsianWidth := IsEastAsian()
// Special case for EastAsian true: if only suffix fits, return suffix.
// This was derived from previous test behavior.
if len(suffixStr) > 0 && currentGlobalEastAsianWidth {
provisionalContentWidth := maxWidth - suffixDisplayWidth
if provisionalContentWidth == 0 { // Exactly enough space for suffix only
return suffixStr // <<<< MODIFIED: No ANSI reset here
return suffixStr
}
}
@@ -263,7 +264,6 @@ func Truncate(s string, maxWidth int, suffix ...string) string {
}
return "" // Cannot fit anything.
}
// If targetContentForIteration is 0, loop won't run, result will be empty string, then suffix is added.
var contentBuf bytes.Buffer
var currentContentDisplayWidth int
@@ -271,8 +271,7 @@ func Truncate(s string, maxWidth int, suffix ...string) string {
inAnsiSequence := false
ansiWrittenToContent := false
options := newOptions()
options.EastAsianWidth = currentGlobalEastAsianWidth
dwOpts := displaywidth.Options{EastAsianWidth: currentGlobalEastAsianWidth}
for _, r := range s {
if r == '\x1b' {
@@ -306,7 +305,7 @@ func Truncate(s string, maxWidth int, suffix ...string) string {
ansiSeqBuf.Reset()
}
} else { // Normal character
runeDisplayWidth := options.Rune(r)
runeDisplayWidth := dwOpts.Rune(r)
if targetContentForIteration == 0 { // No budget for content at all
break
}
@@ -320,32 +319,51 @@ func Truncate(s string, maxWidth int, suffix ...string) string {
result := contentBuf.String()
// Suffix is added if:
// 1. A suffix string is provided.
// 2. Truncation actually happened (sDisplayWidth > maxWidth originally)
// OR if the content part is empty but a suffix is meant to be shown
// (e.g. targetContentForIteration was 0).
if len(suffixStr) > 0 {
// Add suffix if we are in the truncation path (sDisplayWidth > maxWidth)
// OR if targetContentForIteration was 0 (meaning only suffix should be shown)
// but we must ensure we don't exceed original maxWidth.
// The logic above for targetContentForIteration already ensures space.
needsReset := false
// Condition for reset: if styling was active in 's' and might affect suffix
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[") {
// If result has content and ANSI, and original had ANSI, and result not already reset
// 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"
}
if needsReset {
result += "\x1b[0m"
}
// Suffix is added if provided.
if len(suffixStr) > 0 {
result += suffixStr
}
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
}
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()
if widthCache == nil {
return 0, 0, 0
}
return widthCache.Len(), widthCache.Cap(), widthCache.HitRate()
}
+8 -10
View File
@@ -8,11 +8,11 @@ import (
"reflect"
"runtime"
"strings"
"sync"
"github.com/olekukonko/errors"
"github.com/olekukonko/ll"
"github.com/olekukonko/ll/lh"
"github.com/olekukonko/tablewriter/pkg/twcache"
"github.com/olekukonko/tablewriter/pkg/twwarp"
"github.com/olekukonko/tablewriter/pkg/twwidth"
"github.com/olekukonko/tablewriter/renderer"
@@ -52,9 +52,7 @@ type Table struct {
streamRowCounter int // Counter for rows rendered in streaming mode (0-indexed logical rows)
// cache
stringerCache map[reflect.Type]reflect.Value // Cache for stringer reflection
stringerCacheMu sync.RWMutex // Mutex for thread-safe cache access
stringerCacheEnabled bool // Flag to enable/disable caching
stringerCache twcache.Cache[reflect.Type, reflect.Value] // Cache for stringer reflection
batchRenderNumCols int
isBatchRenderNumColsSet bool
@@ -126,8 +124,7 @@ func NewTable(w io.Writer, opts ...Option) *Table {
streamRowCounter: 0,
// Cache
stringerCache: make(map[reflect.Type]reflect.Value),
stringerCacheEnabled: false, // Disabled by default
stringerCache: twcache.NewLRU[reflect.Type, reflect.Value](tw.DefaultCacheStringCapacity),
}
// set Options
@@ -483,10 +480,11 @@ func (t *Table) Reset() {
t.streamRowCounter = 0
// The stringer and its cache are part of the table's configuration,
if t.stringerCacheEnabled {
t.stringerCacheMu.Lock()
t.stringerCache = make(map[reflect.Type]reflect.Value)
t.stringerCacheMu.Unlock()
if t.stringerCache == nil {
t.stringerCache = twcache.NewLRU[reflect.Type, reflect.Value](tw.DefaultCacheStringCapacity)
t.logger.Debug("Reset(): Stringer cache reset to default capacity.")
} else {
t.stringerCache.Purge()
t.logger.Debug("Reset(): Stringer cache cleared.")
}
+2
View File
@@ -8,6 +8,8 @@ const (
Success = 1 // Operation succeeded
MinimumColumnWidth = 8
DefaultCacheStringCapacity = 10 * 1024 // 10 KB
)
const (
+11 -18
View File
@@ -1064,17 +1064,13 @@ func (t *Table) convertToStringer(input interface{}) ([]string, error) {
t.logger.Debugf("convertToString attempt %v using %v", input, t.stringer)
inputType := reflect.TypeOf(input)
stringerFuncVal := reflect.ValueOf(t.stringer)
stringerFuncType := stringerFuncVal.Type()
// Cache lookup (simplified, actual cache logic can be more complex)
if t.stringerCacheEnabled {
t.stringerCacheMu.RLock()
cachedFunc, ok := t.stringerCache[inputType]
t.stringerCacheMu.RUnlock()
if ok {
// Add proper type checking for cachedFunc against input here if necessary
// Cache lookup using twcache.LRU
// This assumes t.stringerCache is *twcache.LRU[reflect.Type, reflect.Value]
if t.stringerCache != nil {
if cachedFunc, ok := t.stringerCache.Get(inputType); ok {
t.logger.Debugf("convertToStringer: Cache hit for type %v", inputType)
// We can proceed to call it immediately because it's already been validated/cached
results := cachedFunc.Call([]reflect.Value{reflect.ValueOf(input)})
if len(results) == 1 && results[0].Type() == reflect.TypeOf([]string{}) {
return results[0].Interface().([]string), nil
@@ -1082,6 +1078,9 @@ func (t *Table) convertToStringer(input interface{}) ([]string, error) {
}
}
stringerFuncVal := reflect.ValueOf(t.stringer)
stringerFuncType := stringerFuncVal.Type()
// Robust type checking for the stringer function
validSignature := stringerFuncVal.Kind() == reflect.Func &&
stringerFuncType.NumIn() == 1 &&
@@ -1105,10 +1104,6 @@ func (t *Table) convertToStringer(input interface{}) ([]string, error) {
}
} else if paramType.Kind() == reflect.Interface || (paramType.Kind() == reflect.Ptr && paramType.Elem().Kind() != reflect.Interface) {
// If input is nil, it can be assigned if stringer expects an interface or a pointer type
// (but not a pointer to an interface, which is rare for stringers).
// A nil value for a concrete type parameter would cause a panic on Call.
// So, if paramType is not an interface/pointer, and input is nil, it's an issue.
// This needs careful handling. For now, assume assignable if interface/pointer.
assignable = true
}
@@ -1120,7 +1115,6 @@ func (t *Table) convertToStringer(input interface{}) ([]string, error) {
if input == nil {
// If input is nil, we must pass a zero value of the stringer's parameter type
// if that type is a pointer or interface.
// Passing reflect.ValueOf(nil) directly will cause issues if paramType is concrete.
callArgs = []reflect.Value{reflect.Zero(paramType)}
} else {
callArgs = []reflect.Value{reflect.ValueOf(input)}
@@ -1128,10 +1122,9 @@ func (t *Table) convertToStringer(input interface{}) ([]string, error) {
resultValues := stringerFuncVal.Call(callArgs)
if t.stringerCacheEnabled && inputType != nil { // Only cache if inputType is valid
t.stringerCacheMu.Lock()
t.stringerCache[inputType] = stringerFuncVal
t.stringerCacheMu.Unlock()
// Add to cache if enabled (not nil) and input type is valid
if t.stringerCache != nil && inputType != nil {
t.stringerCache.Add(inputType, stringerFuncVal)
}
return resultValues[0].Interface().([]string), nil