Compare commits

..

11 Commits

Author SHA1 Message Date
Junegunn Choi f70cc6738d Redraw when change-header changes line count
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
The inline header slot's row budget depends on header content length,
but resizeIfNeeded() tolerates a shorter-than-wanted inline window, so
the stale slot stays. Drive a redraw on length change to re-run the
layout.
2026-04-19 22:40:33 +09:00
Junegunn Choi dacb87abca Let inline sections take precedence over --header-first
--header-first previously was rejected with --header-border=inline or
--header-lines-border=inline. Now, inline placement wins: an inline
section stays inside the list frame, and --header-first only affects
non-inline sections (mainly the main --header).
2026-04-19 22:39:47 +09:00
Junegunn Choi abfa60b7d0 Set inline separator caps per side 2026-04-19 20:47:38 +09:00
Junegunn Choi 12199823ab Update man page: clarify inline list-border shape requirements 2026-04-19 20:47:31 +09:00
Junegunn Choi 2c459ffdff Update CHANGELOG: clarify inline list-border shape requirements 2026-04-19 20:47:25 +09:00
Junegunn Choi f80ba22ab9 Fix misleading comment on printHeader nil-window guard
The guard fires when hasHeaderWindow() returned false at resize time,
not when addInline had no budget (placeInlineStack always leaves a
non-nil 0-height placeholder).
2026-04-19 20:47:17 +09:00
Junegunn Choi 4cd97ba35b Fix inline header/footer border color when falling back to line
InitTheme was called before the runtime coerced BorderInline to
BorderLine, so HeaderBorder / FooterBorder inherited from ListBorder
even when the effective shape was 'line'. Mirror the coercion so
color inheritance matches the rendered shape.
2026-04-19 20:47:09 +09:00
Junegunn Choi 332382e5e7 Add --{header,header-lines,footer}-border=inline
New BorderShape that embeds the section inside the --list-border
frame, joined to the list content by a horizontal separator with
T-junctions where the list shape has side borders. Requires a list
border with both top and bottom segments; falls back to 'line'
otherwise. Stacks when multiple sections are inline.

Sections inherit --color list-border by default and are colored as a
uniform block via their own --color *-border and *-bg.

Incompatible with --header-first. --header-border=inline requires
--header-lines-border to be inline or unset.
2026-04-19 20:47:00 +09:00
Junegunn Choi 987c37cb2d Add a test case for click-header with --header and --header-lines combined
The two sections swap order between layouts, and header-lines are
reversed under layout=default, so expected LINE values differ per
layout.
2026-04-19 20:46:49 +09:00
Junegunn Choi 9deb7c5489 Add test cases for click-header and click-footer
Injects SGR 1006 mouse events via tmux send-keys -l to exercise
FZF_CLICK_HEADER_* / FZF_CLICK_FOOTER_* across all three layouts,
with and without a header border.
2026-04-19 20:46:41 +09:00
Junegunn Choi 5352b88c5a Clean up non-ascii characters 2026-04-19 20:28:28 +09:00
11 changed files with 80 additions and 167 deletions
+4 -14
View File
@@ -3,29 +3,19 @@ CHANGELOG
0.72.0 0.72.0
------ ------
_Release highlights: https://junegunn.github.io/fzf/releases/0.72.0/_
- `--header-border`, `--header-lines-border`, and `--footer-border` now accept a new `inline` style that embeds the section inside the list frame, separated from the list content by a horizontal line. When the list border has side segments, the separator joins them as T-junctions. - `--header-border`, `--header-lines-border`, and `--footer-border` now accept a new `inline` style that embeds the section inside the list frame, separated from the list content by a horizontal line. When the list border has side segments, the separator joins them as T-junctions.
- Requires a `--list-border` shape that has both top and bottom segments (`rounded`, `sharp`, `bold`, `double`, `block`, `thinblock`, or `horizontal`); falls back to `line` otherwise. `horizontal` has no side borders, so the separator is drawn without T-junction endpoints. - Requires a `--list-border` shape that has both top and bottom segments (`rounded`, `sharp`, `bold`, `double`, `block`, `thinblock`, or `horizontal`); falls back to `line` otherwise. `horizontal` has no side borders, so the separator is drawn without T-junction endpoints.
- Sections stack. Example combining all three: - Sections stack. Example combining all three:
```sh ```sh
ps -ef | fzf --reverse --style full \ ps -ef | fzf --reverse --style full:double \
--header 'Select a process' --header-lines 1 \ --header 'Select a process' --header-lines 1 \
--bind 'load:transform-footer:echo $FZF_TOTAL_COUNT processes' \ --bind 'load:transform-footer:echo $FZF_TOTAL_COUNT processes' \
--header-border dashed --header-first \ --header-border=inline --header-lines-border=inline \
--header-lines-border inline --footer-border inline --footer-border=inline
``` ```
- `--header-label` and `--footer-label` render on their respective separator row. - `--header-label` and `--footer-label` render on their respective separator row.
- The separator inherits `--color list-border` when the section's own border color is not explicitly set. - The separator inherits `--color list-border` when the section's own border color is not explicitly set.
- `inline` takes precedence over `--header-first`: the inline section stays inside the list frame. `--header-border=inline` requires `--header-lines-border` to be `inline` or unset. - `--header-first` is not compatible with `--header-border=inline` or `--header-lines-border=inline`; `--header-border=inline` requires `--header-lines-border` to be `inline` or unset.
- New `dashed` border style with dashed edges (`` / ``) and rounded corners.
- `--border=dashed`, `--list-border=dashed`, etc.
- Works with inline sections (T-junctions render correctly).
- [vim] Move and resize popup window when detecting `VimResized` event (#4778) (@Vulcalien)
- Bug fixes
- Fixed gutter display in `--style=minimal`
- Fixed arrow keys / Home / End without modifiers being ignored under the kitty keyboard protocol (#4776) (@TymekDev)
- bash: Persist history deletion when `histappend` is on (#4764)
0.71.0 0.71.0
------ ------
+1 -1
View File
@@ -2,7 +2,7 @@
set -u set -u
version=0.72.0 version=0.71.0
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=2 update_config=2
+1 -1
View File
@@ -1,4 +1,4 @@
$version="0.72.0" $version="0.71.0"
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition $fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"github.com/junegunn/fzf/src/protector" "github.com/junegunn/fzf/src/protector"
) )
var version = "0.72" var version = "0.71"
var revision = "devel" var revision = "devel"
//go:embed shell/key-bindings.bash //go:embed shell/key-bindings.bash
+1 -1
View File
@@ -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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf\-tmux 1 "Apr 2026" "fzf 0.72.0" "fzf\-tmux - open fzf in tmux split pane" .TH fzf\-tmux 1 "Apr 2026" "fzf 0.71.0" "fzf\-tmux - open fzf in tmux split pane"
.SH NAME .SH NAME
fzf\-tmux - open fzf in tmux split pane fzf\-tmux - open fzf in tmux split pane
+2 -6
View File
@@ -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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "Apr 2026" "fzf 0.72.0" "fzf - a command-line fuzzy finder" .TH fzf 1 "Apr 2026" "fzf 0.71.0" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -517,8 +517,6 @@ Draw border around the finder
.br .br
.BR double " Border with double lines" .BR double " Border with double lines"
.br .br
.BR dashed " Border with dashed lines and rounded corners"
.br
.BR block " Border using block elements; suitable when using different background colors" .BR block " Border using block elements; suitable when using different background colors"
.br .br
.BR thinblock " Border using legacy computing symbols; may not be displayed on some terminals" .BR thinblock " Border using legacy computing symbols; may not be displayed on some terminals"
@@ -957,8 +955,6 @@ Should be used with one of the following \fB\-\-preview\-window\fR options.
.br .br
.B * border\-double .B * border\-double
.br .br
.B * border\-dashed
.br
.B * border\-block .B * border\-block
.br .br
.B * border\-thinblock .B * border\-thinblock
@@ -1108,7 +1104,7 @@ separator line between the header window and the list section. \fBinline\fR
style embeds the header inside the list border frame, joined to the list style embeds the header inside the list border frame, joined to the list
section by a horizontal separator; it requires a \fB\-\-list\-border\fR section by a horizontal separator; it requires a \fB\-\-list\-border\fR
shape that has both top and bottom segments (rounded / sharp / bold / shape that has both top and bottom segments (rounded / sharp / bold /
double / dashed / block / thinblock / horizontal) and falls back to \fBline\fR double / block / thinblock / horizontal) and falls back to \fBline\fR
otherwise. When the list border also has side segments, the separator otherwise. When the list border also has side segments, the separator
joins them with T-junctions; \fBhorizontal\fR has no side borders, so the joins them with T-junctions; \fBhorizontal\fR has no side borders, so the
separator is drawn without T-junction endpoints. Takes precedence over separator is drawn without T-junction endpoints. Takes precedence over
+25 -57
View File
@@ -896,7 +896,6 @@ function! s:execute_term(dict, command, temps) abort
endif endif
endfunction endfunction
function! fzf.on_exit(id, code, ...) function! fzf.on_exit(id, code, ...)
silent! autocmd! fzf_popup_resize
if s:getpos() == self.ppos " {'window': 'enew'} if s:getpos() == self.ppos " {'window': 'enew'}
for [opt, val] in items(self.winopts) for [opt, val] in items(self.winopts)
execute 'let' opt '=' val execute 'let' opt '=' val
@@ -1024,17 +1023,15 @@ function! s:callback(dict, lines) abort
endfunction endfunction
if has('nvim') if has('nvim')
function! s:create_popup() abort function s:create_popup(opts) abort
let opts = s:popup_bounds()
let opts = extend({'relative': 'editor', 'style': 'minimal'}, opts)
let buf = nvim_create_buf(v:false, v:true) let buf = nvim_create_buf(v:false, v:true)
let s:popup_id = nvim_open_win(buf, v:true, opts) let opts = extend({'relative': 'editor', 'style': 'minimal'}, a:opts)
call setwinvar(s:popup_id, '&colorcolumn', '') let win = nvim_open_win(buf, v:true, opts)
call setwinvar(win, '&colorcolumn', '')
" Colors " Colors
try try
call setwinvar(s:popup_id, '&winhighlight', 'Pmenu:,Normal:Normal') call setwinvar(win, '&winhighlight', 'Pmenu:,Normal:Normal')
let rules = get(g:, 'fzf_colors', {}) let rules = get(g:, 'fzf_colors', {})
if has_key(rules, 'bg') if has_key(rules, 'bg')
let color = call('s:get_color', rules.bg) let color = call('s:get_color', rules.bg)
@@ -1042,61 +1039,40 @@ if has('nvim')
let ns = nvim_create_namespace('fzf_popup') let ns = nvim_create_namespace('fzf_popup')
let hl = nvim_set_hl(ns, 'Normal', let hl = nvim_set_hl(ns, 'Normal',
\ &termguicolors ? { 'bg': color } : { 'ctermbg': str2nr(color) }) \ &termguicolors ? { 'bg': color } : { 'ctermbg': str2nr(color) })
call nvim_win_set_hl_ns(s:popup_id, ns) call nvim_win_set_hl_ns(win, ns)
endif endif
endif endif
catch catch
endtry endtry
return buf return buf
endfunction endfunction
function! s:resize_popup() abort
if !exists('s:popup_id') || !nvim_win_is_valid(s:popup_id)
return
endif
let opts = s:popup_bounds()
let opts = extend({'relative': 'editor'}, opts)
call nvim_win_set_config(s:popup_id, opts)
endfunction
else else
function! s:create_popup() abort function! s:create_popup(opts) abort
function! s:popup_create(buf) let s:popup_create = {buf -> popup_create(buf, #{
let s:popup_id = popup_create(a:buf, #{zindex: 1000}) \ line: a:opts.row,
call s:resize_popup() \ col: a:opts.col,
endfunction \ minwidth: a:opts.width,
\ maxwidth: a:opts.width,
\ minheight: a:opts.height,
\ maxheight: a:opts.height,
\ zindex: 1000,
\ })}
autocmd TerminalOpen * ++once call s:popup_create(str2nr(expand('<abuf>'))) autocmd TerminalOpen * ++once call s:popup_create(str2nr(expand('<abuf>')))
endfunction endfunction
function! s:resize_popup() abort
if !exists('s:popup_id') || empty(popup_getpos(s:popup_id))
return
endif
let opts = s:popup_bounds()
call popup_move(s:popup_id, {
\ 'line': opts.row,
\ 'col': opts.col,
\ 'minwidth': opts.width,
\ 'maxwidth': opts.width,
\ 'minheight': opts.height,
\ 'maxheight': opts.height,
\ })
endfunction
endif endif
function! s:popup_bounds() abort function! s:popup(opts) abort
let opts = s:popup_opts let xoffset = get(a:opts, 'xoffset', 0.5)
let yoffset = get(a:opts, 'yoffset', 0.5)
let xoffset = get(opts, 'xoffset', 0.5) let relative = get(a:opts, 'relative', 0)
let yoffset = get(opts, 'yoffset', 0.5)
let relative = get(opts, 'relative', 0)
" Use current window size for positioning relatively positioned popups " Use current window size for positioning relatively positioned popups
let columns = relative ? winwidth(0) : &columns let columns = relative ? winwidth(0) : &columns
let lines = relative ? winheight(0) : (&lines - has('nvim')) let lines = relative ? winheight(0) : (&lines - has('nvim'))
" Size and position " Size and position
let width = min([max([8, opts.width > 1 ? opts.width : float2nr(columns * opts.width)]), columns]) let width = min([max([8, a:opts.width > 1 ? a:opts.width : float2nr(columns * a:opts.width)]), columns])
let height = min([max([4, opts.height > 1 ? opts.height : float2nr(lines * opts.height)]), lines]) let height = min([max([4, a:opts.height > 1 ? a:opts.height : float2nr(lines * a:opts.height)]), lines])
let row = float2nr(yoffset * (lines - height)) + (relative ? win_screenpos(0)[0] - 1 : 0) let row = float2nr(yoffset * (lines - height)) + (relative ? win_screenpos(0)[0] - 1 : 0)
let col = float2nr(xoffset * (columns - width)) + (relative ? win_screenpos(0)[1] - 1 : 0) let col = float2nr(xoffset * (columns - width)) + (relative ? win_screenpos(0)[1] - 1 : 0)
@@ -1106,17 +1082,9 @@ function! s:popup_bounds() abort
let row += !has('nvim') let row += !has('nvim')
let col += !has('nvim') let col += !has('nvim')
return { 'row': row, 'col': col, 'width': width, 'height': height } call s:create_popup({
endfunction \ 'row': row, 'col': col, 'width': width, 'height': height
\ })
function! s:popup(opts) abort
let s:popup_opts = a:opts
call s:create_popup()
augroup fzf_popup_resize
autocmd!
autocmd VimResized * call s:resize_popup()
augroup END
endfunction endfunction
let s:default_action = { let s:default_action = {
+8 -12
View File
@@ -85,7 +85,7 @@ Usage: fzf [options]
--margin=MARGIN Screen margin (TRBL | TB,RL | T,RL,B | T,R,B,L) --margin=MARGIN Screen margin (TRBL | TB,RL | T,RL,B | T,R,B,L)
--padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L) --padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L)
--border[=STYLE] Draw border around the finder --border[=STYLE] Draw border around the finder
[rounded|sharp|bold|block|thinblock|double|dashed|horizontal|vertical| [rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
top|bottom|left|right|line|none] (default: rounded) top|bottom|left|right|line|none] (default: rounded)
--border-label=LABEL Label to print on the border --border-label=LABEL Label to print on the border
--border-label-pos=COL Position of the border label --border-label-pos=COL Position of the border label
@@ -128,7 +128,7 @@ Usage: fzf [options]
(each for list section and preview window) (each for list section and preview window)
--no-scrollbar Hide scrollbar --no-scrollbar Hide scrollbar
--list-border[=STYLE] Draw border around the list section --list-border[=STYLE] Draw border around the list section
[rounded|sharp|bold|block|thinblock|double|dashed|horizontal|vertical| [rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
top|bottom|left|right|none] (default: rounded) top|bottom|left|right|none] (default: rounded)
--list-label=LABEL Label to print on the list border --list-label=LABEL Label to print on the list border
--list-label-pos=COL Position of the list label --list-label-pos=COL Position of the list label
@@ -148,7 +148,7 @@ Usage: fzf [options]
--ghost=TEXT Ghost text to display when the input is empty --ghost=TEXT Ghost text to display when the input is empty
--filepath-word Make word-wise movements respect path separators --filepath-word Make word-wise movements respect path separators
--input-border[=STYLE] Draw border around the input section --input-border[=STYLE] Draw border around the input section
[rounded|sharp|bold|block|thinblock|double|dashed|horizontal|vertical| [rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
top|bottom|left|right|line|none] (default: rounded) top|bottom|left|right|line|none] (default: rounded)
--input-label=LABEL Label to print on the input border --input-label=LABEL Label to print on the input border
--input-label-pos=COL Position of the input label --input-label-pos=COL Position of the input label
@@ -165,7 +165,7 @@ Usage: fzf [options]
[,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES] [,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES]
[,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)] [,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]
--preview-border[=STYLE] Short for --preview-window=border-STYLE --preview-border[=STYLE] Short for --preview-window=border-STYLE
[rounded|sharp|bold|block|thinblock|double|dashed|horizontal|vertical| [rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
top|bottom|left|right|line|none] (default: rounded) top|bottom|left|right|line|none] (default: rounded)
--preview-label=LABEL --preview-label=LABEL
--preview-label-pos=N Same as --border-label and --border-label-pos, --preview-label-pos=N Same as --border-label and --border-label-pos,
@@ -177,7 +177,7 @@ Usage: fzf [options]
--header-lines=N The first N lines of the input are treated as header --header-lines=N The first N lines of the input are treated as header
--header-first Print header before the prompt line --header-first Print header before the prompt line
--header-border[=STYLE] Draw border around the header section --header-border[=STYLE] Draw border around the header section
[rounded|sharp|bold|block|thinblock|double|dashed|horizontal|vertical| [rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
top|bottom|left|right|line|inline|none] (default: rounded) top|bottom|left|right|line|inline|none] (default: rounded)
--header-lines-border[=STYLE] --header-lines-border[=STYLE]
Display header from --header-lines with a separate border. Display header from --header-lines with a separate border.
@@ -192,7 +192,7 @@ Usage: fzf [options]
FOOTER FOOTER
--footer=STR String to print as footer --footer=STR String to print as footer
--footer-border[=STYLE] Draw border around the footer section --footer-border[=STYLE] Draw border around the footer section
[rounded|sharp|bold|block|thinblock|double|dashed|horizontal|vertical| [rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
top|bottom|left|right|line|inline|none] (default: line) top|bottom|left|right|line|inline|none] (default: line)
--footer-label=LABEL Label to print on the footer border --footer-label=LABEL Label to print on the footer border
--footer-label-pos=COL Position of the footer label --footer-label-pos=COL Position of the footer label
@@ -968,8 +968,6 @@ func parseBorder(str string, optional bool) (tui.BorderShape, error) {
return tui.BorderThinBlock, nil return tui.BorderThinBlock, nil
case "double": case "double":
return tui.BorderDouble, nil return tui.BorderDouble, nil
case "dashed":
return tui.BorderDashed, nil
case "horizontal": case "horizontal":
return tui.BorderHorizontal, nil return tui.BorderHorizontal, nil
case "vertical": case "vertical":
@@ -988,7 +986,7 @@ func parseBorder(str string, optional bool) (tui.BorderShape, error) {
if optional && str == "" { if optional && str == "" {
return defaultBorderShape, nil return defaultBorderShape, nil
} }
return tui.BorderNone, errors.New("invalid border style (expected: rounded|sharp|bold|block|thinblock|double|dashed|horizontal|vertical|top|bottom|left|right|line|inline|none)") return tui.BorderNone, errors.New("invalid border style (expected: rounded|sharp|bold|block|thinblock|double|horizontal|vertical|top|bottom|left|right|line|inline|none)")
} }
func parseKeyChords(str string, message string) (map[tui.Event]string, []tui.Event, error) { func parseKeyChords(str string, message string) (map[tui.Event]string, []tui.Event, error) {
@@ -1572,7 +1570,7 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, *tui
case "info": case "info":
mergeAttr(&theme.Info) mergeAttr(&theme.Info)
case "pointer": case "pointer":
mergeAttr(&theme.Pointer) mergeAttr(&theme.Cursor)
case "marker": case "marker":
mergeAttr(&theme.Marker) mergeAttr(&theme.Marker)
case "header", "header-fg": case "header", "header-fg":
@@ -2343,8 +2341,6 @@ func parsePreviewWindowImpl(opts *previewOpts, input string) error {
opts.border = tui.BorderThinBlock opts.border = tui.BorderThinBlock
case "border-double": case "border-double":
opts.border = tui.BorderDouble opts.border = tui.BorderDouble
case "border-dashed":
opts.border = tui.BorderDashed
case "noborder", "border-none": case "noborder", "border-none":
opts.border = tui.BorderNone opts.border = tui.BorderNone
case "border-horizontal": case "border-horizontal":
+11 -18
View File
@@ -1816,16 +1816,14 @@ func (t *Terminal) changeHeader(header string) bool {
return needFullRedraw return needFullRedraw
} }
func (t *Terminal) changeFooter(footer string) bool { func (t *Terminal) changeFooter(footer string) {
var lines []string var lines []string
if len(footer) > 0 { if len(footer) > 0 {
lines = strings.Split(strings.TrimSuffix(footer, "\n"), "\n") lines = strings.Split(strings.TrimSuffix(footer, "\n"), "\n")
} }
needFullRedraw := len(t.footer) != len(lines)
t.footer = lines t.footer = lines
t.clickFooterLine = 0 t.clickFooterLine = 0
t.clickFooterColumn = 0 t.clickFooterColumn = 0
return needFullRedraw
} }
// UpdateHeader updates the header // UpdateHeader updates the header
@@ -2157,7 +2155,7 @@ func (t *Terminal) adjustMarginAndPadding() (int, int, [4]int, [4]int) {
if idx == 3 { if idx == 3 {
extraMargin[idx] += 1 + bw extraMargin[idx] += 1 + bw
} }
case tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderBlock, tui.BorderThinBlock, tui.BorderDouble, tui.BorderDashed: case tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderBlock, tui.BorderThinBlock, tui.BorderDouble:
extraMargin[idx] += 1 + bw*(idx%2) extraMargin[idx] += 1 + bw*(idx%2)
} }
marginInt[idx] = sizeSpecToInt(idx, sizeSpec) + extraMargin[idx] marginInt[idx] = sizeSpecToInt(idx, sizeSpec) + extraMargin[idx]
@@ -3019,7 +3017,7 @@ func (t *Terminal) printLabel(window tui.Window, render labelPrinter, opts label
return return
} }
switch borderShape { switch borderShape {
case tui.BorderHorizontal, tui.BorderTop, tui.BorderBottom, tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderBlock, tui.BorderThinBlock, tui.BorderDouble, tui.BorderDashed: case tui.BorderHorizontal, tui.BorderTop, tui.BorderBottom, tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderBlock, tui.BorderThinBlock, tui.BorderDouble:
if redrawBorder { if redrawBorder {
window.DrawHBorder() window.DrawHBorder()
} }
@@ -3604,18 +3602,18 @@ func (t *Terminal) renderEmptyLine(line int, barRange [2]int) {
func (t *Terminal) gutter(current bool, alt bool) { func (t *Terminal) gutter(current bool, alt bool) {
var color tui.ColorPair var color tui.ColorPair
if current { if current {
color = tui.ColCurrentPointerEmpty color = tui.ColCurrentCursorEmpty
} else if !t.raw && t.gutterReverse || t.raw && t.gutterRawReverse { } else if !t.raw && t.gutterReverse || t.raw && t.gutterRawReverse {
if alt { if alt {
color = tui.ColAltPointerEmpty color = tui.ColAltCursorEmpty
} else { } else {
color = tui.ColPointerEmpty color = tui.ColCursorEmpty
} }
} else { } else {
if alt { if alt {
color = tui.ColAltPointerEmptyChar color = tui.ColAltCursorEmptyChar
} else { } else {
color = tui.ColPointerEmptyChar color = tui.ColCursorEmptyChar
} }
} }
gutter := t.pointerEmpty gutter := t.pointerEmpty
@@ -3803,7 +3801,7 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
if len(label) == 0 { if len(label) == 0 {
t.gutter(true, false) t.gutter(true, false)
} else { } else {
t.window.CPrint(tui.ColCurrentPointer, label) t.window.CPrint(tui.ColCurrentCursor, label)
} }
if w-t.markerLen < 0 { if w-t.markerLen < 0 {
return indentSize return indentSize
@@ -3832,7 +3830,7 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
if len(label) == 0 { if len(label) == 0 {
t.gutter(false, index%2 == 1) t.gutter(false, index%2 == 1)
} else { } else {
t.window.CPrint(tui.ColPointer, label) t.window.CPrint(tui.ColCursor, label)
} }
if w-t.markerLen < 0 { if w-t.markerLen < 0 {
return indentSize return indentSize
@@ -6798,12 +6796,7 @@ func (t *Terminal) Loop() error {
}) })
case actChangeFooter, actTransformFooter, actBgTransformFooter: case actChangeFooter, actTransformFooter, actBgTransformFooter:
capture(false, func(footer string) { capture(false, func(footer string) {
if t.changeFooter(footer) && t.footerBorderShape == tui.BorderInline { t.changeFooter(footer)
// resizeIfNeeded() tolerates a shorter-than-wanted inline
// window, so a length change can leave the inline slot
// stale. Force a redraw to re-run the layout.
req(reqRedraw)
}
req(reqFooter) req(reqFooter)
}) })
case actChangeHeaderLabel, actTransformHeaderLabel, actBgTransformHeaderLabel: case actChangeHeaderLabel, actTransformHeaderLabel, actBgTransformHeaderLabel:
+22 -40
View File
@@ -506,7 +506,7 @@ type ColorTheme struct {
CurrentMatch ColorAttr CurrentMatch ColorAttr
Spinner ColorAttr Spinner ColorAttr
Info ColorAttr Info ColorAttr
Pointer ColorAttr Cursor ColorAttr
Marker ColorAttr Marker ColorAttr
Header ColorAttr Header ColorAttr
HeaderBg ColorAttr HeaderBg ColorAttr
@@ -596,7 +596,6 @@ const (
BorderLeft BorderLeft
BorderRight BorderRight
BorderInline BorderInline
BorderDashed
) )
func (s BorderShape) HasLeft() bool { func (s BorderShape) HasLeft() bool {
@@ -760,23 +759,6 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
leftMid: '╠', leftMid: '╠',
rightMid: '╣', rightMid: '╣',
} }
case BorderDashed:
// Terminal cells are taller than wide (~2:1), so horizontals can use a
// sparse stub per cell while verticals need more dashes per cell to look
// evenly dashed. Rounded corners and sharp T-junction mids.
return BorderStyle{
shape: shape,
top: '╶',
bottom: '╶',
left: '┆',
right: '┆',
topLeft: '╭',
topRight: '╮',
bottomLeft: '╰',
bottomRight: '╯',
leftMid: '├',
rightMid: '┤',
}
} }
return BorderStyle{ return BorderStyle{
shape: shape, shape: shape,
@@ -948,18 +930,18 @@ var (
ColDisabled ColorPair ColDisabled ColorPair
ColGhost ColorPair ColGhost ColorPair
ColMatch ColorPair ColMatch ColorPair
ColPointer ColorPair ColCursor ColorPair
ColPointerEmpty ColorPair ColCursorEmpty ColorPair
ColPointerEmptyChar ColorPair ColCursorEmptyChar ColorPair
ColAltPointerEmpty ColorPair ColAltCursorEmpty ColorPair
ColAltPointerEmptyChar ColorPair ColAltCursorEmptyChar ColorPair
ColMarker ColorPair ColMarker ColorPair
ColSelected ColorPair ColSelected ColorPair
ColSelectedMatch ColorPair ColSelectedMatch ColorPair
ColCurrent ColorPair ColCurrent ColorPair
ColCurrentMatch ColorPair ColCurrentMatch ColorPair
ColCurrentPointer ColorPair ColCurrentCursor ColorPair
ColCurrentPointerEmpty ColorPair ColCurrentCursorEmpty ColorPair
ColCurrentMarker ColorPair ColCurrentMarker ColorPair
ColCurrentSelectedEmpty ColorPair ColCurrentSelectedEmpty ColorPair
ColSpinner ColorPair ColSpinner ColorPair
@@ -1008,7 +990,7 @@ func init() {
CurrentMatch: undefined, CurrentMatch: undefined,
Spinner: defaultColor, Spinner: defaultColor,
Info: defaultColor, Info: defaultColor,
Pointer: defaultColor, Cursor: defaultColor,
Marker: defaultColor, Marker: defaultColor,
Header: defaultColor, Header: defaultColor,
Border: undefined, Border: undefined,
@@ -1058,7 +1040,7 @@ func init() {
CurrentMatch: undefined, CurrentMatch: undefined,
Spinner: undefined, Spinner: undefined,
Info: undefined, Info: undefined,
Pointer: undefined, Cursor: undefined,
Marker: undefined, Marker: undefined,
Header: undefined, Header: undefined,
Footer: undefined, Footer: undefined,
@@ -1109,7 +1091,7 @@ func init() {
CurrentMatch: ColorAttr{colBrightGreen, AttrUndefined}, CurrentMatch: ColorAttr{colBrightGreen, AttrUndefined},
Spinner: ColorAttr{colGreen, AttrUndefined}, Spinner: ColorAttr{colGreen, AttrUndefined},
Info: ColorAttr{colYellow, AttrUndefined}, Info: ColorAttr{colYellow, AttrUndefined},
Pointer: ColorAttr{colRed, AttrUndefined}, Cursor: ColorAttr{colRed, AttrUndefined},
Marker: ColorAttr{colMagenta, AttrUndefined}, Marker: ColorAttr{colMagenta, AttrUndefined},
Header: ColorAttr{colCyan, AttrUndefined}, Header: ColorAttr{colCyan, AttrUndefined},
Footer: ColorAttr{colCyan, AttrUndefined}, Footer: ColorAttr{colCyan, AttrUndefined},
@@ -1160,7 +1142,7 @@ func init() {
CurrentMatch: ColorAttr{151, AttrUndefined}, CurrentMatch: ColorAttr{151, AttrUndefined},
Spinner: ColorAttr{148, AttrUndefined}, Spinner: ColorAttr{148, AttrUndefined},
Info: ColorAttr{144, AttrUndefined}, Info: ColorAttr{144, AttrUndefined},
Pointer: ColorAttr{161, AttrUndefined}, Cursor: ColorAttr{161, AttrUndefined},
Marker: ColorAttr{168, AttrUndefined}, Marker: ColorAttr{168, AttrUndefined},
Header: ColorAttr{109, AttrUndefined}, Header: ColorAttr{109, AttrUndefined},
Footer: ColorAttr{109, AttrUndefined}, Footer: ColorAttr{109, AttrUndefined},
@@ -1211,7 +1193,7 @@ func init() {
CurrentMatch: ColorAttr{23, AttrUndefined}, CurrentMatch: ColorAttr{23, AttrUndefined},
Spinner: ColorAttr{65, AttrUndefined}, Spinner: ColorAttr{65, AttrUndefined},
Info: ColorAttr{101, AttrUndefined}, Info: ColorAttr{101, AttrUndefined},
Pointer: ColorAttr{161, AttrUndefined}, Cursor: ColorAttr{161, AttrUndefined},
Marker: ColorAttr{168, AttrUndefined}, Marker: ColorAttr{168, AttrUndefined},
Header: ColorAttr{31, AttrUndefined}, Header: ColorAttr{31, AttrUndefined},
Footer: ColorAttr{31, AttrUndefined}, Footer: ColorAttr{31, AttrUndefined},
@@ -1262,7 +1244,7 @@ func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, boldify bool, forceBlac
theme.CurrentMatch = boldify(theme.CurrentMatch) theme.CurrentMatch = boldify(theme.CurrentMatch)
theme.Prompt = boldify(theme.Prompt) theme.Prompt = boldify(theme.Prompt)
theme.Input = boldify(theme.Input) theme.Input = boldify(theme.Input)
theme.Pointer = boldify(theme.Pointer) theme.Cursor = boldify(theme.Cursor)
theme.Spinner = boldify(theme.Spinner) theme.Spinner = boldify(theme.Spinner)
} }
@@ -1306,7 +1288,7 @@ func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, boldify bool, forceBlac
theme.CurrentMatch = o(baseTheme.CurrentMatch, currentMatch) theme.CurrentMatch = o(baseTheme.CurrentMatch, currentMatch)
theme.Spinner = o(baseTheme.Spinner, theme.Spinner) theme.Spinner = o(baseTheme.Spinner, theme.Spinner)
theme.Info = o(baseTheme.Info, theme.Info) theme.Info = o(baseTheme.Info, theme.Info)
theme.Pointer = o(baseTheme.Pointer, theme.Pointer) theme.Cursor = o(baseTheme.Cursor, theme.Cursor)
theme.Marker = o(baseTheme.Marker, theme.Marker) theme.Marker = o(baseTheme.Marker, theme.Marker)
theme.Header = o(baseTheme.Header, theme.Header) theme.Header = o(baseTheme.Header, theme.Header)
theme.Footer = o(baseTheme.Footer, theme.Footer) theme.Footer = o(baseTheme.Footer, theme.Footer)
@@ -1422,11 +1404,11 @@ func initPalette(theme *ColorTheme) {
ColDisabled = pair(theme.Disabled, theme.InputBg) ColDisabled = pair(theme.Disabled, theme.InputBg)
ColMatch = pair(theme.Match, theme.ListBg) ColMatch = pair(theme.Match, theme.ListBg)
ColSelectedMatch = pair(theme.SelectedMatch, theme.SelectedBg) ColSelectedMatch = pair(theme.SelectedMatch, theme.SelectedBg)
ColPointer = pair(theme.Pointer, theme.Gutter) ColCursor = pair(theme.Cursor, theme.Gutter)
ColPointerEmpty = pair(blank, theme.Gutter) ColCursorEmpty = pair(blank, theme.Gutter)
ColPointerEmptyChar = pair(theme.Gutter, theme.ListBg) ColCursorEmptyChar = pair(theme.Gutter, theme.ListBg)
ColAltPointerEmpty = pair(blank, theme.AltGutter) ColAltCursorEmpty = pair(blank, theme.AltGutter)
ColAltPointerEmptyChar = pair(theme.AltGutter, theme.ListBg) ColAltCursorEmptyChar = pair(theme.AltGutter, theme.ListBg)
if theme.SelectedBg.Color != theme.ListBg.Color { if theme.SelectedBg.Color != theme.ListBg.Color {
ColMarker = pair(theme.Marker, theme.SelectedBg) ColMarker = pair(theme.Marker, theme.SelectedBg)
} else { } else {
@@ -1434,8 +1416,8 @@ func initPalette(theme *ColorTheme) {
} }
ColCurrent = pair(theme.Current, theme.DarkBg) ColCurrent = pair(theme.Current, theme.DarkBg)
ColCurrentMatch = pair(theme.CurrentMatch, theme.DarkBg) ColCurrentMatch = pair(theme.CurrentMatch, theme.DarkBg)
ColCurrentPointer = pair(theme.Pointer, theme.DarkBg) ColCurrentCursor = pair(theme.Cursor, theme.DarkBg)
ColCurrentPointerEmpty = pair(blank, theme.DarkBg) ColCurrentCursorEmpty = pair(blank, theme.DarkBg)
ColCurrentMarker = pair(theme.Marker, theme.DarkBg) ColCurrentMarker = pair(theme.Marker, theme.DarkBg)
ColCurrentSelectedEmpty = pair(blank, theme.DarkBg) ColCurrentSelectedEmpty = pair(blank, theme.DarkBg)
ColSpinner = pair(theme.Spinner, theme.InputBg) ColSpinner = pair(theme.Spinner, theme.InputBg)
+4 -16
View File
@@ -1497,7 +1497,7 @@ class TestLayout < TestInteractive
tmux.send_keys %(seq 5 | #{FZF} --style full --header foo --header-first --header-border inline), :Enter tmux.send_keys %(seq 5 | #{FZF} --style full --header foo --header-first --header-border inline), :Enter
tmux.until do |lines| tmux.until do |lines|
foo_idx = lines.index { |l| l.match?(/\A│\s+foo\s+│\z/) } foo_idx = lines.index { |l| l.match?(/\A│\s+foo\s+│\z/) }
input_idx = lines.index { |l| l.match?(%r{\A│\s+>\s+\d+/\d+\s+│\z}) } input_idx = lines.index { |l| l.match?(/\A│\s+>\s+\d+\/\d+\s+│\z/) }
foo_idx && input_idx && foo_idx < input_idx foo_idx && input_idx && foo_idx < input_idx
end end
end end
@@ -1510,7 +1510,7 @@ class TestLayout < TestInteractive
tmux.until do |lines| tmux.until do |lines|
one_idx = lines.index { |l| l.match?(/\A│\s+1\s+│\z/) } one_idx = lines.index { |l| l.match?(/\A│\s+1\s+│\z/) }
foo_idx = lines.index { |l| l.match?(/\A│\s+foo\s+│\z/) } foo_idx = lines.index { |l| l.match?(/\A│\s+foo\s+│\z/) }
input_idx = lines.index { |l| l.match?(%r{\A│\s+>\s+\d+/\d+\s+│\z}) } input_idx = lines.index { |l| l.match?(/\A│\s+>\s+\d+\/\d+\s+│\z/) }
one_idx && foo_idx && input_idx && one_idx < input_idx && input_idx < foo_idx one_idx && foo_idx && input_idx && one_idx < input_idx && input_idx < foo_idx
end end
end end
@@ -1522,7 +1522,7 @@ class TestLayout < TestInteractive
tmux.send_keys %(seq 5 | #{FZF} --style full --header-lines 1 --header-first --header-lines-border inline), :Enter tmux.send_keys %(seq 5 | #{FZF} --style full --header-lines 1 --header-first --header-lines-border inline), :Enter
tmux.until do |lines| tmux.until do |lines|
one_idx = lines.index { |l| l.match?(/\A│\s+1\s+│\z/) } one_idx = lines.index { |l| l.match?(/\A│\s+1\s+│\z/) }
input_idx = lines.index { |l| l.match?(%r{\A│\s+>\s+\d+/\d+\s+│\z}) } input_idx = lines.index { |l| l.match?(/\A│\s+>\s+\d+\/\d+\s+│\z/) }
one_idx && input_idx && one_idx < input_idx one_idx && input_idx && one_idx < input_idx
end end
end end
@@ -1534,24 +1534,12 @@ class TestLayout < TestInteractive
def test_inline_change_header_grows_slot def test_inline_change_header_grows_slot
tmux.send_keys %(seq 5 | #{FZF} --style full --header-lines 1 --header-border inline --bind space:change-header:tada), :Enter tmux.send_keys %(seq 5 | #{FZF} --style full --header-lines 1 --header-border inline --bind space:change-header:tada), :Enter
tmux.until { |lines| lines.any_include?(/\A│\s+1\s+│\z/) } tmux.until { |lines| lines.any_include?(/\A│\s+1\s+│\z/) }
tmux.send_keys :Space tmux.send_keys ' '
tmux.until do |lines| tmux.until do |lines|
lines.any_include?(/\A│\s+1\s+│\z/) && lines.any_include?(/\A│\s+tada\s+│\z/) lines.any_include?(/\A│\s+1\s+│\z/) && lines.any_include?(/\A│\s+tada\s+│\z/)
end end
end end
# Regression: with --footer-border=inline, change-footer that grows the
# footer line count left the inline slot sized for the old length, so
# extra lines were clipped.
def test_inline_change_footer_grows_slot
tmux.send_keys %(seq 5 | #{FZF} --style full --footer-border inline --footer one --bind $'space:change-footer:one\\ntwo'), :Enter
tmux.until { |lines| lines.any_include?(/\A│\s+one\s+│\z/) }
tmux.send_keys :Space
tmux.until do |lines|
lines.any_include?(/\A│\s+one\s+│\z/) && lines.any_include?(/\A│\s+two\s+│\z/)
end
end
# Invalid inline combinations must be rejected at startup. # Invalid inline combinations must be rejected at startup.
def test_inline_rejected_on_unsupported_options def test_inline_rejected_on_unsupported_options
[ [