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:
20
CHANGELOG.md
20
CHANGELOG.md
@@ -3,6 +3,14 @@ CHANGELOG
|
|||||||
|
|
||||||
0.68.0
|
0.68.0
|
||||||
------
|
------
|
||||||
|
- Implemented word wrapping in the preview window
|
||||||
|
- Added `wrap-word` flag for `--preview-window` to enable word-level wrapping
|
||||||
|
- Added `toggle-preview-wrap-word` action
|
||||||
|
```sh
|
||||||
|
fzf --preview 'bat --style=plain --color=always {}' \
|
||||||
|
--preview-window wrap-word \
|
||||||
|
--bind space:toggle-preview-wrap-word
|
||||||
|
```
|
||||||
- Added support for underline style variants in `--color`:
|
- Added support for underline style variants in `--color`:
|
||||||
`underline-double`, `underline-curly`, `underline-dotted`, `underline-dashed`
|
`underline-double`, `underline-curly`, `underline-dotted`, `underline-dashed`
|
||||||
```sh
|
```sh
|
||||||
@@ -22,17 +30,17 @@ CHANGELOG
|
|||||||
- Bug fixes
|
- Bug fixes
|
||||||
- Fixed symlinks to directories being returned as files (#4676) (@skk64)
|
- Fixed symlinks to directories being returned as files (#4676) (@skk64)
|
||||||
- Fixed SIGHUP signal handling (#4668) (@LangLangBart)
|
- Fixed SIGHUP signal handling (#4668) (@LangLangBart)
|
||||||
- Fixed preview process not killed on exit
|
- Fixed preview process not killed on exit (#4667)
|
||||||
- Fixed coloring of items with zero-width characters
|
- Fixed coloring of items with zero-width characters (#4620)
|
||||||
- Fixed `track-current` unset after a combined movement action
|
- Fixed `track-current` unset after a combined movement action (#4649)
|
||||||
- Fixed `--accept-nth` being ignored in filter mode (#4636) (@charemma)
|
- Fixed `--accept-nth` being ignored in filter mode (#4636) (@charemma)
|
||||||
- Fixed display width calculation with `maxWidth` (#4596) (@LangLangBart)
|
- Fixed display width calculation with `maxWidth` (#4596) (@LangLangBart)
|
||||||
- Fixed clearing of the rest of the current line on start
|
- Fixed clearing of the rest of the current line on start (#4652)
|
||||||
- Fixed `x-api-key` header not required for GET requests
|
- Fixed `x-api-key` header not required for GET requests (#4627)
|
||||||
- Fixed key reading not cancelled when `execute` triggered via a server request (#4653)
|
- Fixed key reading not cancelled when `execute` triggered via a server request (#4653)
|
||||||
- Fixed rebind of readline command `redraw-current-line` (#4635) (@jameslazo)
|
- Fixed rebind of readline command `redraw-current-line` (#4635) (@jameslazo)
|
||||||
- Fixed `fzf-tmux` `TERM` quoting and added `mktemp` usage (#4664) (@Goofygiraffe06)
|
- Fixed `fzf-tmux` `TERM` quoting and added `mktemp` usage (#4664) (@Goofygiraffe06)
|
||||||
- Do not allow very long queries in `FuzzyMatchV2`
|
- Do not allow very long queries in `FuzzyMatchV2` (#4608)
|
||||||
|
|
||||||
0.67.0
|
0.67.0
|
||||||
------
|
------
|
||||||
|
|||||||
@@ -926,7 +926,7 @@ default value 0 (or \fBcenter\fR) will put the label at the center of the
|
|||||||
border line.
|
border line.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-preview\-window=" "[POSITION][,SIZE[%]][,border\-STYLE][,[no]wrap][,[no]follow][,[no]cycle][,[no]info][,[no]hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]"
|
.BI "\-\-preview\-window=" "[POSITION][,SIZE[%]][,border\-STYLE][,[no]wrap][,wrap\-word][,[no]follow][,[no]cycle][,[no]info][,[no]hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]"
|
||||||
|
|
||||||
.RS
|
.RS
|
||||||
.B POSITION: (default: right)
|
.B POSITION: (default: right)
|
||||||
@@ -944,7 +944,8 @@ default until \fBtoggle\-preview\fR action is triggered.
|
|||||||
execute the command in the background.
|
execute the command in the background.
|
||||||
|
|
||||||
* Long lines are truncated by default. Line wrap can be enabled with
|
* Long lines are truncated by default. Line wrap can be enabled with
|
||||||
\fBwrap\fR flag.
|
\fBwrap\fR flag. \fBwrap\-word\fR flag enables word-level wrapping, which
|
||||||
|
breaks lines at word boundaries instead of mid-word.
|
||||||
|
|
||||||
* Preview window will automatically scroll to the bottom when \fBfollow\fR
|
* Preview window will automatically scroll to the bottom when \fBfollow\fR
|
||||||
flag is set, similarly to how \fBtail \-f\fR works.
|
flag is set, similarly to how \fBtail \-f\fR works.
|
||||||
@@ -1961,6 +1962,7 @@ A key or an event can be bound to one or more of the following actions.
|
|||||||
\fBtoggle\-multi\-line\fR
|
\fBtoggle\-multi\-line\fR
|
||||||
\fBtoggle\-preview\fR
|
\fBtoggle\-preview\fR
|
||||||
\fBtoggle\-preview\-wrap\fR
|
\fBtoggle\-preview\-wrap\fR
|
||||||
|
\fBtoggle\-preview\-wrap\-word\fR
|
||||||
\fBtoggle\-raw\fR (toggle raw mode for displaying non-matching items)
|
\fBtoggle\-raw\fR (toggle raw mode for displaying non-matching items)
|
||||||
\fBtoggle\-search\fR (toggle search functionality)
|
\fBtoggle\-search\fR (toggle search functionality)
|
||||||
\fBtoggle\-sort\fR
|
\fBtoggle\-sort\fR
|
||||||
|
|||||||
@@ -107,82 +107,83 @@ func _() {
|
|||||||
_ = x[actHidePreview-96]
|
_ = x[actHidePreview-96]
|
||||||
_ = x[actTogglePreview-97]
|
_ = x[actTogglePreview-97]
|
||||||
_ = x[actTogglePreviewWrap-98]
|
_ = x[actTogglePreviewWrap-98]
|
||||||
_ = x[actTransform-99]
|
_ = x[actTogglePreviewWrapWord-99]
|
||||||
_ = x[actTransformBorderLabel-100]
|
_ = x[actTransform-100]
|
||||||
_ = x[actTransformGhost-101]
|
_ = x[actTransformBorderLabel-101]
|
||||||
_ = x[actTransformHeader-102]
|
_ = x[actTransformGhost-102]
|
||||||
_ = x[actTransformFooter-103]
|
_ = x[actTransformHeader-103]
|
||||||
_ = x[actTransformHeaderLabel-104]
|
_ = x[actTransformFooter-104]
|
||||||
_ = x[actTransformFooterLabel-105]
|
_ = x[actTransformHeaderLabel-105]
|
||||||
_ = x[actTransformInputLabel-106]
|
_ = x[actTransformFooterLabel-106]
|
||||||
_ = x[actTransformListLabel-107]
|
_ = x[actTransformInputLabel-107]
|
||||||
_ = x[actTransformNth-108]
|
_ = x[actTransformListLabel-108]
|
||||||
_ = x[actTransformPointer-109]
|
_ = x[actTransformNth-109]
|
||||||
_ = x[actTransformPreviewLabel-110]
|
_ = x[actTransformPointer-110]
|
||||||
_ = x[actTransformPrompt-111]
|
_ = x[actTransformPreviewLabel-111]
|
||||||
_ = x[actTransformQuery-112]
|
_ = x[actTransformPrompt-112]
|
||||||
_ = x[actTransformSearch-113]
|
_ = x[actTransformQuery-113]
|
||||||
_ = x[actTrigger-114]
|
_ = x[actTransformSearch-114]
|
||||||
_ = x[actBgTransform-115]
|
_ = x[actTrigger-115]
|
||||||
_ = x[actBgTransformBorderLabel-116]
|
_ = x[actBgTransform-116]
|
||||||
_ = x[actBgTransformGhost-117]
|
_ = x[actBgTransformBorderLabel-117]
|
||||||
_ = x[actBgTransformHeader-118]
|
_ = x[actBgTransformGhost-118]
|
||||||
_ = x[actBgTransformFooter-119]
|
_ = x[actBgTransformHeader-119]
|
||||||
_ = x[actBgTransformHeaderLabel-120]
|
_ = x[actBgTransformFooter-120]
|
||||||
_ = x[actBgTransformFooterLabel-121]
|
_ = x[actBgTransformHeaderLabel-121]
|
||||||
_ = x[actBgTransformInputLabel-122]
|
_ = x[actBgTransformFooterLabel-122]
|
||||||
_ = x[actBgTransformListLabel-123]
|
_ = x[actBgTransformInputLabel-123]
|
||||||
_ = x[actBgTransformNth-124]
|
_ = x[actBgTransformListLabel-124]
|
||||||
_ = x[actBgTransformPointer-125]
|
_ = x[actBgTransformNth-125]
|
||||||
_ = x[actBgTransformPreviewLabel-126]
|
_ = x[actBgTransformPointer-126]
|
||||||
_ = x[actBgTransformPrompt-127]
|
_ = x[actBgTransformPreviewLabel-127]
|
||||||
_ = x[actBgTransformQuery-128]
|
_ = x[actBgTransformPrompt-128]
|
||||||
_ = x[actBgTransformSearch-129]
|
_ = x[actBgTransformQuery-129]
|
||||||
_ = x[actBgCancel-130]
|
_ = x[actBgTransformSearch-130]
|
||||||
_ = x[actSearch-131]
|
_ = x[actBgCancel-131]
|
||||||
_ = x[actPreview-132]
|
_ = x[actSearch-132]
|
||||||
_ = x[actPreviewTop-133]
|
_ = x[actPreview-133]
|
||||||
_ = x[actPreviewBottom-134]
|
_ = x[actPreviewTop-134]
|
||||||
_ = x[actPreviewUp-135]
|
_ = x[actPreviewBottom-135]
|
||||||
_ = x[actPreviewDown-136]
|
_ = x[actPreviewUp-136]
|
||||||
_ = x[actPreviewPageUp-137]
|
_ = x[actPreviewDown-137]
|
||||||
_ = x[actPreviewPageDown-138]
|
_ = x[actPreviewPageUp-138]
|
||||||
_ = x[actPreviewHalfPageUp-139]
|
_ = x[actPreviewPageDown-139]
|
||||||
_ = x[actPreviewHalfPageDown-140]
|
_ = x[actPreviewHalfPageUp-140]
|
||||||
_ = x[actPrevHistory-141]
|
_ = x[actPreviewHalfPageDown-141]
|
||||||
_ = x[actPrevSelected-142]
|
_ = x[actPrevHistory-142]
|
||||||
_ = x[actPrint-143]
|
_ = x[actPrevSelected-143]
|
||||||
_ = x[actPut-144]
|
_ = x[actPrint-144]
|
||||||
_ = x[actNextHistory-145]
|
_ = x[actPut-145]
|
||||||
_ = x[actNextSelected-146]
|
_ = x[actNextHistory-146]
|
||||||
_ = x[actExecute-147]
|
_ = x[actNextSelected-147]
|
||||||
_ = x[actExecuteSilent-148]
|
_ = x[actExecute-148]
|
||||||
_ = x[actExecuteMulti-149]
|
_ = x[actExecuteSilent-149]
|
||||||
_ = x[actSigStop-150]
|
_ = x[actExecuteMulti-150]
|
||||||
_ = x[actBest-151]
|
_ = x[actSigStop-151]
|
||||||
_ = x[actFirst-152]
|
_ = x[actBest-152]
|
||||||
_ = x[actLast-153]
|
_ = x[actFirst-153]
|
||||||
_ = x[actReload-154]
|
_ = x[actLast-154]
|
||||||
_ = x[actReloadSync-155]
|
_ = x[actReload-155]
|
||||||
_ = x[actDisableSearch-156]
|
_ = x[actReloadSync-156]
|
||||||
_ = x[actEnableSearch-157]
|
_ = x[actDisableSearch-157]
|
||||||
_ = x[actSelect-158]
|
_ = x[actEnableSearch-158]
|
||||||
_ = x[actDeselect-159]
|
_ = x[actSelect-159]
|
||||||
_ = x[actUnbind-160]
|
_ = x[actDeselect-160]
|
||||||
_ = x[actRebind-161]
|
_ = x[actUnbind-161]
|
||||||
_ = x[actToggleBind-162]
|
_ = x[actRebind-162]
|
||||||
_ = x[actBecome-163]
|
_ = x[actToggleBind-163]
|
||||||
_ = x[actShowHeader-164]
|
_ = x[actBecome-164]
|
||||||
_ = x[actHideHeader-165]
|
_ = x[actShowHeader-165]
|
||||||
_ = x[actBell-166]
|
_ = x[actHideHeader-166]
|
||||||
_ = x[actExclude-167]
|
_ = x[actBell-167]
|
||||||
_ = x[actExcludeMulti-168]
|
_ = x[actExclude-168]
|
||||||
_ = x[actAsync-169]
|
_ = 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 {
|
func (i actionType) String() string {
|
||||||
if i < 0 || i >= actionType(len(_actionType_index)-1) {
|
if i < 0 || i >= actionType(len(_actionType_index)-1) {
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ Usage: fzf [options]
|
|||||||
--preview=COMMAND Command to preview highlighted line ({})
|
--preview=COMMAND Command to preview highlighted line ({})
|
||||||
--preview-window=OPT Preview window layout (default: right:50%)
|
--preview-window=OPT Preview window layout (default: right:50%)
|
||||||
[up|down|left|right][,SIZE[%]]
|
[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]
|
[,[no]hidden][,border-STYLE]
|
||||||
[,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES]
|
[,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES]
|
||||||
[,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]
|
[,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]
|
||||||
@@ -367,6 +367,7 @@ type previewOpts struct {
|
|||||||
scroll string
|
scroll string
|
||||||
hidden bool
|
hidden bool
|
||||||
wrap bool
|
wrap bool
|
||||||
|
wrapWord bool
|
||||||
cycle bool
|
cycle bool
|
||||||
follow bool
|
follow bool
|
||||||
info bool
|
info bool
|
||||||
@@ -543,7 +544,7 @@ func (o *previewOpts) compare(active *previewOpts, b *previewOpts) previewOptsCo
|
|||||||
return previewOptsDifferentLayout
|
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
|
return previewOptsSame
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -691,7 +692,13 @@ func filterNonEmpty(input []string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func defaultPreviewOpts(command string) previewOpts {
|
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 {
|
func defaultOptions() *Options {
|
||||||
@@ -1863,6 +1870,8 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
|||||||
appendAction(actTogglePreview)
|
appendAction(actTogglePreview)
|
||||||
case "toggle-preview-wrap":
|
case "toggle-preview-wrap":
|
||||||
appendAction(actTogglePreviewWrap)
|
appendAction(actTogglePreviewWrap)
|
||||||
|
case "toggle-preview-wrap-word":
|
||||||
|
appendAction(actTogglePreviewWrapWord)
|
||||||
case "toggle-sort":
|
case "toggle-sort":
|
||||||
appendAction(actToggleSort)
|
appendAction(actToggleSort)
|
||||||
case "offset-up":
|
case "offset-up":
|
||||||
@@ -2274,8 +2283,13 @@ func parsePreviewWindowImpl(opts *previewOpts, input string) error {
|
|||||||
opts.hidden = false
|
opts.hidden = false
|
||||||
case "wrap":
|
case "wrap":
|
||||||
opts.wrap = true
|
opts.wrap = true
|
||||||
|
opts.wrapWord = false
|
||||||
|
case "wrap-word":
|
||||||
|
opts.wrap = true
|
||||||
|
opts.wrapWord = true
|
||||||
case "nowrap":
|
case "nowrap":
|
||||||
opts.wrap = false
|
opts.wrap = false
|
||||||
|
opts.wrapWord = false
|
||||||
case "cycle":
|
case "cycle":
|
||||||
opts.cycle = true
|
opts.cycle = true
|
||||||
case "nocycle":
|
case "nocycle":
|
||||||
|
|||||||
@@ -448,6 +448,20 @@ func TestPreviewOpts(t *testing.T) {
|
|||||||
opts.Preview.size.size == 70) {
|
opts.Preview.size.size == 70) {
|
||||||
t.Error(opts.Preview)
|
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) {
|
func TestAdditiveExpect(t *testing.T) {
|
||||||
|
|||||||
185
src/terminal.go
185
src/terminal.go
@@ -617,6 +617,7 @@ const (
|
|||||||
actHidePreview
|
actHidePreview
|
||||||
actTogglePreview
|
actTogglePreview
|
||||||
actTogglePreviewWrap
|
actTogglePreviewWrap
|
||||||
|
actTogglePreviewWrapWord
|
||||||
|
|
||||||
actTransform
|
actTransform
|
||||||
actTransformBorderLabel
|
actTransformBorderLabel
|
||||||
@@ -941,7 +942,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
|
|||||||
}
|
}
|
||||||
if fullscreen {
|
if fullscreen {
|
||||||
if tui.HasFullscreenRenderer() {
|
if tui.HasFullscreenRenderer() {
|
||||||
renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse)
|
renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop)
|
||||||
} else {
|
} else {
|
||||||
renderer, err = tui.NewLightRenderer(opts.TtyDefault, ttyin, opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit,
|
renderer, err = tui.NewLightRenderer(opts.TtyDefault, ttyin, opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit,
|
||||||
true, func(h int) int { return h })
|
true, func(h int) int { return h })
|
||||||
@@ -4089,6 +4090,75 @@ func extractPassThroughs(line string) ([]string, string) {
|
|||||||
return passThroughs, transformed
|
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) {
|
func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unchanged bool) {
|
||||||
maxWidth := t.pwindow.Width()
|
maxWidth := t.pwindow.Width()
|
||||||
var ansi *ansiState
|
var ansi *ansiState
|
||||||
@@ -4182,48 +4252,77 @@ Loop:
|
|||||||
continue
|
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
|
var fillRet tui.FillReturn
|
||||||
prefixWidth := 0
|
wrap := t.activePreviewOpts.wrap
|
||||||
var url *url
|
for subIdx, subLine := range subLines {
|
||||||
_, _, ansi = extractColor(line, ansi, func(str string, ansi *ansiState) bool {
|
// Render wrap sign for continuation sub-lines
|
||||||
trimmed := []rune(str)
|
if subIdx > 0 {
|
||||||
isTrimmed := false
|
if fillRet == tui.FillContinue {
|
||||||
if !t.activePreviewOpts.wrap {
|
fillRet = t.pwindow.Fill("\n")
|
||||||
trimmed, isTrimmed = t.trimRight(trimmed, maxWidth-t.pwindow.X())
|
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
|
prefixWidth := t.pwindow.X()
|
||||||
t.pwindow.LinkBegin(url.uri, url.params)
|
var url *url
|
||||||
}
|
_, _, ansi = extractColor(subLine, ansi, func(str string, ansi *ansiState) bool {
|
||||||
if url != nil && (ansi == nil || ansi.url == nil) {
|
trimmed := []rune(str)
|
||||||
url = nil
|
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()
|
t.pwindow.LinkEnd()
|
||||||
}
|
}
|
||||||
if ansi != nil {
|
|
||||||
lbg = ansi.lbg
|
if fillRet == tui.FillSuspend {
|
||||||
} else {
|
t.previewed.filled = true
|
||||||
lbg = -1
|
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()
|
t.previewer.scrollable = t.previewer.scrollable || t.pwindow.Y() == height-1 && t.pwindow.X() == t.pwindow.Width()
|
||||||
if fillRet == tui.FillNextLine {
|
if fillRet == tui.FillNextLine {
|
||||||
continue
|
continue
|
||||||
@@ -5972,9 +6071,17 @@ func (t *Terminal) Loop() error {
|
|||||||
t.cancelPreview()
|
t.cancelPreview()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case actTogglePreviewWrap:
|
case actTogglePreviewWrap, actTogglePreviewWrapWord:
|
||||||
if t.hasPreviewWindow() {
|
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
|
// Reset preview version so that full redraw occurs
|
||||||
t.previewed.version = 0
|
t.previewed.version = 0
|
||||||
req(reqPreviewRefresh)
|
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"
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/junegunn/fzf/src/util"
|
"github.com/junegunn/fzf/src/util"
|
||||||
"github.com/rivo/uniseg"
|
|
||||||
|
|
||||||
"golang.org/x/term"
|
"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)
|
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 {
|
func (w *LightWindow) fill(str string, resetCode string) FillReturn {
|
||||||
allLines := strings.Split(str, "\n")
|
allLines := strings.Split(str, "\n")
|
||||||
for i, line := range allLines {
|
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 {
|
for j, wl := range lines {
|
||||||
w.stderrInternal(wl.text, false, resetCode)
|
w.stderrInternal(wl.Text, false, resetCode)
|
||||||
w.posx += wl.displayWidth
|
w.posx += wl.DisplayWidth
|
||||||
|
|
||||||
// Wrap line
|
// Wrap line
|
||||||
if j < len(lines)-1 || i < len(allLines)-1 {
|
if j < len(lines)-1 || i < len(allLines)-1 {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package tui
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gdamore/tcell/v2"
|
"github.com/gdamore/tcell/v2"
|
||||||
@@ -53,6 +54,7 @@ type TcellWindow struct {
|
|||||||
showCursor bool
|
showCursor bool
|
||||||
wrapSign string
|
wrapSign string
|
||||||
wrapSignWidth int
|
wrapSignWidth int
|
||||||
|
tabstop int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *TcellWindow) Top() int {
|
func (w *TcellWindow) Top() int {
|
||||||
@@ -757,7 +759,8 @@ func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int,
|
|||||||
height: height,
|
height: height,
|
||||||
normal: normal,
|
normal: normal,
|
||||||
borderStyle: borderStyle,
|
borderStyle: borderStyle,
|
||||||
showCursor: r.showCursor}
|
showCursor: r.showCursor,
|
||||||
|
tabstop: r.tabstop}
|
||||||
w.Erase()
|
w.Erase()
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
@@ -894,10 +897,8 @@ func (w *TcellWindow) CPrint(pair ColorPair, text string) {
|
|||||||
w.printString(text, pair)
|
w.printString(text, pair)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
|
func (w *TcellWindow) pairStyle(pair ColorPair) tcell.Style {
|
||||||
lx := 0
|
|
||||||
a := pair.Attr()
|
a := pair.Attr()
|
||||||
|
|
||||||
var style tcell.Style
|
var style tcell.Style
|
||||||
if w.color {
|
if w.color {
|
||||||
style = pair.style()
|
style = pair.style()
|
||||||
@@ -919,61 +920,61 @@ func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
|
|||||||
} else {
|
} else {
|
||||||
style = style.Underline(false)
|
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)
|
gr := uniseg.NewGraphemes(text)
|
||||||
Loop:
|
|
||||||
for gr.Next() {
|
for gr.Next() {
|
||||||
st := style
|
st := style
|
||||||
rs := gr.Runes()
|
rs := gr.Runes()
|
||||||
if len(rs) == 1 {
|
if len(rs) == 1 && rs[0] == '\r' {
|
||||||
r := rs[0]
|
st = style.Dim(true)
|
||||||
switch r {
|
rs[0] = '␍'
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
xPos := w.left + w.lastX
|
||||||
yPos := w.top + w.lastY
|
yPos := w.top + w.lastY
|
||||||
if yPos >= (w.top + w.height) {
|
if xPos < (w.left+w.width) && yPos < (w.top+w.height) {
|
||||||
return FillSuspend
|
_screen.SetContent(xPos, yPos, rs[0], rs[1:], st)
|
||||||
}
|
}
|
||||||
|
w.lastX += util.StringWidth(string(rs))
|
||||||
_screen.SetContent(xPos, yPos, rs[0], rs[1:], st)
|
|
||||||
lx += 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.lastY++
|
||||||
w.lastX = 0
|
w.lastX = 0
|
||||||
return FillNextLine
|
return FillNextLine
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ func TestGetCharEventKey(t *testing.T) {
|
|||||||
{giveKey{tcell.KeyPause, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // unhandled
|
{giveKey{tcell.KeyPause, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // unhandled
|
||||||
|
|
||||||
}
|
}
|
||||||
r := NewFullscreenRenderer(&ColorTheme{}, false, false)
|
r := NewFullscreenRenderer(&ColorTheme{}, false, false, 8)
|
||||||
r.Init()
|
r.Init()
|
||||||
|
|
||||||
// run and evaluate the tests
|
// 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())
|
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
|
// 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
|
// skip Resize events, those are sometimes put in the buffer outside of this test
|
||||||
if initialResizeAsInvalid && gotEvent.Type == Invalid {
|
if initialResizeAsInvalid && gotEvent.Type == Invalid {
|
||||||
t.Logf("Resize as Invalid swallowed")
|
t.Logf("Resize as Invalid swallowed")
|
||||||
initialResizeAsInvalid = false
|
initialResizeAsInvalid = false
|
||||||
gotEvent = r.GetChar()
|
gotEvent = r.GetChar(true)
|
||||||
}
|
}
|
||||||
if gotEvent.Type == Resize {
|
if gotEvent.Type == Resize {
|
||||||
t.Logf("Resize swallowed")
|
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("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)
|
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(true).Type", gotEvent.Type, test.wantKey.Type)
|
||||||
assert(t, "r.GetChar().Char", gotEvent.Char, test.wantKey.Char)
|
assert(t, "r.GetChar(true).Char", gotEvent.Char, test.wantKey.Char)
|
||||||
}
|
}
|
||||||
|
|
||||||
r.Close()
|
r.Close()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package tui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/junegunn/fzf/src/util"
|
"github.com/junegunn/fzf/src/util"
|
||||||
@@ -829,16 +830,18 @@ type FullscreenRenderer struct {
|
|||||||
theme *ColorTheme
|
theme *ColorTheme
|
||||||
mouse bool
|
mouse bool
|
||||||
forceBlack bool
|
forceBlack bool
|
||||||
|
tabstop int
|
||||||
prevDownTime time.Time
|
prevDownTime time.Time
|
||||||
clicks [][2]int
|
clicks [][2]int
|
||||||
showCursor bool
|
showCursor bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Renderer {
|
func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int) Renderer {
|
||||||
r := &FullscreenRenderer{
|
r := &FullscreenRenderer{
|
||||||
theme: theme,
|
theme: theme,
|
||||||
mouse: mouse,
|
mouse: mouse,
|
||||||
forceBlack: forceBlack,
|
forceBlack: forceBlack,
|
||||||
|
tabstop: tabstop,
|
||||||
prevDownTime: time.Unix(0, 0),
|
prevDownTime: time.Unix(0, 0),
|
||||||
clicks: [][2]int{},
|
clicks: [][2]int{},
|
||||||
showCursor: true}
|
showCursor: true}
|
||||||
@@ -1360,3 +1363,45 @@ func initPalette(theme *ColorTheme) {
|
|||||||
func runeWidth(r rune) int {
|
func runeWidth(r rune) int {
|
||||||
return uniseg.StringWidth(string(r))
|
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"
|
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) {
|
func TestHexToColor(t *testing.T) {
|
||||||
assert := func(expr string, r, g, b int) {
|
assert := func(expr string, r, g, b int) {
|
||||||
color := HexToColor(expr)
|
color := HexToColor(expr)
|
||||||
|
|||||||
Reference in New Issue
Block a user