mirror of
https://github.com/junegunn/fzf.git
synced 2026-02-22 01:28:43 +08:00
Implement word wrapping in the preview window
Example:
fzf --preview 'bat --style=plain --color=always {}' \
--preview-window wrap-word \
--bind space:toggle-preview-wrap-word
Close https://github.com/junegunn/fzf/discussions/3383
This commit is contained in:
@@ -107,82 +107,83 @@ func _() {
|
||||
_ = x[actHidePreview-96]
|
||||
_ = x[actTogglePreview-97]
|
||||
_ = x[actTogglePreviewWrap-98]
|
||||
_ = x[actTransform-99]
|
||||
_ = x[actTransformBorderLabel-100]
|
||||
_ = x[actTransformGhost-101]
|
||||
_ = x[actTransformHeader-102]
|
||||
_ = x[actTransformFooter-103]
|
||||
_ = x[actTransformHeaderLabel-104]
|
||||
_ = x[actTransformFooterLabel-105]
|
||||
_ = x[actTransformInputLabel-106]
|
||||
_ = x[actTransformListLabel-107]
|
||||
_ = x[actTransformNth-108]
|
||||
_ = x[actTransformPointer-109]
|
||||
_ = x[actTransformPreviewLabel-110]
|
||||
_ = x[actTransformPrompt-111]
|
||||
_ = x[actTransformQuery-112]
|
||||
_ = x[actTransformSearch-113]
|
||||
_ = x[actTrigger-114]
|
||||
_ = x[actBgTransform-115]
|
||||
_ = x[actBgTransformBorderLabel-116]
|
||||
_ = x[actBgTransformGhost-117]
|
||||
_ = x[actBgTransformHeader-118]
|
||||
_ = x[actBgTransformFooter-119]
|
||||
_ = x[actBgTransformHeaderLabel-120]
|
||||
_ = x[actBgTransformFooterLabel-121]
|
||||
_ = x[actBgTransformInputLabel-122]
|
||||
_ = x[actBgTransformListLabel-123]
|
||||
_ = x[actBgTransformNth-124]
|
||||
_ = x[actBgTransformPointer-125]
|
||||
_ = x[actBgTransformPreviewLabel-126]
|
||||
_ = x[actBgTransformPrompt-127]
|
||||
_ = x[actBgTransformQuery-128]
|
||||
_ = x[actBgTransformSearch-129]
|
||||
_ = x[actBgCancel-130]
|
||||
_ = x[actSearch-131]
|
||||
_ = x[actPreview-132]
|
||||
_ = x[actPreviewTop-133]
|
||||
_ = x[actPreviewBottom-134]
|
||||
_ = x[actPreviewUp-135]
|
||||
_ = x[actPreviewDown-136]
|
||||
_ = x[actPreviewPageUp-137]
|
||||
_ = x[actPreviewPageDown-138]
|
||||
_ = x[actPreviewHalfPageUp-139]
|
||||
_ = x[actPreviewHalfPageDown-140]
|
||||
_ = x[actPrevHistory-141]
|
||||
_ = x[actPrevSelected-142]
|
||||
_ = x[actPrint-143]
|
||||
_ = x[actPut-144]
|
||||
_ = x[actNextHistory-145]
|
||||
_ = x[actNextSelected-146]
|
||||
_ = x[actExecute-147]
|
||||
_ = x[actExecuteSilent-148]
|
||||
_ = x[actExecuteMulti-149]
|
||||
_ = x[actSigStop-150]
|
||||
_ = x[actBest-151]
|
||||
_ = x[actFirst-152]
|
||||
_ = x[actLast-153]
|
||||
_ = x[actReload-154]
|
||||
_ = x[actReloadSync-155]
|
||||
_ = x[actDisableSearch-156]
|
||||
_ = x[actEnableSearch-157]
|
||||
_ = x[actSelect-158]
|
||||
_ = x[actDeselect-159]
|
||||
_ = x[actUnbind-160]
|
||||
_ = x[actRebind-161]
|
||||
_ = x[actToggleBind-162]
|
||||
_ = x[actBecome-163]
|
||||
_ = x[actShowHeader-164]
|
||||
_ = x[actHideHeader-165]
|
||||
_ = x[actBell-166]
|
||||
_ = x[actExclude-167]
|
||||
_ = x[actExcludeMulti-168]
|
||||
_ = x[actAsync-169]
|
||||
_ = x[actTogglePreviewWrapWord-99]
|
||||
_ = x[actTransform-100]
|
||||
_ = x[actTransformBorderLabel-101]
|
||||
_ = x[actTransformGhost-102]
|
||||
_ = x[actTransformHeader-103]
|
||||
_ = x[actTransformFooter-104]
|
||||
_ = x[actTransformHeaderLabel-105]
|
||||
_ = x[actTransformFooterLabel-106]
|
||||
_ = x[actTransformInputLabel-107]
|
||||
_ = x[actTransformListLabel-108]
|
||||
_ = x[actTransformNth-109]
|
||||
_ = x[actTransformPointer-110]
|
||||
_ = x[actTransformPreviewLabel-111]
|
||||
_ = x[actTransformPrompt-112]
|
||||
_ = x[actTransformQuery-113]
|
||||
_ = x[actTransformSearch-114]
|
||||
_ = x[actTrigger-115]
|
||||
_ = x[actBgTransform-116]
|
||||
_ = x[actBgTransformBorderLabel-117]
|
||||
_ = x[actBgTransformGhost-118]
|
||||
_ = x[actBgTransformHeader-119]
|
||||
_ = x[actBgTransformFooter-120]
|
||||
_ = x[actBgTransformHeaderLabel-121]
|
||||
_ = x[actBgTransformFooterLabel-122]
|
||||
_ = x[actBgTransformInputLabel-123]
|
||||
_ = x[actBgTransformListLabel-124]
|
||||
_ = x[actBgTransformNth-125]
|
||||
_ = x[actBgTransformPointer-126]
|
||||
_ = x[actBgTransformPreviewLabel-127]
|
||||
_ = x[actBgTransformPrompt-128]
|
||||
_ = x[actBgTransformQuery-129]
|
||||
_ = x[actBgTransformSearch-130]
|
||||
_ = x[actBgCancel-131]
|
||||
_ = x[actSearch-132]
|
||||
_ = x[actPreview-133]
|
||||
_ = x[actPreviewTop-134]
|
||||
_ = x[actPreviewBottom-135]
|
||||
_ = x[actPreviewUp-136]
|
||||
_ = x[actPreviewDown-137]
|
||||
_ = x[actPreviewPageUp-138]
|
||||
_ = x[actPreviewPageDown-139]
|
||||
_ = x[actPreviewHalfPageUp-140]
|
||||
_ = x[actPreviewHalfPageDown-141]
|
||||
_ = x[actPrevHistory-142]
|
||||
_ = x[actPrevSelected-143]
|
||||
_ = x[actPrint-144]
|
||||
_ = x[actPut-145]
|
||||
_ = x[actNextHistory-146]
|
||||
_ = x[actNextSelected-147]
|
||||
_ = x[actExecute-148]
|
||||
_ = x[actExecuteSilent-149]
|
||||
_ = x[actExecuteMulti-150]
|
||||
_ = x[actSigStop-151]
|
||||
_ = x[actBest-152]
|
||||
_ = x[actFirst-153]
|
||||
_ = x[actLast-154]
|
||||
_ = x[actReload-155]
|
||||
_ = x[actReloadSync-156]
|
||||
_ = x[actDisableSearch-157]
|
||||
_ = x[actEnableSearch-158]
|
||||
_ = x[actSelect-159]
|
||||
_ = x[actDeselect-160]
|
||||
_ = x[actUnbind-161]
|
||||
_ = x[actRebind-162]
|
||||
_ = x[actToggleBind-163]
|
||||
_ = x[actBecome-164]
|
||||
_ = x[actShowHeader-165]
|
||||
_ = x[actHideHeader-166]
|
||||
_ = x[actBell-167]
|
||||
_ = x[actExclude-168]
|
||||
_ = x[actExcludeMulti-169]
|
||||
_ = x[actAsync-170]
|
||||
}
|
||||
|
||||
const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactBackwardSubWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactForwardSubWordactKillLineactKillWordactKillSubWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactBackwardKillSubWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactToggleRawactEnableRawactDisableRawactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactDownMatchactUpactUpMatchactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactTriggeractBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactBgCancelactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactBestactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsync"
|
||||
const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactBackwardSubWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactForwardSubWordactKillLineactKillWordactKillSubWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactBackwardKillSubWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactToggleRawactEnableRawactDisableRawactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactDownMatchactUpactUpMatchactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTogglePreviewWrapWordactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactTriggeractBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactBgCancelactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactBestactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsync"
|
||||
|
||||
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 57, 77, 84, 92, 110, 118, 127, 144, 165, 180, 201, 225, 240, 258, 267, 287, 301, 316, 331, 351, 371, 390, 408, 422, 434, 450, 466, 487, 509, 524, 538, 552, 565, 582, 590, 603, 619, 631, 639, 653, 667, 684, 695, 706, 720, 738, 755, 762, 781, 803, 815, 829, 838, 853, 865, 878, 889, 900, 912, 926, 947, 962, 975, 993, 1009, 1021, 1033, 1046, 1061, 1075, 1087, 1099, 1116, 1123, 1135, 1140, 1150, 1159, 1170, 1181, 1194, 1209, 1220, 1233, 1248, 1255, 1268, 1281, 1298, 1313, 1326, 1340, 1354, 1370, 1390, 1402, 1425, 1442, 1460, 1478, 1501, 1524, 1546, 1567, 1582, 1601, 1625, 1643, 1660, 1678, 1688, 1702, 1727, 1746, 1766, 1786, 1811, 1836, 1860, 1883, 1900, 1921, 1947, 1967, 1986, 2006, 2017, 2026, 2036, 2049, 2065, 2077, 2091, 2107, 2125, 2145, 2167, 2181, 2196, 2204, 2210, 2224, 2239, 2249, 2265, 2280, 2290, 2297, 2305, 2312, 2321, 2334, 2350, 2365, 2374, 2385, 2394, 2403, 2416, 2425, 2438, 2451, 2458, 2468, 2483, 2491}
|
||||
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 57, 77, 84, 92, 110, 118, 127, 144, 165, 180, 201, 225, 240, 258, 267, 287, 301, 316, 331, 351, 371, 390, 408, 422, 434, 450, 466, 487, 509, 524, 538, 552, 565, 582, 590, 603, 619, 631, 639, 653, 667, 684, 695, 706, 720, 738, 755, 762, 781, 803, 815, 829, 838, 853, 865, 878, 889, 900, 912, 926, 947, 962, 975, 993, 1009, 1021, 1033, 1046, 1061, 1075, 1087, 1099, 1116, 1123, 1135, 1140, 1150, 1159, 1170, 1181, 1194, 1209, 1220, 1233, 1248, 1255, 1268, 1281, 1298, 1313, 1326, 1340, 1354, 1370, 1390, 1414, 1426, 1449, 1466, 1484, 1502, 1525, 1548, 1570, 1591, 1606, 1625, 1649, 1667, 1684, 1702, 1712, 1726, 1751, 1770, 1790, 1810, 1835, 1860, 1884, 1907, 1924, 1945, 1971, 1991, 2010, 2030, 2041, 2050, 2060, 2073, 2089, 2101, 2115, 2131, 2149, 2169, 2191, 2205, 2220, 2228, 2234, 2248, 2263, 2273, 2289, 2304, 2314, 2321, 2329, 2336, 2345, 2358, 2374, 2389, 2398, 2409, 2418, 2427, 2440, 2449, 2462, 2475, 2482, 2492, 2507, 2515}
|
||||
|
||||
func (i actionType) String() string {
|
||||
if i < 0 || i >= actionType(len(_actionType_index)-1) {
|
||||
|
||||
@@ -157,7 +157,7 @@ Usage: fzf [options]
|
||||
--preview=COMMAND Command to preview highlighted line ({})
|
||||
--preview-window=OPT Preview window layout (default: right:50%)
|
||||
[up|down|left|right][,SIZE[%]]
|
||||
[,[no]wrap][,[no]cycle][,[no]follow][,[no]info]
|
||||
[,[no]wrap[-word]][,[no]cycle][,[no]follow][,[no]info]
|
||||
[,[no]hidden][,border-STYLE]
|
||||
[,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES]
|
||||
[,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]
|
||||
@@ -367,6 +367,7 @@ type previewOpts struct {
|
||||
scroll string
|
||||
hidden bool
|
||||
wrap bool
|
||||
wrapWord bool
|
||||
cycle bool
|
||||
follow bool
|
||||
info bool
|
||||
@@ -543,7 +544,7 @@ func (o *previewOpts) compare(active *previewOpts, b *previewOpts) previewOptsCo
|
||||
return previewOptsDifferentLayout
|
||||
}
|
||||
|
||||
if a.wrap == b.wrap && a.headerLines == b.headerLines && a.info == b.info && a.scroll == b.scroll {
|
||||
if a.wrap == b.wrap && a.wrapWord == b.wrapWord && a.headerLines == b.headerLines && a.info == b.info && a.scroll == b.scroll {
|
||||
return previewOptsSame
|
||||
}
|
||||
|
||||
@@ -691,7 +692,13 @@ func filterNonEmpty(input []string) []string {
|
||||
}
|
||||
|
||||
func defaultPreviewOpts(command string) previewOpts {
|
||||
return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, true, defaultBorderShape, 0, 0, nil}
|
||||
return previewOpts{
|
||||
command: command,
|
||||
position: posRight,
|
||||
size: sizeSpec{50, true},
|
||||
info: true,
|
||||
border: defaultBorderShape,
|
||||
}
|
||||
}
|
||||
|
||||
func defaultOptions() *Options {
|
||||
@@ -1863,6 +1870,8 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
||||
appendAction(actTogglePreview)
|
||||
case "toggle-preview-wrap":
|
||||
appendAction(actTogglePreviewWrap)
|
||||
case "toggle-preview-wrap-word":
|
||||
appendAction(actTogglePreviewWrapWord)
|
||||
case "toggle-sort":
|
||||
appendAction(actToggleSort)
|
||||
case "offset-up":
|
||||
@@ -2274,8 +2283,13 @@ func parsePreviewWindowImpl(opts *previewOpts, input string) error {
|
||||
opts.hidden = false
|
||||
case "wrap":
|
||||
opts.wrap = true
|
||||
opts.wrapWord = false
|
||||
case "wrap-word":
|
||||
opts.wrap = true
|
||||
opts.wrapWord = true
|
||||
case "nowrap":
|
||||
opts.wrap = false
|
||||
opts.wrapWord = false
|
||||
case "cycle":
|
||||
opts.cycle = true
|
||||
case "nocycle":
|
||||
|
||||
@@ -448,6 +448,20 @@ func TestPreviewOpts(t *testing.T) {
|
||||
opts.Preview.size.size == 70) {
|
||||
t.Error(opts.Preview)
|
||||
}
|
||||
|
||||
// wrap-word tests
|
||||
opts = optsFor("--preview-window=wrap-word")
|
||||
if !(opts.Preview.wrap == true && opts.Preview.wrapWord == true) {
|
||||
t.Errorf("wrap-word: wrap=%v, wrapWord=%v", opts.Preview.wrap, opts.Preview.wrapWord)
|
||||
}
|
||||
opts = optsFor("--preview-window=wrap-word,nowrap")
|
||||
if !(opts.Preview.wrap == false && opts.Preview.wrapWord == false) {
|
||||
t.Errorf("wrap-word,nowrap: wrap=%v, wrapWord=%v", opts.Preview.wrap, opts.Preview.wrapWord)
|
||||
}
|
||||
opts = optsFor("--preview-window=wrap-word,wrap")
|
||||
if !(opts.Preview.wrap == true && opts.Preview.wrapWord == false) {
|
||||
t.Errorf("wrap-word,wrap: wrap=%v, wrapWord=%v", opts.Preview.wrap, opts.Preview.wrapWord)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdditiveExpect(t *testing.T) {
|
||||
|
||||
185
src/terminal.go
185
src/terminal.go
@@ -617,6 +617,7 @@ const (
|
||||
actHidePreview
|
||||
actTogglePreview
|
||||
actTogglePreviewWrap
|
||||
actTogglePreviewWrapWord
|
||||
|
||||
actTransform
|
||||
actTransformBorderLabel
|
||||
@@ -941,7 +942,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
|
||||
}
|
||||
if fullscreen {
|
||||
if tui.HasFullscreenRenderer() {
|
||||
renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse)
|
||||
renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop)
|
||||
} else {
|
||||
renderer, err = tui.NewLightRenderer(opts.TtyDefault, ttyin, opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit,
|
||||
true, func(h int) int { return h })
|
||||
@@ -4089,6 +4090,75 @@ func extractPassThroughs(line string) ([]string, string) {
|
||||
return passThroughs, transformed
|
||||
}
|
||||
|
||||
func (t *Terminal) wordWrapAnsiLine(line string, maxWidth int, wrapSignWidth int) []string {
|
||||
if maxWidth <= 0 {
|
||||
return []string{line}
|
||||
}
|
||||
|
||||
var result []string
|
||||
lineStart := 0
|
||||
width := 0
|
||||
lastSpaceStart := -1
|
||||
lastSpaceEnd := -1
|
||||
widthBeforeLastSpace := 0
|
||||
lastSpaceWidth := 0
|
||||
max := maxWidth
|
||||
pos := 0
|
||||
|
||||
for pos < len(line) {
|
||||
// Find next ANSI escape sequence
|
||||
start, end := nextAnsiEscapeSequence(line[pos:])
|
||||
|
||||
// Determine the end of printable text before the next escape
|
||||
var printableEnd int
|
||||
if start < 0 {
|
||||
printableEnd = len(line)
|
||||
} else {
|
||||
printableEnd = pos + start
|
||||
}
|
||||
|
||||
// Process printable characters using grapheme clusters
|
||||
gr := uniseg.NewGraphemes(line[pos:printableEnd])
|
||||
for gr.Next() {
|
||||
gStart, gEnd := gr.Positions()
|
||||
w := gr.Width()
|
||||
str := gr.Str()
|
||||
|
||||
if str == "\t" {
|
||||
w = t.tabstop - (width % t.tabstop)
|
||||
}
|
||||
|
||||
if str == " " || str == "\t" {
|
||||
lastSpaceStart = pos + gStart
|
||||
lastSpaceEnd = pos + gEnd
|
||||
widthBeforeLastSpace = width
|
||||
lastSpaceWidth = w
|
||||
}
|
||||
|
||||
width += w
|
||||
|
||||
if width > max && lastSpaceEnd > lineStart {
|
||||
result = append(result, line[lineStart:lastSpaceStart])
|
||||
lineStart = lastSpaceEnd
|
||||
width -= widthBeforeLastSpace + lastSpaceWidth
|
||||
lastSpaceStart = -1
|
||||
lastSpaceEnd = -1
|
||||
widthBeforeLastSpace = 0
|
||||
max = maxWidth - wrapSignWidth
|
||||
}
|
||||
}
|
||||
pos = printableEnd
|
||||
|
||||
// Skip the ANSI escape sequence
|
||||
if start >= 0 {
|
||||
pos += end - start
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, line[lineStart:])
|
||||
return result
|
||||
}
|
||||
|
||||
func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unchanged bool) {
|
||||
maxWidth := t.pwindow.Width()
|
||||
var ansi *ansiState
|
||||
@@ -4182,48 +4252,77 @@ Loop:
|
||||
continue
|
||||
}
|
||||
|
||||
// Pre-split line into sub-lines for word wrapping
|
||||
var subLines []string
|
||||
if t.activePreviewOpts.wrapWord {
|
||||
subLines = t.wordWrapAnsiLine(line, maxWidth, t.wrapSignWidth)
|
||||
} else {
|
||||
subLines = []string{line}
|
||||
}
|
||||
|
||||
var fillRet tui.FillReturn
|
||||
prefixWidth := 0
|
||||
var url *url
|
||||
_, _, ansi = extractColor(line, ansi, func(str string, ansi *ansiState) bool {
|
||||
trimmed := []rune(str)
|
||||
isTrimmed := false
|
||||
if !t.activePreviewOpts.wrap {
|
||||
trimmed, isTrimmed = t.trimRight(trimmed, maxWidth-t.pwindow.X())
|
||||
wrap := t.activePreviewOpts.wrap
|
||||
for subIdx, subLine := range subLines {
|
||||
// Render wrap sign for continuation sub-lines
|
||||
if subIdx > 0 {
|
||||
if fillRet == tui.FillContinue {
|
||||
fillRet = t.pwindow.Fill("\n")
|
||||
if fillRet == tui.FillSuspend {
|
||||
t.previewed.filled = true
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), -1, tui.Dim, t.wrapSign)
|
||||
}
|
||||
if url == nil && ansi != nil && ansi.url != nil {
|
||||
url = ansi.url
|
||||
t.pwindow.LinkBegin(url.uri, url.params)
|
||||
}
|
||||
if url != nil && (ansi == nil || ansi.url == nil) {
|
||||
url = nil
|
||||
|
||||
prefixWidth := t.pwindow.X()
|
||||
var url *url
|
||||
_, _, ansi = extractColor(subLine, ansi, func(str string, ansi *ansiState) bool {
|
||||
trimmed := []rune(str)
|
||||
isTrimmed := false
|
||||
if !wrap {
|
||||
trimmed, isTrimmed = t.trimRight(trimmed, maxWidth-t.pwindow.X())
|
||||
}
|
||||
if url == nil && ansi != nil && ansi.url != nil {
|
||||
url = ansi.url
|
||||
t.pwindow.LinkBegin(url.uri, url.params)
|
||||
}
|
||||
if url != nil && (ansi == nil || ansi.url == nil) {
|
||||
url = nil
|
||||
t.pwindow.LinkEnd()
|
||||
}
|
||||
if ansi != nil {
|
||||
lbg = ansi.lbg
|
||||
} else {
|
||||
lbg = -1
|
||||
}
|
||||
str, width := t.processTabs(trimmed, prefixWidth)
|
||||
if width > prefixWidth {
|
||||
prefixWidth = width
|
||||
colored := ansi != nil && ansi.colored()
|
||||
if t.theme.Colored && colored {
|
||||
fillRet = t.pwindow.CFill(ansi.fg, ansi.bg, ansi.ul, ansi.attr, str)
|
||||
} else {
|
||||
attr := tui.AttrRegular
|
||||
if colored {
|
||||
attr = ansi.attr
|
||||
}
|
||||
fillRet = t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), -1, attr, str)
|
||||
}
|
||||
}
|
||||
return !isTrimmed &&
|
||||
(fillRet == tui.FillContinue || wrap && fillRet == tui.FillNextLine)
|
||||
})
|
||||
if url != nil {
|
||||
t.pwindow.LinkEnd()
|
||||
}
|
||||
if ansi != nil {
|
||||
lbg = ansi.lbg
|
||||
} else {
|
||||
lbg = -1
|
||||
|
||||
if fillRet == tui.FillSuspend {
|
||||
t.previewed.filled = true
|
||||
break Loop
|
||||
}
|
||||
str, width := t.processTabs(trimmed, prefixWidth)
|
||||
if width > prefixWidth {
|
||||
prefixWidth = width
|
||||
colored := ansi != nil && ansi.colored()
|
||||
if t.theme.Colored && colored {
|
||||
fillRet = t.pwindow.CFill(ansi.fg, ansi.bg, ansi.ul, ansi.attr, str)
|
||||
} else {
|
||||
attr := tui.AttrRegular
|
||||
if colored {
|
||||
attr = ansi.attr
|
||||
}
|
||||
fillRet = t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), -1, attr, str)
|
||||
}
|
||||
}
|
||||
return !isTrimmed &&
|
||||
(fillRet == tui.FillContinue || t.activePreviewOpts.wrap && fillRet == tui.FillNextLine)
|
||||
})
|
||||
if url != nil {
|
||||
t.pwindow.LinkEnd()
|
||||
}
|
||||
|
||||
t.previewer.scrollable = t.previewer.scrollable || t.pwindow.Y() == height-1 && t.pwindow.X() == t.pwindow.Width()
|
||||
if fillRet == tui.FillNextLine {
|
||||
continue
|
||||
@@ -5972,9 +6071,17 @@ func (t *Terminal) Loop() error {
|
||||
t.cancelPreview()
|
||||
}
|
||||
}
|
||||
case actTogglePreviewWrap:
|
||||
case actTogglePreviewWrap, actTogglePreviewWrapWord:
|
||||
if t.hasPreviewWindow() {
|
||||
t.activePreviewOpts.wrap = !t.activePreviewOpts.wrap
|
||||
if a.t == actTogglePreviewWrapWord {
|
||||
t.activePreviewOpts.wrapWord = !t.activePreviewOpts.wrapWord
|
||||
t.activePreviewOpts.wrap = t.activePreviewOpts.wrapWord
|
||||
} else {
|
||||
t.activePreviewOpts.wrap = !t.activePreviewOpts.wrap
|
||||
if !t.activePreviewOpts.wrap {
|
||||
t.activePreviewOpts.wrapWord = false
|
||||
}
|
||||
}
|
||||
// Reset preview version so that full redraw occurs
|
||||
t.previewed.version = 0
|
||||
req(reqPreviewRefresh)
|
||||
|
||||
@@ -699,3 +699,72 @@ func readFile(path string) ([]byte, error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWordWrapAnsiLine(t *testing.T) {
|
||||
term := &Terminal{}
|
||||
|
||||
// Simple wrapping
|
||||
result := term.wordWrapAnsiLine("hello world", 7, 2)
|
||||
if len(result) != 2 || result[0] != "hello" || result[1] != "world" {
|
||||
t.Errorf("Simple: %q", result)
|
||||
}
|
||||
|
||||
// No wrapping needed
|
||||
result = term.wordWrapAnsiLine("hello", 10, 2)
|
||||
if len(result) != 1 || result[0] != "hello" {
|
||||
t.Errorf("No wrap: %q", result)
|
||||
}
|
||||
|
||||
// ANSI codes preserved across split
|
||||
result = term.wordWrapAnsiLine("\x1b[31mhello \x1b[32mworld", 8, 2)
|
||||
if len(result) != 2 || result[0] != "\x1b[31mhello" || result[1] != "\x1b[32mworld" {
|
||||
t.Errorf("ANSI: %q", result)
|
||||
}
|
||||
|
||||
// Long word (no space) — no break, let character wrapping handle it
|
||||
result = term.wordWrapAnsiLine("abcdefghij", 5, 2)
|
||||
if len(result) != 1 || result[0] != "abcdefghij" {
|
||||
t.Errorf("Long word: %q", result)
|
||||
}
|
||||
|
||||
// Multiple words with continuation wrapSignWidth
|
||||
result = term.wordWrapAnsiLine("aa bb cc dd", 5, 2)
|
||||
// max=5 for first line, max=3 for continuations (5-2)
|
||||
// "aa bb" (5 wide), split at second space -> "aa bb" | "cc" | "dd"
|
||||
if len(result) != 3 || result[0] != "aa bb" || result[1] != "cc" || result[2] != "dd" {
|
||||
t.Errorf("Multiple words: %q", result)
|
||||
}
|
||||
|
||||
// Empty string
|
||||
result = term.wordWrapAnsiLine("", 10, 2)
|
||||
if len(result) != 1 || result[0] != "" {
|
||||
t.Errorf("Empty: %q", result)
|
||||
}
|
||||
|
||||
// OSC 8 hyperlink preserved
|
||||
result = term.wordWrapAnsiLine("\x1b]8;;http://example.com\x1b\\click here\x1b]8;;\x1b\\", 8, 2)
|
||||
if len(result) != 2 {
|
||||
t.Errorf("Hyperlink split count: %d, %q", len(result), result)
|
||||
}
|
||||
|
||||
// Tab handling: tab expands to tabstop-aligned width
|
||||
term.tabstop = 8
|
||||
// "\thi there" — tab at column 0 expands to 8, total "hi" starts at 8
|
||||
// maxWidth=15: "\thi" = 10 wide, "there" = 5 wide, total 16 > 15, wrap at space
|
||||
result = term.wordWrapAnsiLine("\thi there", 15, 2)
|
||||
if len(result) != 2 || result[0] != "\thi" || result[1] != "there" {
|
||||
t.Errorf("Tab: %q", result)
|
||||
}
|
||||
|
||||
// Tab as word boundary: "hello"(5) + tab(3→col8) + "world"(5) = 13 total
|
||||
// maxWidth=13: fits without wrapping
|
||||
result = term.wordWrapAnsiLine("hello\tworld", 13, 2)
|
||||
if len(result) != 1 || result[0] != "hello\tworld" {
|
||||
t.Errorf("Tab no wrap: %q", result)
|
||||
}
|
||||
// maxWidth=12: 13 > 12, wraps at tab
|
||||
result = term.wordWrapAnsiLine("hello\tworld", 12, 2)
|
||||
if len(result) != 2 || result[0] != "hello" || result[1] != "world" {
|
||||
t.Errorf("Tab wrap: %q", result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
"github.com/rivo/uniseg"
|
||||
|
||||
"golang.org/x/term"
|
||||
)
|
||||
@@ -1419,52 +1418,13 @@ func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) {
|
||||
w.stderrInternal(cleanse(text), false, code)
|
||||
}
|
||||
|
||||
type wrappedLine struct {
|
||||
text string
|
||||
displayWidth int
|
||||
}
|
||||
|
||||
func wrapLine(input string, prefixLength int, initialMax int, tabstop int, wrapSignWidth int) []wrappedLine {
|
||||
lines := []wrappedLine{}
|
||||
width := 0
|
||||
line := ""
|
||||
gr := uniseg.NewGraphemes(input)
|
||||
max := initialMax
|
||||
for gr.Next() {
|
||||
rs := gr.Runes()
|
||||
str := string(rs)
|
||||
var w int
|
||||
if len(rs) == 1 && rs[0] == '\t' {
|
||||
w = tabstop - (prefixLength+width)%tabstop
|
||||
str = repeat(' ', w)
|
||||
} else if rs[0] == '\r' {
|
||||
w++
|
||||
} else {
|
||||
w = uniseg.StringWidth(str)
|
||||
}
|
||||
width += w
|
||||
|
||||
if prefixLength+width <= max {
|
||||
line += str
|
||||
} else {
|
||||
lines = append(lines, wrappedLine{string(line), width - w})
|
||||
line = str
|
||||
prefixLength = 0
|
||||
width = w
|
||||
max = initialMax - wrapSignWidth
|
||||
}
|
||||
}
|
||||
lines = append(lines, wrappedLine{string(line), width})
|
||||
return lines
|
||||
}
|
||||
|
||||
func (w *LightWindow) fill(str string, resetCode string) FillReturn {
|
||||
allLines := strings.Split(str, "\n")
|
||||
for i, line := range allLines {
|
||||
lines := wrapLine(line, w.posx, w.width, w.tabstop, w.wrapSignWidth)
|
||||
lines := WrapLine(line, w.posx, w.width, w.tabstop, w.wrapSignWidth)
|
||||
for j, wl := range lines {
|
||||
w.stderrInternal(wl.text, false, resetCode)
|
||||
w.posx += wl.displayWidth
|
||||
w.stderrInternal(wl.Text, false, resetCode)
|
||||
w.posx += wl.DisplayWidth
|
||||
|
||||
// Wrap line
|
||||
if j < len(lines)-1 || i < len(allLines)-1 {
|
||||
|
||||
@@ -5,6 +5,7 @@ package tui
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
@@ -53,6 +54,7 @@ type TcellWindow struct {
|
||||
showCursor bool
|
||||
wrapSign string
|
||||
wrapSignWidth int
|
||||
tabstop int
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Top() int {
|
||||
@@ -757,7 +759,8 @@ func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int,
|
||||
height: height,
|
||||
normal: normal,
|
||||
borderStyle: borderStyle,
|
||||
showCursor: r.showCursor}
|
||||
showCursor: r.showCursor,
|
||||
tabstop: r.tabstop}
|
||||
w.Erase()
|
||||
return w
|
||||
}
|
||||
@@ -894,10 +897,8 @@ func (w *TcellWindow) CPrint(pair ColorPair, text string) {
|
||||
w.printString(text, pair)
|
||||
}
|
||||
|
||||
func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
|
||||
lx := 0
|
||||
func (w *TcellWindow) pairStyle(pair ColorPair) tcell.Style {
|
||||
a := pair.Attr()
|
||||
|
||||
var style tcell.Style
|
||||
if w.color {
|
||||
style = pair.style()
|
||||
@@ -919,61 +920,61 @@ func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
|
||||
} else {
|
||||
style = style.Underline(false)
|
||||
}
|
||||
style = w.withUrl(style)
|
||||
return w.withUrl(style)
|
||||
}
|
||||
|
||||
func (w *TcellWindow) renderGraphemes(text string, style tcell.Style) {
|
||||
gr := uniseg.NewGraphemes(text)
|
||||
Loop:
|
||||
for gr.Next() {
|
||||
st := style
|
||||
rs := gr.Runes()
|
||||
if len(rs) == 1 {
|
||||
r := rs[0]
|
||||
switch r {
|
||||
case '\r':
|
||||
st = style.Dim(true)
|
||||
rs[0] = '␍'
|
||||
case '\n':
|
||||
w.lastY++
|
||||
w.lastX = 0
|
||||
lx = 0
|
||||
continue Loop
|
||||
}
|
||||
}
|
||||
|
||||
// word wrap:
|
||||
xPos := w.left + w.lastX + lx
|
||||
if xPos >= w.left+w.width {
|
||||
w.lastY++
|
||||
if w.lastY >= w.height {
|
||||
return FillSuspend
|
||||
}
|
||||
w.lastX = 0
|
||||
lx = 0
|
||||
xPos = w.left
|
||||
sign := w.wrapSign
|
||||
if w.wrapSignWidth > w.width {
|
||||
runes, _ := util.Truncate(sign, w.width)
|
||||
sign = string(runes)
|
||||
}
|
||||
wgr := uniseg.NewGraphemes(sign)
|
||||
for wgr.Next() {
|
||||
rs := wgr.Runes()
|
||||
_screen.SetContent(w.left+lx, w.top+w.lastY, rs[0], rs[1:], style.Dim(true))
|
||||
lx += uniseg.StringWidth(string(rs))
|
||||
}
|
||||
xPos = w.left + lx
|
||||
if len(rs) == 1 && rs[0] == '\r' {
|
||||
st = style.Dim(true)
|
||||
rs[0] = '␍'
|
||||
}
|
||||
|
||||
xPos := w.left + w.lastX
|
||||
yPos := w.top + w.lastY
|
||||
if yPos >= (w.top + w.height) {
|
||||
return FillSuspend
|
||||
if xPos < (w.left+w.width) && yPos < (w.top+w.height) {
|
||||
_screen.SetContent(xPos, yPos, rs[0], rs[1:], st)
|
||||
}
|
||||
|
||||
_screen.SetContent(xPos, yPos, rs[0], rs[1:], st)
|
||||
lx += util.StringWidth(string(rs))
|
||||
w.lastX += util.StringWidth(string(rs))
|
||||
}
|
||||
w.lastX += lx
|
||||
if w.lastX == w.width {
|
||||
}
|
||||
|
||||
func (w *TcellWindow) renderWrapSign(style tcell.Style) {
|
||||
sign := w.wrapSign
|
||||
if w.wrapSignWidth > w.width {
|
||||
runes, _ := util.Truncate(sign, w.width)
|
||||
sign = string(runes)
|
||||
}
|
||||
gr := uniseg.NewGraphemes(sign)
|
||||
for gr.Next() {
|
||||
rs := gr.Runes()
|
||||
_screen.SetContent(w.left+w.lastX, w.top+w.lastY, rs[0], rs[1:], style.Dim(true))
|
||||
w.lastX += uniseg.StringWidth(string(rs))
|
||||
}
|
||||
}
|
||||
|
||||
func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
|
||||
style := w.pairStyle(pair)
|
||||
|
||||
for i, segment := range strings.Split(text, "\n") {
|
||||
for j, wl := range WrapLine(segment, w.lastX, w.width, w.tabstop, w.wrapSignWidth) {
|
||||
if i > 0 || j > 0 {
|
||||
w.lastY++
|
||||
if w.lastY >= w.height {
|
||||
return FillSuspend
|
||||
}
|
||||
w.lastX = 0
|
||||
if j > 0 {
|
||||
w.renderWrapSign(style)
|
||||
}
|
||||
}
|
||||
w.renderGraphemes(wl.Text, style)
|
||||
}
|
||||
}
|
||||
if w.lastX >= w.width {
|
||||
w.lastY++
|
||||
w.lastX = 0
|
||||
return FillNextLine
|
||||
|
||||
@@ -253,7 +253,7 @@ func TestGetCharEventKey(t *testing.T) {
|
||||
{giveKey{tcell.KeyPause, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // unhandled
|
||||
|
||||
}
|
||||
r := NewFullscreenRenderer(&ColorTheme{}, false, false)
|
||||
r := NewFullscreenRenderer(&ColorTheme{}, false, false, 8)
|
||||
r.Init()
|
||||
|
||||
// run and evaluate the tests
|
||||
@@ -265,22 +265,22 @@ func TestGetCharEventKey(t *testing.T) {
|
||||
t.Logf("giveEvent = %T{key: %v, ch: %q (%[3]v), mod: %#04b}\n", giveEvent, giveEvent.Key(), giveEvent.Rune(), giveEvent.Modifiers())
|
||||
|
||||
// process the event in fzf and evaluate the test
|
||||
gotEvent := r.GetChar()
|
||||
gotEvent := r.GetChar(true)
|
||||
// skip Resize events, those are sometimes put in the buffer outside of this test
|
||||
if initialResizeAsInvalid && gotEvent.Type == Invalid {
|
||||
t.Logf("Resize as Invalid swallowed")
|
||||
initialResizeAsInvalid = false
|
||||
gotEvent = r.GetChar()
|
||||
gotEvent = r.GetChar(true)
|
||||
}
|
||||
if gotEvent.Type == Resize {
|
||||
t.Logf("Resize swallowed")
|
||||
gotEvent = r.GetChar()
|
||||
gotEvent = r.GetChar(true)
|
||||
}
|
||||
t.Logf("wantEvent = %T{Type: %v, Char: %q (%[3]v)}\n", test.wantKey, test.wantKey.Type, test.wantKey.Char)
|
||||
t.Logf("gotEvent = %T{Type: %v, Char: %q (%[3]v)}\n", gotEvent, gotEvent.Type, gotEvent.Char)
|
||||
|
||||
assert(t, "r.GetChar().Type", gotEvent.Type, test.wantKey.Type)
|
||||
assert(t, "r.GetChar().Char", gotEvent.Char, test.wantKey.Char)
|
||||
assert(t, "r.GetChar(true).Type", gotEvent.Type, test.wantKey.Type)
|
||||
assert(t, "r.GetChar(true).Char", gotEvent.Char, test.wantKey.Char)
|
||||
}
|
||||
|
||||
r.Close()
|
||||
|
||||
@@ -2,6 +2,7 @@ package tui
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
@@ -829,16 +830,18 @@ type FullscreenRenderer struct {
|
||||
theme *ColorTheme
|
||||
mouse bool
|
||||
forceBlack bool
|
||||
tabstop int
|
||||
prevDownTime time.Time
|
||||
clicks [][2]int
|
||||
showCursor bool
|
||||
}
|
||||
|
||||
func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Renderer {
|
||||
func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int) Renderer {
|
||||
r := &FullscreenRenderer{
|
||||
theme: theme,
|
||||
mouse: mouse,
|
||||
forceBlack: forceBlack,
|
||||
tabstop: tabstop,
|
||||
prevDownTime: time.Unix(0, 0),
|
||||
clicks: [][2]int{},
|
||||
showCursor: true}
|
||||
@@ -1360,3 +1363,45 @@ func initPalette(theme *ColorTheme) {
|
||||
func runeWidth(r rune) int {
|
||||
return uniseg.StringWidth(string(r))
|
||||
}
|
||||
|
||||
// WrappedLine represents a single visual line after character-level wrapping.
|
||||
type WrappedLine struct {
|
||||
Text string
|
||||
DisplayWidth int
|
||||
}
|
||||
|
||||
// WrapLine splits a single line (no embedded \n) into visual lines
|
||||
// that fit within initialMax columns. Character-level wrapping only.
|
||||
func WrapLine(input string, prefixLength int, initialMax int, tabstop int, wrapSignWidth int) []WrappedLine {
|
||||
lines := []WrappedLine{}
|
||||
width := 0
|
||||
line := ""
|
||||
gr := uniseg.NewGraphemes(input)
|
||||
max := initialMax
|
||||
for gr.Next() {
|
||||
rs := gr.Runes()
|
||||
str := string(rs)
|
||||
var w int
|
||||
if len(rs) == 1 && rs[0] == '\t' {
|
||||
w = tabstop - (prefixLength+width)%tabstop
|
||||
str = strings.Repeat(" ", w)
|
||||
} else if rs[0] == '\r' {
|
||||
w++
|
||||
} else {
|
||||
w = uniseg.StringWidth(str)
|
||||
}
|
||||
width += w
|
||||
|
||||
if prefixLength+width <= max {
|
||||
line += str
|
||||
} else {
|
||||
lines = append(lines, WrappedLine{string(line), width - w})
|
||||
line = str
|
||||
prefixLength = 0
|
||||
width = w
|
||||
max = initialMax - wrapSignWidth
|
||||
}
|
||||
}
|
||||
lines = append(lines, WrappedLine{string(line), width})
|
||||
return lines
|
||||
}
|
||||
|
||||
@@ -2,6 +2,46 @@ package tui
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestWrapLine(t *testing.T) {
|
||||
// Basic wrapping
|
||||
lines := WrapLine("hello world", 0, 7, 8, 2)
|
||||
if len(lines) != 2 || lines[0].Text != "hello w" || lines[1].Text != "orld" {
|
||||
t.Errorf("Basic wrap: %v", lines)
|
||||
}
|
||||
|
||||
// Exact fit — no wrapping needed
|
||||
lines = WrapLine("hello", 0, 5, 8, 2)
|
||||
if len(lines) != 1 || lines[0].Text != "hello" || lines[0].DisplayWidth != 5 {
|
||||
t.Errorf("Exact fit: %v", lines)
|
||||
}
|
||||
|
||||
// With prefix length
|
||||
lines = WrapLine("hello", 3, 5, 8, 2)
|
||||
if len(lines) != 2 || lines[0].Text != "he" || lines[1].Text != "llo" {
|
||||
t.Errorf("Prefix length: %v", lines)
|
||||
}
|
||||
|
||||
// Empty string
|
||||
lines = WrapLine("", 0, 10, 8, 2)
|
||||
if len(lines) != 1 || lines[0].Text != "" || lines[0].DisplayWidth != 0 {
|
||||
t.Errorf("Empty string: %v", lines)
|
||||
}
|
||||
|
||||
// Continuation lines account for wrapSignWidth
|
||||
lines = WrapLine("abcdefghij", 0, 5, 8, 2)
|
||||
// First line: "abcde" (5 chars fit in width 5)
|
||||
// Continuation max: 5-2=3, so "fgh" then "ij"
|
||||
if len(lines) != 3 || lines[0].Text != "abcde" || lines[1].Text != "fgh" || lines[2].Text != "ij" {
|
||||
t.Errorf("Continuation: %v", lines)
|
||||
}
|
||||
|
||||
// Tab expansion
|
||||
lines = WrapLine("\there", 0, 10, 4, 2)
|
||||
if len(lines) != 1 || lines[0].DisplayWidth != 8 {
|
||||
t.Errorf("Tab: %v", lines)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHexToColor(t *testing.T) {
|
||||
assert := func(expr string, r, g, b int) {
|
||||
color := HexToColor(expr)
|
||||
|
||||
Reference in New Issue
Block a user