Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] d229762751 Bump ruby/setup-ruby from 1.314.0 to 1.316.0
Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.314.0 to 1.316.0.
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Changelog](https://github.com/ruby/setup-ruby/blob/master/release.rb)
- [Commits](https://github.com/ruby/setup-ruby/compare/9eb537ca036ebaed86729dcb9309076e4c5c3b74...d45b1a4e94b71acab930e56e79c6aa188764e7f9)

---
updated-dependencies:
- dependency-name: ruby/setup-ruby
  dependency-version: 1.316.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-06 13:42:37 +00:00
12 changed files with 80 additions and 636 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
go-version: "1.23" go-version: "1.23"
- name: Setup Ruby - name: Setup Ruby
uses: ruby/setup-ruby@9eb537ca036ebaed86729dcb9309076e4c5c3b74 # v1 uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1
with: with:
ruby-version: 3.4.6 ruby-version: 3.4.6
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
go-version: "1.23" go-version: "1.23"
- name: Setup Ruby - name: Setup Ruby
uses: ruby/setup-ruby@9eb537ca036ebaed86729dcb9309076e4c5c3b74 # v1 uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1
with: with:
ruby-version: 3.0.0 ruby-version: 3.0.0
-19
View File
@@ -96,25 +96,6 @@ archives:
files: files:
- non-existent* - 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: release:
github: github:
owner: junegunn owner: junegunn
-25
View File
@@ -1,31 +1,6 @@
CHANGELOG 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 0.74.0
------ ------
_Release highlights: https://junegunn.github.io/fzf/releases/0.74.0/_ _Release highlights: https://junegunn.github.io/fzf/releases/0.74.0/_
-4
View File
@@ -823,10 +823,6 @@ A synonym for \fB\-\-info=hidden\fB
The given string will be repeated to form the horizontal separator on the info The given string will be repeated to form the horizontal separator on the info
line (default: '─' or '\-' depending on \fB\-\-no\-unicode\fR). 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. ANSI color codes are supported.
.TP .TP
+1 -2
View File
@@ -116,8 +116,7 @@ def __fzf_list_hosts [] {
( (
# Process ssh config files # Process ssh config files
$ssh_configs | append $ssh_configs_d | append $ssh_config_global $ssh_configs | append $ssh_configs_d | append $ssh_config_global
# 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 downcase | str starts-with 'host') or ($it | str downcase | str starts-with 'hostname') }
| 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 | parse --regex '^\s*host(?:name)?\s+(?<hosts>.+)' # Extract hosts after keyword
| default { hosts: null } # Handle lines that don't match regex | default { hosts: null } # Handle lines that don't match regex
| get hosts | get hosts
+1 -4
View File
@@ -3696,10 +3696,7 @@ func (opts *Options) noSeparatorLine() bool {
if opts.Inputless { if opts.Inputless {
return true return true
} }
// NOTE: This does not know that the default separator can be suppressed at sep := opts.Separator == nil && !opts.InputBorderShape.Visible() || opts.Separator != nil && len(*opts.Separator) > 0
// 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) return noSeparatorLine(opts.InfoStyle, sep)
} }
+16 -159
View File
@@ -271,7 +271,6 @@ type Terminal struct {
ghost string ghost string
separator labelPrinter separator labelPrinter
separatorLen int separatorLen int
separatorAuto bool
spinner []string spinner []string
promptString string promptString string
prompt func() prompt func()
@@ -1249,7 +1248,13 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
} }
// Determine input border shape // Determine input border shape
t.inputBorderShape = resolveInputBorderShape(t.layout, t.inputBorderShape) if t.inputBorderShape == tui.BorderLine {
if t.layout == layoutReverse {
t.inputBorderShape = tui.BorderBottom
} else {
t.inputBorderShape = tui.BorderTop
}
}
// Inline borders are embedded between the list's top and bottom horizontals. // 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 // Shapes missing either one (none/phantom/line/single-sided) fall back to a plain
@@ -1294,16 +1299,14 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
} }
} }
// The default separator can be suppressed at runtime depending on the // Disable separator by default if input border is set
// window layout (see separatorLength) if opts.Separator == nil && !t.inputBorderShape.Visible() || opts.Separator != nil && len(*opts.Separator) > 0 {
t.separatorAuto = opts.Separator == nil
bar := "─" bar := "─"
if opts.Separator != nil { if opts.Separator != nil {
bar = *opts.Separator bar = *opts.Separator
} else if !t.unicode { } else if !t.unicode {
bar = "-" bar = "-"
} }
if len(bar) > 0 {
t.separator, t.separatorLen = t.ansiLabelPrinter(bar, &tui.ColSeparator, true) t.separator, t.separatorLen = t.ansiLabelPrinter(bar, &tui.ColSeparator, true)
} }
@@ -1692,129 +1695,7 @@ func (t *Terminal) parsePrompt(prompt string) (func(), int) {
} }
func (t *Terminal) noSeparatorLine() bool { func (t *Terminal) noSeparatorLine() bool {
return t.inputless || noSeparatorLine(t.infoStyle, t.separatorLength() > 0) return t.inputless || noSeparatorLine(t.infoStyle, t.separatorLen > 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) { func getScrollbar(perLine int, total int, height int, offset int) (int, int) {
@@ -3449,7 +3330,6 @@ func (t *Terminal) printInfoImpl() {
} }
pos := 0 pos := 0
line := 0 line := 0
separatorLen := t.separatorLength()
maxHeight := t.window.Height() maxHeight := t.window.Height()
move := func(y int, x int, clear bool) bool { move := func(y int, x int, clear bool) bool {
if y < 0 || y >= maxHeight { if y < 0 || y >= maxHeight {
@@ -3477,7 +3357,7 @@ func (t *Terminal) printInfoImpl() {
str = string(trimmed) str = string(trimmed)
width = maxWidth width = maxWidth
} }
move(line, pos, separatorLen == 0) move(line, pos, t.separatorLen == 0)
if t.reading { if t.reading {
t.window.CPrint(tui.ColSpinner, str) t.window.CPrint(tui.ColSpinner, str)
} else { } else {
@@ -3486,7 +3366,7 @@ func (t *Terminal) printInfoImpl() {
pos += width pos += width
} }
printSeparator := func(fillLength int, pad bool) { printSeparator := func(fillLength int, pad bool) {
if separatorLen > 0 { if t.separatorLen > 0 {
t.separator(t.window, fillLength) t.separator(t.window, fillLength)
t.window.Print(" ") t.window.Print(" ")
} else if pad { } else if pad {
@@ -3495,7 +3375,7 @@ func (t *Terminal) printInfoImpl() {
} }
if t.infoStyle == infoHidden { if t.infoStyle == infoHidden {
if separatorLen > 0 { if t.separatorLen > 0 {
if !move(line+1, 0, false) { if !move(line+1, 0, false) {
return return
} }
@@ -3558,7 +3438,7 @@ func (t *Terminal) printInfoImpl() {
} }
switch t.infoStyle { switch t.infoStyle {
case infoDefault: case infoDefault:
if !move(line+1, 0, separatorLen == 0) { if !move(line+1, 0, t.separatorLen == 0) {
return return
} }
printSpinner() printSpinner()
@@ -3642,7 +3522,7 @@ func (t *Terminal) printInfoImpl() {
} }
if t.infoStyle == infoInlineRight { if t.infoStyle == infoInlineRight {
if separatorLen > 0 { if t.separatorLen > 0 {
if !move(line+1, 0, false) { if !move(line+1, 0, false) {
return return
} }
@@ -8277,8 +8157,6 @@ func (t *Terminal) Loop() error {
case actChangePreviewWindow: case actChangePreviewWindow:
// NOTE: We intentionally use "previewOpts" instead of "activePreviewOpts" here // NOTE: We intentionally use "previewOpts" instead of "activePreviewOpts" here
currentPreviewOpts := t.previewOpts currentPreviewOpts := t.previewOpts
wasNoSeparatorLine := t.noSeparatorLine()
wasSeparatorLength := t.separatorLength()
// Reset preview options and apply the additional options // Reset preview options and apply the additional options
t.previewOpts = t.initialPreviewOpts t.previewOpts = t.initialPreviewOpts
@@ -8299,28 +8177,8 @@ func (t *Terminal) Loop() error {
a.a = strings.Join(append(tokens[1:], tokens[0]), "|") 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 // Full redraw
switch currentPreviewOpts.compare(t.activePreviewOpts, &t.previewOpts) { switch currentPreviewOpts.compare(t.activePreviewOpts, &t.previewOpts) {
case previewOptsSame:
checkSeparator()
case previewOptsDifferentLayout: case previewOptsDifferentLayout:
// Preview command can be running in the background if the size of // Preview command can be running in the background if the size of
// the preview window is 0 but not 'hidden' // the preview window is 0 but not 'hidden'
@@ -8328,7 +8186,7 @@ func (t *Terminal) Loop() error {
// FIXME: One-time preview window can't reappear once hidden // FIXME: One-time preview window can't reappear once hidden
// fzf --bind space:preview:ls --bind 'enter:change-preview-window:down|left|up|hidden|' // fzf --bind space:preview:ls --bind 'enter:change-preview-window:down|left|up|hidden|'
updatePreviewWindow(keepForcedPreview) updatePreviewWindow(t.hasPreviewWindow() && !t.activePreviewOpts.hidden)
if wasHidden && t.hasPreviewWindow() { if wasHidden && t.hasPreviewWindow() {
// Restart // Restart
refreshPreview(t.previewOpts.command) refreshPreview(t.previewOpts.command)
@@ -8342,7 +8200,6 @@ func (t *Terminal) Loop() error {
case previewOptsDifferentContentLayout: case previewOptsDifferentContentLayout:
t.previewed.version = 0 t.previewed.version = 0
req(reqPreviewRefresh) req(reqPreviewRefresh)
checkSeparator()
} }
// Adjust scroll offset // Adjust scroll offset
-10
View File
@@ -13,16 +13,6 @@ import (
// Returns the size of the current window if the tmux server supports // Returns the size of the current window if the tmux server supports
// floating panes (tmux 3.7 or above) // floating panes (tmux 3.7 or above)
func tmuxFloatingPaneInfo() (int, int, bool) { 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") target := os.Getenv("TMUX_PANE")
if target == "" { if target == "" {
return 0, 0, false return 0, 0, false
+31 -55
View File
@@ -39,7 +39,6 @@ func (r *LightRenderer) Bell() {
func (r *LightRenderer) PassThrough(str string) { func (r *LightRenderer) PassThrough(str string) {
r.queued.WriteString("\x1b7" + str + "\x1b8") r.queued.WriteString("\x1b7" + str + "\x1b8")
r.invalidateSGR()
} }
func (r *LightRenderer) stderr(str string) { func (r *LightRenderer) stderr(str string) {
@@ -91,36 +90,14 @@ func (r *LightRenderer) csi(code string) string {
return fullcode 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() { func (r *LightRenderer) flush() {
if r.queued.Len() > 0 { if r.queued.Len() > 0 {
// Leave the terminal in the default SGR state between frames raw := "\x1b[?7l\x1b[?25l" + r.queued.String()
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 { if r.showCursor {
raw += "\x1b[?25h" raw += "\x1b[?25h\x1b[?7h"
} else {
raw += "\x1b[?7h"
} }
raw += "\x1b[?7h\x1b[?2026l"
r.flushRaw(raw) r.flushRaw(raw)
r.queued.Reset() r.queued.Reset()
} }
@@ -151,7 +128,6 @@ type LightRenderer struct {
fullscreen bool fullscreen bool
upOneLine bool upOneLine bool
queued strings.Builder queued strings.Builder
sgr string
y int y int
x int x int
maxHeightFunc func(int) int maxHeightFunc func(int) int
@@ -256,7 +232,7 @@ func (r *LightRenderer) Init() error {
r.csi("G") r.csi("G")
r.csi("K") r.csi("K")
if !r.clearOnExit && !r.fullscreen { if !r.clearOnExit && !r.fullscreen {
r.stderr("\x1b7") // DECSC: save cursor position r.csi("s")
} }
if !r.fullscreen && r.mouse { if !r.fullscreen && r.mouse {
r.yoffset, _ = r.findOffset() r.yoffset, _ = r.findOffset()
@@ -274,15 +250,15 @@ func (r *LightRenderer) makeSpace() {
} }
func (r *LightRenderer) move(y int, x int) { func (r *LightRenderer) move(y int, x int) {
// w.csi("u")
if r.y < y { if r.y < y {
r.csi(fmt.Sprintf("%dB", y-r.y)) r.csi(fmt.Sprintf("%dB", y-r.y))
} else if r.y > y { } else if r.y > y {
r.csi(fmt.Sprintf("%dA", r.y-y)) r.csi(fmt.Sprintf("%dA", r.y-y))
} }
if x == 0 {
r.stderr("\r") r.stderr("\r")
} else { if x > 0 {
r.csi(fmt.Sprintf("%dG", x+1)) r.csi(fmt.Sprintf("%dC", x))
} }
r.y = y r.y = y
r.x = x r.x = x
@@ -1024,9 +1000,6 @@ func (r *LightRenderer) disableModes() {
func (r *LightRenderer) Resume(clear bool, sigcont bool) { func (r *LightRenderer) Resume(clear bool, sigcont bool) {
r.setupTerminal() r.setupTerminal()
// The programs that ran in the meantime may have left the terminal in an
// arbitrary SGR state
r.invalidateSGR()
if clear { if clear {
if r.fullscreen { if r.fullscreen {
r.smcup() r.smcup()
@@ -1048,6 +1021,7 @@ func (r *LightRenderer) Clear() {
if r.fullscreen { if r.fullscreen {
r.csi("H") r.csi("H")
} }
// r.csi("u")
r.origin() r.origin()
r.csi("J") r.csi("J")
r.flush() r.flush()
@@ -1070,6 +1044,7 @@ func (r *LightRenderer) Refresh() {
} }
func (r *LightRenderer) Close() { func (r *LightRenderer) Close() {
// r.csi("u")
if r.clearOnExit { if r.clearOnExit {
if r.fullscreen { if r.fullscreen {
r.rmcup() r.rmcup()
@@ -1081,7 +1056,7 @@ func (r *LightRenderer) Close() {
r.csi("J") r.csi("J")
} }
} else if !r.fullscreen { } else if !r.fullscreen {
r.stderr("\x1b8") // DECRC: restore cursor position r.csi("u")
} }
if !r.showCursor { if !r.showCursor {
r.csi("?25h") r.csi("?25h")
@@ -1298,6 +1273,10 @@ 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) { func (w *LightWindow) stderrInternal(str string, allowNLCR bool, resetCode string) {
w.renderer.stderrInternal(str, allowNLCR, resetCode) w.renderer.stderrInternal(str, allowNLCR, resetCode)
} }
@@ -1436,18 +1415,13 @@ func ulColorCode(c Color) string {
return "" 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) { func (w *LightWindow) csiColor(fg Color, bg Color, ul Color, attr Attr) (bool, string) {
codes := append(attrCodes(attr), colorCodes(fg, bg)...) codes := append(attrCodes(attr), colorCodes(fg, bg)...)
if ulCode := ulColorCode(ul); ulCode != "" { if ulCode := ulColorCode(ul); ulCode != "" {
codes = append(codes, ulCode) codes = append(codes, ulCode)
} }
if len(codes) == 0 { code := w.csi(";" + strings.Join(codes, ";") + "m")
return false, "\x1b[0m" return len(codes) > 0, code
}
return true, "\x1b[;" + strings.Join(codes, ";") + "m"
} }
func (w *LightWindow) Print(text string) { func (w *LightWindow) Print(text string) {
@@ -1460,13 +1434,15 @@ func cleanse(str string) string {
func (w *LightWindow) CPrint(pair ColorPair, text string) { func (w *LightWindow) CPrint(pair ColorPair, text string) {
_, code := w.csiColor(pair.Fg(), pair.Bg(), pair.Ul(), pair.Attr()) _, code := w.csiColor(pair.Fg(), pair.Bg(), pair.Ul(), pair.Attr())
w.renderer.setSGR(code)
w.stderrInternal(cleanse(text), false, code) w.stderrInternal(cleanse(text), false, code)
w.csi("0m")
} }
func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) { func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) {
_, code := w.csiColor(fg, bg, colDefault, attr) hasColors, code := w.csiColor(fg, bg, colDefault, attr)
w.renderer.setSGR(code) if hasColors {
defer w.csi("0m")
}
w.stderrInternal(cleanse(text), false, code) w.stderrInternal(cleanse(text), false, code)
} }
@@ -1487,7 +1463,7 @@ func (w *LightWindow) fill(str string, resetCode string) FillReturn {
} }
w.MoveAndClear(w.posy, w.posx) w.MoveAndClear(w.posy, w.posx)
w.Move(w.posy+1, 0) w.Move(w.posy+1, 0)
w.renderer.setSGR(resetCode) w.renderer.stderr(resetCode)
if len(lines) > 1 { if len(lines) > 1 {
sign := w.wrapSign sign := w.wrapSign
width := w.wrapSignWidth width := w.wrapSignWidth
@@ -1497,8 +1473,7 @@ func (w *LightWindow) fill(str string, resetCode string) FillReturn {
width = truncatedWidth width = truncatedWidth
} }
w.stderrInternal(DIM+sign, false, resetCode) w.stderrInternal(DIM+sign, false, resetCode)
w.renderer.invalidateSGR() w.renderer.stderr(resetCode)
w.renderer.setSGR(resetCode)
w.Move(w.posy, width) w.Move(w.posy, width)
} }
} }
@@ -1509,20 +1484,21 @@ func (w *LightWindow) fill(str string, resetCode string) FillReturn {
return FillSuspend return FillSuspend
} }
w.Move(w.posy+1, 0) w.Move(w.posy+1, 0)
w.renderer.setSGR(resetCode) w.renderer.stderr(resetCode)
return FillNextLine return FillNextLine
} }
return FillContinue return FillContinue
} }
func (w *LightWindow) setBg() string { func (w *LightWindow) setBg() string {
// The plain reset code for the default background is still required to if w.bg != colDefault {
// clear the dim attribute after ␍ in the preview window
// e.g. printf "foo\rbar" | fzf --ansi --preview 'printf "foo\rbar"'
_, code := w.csiColor(colDefault, w.bg, colDefault, AttrRegular) _, code := w.csiColor(colDefault, w.bg, colDefault, AttrRegular)
w.renderer.setSGR(code)
return code return code
} }
// Should clear dim attribute after ␍ in the preview window
// e.g. printf "foo\rbar" | fzf --ansi --preview 'printf "foo\rbar"'
return "\x1b[m"
}
func (w *LightWindow) LinkBegin(uri string, params string) { func (w *LightWindow) LinkBegin(uri string, params string) {
w.renderer.queued.WriteString("\x1b]8;" + params + ";" + uri + "\x1b\\") w.renderer.queued.WriteString("\x1b]8;" + params + ";" + uri + "\x1b\\")
@@ -1547,7 +1523,7 @@ func (w *LightWindow) CFill(fg Color, bg Color, ul Color, attr Attr, text string
bg = w.bg bg = w.bg
} }
if hasColors, resetCode := w.csiColor(fg, bg, ul, attr); hasColors { if hasColors, resetCode := w.csiColor(fg, bg, ul, attr); hasColors {
w.renderer.setSGR(resetCode) defer w.csi("0m")
return w.fill(text, resetCode) return w.fill(text, resetCode)
} }
return w.fill(text, w.setBg()) return w.fill(text, w.setBg())
-58
View File
@@ -419,61 +419,3 @@ func TestLightRendererScrollWheel(t *testing.T) {
assertScroll("\x1b[<94;1;1M", 0, "ctrl-alt-shift") // ctrl+alt+shift + left (ignored) 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) 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))
}
}
+21 -290
View File
@@ -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 tmux.send_keys %(seq 5 | #{FZF} --layout=reverse --preview 'echo PREVIEW' --preview-window=next:3 --prompt='line2$ > '), :Enter
expected = <<~OUTPUT expected = <<~OUTPUT
line2$ > line2$ >
5/5 5/5
PREVIEW PREVIEW
@@ -268,7 +268,7 @@ class TestLayout < TestInteractive
5/5 5/5
> >
OUTPUT OUTPUT
tmux.until { assert_block(expected, it) } tmux.until { assert_block(expected, it) }
@@ -597,7 +597,7 @@ class TestLayout < TestInteractive
2 2
1 1
header header
19/97 19/97
> 1 > 1
@@ -628,7 +628,7 @@ class TestLayout < TestInteractive
hello hello
100/100 100/100
> >
BLOCK BLOCK
tmux.until { assert_block(block2, it) } tmux.until { assert_block(block2, it) }
@@ -648,7 +648,7 @@ class TestLayout < TestInteractive
2 2
1 1
98/98 98/98
> >
BLOCK BLOCK
tmux.until { assert_block(block1, it) } tmux.until { assert_block(block1, it) }
@@ -663,7 +663,7 @@ class TestLayout < TestInteractive
1 1
hello hello
98/98 98/98
> >
BLOCK BLOCK
tmux.until { assert_block(block2, it) } tmux.until { assert_block(block2, it) }
@@ -679,7 +679,7 @@ class TestLayout < TestInteractive
4 4
> 3 > 3
98/98 98/98
> >
2 2
@@ -718,7 +718,7 @@ class TestLayout < TestInteractive
2 2
1 1
98/98 98/98
> >
BLOCK BLOCK
tmux.until { assert_block(block1, it) } tmux.until { assert_block(block1, it) }
@@ -734,7 +734,7 @@ class TestLayout < TestInteractive
hello hello
98/98 98/98
> >
BLOCK BLOCK
tmux.until { assert_block(block2, it) } tmux.until { assert_block(block2, it) }
@@ -750,7 +750,7 @@ class TestLayout < TestInteractive
4 4
> 3 > 3
98/98 98/98
> >
2 2
@@ -767,7 +767,7 @@ class TestLayout < TestInteractive
2 2
1 1
98/98 98/98
> >
hello hello
@@ -810,7 +810,7 @@ class TestLayout < TestInteractive
2 2
1 1
header header
19/97 19/97
> 1 > 1
@@ -825,7 +825,7 @@ class TestLayout < TestInteractive
11 11
> 10 > 10
list list
19/97 19/97
> 1 > 1
3 3
@@ -1039,9 +1039,9 @@ class TestLayout < TestInteractive
103 101 103 101 102 102 2/2 103 103 101 103 101 102 102 2/2 103
5/5 102 2/2 102 103 101 103 101 > 2/2 101 101 > 5/5 102 2/2 102 103 101 103 101 > 2/2 101 101 >
> 103 > 103 102 102 > 102 5/5 102 > 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 > > 102 101
2/2 103 101 102 2/2 103 101 102
> 102 > 103 > 102 > 103
103 103
@@ -1099,9 +1099,9 @@ class TestLayout < TestInteractive
> 102 > 102 > 101 > 101 101 101 101 > 1 > 102 > 102 > 101 > 101 101 101 101 > 1
3/3 101 1/1 101 102 102 102 102 102 3/3 101 1/1 101 102 102 102 102 102
> 102 > 102 HEAD 101 HEAD HEAD HEAD 101 1/1 101 > 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 > > HEAD > > > HEAD HEAD >
1/1 1/1
> 101 101 > 101 101
102 102 102 102
HEAD HEAD HEAD HEAD
@@ -1159,9 +1159,9 @@ class TestLayout < TestInteractive
> 102 > 102 > 101 > 101 > 3 > 3 > 3 101 3 > 102 > 102 > 101 > 101 > 3 > 3 > 3 101 3
3/3 101 1/1 101 102 102 102 3/3 101 1/1 101 102 102 102
> 102 > 102 HEAD 101 HEAD 101 1/1 101 > 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 > > HEAD > HEAD HEAD >
1/1 1/1
> 101 101 > 101 101
102 102 102 102
HEAD HEAD HEAD HEAD
@@ -1191,7 +1191,7 @@ class TestLayout < TestInteractive
label label
header header
10/10 10/10
> >
BLOCK BLOCK
@@ -1228,282 +1228,13 @@ class TestLayout < TestInteractive
end end
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 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 tmux.send_keys %(seq 10 | #{FZF} --header-lines 1 --header-border sharp --no-list-border --pointer '' --marker ''), :Enter
block = <<~BLOCK block = <<~BLOCK
1 1
9/9 9/9
> >
BLOCK BLOCK
tmux.until { assert_block(block, it) } tmux.until { assert_block(block, it) }