mirror of
https://github.com/junegunn/fzf
synced 2026-07-18 16:13:19 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eae8d9d27e | |||
| e58c5e6bd8 | |||
| 7fe4831be6 | |||
| 6030226835 | |||
| e877b6f846 | |||
| b9cbe3a6b8 | |||
| 7e134f0a78 | |||
| 3c0de84dab | |||
| 374b01242d | |||
| c0860259fa | |||
| 1462f1358d | |||
| 8e340dabbc | |||
| 4dcedc01df | |||
| 24832e97ef |
@@ -96,6 +96,25 @@ archives:
|
||||
files:
|
||||
- non-existent*
|
||||
|
||||
nfpms:
|
||||
- package_name: fzf
|
||||
file_name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
|
||||
vendor: junegunn
|
||||
homepage: https://github.com/junegunn/fzf
|
||||
maintainer: Junegunn Choi <junegunn.c@gmail.com>
|
||||
description: Command-line fuzzy finder
|
||||
license: MIT
|
||||
section: utils
|
||||
formats:
|
||||
- deb
|
||||
ids:
|
||||
- fzf
|
||||
contents:
|
||||
- src: man/man1/fzf.1
|
||||
dst: /usr/share/man/man1/fzf.1
|
||||
- src: LICENSE
|
||||
dst: /usr/share/doc/fzf/copyright
|
||||
|
||||
release:
|
||||
github:
|
||||
owner: junegunn
|
||||
|
||||
@@ -1,6 +1,31 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
0.74.1
|
||||
------
|
||||
- The default separator on the info line is no longer shown when the input section is already visually separated from the list section by a border line
|
||||
```sh
|
||||
# No separator shown below the header border
|
||||
fzf --style full --input-border none --header foo
|
||||
|
||||
# Separator shown; no border separates the input section from the list section
|
||||
fzf --style full --input-border none --header foo --no-header-border
|
||||
|
||||
# No separator below the border of the preview window at 'next' position
|
||||
fzf --preview : --preview-window next
|
||||
|
||||
# Conversely, separator is now shown when the input border does not draw
|
||||
# a line facing the list section
|
||||
fzf --input-border bottom
|
||||
```
|
||||
- Rendering improvements
|
||||
- Each frame is now wrapped in synchronized update mode (mode 2026) to reduce flickering on supported terminals
|
||||
- Reduced rendering output by 10-23% by skipping redundant SGR sequences
|
||||
- Fixed ghost characters and misplaced colors inside Zellij by using CHA instead of CR + CUF for horizontal cursor movement (#4858, zellij-org/zellij#5370)
|
||||
- Fixed cursor restoration on exit with `--height --no-clear` inside Neovim terminal by using DECSC/DECRC instead of `CSI s`/`CSI u`
|
||||
- nushell: fixed deprecation error of `str downcase` on nushell 0.114.0 or above (#4857) (@sim590)
|
||||
- Each release now includes `.deb` packages for easy installation on Debian-based distros (#4859)
|
||||
|
||||
0.74.0
|
||||
------
|
||||
_Release highlights: https://junegunn.github.io/fzf/releases/0.74.0/_
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ triggered by a tag push.
|
||||
2. Verify file consistency, sign the tag, and push the tag.
|
||||
|
||||
```sh
|
||||
make tag VERSION=0.74.0
|
||||
make tag VERSION=0.74.1
|
||||
```
|
||||
|
||||
`make tag` runs `prerelease` first (checks that the version
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
set -u
|
||||
|
||||
version=0.74.0
|
||||
version=0.74.1
|
||||
auto_completion=
|
||||
key_bindings=
|
||||
update_config=2
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
$version="0.74.0"
|
||||
$version="0.74.1"
|
||||
|
||||
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
..
|
||||
.TH fzf\-tmux 1 "Jul 2026" "fzf 0.74.0" "fzf\-tmux - open fzf in tmux split pane"
|
||||
.TH fzf\-tmux 1 "Jul 2026" "fzf 0.74.1" "fzf\-tmux - open fzf in tmux split pane"
|
||||
|
||||
.SH NAME
|
||||
fzf\-tmux - open fzf in tmux split pane
|
||||
|
||||
+5
-1
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
..
|
||||
.TH fzf 1 "Jul 2026" "fzf 0.74.0" "fzf - a command-line fuzzy finder"
|
||||
.TH fzf 1 "Jul 2026" "fzf 0.74.1" "fzf - a command-line fuzzy finder"
|
||||
|
||||
.SH NAME
|
||||
fzf - a command-line fuzzy finder
|
||||
@@ -823,6 +823,10 @@ A synonym for \fB\-\-info=hidden\fB
|
||||
The given string will be repeated to form the horizontal separator on the info
|
||||
line (default: '─' or '\-' depending on \fB\-\-no\-unicode\fR).
|
||||
|
||||
Unless explicitly specified, the separator is not displayed if the input
|
||||
section is already visually separated from the list section by a border line
|
||||
(e.g. \fB\-\-input\-border\fR or \fB\-\-header\-border\fR).
|
||||
|
||||
ANSI color codes are supported.
|
||||
|
||||
.TP
|
||||
|
||||
+2
-1
@@ -116,7 +116,8 @@ def __fzf_list_hosts [] {
|
||||
(
|
||||
# Process ssh config files
|
||||
$ssh_configs | append $ssh_configs_d | append $ssh_config_global
|
||||
| where {|it| ($it | str downcase | str starts-with 'host') or ($it | str downcase | str starts-with 'hostname') }
|
||||
# NOTE: str lowercase is new from Nushell 0.114.0. Please upgrade to Nushell >=0.114.0 if you are seeing `extra positional argument` error regarding `str lowercase` in this block.
|
||||
| where {|it| ($it | str lowercase | str starts-with 'host') or ($it | str lowercase | str starts-with 'hostname') }
|
||||
| parse --regex '^\s*host(?:name)?\s+(?<hosts>.+)' # Extract hosts after keyword
|
||||
| default { hosts: null } # Handle lines that don't match regex
|
||||
| get hosts
|
||||
|
||||
+4
-1
@@ -3696,7 +3696,10 @@ func (opts *Options) noSeparatorLine() bool {
|
||||
if opts.Inputless {
|
||||
return true
|
||||
}
|
||||
sep := opts.Separator == nil && !opts.InputBorderShape.Visible() || opts.Separator != nil && len(*opts.Separator) > 0
|
||||
// NOTE: This does not know that the default separator can be suppressed at
|
||||
// runtime (see Terminal.separatorLength), so the minimum heights derived
|
||||
// from it can be one line taller than strictly necessary.
|
||||
sep := opts.Separator == nil && !inputBorderFacesList(opts.Layout, opts.InputBorderShape) || opts.Separator != nil && len(*opts.Separator) > 0
|
||||
return noSeparatorLine(opts.InfoStyle, sep)
|
||||
}
|
||||
|
||||
|
||||
+165
-22
@@ -271,6 +271,7 @@ type Terminal struct {
|
||||
ghost string
|
||||
separator labelPrinter
|
||||
separatorLen int
|
||||
separatorAuto bool
|
||||
spinner []string
|
||||
promptString string
|
||||
prompt func()
|
||||
@@ -1248,13 +1249,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
|
||||
}
|
||||
|
||||
// Determine input border shape
|
||||
if t.inputBorderShape == tui.BorderLine {
|
||||
if t.layout == layoutReverse {
|
||||
t.inputBorderShape = tui.BorderBottom
|
||||
} else {
|
||||
t.inputBorderShape = tui.BorderTop
|
||||
}
|
||||
}
|
||||
t.inputBorderShape = resolveInputBorderShape(t.layout, t.inputBorderShape)
|
||||
|
||||
// Inline borders are embedded between the list's top and bottom horizontals.
|
||||
// Shapes missing either one (none/phantom/line/single-sided) fall back to a plain
|
||||
@@ -1299,14 +1294,16 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
|
||||
}
|
||||
}
|
||||
|
||||
// Disable separator by default if input border is set
|
||||
if opts.Separator == nil && !t.inputBorderShape.Visible() || opts.Separator != nil && len(*opts.Separator) > 0 {
|
||||
bar := "─"
|
||||
if opts.Separator != nil {
|
||||
bar = *opts.Separator
|
||||
} else if !t.unicode {
|
||||
bar = "-"
|
||||
}
|
||||
// The default separator can be suppressed at runtime depending on the
|
||||
// window layout (see separatorLength)
|
||||
t.separatorAuto = opts.Separator == nil
|
||||
bar := "─"
|
||||
if opts.Separator != nil {
|
||||
bar = *opts.Separator
|
||||
} else if !t.unicode {
|
||||
bar = "-"
|
||||
}
|
||||
if len(bar) > 0 {
|
||||
t.separator, t.separatorLen = t.ansiLabelPrinter(bar, &tui.ColSeparator, true)
|
||||
}
|
||||
|
||||
@@ -1695,7 +1692,129 @@ func (t *Terminal) parsePrompt(prompt string) (func(), int) {
|
||||
}
|
||||
|
||||
func (t *Terminal) noSeparatorLine() bool {
|
||||
return t.inputless || noSeparatorLine(t.infoStyle, t.separatorLen > 0)
|
||||
return t.inputless || noSeparatorLine(t.infoStyle, t.separatorLength() > 0)
|
||||
}
|
||||
|
||||
// Effective length of the horizontal separator. The default separator is
|
||||
// suppressed when the input section is already visually separated from the
|
||||
// list section by a border line.
|
||||
func (t *Terminal) separatorLength() int {
|
||||
if t.separatorAuto && t.separatedByBorder() {
|
||||
return 0
|
||||
}
|
||||
return t.separatorLen
|
||||
}
|
||||
|
||||
// BorderLine for the input border resolves to a single line on the side
|
||||
// facing the list section
|
||||
func resolveInputBorderShape(layout layoutType, shape tui.BorderShape) tui.BorderShape {
|
||||
if shape != tui.BorderLine {
|
||||
return shape
|
||||
}
|
||||
if layout == layoutReverse {
|
||||
return tui.BorderBottom
|
||||
}
|
||||
return tui.BorderTop
|
||||
}
|
||||
|
||||
// Whether the input border draws a line on the side facing the list section
|
||||
func inputBorderFacesList(layout layoutType, shape tui.BorderShape) bool {
|
||||
shape = resolveInputBorderShape(layout, shape)
|
||||
if layout == layoutReverse {
|
||||
return shape.HasBottom()
|
||||
}
|
||||
return shape.HasTop()
|
||||
}
|
||||
|
||||
// Whether a border line is already drawn between the info line and the
|
||||
// section next to it on the list side, making the default horizontal
|
||||
// separator redundant
|
||||
func (t *Terminal) separatedByBorder() bool {
|
||||
// The input border itself draws a line on the side facing the list section
|
||||
if inputBorderFacesList(t.layout, t.inputBorderShape) {
|
||||
return true
|
||||
}
|
||||
// A preview window at 'next' position sits right next to the input
|
||||
// section, in front of the sections handled below
|
||||
anyNext := false
|
||||
for po := &t.previewOpts; po != nil; po = po.alternative {
|
||||
if po.position == posNext {
|
||||
anyNext = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if anyNext && t.hasPreviewer() {
|
||||
// The separator is hidden only when every spec in the threshold chain
|
||||
// displays a preview window at 'next' position with a border line
|
||||
// facing the input section. We cannot single out the active spec;
|
||||
// which one is active depends on the terminal size, and the
|
||||
// resolution (computePreviewSize in resizeWindows) itself consults
|
||||
// noSeparatorLine. When not every spec qualifies, err on the side of
|
||||
// showing the separator.
|
||||
if len(t.previewOpts.command) == 0 {
|
||||
// No preview command, so no preview window is normally shown, but
|
||||
// the preview(...) action can still display one at any time
|
||||
return false
|
||||
}
|
||||
// NOTE: resizeWindows can clear 'hidden' mid-layout when the
|
||||
// preview(...) action forces the window, so the frame being laid out
|
||||
// can briefly disagree with this decision until the next relayout
|
||||
for po := &t.previewOpts; po != nil; po = po.alternative {
|
||||
if po.position != posNext || po.hidden || po.size.size == 0 || !t.borderFacingInput(po.Border(t.layout)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
hasHeaderLinesWindow, headerLinesShape := t.determineHeaderLinesShape()
|
||||
hasHeaderWindow := t.hasHeaderWindow()
|
||||
// Mirror of hasInputWindow in resizeWindows; a 'next' preview position
|
||||
// gives the input section its own window even when no preview window is
|
||||
// shown. A 'next' spec here implies there is no previewer (the block
|
||||
// above returns otherwise), and without a previewer the position is
|
||||
// resolved from the first spec alone, so a 'next' in a threshold
|
||||
// alternative does not count.
|
||||
hasInputWindow := t.previewOpts.position == posNext || t.inputBorderShape.Visible() || hasHeaderWindow || hasHeaderLinesWindow
|
||||
if !hasInputWindow {
|
||||
// The input section is embedded in the list window and no border
|
||||
// separates the two
|
||||
return false
|
||||
}
|
||||
|
||||
// The input section has its own window. Determine which section it is
|
||||
// facing, and check if the border of that section draws a line toward it.
|
||||
// NOTE: hasHeaderWindow can be true with no header content when the input
|
||||
// border is visible (see Terminal.hasHeaderWindow). An empty header window
|
||||
// takes no space, so it cannot be the facing section.
|
||||
hasHeaderSection := hasHeaderWindow && t.visibleHeaderLines() > 0
|
||||
if hasHeaderSection && !t.headerFirst && t.headerBorderShape != tui.BorderInline {
|
||||
// Header section is facing the input section. An inline header is
|
||||
// excluded; it is drawn inside the list border and is covered by the
|
||||
// branches below.
|
||||
return t.borderFacingInput(t.headerBorderShape)
|
||||
}
|
||||
// Header lines section is facing the input section, except in
|
||||
// reverse-list layout where it is on the other side of the list section,
|
||||
// or with --header-first and no header section where it is moved past
|
||||
// the input section
|
||||
if hasHeaderLinesWindow && t.layout != layoutReverseList && (hasHeaderWindow || !t.headerFirst) {
|
||||
return t.borderFacingInput(headerLinesShape)
|
||||
}
|
||||
// The input section sits right next to the list section
|
||||
return t.borderFacingInput(t.listBorderShape)
|
||||
}
|
||||
|
||||
// Whether the border of the section next to the input section on the list
|
||||
// side draws a horizontal line facing the info line
|
||||
func (t *Terminal) borderFacingInput(shape tui.BorderShape) bool {
|
||||
if shape == tui.BorderInline {
|
||||
// Embedded between the horizontal lines of the list border
|
||||
return true
|
||||
}
|
||||
if t.layout == layoutReverse {
|
||||
return shape.HasTop()
|
||||
}
|
||||
return shape.HasBottom()
|
||||
}
|
||||
|
||||
func getScrollbar(perLine int, total int, height int, offset int) (int, int) {
|
||||
@@ -3330,6 +3449,7 @@ func (t *Terminal) printInfoImpl() {
|
||||
}
|
||||
pos := 0
|
||||
line := 0
|
||||
separatorLen := t.separatorLength()
|
||||
maxHeight := t.window.Height()
|
||||
move := func(y int, x int, clear bool) bool {
|
||||
if y < 0 || y >= maxHeight {
|
||||
@@ -3357,7 +3477,7 @@ func (t *Terminal) printInfoImpl() {
|
||||
str = string(trimmed)
|
||||
width = maxWidth
|
||||
}
|
||||
move(line, pos, t.separatorLen == 0)
|
||||
move(line, pos, separatorLen == 0)
|
||||
if t.reading {
|
||||
t.window.CPrint(tui.ColSpinner, str)
|
||||
} else {
|
||||
@@ -3366,7 +3486,7 @@ func (t *Terminal) printInfoImpl() {
|
||||
pos += width
|
||||
}
|
||||
printSeparator := func(fillLength int, pad bool) {
|
||||
if t.separatorLen > 0 {
|
||||
if separatorLen > 0 {
|
||||
t.separator(t.window, fillLength)
|
||||
t.window.Print(" ")
|
||||
} else if pad {
|
||||
@@ -3375,7 +3495,7 @@ func (t *Terminal) printInfoImpl() {
|
||||
}
|
||||
|
||||
if t.infoStyle == infoHidden {
|
||||
if t.separatorLen > 0 {
|
||||
if separatorLen > 0 {
|
||||
if !move(line+1, 0, false) {
|
||||
return
|
||||
}
|
||||
@@ -3438,7 +3558,7 @@ func (t *Terminal) printInfoImpl() {
|
||||
}
|
||||
switch t.infoStyle {
|
||||
case infoDefault:
|
||||
if !move(line+1, 0, t.separatorLen == 0) {
|
||||
if !move(line+1, 0, separatorLen == 0) {
|
||||
return
|
||||
}
|
||||
printSpinner()
|
||||
@@ -3522,7 +3642,7 @@ func (t *Terminal) printInfoImpl() {
|
||||
}
|
||||
|
||||
if t.infoStyle == infoInlineRight {
|
||||
if t.separatorLen > 0 {
|
||||
if separatorLen > 0 {
|
||||
if !move(line+1, 0, false) {
|
||||
return
|
||||
}
|
||||
@@ -8157,6 +8277,8 @@ func (t *Terminal) Loop() error {
|
||||
case actChangePreviewWindow:
|
||||
// NOTE: We intentionally use "previewOpts" instead of "activePreviewOpts" here
|
||||
currentPreviewOpts := t.previewOpts
|
||||
wasNoSeparatorLine := t.noSeparatorLine()
|
||||
wasSeparatorLength := t.separatorLength()
|
||||
|
||||
// Reset preview options and apply the additional options
|
||||
t.previewOpts = t.initialPreviewOpts
|
||||
@@ -8177,8 +8299,28 @@ func (t *Terminal) Loop() error {
|
||||
a.a = strings.Join(append(tokens[1:], tokens[0]), "|")
|
||||
}
|
||||
|
||||
// Do not drop a preview window forced by the preview(...) action
|
||||
keepForcedPreview := t.hasPreviewWindow() && !t.activePreviewOpts.hidden
|
||||
|
||||
// The default separator is decided from the whole threshold
|
||||
// chain (separatedByBorder), which compare does not fully
|
||||
// inspect, so it can change even when compare finds no
|
||||
// layout difference
|
||||
checkSeparator := func() {
|
||||
if t.noSeparatorLine() != wasNoSeparatorLine {
|
||||
// Number of lines changed; relayout
|
||||
updatePreviewWindow(keepForcedPreview)
|
||||
req(reqPreviewRefresh)
|
||||
} else if t.separatorLength() != wasSeparatorLength {
|
||||
// Only the content of the info line changed
|
||||
req(reqInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// Full redraw
|
||||
switch currentPreviewOpts.compare(t.activePreviewOpts, &t.previewOpts) {
|
||||
case previewOptsSame:
|
||||
checkSeparator()
|
||||
case previewOptsDifferentLayout:
|
||||
// Preview command can be running in the background if the size of
|
||||
// the preview window is 0 but not 'hidden'
|
||||
@@ -8186,7 +8328,7 @@ func (t *Terminal) Loop() error {
|
||||
|
||||
// FIXME: One-time preview window can't reappear once hidden
|
||||
// fzf --bind space:preview:ls --bind 'enter:change-preview-window:down|left|up|hidden|'
|
||||
updatePreviewWindow(t.hasPreviewWindow() && !t.activePreviewOpts.hidden)
|
||||
updatePreviewWindow(keepForcedPreview)
|
||||
if wasHidden && t.hasPreviewWindow() {
|
||||
// Restart
|
||||
refreshPreview(t.previewOpts.command)
|
||||
@@ -8200,6 +8342,7 @@ func (t *Terminal) Loop() error {
|
||||
case previewOptsDifferentContentLayout:
|
||||
t.previewed.version = 0
|
||||
req(reqPreviewRefresh)
|
||||
checkSeparator()
|
||||
}
|
||||
|
||||
// Adjust scroll offset
|
||||
|
||||
+10
@@ -13,6 +13,16 @@ import (
|
||||
// Returns the size of the current window if the tmux server supports
|
||||
// floating panes (tmux 3.7 or above)
|
||||
func tmuxFloatingPaneInfo() (int, int, bool) {
|
||||
// TMUX_PANE is not set when fzf is not started from within a pane
|
||||
// (e.g. 'bind-key 0 run-shell "fzf --popup"'). Do not use a floating
|
||||
// pane there; a blocking run-shell suspends key processing for the
|
||||
// client until the command exits, so fzf in a floating pane would
|
||||
// never receive a key and the client would appear frozen until the
|
||||
// process is killed externally. A popup is unaffected; its input is
|
||||
// handled at the client overlay level, bypassing the suspended path.
|
||||
// The right approach for such bindings is 'run-shell -b', which does
|
||||
// not suspend key processing, as discussed in:
|
||||
// https://github.com/tmux/tmux/issues/5384
|
||||
target := os.Getenv("TMUX_PANE")
|
||||
if target == "" {
|
||||
return 0, 0, false
|
||||
|
||||
+57
-33
@@ -39,6 +39,7 @@ func (r *LightRenderer) Bell() {
|
||||
|
||||
func (r *LightRenderer) PassThrough(str string) {
|
||||
r.queued.WriteString("\x1b7" + str + "\x1b8")
|
||||
r.invalidateSGR()
|
||||
}
|
||||
|
||||
func (r *LightRenderer) stderr(str string) {
|
||||
@@ -90,14 +91,36 @@ func (r *LightRenderer) csi(code string) string {
|
||||
return fullcode
|
||||
}
|
||||
|
||||
// setSGR emits the given SGR sequence only when the terminal is not already
|
||||
// in that state. Sequences built by csiColor start with a reset parameter,
|
||||
// so each of them fully determines the state on its own. The zero value of
|
||||
// r.sgr never matches a sequence, so the first update after Init, Resume, or
|
||||
// invalidateSGR is always emitted.
|
||||
func (r *LightRenderer) setSGR(code string) {
|
||||
if code != r.sgr {
|
||||
r.stderr(code)
|
||||
r.sgr = code
|
||||
}
|
||||
}
|
||||
|
||||
// invalidateSGR marks the terminal SGR state as unknown, e.g. after raw
|
||||
// output that may have changed it behind the renderer's back.
|
||||
func (r *LightRenderer) invalidateSGR() {
|
||||
r.sgr = ""
|
||||
}
|
||||
|
||||
func (r *LightRenderer) flush() {
|
||||
if r.queued.Len() > 0 {
|
||||
raw := "\x1b[?7l\x1b[?25l" + r.queued.String()
|
||||
// Leave the terminal in the default SGR state between frames
|
||||
r.setSGR("\x1b[0m")
|
||||
// Wrap the frame in synchronized update mode (2026) so that the
|
||||
// terminal applies it atomically. Terminals without support ignore
|
||||
// the unknown private mode and behave as before.
|
||||
raw := "\x1b[?2026h\x1b[?7l\x1b[?25l" + r.queued.String()
|
||||
if r.showCursor {
|
||||
raw += "\x1b[?25h\x1b[?7h"
|
||||
} else {
|
||||
raw += "\x1b[?7h"
|
||||
raw += "\x1b[?25h"
|
||||
}
|
||||
raw += "\x1b[?7h\x1b[?2026l"
|
||||
r.flushRaw(raw)
|
||||
r.queued.Reset()
|
||||
}
|
||||
@@ -128,6 +151,7 @@ type LightRenderer struct {
|
||||
fullscreen bool
|
||||
upOneLine bool
|
||||
queued strings.Builder
|
||||
sgr string
|
||||
y int
|
||||
x int
|
||||
maxHeightFunc func(int) int
|
||||
@@ -232,7 +256,7 @@ func (r *LightRenderer) Init() error {
|
||||
r.csi("G")
|
||||
r.csi("K")
|
||||
if !r.clearOnExit && !r.fullscreen {
|
||||
r.csi("s")
|
||||
r.stderr("\x1b7") // DECSC: save cursor position
|
||||
}
|
||||
if !r.fullscreen && r.mouse {
|
||||
r.yoffset, _ = r.findOffset()
|
||||
@@ -250,15 +274,15 @@ func (r *LightRenderer) makeSpace() {
|
||||
}
|
||||
|
||||
func (r *LightRenderer) move(y int, x int) {
|
||||
// w.csi("u")
|
||||
if r.y < y {
|
||||
r.csi(fmt.Sprintf("%dB", y-r.y))
|
||||
} else if r.y > y {
|
||||
r.csi(fmt.Sprintf("%dA", r.y-y))
|
||||
}
|
||||
r.stderr("\r")
|
||||
if x > 0 {
|
||||
r.csi(fmt.Sprintf("%dC", x))
|
||||
if x == 0 {
|
||||
r.stderr("\r")
|
||||
} else {
|
||||
r.csi(fmt.Sprintf("%dG", x+1))
|
||||
}
|
||||
r.y = y
|
||||
r.x = x
|
||||
@@ -1000,6 +1024,9 @@ func (r *LightRenderer) disableModes() {
|
||||
|
||||
func (r *LightRenderer) Resume(clear bool, sigcont bool) {
|
||||
r.setupTerminal()
|
||||
// The programs that ran in the meantime may have left the terminal in an
|
||||
// arbitrary SGR state
|
||||
r.invalidateSGR()
|
||||
if clear {
|
||||
if r.fullscreen {
|
||||
r.smcup()
|
||||
@@ -1021,7 +1048,6 @@ func (r *LightRenderer) Clear() {
|
||||
if r.fullscreen {
|
||||
r.csi("H")
|
||||
}
|
||||
// r.csi("u")
|
||||
r.origin()
|
||||
r.csi("J")
|
||||
r.flush()
|
||||
@@ -1044,7 +1070,6 @@ func (r *LightRenderer) Refresh() {
|
||||
}
|
||||
|
||||
func (r *LightRenderer) Close() {
|
||||
// r.csi("u")
|
||||
if r.clearOnExit {
|
||||
if r.fullscreen {
|
||||
r.rmcup()
|
||||
@@ -1056,7 +1081,7 @@ func (r *LightRenderer) Close() {
|
||||
r.csi("J")
|
||||
}
|
||||
} else if !r.fullscreen {
|
||||
r.csi("u")
|
||||
r.stderr("\x1b8") // DECRC: restore cursor position
|
||||
}
|
||||
if !r.showCursor {
|
||||
r.csi("?25h")
|
||||
@@ -1273,10 +1298,6 @@ func (w *LightWindow) drawBorder(onlyHorizontal bool) {
|
||||
}
|
||||
}
|
||||
|
||||
func (w *LightWindow) csi(code string) string {
|
||||
return w.renderer.csi(code)
|
||||
}
|
||||
|
||||
func (w *LightWindow) stderrInternal(str string, allowNLCR bool, resetCode string) {
|
||||
w.renderer.stderrInternal(str, allowNLCR, resetCode)
|
||||
}
|
||||
@@ -1415,13 +1436,18 @@ func ulColorCode(c Color) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// csiColor builds the SGR sequence for the given colors and attributes
|
||||
// without emitting it. The sequence starts with a reset parameter, so it
|
||||
// fully determines the SGR state on its own.
|
||||
func (w *LightWindow) csiColor(fg Color, bg Color, ul Color, attr Attr) (bool, string) {
|
||||
codes := append(attrCodes(attr), colorCodes(fg, bg)...)
|
||||
if ulCode := ulColorCode(ul); ulCode != "" {
|
||||
codes = append(codes, ulCode)
|
||||
}
|
||||
code := w.csi(";" + strings.Join(codes, ";") + "m")
|
||||
return len(codes) > 0, code
|
||||
if len(codes) == 0 {
|
||||
return false, "\x1b[0m"
|
||||
}
|
||||
return true, "\x1b[;" + strings.Join(codes, ";") + "m"
|
||||
}
|
||||
|
||||
func (w *LightWindow) Print(text string) {
|
||||
@@ -1434,15 +1460,13 @@ func cleanse(str string) string {
|
||||
|
||||
func (w *LightWindow) CPrint(pair ColorPair, text string) {
|
||||
_, code := w.csiColor(pair.Fg(), pair.Bg(), pair.Ul(), pair.Attr())
|
||||
w.renderer.setSGR(code)
|
||||
w.stderrInternal(cleanse(text), false, code)
|
||||
w.csi("0m")
|
||||
}
|
||||
|
||||
func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) {
|
||||
hasColors, code := w.csiColor(fg, bg, colDefault, attr)
|
||||
if hasColors {
|
||||
defer w.csi("0m")
|
||||
}
|
||||
_, code := w.csiColor(fg, bg, colDefault, attr)
|
||||
w.renderer.setSGR(code)
|
||||
w.stderrInternal(cleanse(text), false, code)
|
||||
}
|
||||
|
||||
@@ -1463,7 +1487,7 @@ func (w *LightWindow) fill(str string, resetCode string) FillReturn {
|
||||
}
|
||||
w.MoveAndClear(w.posy, w.posx)
|
||||
w.Move(w.posy+1, 0)
|
||||
w.renderer.stderr(resetCode)
|
||||
w.renderer.setSGR(resetCode)
|
||||
if len(lines) > 1 {
|
||||
sign := w.wrapSign
|
||||
width := w.wrapSignWidth
|
||||
@@ -1473,7 +1497,8 @@ func (w *LightWindow) fill(str string, resetCode string) FillReturn {
|
||||
width = truncatedWidth
|
||||
}
|
||||
w.stderrInternal(DIM+sign, false, resetCode)
|
||||
w.renderer.stderr(resetCode)
|
||||
w.renderer.invalidateSGR()
|
||||
w.renderer.setSGR(resetCode)
|
||||
w.Move(w.posy, width)
|
||||
}
|
||||
}
|
||||
@@ -1484,20 +1509,19 @@ func (w *LightWindow) fill(str string, resetCode string) FillReturn {
|
||||
return FillSuspend
|
||||
}
|
||||
w.Move(w.posy+1, 0)
|
||||
w.renderer.stderr(resetCode)
|
||||
w.renderer.setSGR(resetCode)
|
||||
return FillNextLine
|
||||
}
|
||||
return FillContinue
|
||||
}
|
||||
|
||||
func (w *LightWindow) setBg() string {
|
||||
if w.bg != colDefault {
|
||||
_, code := w.csiColor(colDefault, w.bg, colDefault, AttrRegular)
|
||||
return code
|
||||
}
|
||||
// Should clear dim attribute after ␍ in the preview window
|
||||
// The plain reset code for the default background is still required to
|
||||
// clear the dim attribute after ␍ in the preview window
|
||||
// e.g. printf "foo\rbar" | fzf --ansi --preview 'printf "foo\rbar"'
|
||||
return "\x1b[m"
|
||||
_, code := w.csiColor(colDefault, w.bg, colDefault, AttrRegular)
|
||||
w.renderer.setSGR(code)
|
||||
return code
|
||||
}
|
||||
|
||||
func (w *LightWindow) LinkBegin(uri string, params string) {
|
||||
@@ -1523,7 +1547,7 @@ func (w *LightWindow) CFill(fg Color, bg Color, ul Color, attr Attr, text string
|
||||
bg = w.bg
|
||||
}
|
||||
if hasColors, resetCode := w.csiColor(fg, bg, ul, attr); hasColors {
|
||||
defer w.csi("0m")
|
||||
w.renderer.setSGR(resetCode)
|
||||
return w.fill(text, resetCode)
|
||||
}
|
||||
return w.fill(text, w.setBg())
|
||||
|
||||
@@ -419,3 +419,61 @@ func TestLightRendererScrollWheel(t *testing.T) {
|
||||
assertScroll("\x1b[<94;1;1M", 0, "ctrl-alt-shift") // ctrl+alt+shift + left (ignored)
|
||||
assertScroll("\x1b[<95;1;1M", 0, "ctrl-alt-shift") // ctrl+alt+shift + right (ignored)
|
||||
}
|
||||
|
||||
// Assert the exact byte stream emitted for a scripted rendering sequence,
|
||||
// locking down the SGR deduplication behavior of the light renderer.
|
||||
func TestLightRendererSGRDeduplication(t *testing.T) {
|
||||
renderer, _ := NewLightRenderer(
|
||||
"", nil, &ColorTheme{}, true, false, 8, false, true,
|
||||
func(h int) int { return h })
|
||||
r := renderer.(*LightRenderer)
|
||||
r.width = 80
|
||||
r.height = 24
|
||||
|
||||
out, err := os.CreateTemp(t.TempDir(), "fzf-ttyout")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.ttyout = out
|
||||
|
||||
w := r.NewWindow(0, 0, 40, 4, WindowList, MakeBorderStyle(BorderNone, true), false).(*LightWindow)
|
||||
w.fg = colDefault
|
||||
w.bg = colDefault
|
||||
r.queued.Reset()
|
||||
|
||||
red := NewColorPair(196, colDefault, AttrRegular)
|
||||
blue := NewColorPair(21, colDefault, AttrRegular)
|
||||
|
||||
w.Move(0, 0)
|
||||
w.CPrint(red, "ab")
|
||||
w.CPrint(red, "cd") // same pair; no SGR expected
|
||||
w.CPrint(blue, "ef")
|
||||
w.Print("gh") // default colors; one reset expected
|
||||
w.Print("ij") // still default; no SGR expected
|
||||
w.Move(1, 2)
|
||||
w.CPrint(red, "kl")
|
||||
r.invalidateSGR()
|
||||
w.CPrint(red, "mn") // re-emitted after invalidation
|
||||
r.flush() // adds a trailing reset to leave the default state
|
||||
|
||||
expected := "\x1b[?2026h\x1b[?7l\x1b[?25l" +
|
||||
"\r" +
|
||||
"\x1b[;38;5;196mab" +
|
||||
"cd" +
|
||||
"\x1b[;38;5;21mef" +
|
||||
"\x1b[0mgh" +
|
||||
"ij" +
|
||||
"\x1b[1B\x1b[3G" +
|
||||
"\x1b[;38;5;196mkl" +
|
||||
"\x1b[;38;5;196mmn" +
|
||||
"\x1b[0m" +
|
||||
"\x1b[?25h\x1b[?7h\x1b[?2026l"
|
||||
|
||||
bytes, err := os.ReadFile(out.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(bytes) != expected {
|
||||
t.Errorf("expected: %q\n got: %q", expected, string(bytes))
|
||||
}
|
||||
}
|
||||
|
||||
+290
-21
@@ -248,7 +248,7 @@ class TestLayout < TestInteractive
|
||||
tmux.send_keys %(seq 5 | #{FZF} --layout=reverse --preview 'echo PREVIEW' --preview-window=next:3 --prompt='line2$ > '), :Enter
|
||||
expected = <<~OUTPUT
|
||||
line2$ >
|
||||
5/5 ───
|
||||
5/5
|
||||
╭────────
|
||||
│ PREVIEW
|
||||
│
|
||||
@@ -268,7 +268,7 @@ class TestLayout < TestInteractive
|
||||
│
|
||||
│
|
||||
╰────────
|
||||
5/5 ───
|
||||
5/5
|
||||
>
|
||||
OUTPUT
|
||||
tmux.until { assert_block(expected, it) }
|
||||
@@ -597,7 +597,7 @@ class TestLayout < TestInteractive
|
||||
│ │ 2
|
||||
│ │ 1
|
||||
│ └header──
|
||||
│ 19/97 ─
|
||||
│ 19/97
|
||||
│ > 1
|
||||
│
|
||||
╰────────────
|
||||
@@ -628,7 +628,7 @@ class TestLayout < TestInteractive
|
||||
╭────────────
|
||||
│ hello
|
||||
╰────────────
|
||||
100/100 ─
|
||||
100/100
|
||||
>
|
||||
BLOCK
|
||||
tmux.until { assert_block(block2, it) }
|
||||
@@ -648,7 +648,7 @@ class TestLayout < TestInteractive
|
||||
│ 2
|
||||
│ 1
|
||||
╰──────────
|
||||
98/98 ─
|
||||
98/98
|
||||
>
|
||||
BLOCK
|
||||
tmux.until { assert_block(block1, it) }
|
||||
@@ -663,7 +663,7 @@ class TestLayout < TestInteractive
|
||||
│ 1
|
||||
│ hello
|
||||
╰──────────
|
||||
98/98 ─
|
||||
98/98
|
||||
>
|
||||
BLOCK
|
||||
tmux.until { assert_block(block2, it) }
|
||||
@@ -679,7 +679,7 @@ class TestLayout < TestInteractive
|
||||
│ 4
|
||||
│ > 3
|
||||
╰──────────
|
||||
98/98 ─
|
||||
98/98
|
||||
>
|
||||
╭──────────
|
||||
│ 2
|
||||
@@ -718,7 +718,7 @@ class TestLayout < TestInteractive
|
||||
║ 2
|
||||
║ 1
|
||||
╚══════════
|
||||
98/98 ─
|
||||
98/98
|
||||
>
|
||||
BLOCK
|
||||
tmux.until { assert_block(block1, it) }
|
||||
@@ -734,7 +734,7 @@ class TestLayout < TestInteractive
|
||||
╭──────────
|
||||
│ hello
|
||||
╰──────────
|
||||
98/98 ─
|
||||
98/98
|
||||
>
|
||||
BLOCK
|
||||
tmux.until { assert_block(block2, it) }
|
||||
@@ -750,7 +750,7 @@ class TestLayout < TestInteractive
|
||||
│ 4
|
||||
│ > 3
|
||||
╰──────────
|
||||
98/98 ─
|
||||
98/98
|
||||
>
|
||||
╔══════════
|
||||
║ 2
|
||||
@@ -767,7 +767,7 @@ class TestLayout < TestInteractive
|
||||
║ 2
|
||||
║ 1
|
||||
╚══════════
|
||||
98/98 ─
|
||||
98/98
|
||||
>
|
||||
╭──────────
|
||||
│ hello
|
||||
@@ -810,7 +810,7 @@ class TestLayout < TestInteractive
|
||||
│ │ 2
|
||||
│ │ 1
|
||||
│ └header────
|
||||
│ 19/97 ─
|
||||
│ 19/97
|
||||
│ > 1
|
||||
│
|
||||
╰──────────────
|
||||
@@ -825,7 +825,7 @@ class TestLayout < TestInteractive
|
||||
│ ║ 11
|
||||
│ ║ > 10
|
||||
│ ╚list══════
|
||||
│ 19/97 ─
|
||||
│ 19/97
|
||||
│ > 1
|
||||
│ ┌──────────
|
||||
│ │ 3
|
||||
@@ -1039,9 +1039,9 @@ class TestLayout < TestInteractive
|
||||
└──────── │ 103 │ 101 │ 103 │ 101 │ │ 102 │ ┌────── │ │ 102 │ ┌────── │ 2/2 ─ │ ┌─────── │ ┌───── │ 103 │ ┌─────── │ ┌───────
|
||||
│ 5/5 ─ │ 102 │ 2/2 ─ │ 102 │ │ 103 │ │ 101 │ │ 103 │ │ 101 │ > │ │ 2/2 │ │ 101 │ ┌─────── │ │ 101 │ │ >
|
||||
│ > │ 103 │ > │ 103 │ └────── │ │ 102 │ └────── │ │ 102 │ ┌────── │ │ > │ │ 102 │ │ 5/5 │ │ 102 │ └───────
|
||||
└──────── └──────── └──────── └──────── │ 5/5 ─ │ │ 103 │ 2/2 ─ │ │ 103 │ │ 101 │ └─────── │ │ 103 │ │ > │ │ 103 │ ┌───────
|
||||
└──────── └──────── └──────── └──────── │ 5/5 │ │ 103 │ 2/2 │ │ 103 │ │ 101 │ └─────── │ │ 103 │ │ > │ │ 103 │ ┌───────
|
||||
│ > │ └────── │ > │ └────── │ │ 102 │ ┌─────── │ └───── │ └─────── │ └─────── │ │ 101
|
||||
└──────── └──────── └──────── │ 2/2 ─ │ │ 103 │ │ 101 └─────── └───────── │ ┌─────── │ │ 102
|
||||
└──────── └──────── └──────── │ 2/2 │ │ 103 │ │ 101 └─────── └───────── │ ┌─────── │ │ 102
|
||||
│ > │ └────── │ │ 102 │ │ > │ │ 103
|
||||
└──────── └──────── │ │ 103 │ └─────── │ └───────
|
||||
│ └─────── └───────── └─────────
|
||||
@@ -1099,9 +1099,9 @@ class TestLayout < TestInteractive
|
||||
│ > │ 102 │ > │ 102 │ > │ │ 101 │ > │ │ 101 │ │ 101 │ │ 101 │ └────── │ └─────── │ └────── │ 101 │ │ > 1
|
||||
└──────── │ 3/3 ─ │ 101 │ 1/1 ─ │ 101 │ │ 102 │ ┌────── │ │ 102 │ │ 102 │ │ 102 │ ┌────── │ ┌─────── │ ┌────── │ 102 │ └───────
|
||||
│ > │ 102 │ > │ 102 │ └─HEAD─ │ │ 101 │ └─HEAD─ │ └─HEAD─ │ └─HEAD─ │ │ 101 │ │ 1/1 │ │ 101 │ ─────── │ ┌───────
|
||||
└──────── └──────── └──────── └──────── │ 3/3 ─ │ │ 102 │ 1/1 ─ │ 1/1 ─ │ 1/1 ─ │ │ 102 │ │ > │ │ 102 │ 3/3 │ │ >
|
||||
└──────── └──────── └──────── └──────── │ 3/3 │ │ 102 │ 1/1 │ 1/1 │ 1/1 │ │ 102 │ │ > │ │ 102 │ 3/3 │ │ >
|
||||
│ > │ └─HEAD─ │ > │ > │ > │ └─HEAD─ │ └─────── │ └─HEAD─ │ > │ └───────
|
||||
└──────── └──────── └──────── └──────── └──────── │ 1/1 ─ │ ┌─────── └──────── └──────── │ ┌───────
|
||||
└──────── └──────── └──────── └──────── └──────── │ 1/1 │ ┌─────── └──────── └──────── │ ┌───────
|
||||
│ > │ │ 101 │ │ 101
|
||||
└──────── │ │ 102 │ │ 102
|
||||
│ └─HEAD── │ └─HEAD──
|
||||
@@ -1159,9 +1159,9 @@ class TestLayout < TestInteractive
|
||||
│ > │ 102 │ > │ 102 │ > │ │ 101 │ > │ │ 101 │ > 3 │ > 3 │ > 3 │ 101 │ │ 3
|
||||
└──────── │ 3/3 ─ │ 101 │ 1/1 ─ │ 101 │ │ 102 │ ┌────── │ │ 102 │ ┌────── │ ┌─────── │ ┌────── │ 102 │ └───────
|
||||
│ > │ 102 │ > │ 102 │ └─HEAD─ │ │ 101 │ └─HEAD─ │ │ 101 │ │ 1/1 │ │ 101 │ ─────── │ ┌───────
|
||||
└──────── └──────── └──────── └──────── │ 3/3 ─ │ │ 102 │ 1/1 ─ │ │ 102 │ │ > │ │ 102 │ 3/3 │ │ >
|
||||
└──────── └──────── └──────── └──────── │ 3/3 │ │ 102 │ 1/1 │ │ 102 │ │ > │ │ 102 │ 3/3 │ │ >
|
||||
│ > │ └─HEAD─ │ > │ └─HEAD─ │ └─────── │ └─HEAD─ │ > │ └───────
|
||||
└──────── └──────── └──────── │ 1/1 ─ │ ┌─────── └──────── └──────── │ ┌───────
|
||||
└──────── └──────── └──────── │ 1/1 │ ┌─────── └──────── └──────── │ ┌───────
|
||||
│ > │ │ 101 │ │ 101
|
||||
└──────── │ │ 102 │ │ 102
|
||||
│ └─HEAD── │ └─HEAD──
|
||||
@@ -1191,7 +1191,7 @@ class TestLayout < TestInteractive
|
||||
│ ┌─label──
|
||||
│ │ header
|
||||
│ └────────
|
||||
│ 10/10 ─
|
||||
│ 10/10
|
||||
│ >
|
||||
└──────────
|
||||
BLOCK
|
||||
@@ -1228,13 +1228,282 @@ class TestLayout < TestInteractive
|
||||
end
|
||||
end
|
||||
|
||||
def test_separator_with_input_border
|
||||
# Border line below input does not face the list; separator is shown
|
||||
tmux.send_keys %(seq 100 | #{FZF} --input-border bottom), :Enter
|
||||
block = <<~BLOCK
|
||||
> 1
|
||||
100/100 ──
|
||||
>
|
||||
────────────
|
||||
BLOCK
|
||||
tmux.until { assert_block(block, it) }
|
||||
teardown
|
||||
setup
|
||||
|
||||
# List border faces the input instead; separator is hidden
|
||||
tmux.send_keys %(seq 100 | #{FZF} --input-border bottom --list-border rounded), :Enter
|
||||
block = <<~BLOCK
|
||||
│ > 1
|
||||
╰────────────
|
||||
100/100
|
||||
>
|
||||
─────────────
|
||||
BLOCK
|
||||
tmux.until { assert_block(block, it) }
|
||||
teardown
|
||||
setup
|
||||
|
||||
# Border line above input faces the list; separator is hidden
|
||||
tmux.send_keys %(seq 100 | #{FZF} --input-border top), :Enter
|
||||
block = <<~BLOCK
|
||||
> 1
|
||||
────────────
|
||||
100/100
|
||||
>
|
||||
BLOCK
|
||||
tmux.until { assert_block(block, it) }
|
||||
end
|
||||
|
||||
def test_separator_with_input_border_reverse
|
||||
# Border line above input does not face the list; separator is shown
|
||||
tmux.send_keys %(seq 100 | #{FZF} --layout reverse --input-border top), :Enter
|
||||
block = <<~BLOCK
|
||||
────────────
|
||||
>
|
||||
100/100 ──
|
||||
> 1
|
||||
BLOCK
|
||||
tmux.until { assert_block(block, it) }
|
||||
teardown
|
||||
setup
|
||||
|
||||
# Border line below input faces the list; separator is hidden
|
||||
tmux.send_keys %(seq 100 | #{FZF} --layout reverse --input-border bottom), :Enter
|
||||
block = <<~BLOCK
|
||||
>
|
||||
100/100
|
||||
────────────
|
||||
> 1
|
||||
BLOCK
|
||||
tmux.until { assert_block(block, it) }
|
||||
end
|
||||
|
||||
def test_separator_with_preview_next
|
||||
# Preview window at 'next' position sits next to the input section, so the
|
||||
# header border does not hide the separator
|
||||
tmux.send_keys %(seq 100 | #{FZF} --header foo --header-border rounded --no-list-border --preview 'echo hi' --preview-window next,1,border-none), :Enter
|
||||
block = <<~BLOCK
|
||||
╰────────────
|
||||
hi
|
||||
100/100 ───
|
||||
>
|
||||
BLOCK
|
||||
tmux.until { assert_block(block, it) }
|
||||
teardown
|
||||
setup
|
||||
|
||||
# Same when 'next' position comes from a threshold alternative
|
||||
tmux.send_keys %(seq 100 | #{FZF} --header foo --header-border rounded --no-list-border --preview 'echo hi' --preview-window 'up,40%,<9999(next,1,border-none)'), :Enter
|
||||
tmux.until { assert_block(block, it) }
|
||||
teardown
|
||||
setup
|
||||
|
||||
# A border of the preview window at 'next' position facing the input
|
||||
# section hides the separator
|
||||
tmux.send_keys %(seq 100 | #{FZF} --preview : --preview-window next,3), :Enter
|
||||
block = <<~BLOCK
|
||||
╰────────────
|
||||
100/100
|
||||
>
|
||||
BLOCK
|
||||
tmux.until { assert_block(block, it) }
|
||||
teardown
|
||||
setup
|
||||
|
||||
# Same when every spec in the threshold chain is a bordered preview
|
||||
# window at 'next' position
|
||||
tmux.send_keys %(seq 100 | #{FZF} --preview : --preview-window 'next,3,<15(next,3)'), :Enter
|
||||
tmux.until { assert_block(block, it) }
|
||||
end
|
||||
|
||||
def test_separator_with_preview_next_toggle
|
||||
tmux.send_keys %(seq 100 | #{FZF} --preview 'echo hi' --preview-window next,3,hidden --bind space:toggle-preview), :Enter
|
||||
hidden = <<~BLOCK
|
||||
> 1
|
||||
100/100 ──
|
||||
>
|
||||
BLOCK
|
||||
tmux.until { assert_block(hidden, it) }
|
||||
|
||||
tmux.send_keys :Space
|
||||
shown = <<~BLOCK
|
||||
│ hi
|
||||
│
|
||||
│
|
||||
╰────────────
|
||||
100/100
|
||||
>
|
||||
BLOCK
|
||||
tmux.until { assert_block(shown, it) }
|
||||
|
||||
tmux.send_keys :Space
|
||||
tmux.until { assert_block(hidden, it) }
|
||||
end
|
||||
|
||||
def test_separator_with_preview_next_alternative_only
|
||||
# 'next' appears only in a threshold alternative and there is no
|
||||
# previewer, so the input section stays embedded in the list window and
|
||||
# needs the separator
|
||||
tmux.send_keys %(seq 100 | #{FZF} --preview-window '<9999(next)' --list-border rounded), :Enter
|
||||
block = <<~BLOCK
|
||||
│ > 1
|
||||
│ 100/100 ──
|
||||
│ >
|
||||
╰─────────────
|
||||
BLOCK
|
||||
tmux.until { assert_block(block, it) }
|
||||
end
|
||||
|
||||
def test_separator_with_preview_action_at_next
|
||||
# preview(...) action can display an ad-hoc preview window at 'next'
|
||||
# position at any time; the list border must not hide the separator
|
||||
tmux.send_keys %(seq 100 | #{FZF} --list-border rounded --preview-window 'next,border-left,5' --bind 'space:preview(echo hi)'), :Enter
|
||||
block = <<~BLOCK
|
||||
╰─────────────
|
||||
100/100 ──
|
||||
>
|
||||
BLOCK
|
||||
tmux.until { assert_block(block, it) }
|
||||
|
||||
tmux.send_keys :Space
|
||||
block = <<~BLOCK
|
||||
│ hi
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
100/100 ──
|
||||
>
|
||||
BLOCK
|
||||
tmux.until { assert_block(block, it) }
|
||||
end
|
||||
|
||||
def test_separator_change_preview_window_keeps_forced_preview
|
||||
# Relayout triggered by the separator visibility change must not drop a
|
||||
# preview window opened via the preview(...) action
|
||||
tmux.send_keys %(seq 100 | #{FZF} --info hidden --input-border left --list-border --preview-window 'up,50%,<5(next)' --bind 'space:preview(echo AD-HOC)' --bind 'x:change-preview-window(up,50%,<5(up))'), :Enter
|
||||
tmux.until { |lines| assert_includes lines.join, '│ ─' }
|
||||
|
||||
tmux.send_keys :Space
|
||||
tmux.until do |lines|
|
||||
assert_includes lines.join, 'AD-HOC'
|
||||
assert_includes lines.join, '│ ─'
|
||||
end
|
||||
|
||||
# Only the inactive threshold alternative changes; separator is hidden
|
||||
# by the list border, and the forced preview window must survive
|
||||
tmux.send_keys :x
|
||||
tmux.until do |lines|
|
||||
assert_includes lines.join, 'AD-HOC'
|
||||
refute_includes lines.join, '│ ─'
|
||||
end
|
||||
end
|
||||
|
||||
def test_separator_with_change_preview_window_chain
|
||||
# change-preview-window that only alters a non-active spec must still
|
||||
# trigger a relayout when it changes the separator visibility
|
||||
tmux.send_keys %(seq 100 | #{FZF} --info hidden --layout reverse --preview 'echo hi' --preview-window 'next,3,border-none,<100(next,3,border-top)' --bind 'space:change-preview-window(next,3,border-top,<100(next,3,border-top))'), :Enter
|
||||
block = <<~BLOCK
|
||||
>
|
||||
──────────
|
||||
──────────
|
||||
hi
|
||||
BLOCK
|
||||
tmux.until { assert_block(block, it) }
|
||||
|
||||
tmux.send_keys :Space
|
||||
block = <<~BLOCK
|
||||
>
|
||||
──────────
|
||||
hi
|
||||
BLOCK
|
||||
tmux.until { assert_block(block, it) }
|
||||
teardown
|
||||
setup
|
||||
|
||||
# Also when only the drawn bar changes; the info line row count is
|
||||
# constant with the default info style
|
||||
tmux.send_keys %(seq 100 | #{FZF} --layout reverse --preview 'echo hi' --preview-window 'next,3,border-none,<100(next,3,border-top)' --bind 'space:change-preview-window(next,3,border-top,<100(next,3,border-top))'), :Enter
|
||||
block = <<~BLOCK
|
||||
>
|
||||
100/100 ──
|
||||
BLOCK
|
||||
tmux.until { assert_block(block, it) }
|
||||
|
||||
tmux.send_keys :Space
|
||||
block = <<~BLOCK
|
||||
>
|
||||
100/100
|
||||
────────────
|
||||
BLOCK
|
||||
tmux.until { assert_block(block, it) }
|
||||
teardown
|
||||
setup
|
||||
|
||||
# Also when compare finds a content-layout difference (scroll offset)
|
||||
tmux.send_keys %(seq 100 | #{FZF} --info hidden --preview 'echo hi' --preview-window 'next,3,<2(up,3)' --bind 'space:change-preview-window(next,3,+1,<2(next,3))'), :Enter
|
||||
block = <<~BLOCK
|
||||
╰────────────
|
||||
─────────────
|
||||
>
|
||||
BLOCK
|
||||
tmux.until { assert_block(block, it) }
|
||||
|
||||
tmux.send_keys :Space
|
||||
block = <<~BLOCK
|
||||
╰────────────
|
||||
>
|
||||
BLOCK
|
||||
tmux.until { assert_block(block, it) }
|
||||
end
|
||||
|
||||
def test_separator_with_inline_header_border
|
||||
# Inline header is drawn inside the list border; bare header lines sit
|
||||
# next to the input section, so the separator is shown
|
||||
tmux.send_keys %(seq 100 | #{FZF} --list-border rounded --header foo --header-lines 2 --header-border inline --header-lines-border none), :Enter
|
||||
block = <<~BLOCK
|
||||
╰────────────
|
||||
2
|
||||
1
|
||||
98/98 ───
|
||||
>
|
||||
BLOCK
|
||||
tmux.until { assert_block(block, it) }
|
||||
teardown
|
||||
setup
|
||||
|
||||
# Header lines are embedded in the list border along with the inline
|
||||
# header; the list border faces the input section and hides the separator
|
||||
tmux.send_keys %(seq 100 | #{FZF} --list-border rounded --header foo --header-lines 2 --header-border inline), :Enter
|
||||
block = <<~BLOCK
|
||||
│ 2
|
||||
│ 1
|
||||
│ foo
|
||||
╰────────────
|
||||
98/98
|
||||
>
|
||||
BLOCK
|
||||
tmux.until { assert_block(block, it) }
|
||||
end
|
||||
|
||||
def test_header_border_no_pointer_and_marker
|
||||
tmux.send_keys %(seq 10 | #{FZF} --header-lines 1 --header-border sharp --no-list-border --pointer '' --marker ''), :Enter
|
||||
block = <<~BLOCK
|
||||
┌──────
|
||||
│ 1
|
||||
└──────
|
||||
9/9 ─
|
||||
9/9
|
||||
>
|
||||
BLOCK
|
||||
tmux.until { assert_block(block, it) }
|
||||
|
||||
Reference in New Issue
Block a user