Compare commits

...

48 Commits

Author SHA1 Message Date
dependabot[bot] 630571ec12 Bump github.com/mattn/go-isatty from 0.0.20 to 0.0.22
Bumps [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) from 0.0.20 to 0.0.22.
- [Commits](https://github.com/mattn/go-isatty/compare/v0.0.20...v0.0.22)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-isatty
  dependency-version: 0.0.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-27 17:01:54 +00:00
Junegunn Choi 6fefe02546 0.72.0
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
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
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
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)
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
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
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Close #4776
2026-04-19 14:38:36 +09:00
Daniel Levin 06e70570e2 Update Makefile to support builds on Illumos
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Close #4772
2026-04-13 20:07:55 +09:00
Junegunn Choi bd4a18d0a9 Replace sort.Sort(ByOrder(...)) with slices.SortFunc
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
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
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Fix #4764

Patch suggested by @bitraid.
2026-04-11 17:03:25 +09:00
Junegunn Choi 0eb2ae9f8b Fix gutter display in --style=minimal
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2026-04-08 23:28:10 +09:00
Junegunn Choi 1831500018 Update README
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
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
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
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
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2026-04-03 18:58:57 +09:00
Junegunn Choi 0f0751b3b6 Add CODEOWNERS
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2026-04-03 00:01:37 +09:00
dependabot[bot] cc16a97a40 Bump ruby/setup-ruby from 1.296.0 to 1.299.0 (#4753)
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
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)
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2026-03-30 13:20:11 +09:00
PJ Eby fff7eda9d7 Terminate child processes on Windows (#4723)
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
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 🚀
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2026-03-28 15:05:52 +00:00
harrywzl 55d5b153e6 docs: fix duplicate table rows and minor grammar/formatting in fzf.txt (#4743)
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
2026-03-27 15:01:12 +09:00
Junegunn Choi 57695b0309 Remove deprecated FZF_TMUX vars from shell script headers
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
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)
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Fix #4735
Close #4736
2026-03-25 15:22:49 +09:00
cui 14e3995a06 Fix nthTransformer parts slice preallocation (#4734)
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
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
51 changed files with 1855 additions and 1045 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
root = true
[*.{sh,bash}]
[*.{sh,bash,fish}]
indent_style = space
indent_size = 2
simplify = true
+1
View File
@@ -0,0 +1 @@
* @junegunn
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
go-version: "1.23"
- name: Setup Ruby
uses: ruby/setup-ruby@v1
uses: ruby/setup-ruby@3ff19f5e2baf30647122352b96108b1fbe250c64 # v1
with:
ruby-version: 3.4.6
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
go-version: "1.23"
- name: Setup Ruby
uses: ruby/setup-ruby@v1
uses: ruby/setup-ruby@3ff19f5e2baf30647122352b96108b1fbe250c64 # v1
with:
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
steps:
- 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:
runs-on: ubuntu-latest
steps:
- uses: vedantmgoyal2009/winget-releaser@v2
- uses: vedantmgoyal2009/winget-releaser@4ffc7888bffd451b357355dc214d43bb9f23917e # v2
with:
identifier: junegunn.fzf
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.
- The value of `--preview-window` option consists of 5 components delimited
by `,`
1. `up` Position of the preview window
1. `60%` Size of the preview window
1. `border-bottom` Preview window border only on the bottom side
1. `+{2}+3/3` Scroll offset of the preview contents
1. `~3` Fixed header
1. `up` -- Position of the preview window
1. `60%` -- Size of the preview window
1. `border-bottom` -- Preview window border only on the bottom side
1. `+{2}+3/3` -- Scroll offset of the preview contents
1. `~3` -- Fixed header
- 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
positioned near the center of the preview window.
- `+{2}` The base offset is extracted from the second token
- `+3` We add 3 lines to the base offset to compensate for the header
- `+{2}` -- The base offset is extracted from the second token
- `+3` -- We add 3 lines to the base offset to compensate for the header
part of `bat` output
- ```
───────┬──────────────────────────────────────────────────────────
+48 -6
View File
@@ -1,8 +1,40 @@
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
------
_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`
- 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.
@@ -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.
```
=== query: 'linux' ===
[all] baseline: 17.12ms current: 14.28ms (1.20x) matches: 179966 (12.79%)
[1T] baseline: 136.49ms current: 137.25ms (0.99x) matches: 179966 (12.79%)
[2T] baseline: 75.74ms current: 68.75ms (1.10x) matches: 179966 (12.79%)
[4T] baseline: 41.16ms current: 34.97ms (1.18x) matches: 179966 (12.79%)
[8T] baseline: 32.82ms current: 17.79ms (1.84x) matches: 179966 (12.79%)
[all] baseline: 21.95ms current: 17.47ms (1.26x) matches: 179966 (12.79%)
[1T] baseline: 179.63ms current: 180.53ms (1.00x) matches: 179966 (12.79%)
[2T] baseline: 97.38ms current: 90.05ms (1.08x) matches: 179966 (12.79%)
[4T] baseline: 53.83ms current: 44.77ms (1.20x) 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.
- With the reduced per-entry cost, the cache now has broader coverage.
- Shell integration improvements
- 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)
- Allow adaptive height with negative value (`--height=~-HEIGHT`) (#4682)
- 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)
- Fixed AWK tokenizer not treating a new line character as whitespace
- 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 `--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
------
+2
View File
@@ -53,6 +53,8 @@ ifeq ($(UNAME_M),x86_64)
BINARY := $(BINARY64)
else ifeq ($(UNAME_M),amd64)
BINARY := $(BINARY64)
else ifeq ($(UNAME_M),i86pc)
BINARY := $(BINARY64)
else ifeq ($(UNAME_M),s390x)
BINARY := $(BINARYS390)
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
: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
vertical splits respectively.
@@ -218,7 +218,6 @@ list:
`fg` / `bg` / `hl` | Item (foreground / background / highlight)
`fg+` / `bg+` / `hl+` | Current item (foreground / background / highlight)
`preview-fg` / `preview-bg` | Preview window text and background
`hl` / `hl+` | Highlighted substrings (normal / current)
`gutter` | Background of the gutter on the left
`pointer` | Pointer to the current line ( `>` )
`marker` | Multi-select marker ( `>` )
@@ -229,7 +228,6 @@ list:
`query` | Query string
`disabled` | Query string when search is disabled
`prompt` | Prompt before query ( `> ` )
`pointer` | Pointer to the current line ( `>` )
----------------------------+------------------------------------------------------
- `component` specifies the component (`fg` / `bg`) from which to extract the
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
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:
>
:echo fzf#wrap()
+1 -1
View File
@@ -4,7 +4,7 @@ require (
github.com/charlievieth/fastwalk v1.0.14
github.com/gdamore/tcell/v2 v2.9.0
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741
github.com/mattn/go-isatty v0.0.20
github.com/mattn/go-isatty v0.0.22
github.com/rivo/uniseg v0.4.7
golang.org/x/sys v0.35.0
golang.org/x/term v0.34.0
+2 -3
View File
@@ -8,8 +8,8 @@ github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741 h1:7dYDtfMD
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741/go.mod h1:6EILKtGpo5t+KLb85LNZLAF6P9LKp78hJI80PXMcn3c=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -33,7 +33,6 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+9 -4
View File
@@ -2,7 +2,7 @@
set -u
version=0.70.0
version=0.72.0
auto_completion=
key_bindings=
update_config=2
@@ -112,10 +112,15 @@ link_fzf_in_path() {
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() {
command -v curl > /dev/null &&
if [[ $1 =~ tar.gz$ ]]; then
curl -fL $1 | tar --no-same-owner -xzf -
curl -fL $1 | tar $tar_opts
else
local temp=${TMPDIR:-/tmp}/fzf.zip
curl -fLo "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
@@ -125,7 +130,7 @@ try_curl() {
try_wget() {
command -v wget > /dev/null &&
if [[ $1 =~ tar.gz$ ]]; then
wget -O - $1 | tar --no-same-owner -xzf -
wget -O - $1 | tar $tar_opts
else
local temp=${TMPDIR:-/tmp}/fzf.zip
wget -O "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
@@ -439,7 +444,7 @@ if [ $update_config -eq 1 ]; then
echo
fi
[[ $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 'Use uninstall script to remove fzf.'
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
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"github.com/junegunn/fzf/src/protector"
)
var version = "0.70"
var version = "0.72"
var revision = "devel"
//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
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
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
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
fzf - a command-line fuzzy finder
@@ -384,7 +384,7 @@ Use black background
.SS DISPLAY MODE
.TP
.BI "\-\-height=" "[~]HEIGHT[%]"
.BI "\-\-height=" "[~][\-]HEIGHT[%]"
Display fzf window below the cursor with the given height instead of using
the full screen.
@@ -394,17 +394,19 @@ height minus the given value.
fzf \-\-height=\-1
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
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:
.br
* Cannot be used with top/bottom margin and padding given in percent size
.br
* Negative value is not allowed
.br
* It will not find the right size when there are multi-line items
.TP
@@ -415,25 +417,26 @@ layout options so that the specified number of items are visible in the list
section (default: \fB10+\fR).
Ignored when \fB\-\-height\fR is not specified or set as an absolute value.
.TP
.BI "\-\-tmux" "[=[center|top|bottom|left|right][,SIZE[%]][,SIZE[%]][,border-native]]"
Start fzf in a tmux popup (default \fBcenter,50%\fR). Requires tmux 3.3 or
later. This option is ignored if you are not running fzf inside tmux.
.BI "\-\-popup" "[=[center|top|bottom|left|right][,SIZE[%]][,SIZE[%]][,border-native]]"
Start fzf in a tmux popup or in a Zellij floating pane (default
\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.
\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
fzf \-\-tmux right,40%
fzf \-\-popup right,40%
# 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
fzf \-\-tmux top,80%,40%
fzf \-\-popup top,80%,40%
# Popup with a native tmux border in the center with 80% width and height
fzf \-\-tmux center,80%,border\-native\fR
# Popup with a native tmux or Zellij border in the center with 80% width and height
fzf \-\-popup center,80%,border\-native\fR
.SS LAYOUT
.TP
@@ -514,6 +517,8 @@ Draw border around the finder
.br
.BR double " Border with double lines"
.br
.BR dashed " Border with dashed lines and rounded corners"
.br
.BR block " Border using block elements; suitable when using different background colors"
.br
.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
.B * border\-double
.br
.B * border\-dashed
.br
.B * border\-block
.br
.B * border\-thinblock
@@ -1097,7 +1104,17 @@ Print header before the prompt line. When both normal header and header lines
.TP
.BI "\-\-header\-border" [=STYLE]
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
.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
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.
\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
@@ -1126,7 +1147,10 @@ are not affected by \fB\-\-with\-nth\fR. ANSI color codes are processed even whe
.TP
.BI "\-\-footer\-border" [=STYLE]
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
.BI "\-\-footer\-label" [=LABEL]
+57 -25
View File
@@ -896,6 +896,7 @@ function! s:execute_term(dict, command, temps) abort
endif
endfunction
function! fzf.on_exit(id, code, ...)
silent! autocmd! fzf_popup_resize
if s:getpos() == self.ppos " {'window': 'enew'}
for [opt, val] in items(self.winopts)
execute 'let' opt '=' val
@@ -1023,15 +1024,17 @@ function! s:callback(dict, lines) abort
endfunction
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 opts = extend({'relative': 'editor', 'style': 'minimal'}, a:opts)
let win = nvim_open_win(buf, v:true, opts)
call setwinvar(win, '&colorcolumn', '')
let s:popup_id = nvim_open_win(buf, v:true, opts)
call setwinvar(s:popup_id, '&colorcolumn', '')
" Colors
try
call setwinvar(win, '&winhighlight', 'Pmenu:,Normal:Normal')
call setwinvar(s:popup_id, '&winhighlight', 'Pmenu:,Normal:Normal')
let rules = get(g:, 'fzf_colors', {})
if has_key(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 hl = nvim_set_hl(ns, 'Normal',
\ &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
catch
endtry
return buf
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
function! s:create_popup(opts) abort
let s:popup_create = {buf -> popup_create(buf, #{
\ line: a:opts.row,
\ col: a:opts.col,
\ minwidth: a:opts.width,
\ maxwidth: a:opts.width,
\ minheight: a:opts.height,
\ maxheight: a:opts.height,
\ zindex: 1000,
\ })}
function! s:create_popup() abort
function! s:popup_create(buf)
let s:popup_id = popup_create(a:buf, #{zindex: 1000})
call s:resize_popup()
endfunction
autocmd TerminalOpen * ++once call s:popup_create(str2nr(expand('<abuf>')))
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
function! s:popup(opts) abort
let xoffset = get(a:opts, 'xoffset', 0.5)
let yoffset = get(a:opts, 'yoffset', 0.5)
let relative = get(a:opts, 'relative', 0)
function! s:popup_bounds() abort
let opts = s:popup_opts
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
let columns = relative ? winwidth(0) : &columns
let lines = relative ? winheight(0) : (&lines - has('nvim'))
" Size and position
let width = min([max([8, a:opts.width > 1 ? a:opts.width : float2nr(columns * a:opts.width)]), columns])
let height = min([max([4, a:opts.height > 1 ? a:opts.height : float2nr(lines * a:opts.height)]), lines])
let width = min([max([8, opts.width > 1 ? opts.width : float2nr(columns * opts.width)]), columns])
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 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 col += !has('nvim')
call s:create_popup({
\ 'row': row, 'col': col, 'width': width, 'height': height
\ })
return { '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
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
#
# - $FZF_TMUX (default: 0)
# - $FZF_TMUX_OPTS (default: empty)
# - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty)
# - $FZF_COMPLETION_PATH_OPTS (default: empty)
+160 -232
View File
@@ -4,238 +4,166 @@
# / __/ / /_/ __/
# /_/ /___/_/ completion.fish
#
# - $FZF_COMPLETION_OPTS (default: empty)
# - $FZF_COMPLETION_OPTS
# - $FZF_EXPANSION_OPTS
function fzf_completion_setup
#----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
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'
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
# 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
# be used anywhere in the script, otherwise the source command will fail.
if string match -qr -- '^[12]\\.|^3\\.[0-3]' $version
echo "fzf completion script requires fish version 3.4.0 or newer." >&2
return 1
else if not command -q fzf
echo "fzf was not found in path." >&2
return 1
end
# Run setup
fzf_completion_setup
function fzf_complete -w fzf -d 'fzf command completion and wildcard expansion search'
# 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
#
# - $FZF_TMUX (default: 0)
# - $FZF_TMUX_OPTS (default: empty)
# - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty)
# - $FZF_COMPLETION_PATH_OPTS (default: empty)
+4 -1
View File
@@ -4,7 +4,6 @@
# / __/ / /_/ __/
# /_/ /___/_/ key-bindings.bash
#
# - $FZF_TMUX_OPTS
# - $FZF_CTRL_T_COMMAND
# - $FZF_CTRL_T_OPTS
# - $FZF_CTRL_R_COMMAND
@@ -85,6 +84,10 @@ __fzf_history_delete() {
for offset in "${offsets[@]}"; do
builtin history -d "$offset"
done
if [[ ${#offsets[@]} -gt 0 ]] && shopt -q histappend; then
builtin history -w
fi
}
if command -v perl > /dev/null; then
+46 -113
View File
@@ -4,7 +4,6 @@
# / __/ / /_/ __/
# /_/ /___/_/ key-bindings.fish
#
# - $FZF_TMUX_OPTS
# - $FZF_CTRL_T_COMMAND
# - $FZF_CTRL_T_OPTS
# - $FZF_CTRL_R_COMMAND
@@ -12,35 +11,29 @@
# - $FZF_ALT_C_COMMAND
# - $FZF_ALT_C_OPTS
# 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
# Check fish version
if set -l -- fish_ver (string match -r '^(\d+)\.(\d+)' $version 2>/dev/null)
and test "$fish_ver[2]" -lt 3 -o "$fish_ver[2]" -eq 3 -a "$fish_ver[3]" -lt 1
echo "This script requires fish version 3.1b1 or newer." >&2
# 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
# be used anywhere in the script, otherwise the source command will fail.
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
else if not type -q fzf
else if not command -q fzf
echo "fzf was not found in path." >&2
return 1
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
# $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]
$FZF_DEFAULT_OPTS $argv[2..]
end
function __fzfcmd
@@ -54,107 +47,59 @@ function fzf_key_bindings
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'
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
# Don't use option prefix if " -- " is preceded.
string match -qv -- '* -- *' (string sub -l (commandline -Cp) -- (commandline -p))
and set -- match_regex "(?<prefix>$prefix_regex)?$match_regex"
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)
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$' '')
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)' '')
end
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 -- 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
string match -q -r -- '(?<fzf_query>^[\\s\\S]*?(?=\\n?$)$)' \
(string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\\n)$' '' $fzf_query | string collect -N)
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)
if not string match -q -- '.' $dir; or string match -qr -- '^\\.(/|$)' $fzf_query
# Strip $dir from $fzf_query - preserve trailing newlines.
if string match -qr -- '^\\d\\d+|^[4-9]' $version
# fish v4.0.0 and newer
string match -q -r -- '^'(string escape --style=regex -- $dir)'/?(?<fzf_query>[\\s\\S]*)' $fzf_query
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$' '')
string match -q -r -- '^/?(?<fzf_query>[\\s\\S]*?(?=\\n?$)$)' \
(string replace -- "$dir" '' $fzf_query | string collect -N)
end
end
end
string escape -n -- "$dir" "$fzf_query" "$prefix"
end
#----END INCLUDE
# Store current token in $dir as root for the 'find' command
function fzf-file-widget -d "List files and folders"
@@ -171,7 +116,7 @@ function fzf_key_bindings
set -lx FZF_DEFAULT_OPTS_FILE
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
end
@@ -183,45 +128,33 @@ function fzf_key_bindings
set -l -- fzf_query (string escape -- $command_line[$current_line])
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="↳ "' \
'--bind=\'shift-delete:execute-silent(for i in (string split0 -- <{+f}); eval builtin history delete --exact --case-sensitive -- (string escape -n -- $i | string replace -r "^\d*\\\\\\t" ""); end)+reload(eval $FZF_DEFAULT_COMMAND)\'' \
'--bind="alt-enter:become(string join0 -- (string collect -- {+2..} | fish_indent -i))"' \
'--with-nth=2.. --nth=2..,.. --scheme=history --multi --no-multi-line' \
'--no-wrap --wrap-sign="\t\t\t↳ " --preview-wrap-sign="↳ " --freeze-left=1' \
'--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" \
'--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
if string match -qvr -- '--preview[= ]' "$FZF_DEFAULT_OPTS"
# Convert the highlighted timestamp using the date command if available
set -l -- date_cmd '{1}'
if type -q date
if date -d @0 '+%s' 2>/dev/null | string match -q 0
# GNU date
set -- date_cmd '(date -d @{1} \\"+%F %a %T\\")'
else if date -r 0 '+%s' 2>/dev/null | string match -q 0
# BSD date
set -- date_cmd '(date -r {1} \\"+%F %a %T\\")'
end
end
# Prepend the options to allow user customizations
# Prepend the options to allow user overrides
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"' \
'--preview="string collect -- (test \\"$FZF_SELECT_COUNT\\" -gt 0; and string collect -- {+2..}) \\"\\n# \\"'$date_cmd' {2..} | fish_indent --ansi"' \
'--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="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"'
end
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
if set -l -- v (string match -r -- '^(\d+)\.(\d+)(?:\.(\d+))?' $version)
and test "$v[2]" -gt 4 -o "$v[2]" -eq 4 -a \
\( "$v[3]" -gt 3 -o "$v[3]" -eq 3 -a \
\( -n "$v[4]" -a "$v[4]" -ge 3 \) \)
if string match -qr -- '^\\d\\d+|^4\\.[4-9]|^4\\.3\\.[3-9]' $version
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
# Merge history from other sessions before searching
-1
View File
@@ -4,7 +4,6 @@
# / __/ / /_/ __/
# /_/ /___/_/ key-bindings.zsh
#
# - $FZF_TMUX_OPTS
# - $FZF_CTRL_T_COMMAND
# - $FZF_CTRL_T_OPTS
# - $FZF_CTRL_R_COMMAND
+12 -14
View File
@@ -8,26 +8,24 @@ dir=${0%"${0##*/}"}
update() {
{
sed -n "1,/^#----BEGIN INCLUDE $1/p" "$2"
sed -n '1,/^#----BEGIN INCLUDE common\.sh/p' "$1"
cat << EOF
# NOTE: Do not directly edit this section, which is copied from "$1".
# To modify it, one can edit "$1" and run "./update.sh" to apply
# the changes. See code comments in "$1" for the implementation details.
# NOTE: Do not directly edit this section, which is copied from "common.sh".
# To modify it, one can edit "common.sh" and run "./update.sh" to apply
# the changes. See code comments in "common.sh" for the implementation details.
EOF
echo
grep -v '^[[:blank:]]*#' "$dir/$1" # remove code comments from the common file
sed -n '/^#----END INCLUDE/,$p' "$2"
} > "$2.part"
grep -v '^[[:blank:]]*#' "$dir/common.sh" # remove code comments in common.sh
sed -n '/^#----END INCLUDE/,$p' "$1"
} > "$1.part"
mv -f "$2.part" "$2"
mv -f "$1.part" "$1"
}
update "common.sh" "$dir/completion.bash"
update "common.sh" "$dir/completion.zsh"
update "common.sh" "$dir/key-bindings.bash"
update "common.sh" "$dir/key-bindings.zsh"
update "common.fish" "$dir/completion.fish"
update "common.fish" "$dir/key-bindings.fish"
update "$dir/completion.bash"
update "$dir/completion.zsh"
update "$dir/key-bindings.bash"
update "$dir/key-bindings.zsh"
# Check if --check is in ARGV
check=0
+5 -5
View File
@@ -2,10 +2,10 @@
## 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`.
`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`.
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:
1. **Table-driven tests** known inputs with expected outputs.
2. **Exhaustive tests** all lengths 0256, every match position, no-match
1. **Table-driven tests** -- known inputs with expected outputs.
2. **Exhaustive tests** -- all lengths 0256, every match position, no-match
cases, and both-bytes-present cases, compared against a simple loop
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.
+1 -1
View File
@@ -78,7 +78,7 @@ loop:
CBZ R6, loop
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, V4.B16, V4.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
case 4:
if sep == ':' {
// SGR 4:N underline style sub-parameter
// SGR 4:N - underline style sub-parameter
var subNum int
subNum, _, ansiCode = parseAnsiCode(ansiCode)
state.attr = state.attr &^ tui.UnderlineStyleMask
+3
View File
@@ -56,6 +56,9 @@ func Run(opts *Options) (int, error) {
if opts.useTmux() {
return runTmux(os.Args, opts)
}
if opts.useZellij() {
return runZellij(os.Args, opts)
}
if needWinpty(opts) {
return runWinpty(os.Args, opts)
+46 -22
View File
@@ -66,7 +66,7 @@ Usage: fzf [options]
--no-bold Do not use bold text
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.
A negative value is calculated as the terminal height
minus the given value.
@@ -75,16 +75,17 @@ Usage: fzf [options]
--min-height=HEIGHT[+] Minimum height when --height is given as a percentage.
Add '+' to automatically increase the value
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[%]]
[,border-native] (default: center,50%)
--tmux[=OPTS] Alias for --popup
LAYOUT
--layout=LAYOUT Choose layout: [default|reverse|reverse-list]
--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)
--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)
--border-label=LABEL Label to print on the border
--border-label-pos=COL Position of the border label
@@ -127,7 +128,7 @@ Usage: fzf [options]
(each for list section and preview window)
--no-scrollbar Hide scrollbar
--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)
--list-label=LABEL Label to print on the list border
--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
--filepath-word Make word-wise movements respect path separators
--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)
--input-label=LABEL Label to print on the input border
--input-label-pos=COL Position of the input label
@@ -164,7 +165,7 @@ Usage: fzf [options]
[,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES]
[,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]
--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)
--preview-label=LABEL
--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-first Print header before the prompt line
--header-border[=STYLE] Draw border around the header section
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
top|bottom|left|right|line|none] (default: rounded)
[rounded|sharp|bold|block|thinblock|double|dashed|horizontal|vertical|
top|bottom|left|right|line|inline|none] (default: rounded)
--header-lines-border[=STYLE]
Display header from --header-lines with a separate 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-pos=COL Position of the header label
[POSITIVE_INTEGER: columns from left|
@@ -190,8 +192,8 @@ Usage: fzf [options]
FOOTER
--footer=STR String to print as footer
--footer-border[=STYLE] Draw border around the footer section
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
top|bottom|left|right|line|none] (default: line)
[rounded|sharp|bold|block|thinblock|double|dashed|horizontal|vertical|
top|bottom|left|right|line|inline|none] (default: line)
--footer-label=LABEL Label to print on the footer border
--footer-label-pos=COL Position of the footer label
[POSITIVE_INTEGER: columns from left|
@@ -417,7 +419,7 @@ func parseTmuxOptions(arg string, index int) (*tmuxOptions, error) {
var err error
opts := defaultTmuxOptions(index)
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 {
return nil, errorToReturn
}
@@ -869,7 +871,7 @@ func nthTransformer(str string) (func(Delimiter) func([]Token, int32) string, er
nth []Range
}
parts := make([]NthParts, len(indexes))
parts := make([]NthParts, 0, len(indexes))
idx := 0
for _, index := range indexes {
if idx < index[0] {
@@ -952,6 +954,8 @@ func parseBorder(str string, optional bool) (tui.BorderShape, error) {
switch str {
case "line":
return tui.BorderLine, nil
case "inline":
return tui.BorderInline, nil
case "rounded":
return tui.BorderRounded, nil
case "sharp":
@@ -964,6 +968,8 @@ func parseBorder(str string, optional bool) (tui.BorderShape, error) {
return tui.BorderThinBlock, nil
case "double":
return tui.BorderDouble, nil
case "dashed":
return tui.BorderDashed, nil
case "horizontal":
return tui.BorderHorizontal, nil
case "vertical":
@@ -982,7 +988,7 @@ func parseBorder(str string, optional bool) (tui.BorderShape, error) {
if optional && str == "" {
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) {
@@ -1566,7 +1572,7 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, *tui
case "info":
mergeAttr(&theme.Info)
case "pointer":
mergeAttr(&theme.Cursor)
mergeAttr(&theme.Pointer)
case "marker":
mergeAttr(&theme.Marker)
case "header", "header-fg":
@@ -2221,9 +2227,6 @@ func parseHeight(str string, index int) (heightSpec, error) {
str = str[1:]
}
if strings.HasPrefix(str, "-") {
if heightSpec.auto {
return heightSpec, errors.New("negative(-) height is not compatible with adaptive(~) height")
}
heightSpec.inverse = true
str = str[1:]
}
@@ -2340,6 +2343,8 @@ func parsePreviewWindowImpl(opts *previewOpts, input string) error {
opts.border = tui.BorderThinBlock
case "border-double":
opts.border = tui.BorderDouble
case "border-dashed":
opts.border = tui.BorderDashed
case "noborder", "border-none":
opts.border = tui.BorderNone
case "border-horizontal":
@@ -2636,7 +2641,7 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
opts.Version = true
case "--no-winpty":
opts.NoWinpty = true
case "--tmux":
case "--tmux", "--popup":
given, str := optionalNextString()
if given {
if opts.Tmux, err = parseTmuxOptions(str, index); err != nil {
@@ -2645,7 +2650,7 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
} else {
opts.Tmux = defaultTmuxOptions(index)
}
case "--no-tmux":
case "--no-tmux", "--no-popup":
opts.Tmux = nil
case "--tty-default":
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
case "--height":
str, err := nextString("height required: [~]HEIGHT[%]")
str, err := nextString("height required: [~][-]HEIGHT[%]")
if err != nil {
return err
}
@@ -3518,7 +3523,9 @@ func applyPreset(opts *Options, preset string) error {
opts.Preview.border = tui.BorderLine
opts.Preview.info = false
opts.InfoStyle = infoDefault
opts.Theme.Gutter = tui.ColorAttr{Color: -1, Attr: 0}
opts.Theme.Gutter = tui.NewColorAttr()
space := " "
opts.Gutter = &space
empty := ""
opts.Separator = &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]} {
if s.percent {
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)")
}
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
}
@@ -3627,6 +3647,10 @@ func (opts *Options) useTmux() bool {
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 {
if opts.Inputless {
return true
+26
View File
@@ -23,6 +23,32 @@ func escapeSingleQuote(str string) string {
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) {
ns := time.Now().UnixNano()
output := filepath.Join(os.TempDir(), fmt.Sprintf("fzf-%s-%d", name, ns))
+17 -17
View File
@@ -2,6 +2,7 @@ package fzf
import (
"math"
"slices"
"sort"
"unicode"
@@ -30,7 +31,7 @@ type Result struct {
func buildResult(item *Item, offsets []Offset, score int) Result {
if len(offsets) > 1 {
sort.Sort(ByOrder(offsets))
slices.SortFunc(offsets, compareOffsets)
}
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
// ------------ ---- -- ----
@@ -297,21 +298,20 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
return colors
}
// ByOrder is for sorting substring offsets
type ByOrder []Offset
func (a ByOrder) Len() int {
return len(a)
}
func (a ByOrder) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a ByOrder) Less(i, j int) bool {
ioff := a[i]
joff := a[j]
return (ioff[0] < joff[0]) || (ioff[0] == joff[0]) && (ioff[1] <= joff[1])
func compareOffsets(a, b Offset) int {
if a[0] < b[0] {
return -1
}
if a[0] > b[0] {
return 1
}
if a[1] < b[1] {
return -1
}
if a[1] > b[1] {
return 1
}
return 0
}
// ByRelevance is for sorting Items
+2 -1
View File
@@ -3,6 +3,7 @@ package fzf
import (
"math"
"math/rand"
"slices"
"sort"
"testing"
@@ -19,7 +20,7 @@ func TestOffsetSort(t *testing.T) {
offsets := []Offset{
{3, 5}, {2, 7},
{1, 3}, {2, 9}}
sort.Sort(ByOrder(offsets))
slices.SortFunc(offsets, compareOffsets)
if offsets[0][0] != 1 || offsets[0][1] != 3 ||
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() {
server := httpServer{
apiKey: []byte(apiKey),
actionChannel: actionChannel,
getHandler: getHandler,
}
for {
conn, err := listener.Accept()
if err != nil {
+344 -84
View File
@@ -13,6 +13,7 @@ import (
"os/exec"
"os/signal"
"regexp"
"slices"
"sort"
"strconv"
"strings"
@@ -1166,8 +1167,15 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
if baseTheme == nil {
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*
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
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
if t.headerBorderShape == tui.BorderLine {
if t.layout == layoutReverse {
@@ -1792,14 +1816,16 @@ func (t *Terminal) changeHeader(header string) bool {
return needFullRedraw
}
func (t *Terminal) changeFooter(footer string) {
func (t *Terminal) changeFooter(footer string) bool {
var lines []string
if len(footer) > 0 {
lines = strings.Split(strings.TrimSuffix(footer, "\n"), "\n")
}
needFullRedraw := len(t.footer) != len(lines)
t.footer = lines
t.clickFooterLine = 0
t.clickFooterColumn = 0
return needFullRedraw
}
// UpdateHeader updates the header
@@ -2131,7 +2157,7 @@ func (t *Terminal) adjustMarginAndPadding() (int, int, [4]int, [4]int) {
if idx == 3 {
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)
}
marginInt[idx] = sizeSpecToInt(idx, sizeSpec) + extraMargin[idx]
@@ -2236,6 +2262,98 @@ func (t *Terminal) determineHeaderLinesShape() (bool, tui.BorderShape) {
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) {
t.clearNumLinesCache()
t.forcePreview = forcePreview
@@ -2243,7 +2361,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
width := screenWidth - marginInt[1] - marginInt[3]
height := screenHeight - marginInt[0] - marginInt[2]
t.prevLines = make([]itemLine, screenHeight)
t.prevLines = make([]itemLine, max(1, screenHeight))
if t.border != nil && redrawBorder {
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
headerBorderHeight := 0
if hasHeaderWindow {
@@ -2359,37 +2521,62 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
if hasHeaderLinesWindow {
headerWindowHeight -= t.headerLines
}
headerBorderHeight = util.Constrain(borderLines(t.headerBorderShape)+headerWindowHeight, 0, availableLines)
if t.layout == layoutReverse {
shift += headerBorderHeight
shrink += headerBorderHeight
if t.headerBorderShape == tui.BorderInline {
addInline(headerWindowHeight, inlineRoleHeader)
} 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
if hasHeaderLinesWindow {
headerLinesHeight = util.Constrain(borderLines(headerLinesShape)+t.headerLines, 0, availableLines)
if t.layout != layoutDefault {
shift += headerLinesHeight
shrink += headerLinesHeight
if headerLinesShape == tui.BorderInline {
addInline(t.headerLines, inlineRoleHeaderLines)
} 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
if hasFooterWindow {
// Footer lines should not take all available lines
footerBorderHeight = util.Constrain(borderLines(t.footerBorderShape)+len(t.footer), 0, availableLines)
shrink += footerBorderHeight
if t.layout != layoutReverse {
shift += footerBorderHeight
if t.footerBorderShape == tui.BorderInline {
addInline(len(t.footer), inlineRoleFooter)
} else {
// Footer lines should not take all available lines
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
@@ -2500,12 +2687,12 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
if previewOpts.position == posUp {
innerBorderFn(marginInt[0]+pheight, marginInt[3], width, height-pheight)
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)
} else {
innerBorderFn(marginInt[0], marginInt[3], width, height-pheight)
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)
}
case posLeft, posRight:
@@ -2544,7 +2731,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
m = 1
}
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
// 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)
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
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)
t.window = t.tui.NewWindow(
innerMarginInt[0]+shift,
innerMarginInt[0]+shift+inlineTopLines,
innerMarginInt[3],
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 {
@@ -2654,7 +2846,11 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
if hasInputWindow {
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 {
case layoutDefault:
btop = w.Top() + w.Height()
@@ -2699,7 +2895,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
}
// Set up header border
if hasHeaderWindow {
if hasHeaderWindow && t.headerBorderShape != tui.BorderInline {
var btop int
if hasInputWindow && t.headerFirst {
if t.layout == layoutReverse {
@@ -2727,7 +2923,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
}
// Set up header lines border
if hasHeaderLinesWindow {
if hasHeaderLinesWindow && headerLinesShape != tui.BorderInline {
var btop int
// NOTE: We still have to handle --header-first here in case
// --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
if hasFooterWindow {
if hasFooterWindow && t.footerBorderShape != tui.BorderInline {
var btop int
if t.layout == layoutReverse {
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)
}
// Print border label
t.printLabel(t.wborder, t.listLabel, t.listLabelOpts, t.listLabelLen, t.listBorderShape, false)
// When the list label lands on an edge owned by an inline section, swap its bg
// 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.pborder, t.previewLabel, t.previewLabelOpts, t.previewLabelLen, t.activePreviewOpts.Border(), 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)
}
// 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) {
if window == nil {
if window == nil || window.Height() == 0 {
return
}
if window.Height() == 0 {
return
}
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 {
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
if borderShape == tui.BorderBottom || opts.bottom {
row = window.Height() - 1
}
window.Move(row, col)
render(window, window.Width())
t.printLabelAt(window, render, opts, length, row)
}
}
@@ -3184,8 +3395,19 @@ func (t *Terminal) resizeIfNeeded() bool {
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
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 {
t.printAll()
return true
@@ -3199,14 +3421,12 @@ func (t *Terminal) resizeIfNeeded() bool {
if needHeaderLinesWindow {
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) ||
(!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 && t.headerLines != t.headerLinesWindow.Height()) {
(needHeaderLinesWindow && t.headerLinesWindow != nil && mismatch(t.headerLinesShape, t.headerLinesWindow.Height(), t.headerLines)) {
t.printAll()
return true
}
@@ -3218,14 +3438,23 @@ func (t *Terminal) printHeader() {
return
}
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 {
// headerWindow is nil when hasHeaderWindow() returned false at resize time,
// e.g. --header-border=inline combined with empty header content. Don't
// delegate to printHeaderImpl because its nil-window branch folds the 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
// list when --no-list-border is in effect).
if !(t.headerBorderShape == tui.BorderInline && t.headerWindow == nil) {
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.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() {
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
if 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) {
var color tui.ColorPair
if current {
color = tui.ColCurrentCursorEmpty
color = tui.ColCurrentPointerEmpty
} else if !t.raw && t.gutterReverse || t.raw && t.gutterRawReverse {
if alt {
color = tui.ColAltCursorEmpty
color = tui.ColAltPointerEmpty
} else {
color = tui.ColCursorEmpty
color = tui.ColPointerEmpty
}
} else {
if alt {
color = tui.ColAltCursorEmptyChar
color = tui.ColAltPointerEmptyChar
} else {
color = tui.ColCursorEmptyChar
color = tui.ColPointerEmptyChar
}
}
gutter := t.pointerEmpty
@@ -3571,7 +3803,7 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
if len(label) == 0 {
t.gutter(true, false)
} else {
t.window.CPrint(tui.ColCurrentCursor, label)
t.window.CPrint(tui.ColCurrentPointer, label)
}
if w-t.markerLen < 0 {
return indentSize
@@ -3600,7 +3832,7 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
if len(label) == 0 {
t.gutter(false, index%2 == 1)
} else {
t.window.CPrint(tui.ColCursor, label)
t.window.CPrint(tui.ColPointer, label)
}
if w-t.markerLen < 0 {
return indentSize
@@ -3751,7 +3983,7 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
offset := Offset{int32(p), int32(p + w)}
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.
@@ -3788,7 +4020,7 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
end := start + int32(length)
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)
@@ -5951,11 +6183,28 @@ func (t *Terminal) Loop() error {
case reqRedrawInputLabel:
t.printLabel(t.inputBorder, t.inputLabel, t.inputLabelOpts, t.inputLabelLen, t.inputBorderShape, true)
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:
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:
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:
t.printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape, true)
case reqRedrawPreviewLabel:
@@ -6534,16 +6783,27 @@ func (t *Terminal) Loop() error {
t.cx = len(t.input)
case actChangeHeader, actTransformHeader, actBgTransformHeader:
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) {
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)
})
case actChangeFooter, actTransformFooter, actBgTransformFooter:
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)
})
case actChangeHeaderLabel, actTransformHeaderLabel, actBgTransformHeaderLabel:
+2 -2
View File
@@ -721,7 +721,7 @@ func TestWordWrapAnsiLine(t *testing.T) {
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)
if len(result) != 1 || result[0] != "abcdefghij" {
t.Errorf("Long word: %q", result)
@@ -749,7 +749,7 @@ func TestWordWrapAnsiLine(t *testing.T) {
// Tab handling: tab expands to tabstop-aligned width
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
result = term.wordWrapAnsiLine("\thi there", 15, 2)
if len(result) != 2 || result[0] != "\thi" || result[1] != "there" {
+1 -29
View File
@@ -1,39 +1,11 @@
package fzf
import (
"os"
"os/exec"
"github.com/junegunn/fzf/src/tui"
)
func runTmux(args []string, opts *Options) (int, error) {
// Prepare arguments
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 = "."
}
argStr, dir := popupArgStr(args, opts)
// Set tmux options for popup placement
// 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] {
case '1', '2', '3', '4', '5', '6', '7', '8', '9':
// Kitty iTerm2 WezTerm
// ARROW "\e[1;1D"
// SHIFT-ARROW "\e[1;2D"
// ALT-SHIFT-ARROW "\e[1;4D" "\e[1;10D" "\e[1;4D"
// CTRL-SHIFT-ARROW "\e[1;6D" N/A
@@ -743,6 +744,7 @@ func (r *LightRenderer) escSequence(sz *int) Event {
if shift {
return Event{ShiftUp, 0, nil}
}
return Event{Up, 0, nil}
case 'B':
if ctrlAltShift {
return Event{CtrlAltShiftDown, 0, nil}
@@ -765,6 +767,7 @@ func (r *LightRenderer) escSequence(sz *int) Event {
if shift {
return Event{ShiftDown, 0, nil}
}
return Event{Down, 0, nil}
case 'C':
if ctrlAltShift {
return Event{CtrlAltShiftRight, 0, nil}
@@ -787,6 +790,7 @@ func (r *LightRenderer) escSequence(sz *int) Event {
if alt {
return Event{AltRight, 0, nil}
}
return Event{Right, 0, nil}
case 'D':
if ctrlAltShift {
return Event{CtrlAltShiftLeft, 0, nil}
@@ -809,6 +813,7 @@ func (r *LightRenderer) escSequence(sz *int) Event {
if shift {
return Event{ShiftLeft, 0, nil}
}
return Event{Left, 0, nil}
case 'H':
if ctrlAltShift {
return Event{CtrlAltShiftHome, 0, nil}
@@ -831,6 +836,7 @@ func (r *LightRenderer) escSequence(sz *int) Event {
if shift {
return Event{ShiftHome, 0, nil}
}
return Event{Home, 0, nil}
case 'F':
if ctrlAltShift {
return Event{CtrlAltShiftEnd, 0, nil}
@@ -853,6 +859,7 @@ func (r *LightRenderer) escSequence(sz *int) Event {
if shift {
return Event{ShiftEnd, 0, nil}
}
return Event{End, 0, nil}
}
} // r.buffer[4]
} // r.buffer[3]
@@ -1122,127 +1129,144 @@ func (w *LightWindow) DrawHBorder() {
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) {
if w.height == 0 {
return
}
switch w.border.shape {
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble:
w.drawBorderAround(onlyHorizontal)
case BorderHorizontal:
w.drawBorderHorizontal(true, true)
case BorderVertical:
if onlyHorizontal {
return
}
w.drawBorderVertical(true, true)
case BorderTop:
w.drawBorderHorizontal(true, false)
case BorderBottom:
w.drawBorderHorizontal(false, true)
case BorderLeft:
if onlyHorizontal {
return
}
w.drawBorderVertical(true, false)
case BorderRight:
if onlyHorizontal {
return
}
w.drawBorderVertical(false, true)
shape := w.border.shape
if shape == BorderNone {
return
}
}
color := BorderColor(w.windowType)
hasLeft := shape.HasLeft()
hasRight := shape.HasRight()
func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
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)
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 shape.HasTop() {
var leftCap, rightCap rune
if hasLeft {
leftCap = w.border.topLeft
}
if right {
w.Move(y, w.width-vw-1)
w.CPrint(color, " ") // Margin
w.CPrint(color, string(w.border.right))
if hasRight {
rightCap = w.border.topRight
}
w.drawHLine(0, w.border.top, leftCap, rightCap, color)
}
}
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 {
if !onlyHorizontal && (hasLeft || hasRight) {
vw := runeWidth(w.border.left)
for y := 1; y < w.height-1; y++ {
w.Move(y, 0)
w.CPrint(color, string(w.border.left))
w.CPrint(color, " ") // Margin
w.Move(y, w.width-vw-1)
w.CPrint(color, " ") // Margin
w.CPrint(color, string(w.border.right))
for y := 0; y < w.height; y++ {
// Corner rows are already painted by drawHLine above / below.
if (y == 0 && shape.HasTop()) || (y == w.height-1 && shape.HasBottom()) {
continue
}
if hasLeft {
w.Move(y, 0)
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)
rem = (w.width - bcw) % hw
w.CPrint(color, string(w.border.bottomLeft)+repeat(w.border.bottom, (w.width-bcw)/hw)+repeat(' ', rem)+string(w.border.bottomRight))
if 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) csi(code string) string {
+135 -54
View File
@@ -1017,6 +1017,115 @@ func (w *TcellWindow) DrawHBorder() {
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) {
if w.height == 0 {
return
@@ -1031,72 +1140,44 @@ func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
top := w.top
bot := top + w.height
var style tcell.Style
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()
}
style := w.borderStyleFor(w.windowType)
hw := runeWidth(w.borderStyle.top)
switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderHorizontal, BorderTop:
max := right - 2*hw
if shape == BorderHorizontal || shape == BorderTop {
max = right - hw
hasLeft := shape.HasLeft()
hasRight := shape.HasRight()
if shape.HasTop() {
var leftCap, rightCap rune
if hasLeft {
leftCap = w.borderStyle.topLeft
}
// tcell has an issue displaying two overlapping wide runes
// e.g. SetContent( HH )
// SetContent( TR )
// ==================
// ( HH ) => TR is ignored
for x := left; x <= max; x += hw {
_screen.SetContent(x, top, w.borderStyle.top, nil, style)
if hasRight {
rightCap = w.borderStyle.topRight
}
w.drawHLine(top, w.borderStyle.top, leftCap, rightCap, style)
}
switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderHorizontal, BorderBottom:
max := right - 2*hw
if shape == BorderHorizontal || shape == BorderBottom {
max = right - hw
if shape.HasBottom() {
var leftCap, rightCap rune
if hasLeft {
leftCap = w.borderStyle.bottomLeft
}
for x := left; x <= max; x += hw {
_screen.SetContent(x, bot-1, w.borderStyle.bottom, nil, style)
if hasRight {
rightCap = w.borderStyle.bottomRight
}
w.drawHLine(bot-1, w.borderStyle.bottom, leftCap, rightCap, style)
}
if !onlyHorizontal {
switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderVertical, BorderLeft:
for y := top; y < bot; y++ {
vw := runeWidth(w.borderStyle.right)
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)
}
}
switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderVertical, BorderRight:
vw := runeWidth(w.borderStyle.right)
for y := top; y < bot; y++ {
if hasRight {
_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
Spinner ColorAttr
Info ColorAttr
Cursor ColorAttr
Pointer ColorAttr
Marker ColorAttr
Header ColorAttr
HeaderBg ColorAttr
@@ -595,11 +595,13 @@ const (
BorderBottom
BorderLeft
BorderRight
BorderInline
BorderDashed
)
func (s BorderShape) HasLeft() bool {
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 true
@@ -607,7 +609,7 @@ func (s BorderShape) HasLeft() bool {
func (s BorderShape) HasRight() bool {
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 true
@@ -615,7 +617,7 @@ func (s BorderShape) HasRight() bool {
func (s BorderShape) HasTop() bool {
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 true
@@ -623,7 +625,7 @@ func (s BorderShape) HasTop() bool {
func (s BorderShape) HasBottom() bool {
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 true
@@ -643,6 +645,8 @@ type BorderStyle struct {
topRight rune
bottomLeft rune
bottomRight rune
leftMid rune
rightMid rune
}
type BorderCharacter int
@@ -658,7 +662,9 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
topLeft: ' ',
topRight: ' ',
bottomLeft: ' ',
bottomRight: ' '}
bottomRight: ' ',
leftMid: ' ',
rightMid: ' '}
}
if !unicode {
return BorderStyle{
@@ -671,6 +677,8 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
topRight: '+',
bottomLeft: '+',
bottomRight: '+',
leftMid: '+',
rightMid: '+',
}
}
switch shape {
@@ -685,6 +693,8 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
topRight: '┐',
bottomLeft: '└',
bottomRight: '┘',
leftMid: '├',
rightMid: '┤',
}
case BorderBold:
return BorderStyle{
@@ -697,6 +707,8 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
topRight: '┓',
bottomLeft: '┗',
bottomRight: '┛',
leftMid: '┣',
rightMid: '┫',
}
case BorderBlock:
// ▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜
@@ -712,6 +724,8 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
topRight: '▜',
bottomLeft: '▙',
bottomRight: '▟',
leftMid: '▌',
rightMid: '▐',
}
case BorderThinBlock:
@@ -728,6 +742,8 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
topRight: '🭾',
bottomLeft: '🭼',
bottomRight: '🭿',
leftMid: '▏',
rightMid: '▕',
}
case BorderDouble:
@@ -741,6 +757,25 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
topRight: '╗',
bottomLeft: '╚',
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{
@@ -753,6 +788,8 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
topRight: '╮',
bottomLeft: '╰',
bottomRight: '╯',
leftMid: '├',
rightMid: '┤',
}
}
@@ -774,6 +811,35 @@ const (
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 {
DefaultTheme() *ColorTheme
Init() error
@@ -811,6 +877,19 @@ type Window interface {
DrawBorder()
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()
FinishFill()
@@ -869,18 +948,18 @@ var (
ColDisabled ColorPair
ColGhost ColorPair
ColMatch ColorPair
ColCursor ColorPair
ColCursorEmpty ColorPair
ColCursorEmptyChar ColorPair
ColAltCursorEmpty ColorPair
ColAltCursorEmptyChar ColorPair
ColPointer ColorPair
ColPointerEmpty ColorPair
ColPointerEmptyChar ColorPair
ColAltPointerEmpty ColorPair
ColAltPointerEmptyChar ColorPair
ColMarker ColorPair
ColSelected ColorPair
ColSelectedMatch ColorPair
ColCurrent ColorPair
ColCurrentMatch ColorPair
ColCurrentCursor ColorPair
ColCurrentCursorEmpty ColorPair
ColCurrentPointer ColorPair
ColCurrentPointerEmpty ColorPair
ColCurrentMarker ColorPair
ColCurrentSelectedEmpty ColorPair
ColSpinner ColorPair
@@ -929,7 +1008,7 @@ func init() {
CurrentMatch: undefined,
Spinner: defaultColor,
Info: defaultColor,
Cursor: defaultColor,
Pointer: defaultColor,
Marker: defaultColor,
Header: defaultColor,
Border: undefined,
@@ -979,7 +1058,7 @@ func init() {
CurrentMatch: undefined,
Spinner: undefined,
Info: undefined,
Cursor: undefined,
Pointer: undefined,
Marker: undefined,
Header: undefined,
Footer: undefined,
@@ -1030,7 +1109,7 @@ func init() {
CurrentMatch: ColorAttr{colBrightGreen, AttrUndefined},
Spinner: ColorAttr{colGreen, AttrUndefined},
Info: ColorAttr{colYellow, AttrUndefined},
Cursor: ColorAttr{colRed, AttrUndefined},
Pointer: ColorAttr{colRed, AttrUndefined},
Marker: ColorAttr{colMagenta, AttrUndefined},
Header: ColorAttr{colCyan, AttrUndefined},
Footer: ColorAttr{colCyan, AttrUndefined},
@@ -1081,7 +1160,7 @@ func init() {
CurrentMatch: ColorAttr{151, AttrUndefined},
Spinner: ColorAttr{148, AttrUndefined},
Info: ColorAttr{144, AttrUndefined},
Cursor: ColorAttr{161, AttrUndefined},
Pointer: ColorAttr{161, AttrUndefined},
Marker: ColorAttr{168, AttrUndefined},
Header: ColorAttr{109, AttrUndefined},
Footer: ColorAttr{109, AttrUndefined},
@@ -1132,7 +1211,7 @@ func init() {
CurrentMatch: ColorAttr{23, AttrUndefined},
Spinner: ColorAttr{65, AttrUndefined},
Info: ColorAttr{101, AttrUndefined},
Cursor: ColorAttr{161, AttrUndefined},
Pointer: ColorAttr{161, AttrUndefined},
Marker: ColorAttr{168, AttrUndefined},
Header: 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 {
theme.Bg = ColorAttr{colBlack, AttrUndefined}
}
@@ -1183,7 +1262,7 @@ func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, boldify bool, forceBlac
theme.CurrentMatch = boldify(theme.CurrentMatch)
theme.Prompt = boldify(theme.Prompt)
theme.Input = boldify(theme.Input)
theme.Cursor = boldify(theme.Cursor)
theme.Pointer = boldify(theme.Pointer)
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.Spinner = o(baseTheme.Spinner, theme.Spinner)
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.Header = o(baseTheme.Header, theme.Header)
theme.Footer = o(baseTheme.Footer, theme.Footer)
@@ -1300,11 +1379,22 @@ func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, boldify bool, forceBlac
} else {
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.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)
if theme.Nomatch.IsUndefined() {
@@ -1332,11 +1422,11 @@ func initPalette(theme *ColorTheme) {
ColDisabled = pair(theme.Disabled, theme.InputBg)
ColMatch = pair(theme.Match, theme.ListBg)
ColSelectedMatch = pair(theme.SelectedMatch, theme.SelectedBg)
ColCursor = pair(theme.Cursor, theme.Gutter)
ColCursorEmpty = pair(blank, theme.Gutter)
ColCursorEmptyChar = pair(theme.Gutter, theme.ListBg)
ColAltCursorEmpty = pair(blank, theme.AltGutter)
ColAltCursorEmptyChar = pair(theme.AltGutter, theme.ListBg)
ColPointer = pair(theme.Pointer, theme.Gutter)
ColPointerEmpty = pair(blank, theme.Gutter)
ColPointerEmptyChar = pair(theme.Gutter, theme.ListBg)
ColAltPointerEmpty = pair(blank, theme.AltGutter)
ColAltPointerEmptyChar = pair(theme.AltGutter, theme.ListBg)
if theme.SelectedBg.Color != theme.ListBg.Color {
ColMarker = pair(theme.Marker, theme.SelectedBg)
} else {
@@ -1344,8 +1434,8 @@ func initPalette(theme *ColorTheme) {
}
ColCurrent = pair(theme.Current, theme.DarkBg)
ColCurrentMatch = pair(theme.CurrentMatch, theme.DarkBg)
ColCurrentCursor = pair(theme.Cursor, theme.DarkBg)
ColCurrentCursorEmpty = pair(blank, theme.DarkBg)
ColCurrentPointer = pair(theme.Pointer, theme.DarkBg)
ColCurrentPointerEmpty = pair(blank, theme.DarkBg)
ColCurrentMarker = pair(theme.Marker, theme.DarkBg)
ColCurrentSelectedEmpty = pair(blank, theme.DarkBg)
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)
}
// Exact fit no wrapping needed
// Exact fit - no wrapping needed
lines = WrapLine("hello", 0, 5, 8, 2)
if len(lines) != 1 || lines[0].Text != "hello" || lines[0].DisplayWidth != 5 {
t.Errorf("Exact fit: %v", lines)
+44 -7
View File
@@ -11,6 +11,8 @@ import (
"strings"
"sync/atomic"
"syscall"
"golang.org/x/sys/windows"
)
type shellType int
@@ -19,6 +21,7 @@ const (
shellTypeUnknown shellType = iota
shellTypeCmd
shellTypePowerShell
shellTypePwsh
)
var escapeRegex = regexp.MustCompile(`[&|<>()^%!"]`)
@@ -46,7 +49,10 @@ func NewExecutor(withShell string) *Executor {
} else if strings.HasPrefix(basename, "cmd") {
shellType = shellTypeCmd
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
args = []string{"-NoProfile", "-Command"}
} else {
@@ -56,8 +62,12 @@ func NewExecutor(withShell string) *Executor {
}
// 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,
// but it is left as is now because no adverse effect has been observed.
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)
}
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
if x.shellType == shellTypeCmd {
cmd = exec.Command(shell)
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: false,
CmdLine: fmt.Sprintf(`%s "%s"`, strings.Join(x.args, " "), command),
CreationFlags: 0,
CreationFlags: creationFlags,
}
} 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{
HideWindow: false,
CreationFlags: 0,
CreationFlags: creationFlags,
}
}
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"
*/
return escapeArg(entry)
case shellTypePowerShell:
case shellTypePowerShell, shellTypePwsh:
escaped := strings.ReplaceAll(entry, `"`, `\"`)
return "'" + strings.ReplaceAll(escaped, "'", "''") + "'"
default:
@@ -166,6 +188,21 @@ func (x *Executor) QuoteEntry(entry string) string {
// KillCommand kills the process for the given command
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()
}
+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_API_KEY
# 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_COMPLETION_TRIGGER '++'
set -gx fish_history fzf_test
# Add fzf to PATH
+82
View File
@@ -105,6 +105,23 @@ class Tmux
go(%W[send-keys -t #{win}] + args.map(&:to_s))
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)
system('tmux', 'setb', str, ';', 'pasteb', '-t', win, ';', 'send-keys', '-t', win, 'Enter')
end
@@ -113,6 +130,71 @@ class Tmux
go(%W[capture-pane -p -J -t #{win}]).map(&:rstrip).reverse.drop_while(&:empty?).reverse
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)
lines = nil
begin
+9 -9
View File
@@ -1672,7 +1672,7 @@ class TestCore < TestInteractive
end
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.until do |lines|
assert_equal 1000, lines.match_count
@@ -1694,7 +1694,7 @@ class TestCore < TestInteractive
tmux.send_keys :Up
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.until do |lines|
assert_equal 3, lines.match_count
@@ -1709,7 +1709,7 @@ class TestCore < TestInteractive
tmux.send_keys :Up
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)
tmux.send_keys 'C-r'
tmux.until do |lines|
@@ -1727,7 +1727,7 @@ class TestCore < TestInteractive
assert_includes lines[-2], '+T'
end
# Trigger slow reload should show +T* while blocked
# Trigger slow reload - should show +T* while blocked
tmux.send_keys 'C-r'
tmux.until { |lines| assert_includes lines[-2], '+T*' }
@@ -1769,7 +1769,7 @@ class TestCore < TestInteractive
assert_includes lines, '> 1'
end
# Trigger reload blocked during initial sleep
# Trigger reload - blocked during initial sleep
tmux.send_keys 'C-r'
tmux.until { |lines| assert_includes lines[-2], '+T*' }
# Match "1" arrives, unblocks before the remaining items load
@@ -1790,7 +1790,7 @@ class TestCore < TestInteractive
assert_includes lines, '> 1'
end
# Trigger reload-sync every observable state must be either:
# Trigger reload-sync - every observable state must be either:
# 1. +T* (still blocked), or
# 2. final state (count=10, +T without *)
# Any other combination (e.g. unblocked while count < 10) is a bug.
@@ -1835,7 +1835,7 @@ class TestCore < TestInteractive
tmux.send_keys :Up
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.until { |lines| assert_includes lines[-2], '+T*' }
# After stream completes, unblocks with cursor at same position (second item)
@@ -1857,7 +1857,7 @@ class TestCore < TestInteractive
tmux.send_keys 'C-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.until do |lines|
assert_equal 3, lines.match_count
@@ -1876,7 +1876,7 @@ class TestCore < TestInteractive
tmux.send_keys :Up, :Up, :Tab
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.until do |lines|
assert_equal 3, lines.match_count
+333
View File
@@ -1298,4 +1298,337 @@ class TestLayout < TestInteractive
tmux.send_keys :Enter
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
+1 -1
View File
@@ -40,7 +40,7 @@ class TestServer < TestInteractive
assert_equal [0, 1], state[:current][:positions]
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> ')
tmux.until { |lines| assert_equal 100, lines.item_count }
tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] }