Compare commits

...

47 Commits

Author SHA1 Message Date
Junegunn Choi 6fefe02546 0.72.0 2026-04-26 18:04:37 +09:00
Junegunn Choi f783582561 Rename internal variables to match user-facing names 2026-04-26 14:47:57 +09:00
Junegunn Choi af42fde089 Fix rubocop errors 2026-04-26 14:47:06 +09:00
Junegunn Choi 27dab2422e Change the example command in the 0.72.0 changelog 2026-04-21 18:48:00 +09:00
Junegunn Choi 56be41218c Redraw when change-footer changes line count
Mirror of the earlier change-header fix. The inline footer slot's row
budget depends on footer content length, but resizeIfNeeded() tolerates
a shorter-than-wanted inline window, so extra lines get clipped. Drive
a redraw on length change to re-run the layout.
2026-04-21 18:41:48 +09:00
Junegunn Choi 7782da6c00 Add dashed border style
New --border=dashed / --list-border=dashed / --header-border=dashed etc.
Uses U+2576 (╶) for horizontal edges and U+2506 (┆) for verticals, with
rounded corners (╭╮╰╯) and sharp T-junction mids (├┤). Terminal cells
are taller than wide (~2:1), so horizontals use a sparse stub per cell
while verticals need more dashes per cell to look evenly dashed.
Works with inline sections.
2026-04-20 22:23:41 +09:00
Junegunn Choi 43f0508dd2 Update CHANGELOG (0.72.0) 2026-04-20 00:55:43 +09:00
Junegunn Choi 1986d101e0 Redraw when change-header changes line count
The inline header slot's row budget depends on header content length,
but resizeIfNeeded() tolerates a shorter-than-wanted inline window, so
the stale slot stays. Drive a redraw on length change to re-run the
layout.
2026-04-20 00:47:16 +09:00
Junegunn Choi dd7a081b93 Let inline sections take precedence over --header-first
--header-first previously was rejected with --header-border=inline or
--header-lines-border=inline. Now, inline placement wins: an inline
section stays inside the list frame, and --header-first only affects
non-inline sections (mainly the main --header).
2026-04-20 00:47:16 +09:00
Junegunn Choi d247284bc3 Set inline separator caps per side 2026-04-20 00:47:16 +09:00
Junegunn Choi 6f3e5de150 Update man page: clarify inline list-border shape requirements 2026-04-20 00:47:16 +09:00
Junegunn Choi 0142fe9d04 Update CHANGELOG: clarify inline list-border shape requirements 2026-04-20 00:47:16 +09:00
Junegunn Choi 251b08b831 Fix misleading comment on printHeader nil-window guard
The guard fires when hasHeaderWindow() returned false at resize time,
not when addInline had no budget (placeInlineStack always leaves a
non-nil 0-height placeholder).
2026-04-20 00:47:16 +09:00
Junegunn Choi 01567b4f06 Fix inline header/footer border color when falling back to line
InitTheme was called before the runtime coerced BorderInline to
BorderLine, so HeaderBorder / FooterBorder inherited from ListBorder
even when the effective shape was 'line'. Mirror the coercion so
color inheritance matches the rendered shape.
2026-04-20 00:47:16 +09:00
Junegunn Choi d754cfab87 Add --{header,header-lines,footer}-border=inline
New BorderShape that embeds the section inside the --list-border
frame, joined to the list content by a horizontal separator with
T-junctions where the list shape has side borders. Requires a list
border with both top and bottom segments; falls back to 'line'
otherwise. Stacks when multiple sections are inline.

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

Incompatible with --header-first. --header-border=inline requires
--header-lines-border to be inline or unset.
2026-04-20 00:47:16 +09:00
Junegunn Choi ef6eba1b89 Add a test case for click-header with --header and --header-lines combined
The two sections swap order between layouts, and header-lines are
reversed under layout=default, so expected LINE values differ per
layout.
2026-04-20 00:47:16 +09:00
Junegunn Choi faa6a7e9f6 Add test cases for click-header and click-footer
Injects SGR 1006 mouse events via tmux send-keys -l to exercise
FZF_CLICK_HEADER_* / FZF_CLICK_FOOTER_* across all three layouts,
with and without a header border.
2026-04-20 00:47:16 +09:00
Junegunn Choi 8b66489987 Clean up non-ascii characters 2026-04-20 00:47:16 +09:00
Vulcalien 780a624ed7 [vim] Move and resize popup window when detecting VimResized event
Close #4778
2026-04-20 00:25:02 +09:00
Tymoteusz Makowski 1ad7c617b1 fix: add no-modifiers fallbacks
Close #4776
2026-04-19 14:38:36 +09:00
Daniel Levin 06e70570e2 Update Makefile to support builds on Illumos
Close #4772
2026-04-13 20:07:55 +09:00
Junegunn Choi bd4a18d0a9 Replace sort.Sort(ByOrder(...)) with slices.SortFunc 2026-04-12 10:43:47 +09:00
Junegunn Choi 1b0d448c6e Update README: --tmux -> --popup 2026-04-12 09:57:26 +09:00
Junegunn Choi f584cf38f7 [bash] Persist history deletion when histappend is on
Fix #4764

Patch suggested by @bitraid.
2026-04-11 17:03:25 +09:00
Junegunn Choi 0eb2ae9f8b Fix gutter display in --style=minimal 2026-04-08 23:28:10 +09:00
Junegunn Choi 1831500018 Update README
I was unable to make the sponsors action to work with the new branch
protection rule. So I'm removing the sponsors section for now until
I can properly set it up again.
2026-04-05 22:20:25 +09:00
Junegunn Choi 62899fd74e 0.71.0 2026-04-04 14:04:34 +09:00
Junegunn Choi d1cea64a0e Allow adaptive height with negative value
Fix #4682
2026-04-04 10:13:19 +09:00
bitraid d7a132b8bf Update changelog 2026-04-04 09:48:00 +09:00
bitraid 59b82670f7 fish: Key bindings now requires fish v3.4.0+ 2026-04-04 09:47:59 +09:00
bitraid c516acaa89 Add .fish to .editorconfig shell script rules 2026-04-04 09:47:59 +09:00
bitraid 11ff4745ad Fix typo in install script 2026-04-04 09:47:59 +09:00
bitraid 23164c2263 fish: Completion script rewrite (SHIFT-TAB) 2026-04-04 09:47:59 +09:00
bitraid 9f8294de62 fish: Fixes and improvements to CTRL-R (#4703)
Fixes:
- Commands with trailing newlines
- Very long previews

Improvements:
- SHIFT-DELETE performance
- Bind ALT-T: cycle command prefix (timestamp, date/time, none)
- Set comment color for prefix
2026-04-04 09:47:59 +09:00
Junegunn Choi b7138e67a6 Update README 2026-04-03 18:58:57 +09:00
Junegunn Choi 0f0751b3b6 Add CODEOWNERS 2026-04-03 00:01:37 +09:00
dependabot[bot] cc16a97a40 Bump ruby/setup-ruby from 1.296.0 to 1.299.0 (#4753)
Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.296.0 to 1.299.0.
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Changelog](https://github.com/ruby/setup-ruby/blob/master/release.rb)
- [Commits](https://github.com/ruby/setup-ruby/compare/eab2afb99481ca09a4e91171a8e0aee0e89bfedd...3ff19f5e2baf30647122352b96108b1fbe250c64)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 00:10:35 +09:00
dagecko 3ccf162cf0 fix: pin 6 unpinned action(s) (#4750) 2026-03-30 13:20:11 +09:00
PJ Eby fff7eda9d7 Terminate child processes on Windows (#4723)
Windows doesn't have signals, the default Kill doesn't actually kill
anything, and other forms of termination don't allow cleanup. So we
spawn preview processes in a new process group and send them a
CTRL_BREAK_EVENT to terminate.

However, we only do this for "pwsh" (PowerShell 7+) and unknown/
posix-ish shells, because cmd.exe and Windows PowerShell
("powershell.exe") don't always exit on Ctrl-Break. pwsh also needs
the -NonInteractive flag to exit on Ctrl-Break.

If the process wasn't given its own group, or if sending the console
control event fails, we fall back to the standard Kill (which likely
won't help, but doesn't hurt to try).

Fix #3134
2026-03-29 12:40:24 +09:00
junegunn d198c0c9de Deploying to master from @ junegunn/fzf@55d5b153e6 🚀 2026-03-28 15:05:52 +00:00
harrywzl 55d5b153e6 docs: fix duplicate table rows and minor grammar/formatting in fzf.txt (#4743) 2026-03-27 15:01:12 +09:00
Junegunn Choi 57695b0309 Remove deprecated FZF_TMUX vars from shell script headers 2026-03-26 21:35:14 +09:00
Junegunn Choi 6f17d49dbb Support zellij floating pane via --popup (new name for --tmux) (#4145) 2026-03-26 21:32:56 +09:00
Junegunn Choi 87586ac5eb Only use --no-same-owner with tar when supported
Fix #4740
Fix #4741
2026-03-26 21:06:58 +09:00
Junegunn Choi 12be5e7b83 Skip adaptive height validation when --tmux overrides --height
Fix #4742
2026-03-26 20:56:36 +09:00
Qingwei Li 1acf980e95 enhancement: move server declaration to go func to allocate it in stack (#4739)
Fix #4735
Close #4736
2026-03-25 15:22:49 +09:00
cui 14e3995a06 Fix nthTransformer parts slice preallocation (#4734)
Use make([]NthParts, 0, len(indexes)) so the slice starts empty with
reserved capacity. The previous length-len(indexes) allocation left
leading zero NthParts entries before appended elements.
2026-03-23 09:55:00 +09:00
49 changed files with 1852 additions and 1041 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
root = true root = true
[*.{sh,bash}] [*.{sh,bash,fish}]
indent_style = space indent_style = space
indent_size = 2 indent_size = 2
simplify = true simplify = true
+1
View File
@@ -0,0 +1 @@
* @junegunn
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
go-version: "1.23" go-version: "1.23"
- name: Setup Ruby - name: Setup Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@3ff19f5e2baf30647122352b96108b1fbe250c64 # v1
with: with:
ruby-version: 3.4.6 ruby-version: 3.4.6
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
go-version: "1.23" go-version: "1.23"
- name: Setup Ruby - name: Setup Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@3ff19f5e2baf30647122352b96108b1fbe250c64 # v1
with: with:
ruby-version: 3.0.0 ruby-version: 3.0.0
-24
View File
@@ -1,24 +0,0 @@
---
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: '.'
+1 -1
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@v1.29.4 - uses: crate-ci/typos@685eb3d55be2f85191e8c84acb9f44d7756f84ab # v1.29.4
+1 -1
View File
@@ -7,7 +7,7 @@ jobs:
publish: publish:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: vedantmgoyal2009/winget-releaser@v2 - uses: vedantmgoyal2009/winget-releaser@4ffc7888bffd451b357355dc214d43bb9f23917e # v2
with: with:
identifier: junegunn.fzf identifier: junegunn.fzf
installers-regex: '-windows_(armv7|arm64|amd64)\.zip$' installers-regex: '-windows_(armv7|arm64|amd64)\.zip$'
+7 -7
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
- ``` - ```
───────┬────────────────────────────────────────────────────────── ───────┬──────────────────────────────────────────────────────────
+48 -6
View File
@@ -1,8 +1,40 @@
CHANGELOG CHANGELOG
========= =========
0.72.0
------
_Release highlights: https://junegunn.github.io/fzf/releases/0.72.0/_
- `--header-border`, `--header-lines-border`, and `--footer-border` now accept a new `inline` style that embeds the section inside the list frame, separated from the list content by a horizontal line. When the list border has side segments, the separator joins them as T-junctions.
- 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 \
--header 'Select a process' --header-lines 1 \
--bind 'load:transform-footer:echo $FZF_TOTAL_COUNT processes' \
--header-border dashed --header-first \
--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.
- New `dashed` border style with dashed edges (`` / ``) and rounded corners.
- `--border=dashed`, `--list-border=dashed`, etc.
- Works with inline sections (T-junctions render correctly).
- [vim] Move and resize popup window when detecting `VimResized` event (#4778) (@Vulcalien)
- Bug fixes
- Fixed gutter display in `--style=minimal`
- Fixed arrow keys / Home / End without modifiers being ignored under the kitty keyboard protocol (#4776) (@TymekDev)
- bash: Persist history deletion when `histappend` is on (#4764)
0.71.0 0.71.0
------ ------
_Release highlights: https://junegunn.github.io/fzf/releases/0.71.0/_
- Added `--popup` as a new name for `--tmux` with Zellij support
- `--popup` starts fzf in a tmux popup or a Zellij floating pane
- `--tmux` is now an alias for `--popup`
- Requires tmux 3.3+ or Zellij 0.44+
- Cross-reload item identity with `--id-nth` - Cross-reload item identity with `--id-nth`
- Added `--id-nth=NTH` to define item identity fields for cross-reload operations - Added `--id-nth=NTH` to define item identity fields for cross-reload operations
- When a `reload` is triggered with tracking enabled, fzf searches for the tracked item by its identity fields in the new list. - When a `reload` is triggered with tracking enabled, fzf searches for the tracked item by its identity fields in the new list.
@@ -17,24 +49,34 @@ CHANGELOG
- 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: 17.12ms current: 14.28ms (1.20x) matches: 179966 (12.79%) [all] baseline: 21.95ms current: 17.47ms (1.26x) matches: 179966 (12.79%)
[1T] baseline: 136.49ms current: 137.25ms (0.99x) matches: 179966 (12.79%) [1T] baseline: 179.63ms current: 180.53ms (1.00x) matches: 179966 (12.79%)
[2T] baseline: 75.74ms current: 68.75ms (1.10x) matches: 179966 (12.79%) [2T] baseline: 97.38ms current: 90.05ms (1.08x) matches: 179966 (12.79%)
[4T] baseline: 41.16ms current: 34.97ms (1.18x) matches: 179966 (12.79%) [4T] baseline: 53.83ms current: 44.77ms (1.20x) matches: 179966 (12.79%)
[8T] baseline: 32.82ms current: 17.79ms (1.84x) matches: 179966 (12.79%) [8T] baseline: 41.66ms current: 22.58ms (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: Improved command history (CTRL-R) (#4703) (@bitraid) - fish:
- 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
------ ------
+2
View File
@@ -53,6 +53,8 @@ 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)
+103 -60
View File
File diff suppressed because one or more lines are too long
+2 -4
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,7 +218,6 @@ 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 ( `>` )
@@ -229,7 +228,6 @@ 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
@@ -245,7 +243,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 the setting by printing You can examine the color option generated according to 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()
+9 -4
View File
@@ -2,7 +2,7 @@
set -u set -u
version=0.70.0 version=0.72.0
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=2 update_config=2
@@ -112,10 +112,15 @@ 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 --no-same-owner -xzf - curl -fL $1 | tar $tar_opts
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"
@@ -125,7 +130,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 --no-same-owner -xzf - wget -O - $1 | tar $tar_opts
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"
@@ -439,7 +444,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 ' fzf_user_key_bindings # fish' [[ $shells =~ fish && $lno_func -ne 0 ]] && echo ' fish_user_key_bindings # fish'
echo echo
echo 'Use uninstall script to remove fzf.' echo 'Use uninstall script to remove fzf.'
echo echo
+1 -1
View File
@@ -1,4 +1,4 @@
$version="0.70.0" $version="0.72.0"
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition $fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"github.com/junegunn/fzf/src/protector" "github.com/junegunn/fzf/src/protector"
) )
var version = "0.70" var version = "0.72"
var revision = "devel" var revision = "devel"
//go:embed shell/key-bindings.bash //go:embed shell/key-bindings.bash
+1 -1
View File
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf\-tmux 1 "Mar 2026" "fzf 0.70.0" "fzf\-tmux - open fzf in tmux split pane" .TH fzf\-tmux 1 "Apr 2026" "fzf 0.72.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
+40 -16
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 "Mar 2026" "fzf 0.70.0" "fzf - a command-line fuzzy finder" .TH fzf 1 "Apr 2026" "fzf 0.72.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,17 +394,19 @@ 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. range according to the input size. You can combine \fB~\fR with a negative
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
@@ -415,25 +417,26 @@ layout options so that the specified number of items are visible in the list
section (default: \fB10+\fR). section (default: \fB10+\fR).
Ignored when \fB\-\-height\fR is not specified or set as an absolute value. Ignored when \fB\-\-height\fR is not specified or set as an absolute value.
.TP .TP
.BI "\-\-tmux" "[=[center|top|bottom|left|right][,SIZE[%]][,SIZE[%]][,border-native]]" .BI "\-\-popup" "[=[center|top|bottom|left|right][,SIZE[%]][,SIZE[%]][,border-native]]"
Start fzf in a tmux popup (default \fBcenter,50%\fR). Requires tmux 3.3 or Start fzf in a tmux popup or in a Zellij floating pane (default
later. This option is ignored if you are not running fzf inside tmux. \fBcenter,50%\fR). Requires tmux 3.3+ or Zellij 0.44+. This option is ignored if you
are not running fzf inside tmux or Zellij. \fB\-\-tmux\fR is an alias for this option.
e.g. e.g.
\fB# Popup in the center with 70% width and height \fB# Popup in the center with 70% width and height
fzf \-\-tmux 70% fzf \-\-popup 70%
# Popup on the left with 40% width and 100% height # Popup on the left with 40% width and 100% height
fzf \-\-tmux right,40% fzf \-\-popup right,40%
# Popup on the bottom with 100% width and 30% height # Popup on the bottom with 100% width and 30% height
fzf \-\-tmux bottom,30% fzf \-\-popup bottom,30%
# Popup on the top with 80% width and 40% height # Popup on the top with 80% width and 40% height
fzf \-\-tmux top,80%,40% fzf \-\-popup top,80%,40%
# Popup with a native tmux border in the center with 80% width and height # Popup with a native tmux or Zellij border in the center with 80% width and height
fzf \-\-tmux center,80%,border\-native\fR fzf \-\-popup center,80%,border\-native\fR
.SS LAYOUT .SS LAYOUT
.TP .TP
@@ -514,6 +517,8 @@ Draw border around the finder
.br .br
.BR double " Border with double lines" .BR double " Border with double lines"
.br .br
.BR dashed " Border with dashed lines and rounded corners"
.br
.BR block " Border using block elements; suitable when using different background colors" .BR block " Border using block elements; suitable when using different background colors"
.br .br
.BR thinblock " Border using legacy computing symbols; may not be displayed on some terminals" .BR thinblock " Border using legacy computing symbols; may not be displayed on some terminals"
@@ -952,6 +957,8 @@ Should be used with one of the following \fB\-\-preview\-window\fR options.
.br .br
.B * border\-double .B * border\-double
.br .br
.B * border\-dashed
.br
.B * border\-block .B * border\-block
.br .br
.B * border\-thinblock .B * border\-thinblock
@@ -1097,7 +1104,17 @@ 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. separator line between the header window and the list section. \fBinline\fR
style embeds the header inside the list border frame, joined to the list
section by a horizontal separator; it requires a \fB\-\-list\-border\fR
shape that has both top and bottom segments (rounded / sharp / bold /
double / dashed / 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]
@@ -1113,6 +1130,10 @@ 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
@@ -1126,7 +1147,10 @@ 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. separator line between the footer and the list section. \fBinline\fR style
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]
+57 -25
View File
@@ -896,6 +896,7 @@ 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
@@ -1023,15 +1024,17 @@ function! s:callback(dict, lines) abort
endfunction endfunction
if has('nvim') if has('nvim')
function s:create_popup(opts) abort function! s:create_popup() 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 opts = extend({'relative': 'editor', 'style': 'minimal'}, a:opts) let s:popup_id = nvim_open_win(buf, v:true, opts)
let win = nvim_open_win(buf, v:true, opts) call setwinvar(s:popup_id, '&colorcolumn', '')
call setwinvar(win, '&colorcolumn', '')
" Colors " Colors
try try
call setwinvar(win, '&winhighlight', 'Pmenu:,Normal:Normal') call setwinvar(s:popup_id, '&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)
@@ -1039,40 +1042,61 @@ 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(win, ns) call nvim_win_set_hl_ns(s:popup_id, 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(opts) abort function! s:create_popup() abort
let s:popup_create = {buf -> popup_create(buf, #{ function! s:popup_create(buf)
\ line: a:opts.row, let s:popup_id = popup_create(a:buf, #{zindex: 1000})
\ col: a:opts.col, call s:resize_popup()
\ minwidth: a:opts.width, endfunction
\ 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(opts) abort function! s:popup_bounds() abort
let xoffset = get(a:opts, 'xoffset', 0.5) let opts = s:popup_opts
let yoffset = get(a:opts, 'yoffset', 0.5)
let relative = get(a:opts, 'relative', 0) let xoffset = get(opts, 'xoffset', 0.5)
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, a:opts.width > 1 ? a:opts.width : float2nr(columns * a:opts.width)]), columns]) let width = min([max([8, opts.width > 1 ? opts.width : float2nr(columns * opts.width)]), columns])
let height = min([max([4, a:opts.height > 1 ? a:opts.height : float2nr(lines * a:opts.height)]), lines]) let height = min([max([4, opts.height > 1 ? opts.height : float2nr(lines * 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)
@@ -1082,9 +1106,17 @@ function! s:popup(opts) abort
let row += !has('nvim') let row += !has('nvim')
let col += !has('nvim') let col += !has('nvim')
call s:create_popup({ return { 'row': row, 'col': col, 'width': width, 'height': height }
\ 'row': row, 'col': col, 'width': width, 'height': height endfunction
\ })
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
View File
@@ -1,141 +0,0 @@
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
-2
View File
@@ -4,8 +4,6 @@
# / __/ / /_/ __/ # / __/ / /_/ __/
# /_/ /___/_/ 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)
+160 -232
View File
@@ -4,238 +4,166 @@
# / __/ / /_/ __/ # / __/ / /_/ __/
# /_/ /___/_/ completion.fish # /_/ /___/_/ completion.fish
# #
# - $FZF_COMPLETION_OPTS (default: empty) # - $FZF_COMPLETION_OPTS
# - $FZF_EXPANSION_OPTS
function fzf_completion_setup # The oldest supported fish version is 3.4.0. For this message being able to be
# displayed on older versions, the command substitution syntax $() should not
#----BEGIN INCLUDE common.fish # be used anywhere in the script, otherwise the source command will fail.
# NOTE: Do not directly edit this section, which is copied from "common.fish". if string match -qr -- '^[12]\\.|^3\\.[0-3]' $version
# To modify it, one can edit "common.fish" and run "./update.sh" to apply echo "fzf completion script requires fish version 3.4.0 or newer." >&2
# the changes. See code comments in "common.fish" for the implementation details. return 1
else if not command -q fzf
function __fzf_defaults echo "fzf was not found in path." >&2
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40% return 1
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'
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'
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
if not string match -q -- '.' $dir; or string match -q -r -- '^\./|^\.$' $fzf_query
if test "$fish_major" -ge 4
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
string match -q -r -- '^/?(?<fzf_query>[\s\S]*?(?=\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
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
# Bind Shift-Tab to fzf-completion (Tab retains native Fish behavior)
if test (string match -r -- '^\d+' $version) -ge 4
bind shift-tab fzf-completion
bind -M insert shift-tab fzf-completion
else
bind -k btab fzf-completion
bind -M insert -k btab fzf-completion
end
end end
# Run setup function fzf_complete -w fzf -d 'fzf command completion and wildcard expansion search'
fzf_completion_setup # Restore the default shift-tab behavior on tab completions
if commandline --paging-mode
commandline -f complete-and-search
return
end
# Remove any trailing unescaped backslash from token and update command line
set -l -- token (string replace -r -- '(?<!\\\\)(?:\\\\\\\\)*\\K\\\\$' '' (commandline -t | string collect) | string collect)
commandline -rt -- $token
# Remove any line breaks from token
set -- token (string replace -ra -- '\\\\\\n' '' $token | string collect)
# 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
$fzf_cmd | while read -l r; set -a -- result $r; end
end
# Wildcard expansion
else if test -n "$glob_pattern"
# Set the command to be run by fzf, so there is a visual indicator and an
# easy way to abort on long recursive searches.
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
set -- tabstop 4
end
# Determine the tabstop length for description alignment
set -l -- max_columns (math $COLUMNS - 40)
for i in $list[1..500]
set -l -- item (string split -f 1 -- \t $i)
and set -l -- len (string length -V -- $item)
and test "$len" -gt "$tabstop" -a "$len" -lt "$max_columns"
and set -- tabstop $len
end
set -- tabstop (math $tabstop + 4)
set -- result (string collect -- $list | $fzf_cmd --delimiter="\t" --tabstop=$tabstop --wrap-sign=\t"↳ " --accept-nth=1)
end
end
# Update command line
if test -n "$result"
# No extra space after single selection that ends with path separator
set -l -- tail ' '
test (count $result) -eq 1
and string match -q -- '*/' "$result"
and set -- tail ''
commandline -rt -- (string join -- ' ' $result)$tail
end
commandline -f repaint
end
function _fzf_complete
set -l fzf_args
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
-2
View File
@@ -4,8 +4,6 @@
# / __/ / /_/ __/ # / __/ / /_/ __/
# /_/ /___/_/ 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)
+4 -1
View File
@@ -4,7 +4,6 @@
# / __/ / /_/ __/ # / __/ / /_/ __/
# /_/ /___/_/ 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
@@ -85,6 +84,10 @@ __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
+46 -113
View File
@@ -4,7 +4,6 @@
# / __/ / /_/ __/ # / __/ / /_/ __/
# /_/ /___/_/ 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
@@ -12,35 +11,29 @@
# - $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
# Check fish version # The oldest supported fish version is 3.4.0. For this message being able to be
if set -l -- fish_ver (string match -r '^(\d+)\.(\d+)' $version 2>/dev/null) # displayed on older versions, the command substitution syntax $() should not
and test "$fish_ver[2]" -lt 3 -o "$fish_ver[2]" -eq 3 -a "$fish_ver[3]" -lt 1 # be used anywhere in the script, otherwise the source command will fail.
echo "This script requires fish version 3.1b1 or newer." >&2 if string match -qr -- '^[12]\\.|^3\\.[0-3]' $version
echo "fzf key bindings script requires fish version 3.4.0 or newer." >&2
return 1 return 1
else if not type -q fzf else if not command -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..-1] $FZF_DEFAULT_OPTS $argv[2..]
end end
function __fzfcmd function __fzfcmd
@@ -54,107 +47,59 @@ 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 -- fish_major (string match -r -- '^\d+' $version) set -l -- match_regex '(?<fzf_query>[\\s\\S]*?(?=\\n?$)$)'
set -l -- fish_minor (string match -r -- '^\d+\.(\d+)' $version)[2] set -l -- prefix_regex '^-[^\\s=]+=|^-(?!-)\\S'
set -l -- match_regex '(?<fzf_query>[\s\S]*?(?=\n?$)$)' # Don't use option prefix if " -- " is preceded.
set -l -- prefix_regex '^-[^\s=]+=|^-(?!-)\S' string match -qv -- '* -- *' (string sub -l (commandline -Cp) -- (commandline -p))
if test "$fish_major" -eq 3 -a "$fish_minor" -lt 3 and set -- match_regex "(?<prefix>$prefix_regex)?$match_regex"
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 # Set $prefix and expanded $fzf_query with preserved trailing newlines.
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 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 else
set -l -- cl_token (commandline --current-token --tokenize | string collect -N) string match -q -r -- $match_regex (commandline --current-token --tokenize | string collect -N)
set -- prefix (string match -r -- $prefix_regex $cl_token) eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\\(?=~)|\\\\(?=\\$\\w)' '')
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"
if test \( "$fish_major" -ge 4 \) -o \( "$fish_major" -eq 3 -a "$fish_minor" -ge 5 \) # Normalize path in $fzf_query, set $dir to the longest existing directory.
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
if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2 string match -q -r -- '(?<fzf_query>^[\\s\\S]*?(?=\\n?$)$)' \
string match -q -r -- '(?<fzf_query>^[\s\S]*?(?=\n?$)$)' \ (string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\\n)$' '' $fzf_query | string collect -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 -q -r -- '^\./|^\.$' $fzf_query if not string match -q -- '.' $dir; or string match -qr -- '^\\.(/|$)' $fzf_query
if test "$fish_major" -ge 4 # Strip $dir from $fzf_query - preserve trailing newlines.
string match -q -r -- '^'(string escape --style=regex -- $dir)'/?(?<fzf_query>[\s\S]*)' $fzf_query if string match -qr -- '^\\d\\d+|^[4-9]' $version
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2 # fish v4.0.0 and newer
string match -q -r -- '^/?(?<fzf_query>[\s\S]*?(?=\n?$)$)' \ string match -q -r -- '^'(string escape --style=regex -- $dir)'/?(?<fzf_query>[\\s\\S]*)' $fzf_query
(string replace -- "$dir" '' $fzf_query | string collect -N)
else else
set -- fzf_query (string replace -- "$dir" '' $fzf_query | string collect -N) string match -q -r -- '^/?(?<fzf_query>[\\s\\S]*?(?=\\n?$)$)' \
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^/?|\\\n$' '') (string replace -- "$dir" '' $fzf_query | string collect -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"
@@ -171,7 +116,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 --no-quoted -- $result))' ' and commandline -rt -- (string join -- ' ' $prefix(string escape -n -- $result))' '
commandline -f repaint commandline -f repaint
end end
@@ -183,45 +128,33 @@ 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 '' \
'--nth=2..,.. --scheme=history --multi --no-multi-line --no-wrap --wrap-sign="\t\t\t↳ " --preview-wrap-sign="↳ "' \ '--with-nth=2.. --nth=2..,.. --scheme=history --multi --no-multi-line' \
'--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)\'' \ '--no-wrap --wrap-sign="\t\t\t↳ " --preview-wrap-sign="↳ " --freeze-left=1' \
'--bind="alt-enter:become(string join0 -- (string collect -- {+2..} | fish_indent -i))"' \ '--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-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=2.. --delimiter="\t" --tabstop=4 --read0 --print0 --with-shell='(status fish-path)\\ -c) '--accept-nth=3.. --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"
# Convert the highlighted timestamp using the date command if available # Prepend the options to allow user overrides
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,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"' \ '--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"' \
'--preview="string collect -- (test \\"$FZF_SELECT_COUNT\\" -gt 0; and string collect -- {+2..}) \\"\\n# \\"'$date_cmd' {2..} | fish_indent --ansi"' \ '--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-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 --show-time="%s%t"' set -lx -- FZF_DEFAULT_COMMAND 'builtin history -z'
# Enable syntax highlighting colors on fish v4.3.3 and newer # Enable syntax highlighting colors on fish v4.3.3 and newer
if set -l -- v (string match -r -- '^(\d+)\.(\d+)(?:\.(\d+))?' $version) if string match -qr -- '^\\d\\d+|^4\\.[4-9]|^4\\.3\\.[3-9]' $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' 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)'
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
-1
View File
@@ -4,7 +4,6 @@
# / __/ / /_/ __/ # / __/ / /_/ __/
# /_/ /___/_/ 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
+12 -14
View File
@@ -8,26 +8,24 @@ dir=${0%"${0##*/}"}
update() { update() {
{ {
sed -n "1,/^#----BEGIN INCLUDE $1/p" "$2" sed -n '1,/^#----BEGIN INCLUDE common\.sh/p' "$1"
cat << EOF cat << EOF
# NOTE: Do not directly edit this section, which is copied from "$1". # NOTE: Do not directly edit this section, which is copied from "common.sh".
# To modify it, one can edit "$1" and run "./update.sh" to apply # To modify it, one can edit "common.sh" and run "./update.sh" to apply
# the changes. See code comments in "$1" for the implementation details. # the changes. See code comments in "common.sh" for the implementation details.
EOF EOF
echo echo
grep -v '^[[:blank:]]*#' "$dir/$1" # remove code comments from the common file grep -v '^[[:blank:]]*#' "$dir/common.sh" # remove code comments in common.sh
sed -n '/^#----END INCLUDE/,$p' "$2" sed -n '/^#----END INCLUDE/,$p' "$1"
} > "$2.part" } > "$1.part"
mv -f "$2.part" "$2" mv -f "$1.part" "$1"
} }
update "common.sh" "$dir/completion.bash" update "$dir/completion.bash"
update "common.sh" "$dir/completion.zsh" update "$dir/completion.zsh"
update "common.sh" "$dir/key-bindings.bash" update "$dir/key-bindings.bash"
update "common.sh" "$dir/key-bindings.zsh" update "$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
+5 -5
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.
+1 -1
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
+1 -1
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
+3
View File
@@ -56,6 +56,9 @@ func Run(opts *Options) (int, error) {
if opts.useTmux() { if opts.useTmux() {
return runTmux(os.Args, opts) return runTmux(os.Args, opts)
} }
if opts.useZellij() {
return runZellij(os.Args, opts)
}
if needWinpty(opts) { if needWinpty(opts) {
return runWinpty(os.Args, opts) return runWinpty(os.Args, opts)
+46 -22
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.
@@ -75,16 +75,17 @@ Usage: fzf [options]
--min-height=HEIGHT[+] Minimum height when --height is given as a percentage. --min-height=HEIGHT[+] Minimum height when --height is given as a percentage.
Add '+' to automatically increase the value Add '+' to automatically increase the value
according to the other layout options (default: 10+). according to the other layout options (default: 10+).
--tmux[=OPTS] Start fzf in a tmux popup (requires tmux 3.3+) --popup[=OPTS] Start fzf in a popup window (requires tmux 3.3+ or Zellij 0.44+)
[center|top|bottom|left|right][,SIZE[%]][,SIZE[%]] [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
[,border-native] (default: center,50%) [,border-native] (default: center,50%)
--tmux[=OPTS] Alias for --popup
LAYOUT LAYOUT
--layout=LAYOUT Choose layout: [default|reverse|reverse-list] --layout=LAYOUT Choose layout: [default|reverse|reverse-list]
--margin=MARGIN Screen margin (TRBL | TB,RL | T,RL,B | T,R,B,L) --margin=MARGIN Screen margin (TRBL | TB,RL | T,RL,B | T,R,B,L)
--padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L) --padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L)
--border[=STYLE] Draw border around the finder --border[=STYLE] Draw border around the finder
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical| [rounded|sharp|bold|block|thinblock|double|dashed|horizontal|vertical|
top|bottom|left|right|line|none] (default: rounded) top|bottom|left|right|line|none] (default: rounded)
--border-label=LABEL Label to print on the border --border-label=LABEL Label to print on the border
--border-label-pos=COL Position of the border label --border-label-pos=COL Position of the border label
@@ -127,7 +128,7 @@ Usage: fzf [options]
(each for list section and preview window) (each for list section and preview window)
--no-scrollbar Hide scrollbar --no-scrollbar Hide scrollbar
--list-border[=STYLE] Draw border around the list section --list-border[=STYLE] Draw border around the list section
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical| [rounded|sharp|bold|block|thinblock|double|dashed|horizontal|vertical|
top|bottom|left|right|none] (default: rounded) top|bottom|left|right|none] (default: rounded)
--list-label=LABEL Label to print on the list border --list-label=LABEL Label to print on the list border
--list-label-pos=COL Position of the list label --list-label-pos=COL Position of the list label
@@ -147,7 +148,7 @@ Usage: fzf [options]
--ghost=TEXT Ghost text to display when the input is empty --ghost=TEXT Ghost text to display when the input is empty
--filepath-word Make word-wise movements respect path separators --filepath-word Make word-wise movements respect path separators
--input-border[=STYLE] Draw border around the input section --input-border[=STYLE] Draw border around the input section
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical| [rounded|sharp|bold|block|thinblock|double|dashed|horizontal|vertical|
top|bottom|left|right|line|none] (default: rounded) top|bottom|left|right|line|none] (default: rounded)
--input-label=LABEL Label to print on the input border --input-label=LABEL Label to print on the input border
--input-label-pos=COL Position of the input label --input-label-pos=COL Position of the input label
@@ -164,7 +165,7 @@ Usage: fzf [options]
[,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES] [,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES]
[,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)] [,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]
--preview-border[=STYLE] Short for --preview-window=border-STYLE --preview-border[=STYLE] Short for --preview-window=border-STYLE
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical| [rounded|sharp|bold|block|thinblock|double|dashed|horizontal|vertical|
top|bottom|left|right|line|none] (default: rounded) top|bottom|left|right|line|none] (default: rounded)
--preview-label=LABEL --preview-label=LABEL
--preview-label-pos=N Same as --border-label and --border-label-pos, --preview-label-pos=N Same as --border-label and --border-label-pos,
@@ -176,11 +177,12 @@ Usage: fzf [options]
--header-lines=N The first N lines of the input are treated as header --header-lines=N The first N lines of the input are treated as header
--header-first Print header before the prompt line --header-first Print header before the prompt line
--header-border[=STYLE] Draw border around the header section --header-border[=STYLE] Draw border around the header section
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical| [rounded|sharp|bold|block|thinblock|double|dashed|horizontal|vertical|
top|bottom|left|right|line|none] (default: rounded) top|bottom|left|right|line|inline|none] (default: rounded)
--header-lines-border[=STYLE] --header-lines-border[=STYLE]
Display header from --header-lines with a separate border. Display header from --header-lines with a separate border.
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|
@@ -190,8 +192,8 @@ Usage: fzf [options]
FOOTER FOOTER
--footer=STR String to print as footer --footer=STR String to print as footer
--footer-border[=STYLE] Draw border around the footer section --footer-border[=STYLE] Draw border around the footer section
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical| [rounded|sharp|bold|block|thinblock|double|dashed|horizontal|vertical|
top|bottom|left|right|line|none] (default: line) top|bottom|left|right|line|inline|none] (default: line)
--footer-label=LABEL Label to print on the footer border --footer-label=LABEL Label to print on the footer border
--footer-label-pos=COL Position of the footer label --footer-label-pos=COL Position of the footer label
[POSITIVE_INTEGER: columns from left| [POSITIVE_INTEGER: columns from left|
@@ -417,7 +419,7 @@ func parseTmuxOptions(arg string, index int) (*tmuxOptions, error) {
var err error var err error
opts := defaultTmuxOptions(index) opts := defaultTmuxOptions(index)
tokens := splitRegexp.Split(arg, -1) tokens := splitRegexp.Split(arg, -1)
errorToReturn := errors.New("invalid tmux option: " + arg + " (expected: [center|top|bottom|left|right][,SIZE[%]][,SIZE[%][,border-native]])") errorToReturn := errors.New("invalid popup option: " + arg + " (expected: [center|top|bottom|left|right][,SIZE[%]][,SIZE[%][,border-native]])")
if len(tokens) == 0 || len(tokens) > 4 { if len(tokens) == 0 || len(tokens) > 4 {
return nil, errorToReturn return nil, errorToReturn
} }
@@ -869,7 +871,7 @@ func nthTransformer(str string) (func(Delimiter) func([]Token, int32) string, er
nth []Range nth []Range
} }
parts := make([]NthParts, len(indexes)) parts := make([]NthParts, 0, len(indexes))
idx := 0 idx := 0
for _, index := range indexes { for _, index := range indexes {
if idx < index[0] { if idx < index[0] {
@@ -952,6 +954,8 @@ 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":
@@ -964,6 +968,8 @@ func parseBorder(str string, optional bool) (tui.BorderShape, error) {
return tui.BorderThinBlock, nil return tui.BorderThinBlock, nil
case "double": case "double":
return tui.BorderDouble, nil return tui.BorderDouble, nil
case "dashed":
return tui.BorderDashed, nil
case "horizontal": case "horizontal":
return tui.BorderHorizontal, nil return tui.BorderHorizontal, nil
case "vertical": case "vertical":
@@ -982,7 +988,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|none)") return tui.BorderNone, errors.New("invalid border style (expected: rounded|sharp|bold|block|thinblock|double|dashed|horizontal|vertical|top|bottom|left|right|line|inline|none)")
} }
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) {
@@ -1566,7 +1572,7 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, *tui
case "info": case "info":
mergeAttr(&theme.Info) mergeAttr(&theme.Info)
case "pointer": case "pointer":
mergeAttr(&theme.Cursor) mergeAttr(&theme.Pointer)
case "marker": case "marker":
mergeAttr(&theme.Marker) mergeAttr(&theme.Marker)
case "header", "header-fg": case "header", "header-fg":
@@ -2221,9 +2227,6 @@ 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:]
} }
@@ -2340,6 +2343,8 @@ func parsePreviewWindowImpl(opts *previewOpts, input string) error {
opts.border = tui.BorderThinBlock opts.border = tui.BorderThinBlock
case "border-double": case "border-double":
opts.border = tui.BorderDouble opts.border = tui.BorderDouble
case "border-dashed":
opts.border = tui.BorderDashed
case "noborder", "border-none": case "noborder", "border-none":
opts.border = tui.BorderNone opts.border = tui.BorderNone
case "border-horizontal": case "border-horizontal":
@@ -2636,7 +2641,7 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
opts.Version = true opts.Version = true
case "--no-winpty": case "--no-winpty":
opts.NoWinpty = true opts.NoWinpty = true
case "--tmux": case "--tmux", "--popup":
given, str := optionalNextString() given, str := optionalNextString()
if given { if given {
if opts.Tmux, err = parseTmuxOptions(str, index); err != nil { if opts.Tmux, err = parseTmuxOptions(str, index); err != nil {
@@ -2645,7 +2650,7 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
} else { } else {
opts.Tmux = defaultTmuxOptions(index) opts.Tmux = defaultTmuxOptions(index)
} }
case "--no-tmux": case "--no-tmux", "--no-popup":
opts.Tmux = nil opts.Tmux = nil
case "--tty-default": case "--tty-default":
if opts.TtyDefault, err = nextString("tty device name required"); err != nil { if opts.TtyDefault, err = nextString("tty device name required"); err != nil {
@@ -3151,7 +3156,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
} }
@@ -3518,7 +3523,9 @@ 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.ColorAttr{Color: -1, Attr: 0} opts.Theme.Gutter = tui.NewColorAttr()
space := " "
opts.Gutter = &space
empty := "" empty := ""
opts.Separator = &empty opts.Separator = &empty
opts.Scrollbar = &empty opts.Scrollbar = &empty
@@ -3593,7 +3600,7 @@ func validateOptions(opts *Options) error {
} }
} }
if opts.Height.auto { if opts.Height.auto && (opts.Tmux == nil || opts.Tmux.index < opts.Height.index) {
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")
@@ -3610,6 +3617,19 @@ 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
} }
@@ -3627,6 +3647,10 @@ func (opts *Options) useTmux() bool {
return opts.Tmux != nil && len(os.Getenv("TMUX")) > 0 && opts.Tmux.index >= opts.Height.index return opts.Tmux != nil && len(os.Getenv("TMUX")) > 0 && opts.Tmux.index >= opts.Height.index
} }
func (opts *Options) useZellij() bool {
return opts.Tmux != nil && len(os.Getenv("ZELLIJ")) > 0 && opts.Tmux.index >= opts.Height.index
}
func (opts *Options) noSeparatorLine() bool { func (opts *Options) noSeparatorLine() bool {
if opts.Inputless { if opts.Inputless {
return true return true
+26
View File
@@ -23,6 +23,32 @@ func escapeSingleQuote(str string) string {
return "'" + strings.ReplaceAll(str, "'", "'\\''") + "'" return "'" + strings.ReplaceAll(str, "'", "'\\''") + "'"
} }
func popupArgStr(args []string, opts *Options) (string, string) {
fzf, rest := args[0], args[1:]
args = []string{"--bind=ctrl-z:ignore"}
if !opts.Tmux.border && (opts.BorderShape == tui.BorderUndefined || opts.BorderShape == tui.BorderLine) {
if tui.DefaultBorderShape == tui.BorderRounded {
rest = append(rest, "--border=rounded")
} else {
rest = append(rest, "--border=sharp")
}
}
if opts.Tmux.border && opts.Margin == defaultMargin() {
args = append(args, "--margin=0,1")
}
argStr := escapeSingleQuote(fzf)
for _, arg := range append(args, rest...) {
argStr += " " + escapeSingleQuote(arg)
}
argStr += ` --no-popup --no-height`
dir, err := os.Getwd()
if err != nil {
dir = "."
}
return argStr, dir
}
func fifo(name string) (string, error) { func fifo(name string) (string, error) {
ns := time.Now().UnixNano() ns := time.Now().UnixNano()
output := filepath.Join(os.TempDir(), fmt.Sprintf("fzf-%s-%d", name, ns)) output := filepath.Join(os.TempDir(), fmt.Sprintf("fzf-%s-%d", name, ns))
+17 -17
View File
@@ -2,6 +2,7 @@ package fzf
import ( import (
"math" "math"
"slices"
"sort" "sort"
"unicode" "unicode"
@@ -30,7 +31,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 {
sort.Sort(ByOrder(offsets)) slices.SortFunc(offsets, compareOffsets)
} }
minBegin := math.MaxUint16 minBegin := math.MaxUint16
@@ -187,7 +188,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
} }
} }
// sort.Sort(ByOrder(offsets)) // slices.SortFunc(offsets, compareOffsets)
// Merge offsets // Merge offsets
// ------------ ---- -- ---- // ------------ ---- -- ----
@@ -297,21 +298,20 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
return colors return colors
} }
// ByOrder is for sorting substring offsets func compareOffsets(a, b Offset) int {
type ByOrder []Offset if a[0] < b[0] {
return -1
func (a ByOrder) Len() int { }
return len(a) if a[0] > b[0] {
} return 1
}
func (a ByOrder) Swap(i, j int) { if a[1] < b[1] {
a[i], a[j] = a[j], a[i] return -1
} }
if a[1] > b[1] {
func (a ByOrder) Less(i, j int) bool { return 1
ioff := a[i] }
joff := a[j] return 0
return (ioff[0] < joff[0]) || (ioff[0] == joff[0]) && (ioff[1] <= joff[1])
} }
// ByRelevance is for sorting Items // ByRelevance is for sorting Items
+2 -1
View File
@@ -3,6 +3,7 @@ package fzf
import ( import (
"math" "math"
"math/rand" "math/rand"
"slices"
"sort" "sort"
"testing" "testing"
@@ -19,7 +20,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}}
sort.Sort(ByOrder(offsets)) slices.SortFunc(offsets, compareOffsets)
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 ||
+5 -6
View File
@@ -122,13 +122,12 @@ 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 {
+344 -84
View File
@@ -13,6 +13,7 @@ import (
"os/exec" "os/exec"
"os/signal" "os/signal"
"regexp" "regexp"
"slices"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@@ -1166,8 +1167,15 @@ 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(), opts.HeaderBorderShape.Visible()) tui.InitTheme(opts.Theme, baseTheme, opts.Bold, opts.Black, opts.InputBorderShape.Visible(), hasHeader, headerInline, footerInline)
// Gutter character // Gutter character
var gutterChar, gutterRawChar string var gutterChar, gutterRawChar string
@@ -1226,6 +1234,22 @@ 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 {
@@ -1792,14 +1816,16 @@ func (t *Terminal) changeHeader(header string) bool {
return needFullRedraw return needFullRedraw
} }
func (t *Terminal) changeFooter(footer string) { func (t *Terminal) changeFooter(footer string) bool {
var lines []string var lines []string
if len(footer) > 0 { if len(footer) > 0 {
lines = strings.Split(strings.TrimSuffix(footer, "\n"), "\n") lines = strings.Split(strings.TrimSuffix(footer, "\n"), "\n")
} }
needFullRedraw := len(t.footer) != len(lines)
t.footer = lines t.footer = lines
t.clickFooterLine = 0 t.clickFooterLine = 0
t.clickFooterColumn = 0 t.clickFooterColumn = 0
return needFullRedraw
} }
// UpdateHeader updates the header // UpdateHeader updates the header
@@ -2131,7 +2157,7 @@ func (t *Terminal) adjustMarginAndPadding() (int, int, [4]int, [4]int) {
if idx == 3 { if idx == 3 {
extraMargin[idx] += 1 + bw extraMargin[idx] += 1 + bw
} }
case tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderBlock, tui.BorderThinBlock, tui.BorderDouble: case tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderBlock, tui.BorderThinBlock, tui.BorderDouble, tui.BorderDashed:
extraMargin[idx] += 1 + bw*(idx%2) extraMargin[idx] += 1 + bw*(idx%2)
} }
marginInt[idx] = sizeSpecToInt(idx, sizeSpec) + extraMargin[idx] marginInt[idx] = sizeSpecToInt(idx, sizeSpec) + extraMargin[idx]
@@ -2236,6 +2262,98 @@ 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
@@ -2243,7 +2361,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
width := screenWidth - marginInt[1] - marginInt[3] width := screenWidth - marginInt[1] - marginInt[3]
height := screenHeight - marginInt[0] - marginInt[2] height := screenHeight - marginInt[0] - marginInt[2]
t.prevLines = make([]itemLine, screenHeight) t.prevLines = make([]itemLine, max(1, screenHeight))
if t.border != nil && redrawBorder { if t.border != nil && redrawBorder {
t.border = nil t.border = nil
} }
@@ -2352,6 +2470,50 @@ 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 {
@@ -2359,37 +2521,62 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
if hasHeaderLinesWindow { if hasHeaderLinesWindow {
headerWindowHeight -= t.headerLines headerWindowHeight -= t.headerLines
} }
headerBorderHeight = util.Constrain(borderLines(t.headerBorderShape)+headerWindowHeight, 0, availableLines) if t.headerBorderShape == tui.BorderInline {
if t.layout == layoutReverse { addInline(headerWindowHeight, inlineRoleHeader)
shift += headerBorderHeight
shrink += headerBorderHeight
} else { } else {
shrink += headerBorderHeight headerBorderHeight = util.Constrain(borderLines(t.headerBorderShape)+headerWindowHeight, 0, availableLines)
if t.layout == layoutReverse {
shift += headerBorderHeight
shrink += headerBorderHeight
} else {
shrink += headerBorderHeight
}
availableLines -= headerBorderHeight
} }
availableLines -= headerBorderHeight
} }
headerLinesHeight := 0 headerLinesHeight := 0
if hasHeaderLinesWindow { if hasHeaderLinesWindow {
headerLinesHeight = util.Constrain(borderLines(headerLinesShape)+t.headerLines, 0, availableLines) if headerLinesShape == tui.BorderInline {
if t.layout != layoutDefault { addInline(t.headerLines, inlineRoleHeaderLines)
shift += headerLinesHeight
shrink += headerLinesHeight
} else { } else {
shrink += headerLinesHeight headerLinesHeight = util.Constrain(borderLines(headerLinesShape)+t.headerLines, 0, availableLines)
if t.layout != layoutDefault {
shift += headerLinesHeight
shrink += headerLinesHeight
} else {
shrink += headerLinesHeight
}
availableLines -= headerLinesHeight
} }
availableLines -= headerLinesHeight
} }
footerBorderHeight := 0 footerBorderHeight := 0
if hasFooterWindow { if hasFooterWindow {
// Footer lines should not take all available lines if t.footerBorderShape == tui.BorderInline {
footerBorderHeight = util.Constrain(borderLines(t.footerBorderShape)+len(t.footer), 0, availableLines) addInline(len(t.footer), inlineRoleFooter)
shrink += footerBorderHeight } else {
if t.layout != layoutReverse { // Footer lines should not take all available lines
shift += footerBorderHeight footerBorderHeight = util.Constrain(borderLines(t.footerBorderShape)+len(t.footer), 0, availableLines)
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
@@ -2500,12 +2687,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, innerMarginInt[3], innerWidth, innerHeight-pheight-shrink, tui.WindowList, noBorder, true) innerMarginInt[0]+pheight+shift+inlineTopLines, innerMarginInt[3], innerWidth, innerHeight-pheight-shrink-inlineTopLines-inlineBottomLines, 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, innerMarginInt[3], innerWidth, innerHeight-pheight-shrink, tui.WindowList, noBorder, true) innerMarginInt[0]+shift+inlineTopLines, innerMarginInt[3], innerWidth, innerHeight-pheight-shrink-inlineTopLines-inlineBottomLines, 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:
@@ -2544,7 +2731,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, innerMarginInt[3]+pwidth+m, innerWidth-pwidth-m, innerHeight-shrink, tui.WindowList, noBorder, true) innerMarginInt[0]+shift+inlineTopLines, innerMarginInt[3]+pwidth+m, innerWidth-pwidth-m, innerHeight-shrink-inlineTopLines-inlineBottomLines, 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
@@ -2576,7 +2763,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, innerMarginInt[3], innerWidth-pwidth, innerHeight-shrink, tui.WindowList, noBorder, true) innerMarginInt[0]+shift+inlineTopLines, innerMarginInt[3], innerWidth-pwidth, innerHeight-shrink-inlineTopLines-inlineBottomLines, 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)
} }
@@ -2614,10 +2801,15 @@ 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, innerMarginInt[0]+shift+inlineTopLines,
innerMarginInt[3], innerMarginInt[3],
innerWidth, innerWidth,
innerHeight-shrink, tui.WindowList, noBorder, true) innerHeight-shrink-inlineTopLines-inlineBottomLines, 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 {
@@ -2654,7 +2846,11 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
if hasInputWindow { if hasInputWindow {
var btop int var btop int
if (hasHeaderWindow || hasHeaderLinesWindow) && t.headerFirst { // Inline sections live inside the list frame, so they don't participate
// 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()
@@ -2699,7 +2895,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
} }
// Set up header border // Set up header border
if hasHeaderWindow { if hasHeaderWindow && t.headerBorderShape != tui.BorderInline {
var btop int var btop int
if hasInputWindow && t.headerFirst { if hasInputWindow && t.headerFirst {
if t.layout == layoutReverse { if t.layout == layoutReverse {
@@ -2727,7 +2923,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
} }
// Set up header lines border // Set up header lines border
if hasHeaderLinesWindow { if hasHeaderLinesWindow && headerLinesShape != tui.BorderInline {
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
@@ -2760,7 +2956,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
} }
// Set up footer // Set up footer
if hasFooterWindow { if hasFooterWindow && t.footerBorderShape != tui.BorderInline {
var btop int var btop int
if t.layout == layoutReverse { if t.layout == layoutReverse {
btop = w.Top() + w.Height() btop = w.Top() + w.Height()
@@ -2777,8 +2973,21 @@ 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)
} }
// Print border label // When the list label lands on an edge owned by an inline section, swap its bg
t.printLabel(t.wborder, t.listLabel, t.listLabelOpts, t.listLabelLen, t.listBorderShape, false) // so the label reads as part of that section's frame. Fg stays at list-label.
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)
@@ -2786,37 +2995,39 @@ 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 { if window == nil || window.Height() == 0 {
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, tui.BorderDashed:
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
} }
window.Move(row, col) t.printLabelAt(window, render, opts, length, row)
render(window, window.Width())
} }
} }
@@ -3184,8 +3395,19 @@ 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 || t.footerWindow.Height() != len(t.footer)) || if len(t.footer) > 0 && (t.footerWindow == nil || mismatch(t.footerBorderShape, 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
@@ -3199,14 +3421,12 @@ 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 && primaryHeaderLines != t.headerWindow.Height()) || (needHeaderWindow && t.headerWindow != nil && mismatch(t.headerBorderShape, t.headerWindow.Height(), primaryHeaderLines)) ||
(needHeaderLinesWindow && t.headerLinesWindow == nil) || (needHeaderLinesWindow && t.headerLinesWindow == nil) ||
(!needHeaderLinesWindow && t.headerLinesWindow != nil) || (!needHeaderLinesWindow && t.headerLinesWindow != nil) ||
(needHeaderLinesWindow && t.headerLinesWindow != nil && t.headerLines != t.headerLinesWindow.Height()) { (needHeaderLinesWindow && t.headerLinesWindow != nil && mismatch(t.headerLinesShape, t.headerLinesWindow.Height(), t.headerLines)) {
t.printAll() t.printAll()
return true return true
} }
@@ -3218,14 +3438,23 @@ func (t *Terminal) printHeader() {
return return
} }
t.withWindow(t.headerWindow, func() { // headerWindow is nil when hasHeaderWindow() returned false at resize time,
var headerItems []Item // e.g. --header-border=inline combined with empty header content. Don't
if !t.hasHeaderLinesWindow() { // delegate to printHeaderImpl because its nil-window branch folds the header
headerItems = t.header // into the list window, which isn't valid for inline. A nil window is only
} // legitimate when the shape is NOT inline (e.g. header combined with the
t.printHeaderImpl(t.headerWindow, t.headerBorderShape, t.header0, headerItems) // list when --no-list-border is in effect).
}) if !(t.headerBorderShape == tui.BorderInline && t.headerWindow == nil) {
if w, shape := t.determineHeaderLinesShape(); w { t.withWindow(t.headerWindow, func() {
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)
}) })
@@ -3276,7 +3505,10 @@ 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
} }
if borderShape.HasLeft() { // Section borders with their own left side skip past the list border's left column.
// 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
@@ -3372,18 +3604,18 @@ func (t *Terminal) renderEmptyLine(line int, barRange [2]int) {
func (t *Terminal) gutter(current bool, alt bool) { func (t *Terminal) gutter(current bool, alt bool) {
var color tui.ColorPair var color tui.ColorPair
if current { if current {
color = tui.ColCurrentCursorEmpty color = tui.ColCurrentPointerEmpty
} else if !t.raw && t.gutterReverse || t.raw && t.gutterRawReverse { } else if !t.raw && t.gutterReverse || t.raw && t.gutterRawReverse {
if alt { if alt {
color = tui.ColAltCursorEmpty color = tui.ColAltPointerEmpty
} else { } else {
color = tui.ColCursorEmpty color = tui.ColPointerEmpty
} }
} else { } else {
if alt { if alt {
color = tui.ColAltCursorEmptyChar color = tui.ColAltPointerEmptyChar
} else { } else {
color = tui.ColCursorEmptyChar color = tui.ColPointerEmptyChar
} }
} }
gutter := t.pointerEmpty gutter := t.pointerEmpty
@@ -3571,7 +3803,7 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
if len(label) == 0 { if len(label) == 0 {
t.gutter(true, false) t.gutter(true, false)
} else { } else {
t.window.CPrint(tui.ColCurrentCursor, label) t.window.CPrint(tui.ColCurrentPointer, label)
} }
if w-t.markerLen < 0 { if w-t.markerLen < 0 {
return indentSize return indentSize
@@ -3600,7 +3832,7 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
if len(label) == 0 { if len(label) == 0 {
t.gutter(false, index%2 == 1) t.gutter(false, index%2 == 1)
} else { } else {
t.window.CPrint(tui.ColCursor, label) t.window.CPrint(tui.ColPointer, label)
} }
if w-t.markerLen < 0 { if w-t.markerLen < 0 {
return indentSize return indentSize
@@ -3751,7 +3983,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
} }
sort.Sort(ByOrder(charOffsets)) slices.SortFunc(charOffsets, compareOffsets)
} }
// 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.
@@ -3788,7 +4020,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)}
} }
sort.Sort(ByOrder(nthOffsets)) slices.SortFunc(nthOffsets, compareOffsets)
} }
} }
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)
@@ -5951,11 +6183,28 @@ 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:
t.printLabel(t.headerBorder, t.headerLabel, t.headerLabelOpts, t.headerLabelLen, t.headerBorderShape, true) if t.headerBorderShape == tui.BorderInline {
// 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:
t.printLabel(t.footerBorder, t.footerLabel, t.footerLabelOpts, t.footerLabelLen, t.footerBorderShape, true) if t.footerBorderShape == tui.BorderInline {
t.printAll()
} else {
t.printLabel(t.footerBorder, t.footerLabel, t.footerLabelOpts, t.footerLabelLen, t.footerBorderShape, true)
}
case reqRedrawListLabel: case reqRedrawListLabel:
t.printLabel(t.wborder, t.listLabel, t.listLabelOpts, t.listLabelLen, t.listBorderShape, true) // When inline sections are active, the label's bg depends on which
// 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:
@@ -6534,16 +6783,27 @@ 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) {
req(reqList, reqPrompt, reqInfo) // resizeIfNeeded() tolerates a shorter-than-wanted inline
// window, so a length change can leave the inline slot
// stale. Force a redraw to re-run the layout. Non-inline
// shapes are handled by resizeIfNeeded.
if t.headerBorderShape == tui.BorderInline {
req(reqRedraw)
} else {
req(reqList, reqPrompt, reqInfo)
}
} }
req(reqHeader) req(reqHeader)
}) })
case actChangeFooter, actTransformFooter, actBgTransformFooter: case actChangeFooter, actTransformFooter, actBgTransformFooter:
capture(false, func(footer string) { capture(false, func(footer string) {
t.changeFooter(footer) if t.changeFooter(footer) && t.footerBorderShape == tui.BorderInline {
// resizeIfNeeded() tolerates a shorter-than-wanted inline
// window, so a length change can leave the inline slot
// stale. Force a redraw to re-run the layout.
req(reqRedraw)
}
req(reqFooter) req(reqFooter)
}) })
case actChangeHeaderLabel, actTransformHeaderLabel, actBgTransformHeaderLabel: case actChangeHeaderLabel, actTransformHeaderLabel, actBgTransformHeaderLabel:
+2 -2
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" {
+1 -29
View File
@@ -1,39 +1,11 @@
package fzf package fzf
import ( import (
"os"
"os/exec" "os/exec"
"github.com/junegunn/fzf/src/tui"
) )
func runTmux(args []string, opts *Options) (int, error) { func runTmux(args []string, opts *Options) (int, error) {
// Prepare arguments argStr, dir := popupArgStr(args, opts)
fzf, rest := args[0], args[1:]
args = []string{"--bind=ctrl-z:ignore"}
if !opts.Tmux.border && (opts.BorderShape == tui.BorderUndefined || opts.BorderShape == tui.BorderLine) {
// We append --border option at the end, because `--style=full:STYLE`
// may have changed the default border style.
if tui.DefaultBorderShape == tui.BorderRounded {
rest = append(rest, "--border=rounded")
} else {
rest = append(rest, "--border=sharp")
}
}
if opts.Tmux.border && opts.Margin == defaultMargin() {
args = append(args, "--margin=0,1")
}
argStr := escapeSingleQuote(fzf)
for _, arg := range append(args, rest...) {
argStr += " " + escapeSingleQuote(arg)
}
argStr += ` --no-tmux --no-height`
// Get current directory
dir, err := os.Getwd()
if err != nil {
dir = "."
}
// Set tmux options for popup placement // Set tmux options for popup placement
// C Both The centre of the terminal // C Both The centre of the terminal
+133 -109
View File
@@ -689,6 +689,7 @@ 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
@@ -743,6 +744,7 @@ 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}
@@ -765,6 +767,7 @@ 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}
@@ -787,6 +790,7 @@ 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}
@@ -809,6 +813,7 @@ 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}
@@ -831,6 +836,7 @@ 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}
@@ -853,6 +859,7 @@ 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]
@@ -1122,127 +1129,144 @@ 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
} }
switch w.border.shape { shape := w.border.shape
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble: if shape == BorderNone {
w.drawBorderAround(onlyHorizontal) return
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()
func (w *LightWindow) drawBorderHorizontal(top, bottom bool) { if shape.HasTop() {
color := ColBorder var leftCap, rightCap rune
switch w.windowType { if hasLeft {
case WindowList: leftCap = w.border.topLeft
color = ColListBorder
case WindowInput:
color = ColInputBorder
case WindowHeader:
color = ColHeaderBorder
case WindowFooter:
color = ColFooterBorder
case WindowPreview:
color = ColPreviewBorder
}
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 { if hasRight {
w.Move(y, w.width-vw-1) rightCap = w.border.topRight
w.CPrint(color, " ") // Margin
w.CPrint(color, string(w.border.right))
} }
w.drawHLine(0, w.border.top, leftCap, rightCap, color)
} }
} if !onlyHorizontal && (hasLeft || hasRight) {
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 := 1; y < w.height-1; y++ { for y := 0; y < w.height; y++ {
w.Move(y, 0) // Corner rows are already painted by drawHLine above / below.
w.CPrint(color, string(w.border.left)) if (y == 0 && shape.HasTop()) || (y == w.height-1 && shape.HasBottom()) {
w.CPrint(color, " ") // Margin continue
}
w.Move(y, w.width-vw-1) if hasLeft {
w.CPrint(color, " ") // Margin w.Move(y, 0)
w.CPrint(color, string(w.border.right)) w.CPrint(color, string(w.border.left)+" ")
}
if hasRight {
w.Move(y, w.width-vw-1)
w.CPrint(color, " "+string(w.border.right))
}
} }
} }
w.Move(w.height-1, 0) if shape.HasBottom() {
rem = (w.width - bcw) % hw var leftCap, rightCap rune
w.CPrint(color, string(w.border.bottomLeft)+repeat(w.border.bottom, (w.width-bcw)/hw)+repeat(' ', rem)+string(w.border.bottomRight)) 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) csi(code string) string { func (w *LightWindow) csi(code string) string {
+135 -54
View File
@@ -1017,6 +1017,115 @@ 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
@@ -1031,72 +1140,44 @@ func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
top := w.top top := w.top
bot := top + w.height bot := top + w.height
var style tcell.Style style := w.borderStyleFor(w.windowType)
if w.color {
switch w.windowType {
case WindowBase:
style = ColBorder.style()
case WindowList:
style = ColListBorder.style()
case WindowHeader:
style = ColHeaderBorder.style()
case WindowFooter:
style = ColFooterBorder.style()
case WindowInput:
style = ColInputBorder.style()
case WindowPreview:
style = ColPreviewBorder.style()
}
} else {
style = w.normal.style()
}
hw := runeWidth(w.borderStyle.top) hasLeft := shape.HasLeft()
switch shape { hasRight := shape.HasRight()
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderHorizontal, BorderTop:
max := right - 2*hw if shape.HasTop() {
if shape == BorderHorizontal || shape == BorderTop { var leftCap, rightCap rune
max = right - hw if hasLeft {
leftCap = w.borderStyle.topLeft
} }
// tcell has an issue displaying two overlapping wide runes if hasRight {
// e.g. SetContent( HH ) rightCap = w.borderStyle.topRight
// SetContent( TR )
// ==================
// ( HH ) => TR is ignored
for x := left; x <= max; x += hw {
_screen.SetContent(x, top, w.borderStyle.top, nil, style)
} }
w.drawHLine(top, w.borderStyle.top, leftCap, rightCap, style)
} }
switch shape { if shape.HasBottom() {
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderHorizontal, BorderBottom: var leftCap, rightCap rune
max := right - 2*hw if hasLeft {
if shape == BorderHorizontal || shape == BorderBottom { leftCap = w.borderStyle.bottomLeft
max = right - hw
} }
for x := left; x <= max; x += hw { if hasRight {
_screen.SetContent(x, bot-1, w.borderStyle.bottom, nil, style) rightCap = w.borderStyle.bottomRight
} }
w.drawHLine(bot-1, w.borderStyle.bottom, leftCap, rightCap, style)
} }
if !onlyHorizontal { if !onlyHorizontal {
switch shape { vw := runeWidth(w.borderStyle.right)
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderVertical, BorderLeft: for y := top; y < bot; y++ {
for y := top; y < bot; y++ { // Corner rows are already painted by drawHLine above / below.
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)
}
} }
+120 -30
View File
@@ -506,7 +506,7 @@ type ColorTheme struct {
CurrentMatch ColorAttr CurrentMatch ColorAttr
Spinner ColorAttr Spinner ColorAttr
Info ColorAttr Info ColorAttr
Cursor ColorAttr Pointer ColorAttr
Marker ColorAttr Marker ColorAttr
Header ColorAttr Header ColorAttr
HeaderBg ColorAttr HeaderBg ColorAttr
@@ -595,11 +595,13 @@ const (
BorderBottom BorderBottom
BorderLeft BorderLeft
BorderRight BorderRight
BorderInline
BorderDashed
) )
func (s BorderShape) HasLeft() bool { func (s BorderShape) HasLeft() bool {
switch s { switch s {
case BorderNone, BorderPhantom, BorderLine, BorderRight, BorderTop, BorderBottom, BorderHorizontal: // No Left case BorderNone, BorderPhantom, BorderLine, BorderInline, BorderRight, BorderTop, BorderBottom, BorderHorizontal: // No Left
return false return false
} }
return true return true
@@ -607,7 +609,7 @@ func (s BorderShape) HasLeft() bool {
func (s BorderShape) HasRight() bool { func (s BorderShape) HasRight() bool {
switch s { switch s {
case BorderNone, BorderPhantom, BorderLine, BorderLeft, BorderTop, BorderBottom, BorderHorizontal: // No right case BorderNone, BorderPhantom, BorderLine, BorderInline, BorderLeft, BorderTop, BorderBottom, BorderHorizontal: // No right
return false return false
} }
return true return true
@@ -615,7 +617,7 @@ func (s BorderShape) HasRight() bool {
func (s BorderShape) HasTop() bool { func (s BorderShape) HasTop() bool {
switch s { switch s {
case BorderNone, BorderPhantom, BorderLine, BorderLeft, BorderRight, BorderBottom, BorderVertical: // No top case BorderNone, BorderPhantom, BorderLine, BorderInline, BorderLeft, BorderRight, BorderBottom, BorderVertical: // No top
return false return false
} }
return true return true
@@ -623,7 +625,7 @@ func (s BorderShape) HasTop() bool {
func (s BorderShape) HasBottom() bool { func (s BorderShape) HasBottom() bool {
switch s { switch s {
case BorderNone, BorderPhantom, BorderLine, BorderLeft, BorderRight, BorderTop, BorderVertical: // No bottom case BorderNone, BorderPhantom, BorderLine, BorderInline, BorderLeft, BorderRight, BorderTop, BorderVertical: // No bottom
return false return false
} }
return true return true
@@ -643,6 +645,8 @@ 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
@@ -658,7 +662,9 @@ 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{
@@ -671,6 +677,8 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
topRight: '+', topRight: '+',
bottomLeft: '+', bottomLeft: '+',
bottomRight: '+', bottomRight: '+',
leftMid: '+',
rightMid: '+',
} }
} }
switch shape { switch shape {
@@ -685,6 +693,8 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
topRight: '┐', topRight: '┐',
bottomLeft: '└', bottomLeft: '└',
bottomRight: '┘', bottomRight: '┘',
leftMid: '├',
rightMid: '┤',
} }
case BorderBold: case BorderBold:
return BorderStyle{ return BorderStyle{
@@ -697,6 +707,8 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
topRight: '┓', topRight: '┓',
bottomLeft: '┗', bottomLeft: '┗',
bottomRight: '┛', bottomRight: '┛',
leftMid: '┣',
rightMid: '┫',
} }
case BorderBlock: case BorderBlock:
// ▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜ // ▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜
@@ -712,6 +724,8 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
topRight: '▜', topRight: '▜',
bottomLeft: '▙', bottomLeft: '▙',
bottomRight: '▟', bottomRight: '▟',
leftMid: '▌',
rightMid: '▐',
} }
case BorderThinBlock: case BorderThinBlock:
@@ -728,6 +742,8 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
topRight: '🭾', topRight: '🭾',
bottomLeft: '🭼', bottomLeft: '🭼',
bottomRight: '🭿', bottomRight: '🭿',
leftMid: '▏',
rightMid: '▕',
} }
case BorderDouble: case BorderDouble:
@@ -741,6 +757,25 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
topRight: '╗', topRight: '╗',
bottomLeft: '╚', bottomLeft: '╚',
bottomRight: '╝', bottomRight: '╝',
leftMid: '╠',
rightMid: '╣',
}
case BorderDashed:
// Terminal cells are taller than wide (~2:1), so horizontals can use a
// sparse stub per cell while verticals need more dashes per cell to look
// evenly dashed. Rounded corners and sharp T-junction mids.
return BorderStyle{
shape: shape,
top: '╶',
bottom: '╶',
left: '┆',
right: '┆',
topLeft: '╭',
topRight: '╮',
bottomLeft: '╰',
bottomRight: '╯',
leftMid: '├',
rightMid: '┤',
} }
} }
return BorderStyle{ return BorderStyle{
@@ -753,6 +788,8 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
topRight: '╮', topRight: '╮',
bottomLeft: '╰', bottomLeft: '╰',
bottomRight: '╯', bottomRight: '╯',
leftMid: '├',
rightMid: '┤',
} }
} }
@@ -774,6 +811,35 @@ 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
@@ -811,6 +877,19 @@ 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()
@@ -869,18 +948,18 @@ var (
ColDisabled ColorPair ColDisabled ColorPair
ColGhost ColorPair ColGhost ColorPair
ColMatch ColorPair ColMatch ColorPair
ColCursor ColorPair ColPointer ColorPair
ColCursorEmpty ColorPair ColPointerEmpty ColorPair
ColCursorEmptyChar ColorPair ColPointerEmptyChar ColorPair
ColAltCursorEmpty ColorPair ColAltPointerEmpty ColorPair
ColAltCursorEmptyChar ColorPair ColAltPointerEmptyChar ColorPair
ColMarker ColorPair ColMarker ColorPair
ColSelected ColorPair ColSelected ColorPair
ColSelectedMatch ColorPair ColSelectedMatch ColorPair
ColCurrent ColorPair ColCurrent ColorPair
ColCurrentMatch ColorPair ColCurrentMatch ColorPair
ColCurrentCursor ColorPair ColCurrentPointer ColorPair
ColCurrentCursorEmpty ColorPair ColCurrentPointerEmpty ColorPair
ColCurrentMarker ColorPair ColCurrentMarker ColorPair
ColCurrentSelectedEmpty ColorPair ColCurrentSelectedEmpty ColorPair
ColSpinner ColorPair ColSpinner ColorPair
@@ -929,7 +1008,7 @@ func init() {
CurrentMatch: undefined, CurrentMatch: undefined,
Spinner: defaultColor, Spinner: defaultColor,
Info: defaultColor, Info: defaultColor,
Cursor: defaultColor, Pointer: defaultColor,
Marker: defaultColor, Marker: defaultColor,
Header: defaultColor, Header: defaultColor,
Border: undefined, Border: undefined,
@@ -979,7 +1058,7 @@ func init() {
CurrentMatch: undefined, CurrentMatch: undefined,
Spinner: undefined, Spinner: undefined,
Info: undefined, Info: undefined,
Cursor: undefined, Pointer: undefined,
Marker: undefined, Marker: undefined,
Header: undefined, Header: undefined,
Footer: undefined, Footer: undefined,
@@ -1030,7 +1109,7 @@ func init() {
CurrentMatch: ColorAttr{colBrightGreen, AttrUndefined}, CurrentMatch: ColorAttr{colBrightGreen, AttrUndefined},
Spinner: ColorAttr{colGreen, AttrUndefined}, Spinner: ColorAttr{colGreen, AttrUndefined},
Info: ColorAttr{colYellow, AttrUndefined}, Info: ColorAttr{colYellow, AttrUndefined},
Cursor: ColorAttr{colRed, AttrUndefined}, Pointer: ColorAttr{colRed, AttrUndefined},
Marker: ColorAttr{colMagenta, AttrUndefined}, Marker: ColorAttr{colMagenta, AttrUndefined},
Header: ColorAttr{colCyan, AttrUndefined}, Header: ColorAttr{colCyan, AttrUndefined},
Footer: ColorAttr{colCyan, AttrUndefined}, Footer: ColorAttr{colCyan, AttrUndefined},
@@ -1081,7 +1160,7 @@ func init() {
CurrentMatch: ColorAttr{151, AttrUndefined}, CurrentMatch: ColorAttr{151, AttrUndefined},
Spinner: ColorAttr{148, AttrUndefined}, Spinner: ColorAttr{148, AttrUndefined},
Info: ColorAttr{144, AttrUndefined}, Info: ColorAttr{144, AttrUndefined},
Cursor: ColorAttr{161, AttrUndefined}, Pointer: ColorAttr{161, AttrUndefined},
Marker: ColorAttr{168, AttrUndefined}, Marker: ColorAttr{168, AttrUndefined},
Header: ColorAttr{109, AttrUndefined}, Header: ColorAttr{109, AttrUndefined},
Footer: ColorAttr{109, AttrUndefined}, Footer: ColorAttr{109, AttrUndefined},
@@ -1132,7 +1211,7 @@ func init() {
CurrentMatch: ColorAttr{23, AttrUndefined}, CurrentMatch: ColorAttr{23, AttrUndefined},
Spinner: ColorAttr{65, AttrUndefined}, Spinner: ColorAttr{65, AttrUndefined},
Info: ColorAttr{101, AttrUndefined}, Info: ColorAttr{101, AttrUndefined},
Cursor: ColorAttr{161, AttrUndefined}, Pointer: ColorAttr{161, AttrUndefined},
Marker: ColorAttr{168, AttrUndefined}, Marker: ColorAttr{168, AttrUndefined},
Header: ColorAttr{31, AttrUndefined}, Header: ColorAttr{31, AttrUndefined},
Footer: ColorAttr{31, AttrUndefined}, Footer: ColorAttr{31, AttrUndefined},
@@ -1166,7 +1245,7 @@ func init() {
} }
} }
func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, boldify bool, forceBlack bool, hasInputWindow bool, hasHeaderWindow bool) { func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, boldify bool, forceBlack bool, hasInputWindow bool, hasHeaderWindow bool, headerInline bool, footerInline bool) {
if forceBlack { if forceBlack {
theme.Bg = ColorAttr{colBlack, AttrUndefined} theme.Bg = ColorAttr{colBlack, AttrUndefined}
} }
@@ -1183,7 +1262,7 @@ func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, boldify bool, forceBlac
theme.CurrentMatch = boldify(theme.CurrentMatch) theme.CurrentMatch = boldify(theme.CurrentMatch)
theme.Prompt = boldify(theme.Prompt) theme.Prompt = boldify(theme.Prompt)
theme.Input = boldify(theme.Input) theme.Input = boldify(theme.Input)
theme.Cursor = boldify(theme.Cursor) theme.Pointer = boldify(theme.Pointer)
theme.Spinner = boldify(theme.Spinner) theme.Spinner = boldify(theme.Spinner)
} }
@@ -1227,7 +1306,7 @@ func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, boldify bool, forceBlac
theme.CurrentMatch = o(baseTheme.CurrentMatch, currentMatch) theme.CurrentMatch = o(baseTheme.CurrentMatch, currentMatch)
theme.Spinner = o(baseTheme.Spinner, theme.Spinner) theme.Spinner = o(baseTheme.Spinner, theme.Spinner)
theme.Info = o(baseTheme.Info, theme.Info) theme.Info = o(baseTheme.Info, theme.Info)
theme.Cursor = o(baseTheme.Cursor, theme.Cursor) theme.Pointer = o(baseTheme.Pointer, theme.Pointer)
theme.Marker = o(baseTheme.Marker, theme.Marker) theme.Marker = o(baseTheme.Marker, theme.Marker)
theme.Header = o(baseTheme.Header, theme.Header) theme.Header = o(baseTheme.Header, theme.Header)
theme.Footer = o(baseTheme.Footer, theme.Footer) theme.Footer = o(baseTheme.Footer, theme.Footer)
@@ -1300,11 +1379,22 @@ 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)
} }
theme.HeaderBorder = o(theme.Border, theme.HeaderBorder) // Inline header/footer borders sit inside the list frame, so default their color
// 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)
theme.FooterBorder = o(theme.Border, theme.FooterBorder) footerBorderFallback := theme.Border
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() {
@@ -1332,11 +1422,11 @@ func initPalette(theme *ColorTheme) {
ColDisabled = pair(theme.Disabled, theme.InputBg) ColDisabled = pair(theme.Disabled, theme.InputBg)
ColMatch = pair(theme.Match, theme.ListBg) ColMatch = pair(theme.Match, theme.ListBg)
ColSelectedMatch = pair(theme.SelectedMatch, theme.SelectedBg) ColSelectedMatch = pair(theme.SelectedMatch, theme.SelectedBg)
ColCursor = pair(theme.Cursor, theme.Gutter) ColPointer = pair(theme.Pointer, theme.Gutter)
ColCursorEmpty = pair(blank, theme.Gutter) ColPointerEmpty = pair(blank, theme.Gutter)
ColCursorEmptyChar = pair(theme.Gutter, theme.ListBg) ColPointerEmptyChar = pair(theme.Gutter, theme.ListBg)
ColAltCursorEmpty = pair(blank, theme.AltGutter) ColAltPointerEmpty = pair(blank, theme.AltGutter)
ColAltCursorEmptyChar = pair(theme.AltGutter, theme.ListBg) ColAltPointerEmptyChar = pair(theme.AltGutter, theme.ListBg)
if theme.SelectedBg.Color != theme.ListBg.Color { if theme.SelectedBg.Color != theme.ListBg.Color {
ColMarker = pair(theme.Marker, theme.SelectedBg) ColMarker = pair(theme.Marker, theme.SelectedBg)
} else { } else {
@@ -1344,8 +1434,8 @@ func initPalette(theme *ColorTheme) {
} }
ColCurrent = pair(theme.Current, theme.DarkBg) ColCurrent = pair(theme.Current, theme.DarkBg)
ColCurrentMatch = pair(theme.CurrentMatch, theme.DarkBg) ColCurrentMatch = pair(theme.CurrentMatch, theme.DarkBg)
ColCurrentCursor = pair(theme.Cursor, theme.DarkBg) ColCurrentPointer = pair(theme.Pointer, theme.DarkBg)
ColCurrentCursorEmpty = pair(blank, theme.DarkBg) ColCurrentPointerEmpty = pair(blank, theme.DarkBg)
ColCurrentMarker = pair(theme.Marker, theme.DarkBg) ColCurrentMarker = pair(theme.Marker, theme.DarkBg)
ColCurrentSelectedEmpty = pair(blank, theme.DarkBg) ColCurrentSelectedEmpty = pair(blank, theme.DarkBg)
ColSpinner = pair(theme.Spinner, theme.InputBg) ColSpinner = pair(theme.Spinner, theme.InputBg)
+1 -1
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)
+44 -7
View File
@@ -11,6 +11,8 @@ import (
"strings" "strings"
"sync/atomic" "sync/atomic"
"syscall" "syscall"
"golang.org/x/sys/windows"
) )
type shellType int type shellType int
@@ -19,6 +21,7 @@ const (
shellTypeUnknown shellType = iota shellTypeUnknown shellType = iota
shellTypeCmd shellTypeCmd
shellTypePowerShell shellTypePowerShell
shellTypePwsh
) )
var escapeRegex = regexp.MustCompile(`[&|<>()^%!"]`) var escapeRegex = regexp.MustCompile(`[&|<>()^%!"]`)
@@ -46,7 +49,10 @@ 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") || strings.HasPrefix(basename, "powershell") { } else if strings.HasPrefix(basename, "pwsh") {
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 {
@@ -56,8 +62,12 @@ 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 //
// can kill preview process with its child processes at once. // On Windows, setpgid controls whether the spawned process is placed in a new
// 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 {
@@ -73,19 +83,31 @@ 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: 0, CreationFlags: creationFlags,
} }
} else { } else {
cmd = exec.Command(shell, append(x.args, command)...) args := x.args
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: 0, CreationFlags: creationFlags,
} }
} }
return cmd return cmd
@@ -156,7 +178,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: case shellTypePowerShell, shellTypePwsh:
escaped := strings.ReplaceAll(entry, `"`, `\"`) escaped := strings.ReplaceAll(entry, `"`, `\"`)
return "'" + strings.ReplaceAll(escaped, "'", "''") + "'" return "'" + strings.ReplaceAll(escaped, "'", "''") + "'"
default: default:
@@ -166,6 +188,21 @@ 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()
} }
+41
View File
@@ -0,0 +1,41 @@
package fzf
import (
"os/exec"
)
func runZellij(args []string, opts *Options) (int, error) {
argStr, dir := popupArgStr(args, opts)
zellijArgs := []string{
"run", "--floating", "--close-on-exit", "--block-until-exit",
"--cwd", dir,
}
if !opts.Tmux.border {
zellijArgs = append(zellijArgs, "--borderless", "true")
}
switch opts.Tmux.position {
case posUp:
zellijArgs = append(zellijArgs, "-y", "0")
case posDown:
zellijArgs = append(zellijArgs, "-y", "9999")
case posLeft:
zellijArgs = append(zellijArgs, "-x", "0")
case posRight:
zellijArgs = append(zellijArgs, "-x", "9999")
case posCenter:
// Zellij centers floating panes by default
}
zellijArgs = append(zellijArgs, "--width", opts.Tmux.width.String())
zellijArgs = append(zellijArgs, "--height", opts.Tmux.height.String())
zellijArgs = append(zellijArgs, "--")
return runProxy(argStr, func(temp string, needBash bool) (*exec.Cmd, error) {
sh, err := sh(needBash)
if err != nil {
return nil, err
}
zellijArgs = append(zellijArgs, sh, temp)
return exec.Command("zellij", zellijArgs...), nil
}, opts, true)
}
+1 -2
View File
@@ -3,10 +3,9 @@ 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_TRIGGER FZF_COMPLETION_OPTS set -e FZF_COMPLETION_OPTS FZF_EXPANSION_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
+82
View File
@@ -105,6 +105,23 @@ 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
@@ -113,6 +130,71 @@ 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
+9 -9
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
+333
View File
@@ -1298,4 +1298,337 @@ 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?(%r{\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?(%r{\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?(%r{\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 :Space
tmux.until do |lines|
lines.any_include?(/\A│\s+1\s+│\z/) && lines.any_include?(/\A│\s+tada\s+│\z/)
end
end
# Regression: with --footer-border=inline, change-footer that grows the
# footer line count left the inline slot sized for the old length, so
# extra lines were clipped.
def test_inline_change_footer_grows_slot
tmux.send_keys %(seq 5 | #{FZF} --style full --footer-border inline --footer one --bind $'space:change-footer:one\\ntwo'), :Enter
tmux.until { |lines| lines.any_include?(/\A│\s+one\s+│\z/) }
tmux.send_keys :Space
tmux.until do |lines|
lines.any_include?(/\A│\s+one\s+│\z/) && lines.any_include?(/\A│\s+two\s+│\z/)
end
end
# Invalid inline combinations must be rejected at startup.
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
+1 -1
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] }