wrap icon indicating copy to clipboard operation
wrap copied to clipboard

Handling of ASCII HT "\t" in OutputLinePrefix

Open erikb495 opened this issue 5 months ago • 1 comments

I was having trouble with the handling of tabs in OutputLinePrefix. This patch adds a TabWidth option which defaults to 8.

	// TabWidth sets the perceived size of a `\t` (horizontal tab).
	// This is used in the computation of the width of OutputLinePrefix
	// (only).  Setting TabWidth to 0 disables TabWidth calculations.
	// Default: 8
	TabWidth int

The handling of tabs has always been a problem in UNIX. It starts going bad at the tty driver and goes downhill from there. I assume it's just as bad in Linux, BSD, and Mach based systems like OS-X.

Here's the patch with all of the debugging code left in, but turned off. It's based on "github.com/bbrks/wrap/[email protected]".

*** ./wrapper.go	2025/07/26 13:49:43	1.1
--- ./wrapper.go	2025/07/26 21:18:22
***************
*** 1,6 ****
--- 1,7 ----
  package wrap
  
  import (
+ 	"log"
  	"strings"
  	"unicode/utf8"
  )
***************
*** 8,13 ****
--- 9,15 ----
  const (
  	defaultBreakpoints = " -"
  	defaultNewline     = "\n"
+ 	defaultTabWidth    = 8
  )
  
  // Wrapper contains settings for customisable word-wrapping.
***************
*** 30,37 ****
  	// Default: ""
  	OutputLineSuffix string
  
! 	// LimitIncludesPrefixSuffix can be set to false if you don't want prefixes
! 	// and suffixes to be included in the length limits.
  	// Default: true
  	LimitIncludesPrefixSuffix bool
  
--- 32,40 ----
  	// Default: ""
  	OutputLineSuffix string
  
! 	// LimitIncludesPrefixSuffix can be set to false if you don't
! 	// want prefixes and suffixes to be included in the length
! 	// limits.
  	// Default: true
  	LimitIncludesPrefixSuffix bool
  
***************
*** 50,57 ****
  	// Default: false
  	StripTrailingNewline bool
  
! 	// CutLongWords will cause a hard-wrap in the middle of a word if the word's length exceeds the given limit.
  	CutLongWords bool
  }
  
  // NewWrapper returns a new instance of a Wrapper initialised with defaults.
--- 53,68 ----
  	// Default: false
  	StripTrailingNewline bool
  
! 	// CutLongWords will cause a hard-wrap in the middle of a word if
! 	// the word's length exceeds the given limit.
! 	// Default: false
  	CutLongWords bool
+ 
+ 	// TabWidth sets the perceived size of a `\t` (horizontal tab).
+ 	// This is used in the computation of the width of OutputLinePrefix
+ 	// (only).  Setting TabWidth to 0 disables TabWidth calculations.
+ 	// Default: 8
+ 	TabWidth int
  }
  
  // NewWrapper returns a new instance of a Wrapper initialised with defaults.
***************
*** 59,64 ****
--- 70,76 ----
  	return Wrapper{
  		Breakpoints:               defaultBreakpoints,
  		Newline:                   defaultNewline,
+ 		TabWidth:                  defaultTabWidth,
  		LimitIncludesPrefixSuffix: true,
  	}
  }
***************
*** 75,81 ****
  	// Subtract the length of the prefix and suffix from the limit
  	// so we don't break length limits when using them.
  	if w.LimitIncludesPrefixSuffix {
! 		limit -= utf8.RuneCountInString(w.OutputLinePrefix) + utf8.RuneCountInString(w.OutputLineSuffix)
  	}
  
  	var ret string
--- 87,115 ----
  	// Subtract the length of the prefix and suffix from the limit
  	// so we don't break length limits when using them.
  	if w.LimitIncludesPrefixSuffix {
! 		totalTabWidth := 0
! 		effectiveOutputLinePrefixWidth :=
! 			utf8.RuneCountInString(w.OutputLinePrefix)
! 		if w.TabWidth > 0 {
! 			if false {
! 				log.Printf("Build #3a effectiveOutputLinePrefixWidth=%d\n",
! 					effectiveOutputLinePrefixWidth)
! 			}
! 			totalTabWidth =
! 				strings.Count(w.OutputLinePrefix, "\t") *
! 					(w.TabWidth - 1) // Minus 1 for the rune itself.
! 			effectiveOutputLinePrefixWidth += totalTabWidth
! 			if false {
! 				log.Printf("Build #3b totalTabWidth=%d\n", totalTabWidth)
! 				log.Printf("Build #3c effectiveOutputLinePrefixWidth(updated)=%d\n",
! 					effectiveOutputLinePrefixWidth)
! 				log.Printf("Build #3d strings.Count(w.OutputLinePrefix, '\\t')=%d\n",
! 					strings.Count(w.OutputLinePrefix, "\t"))
! 				log.Printf("Build #3e:\t %v \n", []byte(w.OutputLinePrefix))
! 			}
! 		}
! 		limit -= effectiveOutputLinePrefixWidth +
! 			utf8.RuneCountInString(w.OutputLineSuffix)
  	}
  
  	var ret string

erikb495 avatar Jul 26 '25 21:07 erikb495

It'll misbehave if you set OutputLinePrefix to something like #####\t######\t. I hadn't thought of that. It'll assume 13 chars + 14 tab-induced phantom-chars (27) of prefix length. It won't break; it'll just look funny.

It's not easy to address because the width of a tab is set in the tty driver.

Dang it.

-E

erikb495 avatar Jul 26 '25 21:07 erikb495