Compare commits

..

2 Commits

Author SHA1 Message Date
Junegunn Choi
a099d76fa6 Extract common popup argument building into popupArgStr 2026-03-26 21:14:00 +09:00
Junegunn Choi
a5646b46e8 Support zellij floating pane via --popup (new name for --tmux) 2026-03-26 20:47:15 +09:00
45 changed files with 953 additions and 1662 deletions

View File

@@ -1,6 +1,6 @@
root = true root = true
[*.{sh,bash,fish}] [*.{sh,bash}]
indent_style = space indent_style = space
indent_size = 2 indent_size = 2
simplify = true simplify = true

1
.github/CODEOWNERS vendored
View File

@@ -1 +0,0 @@
* @junegunn

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@7372622e62b60b3cb750dcd2b9e32c247ffec26a # v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 3.4.6 ruby-version: 3.4.6

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@7372622e62b60b3cb750dcd2b9e32c247ffec26a # v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 3.0.0 ruby-version: 3.0.0

24
.github/workflows/sponsors.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
---
name: Generate Sponsors README
on:
workflow_dispatch:
schedule:
- cron: 0 15 * * 6
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
uses: actions/checkout@v5
- name: Generate Sponsors 💖
uses: JamesIves/github-sponsors-readme-action@v1
with:
token: ${{ secrets.SPONSORS_TOKEN }}
file: 'README.md'
- name: Deploy to GitHub Pages 🚀
uses: JamesIves/github-pages-deploy-action@v4
with:
branch: master
folder: '.'

View File

@@ -7,4 +7,4 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- uses: crate-ci/typos@685eb3d55be2f85191e8c84acb9f44d7756f84ab # v1.29.4 - uses: crate-ci/typos@v1.29.4

View File

@@ -7,7 +7,7 @@ jobs:
publish: publish:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: vedantmgoyal2009/winget-releaser@4ffc7888bffd451b357355dc214d43bb9f23917e # v2 - uses: vedantmgoyal2009/winget-releaser@v2
with: with:
identifier: junegunn.fzf identifier: junegunn.fzf
installers-regex: '-windows_(armv7|arm64|amd64)\.zip$' installers-regex: '-windows_(armv7|arm64|amd64)\.zip$'

View File

@@ -309,16 +309,16 @@ I know it's a lot to digest, let's try to break down the code.
available color options. available color options.
- The value of `--preview-window` option consists of 5 components delimited - The value of `--preview-window` option consists of 5 components delimited
by `,` by `,`
1. `up` -- Position of the preview window 1. `up` Position of the preview window
1. `60%` -- Size of the preview window 1. `60%` Size of the preview window
1. `border-bottom` -- Preview window border only on the bottom side 1. `border-bottom` Preview window border only on the bottom side
1. `+{2}+3/3` -- Scroll offset of the preview contents 1. `+{2}+3/3` Scroll offset of the preview contents
1. `~3` -- Fixed header 1. `~3` Fixed header
- Let's break down the latter two. We want to display the bat output in the - Let's break down the latter two. We want to display the bat output in the
preview window with a certain scroll offset so that the matching line is preview window with a certain scroll offset so that the matching line is
positioned near the center of the preview window. positioned near the center of the preview window.
- `+{2}` -- The base offset is extracted from the second token - `+{2}` The base offset is extracted from the second token
- `+3` -- We add 3 lines to the base offset to compensate for the header - `+3` We add 3 lines to the base offset to compensate for the header
part of `bat` output part of `bat` output
- ``` - ```
───────┬────────────────────────────────────────────────────────── ───────┬──────────────────────────────────────────────────────────

View File

@@ -1,31 +1,8 @@
CHANGELOG CHANGELOG
========= =========
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.
- 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:
```sh
ps -ef | fzf --reverse --style full:double \
--header 'Select a process' --header-lines 1 \
--bind 'load:transform-footer:echo $FZF_TOTAL_COUNT processes' \
--header-border=inline --header-lines-border=inline \
--footer-border=inline
```
- `--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.
- `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.
- [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
------ ------
_Release highlights: https://junegunn.github.io/fzf/releases/0.71.0/_
- Added `--popup` as a new name for `--tmux` with Zellij support - Added `--popup` as a new name for `--tmux` with Zellij support
- `--popup` starts fzf in a tmux popup or a Zellij floating pane - `--popup` starts fzf in a tmux popup or a Zellij floating pane
- `--tmux` is now an alias for `--popup` - `--tmux` is now an alias for `--popup`
@@ -44,34 +21,24 @@ _Release highlights: https://junegunn.github.io/fzf/releases/0.71.0/_
- The search performance now scales linearly with the number of CPU cores, as we dropped static partitioning to allow better load balancing across threads. - The search performance now scales linearly with the number of CPU cores, as we dropped static partitioning to allow better load balancing across threads.
``` ```
=== query: 'linux' === === query: 'linux' ===
[all] baseline: 21.95ms current: 17.47ms (1.26x) matches: 179966 (12.79%) [all] baseline: 17.12ms current: 14.28ms (1.20x) matches: 179966 (12.79%)
[1T] baseline: 179.63ms current: 180.53ms (1.00x) matches: 179966 (12.79%) [1T] baseline: 136.49ms current: 137.25ms (0.99x) matches: 179966 (12.79%)
[2T] baseline: 97.38ms current: 90.05ms (1.08x) matches: 179966 (12.79%) [2T] baseline: 75.74ms current: 68.75ms (1.10x) matches: 179966 (12.79%)
[4T] baseline: 53.83ms current: 44.77ms (1.20x) matches: 179966 (12.79%) [4T] baseline: 41.16ms current: 34.97ms (1.18x) matches: 179966 (12.79%)
[8T] baseline: 41.66ms current: 22.58ms (1.84x) matches: 179966 (12.79%) [8T] baseline: 32.82ms current: 17.79ms (1.84x) matches: 179966 (12.79%)
``` ```
- Improved the cache structure, reducing memory footprint per entry by 86x. - Improved the cache structure, reducing memory footprint per entry by 86x.
- With the reduced per-entry cost, the cache now has broader coverage. - With the reduced per-entry cost, the cache now has broader coverage.
- Shell integration improvements - Shell integration improvements
- bash: CTRL-R now supports multi-select and `shift-delete` to delete history entries (#4715) - bash: CTRL-R now supports multi-select and `shift-delete` to delete history entries (#4715)
- fish: - fish: Improved command history (CTRL-R) (#4703) (@bitraid)
- Improved command history (CTRL-R) (#4703) (@bitraid)
- Rewrite completion script (SHIFT-TAB) (#4731) (@bitraid)
- Increase minimum fish version requirement to 3.4.0 (#4731) (@bitraid)
- `GET /` HTTP endpoint now includes `positions` field in each match entry, providing the indices of matched characters for external highlighting (#4726) - `GET /` HTTP endpoint now includes `positions` field in each match entry, providing the indices of matched characters for external highlighting (#4726)
- Allow adaptive height with negative value (`--height=~-HEIGHT`) (#4682)
- Bug fixes - Bug fixes
- `--walker=follow` no longer follows symlinks whose target is an ancestor of the walker root, avoiding severe resource exhaustion when a symlink points outside the tree (e.g. Wine's `z:` → `/`) (#4710) - `--walker=follow` no longer follows symlinks whose target is an ancestor of the walker root, avoiding severe resource exhaustion when a symlink points outside the tree (e.g. Wine's `z:` → `/`) (#4710)
- Fixed AWK tokenizer not treating a new line character as whitespace - Fixed AWK tokenizer not treating a new line character as whitespace
- Fixed `--{accept,with}-nth` removing trailing whitespaces with a non-default `--delimiter` - Fixed `--{accept,with}-nth` removing trailing whitespaces with a non-default `--delimiter`
- Fixed OSC8 hyperlinks being mangled when the URL contains unicode characters (#4707) - Fixed OSC8 hyperlinks being mangled when the URL contains unicode characters (#4707)
- Fixed `--with-shell` not handling quoted arguments correctly (#4709) - Fixed `--with-shell` not handling quoted arguments correctly (#4709)
- Fixed child processes not being terminated on Windows (#4723) (@pjeby)
- Fixed preview scrollbar not rendered after `toggle-preview`
- Fixed preview follow/scroll with long wrapped lines
- Fixed tab width when `--frozen-left` is used
- Fixed preview mouse events being processed when no preview window exists
- zsh: Fixed history widget when `sh_glob` option is on (#4714) (@EvanHahn)
0.70.0 0.70.0
------ ------

View File

@@ -53,8 +53,6 @@ ifeq ($(UNAME_M),x86_64)
BINARY := $(BINARY64) BINARY := $(BINARY64)
else ifeq ($(UNAME_M),amd64) else ifeq ($(UNAME_M),amd64)
BINARY := $(BINARY64) BINARY := $(BINARY64)
else ifeq ($(UNAME_M),i86pc)
BINARY := $(BINARY64)
else ifeq ($(UNAME_M),s390x) else ifeq ($(UNAME_M),s390x)
BINARY := $(BINARYS390) BINARY := $(BINARYS390)
else ifeq ($(UNAME_M),i686) else ifeq ($(UNAME_M),i686)

175
README.md

File diff suppressed because one or more lines are too long

View File

@@ -112,7 +112,7 @@ the whole if we start off with `:FZF` command.
" Bang version starts fzf in fullscreen mode " Bang version starts fzf in fullscreen mode
:FZF! :FZF!
< <
Similarly to {ctrlp.vim}{3}, use Enter key, CTRL-T, CTRL-X or CTRL-V to open Similarly to {ctrlp.vim}{3}, use enter key, CTRL-T, CTRL-X or CTRL-V to open
selected files in the current window, in new tabs, in horizontal splits, or in selected files in the current window, in new tabs, in horizontal splits, or in
vertical splits respectively. vertical splits respectively.
@@ -218,6 +218,7 @@ list:
`fg` / `bg` / `hl` | Item (foreground / background / highlight) `fg` / `bg` / `hl` | Item (foreground / background / highlight)
`fg+` / `bg+` / `hl+` | Current item (foreground / background / highlight) `fg+` / `bg+` / `hl+` | Current item (foreground / background / highlight)
`preview-fg` / `preview-bg` | Preview window text and background `preview-fg` / `preview-bg` | Preview window text and background
`hl` / `hl+` | Highlighted substrings (normal / current)
`gutter` | Background of the gutter on the left `gutter` | Background of the gutter on the left
`pointer` | Pointer to the current line ( `>` ) `pointer` | Pointer to the current line ( `>` )
`marker` | Multi-select marker ( `>` ) `marker` | Multi-select marker ( `>` )
@@ -228,6 +229,7 @@ list:
`query` | Query string `query` | Query string
`disabled` | Query string when search is disabled `disabled` | Query string when search is disabled
`prompt` | Prompt before query ( `> ` ) `prompt` | Prompt before query ( `> ` )
`pointer` | Pointer to the current line ( `>` )
----------------------------+------------------------------------------------------ ----------------------------+------------------------------------------------------
- `component` specifies the component (`fg` / `bg`) from which to extract the - `component` specifies the component (`fg` / `bg`) from which to extract the
color when considering each of the following highlight groups color when considering each of the following highlight groups
@@ -243,7 +245,7 @@ if it exists, - otherwise use the `fg` attribute of the `Comment` highlight
group if it exists, - otherwise fall back to the default color settings for group if it exists, - otherwise fall back to the default color settings for
the prompt. the prompt.
You can examine the color option generated according to the setting by printing You can examine the color option generated according the setting by printing
the result of `fzf#wrap()` function like so: the result of `fzf#wrap()` function like so:
> >
:echo fzf#wrap() :echo fzf#wrap()

13
install
View File

@@ -2,7 +2,7 @@
set -u set -u
version=0.71.0 version=0.70.0
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=2 update_config=2
@@ -112,15 +112,10 @@ link_fzf_in_path() {
return 1 return 1
} }
tar_opts="-xzf -"
if tar --no-same-owner -tf /dev/null 2> /dev/null; then
tar_opts="--no-same-owner $tar_opts"
fi
try_curl() { try_curl() {
command -v curl > /dev/null && command -v curl > /dev/null &&
if [[ $1 =~ tar.gz$ ]]; then if [[ $1 =~ tar.gz$ ]]; then
curl -fL $1 | tar $tar_opts curl -fL $1 | tar --no-same-owner -xzf -
else else
local temp=${TMPDIR:-/tmp}/fzf.zip local temp=${TMPDIR:-/tmp}/fzf.zip
curl -fLo "$temp" $1 && unzip -o "$temp" && rm -f "$temp" curl -fLo "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
@@ -130,7 +125,7 @@ try_curl() {
try_wget() { try_wget() {
command -v wget > /dev/null && command -v wget > /dev/null &&
if [[ $1 =~ tar.gz$ ]]; then if [[ $1 =~ tar.gz$ ]]; then
wget -O - $1 | tar $tar_opts wget -O - $1 | tar --no-same-owner -xzf -
else else
local temp=${TMPDIR:-/tmp}/fzf.zip local temp=${TMPDIR:-/tmp}/fzf.zip
wget -O "$temp" $1 && unzip -o "$temp" && rm -f "$temp" wget -O "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
@@ -444,7 +439,7 @@ if [ $update_config -eq 1 ]; then
echo echo
fi fi
[[ $shells =~ zsh ]] && echo " source ${ZDOTDIR:-~}/.zshrc # zsh" [[ $shells =~ zsh ]] && echo " source ${ZDOTDIR:-~}/.zshrc # zsh"
[[ $shells =~ fish && $lno_func -ne 0 ]] && echo ' fish_user_key_bindings # fish' [[ $shells =~ fish && $lno_func -ne 0 ]] && echo ' fzf_user_key_bindings # fish'
echo echo
echo 'Use uninstall script to remove fzf.' echo 'Use uninstall script to remove fzf.'
echo echo

View File

@@ -1,4 +1,4 @@
$version="0.71.0" $version="0.70.0"
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition $fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition

View File

@@ -11,7 +11,7 @@ import (
"github.com/junegunn/fzf/src/protector" "github.com/junegunn/fzf/src/protector"
) )
var version = "0.71" var version = "0.70"
var revision = "devel" var revision = "devel"
//go:embed shell/key-bindings.bash //go:embed shell/key-bindings.bash

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.71.0" "fzf\-tmux - open fzf in tmux split pane" .TH fzf\-tmux 1 "Mar 2026" "fzf 0.70.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

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.71.0" "fzf - a command-line fuzzy finder" .TH fzf 1 "Mar 2026" "fzf 0.70.0" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -384,7 +384,7 @@ Use black background
.SS DISPLAY MODE .SS DISPLAY MODE
.TP .TP
.BI "\-\-height=" "[~][\-]HEIGHT[%]" .BI "\-\-height=" "[~]HEIGHT[%]"
Display fzf window below the cursor with the given height instead of using Display fzf window below the cursor with the given height instead of using
the full screen. the full screen.
@@ -394,19 +394,17 @@ height minus the given value.
fzf \-\-height=\-1 fzf \-\-height=\-1
When prefixed with \fB~\fR, fzf will automatically determine the height in the When prefixed with \fB~\fR, fzf will automatically determine the height in the
range according to the input size. You can combine \fB~\fR with a negative range according to the input size.
value.
# Will not take up 100% of the screen # Will not take up 100% of the screen
seq 5 | fzf \-\-height=~100% seq 5 | fzf \-\-height=~100%
# Adapt to input size, up to terminal height minus 1
seq 5 | fzf \-\-height=~\-1
Adaptive height has the following limitations: Adaptive height has the following limitations:
.br .br
* Cannot be used with top/bottom margin and padding given in percent size * Cannot be used with top/bottom margin and padding given in percent size
.br .br
* Negative value is not allowed
.br
* It will not find the right size when there are multi-line items * It will not find the right size when there are multi-line items
.TP .TP
@@ -1100,17 +1098,7 @@ Print header before the prompt line. When both normal header and header lines
.TP .TP
.BI "\-\-header\-border" [=STYLE] .BI "\-\-header\-border" [=STYLE]
Draw border around the header section. \fBline\fR style draws a single Draw border around the header section. \fBline\fR style draws a single
separator line between the header window and the list section. \fBinline\fR separator line between the header window and the list section.
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
shape that has both top and bottom segments (rounded / sharp / bold /
double / block / thinblock / horizontal) and falls back to \fBline\fR
otherwise. When the list border also has side segments, the separator
joins them with T-junctions; \fBhorizontal\fR has no side borders, so the
separator is drawn without T-junction endpoints. Takes precedence over
\fB\-\-header\-first\fR (the section stays inside the list frame), and
when \fB\-\-header\-lines\fR is also set \fB\-\-header\-lines\-border\fR
must also be \fBinline\fR.
.TP .TP
.BI "\-\-header\-label" [=LABEL] .BI "\-\-header\-label" [=LABEL]
@@ -1126,10 +1114,6 @@ Display header from \fB--header\-lines\fR with a separate border. Pass
\fBnone\fR to still separate the header lines but without a border. To combine \fBnone\fR to still separate the header lines but without a border. To combine
two headers, use \fB\-\-no\-header\-lines\-border\fR. \fBline\fR style draws two headers, use \fB\-\-no\-header\-lines\-border\fR. \fBline\fR style draws
a single separator line between the header lines and the list section. a single separator line between the header lines and the list section.
\fBinline\fR style embeds the header lines inside the list border frame
with a horizontal separator; it requires a \fB\-\-list\-border\fR shape
that has both top and bottom segments, falls back to \fBline\fR
otherwise.
.SS FOOTER .SS FOOTER
@@ -1143,10 +1127,7 @@ are not affected by \fB\-\-with\-nth\fR. ANSI color codes are processed even whe
.TP .TP
.BI "\-\-footer\-border" [=STYLE] .BI "\-\-footer\-border" [=STYLE]
Draw border around the footer section. \fBline\fR style draws a single Draw border around the footer section. \fBline\fR style draws a single
separator line between the footer and the list section. \fBinline\fR style separator line between the footer and the list section.
embeds the footer inside the list border frame with a horizontal separator;
it requires a \fB\-\-list\-border\fR shape that has both top and bottom
segments and falls back to \fBline\fR otherwise.
.TP .TP
.BI "\-\-footer\-label" [=LABEL] .BI "\-\-footer\-label" [=LABEL]

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 = {

141
shell/common.fish Normal file
View File

@@ -0,0 +1,141 @@
function __fzf_defaults
# $argv[1]: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $argv[2..]: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
string join ' ' -- \
"--height $FZF_TMUX_HEIGHT --min-height=20+ --bind=ctrl-z:ignore" $argv[1] \
(test -r "$FZF_DEFAULT_OPTS_FILE"; and string join -- ' ' <$FZF_DEFAULT_OPTS_FILE) \
$FZF_DEFAULT_OPTS $argv[2..-1]
end
function __fzfcmd
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
if test -n "$FZF_TMUX_OPTS"
echo "fzf-tmux $FZF_TMUX_OPTS -- "
else if test "$FZF_TMUX" = "1"
echo "fzf-tmux -d$FZF_TMUX_HEIGHT -- "
else
echo "fzf"
end
end
function __fzf_cmd_tokens -d 'Return command line tokens, skipping leading env assignments and command prefixes'
# Get tokens - use version-appropriate flags
set -l tokens
if test (string match -r -- '^\d+' $version) -ge 4
set -- tokens (commandline -xpc)
else
set -- tokens (commandline -opc)
end
# Filter out leading environment variable assignments
set -l -- var_count 0
for i in $tokens
if string match -qr -- '^[\w]+=' $i
set var_count (math $var_count + 1)
else
break
end
end
set -e -- tokens[0..$var_count]
# Skip command prefixes so callers see the actual command name,
# e.g. "builtin cd" → "cd", "env VAR=1 command cd" → "cd"
while true
switch "$tokens[1]"
case builtin command
set -e -- tokens[1]
test "$tokens[1]" = "--"; and set -e -- tokens[1]
case env
set -e -- tokens[1]
test "$tokens[1]" = "--"; and set -e -- tokens[1]
while string match -qr -- '^[\w]+=' "$tokens[1]"
set -e -- tokens[1]
end
case '*'
break
end
end
string escape -n -- $tokens
end
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
set -l fzf_query ''
set -l prefix ''
set -l dir '.'
# Set variables containing the major and minor fish version numbers, using
# a method compatible with all supported fish versions.
set -l -- fish_major (string match -r -- '^\d+' $version)
set -l -- fish_minor (string match -r -- '^\d+\.(\d+)' $version)[2]
# fish v3.3.0 and newer: Don't use option prefix if " -- " is preceded.
set -l -- match_regex '(?<fzf_query>[\s\S]*?(?=\n?$)$)'
set -l -- prefix_regex '^-[^\s=]+=|^-(?!-)\S'
if test "$fish_major" -eq 3 -a "$fish_minor" -lt 3
or string match -q -v -- '* -- *' (string sub -l (commandline -Cp) -- (commandline -p))
set -- match_regex "(?<prefix>$prefix_regex)?$match_regex"
end
# Set $prefix and expanded $fzf_query with preserved trailing newlines.
if test "$fish_major" -ge 4
# fish v4.0.0 and newer
string match -q -r -- $match_regex (commandline --current-token --tokens-expanded | string collect -N)
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
# fish v3.2.0 - v3.7.1 (last v3)
string match -q -r -- $match_regex (commandline --current-token --tokenize | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)' '')
else
# fish older than v3.2.0 (v3.1b1 - v3.1.2)
set -l -- cl_token (commandline --current-token --tokenize | string collect -N)
set -- prefix (string match -r -- $prefix_regex $cl_token)
set -- fzf_query (string replace -- "$prefix" '' $cl_token | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)|\\\n\\\n$' '')
end
if test -n "$fzf_query"
# Normalize path in $fzf_query, set $dir to the longest existing directory.
if test \( "$fish_major" -ge 4 \) -o \( "$fish_major" -eq 3 -a "$fish_minor" -ge 5 \)
# fish v3.5.0 and newer
set -- fzf_query (path normalize -- $fzf_query)
set -- dir $fzf_query
while not path is -d $dir
set -- dir (path dirname $dir)
end
else
# fish older than v3.5.0 (v3.1b1 - v3.4.1)
if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
# fish v3.2.0 - v3.4.1
string match -q -r -- '(?<fzf_query>^[\s\S]*?(?=\n?$)$)' \
(string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
else
# fish v3.1b1 - v3.1.2
set -- fzf_query (string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r '\\\n$' '')
end
set -- dir $fzf_query
while not test -d "$dir"
set -- dir (dirname -z -- "$dir" | string split0)
end
end
if not string match -q -- '.' $dir; or string match -q -r -- '^\./|^\.$' $fzf_query
# Strip $dir from $fzf_query - preserve trailing newlines.
if test "$fish_major" -ge 4
# fish v4.0.0 and newer
string match -q -r -- '^'(string escape --style=regex -- $dir)'/?(?<fzf_query>[\s\S]*)' $fzf_query
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
# fish v3.2.0 - v3.7.1 (last v3)
string match -q -r -- '^/?(?<fzf_query>[\s\S]*?(?=\n?$)$)' \
(string replace -- "$dir" '' $fzf_query | string collect -N)
else
# fish older than v3.2.0 (v3.1b1 - v3.1.2)
set -- fzf_query (string replace -- "$dir" '' $fzf_query | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^/?|\\\n$' '')
end
end
end
string escape -n -- "$dir" "$fzf_query" "$prefix"
end

View File

@@ -4,6 +4,8 @@
# / __/ / /_/ __/ # / __/ / /_/ __/
# /_/ /___/_/ completion.bash # /_/ /___/_/ completion.bash
# #
# - $FZF_TMUX (default: 0)
# - $FZF_TMUX_OPTS (default: empty)
# - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty) # - $FZF_COMPLETION_OPTS (default: empty)
# - $FZF_COMPLETION_PATH_OPTS (default: empty) # - $FZF_COMPLETION_PATH_OPTS (default: empty)

View File

@@ -4,166 +4,238 @@
# / __/ / /_/ __/ # / __/ / /_/ __/
# /_/ /___/_/ completion.fish # /_/ /___/_/ completion.fish
# #
# - $FZF_COMPLETION_OPTS # - $FZF_COMPLETION_OPTS (default: empty)
# - $FZF_EXPANSION_OPTS
# The oldest supported fish version is 3.4.0. For this message being able to be function fzf_completion_setup
# displayed on older versions, the command substitution syntax $() should not
# be used anywhere in the script, otherwise the source command will fail.
if string match -qr -- '^[12]\\.|^3\\.[0-3]' $version
echo "fzf completion script requires fish version 3.4.0 or newer." >&2
return 1
else if not command -q fzf
echo "fzf was not found in path." >&2
return 1
end
function fzf_complete -w fzf -d 'fzf command completion and wildcard expansion search' #----BEGIN INCLUDE common.fish
# Restore the default shift-tab behavior on tab completions # NOTE: Do not directly edit this section, which is copied from "common.fish".
if commandline --paging-mode # To modify it, one can edit "common.fish" and run "./update.sh" to apply
commandline -f complete-and-search # the changes. See code comments in "common.fish" for the implementation details.
return
function __fzf_defaults
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
string join ' ' -- \
"--height $FZF_TMUX_HEIGHT --min-height=20+ --bind=ctrl-z:ignore" $argv[1] \
(test -r "$FZF_DEFAULT_OPTS_FILE"; and string join -- ' ' <$FZF_DEFAULT_OPTS_FILE) \
$FZF_DEFAULT_OPTS $argv[2..-1]
end end
# Remove any trailing unescaped backslash from token and update command line function __fzfcmd
set -l -- token (string replace -r -- '(?<!\\\\)(?:\\\\\\\\)*\\K\\\\$' '' (commandline -t | string collect) | string collect) test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
commandline -rt -- $token if test -n "$FZF_TMUX_OPTS"
echo "fzf-tmux $FZF_TMUX_OPTS -- "
# Remove any line breaks from token else if test "$FZF_TMUX" = "1"
set -- token (string replace -ra -- '\\\\\\n' '' $token | string collect) echo "fzf-tmux -d$FZF_TMUX_HEIGHT -- "
# regex: Match token with unescaped/unquoted glob character
set -l -- r_glob '^(?:[^\'"\\\\*]|\\\\[\\S\\s]|\'(?:\\\\[\\S\\s]|[^\'\\\\])*\'|"(?:\\\\[\\S\\s]|[^"\\\\])*")*\\*[\\S\\s]*$'
# regex: Match any unbalanced quote character
set -l -- r_quote '^(?>(?:\\\\[\\s\\S]|"(?:[^"\\\\]|\\\\[\\s\\S])*"|\'(?:[^\'\\\\]|\\\\[\\s\\S])*\'|[^\'"\\\\]+)*)\\K[\'"]'
# The expansion pattern is the token with any open quote closed, or is empty.
set -l -- glob_pattern (string match -r -- $r_glob $token | string collect)(string match -r -- $r_quote $token | string collect -a)
set -l -- cl_tokenize_opt '--tokens-expanded'
string match -q -- '3.*' $version
and set -- cl_tokenize_opt '--tokenize'
# Set command line tokens without any leading variable definitions or launcher
# commands (including their options, but not any option arguments).
set -l -- r_cmd '^(?:(?:builtin|command|doas|env|sudo|\\w+=\\S*|-\\S+)\\s+)*\\K[\\s\\S]+'
set -l -- cmd (commandline $cl_tokenize_opt --input=(commandline -pc | string match -r $r_cmd))
test -z "$token"
and set -a -- cmd ''
# Set fzf options
test -z "$FZF_TMUX_HEIGHT"
and set -l -- FZF_TMUX_HEIGHT 40%
set -lax -- FZF_DEFAULT_OPTS \
"--height=$FZF_TMUX_HEIGHT --min-height=20+ --bind=ctrl-z:ignore" \
(test -r "$FZF_DEFAULT_OPTS_FILE"; and string join -- ' ' <$FZF_DEFAULT_OPTS_FILE) \
$FZF_DEFAULT_OPTS '--bind=alt-r:toggle-raw --multi --wrap=word --reverse' \
(if test -n "$glob_pattern"; string collect -- $FZF_EXPANSION_OPTS; else;
string collect -- $FZF_COMPLETION_OPTS; end; string escape -n -- $argv) \
--with-shell=(status fish-path)\\ -c
set -lx FZF_DEFAULT_OPTS_FILE
set -l -- fzf_cmd fzf
test "$FZF_TMUX" = 1
and set -- fzf_cmd fzf-tmux $FZF_TMUX_OPTS -d$FZF_TMUX_HEIGHT --
set -l result
# Get the completion list from stdin when it's not a tty
if not isatty stdin
set -l -- custom_post_func _fzf_post_complete_$cmd[1]
functions -q $custom_post_func
or set -- custom_post_func _fzf_complete_$cmd[1]_post
if functions -q $custom_post_func
$fzf_cmd | $custom_post_func $cmd | while read -l r; set -a -- result $r; end
else if string match -q -- '*--print0*' "$FZF_DEFAULT_OPTS"
$fzf_cmd | while read -lz r; set -a -- result $r; end
else else
$fzf_cmd | while read -l r; set -a -- result $r; end echo "fzf"
end
end
function __fzf_cmd_tokens -d 'Return command line tokens, skipping leading env assignments and command prefixes'
set -l tokens
if test (string match -r -- '^\d+' $version) -ge 4
set -- tokens (commandline -xpc)
else
set -- tokens (commandline -opc)
end end
# Wildcard expansion set -l -- var_count 0
else if test -n "$glob_pattern" for i in $tokens
# Set the command to be run by fzf, so there is a visual indicator and an if string match -qr -- '^[\w]+=' $i
# easy way to abort on long recursive searches. set var_count (math $var_count + 1)
set -lx -- FZF_DEFAULT_COMMAND "for i in $glob_pattern;" \
'test -d "$i"; and string match -qv -- "*/" $i; and set -- i $i/;' \
'string join0 -- $i; end'
set -- result (string escape -n -- ($fzf_cmd --read0 --print0 --scheme=path --no-multi-line | string split0))
# Command completion
else
# Call custom function if defined
set -l -- custom_func _fzf_complete_$cmd[1]
if functions -q $custom_func; and not set -q __fzf_no_custom_complete
set -lx __fzf_no_custom_complete
$custom_func $cmd
return
end
# Workaround for complete not having newlines in results
if string match -qr -- '\\n' $token
set -- token (string replace -ra -- '(?<!\\\\)(?:\\\\\\\\)*\\K\\\\\$' '\\\\\\\\\$' $token | string collect)
set -- token (string unescape -- $token | string collect)
set -- token (string replace -ra -- '\\n' '\\\\n' $token | string collect)
end
set -- list (complete -C --escape -- (string join -- ' ' (commandline -pc $cl_tokenize_opt) $token | string collect))
if test -n "$list"
# Get the initial tabstop value
if set -l -- tabstop (string match -rga -- '--tabstop[= ](?:0*)([1-9]\\d+|[4-9])' "$FZF_DEFAULT_OPTS")[-1]
set -- tabstop (math $tabstop - 4)
else else
set -- tabstop 4 break
end
end
set -e -- tokens[0..$var_count]
while true
switch "$tokens[1]"
case builtin command
set -e -- tokens[1]
test "$tokens[1]" = "--"; and set -e -- tokens[1]
case env
set -e -- tokens[1]
test "$tokens[1]" = "--"; and set -e -- tokens[1]
while string match -qr -- '^[\w]+=' "$tokens[1]"
set -e -- tokens[1]
end
case '*'
break
end
end
string escape -n -- $tokens
end
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
set -l fzf_query ''
set -l prefix ''
set -l dir '.'
set -l -- fish_major (string match -r -- '^\d+' $version)
set -l -- fish_minor (string match -r -- '^\d+\.(\d+)' $version)[2]
set -l -- match_regex '(?<fzf_query>[\s\S]*?(?=\n?$)$)'
set -l -- prefix_regex '^-[^\s=]+=|^-(?!-)\S'
if test "$fish_major" -eq 3 -a "$fish_minor" -lt 3
or string match -q -v -- '* -- *' (string sub -l (commandline -Cp) -- (commandline -p))
set -- match_regex "(?<prefix>$prefix_regex)?$match_regex"
end
if test "$fish_major" -ge 4
string match -q -r -- $match_regex (commandline --current-token --tokens-expanded | string collect -N)
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
string match -q -r -- $match_regex (commandline --current-token --tokenize | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)' '')
else
set -l -- cl_token (commandline --current-token --tokenize | string collect -N)
set -- prefix (string match -r -- $prefix_regex $cl_token)
set -- fzf_query (string replace -- "$prefix" '' $cl_token | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)|\\\n\\\n$' '')
end
if test -n "$fzf_query"
if test \( "$fish_major" -ge 4 \) -o \( "$fish_major" -eq 3 -a "$fish_minor" -ge 5 \)
set -- fzf_query (path normalize -- $fzf_query)
set -- dir $fzf_query
while not path is -d $dir
set -- dir (path dirname $dir)
end
else
if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
string match -q -r -- '(?<fzf_query>^[\s\S]*?(?=\n?$)$)' \
(string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
else
set -- fzf_query (string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r '\\\n$' '')
end
set -- dir $fzf_query
while not test -d "$dir"
set -- dir (dirname -z -- "$dir" | string split0)
end
end end
# Determine the tabstop length for description alignment if not string match -q -- '.' $dir; or string match -q -r -- '^\./|^\.$' $fzf_query
set -l -- max_columns (math $COLUMNS - 40) if test "$fish_major" -ge 4
for i in $list[1..500] string match -q -r -- '^'(string escape --style=regex -- $dir)'/?(?<fzf_query>[\s\S]*)' $fzf_query
set -l -- item (string split -f 1 -- \t $i) else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
and set -l -- len (string length -V -- $item) string match -q -r -- '^/?(?<fzf_query>[\s\S]*?(?=\n?$)$)' \
and test "$len" -gt "$tabstop" -a "$len" -lt "$max_columns" (string replace -- "$dir" '' $fzf_query | string collect -N)
and set -- tabstop $len else
set -- fzf_query (string replace -- "$dir" '' $fzf_query | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^/?|\\\n$' '')
end
end end
set -- tabstop (math $tabstop + 4) end
set -- result (string collect -- $list | $fzf_cmd --delimiter="\t" --tabstop=$tabstop --wrap-sign=\t"↳ " --accept-nth=1) string escape -n -- "$dir" "$fzf_query" "$prefix"
end
#----END INCLUDE
# Use complete builtin for specific commands
function __fzf_complete_native
set -l -- token (commandline -t)
set -l -- completions (eval complete -C \"$argv[1]\")
test -n "$completions"; or begin commandline -f repaint; return; end
# Calculate tabstop based on longest completion item (sample first 500 for performance)
set -l -- tabstop 20
set -l -- sample_size (math "min(500, "(count $completions)")")
for c in $completions[1..$sample_size]
set -l -- len (string length -V -- (string split -- \t $c))
test -n "$len[2]" -a "$len[1]" -gt "$tabstop"
and set -- tabstop $len[1]
end
# limit to 120 to prevent long lines
set -- tabstop (math "min($tabstop + 4, 120)")
set -l result
set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults \
"--reverse --delimiter=\\t --nth=1 --tabstop=$tabstop --color=fg:dim,nth:regular" \
$FZF_COMPLETION_OPTS $argv[2..-1] --accept-nth=1 --read0 --print0)
set -- result (string join0 -- $completions | eval (__fzfcmd) | string split0)
and begin
set -l -- tail ' '
# Append / to bare ~username results (fish omits it unlike other shells)
set -- result (string replace -r -- '^(~\w+)\s?$' '$1/' $result)
# Don't add trailing space if single result is a directory
test (count $result) -eq 1
and string match -q -- '*/' "$result"; and set -- tail ''
set -l -- result (string escape -n -- $result)
string match -q -- '~*' "$token"
and set result (string replace -r -- '^\\\\~' '~' $result)
string match -q -- '$*' "$token"
and set result (string replace -r -- '^\\\\\$' '\$' $result)
commandline -rt -- (string join ' ' -- $result)$tail
end
commandline -f repaint
end
function _fzf_complete
set -l -- args (string escape -- $argv | string join ' ' | string split -- ' -- ')
set -l -- post_func (status function)_(string split -- ' ' $args[2])[1]_post
set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults --reverse $FZF_COMPLETION_OPTS $args[1])
set -lx FZF_DEFAULT_OPTS_FILE
set -lx FZF_DEFAULT_COMMAND
set -l -- fzf_query (commandline -t | string escape)
set -l result
eval (__fzfcmd) --query=$fzf_query | while read -l r; set -a -- result $r; end
and if functions -q $post_func
commandline -rt -- (string collect -- $result | eval $post_func $args[2] | string join ' ')' '
else
commandline -rt -- (string join -- ' ' (string escape -- $result))' '
end
commandline -f repaint
end
# Kill completion (process selection)
function _fzf_complete_kill
set -l -- fzf_query (commandline -t | string escape)
set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults --reverse $FZF_COMPLETION_OPTS \
--accept-nth=2 -m --header-lines=1 --no-preview --wrap)
set -lx FZF_DEFAULT_OPTS_FILE
if type -q ps
set -l -- ps_cmd 'begin command ps -eo user,pid,ppid,start,time,command 2>/dev/null;' \
'or command ps -eo user,pid,ppid,time,args 2>/dev/null;' \
'or command ps --everyone --full --windows 2>/dev/null; end'
set -l -- result (eval $ps_cmd \| (__fzfcmd) --query=$fzf_query)
and commandline -rt -- (string join ' ' -- $result)" "
else
__fzf_complete_native "kill " --multi --query=$fzf_query
end
commandline -f repaint
end
# Main completion function
function fzf-completion
set -l -- tokens (__fzf_cmd_tokens)
set -l -- current_token (commandline -t)
set -l -- cmd_name $tokens[1]
# Route to appropriate completion function
if test -n "$tokens"; and functions -q _fzf_complete_$cmd_name
_fzf_complete_$cmd_name $tokens
else
set -l -- fzf_opt --query=$current_token --multi
__fzf_complete_native "$tokens $current_token" $fzf_opt
end end
end end
# Update command line # Bind Shift-Tab to fzf-completion (Tab retains native Fish behavior)
if test -n "$result" if test (string match -r -- '^\d+' $version) -ge 4
# No extra space after single selection that ends with path separator bind shift-tab fzf-completion
set -l -- tail ' ' bind -M insert shift-tab fzf-completion
test (count $result) -eq 1 else
and string match -q -- '*/' "$result" bind -k btab fzf-completion
and set -- tail '' bind -M insert -k btab fzf-completion
commandline -rt -- (string join -- ' ' $result)$tail
end end
commandline -f repaint
end end
function _fzf_complete # Run setup
set -l fzf_args fzf_completion_setup
for i in $argv
string match -q -- '--' $i; and break
set -a -- fzf_args $i
end
fzf_complete $fzf_args
end
# Bind to shift-tab
if string match -qr -- '^\\d\\d+|^[4-9]' $version
bind shift-tab fzf_complete
bind -M insert shift-tab fzf_complete
else
bind -k btab fzf_complete
bind -M insert -k btab fzf_complete
end

View File

@@ -4,6 +4,8 @@
# / __/ / /_/ __/ # / __/ / /_/ __/
# /_/ /___/_/ completion.zsh # /_/ /___/_/ completion.zsh
# #
# - $FZF_TMUX (default: 0)
# - $FZF_TMUX_OPTS (default: empty)
# - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty) # - $FZF_COMPLETION_OPTS (default: empty)
# - $FZF_COMPLETION_PATH_OPTS (default: empty) # - $FZF_COMPLETION_PATH_OPTS (default: empty)

View File

@@ -4,6 +4,7 @@
# / __/ / /_/ __/ # / __/ / /_/ __/
# /_/ /___/_/ key-bindings.bash # /_/ /___/_/ key-bindings.bash
# #
# - $FZF_TMUX_OPTS
# - $FZF_CTRL_T_COMMAND # - $FZF_CTRL_T_COMMAND
# - $FZF_CTRL_T_OPTS # - $FZF_CTRL_T_OPTS
# - $FZF_CTRL_R_COMMAND # - $FZF_CTRL_R_COMMAND
@@ -84,10 +85,6 @@ __fzf_history_delete() {
for offset in "${offsets[@]}"; do for offset in "${offsets[@]}"; do
builtin history -d "$offset" builtin history -d "$offset"
done done
if [[ ${#offsets[@]} -gt 0 ]] && shopt -q histappend; then
builtin history -w
fi
} }
if command -v perl > /dev/null; then if command -v perl > /dev/null; then

View File

@@ -4,6 +4,7 @@
# / __/ / /_/ __/ # / __/ / /_/ __/
# /_/ /___/_/ key-bindings.fish # /_/ /___/_/ key-bindings.fish
# #
# - $FZF_TMUX_OPTS
# - $FZF_CTRL_T_COMMAND # - $FZF_CTRL_T_COMMAND
# - $FZF_CTRL_T_OPTS # - $FZF_CTRL_T_OPTS
# - $FZF_CTRL_R_COMMAND # - $FZF_CTRL_R_COMMAND
@@ -11,29 +12,35 @@
# - $FZF_ALT_C_COMMAND # - $FZF_ALT_C_COMMAND
# - $FZF_ALT_C_OPTS # - $FZF_ALT_C_OPTS
# Key bindings # Key bindings
# ------------ # ------------
# The oldest supported fish version is 3.1b1. To maintain compatibility, the
# command substitution syntax $(cmd) should never be used, even behind a version
# check, otherwise the source command will fail on fish versions older than 3.4.0.
function fzf_key_bindings function fzf_key_bindings
# The oldest supported fish version is 3.4.0. For this message being able to be # Check fish version
# displayed on older versions, the command substitution syntax $() should not if set -l -- fish_ver (string match -r '^(\d+)\.(\d+)' $version 2>/dev/null)
# be used anywhere in the script, otherwise the source command will fail. and test "$fish_ver[2]" -lt 3 -o "$fish_ver[2]" -eq 3 -a "$fish_ver[3]" -lt 1
if string match -qr -- '^[12]\\.|^3\\.[0-3]' $version echo "This script requires fish version 3.1b1 or newer." >&2
echo "fzf key bindings script requires fish version 3.4.0 or newer." >&2
return 1 return 1
else if not command -q fzf else if not type -q fzf
echo "fzf was not found in path." >&2 echo "fzf was not found in path." >&2
return 1 return 1
end end
#----BEGIN INCLUDE common.fish
# NOTE: Do not directly edit this section, which is copied from "common.fish".
# To modify it, one can edit "common.fish" and run "./update.sh" to apply
# the changes. See code comments in "common.fish" for the implementation details.
function __fzf_defaults function __fzf_defaults
# $argv[1]: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $argv[2..]: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40% test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
string join ' ' -- \ string join ' ' -- \
"--height $FZF_TMUX_HEIGHT --min-height=20+ --bind=ctrl-z:ignore" $argv[1] \ "--height $FZF_TMUX_HEIGHT --min-height=20+ --bind=ctrl-z:ignore" $argv[1] \
(test -r "$FZF_DEFAULT_OPTS_FILE"; and string join -- ' ' <$FZF_DEFAULT_OPTS_FILE) \ (test -r "$FZF_DEFAULT_OPTS_FILE"; and string join -- ' ' <$FZF_DEFAULT_OPTS_FILE) \
$FZF_DEFAULT_OPTS $argv[2..] $FZF_DEFAULT_OPTS $argv[2..-1]
end end
function __fzfcmd function __fzfcmd
@@ -47,59 +54,107 @@ function fzf_key_bindings
end end
end end
function __fzf_cmd_tokens -d 'Return command line tokens, skipping leading env assignments and command prefixes'
set -l tokens
if test (string match -r -- '^\d+' $version) -ge 4
set -- tokens (commandline -xpc)
else
set -- tokens (commandline -opc)
end
set -l -- var_count 0
for i in $tokens
if string match -qr -- '^[\w]+=' $i
set var_count (math $var_count + 1)
else
break
end
end
set -e -- tokens[0..$var_count]
while true
switch "$tokens[1]"
case builtin command
set -e -- tokens[1]
test "$tokens[1]" = "--"; and set -e -- tokens[1]
case env
set -e -- tokens[1]
test "$tokens[1]" = "--"; and set -e -- tokens[1]
while string match -qr -- '^[\w]+=' "$tokens[1]"
set -e -- tokens[1]
end
case '*'
break
end
end
string escape -n -- $tokens
end
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix' function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
set -l fzf_query '' set -l fzf_query ''
set -l prefix '' set -l prefix ''
set -l dir '.' set -l dir '.'
set -l -- match_regex '(?<fzf_query>[\\s\\S]*?(?=\\n?$)$)' set -l -- fish_major (string match -r -- '^\d+' $version)
set -l -- prefix_regex '^-[^\\s=]+=|^-(?!-)\\S' set -l -- fish_minor (string match -r -- '^\d+\.(\d+)' $version)[2]
# Don't use option prefix if " -- " is preceded. set -l -- match_regex '(?<fzf_query>[\s\S]*?(?=\n?$)$)'
string match -qv -- '* -- *' (string sub -l (commandline -Cp) -- (commandline -p)) set -l -- prefix_regex '^-[^\s=]+=|^-(?!-)\S'
and set -- match_regex "(?<prefix>$prefix_regex)?$match_regex" if test "$fish_major" -eq 3 -a "$fish_minor" -lt 3
or string match -q -v -- '* -- *' (string sub -l (commandline -Cp) -- (commandline -p))
set -- match_regex "(?<prefix>$prefix_regex)?$match_regex"
end
# Set $prefix and expanded $fzf_query with preserved trailing newlines. if test "$fish_major" -ge 4
if string match -qr -- '^\\d\\d+|^[4-9]' $version
# fish v4.0.0 and newer
string match -q -r -- $match_regex (commandline --current-token --tokens-expanded | string collect -N) string match -q -r -- $match_regex (commandline --current-token --tokens-expanded | string collect -N)
else else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
string match -q -r -- $match_regex (commandline --current-token --tokenize | string collect -N) string match -q -r -- $match_regex (commandline --current-token --tokenize | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\\(?=~)|\\\\(?=\\$\\w)' '') eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)' '')
else
set -l -- cl_token (commandline --current-token --tokenize | string collect -N)
set -- prefix (string match -r -- $prefix_regex $cl_token)
set -- fzf_query (string replace -- "$prefix" '' $cl_token | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)|\\\n\\\n$' '')
end end
if test -n "$fzf_query" if test -n "$fzf_query"
# Normalize path in $fzf_query, set $dir to the longest existing directory. if test \( "$fish_major" -ge 4 \) -o \( "$fish_major" -eq 3 -a "$fish_minor" -ge 5 \)
if string match -qr -- '^\\d\\d+|^4|^3\\.[5-9]' $version
# fish v3.5.0 and newer
set -- fzf_query (path normalize -- $fzf_query) set -- fzf_query (path normalize -- $fzf_query)
set -- dir $fzf_query set -- dir $fzf_query
while not path is -d $dir while not path is -d $dir
set -- dir (path dirname $dir) set -- dir (path dirname $dir)
end end
else else
string match -q -r -- '(?<fzf_query>^[\\s\\S]*?(?=\\n?$)$)' \ if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
(string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\\n)$' '' $fzf_query | string collect -N) string match -q -r -- '(?<fzf_query>^[\s\S]*?(?=\n?$)$)' \
(string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
else
set -- fzf_query (string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r '\\\n$' '')
end
set -- dir $fzf_query set -- dir $fzf_query
while not test -d "$dir" while not test -d "$dir"
set -- dir (dirname -z -- "$dir" | string split0) set -- dir (dirname -z -- "$dir" | string split0)
end end
end end
if not string match -q -- '.' $dir; or string match -qr -- '^\\.(/|$)' $fzf_query if not string match -q -- '.' $dir; or string match -q -r -- '^\./|^\.$' $fzf_query
# Strip $dir from $fzf_query - preserve trailing newlines. if test "$fish_major" -ge 4
if string match -qr -- '^\\d\\d+|^[4-9]' $version string match -q -r -- '^'(string escape --style=regex -- $dir)'/?(?<fzf_query>[\s\S]*)' $fzf_query
# fish v4.0.0 and newer else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
string match -q -r -- '^'(string escape --style=regex -- $dir)'/?(?<fzf_query>[\\s\\S]*)' $fzf_query string match -q -r -- '^/?(?<fzf_query>[\s\S]*?(?=\n?$)$)' \
else
string match -q -r -- '^/?(?<fzf_query>[\\s\\S]*?(?=\\n?$)$)' \
(string replace -- "$dir" '' $fzf_query | string collect -N) (string replace -- "$dir" '' $fzf_query | string collect -N)
else
set -- fzf_query (string replace -- "$dir" '' $fzf_query | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^/?|\\\n$' '')
end end
end end
end end
string escape -n -- "$dir" "$fzf_query" "$prefix" string escape -n -- "$dir" "$fzf_query" "$prefix"
end end
#----END INCLUDE
# Store current token in $dir as root for the 'find' command # Store current token in $dir as root for the 'find' command
function fzf-file-widget -d "List files and folders" function fzf-file-widget -d "List files and folders"
@@ -116,7 +171,7 @@ function fzf_key_bindings
set -lx FZF_DEFAULT_OPTS_FILE set -lx FZF_DEFAULT_OPTS_FILE
set -l result (eval (__fzfcmd) --walker-root=$dir --query=$fzf_query | string split0) set -l result (eval (__fzfcmd) --walker-root=$dir --query=$fzf_query | string split0)
and commandline -rt -- (string join -- ' ' $prefix(string escape -n -- $result))' ' and commandline -rt -- (string join -- ' ' $prefix(string escape --no-quoted -- $result))' '
commandline -f repaint commandline -f repaint
end end
@@ -128,33 +183,45 @@ function fzf_key_bindings
set -l -- fzf_query (string escape -- $command_line[$current_line]) set -l -- fzf_query (string escape -- $command_line[$current_line])
set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults '' \ set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults '' \
'--with-nth=2.. --nth=2..,.. --scheme=history --multi --no-multi-line' \ '--nth=2..,.. --scheme=history --multi --no-multi-line --no-wrap --wrap-sign="\t\t\t↳ " --preview-wrap-sign="↳ "' \
'--no-wrap --wrap-sign="\t\t\t↳ " --preview-wrap-sign="↳ " --freeze-left=1' \ '--bind=\'shift-delete:execute-silent(for i in (string split0 -- <{+f}); eval builtin history delete --exact --case-sensitive -- (string escape -n -- $i | string replace -r "^\d*\\\\\\t" ""); end)+reload(eval $FZF_DEFAULT_COMMAND)\'' \
'--bind="alt-enter:become(set -g fzf_temp {+sf3..}; string join0 -- (string split0 -- <$fzf_temp | fish_indent -i); unlink $fzf_temp &>/dev/null)"' \ '--bind="alt-enter:become(string join0 -- (string collect -- {+2..} | fish_indent -i))"' \
'--bind="alt-t:change-with-nth(1,3..|3..|2..)"' \
'--bind="shift-delete:execute-silent(eval builtin history delete -Ce -- (string escape -n -- (string split0 -- <{+sf3..})))+reload(eval $FZF_DEFAULT_COMMAND)"' \
"--bind=ctrl-r:toggle-sort,alt-r:toggle-raw --highlight-line $FZF_CTRL_R_OPTS" \ "--bind=ctrl-r:toggle-sort,alt-r:toggle-raw --highlight-line $FZF_CTRL_R_OPTS" \
'--accept-nth=3.. --delimiter="\t" --tabstop=4 --read0 --print0 --with-shell='(status fish-path)\\ -c) '--accept-nth=2.. --delimiter="\t" --tabstop=4 --read0 --print0 --with-shell='(status fish-path)\\ -c)
# Add dynamic preview options if preview command isn't already set by user # Add dynamic preview options if preview command isn't already set by user
if string match -qvr -- '--preview[= ]' "$FZF_DEFAULT_OPTS" if string match -qvr -- '--preview[= ]' "$FZF_DEFAULT_OPTS"
# Prepend the options to allow user overrides # Convert the highlighted timestamp using the date command if available
set -l -- date_cmd '{1}'
if type -q date
if date -d @0 '+%s' 2>/dev/null | string match -q 0
# GNU date
set -- date_cmd '(date -d @{1} \\"+%F %a %T\\")'
else if date -r 0 '+%s' 2>/dev/null | string match -q 0
# BSD date
set -- date_cmd '(date -r {1} \\"+%F %a %T\\")'
end
end
# Prepend the options to allow user customizations
set -p -- FZF_DEFAULT_OPTS \ set -p -- FZF_DEFAULT_OPTS \
'--bind="focus,multi,resize:bg-transform:if test \\"$FZF_COLUMNS\\" -gt 100 -a \\\\( \\"$FZF_SELECT_COUNT\\" -gt 0 -o \\\\( -z \\"$FZF_WRAP\\" -a (string join0 -- <{f3..} | string length) -gt (math $FZF_COLUMNS - (switch $FZF_WITH_NTH; case 2..; echo 13; case 1,3..; echo 25; case 3..; echo 1; end)) \\\\) -o (string split0 -- <{sf3..} | fish_indent | count) -gt 1 \\\\); echo show-preview; else; echo hide-preview; end"' \ '--bind="focus,resize:bg-transform:if test \\"$FZF_COLUMNS\\" -gt 100 -a \\\\( \\"$FZF_SELECT_COUNT\\" -gt 0 -o \\\\( -z \\"$FZF_WRAP\\" -a (string length -- {}) -gt (math $FZF_COLUMNS - 4) \\\\) -o (string collect -- {2..} | fish_indent | count) -gt 1 \\\\); echo show-preview; else echo hide-preview; end"' \
'--preview="test \\"$FZF_SELECT_COUNT\\" -gt 0; and string split0 -- <{+sf3..} | fish_indent (string match -q -- 3.\\\\* $version; or echo -- --only-indent) --ansi; and echo -n \\\\n; string collect -- \\\\#\\\\ {1} (string split0 -- <{sf3..}) | fish_indent --ansi"' \ '--preview="string collect -- (test \\"$FZF_SELECT_COUNT\\" -gt 0; and string collect -- {+2..}) \\"\\n# \\"'$date_cmd' {2..} | fish_indent --ansi"' \
'--preview-window="right,50%,wrap-word,follow,info,hidden"' '--preview-window="right,50%,wrap-word,follow,info,hidden"'
end end
set -lx FZF_DEFAULT_OPTS_FILE set -lx FZF_DEFAULT_OPTS_FILE
set -lx -- FZF_DEFAULT_COMMAND 'builtin history -z' set -lx -- FZF_DEFAULT_COMMAND 'builtin history -z --show-time="%s%t"'
# Enable syntax highlighting colors on fish v4.3.3 and newer # Enable syntax highlighting colors on fish v4.3.3 and newer
if string match -qr -- '^\\d\\d+|^4\\.[4-9]|^4\\.3\\.[3-9]' $version if set -l -- v (string match -r -- '^(\d+)\.(\d+)(?:\.(\d+))?' $version)
and test "$v[2]" -gt 4 -o "$v[2]" -eq 4 -a \
\( "$v[3]" -gt 3 -o "$v[3]" -eq 3 -a \
\( -n "$v[4]" -a "$v[4]" -ge 3 \) \)
set -a -- FZF_DEFAULT_OPTS '--ansi' set -a -- FZF_DEFAULT_OPTS '--ansi'
set -a -- FZF_DEFAULT_COMMAND '--color=always --show-time=(set_color $fish_color_comment)"%F %a %T%t%s%t"(set_color $fish_color_normal)' set -a -- FZF_DEFAULT_COMMAND '--color=always'
else
set -a -- FZF_DEFAULT_COMMAND '--show-time="%F %a %T%t%s%t"'
end end
# Merge history from other sessions before searching # Merge history from other sessions before searching

View File

@@ -4,6 +4,7 @@
# / __/ / /_/ __/ # / __/ / /_/ __/
# /_/ /___/_/ key-bindings.zsh # /_/ /___/_/ key-bindings.zsh
# #
# - $FZF_TMUX_OPTS
# - $FZF_CTRL_T_COMMAND # - $FZF_CTRL_T_COMMAND
# - $FZF_CTRL_T_OPTS # - $FZF_CTRL_T_OPTS
# - $FZF_CTRL_R_COMMAND # - $FZF_CTRL_R_COMMAND

View File

@@ -8,24 +8,26 @@ dir=${0%"${0##*/}"}
update() { update() {
{ {
sed -n '1,/^#----BEGIN INCLUDE common\.sh/p' "$1" sed -n "1,/^#----BEGIN INCLUDE $1/p" "$2"
cat << EOF cat << EOF
# NOTE: Do not directly edit this section, which is copied from "common.sh". # NOTE: Do not directly edit this section, which is copied from "$1".
# To modify it, one can edit "common.sh" and run "./update.sh" to apply # To modify it, one can edit "$1" and run "./update.sh" to apply
# the changes. See code comments in "common.sh" for the implementation details. # the changes. See code comments in "$1" for the implementation details.
EOF EOF
echo echo
grep -v '^[[:blank:]]*#' "$dir/common.sh" # remove code comments in common.sh grep -v '^[[:blank:]]*#' "$dir/$1" # remove code comments from the common file
sed -n '/^#----END INCLUDE/,$p' "$1" sed -n '/^#----END INCLUDE/,$p' "$2"
} > "$1.part" } > "$2.part"
mv -f "$1.part" "$1" mv -f "$2.part" "$2"
} }
update "$dir/completion.bash" update "common.sh" "$dir/completion.bash"
update "$dir/completion.zsh" update "common.sh" "$dir/completion.zsh"
update "$dir/key-bindings.bash" update "common.sh" "$dir/key-bindings.bash"
update "$dir/key-bindings.zsh" update "common.sh" "$dir/key-bindings.zsh"
update "common.fish" "$dir/completion.fish"
update "common.fish" "$dir/key-bindings.fish"
# Check if --check is in ARGV # Check if --check is in ARGV
check=0 check=0

View File

@@ -2,10 +2,10 @@
## What these functions do ## What these functions do
`indexByteTwo(s []byte, b1, b2 byte) int` -- returns the index of the `indexByteTwo(s []byte, b1, b2 byte) int` returns the index of the
**first** occurrence of `b1` or `b2` in `s`, or `-1`. **first** occurrence of `b1` or `b2` in `s`, or `-1`.
`lastIndexByteTwo(s []byte, b1, b2 byte) int` -- returns the index of the `lastIndexByteTwo(s []byte, b1, b2 byte) int` returns the index of the
**last** occurrence of `b1` or `b2` in `s`, or `-1`. **last** occurrence of `b1` or `b2` in `s`, or `-1`.
They are used by the fuzzy matching algorithm (`algo.go`) to skip ahead They are used by the fuzzy matching algorithm (`algo.go`) to skip ahead
@@ -91,9 +91,9 @@ implementations (`2xIndexByte` using `bytes.IndexByte`, and a simple `loop`).
The assembly is verified by three layers of testing: The assembly is verified by three layers of testing:
1. **Table-driven tests** -- known inputs with expected outputs. 1. **Table-driven tests** known inputs with expected outputs.
2. **Exhaustive tests** -- all lengths 0256, every match position, no-match 2. **Exhaustive tests** all lengths 0256, every match position, no-match
cases, and both-bytes-present cases, compared against a simple loop cases, and both-bytes-present cases, compared against a simple loop
reference. reference.
3. **Fuzz tests** -- randomized inputs via `testing.F`, compared against the 3. **Fuzz tests** randomized inputs via `testing.F`, compared against the
same loop reference. same loop reference.

View File

@@ -78,7 +78,7 @@ loop:
CBZ R6, loop CBZ R6, loop
end: end:
// Found something or out of data, build full syndrome // Found something or out of data build full syndrome
VAND V5.B16, V3.B16, V3.B16 VAND V5.B16, V3.B16, V3.B16
VAND V5.B16, V4.B16, V4.B16 VAND V5.B16, V4.B16, V4.B16
VADDP V4.B16, V3.B16, V6.B16 VADDP V4.B16, V3.B16, V6.B16

View File

@@ -484,7 +484,7 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
state.attr = state.attr | tui.Italic state.attr = state.attr | tui.Italic
case 4: case 4:
if sep == ':' { if sep == ':' {
// SGR 4:N - underline style sub-parameter // SGR 4:N underline style sub-parameter
var subNum int var subNum int
subNum, _, ansiCode = parseAnsiCode(ansiCode) subNum, _, ansiCode = parseAnsiCode(ansiCode)
state.attr = state.attr &^ tui.UnderlineStyleMask state.attr = state.attr &^ tui.UnderlineStyleMask

View File

@@ -66,7 +66,7 @@ Usage: fzf [options]
--no-bold Do not use bold text --no-bold Do not use bold text
DISPLAY MODE DISPLAY MODE
--height=[~][-]HEIGHT[%] Display fzf window below the cursor with the given --height=[~]HEIGHT[%] Display fzf window below the cursor with the given
height instead of using fullscreen. height instead of using fullscreen.
A negative value is calculated as the terminal height A negative value is calculated as the terminal height
minus the given value. minus the given value.
@@ -178,11 +178,10 @@ Usage: fzf [options]
--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|horizontal|vertical| [rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
top|bottom|left|right|line|inline|none] (default: rounded) top|bottom|left|right|line|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.
Pass 'none' to still separate it but without a border. Pass 'none' to still separate it but without a border.
Pass 'inline' to embed it inside the list frame.
--header-label=LABEL Label to print on the header border --header-label=LABEL Label to print on the header border
--header-label-pos=COL Position of the header label --header-label-pos=COL Position of the header label
[POSITIVE_INTEGER: columns from left| [POSITIVE_INTEGER: columns from left|
@@ -193,7 +192,7 @@ Usage: fzf [options]
--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|horizontal|vertical| [rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
top|bottom|left|right|line|inline|none] (default: line) top|bottom|left|right|line|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
[POSITIVE_INTEGER: columns from left| [POSITIVE_INTEGER: columns from left|
@@ -871,7 +870,7 @@ func nthTransformer(str string) (func(Delimiter) func([]Token, int32) string, er
nth []Range nth []Range
} }
parts := make([]NthParts, 0, len(indexes)) parts := make([]NthParts, len(indexes))
idx := 0 idx := 0
for _, index := range indexes { for _, index := range indexes {
if idx < index[0] { if idx < index[0] {
@@ -954,8 +953,6 @@ func parseBorder(str string, optional bool) (tui.BorderShape, error) {
switch str { switch str {
case "line": case "line":
return tui.BorderLine, nil return tui.BorderLine, nil
case "inline":
return tui.BorderInline, nil
case "rounded": case "rounded":
return tui.BorderRounded, nil return tui.BorderRounded, nil
case "sharp": case "sharp":
@@ -986,7 +983,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|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|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) {
@@ -2225,6 +2222,9 @@ func parseHeight(str string, index int) (heightSpec, error) {
str = str[1:] str = str[1:]
} }
if strings.HasPrefix(str, "-") { if strings.HasPrefix(str, "-") {
if heightSpec.auto {
return heightSpec, errors.New("negative(-) height is not compatible with adaptive(~) height")
}
heightSpec.inverse = true heightSpec.inverse = true
str = str[1:] str = str[1:]
} }
@@ -3152,7 +3152,7 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
} }
opts.PreviewWrapSign = &str opts.PreviewWrapSign = &str
case "--height": case "--height":
str, err := nextString("height required: [~][-]HEIGHT[%]") str, err := nextString("height required: [~]HEIGHT[%]")
if err != nil { if err != nil {
return err return err
} }
@@ -3519,9 +3519,7 @@ func applyPreset(opts *Options, preset string) error {
opts.Preview.border = tui.BorderLine opts.Preview.border = tui.BorderLine
opts.Preview.info = false opts.Preview.info = false
opts.InfoStyle = infoDefault opts.InfoStyle = infoDefault
opts.Theme.Gutter = tui.NewColorAttr() opts.Theme.Gutter = tui.ColorAttr{Color: -1, Attr: 0}
space := " "
opts.Gutter = &space
empty := "" empty := ""
opts.Separator = &empty opts.Separator = &empty
opts.Scrollbar = &empty opts.Scrollbar = &empty
@@ -3596,7 +3594,7 @@ func validateOptions(opts *Options) error {
} }
} }
if opts.Height.auto && (opts.Tmux == nil || opts.Tmux.index < opts.Height.index) { if opts.Height.auto {
for _, s := range []sizeSpec{opts.Margin[0], opts.Margin[2]} { for _, s := range []sizeSpec{opts.Margin[0], opts.Margin[2]} {
if s.percent { if s.percent {
return errors.New("adaptive height is not compatible with top/bottom percent margin") return errors.New("adaptive height is not compatible with top/bottom percent margin")
@@ -3613,19 +3611,6 @@ func validateOptions(opts *Options) error {
return errors.New("only ANSI attributes are allowed for 'nth' (regular, bold, underline, reverse, dim, italic, strikethrough)") return errors.New("only ANSI attributes are allowed for 'nth' (regular, bold, underline, reverse, dim, italic, strikethrough)")
} }
if opts.BorderShape == tui.BorderInline ||
opts.ListBorderShape == tui.BorderInline ||
opts.InputBorderShape == tui.BorderInline ||
opts.Preview.border == tui.BorderInline {
return errors.New("inline border is only supported for --header-border, --header-lines-border, and --footer-border")
}
if opts.HeaderBorderShape == tui.BorderInline &&
opts.HeaderLinesShape != tui.BorderInline &&
opts.HeaderLinesShape != tui.BorderUndefined &&
opts.HeaderLinesShape != tui.BorderNone {
return errors.New("--header-border=inline requires --header-lines-border to be inline or unset")
}
return nil return nil
} }

View File

@@ -2,7 +2,6 @@ package fzf
import ( import (
"math" "math"
"slices"
"sort" "sort"
"unicode" "unicode"
@@ -31,7 +30,7 @@ type Result struct {
func buildResult(item *Item, offsets []Offset, score int) Result { func buildResult(item *Item, offsets []Offset, score int) Result {
if len(offsets) > 1 { if len(offsets) > 1 {
slices.SortFunc(offsets, compareOffsets) sort.Sort(ByOrder(offsets))
} }
minBegin := math.MaxUint16 minBegin := math.MaxUint16
@@ -188,7 +187,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
} }
} }
// slices.SortFunc(offsets, compareOffsets) // sort.Sort(ByOrder(offsets))
// Merge offsets // Merge offsets
// ------------ ---- -- ---- // ------------ ---- -- ----
@@ -298,20 +297,21 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
return colors return colors
} }
func compareOffsets(a, b Offset) int { // ByOrder is for sorting substring offsets
if a[0] < b[0] { type ByOrder []Offset
return -1
} func (a ByOrder) Len() int {
if a[0] > b[0] { return len(a)
return 1 }
}
if a[1] < b[1] { func (a ByOrder) Swap(i, j int) {
return -1 a[i], a[j] = a[j], a[i]
} }
if a[1] > b[1] {
return 1 func (a ByOrder) Less(i, j int) bool {
} ioff := a[i]
return 0 joff := a[j]
return (ioff[0] < joff[0]) || (ioff[0] == joff[0]) && (ioff[1] <= joff[1])
} }
// ByRelevance is for sorting Items // ByRelevance is for sorting Items

View File

@@ -3,7 +3,6 @@ package fzf
import ( import (
"math" "math"
"math/rand" "math/rand"
"slices"
"sort" "sort"
"testing" "testing"
@@ -20,7 +19,7 @@ func TestOffsetSort(t *testing.T) {
offsets := []Offset{ offsets := []Offset{
{3, 5}, {2, 7}, {3, 5}, {2, 7},
{1, 3}, {2, 9}} {1, 3}, {2, 9}}
slices.SortFunc(offsets, compareOffsets) sort.Sort(ByOrder(offsets))
if offsets[0][0] != 1 || offsets[0][1] != 3 || if offsets[0][0] != 1 || offsets[0][1] != 3 ||
offsets[1][0] != 2 || offsets[1][1] != 7 || offsets[1][0] != 2 || offsets[1][1] != 7 ||

View File

@@ -122,12 +122,13 @@ func startHttpServer(address listenAddress, actionChannel chan []*action, getHan
} }
} }
server := httpServer{
apiKey: []byte(apiKey),
actionChannel: actionChannel,
getHandler: getHandler,
}
go func() { go func() {
server := httpServer{
apiKey: []byte(apiKey),
actionChannel: actionChannel,
getHandler: getHandler,
}
for { for {
conn, err := listener.Accept() conn, err := listener.Accept()
if err != nil { if err != nil {

View File

@@ -13,7 +13,6 @@ import (
"os/exec" "os/exec"
"os/signal" "os/signal"
"regexp" "regexp"
"slices"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@@ -1167,15 +1166,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
if baseTheme == nil { if baseTheme == nil {
baseTheme = renderer.DefaultTheme() baseTheme = renderer.DefaultTheme()
} }
// If the list border can't host an inline separator, the runtime later coerces
// BorderInline to BorderLine. Mirror that here so theme color inheritance matches
// the final rendering rather than the user's requested shape.
inlineListSupported := opts.ListBorderShape.HasTop() && opts.ListBorderShape.HasBottom()
headerInline := inlineListSupported && (opts.HeaderBorderShape == tui.BorderInline || opts.HeaderLinesShape == tui.BorderInline)
footerInline := inlineListSupported && opts.FooterBorderShape == tui.BorderInline
hasHeader := opts.HeaderBorderShape.Visible() || opts.HeaderLinesShape.Visible()
// This should be called before accessing tui.Color* // This should be called before accessing tui.Color*
tui.InitTheme(opts.Theme, baseTheme, opts.Bold, opts.Black, opts.InputBorderShape.Visible(), hasHeader, headerInline, footerInline) tui.InitTheme(opts.Theme, baseTheme, opts.Bold, opts.Black, opts.InputBorderShape.Visible(), opts.HeaderBorderShape.Visible())
// Gutter character // Gutter character
var gutterChar, gutterRawChar string var gutterChar, gutterRawChar string
@@ -1234,22 +1226,6 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
} }
} }
// 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
// horizontal separator (same as BorderLine).
inlineSupported := t.listBorderShape.HasTop() && t.listBorderShape.HasBottom()
if !inlineSupported {
if t.headerBorderShape == tui.BorderInline {
t.headerBorderShape = tui.BorderLine
}
if t.headerLinesShape == tui.BorderInline {
t.headerLinesShape = tui.BorderLine
}
if t.footerBorderShape == tui.BorderInline {
t.footerBorderShape = tui.BorderLine
}
}
// Determine header border shape // Determine header border shape
if t.headerBorderShape == tui.BorderLine { if t.headerBorderShape == tui.BorderLine {
if t.layout == layoutReverse { if t.layout == layoutReverse {
@@ -2260,98 +2236,6 @@ func (t *Terminal) determineHeaderLinesShape() (bool, tui.BorderShape) {
return false, tui.BorderNone return false, tui.BorderNone
} }
// Inline sections live inside wborder rather than consuming shift/shrink/availableLines.
type inlineRole int
const (
inlineRoleHeader inlineRole = iota
inlineRoleHeaderLines
inlineRoleFooter
)
type inlineSlot struct {
role inlineRole
windowType tui.WindowType
contentLines int // 0 when out of budget: a ghost placeholder is created so
// t.headerWindow / t.footerWindow stay non-nil but no frame is painted.
label labelPrinter
labelOpts labelOpts
labelLen int
}
// inlineMetaFor returns (onTop: top stack vs bottom; windowType; isInner:
// adjacent to list content) for the given inline role, derived from the layout.
// Header is outer when paired with header-lines; header-lines is always inner;
// footer is inner except in reverseList where it sits outside header-lines.
func (t *Terminal) inlineMetaFor(role inlineRole) (onTop bool, windowType tui.WindowType, isInner bool) {
switch role {
case inlineRoleHeader:
return t.layout == layoutReverse, tui.WindowHeader, false
case inlineRoleHeaderLines:
return t.layout != layoutDefault, tui.WindowHeader, true
case inlineRoleFooter:
return t.layout != layoutReverse, tui.WindowFooter, t.layout != layoutReverseList
}
return false, tui.WindowBase, false
}
func (t *Terminal) placeInlineSection(win tui.Window, role inlineRole) {
switch role {
case inlineRoleHeader:
t.headerWindow = win
case inlineRoleHeaderLines:
t.headerLinesWindow = win
case inlineRoleFooter:
t.footerWindow = win
}
}
// placeInlineStack walks `slots` in outer-to-inner order. Only the outermost
// slot (index 0) claims the adjacent wborder edge; later slots paint side-only
// frames so the T-junction separator lands between sections.
func (t *Terminal) placeInlineStack(slots []inlineSlot, startRow int, onTop bool) {
firstEdge := tui.SectionEdgeTop
if !onTop {
firstEdge = tui.SectionEdgeBottom
}
noBorder := tui.MakeBorderStyle(tui.BorderNone, t.unicode)
subLeft := t.window.Left()
subWidth := t.window.Width()
cursor := startRow
for i, s := range slots {
var windowTop, sepRow int
if onTop {
windowTop = cursor
sepRow = cursor + s.contentLines - t.wborder.Top()
} else {
windowTop = cursor - s.contentLines + 1
sepRow = windowTop - 1 - t.wborder.Top()
}
// 0-height placeholder keeps t.headerWindow / t.footerWindow non-nil.
win := t.tui.NewWindow(windowTop, subLeft, subWidth, s.contentLines, s.windowType, noBorder, true)
t.placeInlineSection(win, s.role)
if s.contentLines == 0 {
continue
}
secTop := windowTop - t.wborder.Top()
secBottom := secTop + s.contentLines - 1
edge := tui.SectionEdgeNone
if i == 0 {
edge = firstEdge
}
t.wborder.PaintSectionFrame(secTop, secBottom, s.windowType, edge)
// useBottom=onTop so the separator always hugs the list (inner side).
// Matters for thinblock/block where top != bottom char.
t.wborder.DrawHSeparator(sepRow, s.windowType, onTop)
t.printLabelAt(t.wborder, s.label, s.labelOpts, s.labelLen, sepRow)
if onTop {
cursor += s.contentLines + 1
} else {
cursor -= s.contentLines + 1
}
}
}
func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) { func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
t.clearNumLinesCache() t.clearNumLinesCache()
t.forcePreview = forcePreview t.forcePreview = forcePreview
@@ -2468,50 +2352,6 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
} }
} }
// Slices are ordered outer-to-inner: index 0 touches wborder's edge.
var inlineTop []inlineSlot
var inlineBottom []inlineSlot
// Caps contentLines against remaining wborder space. Oversized requests (e.g. a
// huge --header-lines) become 0-height placeholders rather than pushing the list
// window to negative height.
addInline := func(contentLines int, role inlineRole) {
onTop, windowType, isInner := t.inlineMetaFor(role)
used := 0
for _, s := range inlineTop {
if s.contentLines > 0 {
used += s.contentLines + 1
}
}
for _, s := range inlineBottom {
if s.contentLines > 0 {
used += s.contentLines + 1
}
}
remaining := availableLines - borderLines(t.listBorderShape) - used - 1
if remaining < 2 {
contentLines = 0
} else {
contentLines = util.Constrain(contentLines, 1, remaining-1)
}
slot := inlineSlot{role: role, windowType: windowType, contentLines: contentLines}
switch role {
case inlineRoleHeader:
slot.label, slot.labelOpts, slot.labelLen = t.headerLabel, t.headerLabelOpts, t.headerLabelLen
case inlineRoleFooter:
slot.label, slot.labelOpts, slot.labelLen = t.footerLabel, t.footerLabelOpts, t.footerLabelLen
}
target := &inlineTop
if !onTop {
target = &inlineBottom
}
if isInner {
*target = append(*target, slot)
} else {
*target = append([]inlineSlot{slot}, *target...)
}
}
// Adjust position and size of the list window if header border is set // Adjust position and size of the list window if header border is set
headerBorderHeight := 0 headerBorderHeight := 0
if hasHeaderWindow { if hasHeaderWindow {
@@ -2519,62 +2359,37 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
if hasHeaderLinesWindow { if hasHeaderLinesWindow {
headerWindowHeight -= t.headerLines headerWindowHeight -= t.headerLines
} }
if t.headerBorderShape == tui.BorderInline { headerBorderHeight = util.Constrain(borderLines(t.headerBorderShape)+headerWindowHeight, 0, availableLines)
addInline(headerWindowHeight, inlineRoleHeader) if t.layout == layoutReverse {
shift += headerBorderHeight
shrink += headerBorderHeight
} else { } else {
headerBorderHeight = util.Constrain(borderLines(t.headerBorderShape)+headerWindowHeight, 0, availableLines) shrink += headerBorderHeight
if t.layout == layoutReverse {
shift += headerBorderHeight
shrink += headerBorderHeight
} else {
shrink += headerBorderHeight
}
availableLines -= headerBorderHeight
} }
availableLines -= headerBorderHeight
} }
headerLinesHeight := 0 headerLinesHeight := 0
if hasHeaderLinesWindow { if hasHeaderLinesWindow {
if headerLinesShape == tui.BorderInline { headerLinesHeight = util.Constrain(borderLines(headerLinesShape)+t.headerLines, 0, availableLines)
addInline(t.headerLines, inlineRoleHeaderLines) if t.layout != layoutDefault {
shift += headerLinesHeight
shrink += headerLinesHeight
} else { } else {
headerLinesHeight = util.Constrain(borderLines(headerLinesShape)+t.headerLines, 0, availableLines) shrink += headerLinesHeight
if t.layout != layoutDefault {
shift += headerLinesHeight
shrink += headerLinesHeight
} else {
shrink += headerLinesHeight
}
availableLines -= headerLinesHeight
} }
availableLines -= headerLinesHeight
} }
footerBorderHeight := 0 footerBorderHeight := 0
if hasFooterWindow { if hasFooterWindow {
if t.footerBorderShape == tui.BorderInline { // Footer lines should not take all available lines
addInline(len(t.footer), inlineRoleFooter) footerBorderHeight = util.Constrain(borderLines(t.footerBorderShape)+len(t.footer), 0, availableLines)
} else { shrink += footerBorderHeight
// Footer lines should not take all available lines if t.layout != layoutReverse {
footerBorderHeight = util.Constrain(borderLines(t.footerBorderShape)+len(t.footer), 0, availableLines) shift += footerBorderHeight
shrink += footerBorderHeight
if t.layout != layoutReverse {
shift += footerBorderHeight
}
availableLines -= footerBorderHeight
}
}
inlineTopLines := 0
for _, s := range inlineTop {
if s.contentLines > 0 {
inlineTopLines += s.contentLines + 1
}
}
inlineBottomLines := 0
for _, s := range inlineBottom {
if s.contentLines > 0 {
inlineBottomLines += s.contentLines + 1
} }
availableLines -= footerBorderHeight
} }
// Set up list border // Set up list border
@@ -2685,12 +2500,12 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
if previewOpts.position == posUp { if previewOpts.position == posUp {
innerBorderFn(marginInt[0]+pheight, marginInt[3], width, height-pheight) innerBorderFn(marginInt[0]+pheight, marginInt[3], width, height-pheight)
t.window = t.tui.NewWindow( t.window = t.tui.NewWindow(
innerMarginInt[0]+pheight+shift+inlineTopLines, innerMarginInt[3], innerWidth, innerHeight-pheight-shrink-inlineTopLines-inlineBottomLines, tui.WindowList, noBorder, true) innerMarginInt[0]+pheight+shift, innerMarginInt[3], innerWidth, innerHeight-pheight-shrink, tui.WindowList, noBorder, true)
createPreviewWindow(marginInt[0], marginInt[3], width, pheight) createPreviewWindow(marginInt[0], marginInt[3], width, pheight)
} else { } else {
innerBorderFn(marginInt[0], marginInt[3], width, height-pheight) innerBorderFn(marginInt[0], marginInt[3], width, height-pheight)
t.window = t.tui.NewWindow( t.window = t.tui.NewWindow(
innerMarginInt[0]+shift+inlineTopLines, innerMarginInt[3], innerWidth, innerHeight-pheight-shrink-inlineTopLines-inlineBottomLines, tui.WindowList, noBorder, true) innerMarginInt[0]+shift, innerMarginInt[3], innerWidth, innerHeight-pheight-shrink, tui.WindowList, noBorder, true)
createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight) createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight)
} }
case posLeft, posRight: case posLeft, posRight:
@@ -2729,7 +2544,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
m = 1 m = 1
} }
t.window = t.tui.NewWindow( t.window = t.tui.NewWindow(
innerMarginInt[0]+shift+inlineTopLines, innerMarginInt[3]+pwidth+m, innerWidth-pwidth-m, innerHeight-shrink-inlineTopLines-inlineBottomLines, tui.WindowList, noBorder, true) innerMarginInt[0]+shift, innerMarginInt[3]+pwidth+m, innerWidth-pwidth-m, innerHeight-shrink, tui.WindowList, noBorder, true)
// Clear characters on the margin // Clear characters on the margin
// fzf --bind 'space:toggle-preview' --preview ':' --preview-window left,1 // fzf --bind 'space:toggle-preview' --preview ':' --preview-window left,1
@@ -2761,7 +2576,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
} }
innerBorderFn(marginInt[0], marginInt[3], width-pwidth, height) innerBorderFn(marginInt[0], marginInt[3], width-pwidth, height)
t.window = t.tui.NewWindow( t.window = t.tui.NewWindow(
innerMarginInt[0]+shift+inlineTopLines, innerMarginInt[3], innerWidth-pwidth, innerHeight-shrink-inlineTopLines-inlineBottomLines, tui.WindowList, noBorder, true) innerMarginInt[0]+shift, innerMarginInt[3], innerWidth-pwidth, innerHeight-shrink, tui.WindowList, noBorder, true)
x := marginInt[3] + width - pwidth x := marginInt[3] + width - pwidth
createPreviewWindow(marginInt[0], x, pwidth, height) createPreviewWindow(marginInt[0], x, pwidth, height)
} }
@@ -2799,15 +2614,10 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
} }
innerBorderFn(marginInt[0], marginInt[3], width, height) innerBorderFn(marginInt[0], marginInt[3], width, height)
t.window = t.tui.NewWindow( t.window = t.tui.NewWindow(
innerMarginInt[0]+shift+inlineTopLines, innerMarginInt[0]+shift,
innerMarginInt[3], innerMarginInt[3],
innerWidth, innerWidth,
innerHeight-shrink-inlineTopLines-inlineBottomLines, tui.WindowList, noBorder, true) innerHeight-shrink, tui.WindowList, noBorder, true)
}
if len(inlineTop)+len(inlineBottom) > 0 && t.wborder != nil {
t.placeInlineStack(inlineTop, t.window.Top()-inlineTopLines, true)
t.placeInlineStack(inlineBottom, t.window.Top()+t.window.Height()+inlineBottomLines-1, false)
} }
if len(t.scrollbar) == 0 { if len(t.scrollbar) == 0 {
@@ -2844,11 +2654,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
if hasInputWindow { if hasInputWindow {
var btop int var btop int
// Inline sections live inside the list frame, so they don't participate if (hasHeaderWindow || hasHeaderLinesWindow) && t.headerFirst {
// in --header-first repositioning; only non-inline sections do.
hasNonInlineHeader := hasHeaderWindow && t.headerBorderShape != tui.BorderInline
hasNonInlineHeaderLines := hasHeaderLinesWindow && headerLinesShape != tui.BorderInline
if (hasNonInlineHeader || hasNonInlineHeaderLines) && t.headerFirst {
switch t.layout { switch t.layout {
case layoutDefault: case layoutDefault:
btop = w.Top() + w.Height() btop = w.Top() + w.Height()
@@ -2893,7 +2699,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
} }
// Set up header border // Set up header border
if hasHeaderWindow && t.headerBorderShape != tui.BorderInline { if hasHeaderWindow {
var btop int var btop int
if hasInputWindow && t.headerFirst { if hasInputWindow && t.headerFirst {
if t.layout == layoutReverse { if t.layout == layoutReverse {
@@ -2921,7 +2727,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
} }
// Set up header lines border // Set up header lines border
if hasHeaderLinesWindow && headerLinesShape != tui.BorderInline { if hasHeaderLinesWindow {
var btop int var btop int
// NOTE: We still have to handle --header-first here in case // NOTE: We still have to handle --header-first here in case
// --header-lines-border is set. Can't we just use header window instead // --header-lines-border is set. Can't we just use header window instead
@@ -2954,7 +2760,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
} }
// Set up footer // Set up footer
if hasFooterWindow && t.footerBorderShape != tui.BorderInline { if hasFooterWindow {
var btop int var btop int
if t.layout == layoutReverse { if t.layout == layoutReverse {
btop = w.Top() + w.Height() btop = w.Top() + w.Height()
@@ -2971,21 +2777,8 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
t.footerWindow = createInnerWindow(t.footerBorder, t.footerBorderShape, tui.WindowFooter, 0) t.footerWindow = createInnerWindow(t.footerBorder, t.footerBorderShape, tui.WindowFooter, 0)
} }
// When the list label lands on an edge owned by an inline section, swap its bg // Print border label
// so the label reads as part of that section's frame. Fg stays at list-label. t.printLabel(t.wborder, t.listLabel, t.listLabelOpts, t.listLabelLen, t.listBorderShape, false)
listLabel, listLabelLen := t.listLabel, t.listLabelLen
var adjacentSection *inlineSlot
if t.listLabelOpts.bottom && len(inlineBottom) > 0 {
adjacentSection = &inlineBottom[0]
} else if !t.listLabelOpts.bottom && len(inlineTop) > 0 {
adjacentSection = &inlineTop[0]
}
if adjacentSection != nil {
bg := tui.BorderColor(adjacentSection.windowType).Bg()
custom := tui.ColListLabel.WithBg(tui.ColorAttr{Color: bg, Attr: tui.AttrUndefined})
listLabel, listLabelLen = t.ansiLabelPrinter(t.listLabelOpts.label, &custom, false)
}
t.printLabel(t.wborder, listLabel, t.listLabelOpts, listLabelLen, t.listBorderShape, false)
t.printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape, false) t.printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape, false)
t.printLabel(t.pborder, t.previewLabel, t.previewLabelOpts, t.previewLabelLen, t.activePreviewOpts.Border(), false) t.printLabel(t.pborder, t.previewLabel, t.previewLabelOpts, t.previewLabelLen, t.activePreviewOpts.Border(), false)
t.printLabel(t.inputBorder, t.inputLabel, t.inputLabelOpts, t.inputLabelLen, t.inputBorderShape, false) t.printLabel(t.inputBorder, t.inputLabel, t.inputLabelOpts, t.inputLabelLen, t.inputBorderShape, false)
@@ -2993,39 +2786,37 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
t.printLabel(t.footerBorder, t.footerLabel, t.footerLabelOpts, t.footerLabelLen, t.footerBorderShape, false) t.printLabel(t.footerBorder, t.footerLabel, t.footerLabelOpts, t.footerLabelLen, t.footerBorderShape, false)
} }
// printLabelAt positions and renders a label at the given row of `window`. Shared by
// printLabel (which computes row from the border shape) and the inline-section label
// code (which uses an explicit separator row).
func (t *Terminal) printLabelAt(window tui.Window, render labelPrinter, opts labelOpts, length int, row int) {
if window == nil || render == nil || window.Height() == 0 {
return
}
var col int
if opts.column == 0 {
col = max(0, (window.Width()-length)/2)
} else if opts.column < 0 {
col = max(0, window.Width()+opts.column+1-length)
} else {
col = min(opts.column-1, window.Width()-length)
}
window.Move(row, col)
render(window, window.Width())
}
func (t *Terminal) printLabel(window tui.Window, render labelPrinter, opts labelOpts, length int, borderShape tui.BorderShape, redrawBorder bool) { func (t *Terminal) printLabel(window tui.Window, render labelPrinter, opts labelOpts, length int, borderShape tui.BorderShape, redrawBorder bool) {
if window == nil || window.Height() == 0 { if window == nil {
return return
} }
if window.Height() == 0 {
return
}
switch borderShape { switch borderShape {
case tui.BorderHorizontal, tui.BorderTop, tui.BorderBottom, tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderBlock, tui.BorderThinBlock, tui.BorderDouble: 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()
} }
if render == nil {
return
}
var col int
if opts.column == 0 {
col = max(0, (window.Width()-length)/2)
} else if opts.column < 0 {
col = max(0, window.Width()+opts.column+1-length)
} else {
col = min(opts.column-1, window.Width()-length)
}
row := 0 row := 0
if borderShape == tui.BorderBottom || opts.bottom { if borderShape == tui.BorderBottom || opts.bottom {
row = window.Height() - 1 row = window.Height() - 1
} }
t.printLabelAt(window, render, opts, length, row) window.Move(row, col)
render(window, window.Width())
} }
} }
@@ -3393,19 +3184,8 @@ func (t *Terminal) resizeIfNeeded() bool {
return true return true
} }
// Inline sections are budget-capped inside wborder, so window.Height() may be
// smaller than the requested content length. Treat "capped" (height < want) as
// a no-op to avoid triggering a full redraw on every info/header/footer event
// when the user has requested more content than fits.
mismatch := func(shape tui.BorderShape, height, want int) bool {
if shape == tui.BorderInline && height < want {
return false
}
return height != want
}
// Check footer window // Check footer window
if len(t.footer) > 0 && (t.footerWindow == nil || mismatch(t.footerBorderShape, t.footerWindow.Height(), len(t.footer))) || if len(t.footer) > 0 && (t.footerWindow == nil || t.footerWindow.Height() != len(t.footer)) ||
len(t.footer) == 0 && t.footerWindow != nil { len(t.footer) == 0 && t.footerWindow != nil {
t.printAll() t.printAll()
return true return true
@@ -3419,12 +3199,14 @@ func (t *Terminal) resizeIfNeeded() bool {
if needHeaderLinesWindow { if needHeaderLinesWindow {
primaryHeaderLines -= t.headerLines primaryHeaderLines -= t.headerLines
} }
// FIXME: Full redraw is triggered if there are too many lines in the header
// so that the header window cannot display all of them.
if (needHeaderWindow && t.headerWindow == nil) || if (needHeaderWindow && t.headerWindow == nil) ||
(!needHeaderWindow && t.headerWindow != nil) || (!needHeaderWindow && t.headerWindow != nil) ||
(needHeaderWindow && t.headerWindow != nil && mismatch(t.headerBorderShape, t.headerWindow.Height(), primaryHeaderLines)) || (needHeaderWindow && t.headerWindow != nil && primaryHeaderLines != t.headerWindow.Height()) ||
(needHeaderLinesWindow && t.headerLinesWindow == nil) || (needHeaderLinesWindow && t.headerLinesWindow == nil) ||
(!needHeaderLinesWindow && t.headerLinesWindow != nil) || (!needHeaderLinesWindow && t.headerLinesWindow != nil) ||
(needHeaderLinesWindow && t.headerLinesWindow != nil && mismatch(t.headerLinesShape, t.headerLinesWindow.Height(), t.headerLines)) { (needHeaderLinesWindow && t.headerLinesWindow != nil && t.headerLines != t.headerLinesWindow.Height()) {
t.printAll() t.printAll()
return true return true
} }
@@ -3436,23 +3218,14 @@ func (t *Terminal) printHeader() {
return return
} }
// headerWindow is nil when hasHeaderWindow() returned false at resize time, t.withWindow(t.headerWindow, func() {
// e.g. --header-border=inline combined with empty header content. Don't var headerItems []Item
// delegate to printHeaderImpl because its nil-window branch folds the header if !t.hasHeaderLinesWindow() {
// into the list window, which isn't valid for inline. A nil window is only headerItems = t.header
// legitimate when the shape is NOT inline (e.g. header combined with the }
// list when --no-list-border is in effect). t.printHeaderImpl(t.headerWindow, t.headerBorderShape, t.header0, headerItems)
if !(t.headerBorderShape == tui.BorderInline && t.headerWindow == nil) { })
t.withWindow(t.headerWindow, func() { if w, shape := t.determineHeaderLinesShape(); w {
var headerItems []Item
if !t.hasHeaderLinesWindow() {
headerItems = t.header
}
t.printHeaderImpl(t.headerWindow, t.headerBorderShape, t.header0, headerItems)
})
}
if w, shape := t.determineHeaderLinesShape(); w &&
!(shape == tui.BorderInline && t.headerLinesWindow == nil) {
t.withWindow(t.headerLinesWindow, func() { t.withWindow(t.headerLinesWindow, func() {
t.printHeaderImpl(t.headerLinesWindow, shape, nil, t.header) t.printHeaderImpl(t.headerLinesWindow, shape, nil, t.header)
}) })
@@ -3503,10 +3276,7 @@ func (t *Terminal) headerIndentImpl(base int, borderShape tui.BorderShape) int {
if t.listBorderShape.HasLeft() { if t.listBorderShape.HasLeft() {
indentSize += 1 + t.borderWidth indentSize += 1 + t.borderWidth
} }
// Section borders with their own left side skip past the list border's left column. if borderShape.HasLeft() {
// Inline sections also skip it, but only when the list border actually has a left,
// since otherwise the inline window starts flush with the list window.
if borderShape.HasLeft() || (borderShape == tui.BorderInline && t.listBorderShape.HasLeft()) {
indentSize -= 1 + t.borderWidth indentSize -= 1 + t.borderWidth
if indentSize < 0 { if indentSize < 0 {
indentSize = 0 indentSize = 0
@@ -3981,7 +3751,7 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
offset := Offset{int32(p), int32(p + w)} offset := Offset{int32(p), int32(p + w)}
charOffsets[idx] = offset charOffsets[idx] = offset
} }
slices.SortFunc(charOffsets, compareOffsets) sort.Sort(ByOrder(charOffsets))
} }
// When postTask is nil, we're printing header lines. No need to care about nth. // When postTask is nil, we're printing header lines. No need to care about nth.
@@ -4018,7 +3788,7 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
end := start + int32(length) end := start + int32(length)
nthOffsets[i] = Offset{int32(start), int32(end)} nthOffsets[i] = Offset{int32(start), int32(end)}
} }
slices.SortFunc(nthOffsets, compareOffsets) sort.Sort(ByOrder(nthOffsets))
} }
} }
allOffsets := result.colorOffsets(charOffsets, nthOffsets, t.theme, colBase, colMatch, t.nthAttr, nthOverlay, hidden) allOffsets := result.colorOffsets(charOffsets, nthOffsets, t.theme, colBase, colMatch, t.nthAttr, nthOverlay, hidden)
@@ -6181,28 +5951,11 @@ func (t *Terminal) Loop() error {
case reqRedrawInputLabel: case reqRedrawInputLabel:
t.printLabel(t.inputBorder, t.inputLabel, t.inputLabelOpts, t.inputLabelLen, t.inputBorderShape, true) t.printLabel(t.inputBorder, t.inputLabel, t.inputLabelOpts, t.inputLabelLen, t.inputBorderShape, true)
case reqRedrawHeaderLabel: case reqRedrawHeaderLabel:
if t.headerBorderShape == tui.BorderInline { t.printLabel(t.headerBorder, t.headerLabel, t.headerLabelOpts, t.headerLabelLen, t.headerBorderShape, true)
// Inline labels sit on the separator inside wborder; re-run the
// full layout to repaint the separator + label together.
t.printAll()
} else {
t.printLabel(t.headerBorder, t.headerLabel, t.headerLabelOpts, t.headerLabelLen, t.headerBorderShape, true)
}
case reqRedrawFooterLabel: case reqRedrawFooterLabel:
if t.footerBorderShape == tui.BorderInline { t.printLabel(t.footerBorder, t.footerLabel, t.footerLabelOpts, t.footerLabelLen, t.footerBorderShape, true)
t.printAll()
} else {
t.printLabel(t.footerBorder, t.footerLabel, t.footerLabelOpts, t.footerLabelLen, t.footerBorderShape, true)
}
case reqRedrawListLabel: case reqRedrawListLabel:
// When inline sections are active, the label's bg depends on which t.printLabel(t.wborder, t.listLabel, t.listLabelOpts, t.listLabelLen, t.listBorderShape, true)
// section owns the adjacent edge. Rerun the layout to reuse that
// logic rather than duplicating it here.
if t.headerBorderShape == tui.BorderInline || t.headerLinesShape == tui.BorderInline || t.footerBorderShape == tui.BorderInline {
t.printAll()
} else {
t.printLabel(t.wborder, t.listLabel, t.listLabelOpts, t.listLabelLen, t.listBorderShape, true)
}
case reqRedrawBorderLabel: case reqRedrawBorderLabel:
t.printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape, true) t.printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape, true)
case reqRedrawPreviewLabel: case reqRedrawPreviewLabel:
@@ -6781,16 +6534,10 @@ func (t *Terminal) Loop() error {
t.cx = len(t.input) t.cx = len(t.input)
case actChangeHeader, actTransformHeader, actBgTransformHeader: case actChangeHeader, actTransformHeader, actBgTransformHeader:
capture(false, func(header string) { capture(false, func(header string) {
// When a dedicated header window is not used, we may need to
// update other elements as well.
if t.changeHeader(header) { if t.changeHeader(header) {
// resizeIfNeeded() tolerates a shorter-than-wanted inline req(reqList, reqPrompt, reqInfo)
// window, so a length change can leave the inline slot
// stale. Force a redraw to re-run the layout. Non-inline
// shapes are handled by resizeIfNeeded.
if t.headerBorderShape == tui.BorderInline {
req(reqRedraw)
} else {
req(reqList, reqPrompt, reqInfo)
}
} }
req(reqHeader) req(reqHeader)
}) })

View File

@@ -721,7 +721,7 @@ func TestWordWrapAnsiLine(t *testing.T) {
t.Errorf("ANSI: %q", result) t.Errorf("ANSI: %q", result)
} }
// Long word (no space) - no break, let character wrapping handle it // Long word (no space) no break, let character wrapping handle it
result = term.wordWrapAnsiLine("abcdefghij", 5, 2) result = term.wordWrapAnsiLine("abcdefghij", 5, 2)
if len(result) != 1 || result[0] != "abcdefghij" { if len(result) != 1 || result[0] != "abcdefghij" {
t.Errorf("Long word: %q", result) t.Errorf("Long word: %q", result)
@@ -749,7 +749,7 @@ func TestWordWrapAnsiLine(t *testing.T) {
// Tab handling: tab expands to tabstop-aligned width // Tab handling: tab expands to tabstop-aligned width
term.tabstop = 8 term.tabstop = 8
// "\thi there" - tab at column 0 expands to 8, total "hi" starts at 8 // "\thi there" tab at column 0 expands to 8, total "hi" starts at 8
// maxWidth=15: "\thi" = 10 wide, "there" = 5 wide, total 16 > 15, wrap at space // maxWidth=15: "\thi" = 10 wide, "there" = 5 wide, total 16 > 15, wrap at space
result = term.wordWrapAnsiLine("\thi there", 15, 2) result = term.wordWrapAnsiLine("\thi there", 15, 2)
if len(result) != 2 || result[0] != "\thi" || result[1] != "there" { if len(result) != 2 || result[0] != "\thi" || result[1] != "there" {

View File

@@ -689,7 +689,6 @@ func (r *LightRenderer) escSequence(sz *int) Event {
switch r.buffer[4] { switch r.buffer[4] {
case '1', '2', '3', '4', '5', '6', '7', '8', '9': case '1', '2', '3', '4', '5', '6', '7', '8', '9':
// Kitty iTerm2 WezTerm // Kitty iTerm2 WezTerm
// ARROW "\e[1;1D"
// SHIFT-ARROW "\e[1;2D" // SHIFT-ARROW "\e[1;2D"
// ALT-SHIFT-ARROW "\e[1;4D" "\e[1;10D" "\e[1;4D" // ALT-SHIFT-ARROW "\e[1;4D" "\e[1;10D" "\e[1;4D"
// CTRL-SHIFT-ARROW "\e[1;6D" N/A // CTRL-SHIFT-ARROW "\e[1;6D" N/A
@@ -744,7 +743,6 @@ func (r *LightRenderer) escSequence(sz *int) Event {
if shift { if shift {
return Event{ShiftUp, 0, nil} return Event{ShiftUp, 0, nil}
} }
return Event{Up, 0, nil}
case 'B': case 'B':
if ctrlAltShift { if ctrlAltShift {
return Event{CtrlAltShiftDown, 0, nil} return Event{CtrlAltShiftDown, 0, nil}
@@ -767,7 +765,6 @@ func (r *LightRenderer) escSequence(sz *int) Event {
if shift { if shift {
return Event{ShiftDown, 0, nil} return Event{ShiftDown, 0, nil}
} }
return Event{Down, 0, nil}
case 'C': case 'C':
if ctrlAltShift { if ctrlAltShift {
return Event{CtrlAltShiftRight, 0, nil} return Event{CtrlAltShiftRight, 0, nil}
@@ -790,7 +787,6 @@ func (r *LightRenderer) escSequence(sz *int) Event {
if alt { if alt {
return Event{AltRight, 0, nil} return Event{AltRight, 0, nil}
} }
return Event{Right, 0, nil}
case 'D': case 'D':
if ctrlAltShift { if ctrlAltShift {
return Event{CtrlAltShiftLeft, 0, nil} return Event{CtrlAltShiftLeft, 0, nil}
@@ -813,7 +809,6 @@ func (r *LightRenderer) escSequence(sz *int) Event {
if shift { if shift {
return Event{ShiftLeft, 0, nil} return Event{ShiftLeft, 0, nil}
} }
return Event{Left, 0, nil}
case 'H': case 'H':
if ctrlAltShift { if ctrlAltShift {
return Event{CtrlAltShiftHome, 0, nil} return Event{CtrlAltShiftHome, 0, nil}
@@ -836,7 +831,6 @@ func (r *LightRenderer) escSequence(sz *int) Event {
if shift { if shift {
return Event{ShiftHome, 0, nil} return Event{ShiftHome, 0, nil}
} }
return Event{Home, 0, nil}
case 'F': case 'F':
if ctrlAltShift { if ctrlAltShift {
return Event{CtrlAltShiftEnd, 0, nil} return Event{CtrlAltShiftEnd, 0, nil}
@@ -859,7 +853,6 @@ func (r *LightRenderer) escSequence(sz *int) Event {
if shift { if shift {
return Event{ShiftEnd, 0, nil} return Event{ShiftEnd, 0, nil}
} }
return Event{End, 0, nil}
} }
} // r.buffer[4] } // r.buffer[4]
} // r.buffer[3] } // r.buffer[3]
@@ -1129,144 +1122,127 @@ func (w *LightWindow) DrawHBorder() {
w.drawBorder(true) w.drawBorder(true)
} }
// drawHLine fills row `row` with `line` between optional left/right caps.
// A zero rune means "no cap"; caps are placed at the very edges of `w`.
func (w *LightWindow) drawHLine(row int, line, leftCap, rightCap rune, color ColorPair) {
w.Move(row, 0)
hw := runeWidth(line)
width := w.width
if leftCap != 0 {
w.CPrint(color, string(leftCap))
width -= runeWidth(leftCap)
}
if rightCap != 0 {
width -= runeWidth(rightCap)
}
if width < 0 {
width = 0
}
inner := width / hw
rem := width - inner*hw
w.CPrint(color, repeat(line, inner)+repeat(' ', rem))
if rightCap != 0 {
w.CPrint(color, string(rightCap))
}
}
func (w *LightWindow) DrawHSeparator(row int, windowType WindowType, useBottom bool) {
if w.height == 0 {
return
}
shape := w.border.shape
if shape == BorderNone {
return
}
color := BorderColor(windowType)
line := w.border.top
if useBottom {
line = w.border.bottom
}
var leftCap, rightCap rune
if shape.HasLeft() {
leftCap = w.border.leftMid
}
if shape.HasRight() {
rightCap = w.border.rightMid
}
w.drawHLine(row, line, leftCap, rightCap, color)
}
func (w *LightWindow) PaintSectionFrame(topContent, bottomContent int, windowType WindowType, edge SectionEdge) {
if w.height == 0 || w.border.shape == BorderNone {
return
}
color := BorderColor(windowType)
shape := w.border.shape
hasLeft := shape.HasLeft()
hasRight := shape.HasRight()
rightW := runeWidth(w.border.right)
// Content rows: overpaint left/right verticals + their 1-char margin.
for row := topContent; row <= bottomContent; row++ {
if hasLeft {
w.Move(row, 0)
w.CPrint(color, string(w.border.left)+" ")
}
if hasRight {
w.Move(row, w.width-rightW-1)
w.CPrint(color, " "+string(w.border.right))
}
}
if edge == SectionEdgeTop && shape.HasTop() {
var leftCap, rightCap rune
if hasLeft {
leftCap = w.border.topLeft
}
if hasRight {
rightCap = w.border.topRight
}
w.drawHLine(0, w.border.top, leftCap, rightCap, color)
}
if edge == SectionEdgeBottom && shape.HasBottom() {
var leftCap, rightCap rune
if hasLeft {
leftCap = w.border.bottomLeft
}
if hasRight {
rightCap = w.border.bottomRight
}
w.drawHLine(w.height-1, w.border.bottom, leftCap, rightCap, color)
}
}
func (w *LightWindow) drawBorder(onlyHorizontal bool) { func (w *LightWindow) drawBorder(onlyHorizontal bool) {
if w.height == 0 { if w.height == 0 {
return return
} }
shape := w.border.shape switch w.border.shape {
if shape == BorderNone { case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble:
return w.drawBorderAround(onlyHorizontal)
case BorderHorizontal:
w.drawBorderHorizontal(true, true)
case BorderVertical:
if onlyHorizontal {
return
}
w.drawBorderVertical(true, true)
case BorderTop:
w.drawBorderHorizontal(true, false)
case BorderBottom:
w.drawBorderHorizontal(false, true)
case BorderLeft:
if onlyHorizontal {
return
}
w.drawBorderVertical(true, false)
case BorderRight:
if onlyHorizontal {
return
}
w.drawBorderVertical(false, true)
} }
color := BorderColor(w.windowType) }
hasLeft := shape.HasLeft()
hasRight := shape.HasRight()
if shape.HasTop() { func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
var leftCap, rightCap rune color := ColBorder
if hasLeft { switch w.windowType {
leftCap = w.border.topLeft case WindowList:
} color = ColListBorder
if hasRight { case WindowInput:
rightCap = w.border.topRight color = ColInputBorder
} case WindowHeader:
w.drawHLine(0, w.border.top, leftCap, rightCap, color) color = ColHeaderBorder
case WindowFooter:
color = ColFooterBorder
case WindowPreview:
color = ColPreviewBorder
} }
if !onlyHorizontal && (hasLeft || hasRight) { hw := runeWidth(w.border.top)
if top {
w.Move(0, 0)
w.CPrint(color, repeat(w.border.top, w.width/hw))
}
if bottom {
w.Move(w.height-1, 0)
w.CPrint(color, repeat(w.border.bottom, w.width/hw))
}
}
func (w *LightWindow) drawBorderVertical(left, right bool) {
vw := runeWidth(w.border.left)
color := ColBorder
switch w.windowType {
case WindowList:
color = ColListBorder
case WindowInput:
color = ColInputBorder
case WindowHeader:
color = ColHeaderBorder
case WindowFooter:
color = ColFooterBorder
case WindowPreview:
color = ColPreviewBorder
}
for y := 0; y < w.height; y++ {
if left {
w.Move(y, 0)
w.CPrint(color, string(w.border.left))
w.CPrint(color, " ") // Margin
}
if right {
w.Move(y, w.width-vw-1)
w.CPrint(color, " ") // Margin
w.CPrint(color, string(w.border.right))
}
}
}
func (w *LightWindow) drawBorderAround(onlyHorizontal bool) {
w.Move(0, 0)
color := ColBorder
switch w.windowType {
case WindowList:
color = ColListBorder
case WindowInput:
color = ColInputBorder
case WindowHeader:
color = ColHeaderBorder
case WindowFooter:
color = ColFooterBorder
case WindowPreview:
color = ColPreviewBorder
}
hw := runeWidth(w.border.top)
tcw := runeWidth(w.border.topLeft) + runeWidth(w.border.topRight)
bcw := runeWidth(w.border.bottomLeft) + runeWidth(w.border.bottomRight)
rem := (w.width - tcw) % hw
w.CPrint(color, string(w.border.topLeft)+repeat(w.border.top, (w.width-tcw)/hw)+repeat(' ', rem)+string(w.border.topRight))
if !onlyHorizontal {
vw := runeWidth(w.border.left) vw := runeWidth(w.border.left)
for y := 0; y < w.height; y++ { for y := 1; y < w.height-1; y++ {
// Corner rows are already painted by drawHLine above / below. w.Move(y, 0)
if (y == 0 && shape.HasTop()) || (y == w.height-1 && shape.HasBottom()) { w.CPrint(color, string(w.border.left))
continue w.CPrint(color, " ") // Margin
}
if hasLeft { w.Move(y, w.width-vw-1)
w.Move(y, 0) w.CPrint(color, " ") // Margin
w.CPrint(color, string(w.border.left)+" ") w.CPrint(color, string(w.border.right))
}
if hasRight {
w.Move(y, w.width-vw-1)
w.CPrint(color, " "+string(w.border.right))
}
} }
} }
if shape.HasBottom() { w.Move(w.height-1, 0)
var leftCap, rightCap rune rem = (w.width - bcw) % hw
if hasLeft { w.CPrint(color, string(w.border.bottomLeft)+repeat(w.border.bottom, (w.width-bcw)/hw)+repeat(' ', rem)+string(w.border.bottomRight))
leftCap = w.border.bottomLeft
}
if hasRight {
rightCap = w.border.bottomRight
}
w.drawHLine(w.height-1, w.border.bottom, leftCap, rightCap, color)
}
} }
func (w *LightWindow) csi(code string) string { func (w *LightWindow) csi(code string) string {

View File

@@ -1017,115 +1017,6 @@ func (w *TcellWindow) DrawHBorder() {
w.drawBorder(true) w.drawBorder(true)
} }
// borderStyleFor returns the tcell.Style used to draw borders for `wt`, honoring
// whether the window is rendering with colors.
func (w *TcellWindow) borderStyleFor(wt WindowType) tcell.Style {
if !w.color {
return w.normal.style()
}
return BorderColor(wt).style()
}
// drawHLine fills row `y` with `line` between optional left/right caps.
// A zero rune means "no cap"; caps are placed at the very edges of `w`.
// tcell has an issue displaying two overlapping wide runes, so the line
// stops before the cap position rather than overpainting.
func (w *TcellWindow) drawHLine(y int, line, leftCap, rightCap rune, style tcell.Style) {
left := w.left
right := left + w.width
hw := runeWidth(line)
lw := 0
rw := 0
if leftCap != 0 {
lw = runeWidth(leftCap)
}
if rightCap != 0 {
rw = runeWidth(rightCap)
}
for x := left + lw; x <= right-rw-hw; x += hw {
_screen.SetContent(x, y, line, nil, style)
}
if leftCap != 0 {
_screen.SetContent(left, y, leftCap, nil, style)
}
if rightCap != 0 {
_screen.SetContent(right-rw, y, rightCap, nil, style)
}
}
func (w *TcellWindow) DrawHSeparator(row int, windowType WindowType, useBottom bool) {
if w.height == 0 {
return
}
shape := w.borderStyle.shape
if shape == BorderNone {
return
}
style := w.borderStyleFor(windowType)
line := w.borderStyle.top
if useBottom {
line = w.borderStyle.bottom
}
var leftCap, rightCap rune
if shape.HasLeft() {
leftCap = w.borderStyle.leftMid
}
if shape.HasRight() {
rightCap = w.borderStyle.rightMid
}
w.drawHLine(w.top+row, line, leftCap, rightCap, style)
}
func (w *TcellWindow) PaintSectionFrame(topContent, bottomContent int, windowType WindowType, edge SectionEdge) {
if w.height == 0 {
return
}
shape := w.borderStyle.shape
if shape == BorderNone {
return
}
style := w.borderStyleFor(windowType)
left := w.left
right := left + w.width
hasLeft := shape.HasLeft()
hasRight := shape.HasRight()
leftW := runeWidth(w.borderStyle.left)
rightW := runeWidth(w.borderStyle.right)
// Content rows: overpaint the left and right verticals (+ their 1-char margin) in
// the section's color. Inner margin stays at whatever bg the sub-window set.
for row := topContent; row <= bottomContent; row++ {
y := w.top + row
if hasLeft {
_screen.SetContent(left, y, w.borderStyle.left, nil, style)
_screen.SetContent(left+leftW, y, ' ', nil, style)
}
if hasRight {
_screen.SetContent(right-rightW-1, y, ' ', nil, style)
_screen.SetContent(right-rightW, y, w.borderStyle.right, nil, style)
}
}
if edge == SectionEdgeTop && shape.HasTop() {
var leftCap, rightCap rune
if hasLeft {
leftCap = w.borderStyle.topLeft
}
if hasRight {
rightCap = w.borderStyle.topRight
}
w.drawHLine(w.top, w.borderStyle.top, leftCap, rightCap, style)
}
if edge == SectionEdgeBottom && shape.HasBottom() {
var leftCap, rightCap rune
if hasLeft {
leftCap = w.borderStyle.bottomLeft
}
if hasRight {
rightCap = w.borderStyle.bottomRight
}
w.drawHLine(w.top+w.height-1, w.borderStyle.bottom, leftCap, rightCap, style)
}
}
func (w *TcellWindow) drawBorder(onlyHorizontal bool) { func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
if w.height == 0 { if w.height == 0 {
return return
@@ -1140,44 +1031,72 @@ func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
top := w.top top := w.top
bot := top + w.height bot := top + w.height
style := w.borderStyleFor(w.windowType) var style tcell.Style
if w.color {
hasLeft := shape.HasLeft() switch w.windowType {
hasRight := shape.HasRight() case WindowBase:
style = ColBorder.style()
if shape.HasTop() { case WindowList:
var leftCap, rightCap rune style = ColListBorder.style()
if hasLeft { case WindowHeader:
leftCap = w.borderStyle.topLeft style = ColHeaderBorder.style()
case WindowFooter:
style = ColFooterBorder.style()
case WindowInput:
style = ColInputBorder.style()
case WindowPreview:
style = ColPreviewBorder.style()
} }
if hasRight { } else {
rightCap = w.borderStyle.topRight style = w.normal.style()
}
w.drawHLine(top, w.borderStyle.top, leftCap, rightCap, style)
} }
if shape.HasBottom() {
var leftCap, rightCap rune hw := runeWidth(w.borderStyle.top)
if hasLeft { switch shape {
leftCap = w.borderStyle.bottomLeft case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderHorizontal, BorderTop:
max := right - 2*hw
if shape == BorderHorizontal || shape == BorderTop {
max = right - hw
} }
if hasRight { // tcell has an issue displaying two overlapping wide runes
rightCap = w.borderStyle.bottomRight // e.g. SetContent( HH )
// SetContent( TR )
// ==================
// ( HH ) => TR is ignored
for x := left; x <= max; x += hw {
_screen.SetContent(x, top, w.borderStyle.top, nil, style)
}
}
switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderHorizontal, BorderBottom:
max := right - 2*hw
if shape == BorderHorizontal || shape == BorderBottom {
max = right - hw
}
for x := left; x <= max; x += hw {
_screen.SetContent(x, bot-1, w.borderStyle.bottom, nil, style)
} }
w.drawHLine(bot-1, w.borderStyle.bottom, leftCap, rightCap, style)
} }
if !onlyHorizontal { if !onlyHorizontal {
vw := runeWidth(w.borderStyle.right) switch shape {
for y := top; y < bot; y++ { case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderVertical, BorderLeft:
// Corner rows are already painted by drawHLine above / below. for y := top; y < bot; y++ {
if (y == top && shape.HasTop()) || (y == bot-1 && shape.HasBottom()) {
continue
}
if hasLeft {
_screen.SetContent(left, y, w.borderStyle.left, nil, style) _screen.SetContent(left, y, w.borderStyle.left, nil, style)
} }
if hasRight { }
switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderVertical, BorderRight:
vw := runeWidth(w.borderStyle.right)
for y := top; y < bot; y++ {
_screen.SetContent(right-vw, y, w.borderStyle.right, nil, style) _screen.SetContent(right-vw, y, w.borderStyle.right, nil, style)
} }
} }
} }
switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble:
_screen.SetContent(left, top, w.borderStyle.topLeft, nil, style)
_screen.SetContent(right-runeWidth(w.borderStyle.topRight), top, w.borderStyle.topRight, nil, style)
_screen.SetContent(left, bot-1, w.borderStyle.bottomLeft, nil, style)
_screen.SetContent(right-runeWidth(w.borderStyle.bottomRight), bot-1, w.borderStyle.bottomRight, nil, style)
}
} }

View File

@@ -595,12 +595,11 @@ const (
BorderBottom BorderBottom
BorderLeft BorderLeft
BorderRight BorderRight
BorderInline
) )
func (s BorderShape) HasLeft() bool { func (s BorderShape) HasLeft() bool {
switch s { switch s {
case BorderNone, BorderPhantom, BorderLine, BorderInline, BorderRight, BorderTop, BorderBottom, BorderHorizontal: // No Left case BorderNone, BorderPhantom, BorderLine, BorderRight, BorderTop, BorderBottom, BorderHorizontal: // No Left
return false return false
} }
return true return true
@@ -608,7 +607,7 @@ func (s BorderShape) HasLeft() bool {
func (s BorderShape) HasRight() bool { func (s BorderShape) HasRight() bool {
switch s { switch s {
case BorderNone, BorderPhantom, BorderLine, BorderInline, BorderLeft, BorderTop, BorderBottom, BorderHorizontal: // No right case BorderNone, BorderPhantom, BorderLine, BorderLeft, BorderTop, BorderBottom, BorderHorizontal: // No right
return false return false
} }
return true return true
@@ -616,7 +615,7 @@ func (s BorderShape) HasRight() bool {
func (s BorderShape) HasTop() bool { func (s BorderShape) HasTop() bool {
switch s { switch s {
case BorderNone, BorderPhantom, BorderLine, BorderInline, BorderLeft, BorderRight, BorderBottom, BorderVertical: // No top case BorderNone, BorderPhantom, BorderLine, BorderLeft, BorderRight, BorderBottom, BorderVertical: // No top
return false return false
} }
return true return true
@@ -624,7 +623,7 @@ func (s BorderShape) HasTop() bool {
func (s BorderShape) HasBottom() bool { func (s BorderShape) HasBottom() bool {
switch s { switch s {
case BorderNone, BorderPhantom, BorderLine, BorderInline, BorderLeft, BorderRight, BorderTop, BorderVertical: // No bottom case BorderNone, BorderPhantom, BorderLine, BorderLeft, BorderRight, BorderTop, BorderVertical: // No bottom
return false return false
} }
return true return true
@@ -644,8 +643,6 @@ type BorderStyle struct {
topRight rune topRight rune
bottomLeft rune bottomLeft rune
bottomRight rune bottomRight rune
leftMid rune
rightMid rune
} }
type BorderCharacter int type BorderCharacter int
@@ -661,9 +658,7 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
topLeft: ' ', topLeft: ' ',
topRight: ' ', topRight: ' ',
bottomLeft: ' ', bottomLeft: ' ',
bottomRight: ' ', bottomRight: ' '}
leftMid: ' ',
rightMid: ' '}
} }
if !unicode { if !unicode {
return BorderStyle{ return BorderStyle{
@@ -676,8 +671,6 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
topRight: '+', topRight: '+',
bottomLeft: '+', bottomLeft: '+',
bottomRight: '+', bottomRight: '+',
leftMid: '+',
rightMid: '+',
} }
} }
switch shape { switch shape {
@@ -692,8 +685,6 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
topRight: '┐', topRight: '┐',
bottomLeft: '└', bottomLeft: '└',
bottomRight: '┘', bottomRight: '┘',
leftMid: '├',
rightMid: '┤',
} }
case BorderBold: case BorderBold:
return BorderStyle{ return BorderStyle{
@@ -706,8 +697,6 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
topRight: '┓', topRight: '┓',
bottomLeft: '┗', bottomLeft: '┗',
bottomRight: '┛', bottomRight: '┛',
leftMid: '┣',
rightMid: '┫',
} }
case BorderBlock: case BorderBlock:
// ▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜ // ▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜
@@ -723,8 +712,6 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
topRight: '▜', topRight: '▜',
bottomLeft: '▙', bottomLeft: '▙',
bottomRight: '▟', bottomRight: '▟',
leftMid: '▌',
rightMid: '▐',
} }
case BorderThinBlock: case BorderThinBlock:
@@ -741,8 +728,6 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
topRight: '🭾', topRight: '🭾',
bottomLeft: '🭼', bottomLeft: '🭼',
bottomRight: '🭿', bottomRight: '🭿',
leftMid: '▏',
rightMid: '▕',
} }
case BorderDouble: case BorderDouble:
@@ -756,8 +741,6 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
topRight: '╗', topRight: '╗',
bottomLeft: '╚', bottomLeft: '╚',
bottomRight: '╝', bottomRight: '╝',
leftMid: '╠',
rightMid: '╣',
} }
} }
return BorderStyle{ return BorderStyle{
@@ -770,8 +753,6 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
topRight: '╮', topRight: '╮',
bottomLeft: '╰', bottomLeft: '╰',
bottomRight: '╯', bottomRight: '╯',
leftMid: '├',
rightMid: '┤',
} }
} }
@@ -793,35 +774,6 @@ const (
WindowFooter WindowFooter
) )
// BorderColor returns the ColorPair used to draw borders for the given WindowType.
func BorderColor(wt WindowType) ColorPair {
switch wt {
case WindowList:
return ColListBorder
case WindowInput:
return ColInputBorder
case WindowHeader:
return ColHeaderBorder
case WindowFooter:
return ColFooterBorder
case WindowPreview:
return ColPreviewBorder
}
return ColBorder
}
// SectionEdge selects which outer edge of the frame an inline section
// should claim when PaintSectionFrame overpaints its adjacent border.
// SectionEdgeNone paints only the inner verticals (for sections that
// don't touch the outer top or bottom).
type SectionEdge int
const (
SectionEdgeNone SectionEdge = iota
SectionEdgeTop
SectionEdgeBottom
)
type Renderer interface { type Renderer interface {
DefaultTheme() *ColorTheme DefaultTheme() *ColorTheme
Init() error Init() error
@@ -859,19 +811,6 @@ type Window interface {
DrawBorder() DrawBorder()
DrawHBorder() DrawHBorder()
// DrawHSeparator draws an inline horizontal separator at `row` (relative to the
// window's top) using the color for `windowType`. The separator is conceptually
// the section's inner edge (e.g. the bottom border of an inline header), so the
// whole row including junctions carries the section's fg + bg. When useBottom is
// true the `bottom` horizontal char is used instead of `top`; for thinblock/block
// styles this keeps the thin line bonded to the list content on the opposite side.
DrawHSeparator(row int, windowType WindowType, useBottom bool)
// PaintSectionFrame overpaints the border cells around the rows [topContent,
// bottomContent] (inclusive, relative to the window's top) with the color for
// `windowType`. When edge is SectionEdgeTop / SectionEdgeBottom, the
// corresponding outer horizontal (+ corners) is also painted, letting the
// inline section claim that edge of the outer frame.
PaintSectionFrame(topContent, bottomContent int, windowType WindowType, edge SectionEdge)
Refresh() Refresh()
FinishFill() FinishFill()
@@ -1227,7 +1166,7 @@ func init() {
} }
} }
func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, boldify bool, forceBlack bool, hasInputWindow bool, hasHeaderWindow bool, headerInline bool, footerInline bool) { func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, boldify bool, forceBlack bool, hasInputWindow bool, hasHeaderWindow bool) {
if forceBlack { if forceBlack {
theme.Bg = ColorAttr{colBlack, AttrUndefined} theme.Bg = ColorAttr{colBlack, AttrUndefined}
} }
@@ -1361,22 +1300,11 @@ func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, boldify bool, forceBlac
} else { } else {
theme.HeaderBg = o(theme.Bg, theme.ListBg) theme.HeaderBg = o(theme.Bg, theme.ListBg)
} }
// Inline header/footer borders sit inside the list frame, so default their color theme.HeaderBorder = o(theme.Border, theme.HeaderBorder)
// to the list-border color when the user has not explicitly set it. The inline
// separator then matches the surrounding frame.
headerBorderFallback := theme.Border
if headerInline {
headerBorderFallback = theme.ListBorder
}
theme.HeaderBorder = o(headerBorderFallback, theme.HeaderBorder)
theme.HeaderLabel = o(theme.BorderLabel, theme.HeaderLabel) theme.HeaderLabel = o(theme.BorderLabel, theme.HeaderLabel)
theme.FooterBg = o(theme.Bg, theme.FooterBg) theme.FooterBg = o(theme.Bg, theme.FooterBg)
footerBorderFallback := theme.Border theme.FooterBorder = o(theme.Border, theme.FooterBorder)
if footerInline {
footerBorderFallback = theme.ListBorder
}
theme.FooterBorder = o(footerBorderFallback, theme.FooterBorder)
theme.FooterLabel = o(theme.BorderLabel, theme.FooterLabel) theme.FooterLabel = o(theme.BorderLabel, theme.FooterLabel)
if theme.Nomatch.IsUndefined() { if theme.Nomatch.IsUndefined() {

View File

@@ -9,7 +9,7 @@ func TestWrapLine(t *testing.T) {
t.Errorf("Basic wrap: %v", lines) t.Errorf("Basic wrap: %v", lines)
} }
// Exact fit - no wrapping needed // Exact fit no wrapping needed
lines = WrapLine("hello", 0, 5, 8, 2) lines = WrapLine("hello", 0, 5, 8, 2)
if len(lines) != 1 || lines[0].Text != "hello" || lines[0].DisplayWidth != 5 { if len(lines) != 1 || lines[0].Text != "hello" || lines[0].DisplayWidth != 5 {
t.Errorf("Exact fit: %v", lines) t.Errorf("Exact fit: %v", lines)

View File

@@ -11,8 +11,6 @@ import (
"strings" "strings"
"sync/atomic" "sync/atomic"
"syscall" "syscall"
"golang.org/x/sys/windows"
) )
type shellType int type shellType int
@@ -21,7 +19,6 @@ const (
shellTypeUnknown shellType = iota shellTypeUnknown shellType = iota
shellTypeCmd shellTypeCmd
shellTypePowerShell shellTypePowerShell
shellTypePwsh
) )
var escapeRegex = regexp.MustCompile(`[&|<>()^%!"]`) var escapeRegex = regexp.MustCompile(`[&|<>()^%!"]`)
@@ -49,10 +46,7 @@ func NewExecutor(withShell string) *Executor {
} else if strings.HasPrefix(basename, "cmd") { } else if strings.HasPrefix(basename, "cmd") {
shellType = shellTypeCmd shellType = shellTypeCmd
args = []string{"/s/c"} args = []string{"/s/c"}
} else if strings.HasPrefix(basename, "pwsh") { } else if strings.HasPrefix(basename, "pwsh") || strings.HasPrefix(basename, "powershell") {
shellType = shellTypePwsh
args = []string{"-NoProfile", "-Command"}
} else if strings.HasPrefix(basename, "powershell") {
shellType = shellTypePowerShell shellType = shellTypePowerShell
args = []string{"-NoProfile", "-Command"} args = []string{"-NoProfile", "-Command"}
} else { } else {
@@ -62,12 +56,8 @@ func NewExecutor(withShell string) *Executor {
} }
// ExecCommand executes the given command with $SHELL // ExecCommand executes the given command with $SHELL
// // FIXME: setpgid is unused. We set it in the Unix implementation so that we
// On Windows, setpgid controls whether the spawned process is placed in a new // can kill preview process with its child processes at once.
// process group (so that it can be signaled independently, e.g. for previews).
// However, we only do this for "pwsh" and non-standard shells, because cmd.exe
// and Windows PowerShell ("powershell.exe") don't always exit on Ctrl-Break.
//
// NOTE: For "powershell", we should ideally set output encoding to UTF8, // NOTE: For "powershell", we should ideally set output encoding to UTF8,
// but it is left as is now because no adverse effect has been observed. // but it is left as is now because no adverse effect has been observed.
func (x *Executor) ExecCommand(command string, setpgid bool) *exec.Cmd { func (x *Executor) ExecCommand(command string, setpgid bool) *exec.Cmd {
@@ -83,31 +73,19 @@ func (x *Executor) ExecCommand(command string, setpgid bool) *exec.Cmd {
} }
x.shellPath.Store(shell) x.shellPath.Store(shell)
} }
var creationFlags uint32
// Set new process group for pwsh (PowerShell 7+) and unknown/posix-ish shells
if setpgid && (x.shellType == shellTypePwsh || x.shellType == shellTypeUnknown) {
creationFlags = windows.CREATE_NEW_PROCESS_GROUP
}
var cmd *exec.Cmd var cmd *exec.Cmd
if x.shellType == shellTypeCmd { if x.shellType == shellTypeCmd {
cmd = exec.Command(shell) cmd = exec.Command(shell)
cmd.SysProcAttr = &syscall.SysProcAttr{ cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: false, HideWindow: false,
CmdLine: fmt.Sprintf(`%s "%s"`, strings.Join(x.args, " "), command), CmdLine: fmt.Sprintf(`%s "%s"`, strings.Join(x.args, " "), command),
CreationFlags: creationFlags, CreationFlags: 0,
} }
} else { } else {
args := x.args cmd = exec.Command(shell, append(x.args, command)...)
if setpgid && x.shellType == shellTypePwsh {
// pwsh needs -NonInteractive flag to exit on Ctrl-Break
args = append([]string{"-NonInteractive"}, x.args...)
}
cmd = exec.Command(shell, append(args, command)...)
cmd.SysProcAttr = &syscall.SysProcAttr{ cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: false, HideWindow: false,
CreationFlags: creationFlags, CreationFlags: 0,
} }
} }
return cmd return cmd
@@ -178,7 +156,7 @@ func (x *Executor) QuoteEntry(entry string) string {
fd -H --no-ignore -td -d 4 | fzf --preview ".\eza.exe --color=always --tree --level=3 --icons=always {}" --with-shell "powershell -NoProfile -Command" fd -H --no-ignore -td -d 4 | fzf --preview ".\eza.exe --color=always --tree --level=3 --icons=always {}" --with-shell "powershell -NoProfile -Command"
*/ */
return escapeArg(entry) return escapeArg(entry)
case shellTypePowerShell, shellTypePwsh: case shellTypePowerShell:
escaped := strings.ReplaceAll(entry, `"`, `\"`) escaped := strings.ReplaceAll(entry, `"`, `\"`)
return "'" + strings.ReplaceAll(escaped, "'", "''") + "'" return "'" + strings.ReplaceAll(escaped, "'", "''") + "'"
default: default:
@@ -188,21 +166,6 @@ func (x *Executor) QuoteEntry(entry string) string {
// KillCommand kills the process for the given command // KillCommand kills the process for the given command
func KillCommand(cmd *exec.Cmd) error { func KillCommand(cmd *exec.Cmd) error {
// Safely handle nil command or process.
if cmd == nil || cmd.Process == nil {
return nil
}
// If it has its own process group, we can send it Ctrl-Break
if cmd.SysProcAttr != nil && cmd.SysProcAttr.CreationFlags&windows.CREATE_NEW_PROCESS_GROUP != 0 {
if err := windows.GenerateConsoleCtrlEvent(windows.CTRL_BREAK_EVENT, uint32(cmd.Process.Pid)); err == nil {
return nil
}
}
// If it's the same process group, or if sending the console control event
// fails (e.g., no console, different console, or process already exited),
// fall back to a standard kill. This probably won't *help* if there's I/O
// going on, because Wait() will still hang until the I/O finishes unless we
// hard-kill the entire process group. But it doesn't hurt to try!
return cmd.Process.Kill() return cmd.Process.Kill()
} }

View File

@@ -3,9 +3,10 @@ set -e FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS FZF_DEFAULT_OPTS_FILE FZF_TMUX FZF_T
set -e FZF_CTRL_T_COMMAND FZF_CTRL_T_OPTS FZF_ALT_C_COMMAND FZF_ALT_C_OPTS FZF_CTRL_R_OPTS set -e FZF_CTRL_T_COMMAND FZF_CTRL_T_OPTS FZF_ALT_C_COMMAND FZF_ALT_C_OPTS FZF_CTRL_R_OPTS
set -e FZF_API_KEY set -e FZF_API_KEY
# Unset completion-specific variables # Unset completion-specific variables
set -e FZF_COMPLETION_OPTS FZF_EXPANSION_OPTS set -e FZF_COMPLETION_TRIGGER FZF_COMPLETION_OPTS
set -gx FZF_DEFAULT_OPTS "--no-scrollbar --pointer '>' --marker '>'" set -gx FZF_DEFAULT_OPTS "--no-scrollbar --pointer '>' --marker '>'"
set -gx FZF_COMPLETION_TRIGGER '++'
set -gx fish_history fzf_test set -gx fish_history fzf_test
# Add fzf to PATH # Add fzf to PATH

View File

@@ -105,23 +105,6 @@ class Tmux
go(%W[send-keys -t #{win}] + args.map(&:to_s)) go(%W[send-keys -t #{win}] + args.map(&:to_s))
end end
# Simulate a mouse click at the given 1-based column and row using the SGR mouse protocol
# (xterm mouse mode 1006, which fzf enables). The escape sequence is injected as literal
# keystrokes via tmux, and fzf parses it like a real terminal mouse event.
#
# tmux's own mouse handling intercepts these sequences when `set -g mouse on`, so we toggle
# mouse off for the duration of the click and restore the previous state afterwards.
def click(col, row, button: 0)
prev = go(%w[show-options -gv mouse]).first
go(%w[set-option -g mouse off])
begin
seq = "\e[<#{button};#{col};#{row}M\e[<#{button};#{col};#{row}m"
go(%W[send-keys -t #{win} -l #{seq}])
ensure
go(%W[set-option -g mouse #{prev}]) if prev && !prev.empty?
end
end
def paste(str) def paste(str)
system('tmux', 'setb', str, ';', 'pasteb', '-t', win, ';', 'send-keys', '-t', win, 'Enter') system('tmux', 'setb', str, ';', 'pasteb', '-t', win, ';', 'send-keys', '-t', win, 'Enter')
end end
@@ -130,71 +113,6 @@ class Tmux
go(%W[capture-pane -p -J -t #{win}]).map(&:rstrip).reverse.drop_while(&:empty?).reverse go(%W[capture-pane -p -J -t #{win}]).map(&:rstrip).reverse.drop_while(&:empty?).reverse
end end
# Raw pane capture with ANSI escape sequences preserved.
def capture_ansi
go(%W[capture-pane -p -J -e -t #{win}])
end
# 3-bit ANSI bg code (40..47) -> color name used in --color options.
BG_NAMES = %w[black red green yellow blue magenta cyan white].freeze
# Parse `tmux capture-pane -e` output into per-row bg ranges. Each row is an
# array of [col_start, col_end, bg] tuples where bg is one of:
# 'default'
# 'red' / 'green' / 'blue' / ... (3-bit names)
# 'bright-red' / ... (bright variants)
# '256:<n>' (256-color fallback)
# ANSI state persists across rows, matching real terminal behavior.
def bg_ranges
raw = go(%W[capture-pane -p -J -e -t #{win}])
bg = 'default'
raw.map do |row|
cells = []
i = 0
len = row.length
while i < len
c = row[i]
if c == "\e" && row[i + 1] == '['
j = i + 2
j += 1 while j < len && row[j] != 'm'
parts = row[i + 2...j].split(';')
k = 0
while k < parts.length
p = parts[k].to_i
case p
when 0, 49 then bg = 'default'
when 40..47 then bg = BG_NAMES[p - 40]
when 100..107 then bg = "bright-#{BG_NAMES[p - 100]}"
when 48
if parts[k + 1] == '5'
bg = "256:#{parts[k + 2]}"
k += 2
elsif parts[k + 1] == '2'
bg = "rgb:#{parts[k + 2]}:#{parts[k + 3]}:#{parts[k + 4]}"
k += 4
end
end
k += 1
end
i = j + 1
else
cells << bg
i += 1
end
end
ranges = []
start = 0
cells.each_with_index do |b, idx|
if idx.positive? && b != cells[idx - 1]
ranges << [start, idx - 1, cells[idx - 1]]
start = idx
end
end
ranges << [start, cells.length - 1, cells.last] unless cells.empty?
ranges
end
end
def until(refresh = false, timeout: DEFAULT_TIMEOUT) def until(refresh = false, timeout: DEFAULT_TIMEOUT)
lines = nil lines = nil
begin begin

View File

@@ -1672,7 +1672,7 @@ class TestCore < TestInteractive
end end
tmux.send_keys :BSpace, :BSpace, :BSpace tmux.send_keys :BSpace, :BSpace, :BSpace
# Reload with shuffled order - cursor should track "555" # Reload with shuffled order cursor should track "555"
tmux.send_keys 'C-r' tmux.send_keys 'C-r'
tmux.until do |lines| tmux.until do |lines|
assert_equal 1000, lines.match_count assert_equal 1000, lines.match_count
@@ -1694,7 +1694,7 @@ class TestCore < TestInteractive
tmux.send_keys :Up tmux.send_keys :Up
tmux.until { |lines| assert_includes lines, '> 2 banana' } tmux.until { |lines| assert_includes lines, '> 2 banana' }
# Reload - the second field changes, but first field "2" stays # Reload the second field changes, but first field "2" stays
tmux.send_keys 'C-r' tmux.send_keys 'C-r'
tmux.until do |lines| tmux.until do |lines|
assert_equal 3, lines.match_count assert_equal 3, lines.match_count
@@ -1709,7 +1709,7 @@ class TestCore < TestInteractive
tmux.send_keys :Up tmux.send_keys :Up
tmux.until { |lines| assert_includes lines, '> beta' } tmux.until { |lines| assert_includes lines, '> beta' }
# Reload with completely different items - no match for "beta" # Reload with completely different items no match for "beta"
# Cursor stays at the same position (second item) # Cursor stays at the same position (second item)
tmux.send_keys 'C-r' tmux.send_keys 'C-r'
tmux.until do |lines| tmux.until do |lines|
@@ -1727,7 +1727,7 @@ class TestCore < TestInteractive
assert_includes lines[-2], '+T' assert_includes lines[-2], '+T'
end end
# Trigger slow reload - should show +T* while blocked # Trigger slow reload should show +T* while blocked
tmux.send_keys 'C-r' tmux.send_keys 'C-r'
tmux.until { |lines| assert_includes lines[-2], '+T*' } tmux.until { |lines| assert_includes lines[-2], '+T*' }
@@ -1769,7 +1769,7 @@ class TestCore < TestInteractive
assert_includes lines, '> 1' assert_includes lines, '> 1'
end end
# Trigger reload - blocked during initial sleep # Trigger reload blocked during initial sleep
tmux.send_keys 'C-r' tmux.send_keys 'C-r'
tmux.until { |lines| assert_includes lines[-2], '+T*' } tmux.until { |lines| assert_includes lines[-2], '+T*' }
# Match "1" arrives, unblocks before the remaining items load # Match "1" arrives, unblocks before the remaining items load
@@ -1790,7 +1790,7 @@ class TestCore < TestInteractive
assert_includes lines, '> 1' assert_includes lines, '> 1'
end end
# Trigger reload-sync - every observable state must be either: # Trigger reload-sync every observable state must be either:
# 1. +T* (still blocked), or # 1. +T* (still blocked), or
# 2. final state (count=10, +T without *) # 2. final state (count=10, +T without *)
# Any other combination (e.g. unblocked while count < 10) is a bug. # Any other combination (e.g. unblocked while count < 10) is a bug.
@@ -1835,7 +1835,7 @@ class TestCore < TestInteractive
tmux.send_keys :Up tmux.send_keys :Up
tmux.until { |lines| assert_includes lines, '> beta' } tmux.until { |lines| assert_includes lines, '> beta' }
# Reload with completely different items - no match for "beta" # Reload with completely different items no match for "beta"
tmux.send_keys 'C-r' tmux.send_keys 'C-r'
tmux.until { |lines| assert_includes lines[-2], '+T*' } tmux.until { |lines| assert_includes lines[-2], '+T*' }
# After stream completes, unblocks with cursor at same position (second item) # After stream completes, unblocks with cursor at same position (second item)
@@ -1857,7 +1857,7 @@ class TestCore < TestInteractive
tmux.send_keys 'C-t' tmux.send_keys 'C-t'
tmux.until { |lines| assert_includes lines[-2], '+t' } tmux.until { |lines| assert_includes lines[-2], '+t' }
# Reload - should track by field "2" # Reload should track by field "2"
tmux.send_keys 'C-r' tmux.send_keys 'C-r'
tmux.until do |lines| tmux.until do |lines|
assert_equal 3, lines.match_count assert_equal 3, lines.match_count
@@ -1876,7 +1876,7 @@ class TestCore < TestInteractive
tmux.send_keys :Up, :Up, :Tab tmux.send_keys :Up, :Up, :Tab
tmux.until { |lines| assert_includes lines[-2], '(2)' } tmux.until { |lines| assert_includes lines[-2], '(2)' }
# Reload - selections should be preserved by id-nth key # Reload selections should be preserved by id-nth key
tmux.send_keys 'C-r' tmux.send_keys 'C-r'
tmux.until do |lines| tmux.until do |lines|
assert_equal 3, lines.match_count assert_equal 3, lines.match_count

View File

@@ -1298,325 +1298,4 @@ class TestLayout < TestInteractive
tmux.send_keys :Enter tmux.send_keys :Enter
end end
end end
# Locate a word in the currently captured screen and click its first character.
# tmux rows/columns are 1-based; capture indices are 0-based.
def click_word(word)
tmux.capture.each_with_index do |line, idx|
col = line.index(word)
return tmux.click(col + 1, idx + 1) if col
end
flunk("word #{word.inspect} not found on screen")
end
# Launch fzf with a click-{header,footer} binding that echoes FZF_CLICK_* into the prompt,
# then click each word in `clicks` and assert the resulting L/W values.
# `clicks` is an array of [word_to_click, expected_line].
def verify_clicks(kind:, opts:, input:, clicks:)
var = kind.to_s.upcase # HEADER or FOOTER
binding = "click-#{kind}:transform-prompt:" \
"echo \"L=$FZF_CLICK_#{var}_LINE W=$FZF_CLICK_#{var}_WORD> \""
# --multi makes the info line end in " (0)" so the wait regex is unambiguous.
tmux.send_keys %(#{input} | #{FZF} #{opts} --multi --bind '#{binding}'), :Enter
# Wait for fzf to fully render before inspecting the screen, otherwise the echoed
# command line can shadow click targets.
tmux.until { |lines| lines.any_include?(%r{ [0-9]+/[0-9]+ \(0\)}) }
clicks.each do |word, line|
click_word(word)
tmux.until { |lines| assert lines.any_include?("L=#{line} W=#{word}>") }
end
tmux.send_keys 'Escape'
end
# Header lines (--header-lines) are rendered in reverse display order only under
# layout=default; in layout=reverse and layout=reverse-list they keep the input order.
# FZF_CLICK_HEADER_LINE reflects the visual row, so the expected value flips.
HEADER_CLICKS = [%w[Aaa 1], %w[Bbb 2], %w[Ccc 3]].freeze
%w[default reverse reverse-list].each do |layout|
slug = layout.tr('-', '_')
# Plain --header with no border around the header section.
define_method(:"test_click_header_plain_#{slug}") do
verify_clicks(kind: :header,
opts: %(--layout=#{layout} --header $'Aaa\\nBbb\\nCcc'),
input: 'seq 5',
clicks: HEADER_CLICKS)
end
# --header with a framing border (--style full gives --header-border=rounded by default).
define_method(:"test_click_header_border_rounded_#{slug}") do
verify_clicks(kind: :header,
opts: %(--layout=#{layout} --style full --header $'Aaa\\nBbb\\nCcc'),
input: 'seq 5',
clicks: HEADER_CLICKS)
end
# --header-lines consumed from stdin, with its own framing border.
define_method(:"test_click_header_lines_border_rounded_#{slug}") do
clicks_hl = if layout == 'default'
[%w[Xaa 3], %w[Ybb 2], %w[Zcc 1]]
else
[%w[Xaa 1], %w[Ybb 2], %w[Zcc 3]]
end
verify_clicks(kind: :header,
opts: %(--layout=#{layout} --style full --header-lines 3),
input: "(printf 'Xaa\\nYbb\\nZcc\\n'; seq 5)",
clicks: clicks_hl)
end
# --footer with a framing border.
define_method(:"test_click_footer_border_rounded_#{slug}") do
verify_clicks(kind: :footer,
opts: %(--layout=#{layout} --style full --footer $'Foo\\nBar\\nBaz'),
input: 'seq 5',
clicks: [%w[Foo 1], %w[Bar 2], %w[Baz 3]])
end
# --header and --header-lines combined. Click-header numbering concatenates the two
# sections, but the order depends on the layout:
# layoutReverse: custom header (1..N), then header-lines (N+1..N+M)
# layoutDefault: header-lines (1..M, reversed visually), then custom header (M+1..M+N)
# layoutReverseList: header-lines (1..M), then custom header (M+1..M+N)
define_method(:"test_click_header_combined_#{slug}") do
clicks = case layout
when 'reverse'
[%w[Aaa 1], %w[Bbb 2], %w[Ccc 3], %w[Xaa 4], %w[Ybb 5], %w[Zcc 6]]
when 'default'
[%w[Aaa 4], %w[Bbb 5], %w[Ccc 6], %w[Xaa 3], %w[Ybb 2], %w[Zcc 1]]
else # reverse-list
[%w[Aaa 4], %w[Bbb 5], %w[Ccc 6], %w[Xaa 1], %w[Ybb 2], %w[Zcc 3]]
end
verify_clicks(kind: :header,
opts: %(--layout=#{layout} --header $'Aaa\\nBbb\\nCcc' --header-lines 3),
input: "(printf 'Xaa\\nYbb\\nZcc\\n'; seq 5)",
clicks: clicks)
end
# Inline header inside a rounded list border.
define_method(:"test_click_header_border_inline_#{slug}") do
opts = %(--layout=#{layout} --style full --header $'Aaa\\nBbb\\nCcc' --header-border=inline)
verify_clicks(kind: :header, opts: opts, input: 'seq 5', clicks: HEADER_CLICKS)
end
# Inline header inside a horizontal list border (top+bottom only, no T-junctions).
define_method(:"test_click_header_border_inline_horizontal_list_#{slug}") do
opts = %(--layout=#{layout} --style full --list-border=horizontal --header $'Aaa\\nBbb\\nCcc' --header-border=inline)
verify_clicks(kind: :header, opts: opts, input: 'seq 5', clicks: HEADER_CLICKS)
end
# Inline header-lines inside a rounded list border.
define_method(:"test_click_header_lines_border_inline_#{slug}") do
clicks_hl = if layout == 'default'
[%w[Xaa 3], %w[Ybb 2], %w[Zcc 1]]
else
[%w[Xaa 1], %w[Ybb 2], %w[Zcc 3]]
end
opts = %(--layout=#{layout} --style full --header-lines 3 --header-lines-border=inline)
verify_clicks(kind: :header, opts: opts,
input: "(printf 'Xaa\\nYbb\\nZcc\\n'; seq 5)",
clicks: clicks_hl)
end
# Inline footer inside a rounded list border.
define_method(:"test_click_footer_border_inline_#{slug}") do
opts = %(--layout=#{layout} --style full --footer $'Foo\\nBar\\nBaz' --footer-border=inline)
verify_clicks(kind: :footer, opts: opts, input: 'seq 5',
clicks: [%w[Foo 1], %w[Bar 2], %w[Baz 3]])
end
end
# An inline section requesting far more rows than the terminal can fit must not
# break the layout. The list frame must still render inside the pane with both
# corners visible and the prompt line present.
def test_inline_header_lines_oversized
tmux.send_keys %(seq 10000 | #{FZF} --style full --header-border inline --header-lines 9999), :Enter
tmux.until { |lines| lines.any_include?(%r{ [0-9]+/[0-9]+}) }
lines = tmux.capture
# Rounded (light) and sharp (tcell) default border glyphs.
top_corners = /[╭┌]/
bottom_corners = /[╰└]/
assert(lines.any? { |l| l.match?(top_corners) }, "list frame top missing: #{lines.inspect}")
assert(lines.any? { |l| l.match?(bottom_corners) }, "list frame bottom missing: #{lines.inspect}")
assert(lines.any? { |l| l.include?('>') }, "prompt missing: #{lines.inspect}")
tmux.send_keys 'Escape'
end
# A non-inline section that consumes all available rows must still render without
# crashing when another section is inline but has no budget. The inline section's
# content is clipped to 0 but the layout proceeds.
def test_inline_footer_starved_by_non_inline_header
tmux.send_keys %(seq 10000 | #{FZF} --style full --footer-border inline --footer "$(seq 1000)" --header "$(seq 1000)"), :Enter
tmux.until { |lines| lines.any_include?(%r{ [0-9]+/[0-9]+}) }
lines = tmux.capture
assert(lines.any? { |l| l.include?('>') }, "prompt missing: #{lines.inspect}")
tmux.send_keys 'Escape'
end
# Without a line-drawing --list-border, --header-border=inline must silently
# fall back to the `line` style (documented behavior).
def test_inline_falls_back_without_list_border
tmux.send_keys %(seq 5 | #{FZF} --list-border=none --header HEADER --header-border=inline), :Enter
tmux.until { |lines| lines.any_include?(%r{ [0-9]+/[0-9]+}) }
lines = tmux.capture
assert(lines.any? { |l| l.include?('HEADER') }, "header missing: #{lines.inspect}")
# Neither list frame corners (rounded/sharp) nor T-junction runes appear,
# since we've fallen back to a plain line separator.
assert(lines.none? { |l| l.match?(/[╭╮╰╯┌┐└┘├┤]/) }, "unexpected frame glyphs: #{lines.inspect}")
tmux.send_keys 'Escape'
end
# Regression: when --header-border=inline falls back to `line` because the
# list border can't host an inline separator, the header-border color must
# inherit from `border`, not `list-border`. The effective shape is `line`,
# so color inheritance must match what `line` rendering would use.
def test_inline_fallback_does_not_inherit_list_border_color
# Marker attribute (bold) on list-border. If HeaderBorder wrongly inherits
# from ListBorder, the header separator characters will carry the bold
# attribute. --info=hidden and --no-separator strip other separator lines
# so the only row of `─` chars is the header separator.
tmux.send_keys %(seq 5 | #{FZF} --list-border=none --header HEADER --header-border=inline --info=hidden --no-separator --color=bg:-1,list-border:red:bold), :Enter
sep_row = nil
tmux.until do |_|
sep_row = tmux.capture_ansi.find do |row|
stripped = row.gsub(/\e\[[\d;]*m/, '').rstrip
stripped.match?(/\A─+\z/)
end
!sep_row.nil?
end
# Bold (1) or red fg (31) on the header separator means it inherited from
# list-border even though the effective shape is `line` (non-inline).
refute_match(/\e\[(?:[\d;]*;)?(?:1|31)(?:;[\d;]*)?m─/, sep_row,
"header separator inherited list-border attr: #{sep_row.inspect}")
tmux.send_keys 'Escape'
end
# Inline takes precedence over --header-first: the main header stays
# inside the list frame instead of moving below the input.
def test_inline_header_border_overrides_header_first
tmux.send_keys %(seq 5 | #{FZF} --style full --header foo --header-first --header-border inline), :Enter
tmux.until do |lines|
foo_idx = lines.index { |l| l.match?(/\A│\s+foo\s+│\z/) }
input_idx = lines.index { |l| l.match?(/\A│\s+>\s+\d+\/\d+\s+│\z/) }
foo_idx && input_idx && foo_idx < input_idx
end
end
# With both sections present, --header-first still moves the main --header
# below the input while --header-lines-border=inline keeps header-lines
# inside the list frame.
def test_inline_header_lines_with_header_first_and_main_header
tmux.send_keys %(seq 5 | #{FZF} --style full --header foo --header-lines 1 --header-first --header-lines-border inline), :Enter
tmux.until do |lines|
one_idx = lines.index { |l| l.match?(/\A│\s+1\s+│\z/) }
foo_idx = lines.index { |l| l.match?(/\A│\s+foo\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
end
end
# With no main --header, --header-first previously repositioned
# header-lines. Inline now takes precedence: header-lines stays inside
# the list frame.
def test_inline_header_lines_with_header_first_no_main_header
tmux.send_keys %(seq 5 | #{FZF} --style full --header-lines 1 --header-first --header-lines-border inline), :Enter
tmux.until do |lines|
one_idx = lines.index { |l| l.match?(/\A│\s+1\s+│\z/) }
input_idx = lines.index { |l| l.match?(/\A│\s+>\s+\d+\/\d+\s+│\z/) }
one_idx && input_idx && one_idx < input_idx
end
end
# Regression: with --header-border=inline and --header-lines but no
# --header, the inline slot was sized for header-lines only. After
# change-header added a main header line, resizeIfNeeded tolerated the
# too-small slot, so the header-lines line got displaced and disappeared.
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.until { |lines| lines.any_include?(/\A│\s+1\s+│\z/) }
tmux.send_keys ' '
tmux.until do |lines|
lines.any_include?(/\A│\s+1\s+│\z/) && lines.any_include?(/\A│\s+tada\s+│\z/)
end
end
# Invalid inline combinations must be rejected at startup.
def test_inline_rejected_on_unsupported_options
[
['--border=inline', 'inline border is only supported'],
['--list-border=inline', 'inline border is only supported'],
['--input-border=inline', 'inline border is only supported'],
['--preview-window=border-inline --preview :', 'invalid preview window option: border-inline'],
['--header-border=inline --header-lines-border=sharp --header-lines=1',
'--header-border=inline requires --header-lines-border to be inline or unset']
].each do |args, expected|
output = `#{FZF} #{args} < /dev/null 2>&1`
refute_equal 0, $CHILD_STATUS.exitstatus, "expected non-zero exit for: #{args}"
assert_includes output, expected, "wrong error for: #{args}"
end
end
private
# Count rows whose entire width is a single `color` range.
def count_full_rows(ranges_by_row, color)
ranges_by_row.count { |r| r.length == 1 && r[0][2] == color }
end
# Wait until `tmux.bg_ranges` has at least `count` fully-`color` rows; return them.
def wait_for_full_rows(color, count)
ranges = nil
tmux.until do |_|
ranges = tmux.bg_ranges
count_full_rows(ranges, color) >= count
end
ranges
end
public
# Inline header's entire section (outer edge + content-row verticals + separator)
# carries the header-bg color; list rows below carry list-bg.
def test_inline_header_bg_color
tmux.send_keys %(seq 5 | #{FZF} --list-border --reverse --header HEADER --header-border=inline --color=bg:-1,header-border:white,list-border:white,header-bg:red,list-bg:green), :Enter
tmux.until { |lines| lines.any_include?(%r{ [0-9]+/[0-9]+}) }
# 3 fully-red rows: top edge, header content, separator.
ranges = wait_for_full_rows('red', 3)
assert_equal_org(3, count_full_rows(ranges, 'red'))
# List rows below (>=5) are fully green.
assert_operator count_full_rows(ranges, 'green'), :>=, 5
tmux.send_keys 'Escape'
end
# Regression: when --header-lines-border=inline is the only inline section
# (no --header-border), the section must still use header-bg, not list-bg.
def test_inline_header_lines_bg_without_main_header
tmux.send_keys %(seq 5 | #{FZF} --list-border --reverse --header-lines 2 --header-lines-border=inline --color=bg:-1,header-border:white,list-border:white,header-bg:red,list-bg:green), :Enter
tmux.until { |lines| lines.any_include?(%r{ [0-9]+/[0-9]+}) }
# Top edge + 2 content rows + separator = 4 fully-red rows.
ranges = wait_for_full_rows('red', 4)
assert_equal_org(4, count_full_rows(ranges, 'red'))
tmux.send_keys 'Escape'
end
# Inline footer's entire section carries footer-bg; list rows above carry list-bg.
def test_inline_footer_bg_color
tmux.send_keys %(seq 5 | #{FZF} --list-border --footer FOOTER --footer-border=inline --color=bg:-1,footer-border:white,list-border:white,footer-bg:blue,list-bg:green), :Enter
tmux.until { |lines| lines.any_include?(%r{ [0-9]+/[0-9]+}) }
ranges = wait_for_full_rows('blue', 3)
assert_equal_org(3, count_full_rows(ranges, 'blue'))
tmux.send_keys 'Escape'
end
# The list-label's bg is swapped to match the adjacent inline section so it reads as
# part of the section frame rather than a list-colored island on a section-colored edge.
def test_list_label_bg_on_inline_section_edge
tmux.send_keys %(seq 5 | #{FZF} --list-border --reverse --header HEADER --header-border=inline --list-label=LL --color=bg:-1,header-border:white,list-border:white,header-bg:red,list-bg:green,list-label:yellow:bold), :Enter
tmux.until { |lines| lines.any_include?(%r{ [0-9]+/[0-9]+}) }
# The label sits on the header-owned top edge, so the entire row must be a
# single red run (no green breaks where the label cells are).
ranges = wait_for_full_rows('red', 3)
assert_operator count_full_rows(ranges, 'red'), :>=, 3
tmux.send_keys 'Escape'
end
end end

View File

@@ -40,7 +40,7 @@ class TestServer < TestInteractive
assert_equal [0, 1], state[:current][:positions] assert_equal [0, 1], state[:current][:positions]
assert_equal state[:current][:positions], state[:current][:positions].sort assert_equal state[:current][:positions], state[:current][:positions].sort
# No match - no current item # No match no current item
Net::HTTP.post(fn.call, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ') Net::HTTP.post(fn.call, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ')
tmux.until { |lines| assert_equal 100, lines.item_count } tmux.until { |lines| assert_equal 100, lines.item_count }
tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] } tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] }