mirror of
https://github.com/junegunn/fzf.git
synced 2026-04-24 16:42:45 +08:00
Compare commits
2 Commits
dependabot
...
zellij
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a099d76fa6 | ||
|
|
a5646b46e8 |
@@ -1,6 +1,6 @@
|
|||||||
root = true
|
root = true
|
||||||
|
|
||||||
[*.{sh,bash,fish}]
|
[*.{sh,bash}]
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
simplify = true
|
simplify = true
|
||||||
|
|||||||
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -1 +0,0 @@
|
|||||||
* @junegunn
|
|
||||||
2
.github/workflows/linux.yml
vendored
2
.github/workflows/linux.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
|||||||
go-version: "1.23"
|
go-version: "1.23"
|
||||||
|
|
||||||
- name: Setup Ruby
|
- name: Setup Ruby
|
||||||
uses: ruby/setup-ruby@7372622e62b60b3cb750dcd2b9e32c247ffec26a # v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 3.4.6
|
ruby-version: 3.4.6
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/macos.yml
vendored
2
.github/workflows/macos.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
|||||||
go-version: "1.23"
|
go-version: "1.23"
|
||||||
|
|
||||||
- name: Setup Ruby
|
- name: Setup Ruby
|
||||||
uses: ruby/setup-ruby@7372622e62b60b3cb750dcd2b9e32c247ffec26a # v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 3.0.0
|
ruby-version: 3.0.0
|
||||||
|
|
||||||
|
|||||||
24
.github/workflows/sponsors.yml
vendored
Normal file
24
.github/workflows/sponsors.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
name: Generate Sponsors README
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: 0 15 * * 6
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout 🛎️
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Generate Sponsors 💖
|
||||||
|
uses: JamesIves/github-sponsors-readme-action@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.SPONSORS_TOKEN }}
|
||||||
|
file: 'README.md'
|
||||||
|
|
||||||
|
- name: Deploy to GitHub Pages 🚀
|
||||||
|
uses: JamesIves/github-pages-deploy-action@v4
|
||||||
|
with:
|
||||||
|
branch: master
|
||||||
|
folder: '.'
|
||||||
2
.github/workflows/typos.yml
vendored
2
.github/workflows/typos.yml
vendored
@@ -7,4 +7,4 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
- uses: crate-ci/typos@685eb3d55be2f85191e8c84acb9f44d7756f84ab # v1.29.4
|
- uses: crate-ci/typos@v1.29.4
|
||||||
|
|||||||
2
.github/workflows/winget.yml
vendored
2
.github/workflows/winget.yml
vendored
@@ -7,7 +7,7 @@ jobs:
|
|||||||
publish:
|
publish:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: vedantmgoyal2009/winget-releaser@4ffc7888bffd451b357355dc214d43bb9f23917e # v2
|
- uses: vedantmgoyal2009/winget-releaser@v2
|
||||||
with:
|
with:
|
||||||
identifier: junegunn.fzf
|
identifier: junegunn.fzf
|
||||||
installers-regex: '-windows_(armv7|arm64|amd64)\.zip$'
|
installers-regex: '-windows_(armv7|arm64|amd64)\.zip$'
|
||||||
|
|||||||
14
ADVANCED.md
14
ADVANCED.md
@@ -309,16 +309,16 @@ I know it's a lot to digest, let's try to break down the code.
|
|||||||
available color options.
|
available color options.
|
||||||
- The value of `--preview-window` option consists of 5 components delimited
|
- The value of `--preview-window` option consists of 5 components delimited
|
||||||
by `,`
|
by `,`
|
||||||
1. `up` -- Position of the preview window
|
1. `up` — Position of the preview window
|
||||||
1. `60%` -- Size of the preview window
|
1. `60%` — Size of the preview window
|
||||||
1. `border-bottom` -- Preview window border only on the bottom side
|
1. `border-bottom` — Preview window border only on the bottom side
|
||||||
1. `+{2}+3/3` -- Scroll offset of the preview contents
|
1. `+{2}+3/3` — Scroll offset of the preview contents
|
||||||
1. `~3` -- Fixed header
|
1. `~3` — Fixed header
|
||||||
- Let's break down the latter two. We want to display the bat output in the
|
- Let's break down the latter two. We want to display the bat output in the
|
||||||
preview window with a certain scroll offset so that the matching line is
|
preview window with a certain scroll offset so that the matching line is
|
||||||
positioned near the center of the preview window.
|
positioned near the center of the preview window.
|
||||||
- `+{2}` -- The base offset is extracted from the second token
|
- `+{2}` — The base offset is extracted from the second token
|
||||||
- `+3` -- We add 3 lines to the base offset to compensate for the header
|
- `+3` — We add 3 lines to the base offset to compensate for the header
|
||||||
part of `bat` output
|
part of `bat` output
|
||||||
- ```
|
- ```
|
||||||
───────┬──────────────────────────────────────────────────────────
|
───────┬──────────────────────────────────────────────────────────
|
||||||
|
|||||||
45
CHANGELOG.md
45
CHANGELOG.md
@@ -1,31 +1,8 @@
|
|||||||
CHANGELOG
|
CHANGELOG
|
||||||
=========
|
=========
|
||||||
|
|
||||||
0.72.0
|
|
||||||
------
|
|
||||||
- `--header-border`, `--header-lines-border`, and `--footer-border` now accept a new `inline` style that embeds the section inside the list frame, separated from the list content by a horizontal line. When the list border has side segments, the separator joins them as T-junctions.
|
|
||||||
- Requires a `--list-border` shape that has both top and bottom segments (`rounded`, `sharp`, `bold`, `double`, `block`, `thinblock`, or `horizontal`); falls back to `line` otherwise. `horizontal` has no side borders, so the separator is drawn without T-junction endpoints.
|
|
||||||
- Sections stack. Example combining all three:
|
|
||||||
```sh
|
|
||||||
ps -ef | fzf --reverse --style full:double \
|
|
||||||
--header 'Select a process' --header-lines 1 \
|
|
||||||
--bind 'load:transform-footer:echo $FZF_TOTAL_COUNT processes' \
|
|
||||||
--header-border=inline --header-lines-border=inline \
|
|
||||||
--footer-border=inline
|
|
||||||
```
|
|
||||||
- `--header-label` and `--footer-label` render on their respective separator row.
|
|
||||||
- The separator inherits `--color list-border` when the section's own border color is not explicitly set.
|
|
||||||
- `inline` takes precedence over `--header-first`: the inline section stays inside the list frame. `--header-border=inline` requires `--header-lines-border` to be `inline` or unset.
|
|
||||||
- [vim] Move and resize popup window when detecting `VimResized` event (#4778) (@Vulcalien)
|
|
||||||
- Bug fixes
|
|
||||||
- Fixed gutter display in `--style=minimal`
|
|
||||||
- Fixed arrow keys / Home / End without modifiers being ignored under the kitty keyboard protocol (#4776) (@TymekDev)
|
|
||||||
- bash: Persist history deletion when `histappend` is on (#4764)
|
|
||||||
|
|
||||||
0.71.0
|
0.71.0
|
||||||
------
|
------
|
||||||
_Release highlights: https://junegunn.github.io/fzf/releases/0.71.0/_
|
|
||||||
|
|
||||||
- Added `--popup` as a new name for `--tmux` with Zellij support
|
- Added `--popup` as a new name for `--tmux` with Zellij support
|
||||||
- `--popup` starts fzf in a tmux popup or a Zellij floating pane
|
- `--popup` starts fzf in a tmux popup or a Zellij floating pane
|
||||||
- `--tmux` is now an alias for `--popup`
|
- `--tmux` is now an alias for `--popup`
|
||||||
@@ -44,34 +21,24 @@ _Release highlights: https://junegunn.github.io/fzf/releases/0.71.0/_
|
|||||||
- The search performance now scales linearly with the number of CPU cores, as we dropped static partitioning to allow better load balancing across threads.
|
- The search performance now scales linearly with the number of CPU cores, as we dropped static partitioning to allow better load balancing across threads.
|
||||||
```
|
```
|
||||||
=== query: 'linux' ===
|
=== query: 'linux' ===
|
||||||
[all] baseline: 21.95ms current: 17.47ms (1.26x) matches: 179966 (12.79%)
|
[all] baseline: 17.12ms current: 14.28ms (1.20x) matches: 179966 (12.79%)
|
||||||
[1T] baseline: 179.63ms current: 180.53ms (1.00x) matches: 179966 (12.79%)
|
[1T] baseline: 136.49ms current: 137.25ms (0.99x) matches: 179966 (12.79%)
|
||||||
[2T] baseline: 97.38ms current: 90.05ms (1.08x) matches: 179966 (12.79%)
|
[2T] baseline: 75.74ms current: 68.75ms (1.10x) matches: 179966 (12.79%)
|
||||||
[4T] baseline: 53.83ms current: 44.77ms (1.20x) matches: 179966 (12.79%)
|
[4T] baseline: 41.16ms current: 34.97ms (1.18x) matches: 179966 (12.79%)
|
||||||
[8T] baseline: 41.66ms current: 22.58ms (1.84x) matches: 179966 (12.79%)
|
[8T] baseline: 32.82ms current: 17.79ms (1.84x) matches: 179966 (12.79%)
|
||||||
```
|
```
|
||||||
- Improved the cache structure, reducing memory footprint per entry by 86x.
|
- Improved the cache structure, reducing memory footprint per entry by 86x.
|
||||||
- With the reduced per-entry cost, the cache now has broader coverage.
|
- With the reduced per-entry cost, the cache now has broader coverage.
|
||||||
- Shell integration improvements
|
- Shell integration improvements
|
||||||
- bash: CTRL-R now supports multi-select and `shift-delete` to delete history entries (#4715)
|
- bash: CTRL-R now supports multi-select and `shift-delete` to delete history entries (#4715)
|
||||||
- fish:
|
- fish: Improved command history (CTRL-R) (#4703) (@bitraid)
|
||||||
- Improved command history (CTRL-R) (#4703) (@bitraid)
|
|
||||||
- Rewrite completion script (SHIFT-TAB) (#4731) (@bitraid)
|
|
||||||
- Increase minimum fish version requirement to 3.4.0 (#4731) (@bitraid)
|
|
||||||
- `GET /` HTTP endpoint now includes `positions` field in each match entry, providing the indices of matched characters for external highlighting (#4726)
|
- `GET /` HTTP endpoint now includes `positions` field in each match entry, providing the indices of matched characters for external highlighting (#4726)
|
||||||
- Allow adaptive height with negative value (`--height=~-HEIGHT`) (#4682)
|
|
||||||
- Bug fixes
|
- Bug fixes
|
||||||
- `--walker=follow` no longer follows symlinks whose target is an ancestor of the walker root, avoiding severe resource exhaustion when a symlink points outside the tree (e.g. Wine's `z:` → `/`) (#4710)
|
- `--walker=follow` no longer follows symlinks whose target is an ancestor of the walker root, avoiding severe resource exhaustion when a symlink points outside the tree (e.g. Wine's `z:` → `/`) (#4710)
|
||||||
- Fixed AWK tokenizer not treating a new line character as whitespace
|
- Fixed AWK tokenizer not treating a new line character as whitespace
|
||||||
- Fixed `--{accept,with}-nth` removing trailing whitespaces with a non-default `--delimiter`
|
- Fixed `--{accept,with}-nth` removing trailing whitespaces with a non-default `--delimiter`
|
||||||
- Fixed OSC8 hyperlinks being mangled when the URL contains unicode characters (#4707)
|
- Fixed OSC8 hyperlinks being mangled when the URL contains unicode characters (#4707)
|
||||||
- Fixed `--with-shell` not handling quoted arguments correctly (#4709)
|
- Fixed `--with-shell` not handling quoted arguments correctly (#4709)
|
||||||
- Fixed child processes not being terminated on Windows (#4723) (@pjeby)
|
|
||||||
- Fixed preview scrollbar not rendered after `toggle-preview`
|
|
||||||
- Fixed preview follow/scroll with long wrapped lines
|
|
||||||
- Fixed tab width when `--frozen-left` is used
|
|
||||||
- Fixed preview mouse events being processed when no preview window exists
|
|
||||||
- zsh: Fixed history widget when `sh_glob` option is on (#4714) (@EvanHahn)
|
|
||||||
|
|
||||||
0.70.0
|
0.70.0
|
||||||
------
|
------
|
||||||
|
|||||||
2
Makefile
2
Makefile
@@ -53,8 +53,6 @@ ifeq ($(UNAME_M),x86_64)
|
|||||||
BINARY := $(BINARY64)
|
BINARY := $(BINARY64)
|
||||||
else ifeq ($(UNAME_M),amd64)
|
else ifeq ($(UNAME_M),amd64)
|
||||||
BINARY := $(BINARY64)
|
BINARY := $(BINARY64)
|
||||||
else ifeq ($(UNAME_M),i86pc)
|
|
||||||
BINARY := $(BINARY64)
|
|
||||||
else ifeq ($(UNAME_M),s390x)
|
else ifeq ($(UNAME_M),s390x)
|
||||||
BINARY := $(BINARYS390)
|
BINARY := $(BINARYS390)
|
||||||
else ifeq ($(UNAME_M),i686)
|
else ifeq ($(UNAME_M),i686)
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ the whole if we start off with `:FZF` command.
|
|||||||
" Bang version starts fzf in fullscreen mode
|
" Bang version starts fzf in fullscreen mode
|
||||||
:FZF!
|
:FZF!
|
||||||
<
|
<
|
||||||
Similarly to {ctrlp.vim}{3}, use Enter key, CTRL-T, CTRL-X or CTRL-V to open
|
Similarly to {ctrlp.vim}{3}, use enter key, CTRL-T, CTRL-X or CTRL-V to open
|
||||||
selected files in the current window, in new tabs, in horizontal splits, or in
|
selected files in the current window, in new tabs, in horizontal splits, or in
|
||||||
vertical splits respectively.
|
vertical splits respectively.
|
||||||
|
|
||||||
@@ -218,6 +218,7 @@ list:
|
|||||||
`fg` / `bg` / `hl` | Item (foreground / background / highlight)
|
`fg` / `bg` / `hl` | Item (foreground / background / highlight)
|
||||||
`fg+` / `bg+` / `hl+` | Current item (foreground / background / highlight)
|
`fg+` / `bg+` / `hl+` | Current item (foreground / background / highlight)
|
||||||
`preview-fg` / `preview-bg` | Preview window text and background
|
`preview-fg` / `preview-bg` | Preview window text and background
|
||||||
|
`hl` / `hl+` | Highlighted substrings (normal / current)
|
||||||
`gutter` | Background of the gutter on the left
|
`gutter` | Background of the gutter on the left
|
||||||
`pointer` | Pointer to the current line ( `>` )
|
`pointer` | Pointer to the current line ( `>` )
|
||||||
`marker` | Multi-select marker ( `>` )
|
`marker` | Multi-select marker ( `>` )
|
||||||
@@ -228,6 +229,7 @@ list:
|
|||||||
`query` | Query string
|
`query` | Query string
|
||||||
`disabled` | Query string when search is disabled
|
`disabled` | Query string when search is disabled
|
||||||
`prompt` | Prompt before query ( `> ` )
|
`prompt` | Prompt before query ( `> ` )
|
||||||
|
`pointer` | Pointer to the current line ( `>` )
|
||||||
----------------------------+------------------------------------------------------
|
----------------------------+------------------------------------------------------
|
||||||
- `component` specifies the component (`fg` / `bg`) from which to extract the
|
- `component` specifies the component (`fg` / `bg`) from which to extract the
|
||||||
color when considering each of the following highlight groups
|
color when considering each of the following highlight groups
|
||||||
@@ -243,7 +245,7 @@ if it exists, - otherwise use the `fg` attribute of the `Comment` highlight
|
|||||||
group if it exists, - otherwise fall back to the default color settings for
|
group if it exists, - otherwise fall back to the default color settings for
|
||||||
the prompt.
|
the prompt.
|
||||||
|
|
||||||
You can examine the color option generated according to the setting by printing
|
You can examine the color option generated according the setting by printing
|
||||||
the result of `fzf#wrap()` function like so:
|
the result of `fzf#wrap()` function like so:
|
||||||
>
|
>
|
||||||
:echo fzf#wrap()
|
:echo fzf#wrap()
|
||||||
|
|||||||
13
install
13
install
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
version=0.71.0
|
version=0.70.0
|
||||||
auto_completion=
|
auto_completion=
|
||||||
key_bindings=
|
key_bindings=
|
||||||
update_config=2
|
update_config=2
|
||||||
@@ -112,15 +112,10 @@ link_fzf_in_path() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
tar_opts="-xzf -"
|
|
||||||
if tar --no-same-owner -tf /dev/null 2> /dev/null; then
|
|
||||||
tar_opts="--no-same-owner $tar_opts"
|
|
||||||
fi
|
|
||||||
|
|
||||||
try_curl() {
|
try_curl() {
|
||||||
command -v curl > /dev/null &&
|
command -v curl > /dev/null &&
|
||||||
if [[ $1 =~ tar.gz$ ]]; then
|
if [[ $1 =~ tar.gz$ ]]; then
|
||||||
curl -fL $1 | tar $tar_opts
|
curl -fL $1 | tar --no-same-owner -xzf -
|
||||||
else
|
else
|
||||||
local temp=${TMPDIR:-/tmp}/fzf.zip
|
local temp=${TMPDIR:-/tmp}/fzf.zip
|
||||||
curl -fLo "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
|
curl -fLo "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
|
||||||
@@ -130,7 +125,7 @@ try_curl() {
|
|||||||
try_wget() {
|
try_wget() {
|
||||||
command -v wget > /dev/null &&
|
command -v wget > /dev/null &&
|
||||||
if [[ $1 =~ tar.gz$ ]]; then
|
if [[ $1 =~ tar.gz$ ]]; then
|
||||||
wget -O - $1 | tar $tar_opts
|
wget -O - $1 | tar --no-same-owner -xzf -
|
||||||
else
|
else
|
||||||
local temp=${TMPDIR:-/tmp}/fzf.zip
|
local temp=${TMPDIR:-/tmp}/fzf.zip
|
||||||
wget -O "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
|
wget -O "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
|
||||||
@@ -444,7 +439,7 @@ if [ $update_config -eq 1 ]; then
|
|||||||
echo
|
echo
|
||||||
fi
|
fi
|
||||||
[[ $shells =~ zsh ]] && echo " source ${ZDOTDIR:-~}/.zshrc # zsh"
|
[[ $shells =~ zsh ]] && echo " source ${ZDOTDIR:-~}/.zshrc # zsh"
|
||||||
[[ $shells =~ fish && $lno_func -ne 0 ]] && echo ' fish_user_key_bindings # fish'
|
[[ $shells =~ fish && $lno_func -ne 0 ]] && echo ' fzf_user_key_bindings # fish'
|
||||||
echo
|
echo
|
||||||
echo 'Use uninstall script to remove fzf.'
|
echo 'Use uninstall script to remove fzf.'
|
||||||
echo
|
echo
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
$version="0.71.0"
|
$version="0.70.0"
|
||||||
|
|
||||||
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
|
||||||
|
|||||||
2
main.go
2
main.go
@@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/junegunn/fzf/src/protector"
|
"github.com/junegunn/fzf/src/protector"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "0.71"
|
var version = "0.70"
|
||||||
var revision = "devel"
|
var revision = "devel"
|
||||||
|
|
||||||
//go:embed shell/key-bindings.bash
|
//go:embed shell/key-bindings.bash
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
THE SOFTWARE.
|
THE SOFTWARE.
|
||||||
..
|
..
|
||||||
.TH fzf\-tmux 1 "Apr 2026" "fzf 0.71.0" "fzf\-tmux - open fzf in tmux split pane"
|
.TH fzf\-tmux 1 "Mar 2026" "fzf 0.70.0" "fzf\-tmux - open fzf in tmux split pane"
|
||||||
|
|
||||||
.SH NAME
|
.SH NAME
|
||||||
fzf\-tmux - open fzf in tmux split pane
|
fzf\-tmux - open fzf in tmux split pane
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
THE SOFTWARE.
|
THE SOFTWARE.
|
||||||
..
|
..
|
||||||
.TH fzf 1 "Apr 2026" "fzf 0.71.0" "fzf - a command-line fuzzy finder"
|
.TH fzf 1 "Mar 2026" "fzf 0.70.0" "fzf - a command-line fuzzy finder"
|
||||||
|
|
||||||
.SH NAME
|
.SH NAME
|
||||||
fzf - a command-line fuzzy finder
|
fzf - a command-line fuzzy finder
|
||||||
@@ -384,7 +384,7 @@ Use black background
|
|||||||
|
|
||||||
.SS DISPLAY MODE
|
.SS DISPLAY MODE
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-height=" "[~][\-]HEIGHT[%]"
|
.BI "\-\-height=" "[~]HEIGHT[%]"
|
||||||
Display fzf window below the cursor with the given height instead of using
|
Display fzf window below the cursor with the given height instead of using
|
||||||
the full screen.
|
the full screen.
|
||||||
|
|
||||||
@@ -394,19 +394,17 @@ height minus the given value.
|
|||||||
fzf \-\-height=\-1
|
fzf \-\-height=\-1
|
||||||
|
|
||||||
When prefixed with \fB~\fR, fzf will automatically determine the height in the
|
When prefixed with \fB~\fR, fzf will automatically determine the height in the
|
||||||
range according to the input size. You can combine \fB~\fR with a negative
|
range according to the input size.
|
||||||
value.
|
|
||||||
|
|
||||||
# Will not take up 100% of the screen
|
# Will not take up 100% of the screen
|
||||||
seq 5 | fzf \-\-height=~100%
|
seq 5 | fzf \-\-height=~100%
|
||||||
|
|
||||||
# Adapt to input size, up to terminal height minus 1
|
|
||||||
seq 5 | fzf \-\-height=~\-1
|
|
||||||
|
|
||||||
Adaptive height has the following limitations:
|
Adaptive height has the following limitations:
|
||||||
.br
|
.br
|
||||||
* Cannot be used with top/bottom margin and padding given in percent size
|
* Cannot be used with top/bottom margin and padding given in percent size
|
||||||
.br
|
.br
|
||||||
|
* Negative value is not allowed
|
||||||
|
.br
|
||||||
* It will not find the right size when there are multi-line items
|
* It will not find the right size when there are multi-line items
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
@@ -1100,17 +1098,7 @@ Print header before the prompt line. When both normal header and header lines
|
|||||||
.TP
|
.TP
|
||||||
.BI "\-\-header\-border" [=STYLE]
|
.BI "\-\-header\-border" [=STYLE]
|
||||||
Draw border around the header section. \fBline\fR style draws a single
|
Draw border around the header section. \fBline\fR style draws a single
|
||||||
separator line between the header window and the list section. \fBinline\fR
|
separator line between the header window and the list section.
|
||||||
style embeds the header inside the list border frame, joined to the list
|
|
||||||
section by a horizontal separator; it requires a \fB\-\-list\-border\fR
|
|
||||||
shape that has both top and bottom segments (rounded / sharp / bold /
|
|
||||||
double / block / thinblock / horizontal) and falls back to \fBline\fR
|
|
||||||
otherwise. When the list border also has side segments, the separator
|
|
||||||
joins them with T-junctions; \fBhorizontal\fR has no side borders, so the
|
|
||||||
separator is drawn without T-junction endpoints. Takes precedence over
|
|
||||||
\fB\-\-header\-first\fR (the section stays inside the list frame), and
|
|
||||||
when \fB\-\-header\-lines\fR is also set \fB\-\-header\-lines\-border\fR
|
|
||||||
must also be \fBinline\fR.
|
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-header\-label" [=LABEL]
|
.BI "\-\-header\-label" [=LABEL]
|
||||||
@@ -1126,10 +1114,6 @@ Display header from \fB--header\-lines\fR with a separate border. Pass
|
|||||||
\fBnone\fR to still separate the header lines but without a border. To combine
|
\fBnone\fR to still separate the header lines but without a border. To combine
|
||||||
two headers, use \fB\-\-no\-header\-lines\-border\fR. \fBline\fR style draws
|
two headers, use \fB\-\-no\-header\-lines\-border\fR. \fBline\fR style draws
|
||||||
a single separator line between the header lines and the list section.
|
a single separator line between the header lines and the list section.
|
||||||
\fBinline\fR style embeds the header lines inside the list border frame
|
|
||||||
with a horizontal separator; it requires a \fB\-\-list\-border\fR shape
|
|
||||||
that has both top and bottom segments, falls back to \fBline\fR
|
|
||||||
otherwise.
|
|
||||||
|
|
||||||
.SS FOOTER
|
.SS FOOTER
|
||||||
|
|
||||||
@@ -1143,10 +1127,7 @@ are not affected by \fB\-\-with\-nth\fR. ANSI color codes are processed even whe
|
|||||||
.TP
|
.TP
|
||||||
.BI "\-\-footer\-border" [=STYLE]
|
.BI "\-\-footer\-border" [=STYLE]
|
||||||
Draw border around the footer section. \fBline\fR style draws a single
|
Draw border around the footer section. \fBline\fR style draws a single
|
||||||
separator line between the footer and the list section. \fBinline\fR style
|
separator line between the footer and the list section.
|
||||||
embeds the footer inside the list border frame with a horizontal separator;
|
|
||||||
it requires a \fB\-\-list\-border\fR shape that has both top and bottom
|
|
||||||
segments and falls back to \fBline\fR otherwise.
|
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-footer\-label" [=LABEL]
|
.BI "\-\-footer\-label" [=LABEL]
|
||||||
|
|||||||
@@ -896,7 +896,6 @@ function! s:execute_term(dict, command, temps) abort
|
|||||||
endif
|
endif
|
||||||
endfunction
|
endfunction
|
||||||
function! fzf.on_exit(id, code, ...)
|
function! fzf.on_exit(id, code, ...)
|
||||||
silent! autocmd! fzf_popup_resize
|
|
||||||
if s:getpos() == self.ppos " {'window': 'enew'}
|
if s:getpos() == self.ppos " {'window': 'enew'}
|
||||||
for [opt, val] in items(self.winopts)
|
for [opt, val] in items(self.winopts)
|
||||||
execute 'let' opt '=' val
|
execute 'let' opt '=' val
|
||||||
@@ -1024,17 +1023,15 @@ function! s:callback(dict, lines) abort
|
|||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
if has('nvim')
|
if has('nvim')
|
||||||
function! s:create_popup() abort
|
function s:create_popup(opts) abort
|
||||||
let opts = s:popup_bounds()
|
|
||||||
let opts = extend({'relative': 'editor', 'style': 'minimal'}, opts)
|
|
||||||
|
|
||||||
let buf = nvim_create_buf(v:false, v:true)
|
let buf = nvim_create_buf(v:false, v:true)
|
||||||
let s:popup_id = nvim_open_win(buf, v:true, opts)
|
let opts = extend({'relative': 'editor', 'style': 'minimal'}, a:opts)
|
||||||
call setwinvar(s:popup_id, '&colorcolumn', '')
|
let win = nvim_open_win(buf, v:true, opts)
|
||||||
|
call setwinvar(win, '&colorcolumn', '')
|
||||||
|
|
||||||
" Colors
|
" Colors
|
||||||
try
|
try
|
||||||
call setwinvar(s:popup_id, '&winhighlight', 'Pmenu:,Normal:Normal')
|
call setwinvar(win, '&winhighlight', 'Pmenu:,Normal:Normal')
|
||||||
let rules = get(g:, 'fzf_colors', {})
|
let rules = get(g:, 'fzf_colors', {})
|
||||||
if has_key(rules, 'bg')
|
if has_key(rules, 'bg')
|
||||||
let color = call('s:get_color', rules.bg)
|
let color = call('s:get_color', rules.bg)
|
||||||
@@ -1042,61 +1039,40 @@ if has('nvim')
|
|||||||
let ns = nvim_create_namespace('fzf_popup')
|
let ns = nvim_create_namespace('fzf_popup')
|
||||||
let hl = nvim_set_hl(ns, 'Normal',
|
let hl = nvim_set_hl(ns, 'Normal',
|
||||||
\ &termguicolors ? { 'bg': color } : { 'ctermbg': str2nr(color) })
|
\ &termguicolors ? { 'bg': color } : { 'ctermbg': str2nr(color) })
|
||||||
call nvim_win_set_hl_ns(s:popup_id, ns)
|
call nvim_win_set_hl_ns(win, ns)
|
||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
catch
|
catch
|
||||||
endtry
|
endtry
|
||||||
return buf
|
return buf
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
function! s:resize_popup() abort
|
|
||||||
if !exists('s:popup_id') || !nvim_win_is_valid(s:popup_id)
|
|
||||||
return
|
|
||||||
endif
|
|
||||||
let opts = s:popup_bounds()
|
|
||||||
let opts = extend({'relative': 'editor'}, opts)
|
|
||||||
call nvim_win_set_config(s:popup_id, opts)
|
|
||||||
endfunction
|
|
||||||
else
|
else
|
||||||
function! s:create_popup() abort
|
function! s:create_popup(opts) abort
|
||||||
function! s:popup_create(buf)
|
let s:popup_create = {buf -> popup_create(buf, #{
|
||||||
let s:popup_id = popup_create(a:buf, #{zindex: 1000})
|
\ line: a:opts.row,
|
||||||
call s:resize_popup()
|
\ col: a:opts.col,
|
||||||
endfunction
|
\ minwidth: a:opts.width,
|
||||||
|
\ maxwidth: a:opts.width,
|
||||||
|
\ minheight: a:opts.height,
|
||||||
|
\ maxheight: a:opts.height,
|
||||||
|
\ zindex: 1000,
|
||||||
|
\ })}
|
||||||
autocmd TerminalOpen * ++once call s:popup_create(str2nr(expand('<abuf>')))
|
autocmd TerminalOpen * ++once call s:popup_create(str2nr(expand('<abuf>')))
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
function! s:resize_popup() abort
|
|
||||||
if !exists('s:popup_id') || empty(popup_getpos(s:popup_id))
|
|
||||||
return
|
|
||||||
endif
|
|
||||||
let opts = s:popup_bounds()
|
|
||||||
call popup_move(s:popup_id, {
|
|
||||||
\ 'line': opts.row,
|
|
||||||
\ 'col': opts.col,
|
|
||||||
\ 'minwidth': opts.width,
|
|
||||||
\ 'maxwidth': opts.width,
|
|
||||||
\ 'minheight': opts.height,
|
|
||||||
\ 'maxheight': opts.height,
|
|
||||||
\ })
|
|
||||||
endfunction
|
|
||||||
endif
|
endif
|
||||||
|
|
||||||
function! s:popup_bounds() abort
|
function! s:popup(opts) abort
|
||||||
let opts = s:popup_opts
|
let xoffset = get(a:opts, 'xoffset', 0.5)
|
||||||
|
let yoffset = get(a:opts, 'yoffset', 0.5)
|
||||||
let xoffset = get(opts, 'xoffset', 0.5)
|
let relative = get(a:opts, 'relative', 0)
|
||||||
let yoffset = get(opts, 'yoffset', 0.5)
|
|
||||||
let relative = get(opts, 'relative', 0)
|
|
||||||
|
|
||||||
" Use current window size for positioning relatively positioned popups
|
" Use current window size for positioning relatively positioned popups
|
||||||
let columns = relative ? winwidth(0) : &columns
|
let columns = relative ? winwidth(0) : &columns
|
||||||
let lines = relative ? winheight(0) : (&lines - has('nvim'))
|
let lines = relative ? winheight(0) : (&lines - has('nvim'))
|
||||||
|
|
||||||
" Size and position
|
" Size and position
|
||||||
let width = min([max([8, opts.width > 1 ? opts.width : float2nr(columns * opts.width)]), columns])
|
let width = min([max([8, a:opts.width > 1 ? a:opts.width : float2nr(columns * a:opts.width)]), columns])
|
||||||
let height = min([max([4, opts.height > 1 ? opts.height : float2nr(lines * opts.height)]), lines])
|
let height = min([max([4, a:opts.height > 1 ? a:opts.height : float2nr(lines * a:opts.height)]), lines])
|
||||||
let row = float2nr(yoffset * (lines - height)) + (relative ? win_screenpos(0)[0] - 1 : 0)
|
let row = float2nr(yoffset * (lines - height)) + (relative ? win_screenpos(0)[0] - 1 : 0)
|
||||||
let col = float2nr(xoffset * (columns - width)) + (relative ? win_screenpos(0)[1] - 1 : 0)
|
let col = float2nr(xoffset * (columns - width)) + (relative ? win_screenpos(0)[1] - 1 : 0)
|
||||||
|
|
||||||
@@ -1106,17 +1082,9 @@ function! s:popup_bounds() abort
|
|||||||
let row += !has('nvim')
|
let row += !has('nvim')
|
||||||
let col += !has('nvim')
|
let col += !has('nvim')
|
||||||
|
|
||||||
return { 'row': row, 'col': col, 'width': width, 'height': height }
|
call s:create_popup({
|
||||||
endfunction
|
\ 'row': row, 'col': col, 'width': width, 'height': height
|
||||||
|
\ })
|
||||||
function! s:popup(opts) abort
|
|
||||||
let s:popup_opts = a:opts
|
|
||||||
call s:create_popup()
|
|
||||||
|
|
||||||
augroup fzf_popup_resize
|
|
||||||
autocmd!
|
|
||||||
autocmd VimResized * call s:resize_popup()
|
|
||||||
augroup END
|
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
let s:default_action = {
|
let s:default_action = {
|
||||||
|
|||||||
141
shell/common.fish
Normal file
141
shell/common.fish
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
function __fzf_defaults
|
||||||
|
# $argv[1]: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||||
|
# $argv[2..]: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||||
|
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
|
||||||
|
string join ' ' -- \
|
||||||
|
"--height $FZF_TMUX_HEIGHT --min-height=20+ --bind=ctrl-z:ignore" $argv[1] \
|
||||||
|
(test -r "$FZF_DEFAULT_OPTS_FILE"; and string join -- ' ' <$FZF_DEFAULT_OPTS_FILE) \
|
||||||
|
$FZF_DEFAULT_OPTS $argv[2..-1]
|
||||||
|
end
|
||||||
|
|
||||||
|
function __fzfcmd
|
||||||
|
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
|
||||||
|
if test -n "$FZF_TMUX_OPTS"
|
||||||
|
echo "fzf-tmux $FZF_TMUX_OPTS -- "
|
||||||
|
else if test "$FZF_TMUX" = "1"
|
||||||
|
echo "fzf-tmux -d$FZF_TMUX_HEIGHT -- "
|
||||||
|
else
|
||||||
|
echo "fzf"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function __fzf_cmd_tokens -d 'Return command line tokens, skipping leading env assignments and command prefixes'
|
||||||
|
# Get tokens - use version-appropriate flags
|
||||||
|
set -l tokens
|
||||||
|
if test (string match -r -- '^\d+' $version) -ge 4
|
||||||
|
set -- tokens (commandline -xpc)
|
||||||
|
else
|
||||||
|
set -- tokens (commandline -opc)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Filter out leading environment variable assignments
|
||||||
|
set -l -- var_count 0
|
||||||
|
for i in $tokens
|
||||||
|
if string match -qr -- '^[\w]+=' $i
|
||||||
|
set var_count (math $var_count + 1)
|
||||||
|
else
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
set -e -- tokens[0..$var_count]
|
||||||
|
|
||||||
|
# Skip command prefixes so callers see the actual command name,
|
||||||
|
# e.g. "builtin cd" → "cd", "env VAR=1 command cd" → "cd"
|
||||||
|
while true
|
||||||
|
switch "$tokens[1]"
|
||||||
|
case builtin command
|
||||||
|
set -e -- tokens[1]
|
||||||
|
test "$tokens[1]" = "--"; and set -e -- tokens[1]
|
||||||
|
case env
|
||||||
|
set -e -- tokens[1]
|
||||||
|
test "$tokens[1]" = "--"; and set -e -- tokens[1]
|
||||||
|
while string match -qr -- '^[\w]+=' "$tokens[1]"
|
||||||
|
set -e -- tokens[1]
|
||||||
|
end
|
||||||
|
case '*'
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
string escape -n -- $tokens
|
||||||
|
end
|
||||||
|
|
||||||
|
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
|
||||||
|
set -l fzf_query ''
|
||||||
|
set -l prefix ''
|
||||||
|
set -l dir '.'
|
||||||
|
|
||||||
|
# Set variables containing the major and minor fish version numbers, using
|
||||||
|
# a method compatible with all supported fish versions.
|
||||||
|
set -l -- fish_major (string match -r -- '^\d+' $version)
|
||||||
|
set -l -- fish_minor (string match -r -- '^\d+\.(\d+)' $version)[2]
|
||||||
|
|
||||||
|
# fish v3.3.0 and newer: Don't use option prefix if " -- " is preceded.
|
||||||
|
set -l -- match_regex '(?<fzf_query>[\s\S]*?(?=\n?$)$)'
|
||||||
|
set -l -- prefix_regex '^-[^\s=]+=|^-(?!-)\S'
|
||||||
|
if test "$fish_major" -eq 3 -a "$fish_minor" -lt 3
|
||||||
|
or string match -q -v -- '* -- *' (string sub -l (commandline -Cp) -- (commandline -p))
|
||||||
|
set -- match_regex "(?<prefix>$prefix_regex)?$match_regex"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Set $prefix and expanded $fzf_query with preserved trailing newlines.
|
||||||
|
if test "$fish_major" -ge 4
|
||||||
|
# fish v4.0.0 and newer
|
||||||
|
string match -q -r -- $match_regex (commandline --current-token --tokens-expanded | string collect -N)
|
||||||
|
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||||
|
# fish v3.2.0 - v3.7.1 (last v3)
|
||||||
|
string match -q -r -- $match_regex (commandline --current-token --tokenize | string collect -N)
|
||||||
|
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)' '')
|
||||||
|
else
|
||||||
|
# fish older than v3.2.0 (v3.1b1 - v3.1.2)
|
||||||
|
set -l -- cl_token (commandline --current-token --tokenize | string collect -N)
|
||||||
|
set -- prefix (string match -r -- $prefix_regex $cl_token)
|
||||||
|
set -- fzf_query (string replace -- "$prefix" '' $cl_token | string collect -N)
|
||||||
|
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)|\\\n\\\n$' '')
|
||||||
|
end
|
||||||
|
|
||||||
|
if test -n "$fzf_query"
|
||||||
|
# Normalize path in $fzf_query, set $dir to the longest existing directory.
|
||||||
|
if test \( "$fish_major" -ge 4 \) -o \( "$fish_major" -eq 3 -a "$fish_minor" -ge 5 \)
|
||||||
|
# fish v3.5.0 and newer
|
||||||
|
set -- fzf_query (path normalize -- $fzf_query)
|
||||||
|
set -- dir $fzf_query
|
||||||
|
while not path is -d $dir
|
||||||
|
set -- dir (path dirname $dir)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# fish older than v3.5.0 (v3.1b1 - v3.4.1)
|
||||||
|
if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||||
|
# fish v3.2.0 - v3.4.1
|
||||||
|
string match -q -r -- '(?<fzf_query>^[\s\S]*?(?=\n?$)$)' \
|
||||||
|
(string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
|
||||||
|
else
|
||||||
|
# fish v3.1b1 - v3.1.2
|
||||||
|
set -- fzf_query (string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
|
||||||
|
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r '\\\n$' '')
|
||||||
|
end
|
||||||
|
set -- dir $fzf_query
|
||||||
|
while not test -d "$dir"
|
||||||
|
set -- dir (dirname -z -- "$dir" | string split0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not string match -q -- '.' $dir; or string match -q -r -- '^\./|^\.$' $fzf_query
|
||||||
|
# Strip $dir from $fzf_query - preserve trailing newlines.
|
||||||
|
if test "$fish_major" -ge 4
|
||||||
|
# fish v4.0.0 and newer
|
||||||
|
string match -q -r -- '^'(string escape --style=regex -- $dir)'/?(?<fzf_query>[\s\S]*)' $fzf_query
|
||||||
|
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||||
|
# fish v3.2.0 - v3.7.1 (last v3)
|
||||||
|
string match -q -r -- '^/?(?<fzf_query>[\s\S]*?(?=\n?$)$)' \
|
||||||
|
(string replace -- "$dir" '' $fzf_query | string collect -N)
|
||||||
|
else
|
||||||
|
# fish older than v3.2.0 (v3.1b1 - v3.1.2)
|
||||||
|
set -- fzf_query (string replace -- "$dir" '' $fzf_query | string collect -N)
|
||||||
|
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^/?|\\\n$' '')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
string escape -n -- "$dir" "$fzf_query" "$prefix"
|
||||||
|
end
|
||||||
@@ -4,6 +4,8 @@
|
|||||||
# / __/ / /_/ __/
|
# / __/ / /_/ __/
|
||||||
# /_/ /___/_/ completion.bash
|
# /_/ /___/_/ completion.bash
|
||||||
#
|
#
|
||||||
|
# - $FZF_TMUX (default: 0)
|
||||||
|
# - $FZF_TMUX_OPTS (default: empty)
|
||||||
# - $FZF_COMPLETION_TRIGGER (default: '**')
|
# - $FZF_COMPLETION_TRIGGER (default: '**')
|
||||||
# - $FZF_COMPLETION_OPTS (default: empty)
|
# - $FZF_COMPLETION_OPTS (default: empty)
|
||||||
# - $FZF_COMPLETION_PATH_OPTS (default: empty)
|
# - $FZF_COMPLETION_PATH_OPTS (default: empty)
|
||||||
|
|||||||
@@ -4,166 +4,238 @@
|
|||||||
# / __/ / /_/ __/
|
# / __/ / /_/ __/
|
||||||
# /_/ /___/_/ completion.fish
|
# /_/ /___/_/ completion.fish
|
||||||
#
|
#
|
||||||
# - $FZF_COMPLETION_OPTS
|
# - $FZF_COMPLETION_OPTS (default: empty)
|
||||||
# - $FZF_EXPANSION_OPTS
|
|
||||||
|
|
||||||
# The oldest supported fish version is 3.4.0. For this message being able to be
|
function fzf_completion_setup
|
||||||
# displayed on older versions, the command substitution syntax $() should not
|
|
||||||
# be used anywhere in the script, otherwise the source command will fail.
|
|
||||||
if string match -qr -- '^[12]\\.|^3\\.[0-3]' $version
|
|
||||||
echo "fzf completion script requires fish version 3.4.0 or newer." >&2
|
|
||||||
return 1
|
|
||||||
else if not command -q fzf
|
|
||||||
echo "fzf was not found in path." >&2
|
|
||||||
return 1
|
|
||||||
end
|
|
||||||
|
|
||||||
function fzf_complete -w fzf -d 'fzf command completion and wildcard expansion search'
|
#----BEGIN INCLUDE common.fish
|
||||||
# Restore the default shift-tab behavior on tab completions
|
# NOTE: Do not directly edit this section, which is copied from "common.fish".
|
||||||
if commandline --paging-mode
|
# To modify it, one can edit "common.fish" and run "./update.sh" to apply
|
||||||
commandline -f complete-and-search
|
# the changes. See code comments in "common.fish" for the implementation details.
|
||||||
return
|
|
||||||
|
function __fzf_defaults
|
||||||
|
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
|
||||||
|
string join ' ' -- \
|
||||||
|
"--height $FZF_TMUX_HEIGHT --min-height=20+ --bind=ctrl-z:ignore" $argv[1] \
|
||||||
|
(test -r "$FZF_DEFAULT_OPTS_FILE"; and string join -- ' ' <$FZF_DEFAULT_OPTS_FILE) \
|
||||||
|
$FZF_DEFAULT_OPTS $argv[2..-1]
|
||||||
end
|
end
|
||||||
|
|
||||||
# Remove any trailing unescaped backslash from token and update command line
|
function __fzfcmd
|
||||||
set -l -- token (string replace -r -- '(?<!\\\\)(?:\\\\\\\\)*\\K\\\\$' '' (commandline -t | string collect) | string collect)
|
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
|
||||||
commandline -rt -- $token
|
if test -n "$FZF_TMUX_OPTS"
|
||||||
|
echo "fzf-tmux $FZF_TMUX_OPTS -- "
|
||||||
# Remove any line breaks from token
|
else if test "$FZF_TMUX" = "1"
|
||||||
set -- token (string replace -ra -- '\\\\\\n' '' $token | string collect)
|
echo "fzf-tmux -d$FZF_TMUX_HEIGHT -- "
|
||||||
|
|
||||||
# regex: Match token with unescaped/unquoted glob character
|
|
||||||
set -l -- r_glob '^(?:[^\'"\\\\*]|\\\\[\\S\\s]|\'(?:\\\\[\\S\\s]|[^\'\\\\])*\'|"(?:\\\\[\\S\\s]|[^"\\\\])*")*\\*[\\S\\s]*$'
|
|
||||||
|
|
||||||
# regex: Match any unbalanced quote character
|
|
||||||
set -l -- r_quote '^(?>(?:\\\\[\\s\\S]|"(?:[^"\\\\]|\\\\[\\s\\S])*"|\'(?:[^\'\\\\]|\\\\[\\s\\S])*\'|[^\'"\\\\]+)*)\\K[\'"]'
|
|
||||||
|
|
||||||
# The expansion pattern is the token with any open quote closed, or is empty.
|
|
||||||
set -l -- glob_pattern (string match -r -- $r_glob $token | string collect)(string match -r -- $r_quote $token | string collect -a)
|
|
||||||
|
|
||||||
set -l -- cl_tokenize_opt '--tokens-expanded'
|
|
||||||
string match -q -- '3.*' $version
|
|
||||||
and set -- cl_tokenize_opt '--tokenize'
|
|
||||||
|
|
||||||
# Set command line tokens without any leading variable definitions or launcher
|
|
||||||
# commands (including their options, but not any option arguments).
|
|
||||||
set -l -- r_cmd '^(?:(?:builtin|command|doas|env|sudo|\\w+=\\S*|-\\S+)\\s+)*\\K[\\s\\S]+'
|
|
||||||
set -l -- cmd (commandline $cl_tokenize_opt --input=(commandline -pc | string match -r $r_cmd))
|
|
||||||
test -z "$token"
|
|
||||||
and set -a -- cmd ''
|
|
||||||
|
|
||||||
# Set fzf options
|
|
||||||
test -z "$FZF_TMUX_HEIGHT"
|
|
||||||
and set -l -- FZF_TMUX_HEIGHT 40%
|
|
||||||
|
|
||||||
set -lax -- FZF_DEFAULT_OPTS \
|
|
||||||
"--height=$FZF_TMUX_HEIGHT --min-height=20+ --bind=ctrl-z:ignore" \
|
|
||||||
(test -r "$FZF_DEFAULT_OPTS_FILE"; and string join -- ' ' <$FZF_DEFAULT_OPTS_FILE) \
|
|
||||||
$FZF_DEFAULT_OPTS '--bind=alt-r:toggle-raw --multi --wrap=word --reverse' \
|
|
||||||
(if test -n "$glob_pattern"; string collect -- $FZF_EXPANSION_OPTS; else;
|
|
||||||
string collect -- $FZF_COMPLETION_OPTS; end; string escape -n -- $argv) \
|
|
||||||
--with-shell=(status fish-path)\\ -c
|
|
||||||
|
|
||||||
set -lx FZF_DEFAULT_OPTS_FILE
|
|
||||||
|
|
||||||
set -l -- fzf_cmd fzf
|
|
||||||
test "$FZF_TMUX" = 1
|
|
||||||
and set -- fzf_cmd fzf-tmux $FZF_TMUX_OPTS -d$FZF_TMUX_HEIGHT --
|
|
||||||
|
|
||||||
set -l result
|
|
||||||
|
|
||||||
# Get the completion list from stdin when it's not a tty
|
|
||||||
if not isatty stdin
|
|
||||||
set -l -- custom_post_func _fzf_post_complete_$cmd[1]
|
|
||||||
functions -q $custom_post_func
|
|
||||||
or set -- custom_post_func _fzf_complete_$cmd[1]_post
|
|
||||||
|
|
||||||
if functions -q $custom_post_func
|
|
||||||
$fzf_cmd | $custom_post_func $cmd | while read -l r; set -a -- result $r; end
|
|
||||||
else if string match -q -- '*--print0*' "$FZF_DEFAULT_OPTS"
|
|
||||||
$fzf_cmd | while read -lz r; set -a -- result $r; end
|
|
||||||
else
|
else
|
||||||
$fzf_cmd | while read -l r; set -a -- result $r; end
|
echo "fzf"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function __fzf_cmd_tokens -d 'Return command line tokens, skipping leading env assignments and command prefixes'
|
||||||
|
set -l tokens
|
||||||
|
if test (string match -r -- '^\d+' $version) -ge 4
|
||||||
|
set -- tokens (commandline -xpc)
|
||||||
|
else
|
||||||
|
set -- tokens (commandline -opc)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Wildcard expansion
|
set -l -- var_count 0
|
||||||
else if test -n "$glob_pattern"
|
for i in $tokens
|
||||||
# Set the command to be run by fzf, so there is a visual indicator and an
|
if string match -qr -- '^[\w]+=' $i
|
||||||
# easy way to abort on long recursive searches.
|
set var_count (math $var_count + 1)
|
||||||
set -lx -- FZF_DEFAULT_COMMAND "for i in $glob_pattern;" \
|
|
||||||
'test -d "$i"; and string match -qv -- "*/" $i; and set -- i $i/;' \
|
|
||||||
'string join0 -- $i; end'
|
|
||||||
|
|
||||||
set -- result (string escape -n -- ($fzf_cmd --read0 --print0 --scheme=path --no-multi-line | string split0))
|
|
||||||
|
|
||||||
# Command completion
|
|
||||||
else
|
|
||||||
# Call custom function if defined
|
|
||||||
set -l -- custom_func _fzf_complete_$cmd[1]
|
|
||||||
if functions -q $custom_func; and not set -q __fzf_no_custom_complete
|
|
||||||
set -lx __fzf_no_custom_complete
|
|
||||||
$custom_func $cmd
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
# Workaround for complete not having newlines in results
|
|
||||||
if string match -qr -- '\\n' $token
|
|
||||||
set -- token (string replace -ra -- '(?<!\\\\)(?:\\\\\\\\)*\\K\\\\\$' '\\\\\\\\\$' $token | string collect)
|
|
||||||
set -- token (string unescape -- $token | string collect)
|
|
||||||
set -- token (string replace -ra -- '\\n' '\\\\n' $token | string collect)
|
|
||||||
end
|
|
||||||
|
|
||||||
set -- list (complete -C --escape -- (string join -- ' ' (commandline -pc $cl_tokenize_opt) $token | string collect))
|
|
||||||
if test -n "$list"
|
|
||||||
# Get the initial tabstop value
|
|
||||||
if set -l -- tabstop (string match -rga -- '--tabstop[= ](?:0*)([1-9]\\d+|[4-9])' "$FZF_DEFAULT_OPTS")[-1]
|
|
||||||
set -- tabstop (math $tabstop - 4)
|
|
||||||
else
|
else
|
||||||
set -- tabstop 4
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
set -e -- tokens[0..$var_count]
|
||||||
|
|
||||||
|
while true
|
||||||
|
switch "$tokens[1]"
|
||||||
|
case builtin command
|
||||||
|
set -e -- tokens[1]
|
||||||
|
test "$tokens[1]" = "--"; and set -e -- tokens[1]
|
||||||
|
case env
|
||||||
|
set -e -- tokens[1]
|
||||||
|
test "$tokens[1]" = "--"; and set -e -- tokens[1]
|
||||||
|
while string match -qr -- '^[\w]+=' "$tokens[1]"
|
||||||
|
set -e -- tokens[1]
|
||||||
|
end
|
||||||
|
case '*'
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
string escape -n -- $tokens
|
||||||
|
end
|
||||||
|
|
||||||
|
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
|
||||||
|
set -l fzf_query ''
|
||||||
|
set -l prefix ''
|
||||||
|
set -l dir '.'
|
||||||
|
|
||||||
|
set -l -- fish_major (string match -r -- '^\d+' $version)
|
||||||
|
set -l -- fish_minor (string match -r -- '^\d+\.(\d+)' $version)[2]
|
||||||
|
|
||||||
|
set -l -- match_regex '(?<fzf_query>[\s\S]*?(?=\n?$)$)'
|
||||||
|
set -l -- prefix_regex '^-[^\s=]+=|^-(?!-)\S'
|
||||||
|
if test "$fish_major" -eq 3 -a "$fish_minor" -lt 3
|
||||||
|
or string match -q -v -- '* -- *' (string sub -l (commandline -Cp) -- (commandline -p))
|
||||||
|
set -- match_regex "(?<prefix>$prefix_regex)?$match_regex"
|
||||||
|
end
|
||||||
|
|
||||||
|
if test "$fish_major" -ge 4
|
||||||
|
string match -q -r -- $match_regex (commandline --current-token --tokens-expanded | string collect -N)
|
||||||
|
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||||
|
string match -q -r -- $match_regex (commandline --current-token --tokenize | string collect -N)
|
||||||
|
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)' '')
|
||||||
|
else
|
||||||
|
set -l -- cl_token (commandline --current-token --tokenize | string collect -N)
|
||||||
|
set -- prefix (string match -r -- $prefix_regex $cl_token)
|
||||||
|
set -- fzf_query (string replace -- "$prefix" '' $cl_token | string collect -N)
|
||||||
|
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)|\\\n\\\n$' '')
|
||||||
|
end
|
||||||
|
|
||||||
|
if test -n "$fzf_query"
|
||||||
|
if test \( "$fish_major" -ge 4 \) -o \( "$fish_major" -eq 3 -a "$fish_minor" -ge 5 \)
|
||||||
|
set -- fzf_query (path normalize -- $fzf_query)
|
||||||
|
set -- dir $fzf_query
|
||||||
|
while not path is -d $dir
|
||||||
|
set -- dir (path dirname $dir)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||||
|
string match -q -r -- '(?<fzf_query>^[\s\S]*?(?=\n?$)$)' \
|
||||||
|
(string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
|
||||||
|
else
|
||||||
|
set -- fzf_query (string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
|
||||||
|
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r '\\\n$' '')
|
||||||
|
end
|
||||||
|
set -- dir $fzf_query
|
||||||
|
while not test -d "$dir"
|
||||||
|
set -- dir (dirname -z -- "$dir" | string split0)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Determine the tabstop length for description alignment
|
if not string match -q -- '.' $dir; or string match -q -r -- '^\./|^\.$' $fzf_query
|
||||||
set -l -- max_columns (math $COLUMNS - 40)
|
if test "$fish_major" -ge 4
|
||||||
for i in $list[1..500]
|
string match -q -r -- '^'(string escape --style=regex -- $dir)'/?(?<fzf_query>[\s\S]*)' $fzf_query
|
||||||
set -l -- item (string split -f 1 -- \t $i)
|
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||||
and set -l -- len (string length -V -- $item)
|
string match -q -r -- '^/?(?<fzf_query>[\s\S]*?(?=\n?$)$)' \
|
||||||
and test "$len" -gt "$tabstop" -a "$len" -lt "$max_columns"
|
(string replace -- "$dir" '' $fzf_query | string collect -N)
|
||||||
and set -- tabstop $len
|
else
|
||||||
|
set -- fzf_query (string replace -- "$dir" '' $fzf_query | string collect -N)
|
||||||
|
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^/?|\\\n$' '')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
set -- tabstop (math $tabstop + 4)
|
end
|
||||||
|
|
||||||
set -- result (string collect -- $list | $fzf_cmd --delimiter="\t" --tabstop=$tabstop --wrap-sign=\t"↳ " --accept-nth=1)
|
string escape -n -- "$dir" "$fzf_query" "$prefix"
|
||||||
|
end
|
||||||
|
#----END INCLUDE
|
||||||
|
|
||||||
|
# Use complete builtin for specific commands
|
||||||
|
function __fzf_complete_native
|
||||||
|
set -l -- token (commandline -t)
|
||||||
|
set -l -- completions (eval complete -C \"$argv[1]\")
|
||||||
|
test -n "$completions"; or begin commandline -f repaint; return; end
|
||||||
|
|
||||||
|
# Calculate tabstop based on longest completion item (sample first 500 for performance)
|
||||||
|
set -l -- tabstop 20
|
||||||
|
set -l -- sample_size (math "min(500, "(count $completions)")")
|
||||||
|
for c in $completions[1..$sample_size]
|
||||||
|
set -l -- len (string length -V -- (string split -- \t $c))
|
||||||
|
test -n "$len[2]" -a "$len[1]" -gt "$tabstop"
|
||||||
|
and set -- tabstop $len[1]
|
||||||
|
end
|
||||||
|
# limit to 120 to prevent long lines
|
||||||
|
set -- tabstop (math "min($tabstop + 4, 120)")
|
||||||
|
|
||||||
|
set -l result
|
||||||
|
set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults \
|
||||||
|
"--reverse --delimiter=\\t --nth=1 --tabstop=$tabstop --color=fg:dim,nth:regular" \
|
||||||
|
$FZF_COMPLETION_OPTS $argv[2..-1] --accept-nth=1 --read0 --print0)
|
||||||
|
set -- result (string join0 -- $completions | eval (__fzfcmd) | string split0)
|
||||||
|
and begin
|
||||||
|
set -l -- tail ' '
|
||||||
|
# Append / to bare ~username results (fish omits it unlike other shells)
|
||||||
|
set -- result (string replace -r -- '^(~\w+)\s?$' '$1/' $result)
|
||||||
|
# Don't add trailing space if single result is a directory
|
||||||
|
test (count $result) -eq 1
|
||||||
|
and string match -q -- '*/' "$result"; and set -- tail ''
|
||||||
|
|
||||||
|
set -l -- result (string escape -n -- $result)
|
||||||
|
|
||||||
|
string match -q -- '~*' "$token"
|
||||||
|
and set result (string replace -r -- '^\\\\~' '~' $result)
|
||||||
|
|
||||||
|
string match -q -- '$*' "$token"
|
||||||
|
and set result (string replace -r -- '^\\\\\$' '\$' $result)
|
||||||
|
|
||||||
|
commandline -rt -- (string join ' ' -- $result)$tail
|
||||||
|
end
|
||||||
|
commandline -f repaint
|
||||||
|
end
|
||||||
|
|
||||||
|
function _fzf_complete
|
||||||
|
set -l -- args (string escape -- $argv | string join ' ' | string split -- ' -- ')
|
||||||
|
set -l -- post_func (status function)_(string split -- ' ' $args[2])[1]_post
|
||||||
|
set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults --reverse $FZF_COMPLETION_OPTS $args[1])
|
||||||
|
set -lx FZF_DEFAULT_OPTS_FILE
|
||||||
|
set -lx FZF_DEFAULT_COMMAND
|
||||||
|
set -l -- fzf_query (commandline -t | string escape)
|
||||||
|
set -l result
|
||||||
|
eval (__fzfcmd) --query=$fzf_query | while read -l r; set -a -- result $r; end
|
||||||
|
and if functions -q $post_func
|
||||||
|
commandline -rt -- (string collect -- $result | eval $post_func $args[2] | string join ' ')' '
|
||||||
|
else
|
||||||
|
commandline -rt -- (string join -- ' ' (string escape -- $result))' '
|
||||||
|
end
|
||||||
|
commandline -f repaint
|
||||||
|
end
|
||||||
|
|
||||||
|
# Kill completion (process selection)
|
||||||
|
function _fzf_complete_kill
|
||||||
|
set -l -- fzf_query (commandline -t | string escape)
|
||||||
|
set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults --reverse $FZF_COMPLETION_OPTS \
|
||||||
|
--accept-nth=2 -m --header-lines=1 --no-preview --wrap)
|
||||||
|
set -lx FZF_DEFAULT_OPTS_FILE
|
||||||
|
if type -q ps
|
||||||
|
set -l -- ps_cmd 'begin command ps -eo user,pid,ppid,start,time,command 2>/dev/null;' \
|
||||||
|
'or command ps -eo user,pid,ppid,time,args 2>/dev/null;' \
|
||||||
|
'or command ps --everyone --full --windows 2>/dev/null; end'
|
||||||
|
set -l -- result (eval $ps_cmd \| (__fzfcmd) --query=$fzf_query)
|
||||||
|
and commandline -rt -- (string join ' ' -- $result)" "
|
||||||
|
else
|
||||||
|
__fzf_complete_native "kill " --multi --query=$fzf_query
|
||||||
|
end
|
||||||
|
commandline -f repaint
|
||||||
|
end
|
||||||
|
|
||||||
|
# Main completion function
|
||||||
|
function fzf-completion
|
||||||
|
set -l -- tokens (__fzf_cmd_tokens)
|
||||||
|
set -l -- current_token (commandline -t)
|
||||||
|
set -l -- cmd_name $tokens[1]
|
||||||
|
|
||||||
|
# Route to appropriate completion function
|
||||||
|
if test -n "$tokens"; and functions -q _fzf_complete_$cmd_name
|
||||||
|
_fzf_complete_$cmd_name $tokens
|
||||||
|
else
|
||||||
|
set -l -- fzf_opt --query=$current_token --multi
|
||||||
|
__fzf_complete_native "$tokens $current_token" $fzf_opt
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Update command line
|
# Bind Shift-Tab to fzf-completion (Tab retains native Fish behavior)
|
||||||
if test -n "$result"
|
if test (string match -r -- '^\d+' $version) -ge 4
|
||||||
# No extra space after single selection that ends with path separator
|
bind shift-tab fzf-completion
|
||||||
set -l -- tail ' '
|
bind -M insert shift-tab fzf-completion
|
||||||
test (count $result) -eq 1
|
else
|
||||||
and string match -q -- '*/' "$result"
|
bind -k btab fzf-completion
|
||||||
and set -- tail ''
|
bind -M insert -k btab fzf-completion
|
||||||
|
|
||||||
commandline -rt -- (string join -- ' ' $result)$tail
|
|
||||||
end
|
end
|
||||||
|
|
||||||
commandline -f repaint
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function _fzf_complete
|
# Run setup
|
||||||
set -l fzf_args
|
fzf_completion_setup
|
||||||
for i in $argv
|
|
||||||
string match -q -- '--' $i; and break
|
|
||||||
set -a -- fzf_args $i
|
|
||||||
end
|
|
||||||
fzf_complete $fzf_args
|
|
||||||
end
|
|
||||||
|
|
||||||
# Bind to shift-tab
|
|
||||||
if string match -qr -- '^\\d\\d+|^[4-9]' $version
|
|
||||||
bind shift-tab fzf_complete
|
|
||||||
bind -M insert shift-tab fzf_complete
|
|
||||||
else
|
|
||||||
bind -k btab fzf_complete
|
|
||||||
bind -M insert -k btab fzf_complete
|
|
||||||
end
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
# / __/ / /_/ __/
|
# / __/ / /_/ __/
|
||||||
# /_/ /___/_/ completion.zsh
|
# /_/ /___/_/ completion.zsh
|
||||||
#
|
#
|
||||||
|
# - $FZF_TMUX (default: 0)
|
||||||
|
# - $FZF_TMUX_OPTS (default: empty)
|
||||||
# - $FZF_COMPLETION_TRIGGER (default: '**')
|
# - $FZF_COMPLETION_TRIGGER (default: '**')
|
||||||
# - $FZF_COMPLETION_OPTS (default: empty)
|
# - $FZF_COMPLETION_OPTS (default: empty)
|
||||||
# - $FZF_COMPLETION_PATH_OPTS (default: empty)
|
# - $FZF_COMPLETION_PATH_OPTS (default: empty)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
# / __/ / /_/ __/
|
# / __/ / /_/ __/
|
||||||
# /_/ /___/_/ key-bindings.bash
|
# /_/ /___/_/ key-bindings.bash
|
||||||
#
|
#
|
||||||
|
# - $FZF_TMUX_OPTS
|
||||||
# - $FZF_CTRL_T_COMMAND
|
# - $FZF_CTRL_T_COMMAND
|
||||||
# - $FZF_CTRL_T_OPTS
|
# - $FZF_CTRL_T_OPTS
|
||||||
# - $FZF_CTRL_R_COMMAND
|
# - $FZF_CTRL_R_COMMAND
|
||||||
@@ -84,10 +85,6 @@ __fzf_history_delete() {
|
|||||||
for offset in "${offsets[@]}"; do
|
for offset in "${offsets[@]}"; do
|
||||||
builtin history -d "$offset"
|
builtin history -d "$offset"
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ ${#offsets[@]} -gt 0 ]] && shopt -q histappend; then
|
|
||||||
builtin history -w
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if command -v perl > /dev/null; then
|
if command -v perl > /dev/null; then
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
# / __/ / /_/ __/
|
# / __/ / /_/ __/
|
||||||
# /_/ /___/_/ key-bindings.fish
|
# /_/ /___/_/ key-bindings.fish
|
||||||
#
|
#
|
||||||
|
# - $FZF_TMUX_OPTS
|
||||||
# - $FZF_CTRL_T_COMMAND
|
# - $FZF_CTRL_T_COMMAND
|
||||||
# - $FZF_CTRL_T_OPTS
|
# - $FZF_CTRL_T_OPTS
|
||||||
# - $FZF_CTRL_R_COMMAND
|
# - $FZF_CTRL_R_COMMAND
|
||||||
@@ -11,29 +12,35 @@
|
|||||||
# - $FZF_ALT_C_COMMAND
|
# - $FZF_ALT_C_COMMAND
|
||||||
# - $FZF_ALT_C_OPTS
|
# - $FZF_ALT_C_OPTS
|
||||||
|
|
||||||
|
|
||||||
# Key bindings
|
# Key bindings
|
||||||
# ------------
|
# ------------
|
||||||
|
# The oldest supported fish version is 3.1b1. To maintain compatibility, the
|
||||||
|
# command substitution syntax $(cmd) should never be used, even behind a version
|
||||||
|
# check, otherwise the source command will fail on fish versions older than 3.4.0.
|
||||||
function fzf_key_bindings
|
function fzf_key_bindings
|
||||||
|
|
||||||
# The oldest supported fish version is 3.4.0. For this message being able to be
|
# Check fish version
|
||||||
# displayed on older versions, the command substitution syntax $() should not
|
if set -l -- fish_ver (string match -r '^(\d+)\.(\d+)' $version 2>/dev/null)
|
||||||
# be used anywhere in the script, otherwise the source command will fail.
|
and test "$fish_ver[2]" -lt 3 -o "$fish_ver[2]" -eq 3 -a "$fish_ver[3]" -lt 1
|
||||||
if string match -qr -- '^[12]\\.|^3\\.[0-3]' $version
|
echo "This script requires fish version 3.1b1 or newer." >&2
|
||||||
echo "fzf key bindings script requires fish version 3.4.0 or newer." >&2
|
|
||||||
return 1
|
return 1
|
||||||
else if not command -q fzf
|
else if not type -q fzf
|
||||||
echo "fzf was not found in path." >&2
|
echo "fzf was not found in path." >&2
|
||||||
return 1
|
return 1
|
||||||
end
|
end
|
||||||
|
|
||||||
|
#----BEGIN INCLUDE common.fish
|
||||||
|
# NOTE: Do not directly edit this section, which is copied from "common.fish".
|
||||||
|
# To modify it, one can edit "common.fish" and run "./update.sh" to apply
|
||||||
|
# the changes. See code comments in "common.fish" for the implementation details.
|
||||||
|
|
||||||
function __fzf_defaults
|
function __fzf_defaults
|
||||||
# $argv[1]: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
|
||||||
# $argv[2..]: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
|
||||||
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
|
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
|
||||||
string join ' ' -- \
|
string join ' ' -- \
|
||||||
"--height $FZF_TMUX_HEIGHT --min-height=20+ --bind=ctrl-z:ignore" $argv[1] \
|
"--height $FZF_TMUX_HEIGHT --min-height=20+ --bind=ctrl-z:ignore" $argv[1] \
|
||||||
(test -r "$FZF_DEFAULT_OPTS_FILE"; and string join -- ' ' <$FZF_DEFAULT_OPTS_FILE) \
|
(test -r "$FZF_DEFAULT_OPTS_FILE"; and string join -- ' ' <$FZF_DEFAULT_OPTS_FILE) \
|
||||||
$FZF_DEFAULT_OPTS $argv[2..]
|
$FZF_DEFAULT_OPTS $argv[2..-1]
|
||||||
end
|
end
|
||||||
|
|
||||||
function __fzfcmd
|
function __fzfcmd
|
||||||
@@ -47,59 +54,107 @@ function fzf_key_bindings
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function __fzf_cmd_tokens -d 'Return command line tokens, skipping leading env assignments and command prefixes'
|
||||||
|
set -l tokens
|
||||||
|
if test (string match -r -- '^\d+' $version) -ge 4
|
||||||
|
set -- tokens (commandline -xpc)
|
||||||
|
else
|
||||||
|
set -- tokens (commandline -opc)
|
||||||
|
end
|
||||||
|
|
||||||
|
set -l -- var_count 0
|
||||||
|
for i in $tokens
|
||||||
|
if string match -qr -- '^[\w]+=' $i
|
||||||
|
set var_count (math $var_count + 1)
|
||||||
|
else
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
set -e -- tokens[0..$var_count]
|
||||||
|
|
||||||
|
while true
|
||||||
|
switch "$tokens[1]"
|
||||||
|
case builtin command
|
||||||
|
set -e -- tokens[1]
|
||||||
|
test "$tokens[1]" = "--"; and set -e -- tokens[1]
|
||||||
|
case env
|
||||||
|
set -e -- tokens[1]
|
||||||
|
test "$tokens[1]" = "--"; and set -e -- tokens[1]
|
||||||
|
while string match -qr -- '^[\w]+=' "$tokens[1]"
|
||||||
|
set -e -- tokens[1]
|
||||||
|
end
|
||||||
|
case '*'
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
string escape -n -- $tokens
|
||||||
|
end
|
||||||
|
|
||||||
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
|
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
|
||||||
set -l fzf_query ''
|
set -l fzf_query ''
|
||||||
set -l prefix ''
|
set -l prefix ''
|
||||||
set -l dir '.'
|
set -l dir '.'
|
||||||
|
|
||||||
set -l -- match_regex '(?<fzf_query>[\\s\\S]*?(?=\\n?$)$)'
|
set -l -- fish_major (string match -r -- '^\d+' $version)
|
||||||
set -l -- prefix_regex '^-[^\\s=]+=|^-(?!-)\\S'
|
set -l -- fish_minor (string match -r -- '^\d+\.(\d+)' $version)[2]
|
||||||
|
|
||||||
# Don't use option prefix if " -- " is preceded.
|
set -l -- match_regex '(?<fzf_query>[\s\S]*?(?=\n?$)$)'
|
||||||
string match -qv -- '* -- *' (string sub -l (commandline -Cp) -- (commandline -p))
|
set -l -- prefix_regex '^-[^\s=]+=|^-(?!-)\S'
|
||||||
and set -- match_regex "(?<prefix>$prefix_regex)?$match_regex"
|
if test "$fish_major" -eq 3 -a "$fish_minor" -lt 3
|
||||||
|
or string match -q -v -- '* -- *' (string sub -l (commandline -Cp) -- (commandline -p))
|
||||||
|
set -- match_regex "(?<prefix>$prefix_regex)?$match_regex"
|
||||||
|
end
|
||||||
|
|
||||||
# Set $prefix and expanded $fzf_query with preserved trailing newlines.
|
if test "$fish_major" -ge 4
|
||||||
if string match -qr -- '^\\d\\d+|^[4-9]' $version
|
|
||||||
# fish v4.0.0 and newer
|
|
||||||
string match -q -r -- $match_regex (commandline --current-token --tokens-expanded | string collect -N)
|
string match -q -r -- $match_regex (commandline --current-token --tokens-expanded | string collect -N)
|
||||||
else
|
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||||
string match -q -r -- $match_regex (commandline --current-token --tokenize | string collect -N)
|
string match -q -r -- $match_regex (commandline --current-token --tokenize | string collect -N)
|
||||||
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\\(?=~)|\\\\(?=\\$\\w)' '')
|
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)' '')
|
||||||
|
else
|
||||||
|
set -l -- cl_token (commandline --current-token --tokenize | string collect -N)
|
||||||
|
set -- prefix (string match -r -- $prefix_regex $cl_token)
|
||||||
|
set -- fzf_query (string replace -- "$prefix" '' $cl_token | string collect -N)
|
||||||
|
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)|\\\n\\\n$' '')
|
||||||
end
|
end
|
||||||
|
|
||||||
if test -n "$fzf_query"
|
if test -n "$fzf_query"
|
||||||
# Normalize path in $fzf_query, set $dir to the longest existing directory.
|
if test \( "$fish_major" -ge 4 \) -o \( "$fish_major" -eq 3 -a "$fish_minor" -ge 5 \)
|
||||||
if string match -qr -- '^\\d\\d+|^4|^3\\.[5-9]' $version
|
|
||||||
# fish v3.5.0 and newer
|
|
||||||
set -- fzf_query (path normalize -- $fzf_query)
|
set -- fzf_query (path normalize -- $fzf_query)
|
||||||
set -- dir $fzf_query
|
set -- dir $fzf_query
|
||||||
while not path is -d $dir
|
while not path is -d $dir
|
||||||
set -- dir (path dirname $dir)
|
set -- dir (path dirname $dir)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
string match -q -r -- '(?<fzf_query>^[\\s\\S]*?(?=\\n?$)$)' \
|
if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||||
(string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\\n)$' '' $fzf_query | string collect -N)
|
string match -q -r -- '(?<fzf_query>^[\s\S]*?(?=\n?$)$)' \
|
||||||
|
(string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
|
||||||
|
else
|
||||||
|
set -- fzf_query (string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
|
||||||
|
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r '\\\n$' '')
|
||||||
|
end
|
||||||
set -- dir $fzf_query
|
set -- dir $fzf_query
|
||||||
while not test -d "$dir"
|
while not test -d "$dir"
|
||||||
set -- dir (dirname -z -- "$dir" | string split0)
|
set -- dir (dirname -z -- "$dir" | string split0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if not string match -q -- '.' $dir; or string match -qr -- '^\\.(/|$)' $fzf_query
|
if not string match -q -- '.' $dir; or string match -q -r -- '^\./|^\.$' $fzf_query
|
||||||
# Strip $dir from $fzf_query - preserve trailing newlines.
|
if test "$fish_major" -ge 4
|
||||||
if string match -qr -- '^\\d\\d+|^[4-9]' $version
|
string match -q -r -- '^'(string escape --style=regex -- $dir)'/?(?<fzf_query>[\s\S]*)' $fzf_query
|
||||||
# fish v4.0.0 and newer
|
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||||
string match -q -r -- '^'(string escape --style=regex -- $dir)'/?(?<fzf_query>[\\s\\S]*)' $fzf_query
|
string match -q -r -- '^/?(?<fzf_query>[\s\S]*?(?=\n?$)$)' \
|
||||||
else
|
|
||||||
string match -q -r -- '^/?(?<fzf_query>[\\s\\S]*?(?=\\n?$)$)' \
|
|
||||||
(string replace -- "$dir" '' $fzf_query | string collect -N)
|
(string replace -- "$dir" '' $fzf_query | string collect -N)
|
||||||
|
else
|
||||||
|
set -- fzf_query (string replace -- "$dir" '' $fzf_query | string collect -N)
|
||||||
|
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^/?|\\\n$' '')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
string escape -n -- "$dir" "$fzf_query" "$prefix"
|
string escape -n -- "$dir" "$fzf_query" "$prefix"
|
||||||
end
|
end
|
||||||
|
#----END INCLUDE
|
||||||
|
|
||||||
# Store current token in $dir as root for the 'find' command
|
# Store current token in $dir as root for the 'find' command
|
||||||
function fzf-file-widget -d "List files and folders"
|
function fzf-file-widget -d "List files and folders"
|
||||||
@@ -116,7 +171,7 @@ function fzf_key_bindings
|
|||||||
set -lx FZF_DEFAULT_OPTS_FILE
|
set -lx FZF_DEFAULT_OPTS_FILE
|
||||||
|
|
||||||
set -l result (eval (__fzfcmd) --walker-root=$dir --query=$fzf_query | string split0)
|
set -l result (eval (__fzfcmd) --walker-root=$dir --query=$fzf_query | string split0)
|
||||||
and commandline -rt -- (string join -- ' ' $prefix(string escape -n -- $result))' '
|
and commandline -rt -- (string join -- ' ' $prefix(string escape --no-quoted -- $result))' '
|
||||||
|
|
||||||
commandline -f repaint
|
commandline -f repaint
|
||||||
end
|
end
|
||||||
@@ -128,33 +183,45 @@ function fzf_key_bindings
|
|||||||
set -l -- fzf_query (string escape -- $command_line[$current_line])
|
set -l -- fzf_query (string escape -- $command_line[$current_line])
|
||||||
|
|
||||||
set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults '' \
|
set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults '' \
|
||||||
'--with-nth=2.. --nth=2..,.. --scheme=history --multi --no-multi-line' \
|
'--nth=2..,.. --scheme=history --multi --no-multi-line --no-wrap --wrap-sign="\t\t\t↳ " --preview-wrap-sign="↳ "' \
|
||||||
'--no-wrap --wrap-sign="\t\t\t↳ " --preview-wrap-sign="↳ " --freeze-left=1' \
|
'--bind=\'shift-delete:execute-silent(for i in (string split0 -- <{+f}); eval builtin history delete --exact --case-sensitive -- (string escape -n -- $i | string replace -r "^\d*\\\\\\t" ""); end)+reload(eval $FZF_DEFAULT_COMMAND)\'' \
|
||||||
'--bind="alt-enter:become(set -g fzf_temp {+sf3..}; string join0 -- (string split0 -- <$fzf_temp | fish_indent -i); unlink $fzf_temp &>/dev/null)"' \
|
'--bind="alt-enter:become(string join0 -- (string collect -- {+2..} | fish_indent -i))"' \
|
||||||
'--bind="alt-t:change-with-nth(1,3..|3..|2..)"' \
|
|
||||||
'--bind="shift-delete:execute-silent(eval builtin history delete -Ce -- (string escape -n -- (string split0 -- <{+sf3..})))+reload(eval $FZF_DEFAULT_COMMAND)"' \
|
|
||||||
"--bind=ctrl-r:toggle-sort,alt-r:toggle-raw --highlight-line $FZF_CTRL_R_OPTS" \
|
"--bind=ctrl-r:toggle-sort,alt-r:toggle-raw --highlight-line $FZF_CTRL_R_OPTS" \
|
||||||
'--accept-nth=3.. --delimiter="\t" --tabstop=4 --read0 --print0 --with-shell='(status fish-path)\\ -c)
|
'--accept-nth=2.. --delimiter="\t" --tabstop=4 --read0 --print0 --with-shell='(status fish-path)\\ -c)
|
||||||
|
|
||||||
# Add dynamic preview options if preview command isn't already set by user
|
# Add dynamic preview options if preview command isn't already set by user
|
||||||
if string match -qvr -- '--preview[= ]' "$FZF_DEFAULT_OPTS"
|
if string match -qvr -- '--preview[= ]' "$FZF_DEFAULT_OPTS"
|
||||||
# Prepend the options to allow user overrides
|
# Convert the highlighted timestamp using the date command if available
|
||||||
|
set -l -- date_cmd '{1}'
|
||||||
|
if type -q date
|
||||||
|
if date -d @0 '+%s' 2>/dev/null | string match -q 0
|
||||||
|
# GNU date
|
||||||
|
set -- date_cmd '(date -d @{1} \\"+%F %a %T\\")'
|
||||||
|
else if date -r 0 '+%s' 2>/dev/null | string match -q 0
|
||||||
|
# BSD date
|
||||||
|
set -- date_cmd '(date -r {1} \\"+%F %a %T\\")'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Prepend the options to allow user customizations
|
||||||
set -p -- FZF_DEFAULT_OPTS \
|
set -p -- FZF_DEFAULT_OPTS \
|
||||||
'--bind="focus,multi,resize:bg-transform:if test \\"$FZF_COLUMNS\\" -gt 100 -a \\\\( \\"$FZF_SELECT_COUNT\\" -gt 0 -o \\\\( -z \\"$FZF_WRAP\\" -a (string join0 -- <{f3..} | string length) -gt (math $FZF_COLUMNS - (switch $FZF_WITH_NTH; case 2..; echo 13; case 1,3..; echo 25; case 3..; echo 1; end)) \\\\) -o (string split0 -- <{sf3..} | fish_indent | count) -gt 1 \\\\); echo show-preview; else; echo hide-preview; end"' \
|
'--bind="focus,resize:bg-transform:if test \\"$FZF_COLUMNS\\" -gt 100 -a \\\\( \\"$FZF_SELECT_COUNT\\" -gt 0 -o \\\\( -z \\"$FZF_WRAP\\" -a (string length -- {}) -gt (math $FZF_COLUMNS - 4) \\\\) -o (string collect -- {2..} | fish_indent | count) -gt 1 \\\\); echo show-preview; else echo hide-preview; end"' \
|
||||||
'--preview="test \\"$FZF_SELECT_COUNT\\" -gt 0; and string split0 -- <{+sf3..} | fish_indent (string match -q -- 3.\\\\* $version; or echo -- --only-indent) --ansi; and echo -n \\\\n; string collect -- \\\\#\\\\ {1} (string split0 -- <{sf3..}) | fish_indent --ansi"' \
|
'--preview="string collect -- (test \\"$FZF_SELECT_COUNT\\" -gt 0; and string collect -- {+2..}) \\"\\n# \\"'$date_cmd' {2..} | fish_indent --ansi"' \
|
||||||
'--preview-window="right,50%,wrap-word,follow,info,hidden"'
|
'--preview-window="right,50%,wrap-word,follow,info,hidden"'
|
||||||
end
|
end
|
||||||
|
|
||||||
set -lx FZF_DEFAULT_OPTS_FILE
|
set -lx FZF_DEFAULT_OPTS_FILE
|
||||||
|
|
||||||
set -lx -- FZF_DEFAULT_COMMAND 'builtin history -z'
|
set -lx -- FZF_DEFAULT_COMMAND 'builtin history -z --show-time="%s%t"'
|
||||||
|
|
||||||
# Enable syntax highlighting colors on fish v4.3.3 and newer
|
# Enable syntax highlighting colors on fish v4.3.3 and newer
|
||||||
if string match -qr -- '^\\d\\d+|^4\\.[4-9]|^4\\.3\\.[3-9]' $version
|
if set -l -- v (string match -r -- '^(\d+)\.(\d+)(?:\.(\d+))?' $version)
|
||||||
|
and test "$v[2]" -gt 4 -o "$v[2]" -eq 4 -a \
|
||||||
|
\( "$v[3]" -gt 3 -o "$v[3]" -eq 3 -a \
|
||||||
|
\( -n "$v[4]" -a "$v[4]" -ge 3 \) \)
|
||||||
|
|
||||||
set -a -- FZF_DEFAULT_OPTS '--ansi'
|
set -a -- FZF_DEFAULT_OPTS '--ansi'
|
||||||
set -a -- FZF_DEFAULT_COMMAND '--color=always --show-time=(set_color $fish_color_comment)"%F %a %T%t%s%t"(set_color $fish_color_normal)'
|
set -a -- FZF_DEFAULT_COMMAND '--color=always'
|
||||||
else
|
|
||||||
set -a -- FZF_DEFAULT_COMMAND '--show-time="%F %a %T%t%s%t"'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Merge history from other sessions before searching
|
# Merge history from other sessions before searching
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
# / __/ / /_/ __/
|
# / __/ / /_/ __/
|
||||||
# /_/ /___/_/ key-bindings.zsh
|
# /_/ /___/_/ key-bindings.zsh
|
||||||
#
|
#
|
||||||
|
# - $FZF_TMUX_OPTS
|
||||||
# - $FZF_CTRL_T_COMMAND
|
# - $FZF_CTRL_T_COMMAND
|
||||||
# - $FZF_CTRL_T_OPTS
|
# - $FZF_CTRL_T_OPTS
|
||||||
# - $FZF_CTRL_R_COMMAND
|
# - $FZF_CTRL_R_COMMAND
|
||||||
|
|||||||
@@ -8,24 +8,26 @@ dir=${0%"${0##*/}"}
|
|||||||
|
|
||||||
update() {
|
update() {
|
||||||
{
|
{
|
||||||
sed -n '1,/^#----BEGIN INCLUDE common\.sh/p' "$1"
|
sed -n "1,/^#----BEGIN INCLUDE $1/p" "$2"
|
||||||
cat << EOF
|
cat << EOF
|
||||||
# NOTE: Do not directly edit this section, which is copied from "common.sh".
|
# NOTE: Do not directly edit this section, which is copied from "$1".
|
||||||
# To modify it, one can edit "common.sh" and run "./update.sh" to apply
|
# To modify it, one can edit "$1" and run "./update.sh" to apply
|
||||||
# the changes. See code comments in "common.sh" for the implementation details.
|
# the changes. See code comments in "$1" for the implementation details.
|
||||||
EOF
|
EOF
|
||||||
echo
|
echo
|
||||||
grep -v '^[[:blank:]]*#' "$dir/common.sh" # remove code comments in common.sh
|
grep -v '^[[:blank:]]*#' "$dir/$1" # remove code comments from the common file
|
||||||
sed -n '/^#----END INCLUDE/,$p' "$1"
|
sed -n '/^#----END INCLUDE/,$p' "$2"
|
||||||
} > "$1.part"
|
} > "$2.part"
|
||||||
|
|
||||||
mv -f "$1.part" "$1"
|
mv -f "$2.part" "$2"
|
||||||
}
|
}
|
||||||
|
|
||||||
update "$dir/completion.bash"
|
update "common.sh" "$dir/completion.bash"
|
||||||
update "$dir/completion.zsh"
|
update "common.sh" "$dir/completion.zsh"
|
||||||
update "$dir/key-bindings.bash"
|
update "common.sh" "$dir/key-bindings.bash"
|
||||||
update "$dir/key-bindings.zsh"
|
update "common.sh" "$dir/key-bindings.zsh"
|
||||||
|
update "common.fish" "$dir/completion.fish"
|
||||||
|
update "common.fish" "$dir/key-bindings.fish"
|
||||||
|
|
||||||
# Check if --check is in ARGV
|
# Check if --check is in ARGV
|
||||||
check=0
|
check=0
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
## What these functions do
|
## What these functions do
|
||||||
|
|
||||||
`indexByteTwo(s []byte, b1, b2 byte) int` -- returns the index of the
|
`indexByteTwo(s []byte, b1, b2 byte) int` — returns the index of the
|
||||||
**first** occurrence of `b1` or `b2` in `s`, or `-1`.
|
**first** occurrence of `b1` or `b2` in `s`, or `-1`.
|
||||||
|
|
||||||
`lastIndexByteTwo(s []byte, b1, b2 byte) int` -- returns the index of the
|
`lastIndexByteTwo(s []byte, b1, b2 byte) int` — returns the index of the
|
||||||
**last** occurrence of `b1` or `b2` in `s`, or `-1`.
|
**last** occurrence of `b1` or `b2` in `s`, or `-1`.
|
||||||
|
|
||||||
They are used by the fuzzy matching algorithm (`algo.go`) to skip ahead
|
They are used by the fuzzy matching algorithm (`algo.go`) to skip ahead
|
||||||
@@ -91,9 +91,9 @@ implementations (`2xIndexByte` using `bytes.IndexByte`, and a simple `loop`).
|
|||||||
|
|
||||||
The assembly is verified by three layers of testing:
|
The assembly is verified by three layers of testing:
|
||||||
|
|
||||||
1. **Table-driven tests** -- known inputs with expected outputs.
|
1. **Table-driven tests** — known inputs with expected outputs.
|
||||||
2. **Exhaustive tests** -- all lengths 0–256, every match position, no-match
|
2. **Exhaustive tests** — all lengths 0–256, every match position, no-match
|
||||||
cases, and both-bytes-present cases, compared against a simple loop
|
cases, and both-bytes-present cases, compared against a simple loop
|
||||||
reference.
|
reference.
|
||||||
3. **Fuzz tests** -- randomized inputs via `testing.F`, compared against the
|
3. **Fuzz tests** — randomized inputs via `testing.F`, compared against the
|
||||||
same loop reference.
|
same loop reference.
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ loop:
|
|||||||
CBZ R6, loop
|
CBZ R6, loop
|
||||||
|
|
||||||
end:
|
end:
|
||||||
// Found something or out of data, build full syndrome
|
// Found something or out of data — build full syndrome
|
||||||
VAND V5.B16, V3.B16, V3.B16
|
VAND V5.B16, V3.B16, V3.B16
|
||||||
VAND V5.B16, V4.B16, V4.B16
|
VAND V5.B16, V4.B16, V4.B16
|
||||||
VADDP V4.B16, V3.B16, V6.B16
|
VADDP V4.B16, V3.B16, V6.B16
|
||||||
|
|||||||
@@ -484,7 +484,7 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
|||||||
state.attr = state.attr | tui.Italic
|
state.attr = state.attr | tui.Italic
|
||||||
case 4:
|
case 4:
|
||||||
if sep == ':' {
|
if sep == ':' {
|
||||||
// SGR 4:N - underline style sub-parameter
|
// SGR 4:N — underline style sub-parameter
|
||||||
var subNum int
|
var subNum int
|
||||||
subNum, _, ansiCode = parseAnsiCode(ansiCode)
|
subNum, _, ansiCode = parseAnsiCode(ansiCode)
|
||||||
state.attr = state.attr &^ tui.UnderlineStyleMask
|
state.attr = state.attr &^ tui.UnderlineStyleMask
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ Usage: fzf [options]
|
|||||||
--no-bold Do not use bold text
|
--no-bold Do not use bold text
|
||||||
|
|
||||||
DISPLAY MODE
|
DISPLAY MODE
|
||||||
--height=[~][-]HEIGHT[%] Display fzf window below the cursor with the given
|
--height=[~]HEIGHT[%] Display fzf window below the cursor with the given
|
||||||
height instead of using fullscreen.
|
height instead of using fullscreen.
|
||||||
A negative value is calculated as the terminal height
|
A negative value is calculated as the terminal height
|
||||||
minus the given value.
|
minus the given value.
|
||||||
@@ -178,11 +178,10 @@ Usage: fzf [options]
|
|||||||
--header-first Print header before the prompt line
|
--header-first Print header before the prompt line
|
||||||
--header-border[=STYLE] Draw border around the header section
|
--header-border[=STYLE] Draw border around the header section
|
||||||
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
|
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
|
||||||
top|bottom|left|right|line|inline|none] (default: rounded)
|
top|bottom|left|right|line|none] (default: rounded)
|
||||||
--header-lines-border[=STYLE]
|
--header-lines-border[=STYLE]
|
||||||
Display header from --header-lines with a separate border.
|
Display header from --header-lines with a separate border.
|
||||||
Pass 'none' to still separate it but without a border.
|
Pass 'none' to still separate it but without a border.
|
||||||
Pass 'inline' to embed it inside the list frame.
|
|
||||||
--header-label=LABEL Label to print on the header border
|
--header-label=LABEL Label to print on the header border
|
||||||
--header-label-pos=COL Position of the header label
|
--header-label-pos=COL Position of the header label
|
||||||
[POSITIVE_INTEGER: columns from left|
|
[POSITIVE_INTEGER: columns from left|
|
||||||
@@ -193,7 +192,7 @@ Usage: fzf [options]
|
|||||||
--footer=STR String to print as footer
|
--footer=STR String to print as footer
|
||||||
--footer-border[=STYLE] Draw border around the footer section
|
--footer-border[=STYLE] Draw border around the footer section
|
||||||
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
|
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
|
||||||
top|bottom|left|right|line|inline|none] (default: line)
|
top|bottom|left|right|line|none] (default: line)
|
||||||
--footer-label=LABEL Label to print on the footer border
|
--footer-label=LABEL Label to print on the footer border
|
||||||
--footer-label-pos=COL Position of the footer label
|
--footer-label-pos=COL Position of the footer label
|
||||||
[POSITIVE_INTEGER: columns from left|
|
[POSITIVE_INTEGER: columns from left|
|
||||||
@@ -871,7 +870,7 @@ func nthTransformer(str string) (func(Delimiter) func([]Token, int32) string, er
|
|||||||
nth []Range
|
nth []Range
|
||||||
}
|
}
|
||||||
|
|
||||||
parts := make([]NthParts, 0, len(indexes))
|
parts := make([]NthParts, len(indexes))
|
||||||
idx := 0
|
idx := 0
|
||||||
for _, index := range indexes {
|
for _, index := range indexes {
|
||||||
if idx < index[0] {
|
if idx < index[0] {
|
||||||
@@ -954,8 +953,6 @@ func parseBorder(str string, optional bool) (tui.BorderShape, error) {
|
|||||||
switch str {
|
switch str {
|
||||||
case "line":
|
case "line":
|
||||||
return tui.BorderLine, nil
|
return tui.BorderLine, nil
|
||||||
case "inline":
|
|
||||||
return tui.BorderInline, nil
|
|
||||||
case "rounded":
|
case "rounded":
|
||||||
return tui.BorderRounded, nil
|
return tui.BorderRounded, nil
|
||||||
case "sharp":
|
case "sharp":
|
||||||
@@ -986,7 +983,7 @@ func parseBorder(str string, optional bool) (tui.BorderShape, error) {
|
|||||||
if optional && str == "" {
|
if optional && str == "" {
|
||||||
return defaultBorderShape, nil
|
return defaultBorderShape, nil
|
||||||
}
|
}
|
||||||
return tui.BorderNone, errors.New("invalid border style (expected: rounded|sharp|bold|block|thinblock|double|horizontal|vertical|top|bottom|left|right|line|inline|none)")
|
return tui.BorderNone, errors.New("invalid border style (expected: rounded|sharp|bold|block|thinblock|double|horizontal|vertical|top|bottom|left|right|none)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseKeyChords(str string, message string) (map[tui.Event]string, []tui.Event, error) {
|
func parseKeyChords(str string, message string) (map[tui.Event]string, []tui.Event, error) {
|
||||||
@@ -2225,6 +2222,9 @@ func parseHeight(str string, index int) (heightSpec, error) {
|
|||||||
str = str[1:]
|
str = str[1:]
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(str, "-") {
|
if strings.HasPrefix(str, "-") {
|
||||||
|
if heightSpec.auto {
|
||||||
|
return heightSpec, errors.New("negative(-) height is not compatible with adaptive(~) height")
|
||||||
|
}
|
||||||
heightSpec.inverse = true
|
heightSpec.inverse = true
|
||||||
str = str[1:]
|
str = str[1:]
|
||||||
}
|
}
|
||||||
@@ -3152,7 +3152,7 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
|||||||
}
|
}
|
||||||
opts.PreviewWrapSign = &str
|
opts.PreviewWrapSign = &str
|
||||||
case "--height":
|
case "--height":
|
||||||
str, err := nextString("height required: [~][-]HEIGHT[%]")
|
str, err := nextString("height required: [~]HEIGHT[%]")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -3519,9 +3519,7 @@ func applyPreset(opts *Options, preset string) error {
|
|||||||
opts.Preview.border = tui.BorderLine
|
opts.Preview.border = tui.BorderLine
|
||||||
opts.Preview.info = false
|
opts.Preview.info = false
|
||||||
opts.InfoStyle = infoDefault
|
opts.InfoStyle = infoDefault
|
||||||
opts.Theme.Gutter = tui.NewColorAttr()
|
opts.Theme.Gutter = tui.ColorAttr{Color: -1, Attr: 0}
|
||||||
space := " "
|
|
||||||
opts.Gutter = &space
|
|
||||||
empty := ""
|
empty := ""
|
||||||
opts.Separator = &empty
|
opts.Separator = &empty
|
||||||
opts.Scrollbar = &empty
|
opts.Scrollbar = &empty
|
||||||
@@ -3596,7 +3594,7 @@ func validateOptions(opts *Options) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.Height.auto && (opts.Tmux == nil || opts.Tmux.index < opts.Height.index) {
|
if opts.Height.auto {
|
||||||
for _, s := range []sizeSpec{opts.Margin[0], opts.Margin[2]} {
|
for _, s := range []sizeSpec{opts.Margin[0], opts.Margin[2]} {
|
||||||
if s.percent {
|
if s.percent {
|
||||||
return errors.New("adaptive height is not compatible with top/bottom percent margin")
|
return errors.New("adaptive height is not compatible with top/bottom percent margin")
|
||||||
@@ -3613,19 +3611,6 @@ func validateOptions(opts *Options) error {
|
|||||||
return errors.New("only ANSI attributes are allowed for 'nth' (regular, bold, underline, reverse, dim, italic, strikethrough)")
|
return errors.New("only ANSI attributes are allowed for 'nth' (regular, bold, underline, reverse, dim, italic, strikethrough)")
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.BorderShape == tui.BorderInline ||
|
|
||||||
opts.ListBorderShape == tui.BorderInline ||
|
|
||||||
opts.InputBorderShape == tui.BorderInline ||
|
|
||||||
opts.Preview.border == tui.BorderInline {
|
|
||||||
return errors.New("inline border is only supported for --header-border, --header-lines-border, and --footer-border")
|
|
||||||
}
|
|
||||||
if opts.HeaderBorderShape == tui.BorderInline &&
|
|
||||||
opts.HeaderLinesShape != tui.BorderInline &&
|
|
||||||
opts.HeaderLinesShape != tui.BorderUndefined &&
|
|
||||||
opts.HeaderLinesShape != tui.BorderNone {
|
|
||||||
return errors.New("--header-border=inline requires --header-lines-border to be inline or unset")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package fzf
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
"slices"
|
|
||||||
"sort"
|
"sort"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
@@ -31,7 +30,7 @@ type Result struct {
|
|||||||
|
|
||||||
func buildResult(item *Item, offsets []Offset, score int) Result {
|
func buildResult(item *Item, offsets []Offset, score int) Result {
|
||||||
if len(offsets) > 1 {
|
if len(offsets) > 1 {
|
||||||
slices.SortFunc(offsets, compareOffsets)
|
sort.Sort(ByOrder(offsets))
|
||||||
}
|
}
|
||||||
|
|
||||||
minBegin := math.MaxUint16
|
minBegin := math.MaxUint16
|
||||||
@@ -188,7 +187,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// slices.SortFunc(offsets, compareOffsets)
|
// sort.Sort(ByOrder(offsets))
|
||||||
|
|
||||||
// Merge offsets
|
// Merge offsets
|
||||||
// ------------ ---- -- ----
|
// ------------ ---- -- ----
|
||||||
@@ -298,20 +297,21 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
|
|||||||
return colors
|
return colors
|
||||||
}
|
}
|
||||||
|
|
||||||
func compareOffsets(a, b Offset) int {
|
// ByOrder is for sorting substring offsets
|
||||||
if a[0] < b[0] {
|
type ByOrder []Offset
|
||||||
return -1
|
|
||||||
}
|
func (a ByOrder) Len() int {
|
||||||
if a[0] > b[0] {
|
return len(a)
|
||||||
return 1
|
}
|
||||||
}
|
|
||||||
if a[1] < b[1] {
|
func (a ByOrder) Swap(i, j int) {
|
||||||
return -1
|
a[i], a[j] = a[j], a[i]
|
||||||
}
|
}
|
||||||
if a[1] > b[1] {
|
|
||||||
return 1
|
func (a ByOrder) Less(i, j int) bool {
|
||||||
}
|
ioff := a[i]
|
||||||
return 0
|
joff := a[j]
|
||||||
|
return (ioff[0] < joff[0]) || (ioff[0] == joff[0]) && (ioff[1] <= joff[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
// ByRelevance is for sorting Items
|
// ByRelevance is for sorting Items
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package fzf
|
|||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"slices"
|
|
||||||
"sort"
|
"sort"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -20,7 +19,7 @@ func TestOffsetSort(t *testing.T) {
|
|||||||
offsets := []Offset{
|
offsets := []Offset{
|
||||||
{3, 5}, {2, 7},
|
{3, 5}, {2, 7},
|
||||||
{1, 3}, {2, 9}}
|
{1, 3}, {2, 9}}
|
||||||
slices.SortFunc(offsets, compareOffsets)
|
sort.Sort(ByOrder(offsets))
|
||||||
|
|
||||||
if offsets[0][0] != 1 || offsets[0][1] != 3 ||
|
if offsets[0][0] != 1 || offsets[0][1] != 3 ||
|
||||||
offsets[1][0] != 2 || offsets[1][1] != 7 ||
|
offsets[1][0] != 2 || offsets[1][1] != 7 ||
|
||||||
|
|||||||
@@ -122,12 +122,13 @@ func startHttpServer(address listenAddress, actionChannel chan []*action, getHan
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
server := httpServer{
|
||||||
|
apiKey: []byte(apiKey),
|
||||||
|
actionChannel: actionChannel,
|
||||||
|
getHandler: getHandler,
|
||||||
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
server := httpServer{
|
|
||||||
apiKey: []byte(apiKey),
|
|
||||||
actionChannel: actionChannel,
|
|
||||||
getHandler: getHandler,
|
|
||||||
}
|
|
||||||
for {
|
for {
|
||||||
conn, err := listener.Accept()
|
conn, err := listener.Accept()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
397
src/terminal.go
397
src/terminal.go
@@ -13,7 +13,6 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"regexp"
|
"regexp"
|
||||||
"slices"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -1167,15 +1166,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
|
|||||||
if baseTheme == nil {
|
if baseTheme == nil {
|
||||||
baseTheme = renderer.DefaultTheme()
|
baseTheme = renderer.DefaultTheme()
|
||||||
}
|
}
|
||||||
// If the list border can't host an inline separator, the runtime later coerces
|
|
||||||
// BorderInline to BorderLine. Mirror that here so theme color inheritance matches
|
|
||||||
// the final rendering rather than the user's requested shape.
|
|
||||||
inlineListSupported := opts.ListBorderShape.HasTop() && opts.ListBorderShape.HasBottom()
|
|
||||||
headerInline := inlineListSupported && (opts.HeaderBorderShape == tui.BorderInline || opts.HeaderLinesShape == tui.BorderInline)
|
|
||||||
footerInline := inlineListSupported && opts.FooterBorderShape == tui.BorderInline
|
|
||||||
hasHeader := opts.HeaderBorderShape.Visible() || opts.HeaderLinesShape.Visible()
|
|
||||||
// This should be called before accessing tui.Color*
|
// This should be called before accessing tui.Color*
|
||||||
tui.InitTheme(opts.Theme, baseTheme, opts.Bold, opts.Black, opts.InputBorderShape.Visible(), hasHeader, headerInline, footerInline)
|
tui.InitTheme(opts.Theme, baseTheme, opts.Bold, opts.Black, opts.InputBorderShape.Visible(), opts.HeaderBorderShape.Visible())
|
||||||
|
|
||||||
// Gutter character
|
// Gutter character
|
||||||
var gutterChar, gutterRawChar string
|
var gutterChar, gutterRawChar string
|
||||||
@@ -1234,22 +1226,6 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inline borders are embedded between the list's top and bottom horizontals.
|
|
||||||
// Shapes missing either one (none/phantom/line/single-sided) fall back to a plain
|
|
||||||
// horizontal separator (same as BorderLine).
|
|
||||||
inlineSupported := t.listBorderShape.HasTop() && t.listBorderShape.HasBottom()
|
|
||||||
if !inlineSupported {
|
|
||||||
if t.headerBorderShape == tui.BorderInline {
|
|
||||||
t.headerBorderShape = tui.BorderLine
|
|
||||||
}
|
|
||||||
if t.headerLinesShape == tui.BorderInline {
|
|
||||||
t.headerLinesShape = tui.BorderLine
|
|
||||||
}
|
|
||||||
if t.footerBorderShape == tui.BorderInline {
|
|
||||||
t.footerBorderShape = tui.BorderLine
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine header border shape
|
// Determine header border shape
|
||||||
if t.headerBorderShape == tui.BorderLine {
|
if t.headerBorderShape == tui.BorderLine {
|
||||||
if t.layout == layoutReverse {
|
if t.layout == layoutReverse {
|
||||||
@@ -2260,98 +2236,6 @@ func (t *Terminal) determineHeaderLinesShape() (bool, tui.BorderShape) {
|
|||||||
return false, tui.BorderNone
|
return false, tui.BorderNone
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inline sections live inside wborder rather than consuming shift/shrink/availableLines.
|
|
||||||
type inlineRole int
|
|
||||||
|
|
||||||
const (
|
|
||||||
inlineRoleHeader inlineRole = iota
|
|
||||||
inlineRoleHeaderLines
|
|
||||||
inlineRoleFooter
|
|
||||||
)
|
|
||||||
|
|
||||||
type inlineSlot struct {
|
|
||||||
role inlineRole
|
|
||||||
windowType tui.WindowType
|
|
||||||
contentLines int // 0 when out of budget: a ghost placeholder is created so
|
|
||||||
// t.headerWindow / t.footerWindow stay non-nil but no frame is painted.
|
|
||||||
label labelPrinter
|
|
||||||
labelOpts labelOpts
|
|
||||||
labelLen int
|
|
||||||
}
|
|
||||||
|
|
||||||
// inlineMetaFor returns (onTop: top stack vs bottom; windowType; isInner:
|
|
||||||
// adjacent to list content) for the given inline role, derived from the layout.
|
|
||||||
// Header is outer when paired with header-lines; header-lines is always inner;
|
|
||||||
// footer is inner except in reverseList where it sits outside header-lines.
|
|
||||||
func (t *Terminal) inlineMetaFor(role inlineRole) (onTop bool, windowType tui.WindowType, isInner bool) {
|
|
||||||
switch role {
|
|
||||||
case inlineRoleHeader:
|
|
||||||
return t.layout == layoutReverse, tui.WindowHeader, false
|
|
||||||
case inlineRoleHeaderLines:
|
|
||||||
return t.layout != layoutDefault, tui.WindowHeader, true
|
|
||||||
case inlineRoleFooter:
|
|
||||||
return t.layout != layoutReverse, tui.WindowFooter, t.layout != layoutReverseList
|
|
||||||
}
|
|
||||||
return false, tui.WindowBase, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Terminal) placeInlineSection(win tui.Window, role inlineRole) {
|
|
||||||
switch role {
|
|
||||||
case inlineRoleHeader:
|
|
||||||
t.headerWindow = win
|
|
||||||
case inlineRoleHeaderLines:
|
|
||||||
t.headerLinesWindow = win
|
|
||||||
case inlineRoleFooter:
|
|
||||||
t.footerWindow = win
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// placeInlineStack walks `slots` in outer-to-inner order. Only the outermost
|
|
||||||
// slot (index 0) claims the adjacent wborder edge; later slots paint side-only
|
|
||||||
// frames so the T-junction separator lands between sections.
|
|
||||||
func (t *Terminal) placeInlineStack(slots []inlineSlot, startRow int, onTop bool) {
|
|
||||||
firstEdge := tui.SectionEdgeTop
|
|
||||||
if !onTop {
|
|
||||||
firstEdge = tui.SectionEdgeBottom
|
|
||||||
}
|
|
||||||
noBorder := tui.MakeBorderStyle(tui.BorderNone, t.unicode)
|
|
||||||
subLeft := t.window.Left()
|
|
||||||
subWidth := t.window.Width()
|
|
||||||
cursor := startRow
|
|
||||||
for i, s := range slots {
|
|
||||||
var windowTop, sepRow int
|
|
||||||
if onTop {
|
|
||||||
windowTop = cursor
|
|
||||||
sepRow = cursor + s.contentLines - t.wborder.Top()
|
|
||||||
} else {
|
|
||||||
windowTop = cursor - s.contentLines + 1
|
|
||||||
sepRow = windowTop - 1 - t.wborder.Top()
|
|
||||||
}
|
|
||||||
// 0-height placeholder keeps t.headerWindow / t.footerWindow non-nil.
|
|
||||||
win := t.tui.NewWindow(windowTop, subLeft, subWidth, s.contentLines, s.windowType, noBorder, true)
|
|
||||||
t.placeInlineSection(win, s.role)
|
|
||||||
if s.contentLines == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
secTop := windowTop - t.wborder.Top()
|
|
||||||
secBottom := secTop + s.contentLines - 1
|
|
||||||
edge := tui.SectionEdgeNone
|
|
||||||
if i == 0 {
|
|
||||||
edge = firstEdge
|
|
||||||
}
|
|
||||||
t.wborder.PaintSectionFrame(secTop, secBottom, s.windowType, edge)
|
|
||||||
// useBottom=onTop so the separator always hugs the list (inner side).
|
|
||||||
// Matters for thinblock/block where top != bottom char.
|
|
||||||
t.wborder.DrawHSeparator(sepRow, s.windowType, onTop)
|
|
||||||
t.printLabelAt(t.wborder, s.label, s.labelOpts, s.labelLen, sepRow)
|
|
||||||
if onTop {
|
|
||||||
cursor += s.contentLines + 1
|
|
||||||
} else {
|
|
||||||
cursor -= s.contentLines + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
||||||
t.clearNumLinesCache()
|
t.clearNumLinesCache()
|
||||||
t.forcePreview = forcePreview
|
t.forcePreview = forcePreview
|
||||||
@@ -2468,50 +2352,6 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Slices are ordered outer-to-inner: index 0 touches wborder's edge.
|
|
||||||
var inlineTop []inlineSlot
|
|
||||||
var inlineBottom []inlineSlot
|
|
||||||
|
|
||||||
// Caps contentLines against remaining wborder space. Oversized requests (e.g. a
|
|
||||||
// huge --header-lines) become 0-height placeholders rather than pushing the list
|
|
||||||
// window to negative height.
|
|
||||||
addInline := func(contentLines int, role inlineRole) {
|
|
||||||
onTop, windowType, isInner := t.inlineMetaFor(role)
|
|
||||||
used := 0
|
|
||||||
for _, s := range inlineTop {
|
|
||||||
if s.contentLines > 0 {
|
|
||||||
used += s.contentLines + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, s := range inlineBottom {
|
|
||||||
if s.contentLines > 0 {
|
|
||||||
used += s.contentLines + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
remaining := availableLines - borderLines(t.listBorderShape) - used - 1
|
|
||||||
if remaining < 2 {
|
|
||||||
contentLines = 0
|
|
||||||
} else {
|
|
||||||
contentLines = util.Constrain(contentLines, 1, remaining-1)
|
|
||||||
}
|
|
||||||
slot := inlineSlot{role: role, windowType: windowType, contentLines: contentLines}
|
|
||||||
switch role {
|
|
||||||
case inlineRoleHeader:
|
|
||||||
slot.label, slot.labelOpts, slot.labelLen = t.headerLabel, t.headerLabelOpts, t.headerLabelLen
|
|
||||||
case inlineRoleFooter:
|
|
||||||
slot.label, slot.labelOpts, slot.labelLen = t.footerLabel, t.footerLabelOpts, t.footerLabelLen
|
|
||||||
}
|
|
||||||
target := &inlineTop
|
|
||||||
if !onTop {
|
|
||||||
target = &inlineBottom
|
|
||||||
}
|
|
||||||
if isInner {
|
|
||||||
*target = append(*target, slot)
|
|
||||||
} else {
|
|
||||||
*target = append([]inlineSlot{slot}, *target...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adjust position and size of the list window if header border is set
|
// Adjust position and size of the list window if header border is set
|
||||||
headerBorderHeight := 0
|
headerBorderHeight := 0
|
||||||
if hasHeaderWindow {
|
if hasHeaderWindow {
|
||||||
@@ -2519,62 +2359,37 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
|||||||
if hasHeaderLinesWindow {
|
if hasHeaderLinesWindow {
|
||||||
headerWindowHeight -= t.headerLines
|
headerWindowHeight -= t.headerLines
|
||||||
}
|
}
|
||||||
if t.headerBorderShape == tui.BorderInline {
|
headerBorderHeight = util.Constrain(borderLines(t.headerBorderShape)+headerWindowHeight, 0, availableLines)
|
||||||
addInline(headerWindowHeight, inlineRoleHeader)
|
if t.layout == layoutReverse {
|
||||||
|
shift += headerBorderHeight
|
||||||
|
shrink += headerBorderHeight
|
||||||
} else {
|
} else {
|
||||||
headerBorderHeight = util.Constrain(borderLines(t.headerBorderShape)+headerWindowHeight, 0, availableLines)
|
shrink += headerBorderHeight
|
||||||
if t.layout == layoutReverse {
|
|
||||||
shift += headerBorderHeight
|
|
||||||
shrink += headerBorderHeight
|
|
||||||
} else {
|
|
||||||
shrink += headerBorderHeight
|
|
||||||
}
|
|
||||||
availableLines -= headerBorderHeight
|
|
||||||
}
|
}
|
||||||
|
availableLines -= headerBorderHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
headerLinesHeight := 0
|
headerLinesHeight := 0
|
||||||
if hasHeaderLinesWindow {
|
if hasHeaderLinesWindow {
|
||||||
if headerLinesShape == tui.BorderInline {
|
headerLinesHeight = util.Constrain(borderLines(headerLinesShape)+t.headerLines, 0, availableLines)
|
||||||
addInline(t.headerLines, inlineRoleHeaderLines)
|
if t.layout != layoutDefault {
|
||||||
|
shift += headerLinesHeight
|
||||||
|
shrink += headerLinesHeight
|
||||||
} else {
|
} else {
|
||||||
headerLinesHeight = util.Constrain(borderLines(headerLinesShape)+t.headerLines, 0, availableLines)
|
shrink += headerLinesHeight
|
||||||
if t.layout != layoutDefault {
|
|
||||||
shift += headerLinesHeight
|
|
||||||
shrink += headerLinesHeight
|
|
||||||
} else {
|
|
||||||
shrink += headerLinesHeight
|
|
||||||
}
|
|
||||||
availableLines -= headerLinesHeight
|
|
||||||
}
|
}
|
||||||
|
availableLines -= headerLinesHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
footerBorderHeight := 0
|
footerBorderHeight := 0
|
||||||
if hasFooterWindow {
|
if hasFooterWindow {
|
||||||
if t.footerBorderShape == tui.BorderInline {
|
// Footer lines should not take all available lines
|
||||||
addInline(len(t.footer), inlineRoleFooter)
|
footerBorderHeight = util.Constrain(borderLines(t.footerBorderShape)+len(t.footer), 0, availableLines)
|
||||||
} else {
|
shrink += footerBorderHeight
|
||||||
// Footer lines should not take all available lines
|
if t.layout != layoutReverse {
|
||||||
footerBorderHeight = util.Constrain(borderLines(t.footerBorderShape)+len(t.footer), 0, availableLines)
|
shift += footerBorderHeight
|
||||||
shrink += footerBorderHeight
|
|
||||||
if t.layout != layoutReverse {
|
|
||||||
shift += footerBorderHeight
|
|
||||||
}
|
|
||||||
availableLines -= footerBorderHeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inlineTopLines := 0
|
|
||||||
for _, s := range inlineTop {
|
|
||||||
if s.contentLines > 0 {
|
|
||||||
inlineTopLines += s.contentLines + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
inlineBottomLines := 0
|
|
||||||
for _, s := range inlineBottom {
|
|
||||||
if s.contentLines > 0 {
|
|
||||||
inlineBottomLines += s.contentLines + 1
|
|
||||||
}
|
}
|
||||||
|
availableLines -= footerBorderHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up list border
|
// Set up list border
|
||||||
@@ -2685,12 +2500,12 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
|||||||
if previewOpts.position == posUp {
|
if previewOpts.position == posUp {
|
||||||
innerBorderFn(marginInt[0]+pheight, marginInt[3], width, height-pheight)
|
innerBorderFn(marginInt[0]+pheight, marginInt[3], width, height-pheight)
|
||||||
t.window = t.tui.NewWindow(
|
t.window = t.tui.NewWindow(
|
||||||
innerMarginInt[0]+pheight+shift+inlineTopLines, innerMarginInt[3], innerWidth, innerHeight-pheight-shrink-inlineTopLines-inlineBottomLines, tui.WindowList, noBorder, true)
|
innerMarginInt[0]+pheight+shift, innerMarginInt[3], innerWidth, innerHeight-pheight-shrink, tui.WindowList, noBorder, true)
|
||||||
createPreviewWindow(marginInt[0], marginInt[3], width, pheight)
|
createPreviewWindow(marginInt[0], marginInt[3], width, pheight)
|
||||||
} else {
|
} else {
|
||||||
innerBorderFn(marginInt[0], marginInt[3], width, height-pheight)
|
innerBorderFn(marginInt[0], marginInt[3], width, height-pheight)
|
||||||
t.window = t.tui.NewWindow(
|
t.window = t.tui.NewWindow(
|
||||||
innerMarginInt[0]+shift+inlineTopLines, innerMarginInt[3], innerWidth, innerHeight-pheight-shrink-inlineTopLines-inlineBottomLines, tui.WindowList, noBorder, true)
|
innerMarginInt[0]+shift, innerMarginInt[3], innerWidth, innerHeight-pheight-shrink, tui.WindowList, noBorder, true)
|
||||||
createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight)
|
createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight)
|
||||||
}
|
}
|
||||||
case posLeft, posRight:
|
case posLeft, posRight:
|
||||||
@@ -2729,7 +2544,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
|||||||
m = 1
|
m = 1
|
||||||
}
|
}
|
||||||
t.window = t.tui.NewWindow(
|
t.window = t.tui.NewWindow(
|
||||||
innerMarginInt[0]+shift+inlineTopLines, innerMarginInt[3]+pwidth+m, innerWidth-pwidth-m, innerHeight-shrink-inlineTopLines-inlineBottomLines, tui.WindowList, noBorder, true)
|
innerMarginInt[0]+shift, innerMarginInt[3]+pwidth+m, innerWidth-pwidth-m, innerHeight-shrink, tui.WindowList, noBorder, true)
|
||||||
|
|
||||||
// Clear characters on the margin
|
// Clear characters on the margin
|
||||||
// fzf --bind 'space:toggle-preview' --preview ':' --preview-window left,1
|
// fzf --bind 'space:toggle-preview' --preview ':' --preview-window left,1
|
||||||
@@ -2761,7 +2576,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
|||||||
}
|
}
|
||||||
innerBorderFn(marginInt[0], marginInt[3], width-pwidth, height)
|
innerBorderFn(marginInt[0], marginInt[3], width-pwidth, height)
|
||||||
t.window = t.tui.NewWindow(
|
t.window = t.tui.NewWindow(
|
||||||
innerMarginInt[0]+shift+inlineTopLines, innerMarginInt[3], innerWidth-pwidth, innerHeight-shrink-inlineTopLines-inlineBottomLines, tui.WindowList, noBorder, true)
|
innerMarginInt[0]+shift, innerMarginInt[3], innerWidth-pwidth, innerHeight-shrink, tui.WindowList, noBorder, true)
|
||||||
x := marginInt[3] + width - pwidth
|
x := marginInt[3] + width - pwidth
|
||||||
createPreviewWindow(marginInt[0], x, pwidth, height)
|
createPreviewWindow(marginInt[0], x, pwidth, height)
|
||||||
}
|
}
|
||||||
@@ -2799,15 +2614,10 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
|||||||
}
|
}
|
||||||
innerBorderFn(marginInt[0], marginInt[3], width, height)
|
innerBorderFn(marginInt[0], marginInt[3], width, height)
|
||||||
t.window = t.tui.NewWindow(
|
t.window = t.tui.NewWindow(
|
||||||
innerMarginInt[0]+shift+inlineTopLines,
|
innerMarginInt[0]+shift,
|
||||||
innerMarginInt[3],
|
innerMarginInt[3],
|
||||||
innerWidth,
|
innerWidth,
|
||||||
innerHeight-shrink-inlineTopLines-inlineBottomLines, tui.WindowList, noBorder, true)
|
innerHeight-shrink, tui.WindowList, noBorder, true)
|
||||||
}
|
|
||||||
|
|
||||||
if len(inlineTop)+len(inlineBottom) > 0 && t.wborder != nil {
|
|
||||||
t.placeInlineStack(inlineTop, t.window.Top()-inlineTopLines, true)
|
|
||||||
t.placeInlineStack(inlineBottom, t.window.Top()+t.window.Height()+inlineBottomLines-1, false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(t.scrollbar) == 0 {
|
if len(t.scrollbar) == 0 {
|
||||||
@@ -2844,11 +2654,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
|||||||
|
|
||||||
if hasInputWindow {
|
if hasInputWindow {
|
||||||
var btop int
|
var btop int
|
||||||
// Inline sections live inside the list frame, so they don't participate
|
if (hasHeaderWindow || hasHeaderLinesWindow) && t.headerFirst {
|
||||||
// in --header-first repositioning; only non-inline sections do.
|
|
||||||
hasNonInlineHeader := hasHeaderWindow && t.headerBorderShape != tui.BorderInline
|
|
||||||
hasNonInlineHeaderLines := hasHeaderLinesWindow && headerLinesShape != tui.BorderInline
|
|
||||||
if (hasNonInlineHeader || hasNonInlineHeaderLines) && t.headerFirst {
|
|
||||||
switch t.layout {
|
switch t.layout {
|
||||||
case layoutDefault:
|
case layoutDefault:
|
||||||
btop = w.Top() + w.Height()
|
btop = w.Top() + w.Height()
|
||||||
@@ -2893,7 +2699,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set up header border
|
// Set up header border
|
||||||
if hasHeaderWindow && t.headerBorderShape != tui.BorderInline {
|
if hasHeaderWindow {
|
||||||
var btop int
|
var btop int
|
||||||
if hasInputWindow && t.headerFirst {
|
if hasInputWindow && t.headerFirst {
|
||||||
if t.layout == layoutReverse {
|
if t.layout == layoutReverse {
|
||||||
@@ -2921,7 +2727,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set up header lines border
|
// Set up header lines border
|
||||||
if hasHeaderLinesWindow && headerLinesShape != tui.BorderInline {
|
if hasHeaderLinesWindow {
|
||||||
var btop int
|
var btop int
|
||||||
// NOTE: We still have to handle --header-first here in case
|
// NOTE: We still have to handle --header-first here in case
|
||||||
// --header-lines-border is set. Can't we just use header window instead
|
// --header-lines-border is set. Can't we just use header window instead
|
||||||
@@ -2954,7 +2760,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set up footer
|
// Set up footer
|
||||||
if hasFooterWindow && t.footerBorderShape != tui.BorderInline {
|
if hasFooterWindow {
|
||||||
var btop int
|
var btop int
|
||||||
if t.layout == layoutReverse {
|
if t.layout == layoutReverse {
|
||||||
btop = w.Top() + w.Height()
|
btop = w.Top() + w.Height()
|
||||||
@@ -2971,21 +2777,8 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
|||||||
t.footerWindow = createInnerWindow(t.footerBorder, t.footerBorderShape, tui.WindowFooter, 0)
|
t.footerWindow = createInnerWindow(t.footerBorder, t.footerBorderShape, tui.WindowFooter, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// When the list label lands on an edge owned by an inline section, swap its bg
|
// Print border label
|
||||||
// so the label reads as part of that section's frame. Fg stays at list-label.
|
t.printLabel(t.wborder, t.listLabel, t.listLabelOpts, t.listLabelLen, t.listBorderShape, false)
|
||||||
listLabel, listLabelLen := t.listLabel, t.listLabelLen
|
|
||||||
var adjacentSection *inlineSlot
|
|
||||||
if t.listLabelOpts.bottom && len(inlineBottom) > 0 {
|
|
||||||
adjacentSection = &inlineBottom[0]
|
|
||||||
} else if !t.listLabelOpts.bottom && len(inlineTop) > 0 {
|
|
||||||
adjacentSection = &inlineTop[0]
|
|
||||||
}
|
|
||||||
if adjacentSection != nil {
|
|
||||||
bg := tui.BorderColor(adjacentSection.windowType).Bg()
|
|
||||||
custom := tui.ColListLabel.WithBg(tui.ColorAttr{Color: bg, Attr: tui.AttrUndefined})
|
|
||||||
listLabel, listLabelLen = t.ansiLabelPrinter(t.listLabelOpts.label, &custom, false)
|
|
||||||
}
|
|
||||||
t.printLabel(t.wborder, listLabel, t.listLabelOpts, listLabelLen, t.listBorderShape, false)
|
|
||||||
t.printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape, false)
|
t.printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape, false)
|
||||||
t.printLabel(t.pborder, t.previewLabel, t.previewLabelOpts, t.previewLabelLen, t.activePreviewOpts.Border(), false)
|
t.printLabel(t.pborder, t.previewLabel, t.previewLabelOpts, t.previewLabelLen, t.activePreviewOpts.Border(), false)
|
||||||
t.printLabel(t.inputBorder, t.inputLabel, t.inputLabelOpts, t.inputLabelLen, t.inputBorderShape, false)
|
t.printLabel(t.inputBorder, t.inputLabel, t.inputLabelOpts, t.inputLabelLen, t.inputBorderShape, false)
|
||||||
@@ -2993,39 +2786,37 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
|||||||
t.printLabel(t.footerBorder, t.footerLabel, t.footerLabelOpts, t.footerLabelLen, t.footerBorderShape, false)
|
t.printLabel(t.footerBorder, t.footerLabel, t.footerLabelOpts, t.footerLabelLen, t.footerBorderShape, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// printLabelAt positions and renders a label at the given row of `window`. Shared by
|
|
||||||
// printLabel (which computes row from the border shape) and the inline-section label
|
|
||||||
// code (which uses an explicit separator row).
|
|
||||||
func (t *Terminal) printLabelAt(window tui.Window, render labelPrinter, opts labelOpts, length int, row int) {
|
|
||||||
if window == nil || render == nil || window.Height() == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var col int
|
|
||||||
if opts.column == 0 {
|
|
||||||
col = max(0, (window.Width()-length)/2)
|
|
||||||
} else if opts.column < 0 {
|
|
||||||
col = max(0, window.Width()+opts.column+1-length)
|
|
||||||
} else {
|
|
||||||
col = min(opts.column-1, window.Width()-length)
|
|
||||||
}
|
|
||||||
window.Move(row, col)
|
|
||||||
render(window, window.Width())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Terminal) printLabel(window tui.Window, render labelPrinter, opts labelOpts, length int, borderShape tui.BorderShape, redrawBorder bool) {
|
func (t *Terminal) printLabel(window tui.Window, render labelPrinter, opts labelOpts, length int, borderShape tui.BorderShape, redrawBorder bool) {
|
||||||
if window == nil || window.Height() == 0 {
|
if window == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if window.Height() == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
switch borderShape {
|
switch borderShape {
|
||||||
case tui.BorderHorizontal, tui.BorderTop, tui.BorderBottom, tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderBlock, tui.BorderThinBlock, tui.BorderDouble:
|
case tui.BorderHorizontal, tui.BorderTop, tui.BorderBottom, tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderBlock, tui.BorderThinBlock, tui.BorderDouble:
|
||||||
if redrawBorder {
|
if redrawBorder {
|
||||||
window.DrawHBorder()
|
window.DrawHBorder()
|
||||||
}
|
}
|
||||||
|
if render == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var col int
|
||||||
|
if opts.column == 0 {
|
||||||
|
col = max(0, (window.Width()-length)/2)
|
||||||
|
} else if opts.column < 0 {
|
||||||
|
col = max(0, window.Width()+opts.column+1-length)
|
||||||
|
} else {
|
||||||
|
col = min(opts.column-1, window.Width()-length)
|
||||||
|
}
|
||||||
row := 0
|
row := 0
|
||||||
if borderShape == tui.BorderBottom || opts.bottom {
|
if borderShape == tui.BorderBottom || opts.bottom {
|
||||||
row = window.Height() - 1
|
row = window.Height() - 1
|
||||||
}
|
}
|
||||||
t.printLabelAt(window, render, opts, length, row)
|
window.Move(row, col)
|
||||||
|
render(window, window.Width())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3393,19 +3184,8 @@ func (t *Terminal) resizeIfNeeded() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inline sections are budget-capped inside wborder, so window.Height() may be
|
|
||||||
// smaller than the requested content length. Treat "capped" (height < want) as
|
|
||||||
// a no-op to avoid triggering a full redraw on every info/header/footer event
|
|
||||||
// when the user has requested more content than fits.
|
|
||||||
mismatch := func(shape tui.BorderShape, height, want int) bool {
|
|
||||||
if shape == tui.BorderInline && height < want {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return height != want
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check footer window
|
// Check footer window
|
||||||
if len(t.footer) > 0 && (t.footerWindow == nil || mismatch(t.footerBorderShape, t.footerWindow.Height(), len(t.footer))) ||
|
if len(t.footer) > 0 && (t.footerWindow == nil || t.footerWindow.Height() != len(t.footer)) ||
|
||||||
len(t.footer) == 0 && t.footerWindow != nil {
|
len(t.footer) == 0 && t.footerWindow != nil {
|
||||||
t.printAll()
|
t.printAll()
|
||||||
return true
|
return true
|
||||||
@@ -3419,12 +3199,14 @@ func (t *Terminal) resizeIfNeeded() bool {
|
|||||||
if needHeaderLinesWindow {
|
if needHeaderLinesWindow {
|
||||||
primaryHeaderLines -= t.headerLines
|
primaryHeaderLines -= t.headerLines
|
||||||
}
|
}
|
||||||
|
// FIXME: Full redraw is triggered if there are too many lines in the header
|
||||||
|
// so that the header window cannot display all of them.
|
||||||
if (needHeaderWindow && t.headerWindow == nil) ||
|
if (needHeaderWindow && t.headerWindow == nil) ||
|
||||||
(!needHeaderWindow && t.headerWindow != nil) ||
|
(!needHeaderWindow && t.headerWindow != nil) ||
|
||||||
(needHeaderWindow && t.headerWindow != nil && mismatch(t.headerBorderShape, t.headerWindow.Height(), primaryHeaderLines)) ||
|
(needHeaderWindow && t.headerWindow != nil && primaryHeaderLines != t.headerWindow.Height()) ||
|
||||||
(needHeaderLinesWindow && t.headerLinesWindow == nil) ||
|
(needHeaderLinesWindow && t.headerLinesWindow == nil) ||
|
||||||
(!needHeaderLinesWindow && t.headerLinesWindow != nil) ||
|
(!needHeaderLinesWindow && t.headerLinesWindow != nil) ||
|
||||||
(needHeaderLinesWindow && t.headerLinesWindow != nil && mismatch(t.headerLinesShape, t.headerLinesWindow.Height(), t.headerLines)) {
|
(needHeaderLinesWindow && t.headerLinesWindow != nil && t.headerLines != t.headerLinesWindow.Height()) {
|
||||||
t.printAll()
|
t.printAll()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -3436,23 +3218,14 @@ func (t *Terminal) printHeader() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// headerWindow is nil when hasHeaderWindow() returned false at resize time,
|
t.withWindow(t.headerWindow, func() {
|
||||||
// e.g. --header-border=inline combined with empty header content. Don't
|
var headerItems []Item
|
||||||
// delegate to printHeaderImpl because its nil-window branch folds the header
|
if !t.hasHeaderLinesWindow() {
|
||||||
// into the list window, which isn't valid for inline. A nil window is only
|
headerItems = t.header
|
||||||
// legitimate when the shape is NOT inline (e.g. header combined with the
|
}
|
||||||
// list when --no-list-border is in effect).
|
t.printHeaderImpl(t.headerWindow, t.headerBorderShape, t.header0, headerItems)
|
||||||
if !(t.headerBorderShape == tui.BorderInline && t.headerWindow == nil) {
|
})
|
||||||
t.withWindow(t.headerWindow, func() {
|
if w, shape := t.determineHeaderLinesShape(); w {
|
||||||
var headerItems []Item
|
|
||||||
if !t.hasHeaderLinesWindow() {
|
|
||||||
headerItems = t.header
|
|
||||||
}
|
|
||||||
t.printHeaderImpl(t.headerWindow, t.headerBorderShape, t.header0, headerItems)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if w, shape := t.determineHeaderLinesShape(); w &&
|
|
||||||
!(shape == tui.BorderInline && t.headerLinesWindow == nil) {
|
|
||||||
t.withWindow(t.headerLinesWindow, func() {
|
t.withWindow(t.headerLinesWindow, func() {
|
||||||
t.printHeaderImpl(t.headerLinesWindow, shape, nil, t.header)
|
t.printHeaderImpl(t.headerLinesWindow, shape, nil, t.header)
|
||||||
})
|
})
|
||||||
@@ -3503,10 +3276,7 @@ func (t *Terminal) headerIndentImpl(base int, borderShape tui.BorderShape) int {
|
|||||||
if t.listBorderShape.HasLeft() {
|
if t.listBorderShape.HasLeft() {
|
||||||
indentSize += 1 + t.borderWidth
|
indentSize += 1 + t.borderWidth
|
||||||
}
|
}
|
||||||
// Section borders with their own left side skip past the list border's left column.
|
if borderShape.HasLeft() {
|
||||||
// Inline sections also skip it, but only when the list border actually has a left,
|
|
||||||
// since otherwise the inline window starts flush with the list window.
|
|
||||||
if borderShape.HasLeft() || (borderShape == tui.BorderInline && t.listBorderShape.HasLeft()) {
|
|
||||||
indentSize -= 1 + t.borderWidth
|
indentSize -= 1 + t.borderWidth
|
||||||
if indentSize < 0 {
|
if indentSize < 0 {
|
||||||
indentSize = 0
|
indentSize = 0
|
||||||
@@ -3981,7 +3751,7 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
|
|||||||
offset := Offset{int32(p), int32(p + w)}
|
offset := Offset{int32(p), int32(p + w)}
|
||||||
charOffsets[idx] = offset
|
charOffsets[idx] = offset
|
||||||
}
|
}
|
||||||
slices.SortFunc(charOffsets, compareOffsets)
|
sort.Sort(ByOrder(charOffsets))
|
||||||
}
|
}
|
||||||
|
|
||||||
// When postTask is nil, we're printing header lines. No need to care about nth.
|
// When postTask is nil, we're printing header lines. No need to care about nth.
|
||||||
@@ -4018,7 +3788,7 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
|
|||||||
end := start + int32(length)
|
end := start + int32(length)
|
||||||
nthOffsets[i] = Offset{int32(start), int32(end)}
|
nthOffsets[i] = Offset{int32(start), int32(end)}
|
||||||
}
|
}
|
||||||
slices.SortFunc(nthOffsets, compareOffsets)
|
sort.Sort(ByOrder(nthOffsets))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
allOffsets := result.colorOffsets(charOffsets, nthOffsets, t.theme, colBase, colMatch, t.nthAttr, nthOverlay, hidden)
|
allOffsets := result.colorOffsets(charOffsets, nthOffsets, t.theme, colBase, colMatch, t.nthAttr, nthOverlay, hidden)
|
||||||
@@ -6181,28 +5951,11 @@ func (t *Terminal) Loop() error {
|
|||||||
case reqRedrawInputLabel:
|
case reqRedrawInputLabel:
|
||||||
t.printLabel(t.inputBorder, t.inputLabel, t.inputLabelOpts, t.inputLabelLen, t.inputBorderShape, true)
|
t.printLabel(t.inputBorder, t.inputLabel, t.inputLabelOpts, t.inputLabelLen, t.inputBorderShape, true)
|
||||||
case reqRedrawHeaderLabel:
|
case reqRedrawHeaderLabel:
|
||||||
if t.headerBorderShape == tui.BorderInline {
|
t.printLabel(t.headerBorder, t.headerLabel, t.headerLabelOpts, t.headerLabelLen, t.headerBorderShape, true)
|
||||||
// Inline labels sit on the separator inside wborder; re-run the
|
|
||||||
// full layout to repaint the separator + label together.
|
|
||||||
t.printAll()
|
|
||||||
} else {
|
|
||||||
t.printLabel(t.headerBorder, t.headerLabel, t.headerLabelOpts, t.headerLabelLen, t.headerBorderShape, true)
|
|
||||||
}
|
|
||||||
case reqRedrawFooterLabel:
|
case reqRedrawFooterLabel:
|
||||||
if t.footerBorderShape == tui.BorderInline {
|
t.printLabel(t.footerBorder, t.footerLabel, t.footerLabelOpts, t.footerLabelLen, t.footerBorderShape, true)
|
||||||
t.printAll()
|
|
||||||
} else {
|
|
||||||
t.printLabel(t.footerBorder, t.footerLabel, t.footerLabelOpts, t.footerLabelLen, t.footerBorderShape, true)
|
|
||||||
}
|
|
||||||
case reqRedrawListLabel:
|
case reqRedrawListLabel:
|
||||||
// When inline sections are active, the label's bg depends on which
|
t.printLabel(t.wborder, t.listLabel, t.listLabelOpts, t.listLabelLen, t.listBorderShape, true)
|
||||||
// section owns the adjacent edge. Rerun the layout to reuse that
|
|
||||||
// logic rather than duplicating it here.
|
|
||||||
if t.headerBorderShape == tui.BorderInline || t.headerLinesShape == tui.BorderInline || t.footerBorderShape == tui.BorderInline {
|
|
||||||
t.printAll()
|
|
||||||
} else {
|
|
||||||
t.printLabel(t.wborder, t.listLabel, t.listLabelOpts, t.listLabelLen, t.listBorderShape, true)
|
|
||||||
}
|
|
||||||
case reqRedrawBorderLabel:
|
case reqRedrawBorderLabel:
|
||||||
t.printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape, true)
|
t.printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape, true)
|
||||||
case reqRedrawPreviewLabel:
|
case reqRedrawPreviewLabel:
|
||||||
@@ -6781,16 +6534,10 @@ func (t *Terminal) Loop() error {
|
|||||||
t.cx = len(t.input)
|
t.cx = len(t.input)
|
||||||
case actChangeHeader, actTransformHeader, actBgTransformHeader:
|
case actChangeHeader, actTransformHeader, actBgTransformHeader:
|
||||||
capture(false, func(header string) {
|
capture(false, func(header string) {
|
||||||
|
// When a dedicated header window is not used, we may need to
|
||||||
|
// update other elements as well.
|
||||||
if t.changeHeader(header) {
|
if t.changeHeader(header) {
|
||||||
// resizeIfNeeded() tolerates a shorter-than-wanted inline
|
req(reqList, reqPrompt, reqInfo)
|
||||||
// window, so a length change can leave the inline slot
|
|
||||||
// stale. Force a redraw to re-run the layout. Non-inline
|
|
||||||
// shapes are handled by resizeIfNeeded.
|
|
||||||
if t.headerBorderShape == tui.BorderInline {
|
|
||||||
req(reqRedraw)
|
|
||||||
} else {
|
|
||||||
req(reqList, reqPrompt, reqInfo)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
req(reqHeader)
|
req(reqHeader)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -721,7 +721,7 @@ func TestWordWrapAnsiLine(t *testing.T) {
|
|||||||
t.Errorf("ANSI: %q", result)
|
t.Errorf("ANSI: %q", result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Long word (no space) - no break, let character wrapping handle it
|
// Long word (no space) — no break, let character wrapping handle it
|
||||||
result = term.wordWrapAnsiLine("abcdefghij", 5, 2)
|
result = term.wordWrapAnsiLine("abcdefghij", 5, 2)
|
||||||
if len(result) != 1 || result[0] != "abcdefghij" {
|
if len(result) != 1 || result[0] != "abcdefghij" {
|
||||||
t.Errorf("Long word: %q", result)
|
t.Errorf("Long word: %q", result)
|
||||||
@@ -749,7 +749,7 @@ func TestWordWrapAnsiLine(t *testing.T) {
|
|||||||
|
|
||||||
// Tab handling: tab expands to tabstop-aligned width
|
// Tab handling: tab expands to tabstop-aligned width
|
||||||
term.tabstop = 8
|
term.tabstop = 8
|
||||||
// "\thi there" - tab at column 0 expands to 8, total "hi" starts at 8
|
// "\thi there" — tab at column 0 expands to 8, total "hi" starts at 8
|
||||||
// maxWidth=15: "\thi" = 10 wide, "there" = 5 wide, total 16 > 15, wrap at space
|
// maxWidth=15: "\thi" = 10 wide, "there" = 5 wide, total 16 > 15, wrap at space
|
||||||
result = term.wordWrapAnsiLine("\thi there", 15, 2)
|
result = term.wordWrapAnsiLine("\thi there", 15, 2)
|
||||||
if len(result) != 2 || result[0] != "\thi" || result[1] != "there" {
|
if len(result) != 2 || result[0] != "\thi" || result[1] != "there" {
|
||||||
|
|||||||
246
src/tui/light.go
246
src/tui/light.go
@@ -689,7 +689,6 @@ func (r *LightRenderer) escSequence(sz *int) Event {
|
|||||||
switch r.buffer[4] {
|
switch r.buffer[4] {
|
||||||
case '1', '2', '3', '4', '5', '6', '7', '8', '9':
|
case '1', '2', '3', '4', '5', '6', '7', '8', '9':
|
||||||
// Kitty iTerm2 WezTerm
|
// Kitty iTerm2 WezTerm
|
||||||
// ARROW "\e[1;1D"
|
|
||||||
// SHIFT-ARROW "\e[1;2D"
|
// SHIFT-ARROW "\e[1;2D"
|
||||||
// ALT-SHIFT-ARROW "\e[1;4D" "\e[1;10D" "\e[1;4D"
|
// ALT-SHIFT-ARROW "\e[1;4D" "\e[1;10D" "\e[1;4D"
|
||||||
// CTRL-SHIFT-ARROW "\e[1;6D" N/A
|
// CTRL-SHIFT-ARROW "\e[1;6D" N/A
|
||||||
@@ -744,7 +743,6 @@ func (r *LightRenderer) escSequence(sz *int) Event {
|
|||||||
if shift {
|
if shift {
|
||||||
return Event{ShiftUp, 0, nil}
|
return Event{ShiftUp, 0, nil}
|
||||||
}
|
}
|
||||||
return Event{Up, 0, nil}
|
|
||||||
case 'B':
|
case 'B':
|
||||||
if ctrlAltShift {
|
if ctrlAltShift {
|
||||||
return Event{CtrlAltShiftDown, 0, nil}
|
return Event{CtrlAltShiftDown, 0, nil}
|
||||||
@@ -767,7 +765,6 @@ func (r *LightRenderer) escSequence(sz *int) Event {
|
|||||||
if shift {
|
if shift {
|
||||||
return Event{ShiftDown, 0, nil}
|
return Event{ShiftDown, 0, nil}
|
||||||
}
|
}
|
||||||
return Event{Down, 0, nil}
|
|
||||||
case 'C':
|
case 'C':
|
||||||
if ctrlAltShift {
|
if ctrlAltShift {
|
||||||
return Event{CtrlAltShiftRight, 0, nil}
|
return Event{CtrlAltShiftRight, 0, nil}
|
||||||
@@ -790,7 +787,6 @@ func (r *LightRenderer) escSequence(sz *int) Event {
|
|||||||
if alt {
|
if alt {
|
||||||
return Event{AltRight, 0, nil}
|
return Event{AltRight, 0, nil}
|
||||||
}
|
}
|
||||||
return Event{Right, 0, nil}
|
|
||||||
case 'D':
|
case 'D':
|
||||||
if ctrlAltShift {
|
if ctrlAltShift {
|
||||||
return Event{CtrlAltShiftLeft, 0, nil}
|
return Event{CtrlAltShiftLeft, 0, nil}
|
||||||
@@ -813,7 +809,6 @@ func (r *LightRenderer) escSequence(sz *int) Event {
|
|||||||
if shift {
|
if shift {
|
||||||
return Event{ShiftLeft, 0, nil}
|
return Event{ShiftLeft, 0, nil}
|
||||||
}
|
}
|
||||||
return Event{Left, 0, nil}
|
|
||||||
case 'H':
|
case 'H':
|
||||||
if ctrlAltShift {
|
if ctrlAltShift {
|
||||||
return Event{CtrlAltShiftHome, 0, nil}
|
return Event{CtrlAltShiftHome, 0, nil}
|
||||||
@@ -836,7 +831,6 @@ func (r *LightRenderer) escSequence(sz *int) Event {
|
|||||||
if shift {
|
if shift {
|
||||||
return Event{ShiftHome, 0, nil}
|
return Event{ShiftHome, 0, nil}
|
||||||
}
|
}
|
||||||
return Event{Home, 0, nil}
|
|
||||||
case 'F':
|
case 'F':
|
||||||
if ctrlAltShift {
|
if ctrlAltShift {
|
||||||
return Event{CtrlAltShiftEnd, 0, nil}
|
return Event{CtrlAltShiftEnd, 0, nil}
|
||||||
@@ -859,7 +853,6 @@ func (r *LightRenderer) escSequence(sz *int) Event {
|
|||||||
if shift {
|
if shift {
|
||||||
return Event{ShiftEnd, 0, nil}
|
return Event{ShiftEnd, 0, nil}
|
||||||
}
|
}
|
||||||
return Event{End, 0, nil}
|
|
||||||
}
|
}
|
||||||
} // r.buffer[4]
|
} // r.buffer[4]
|
||||||
} // r.buffer[3]
|
} // r.buffer[3]
|
||||||
@@ -1129,144 +1122,127 @@ func (w *LightWindow) DrawHBorder() {
|
|||||||
w.drawBorder(true)
|
w.drawBorder(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// drawHLine fills row `row` with `line` between optional left/right caps.
|
|
||||||
// A zero rune means "no cap"; caps are placed at the very edges of `w`.
|
|
||||||
func (w *LightWindow) drawHLine(row int, line, leftCap, rightCap rune, color ColorPair) {
|
|
||||||
w.Move(row, 0)
|
|
||||||
hw := runeWidth(line)
|
|
||||||
width := w.width
|
|
||||||
if leftCap != 0 {
|
|
||||||
w.CPrint(color, string(leftCap))
|
|
||||||
width -= runeWidth(leftCap)
|
|
||||||
}
|
|
||||||
if rightCap != 0 {
|
|
||||||
width -= runeWidth(rightCap)
|
|
||||||
}
|
|
||||||
if width < 0 {
|
|
||||||
width = 0
|
|
||||||
}
|
|
||||||
inner := width / hw
|
|
||||||
rem := width - inner*hw
|
|
||||||
w.CPrint(color, repeat(line, inner)+repeat(' ', rem))
|
|
||||||
if rightCap != 0 {
|
|
||||||
w.CPrint(color, string(rightCap))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *LightWindow) DrawHSeparator(row int, windowType WindowType, useBottom bool) {
|
|
||||||
if w.height == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
shape := w.border.shape
|
|
||||||
if shape == BorderNone {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
color := BorderColor(windowType)
|
|
||||||
line := w.border.top
|
|
||||||
if useBottom {
|
|
||||||
line = w.border.bottom
|
|
||||||
}
|
|
||||||
var leftCap, rightCap rune
|
|
||||||
if shape.HasLeft() {
|
|
||||||
leftCap = w.border.leftMid
|
|
||||||
}
|
|
||||||
if shape.HasRight() {
|
|
||||||
rightCap = w.border.rightMid
|
|
||||||
}
|
|
||||||
w.drawHLine(row, line, leftCap, rightCap, color)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *LightWindow) PaintSectionFrame(topContent, bottomContent int, windowType WindowType, edge SectionEdge) {
|
|
||||||
if w.height == 0 || w.border.shape == BorderNone {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
color := BorderColor(windowType)
|
|
||||||
shape := w.border.shape
|
|
||||||
hasLeft := shape.HasLeft()
|
|
||||||
hasRight := shape.HasRight()
|
|
||||||
rightW := runeWidth(w.border.right)
|
|
||||||
// Content rows: overpaint left/right verticals + their 1-char margin.
|
|
||||||
for row := topContent; row <= bottomContent; row++ {
|
|
||||||
if hasLeft {
|
|
||||||
w.Move(row, 0)
|
|
||||||
w.CPrint(color, string(w.border.left)+" ")
|
|
||||||
}
|
|
||||||
if hasRight {
|
|
||||||
w.Move(row, w.width-rightW-1)
|
|
||||||
w.CPrint(color, " "+string(w.border.right))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if edge == SectionEdgeTop && shape.HasTop() {
|
|
||||||
var leftCap, rightCap rune
|
|
||||||
if hasLeft {
|
|
||||||
leftCap = w.border.topLeft
|
|
||||||
}
|
|
||||||
if hasRight {
|
|
||||||
rightCap = w.border.topRight
|
|
||||||
}
|
|
||||||
w.drawHLine(0, w.border.top, leftCap, rightCap, color)
|
|
||||||
}
|
|
||||||
if edge == SectionEdgeBottom && shape.HasBottom() {
|
|
||||||
var leftCap, rightCap rune
|
|
||||||
if hasLeft {
|
|
||||||
leftCap = w.border.bottomLeft
|
|
||||||
}
|
|
||||||
if hasRight {
|
|
||||||
rightCap = w.border.bottomRight
|
|
||||||
}
|
|
||||||
w.drawHLine(w.height-1, w.border.bottom, leftCap, rightCap, color)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *LightWindow) drawBorder(onlyHorizontal bool) {
|
func (w *LightWindow) drawBorder(onlyHorizontal bool) {
|
||||||
if w.height == 0 {
|
if w.height == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
shape := w.border.shape
|
switch w.border.shape {
|
||||||
if shape == BorderNone {
|
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble:
|
||||||
return
|
w.drawBorderAround(onlyHorizontal)
|
||||||
|
case BorderHorizontal:
|
||||||
|
w.drawBorderHorizontal(true, true)
|
||||||
|
case BorderVertical:
|
||||||
|
if onlyHorizontal {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.drawBorderVertical(true, true)
|
||||||
|
case BorderTop:
|
||||||
|
w.drawBorderHorizontal(true, false)
|
||||||
|
case BorderBottom:
|
||||||
|
w.drawBorderHorizontal(false, true)
|
||||||
|
case BorderLeft:
|
||||||
|
if onlyHorizontal {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.drawBorderVertical(true, false)
|
||||||
|
case BorderRight:
|
||||||
|
if onlyHorizontal {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.drawBorderVertical(false, true)
|
||||||
}
|
}
|
||||||
color := BorderColor(w.windowType)
|
}
|
||||||
hasLeft := shape.HasLeft()
|
|
||||||
hasRight := shape.HasRight()
|
|
||||||
|
|
||||||
if shape.HasTop() {
|
func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
|
||||||
var leftCap, rightCap rune
|
color := ColBorder
|
||||||
if hasLeft {
|
switch w.windowType {
|
||||||
leftCap = w.border.topLeft
|
case WindowList:
|
||||||
}
|
color = ColListBorder
|
||||||
if hasRight {
|
case WindowInput:
|
||||||
rightCap = w.border.topRight
|
color = ColInputBorder
|
||||||
}
|
case WindowHeader:
|
||||||
w.drawHLine(0, w.border.top, leftCap, rightCap, color)
|
color = ColHeaderBorder
|
||||||
|
case WindowFooter:
|
||||||
|
color = ColFooterBorder
|
||||||
|
case WindowPreview:
|
||||||
|
color = ColPreviewBorder
|
||||||
}
|
}
|
||||||
if !onlyHorizontal && (hasLeft || hasRight) {
|
hw := runeWidth(w.border.top)
|
||||||
|
if top {
|
||||||
|
w.Move(0, 0)
|
||||||
|
w.CPrint(color, repeat(w.border.top, w.width/hw))
|
||||||
|
}
|
||||||
|
|
||||||
|
if bottom {
|
||||||
|
w.Move(w.height-1, 0)
|
||||||
|
w.CPrint(color, repeat(w.border.bottom, w.width/hw))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *LightWindow) drawBorderVertical(left, right bool) {
|
||||||
|
vw := runeWidth(w.border.left)
|
||||||
|
color := ColBorder
|
||||||
|
switch w.windowType {
|
||||||
|
case WindowList:
|
||||||
|
color = ColListBorder
|
||||||
|
case WindowInput:
|
||||||
|
color = ColInputBorder
|
||||||
|
case WindowHeader:
|
||||||
|
color = ColHeaderBorder
|
||||||
|
case WindowFooter:
|
||||||
|
color = ColFooterBorder
|
||||||
|
case WindowPreview:
|
||||||
|
color = ColPreviewBorder
|
||||||
|
}
|
||||||
|
for y := 0; y < w.height; y++ {
|
||||||
|
if left {
|
||||||
|
w.Move(y, 0)
|
||||||
|
w.CPrint(color, string(w.border.left))
|
||||||
|
w.CPrint(color, " ") // Margin
|
||||||
|
}
|
||||||
|
if right {
|
||||||
|
w.Move(y, w.width-vw-1)
|
||||||
|
w.CPrint(color, " ") // Margin
|
||||||
|
w.CPrint(color, string(w.border.right))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *LightWindow) drawBorderAround(onlyHorizontal bool) {
|
||||||
|
w.Move(0, 0)
|
||||||
|
color := ColBorder
|
||||||
|
switch w.windowType {
|
||||||
|
case WindowList:
|
||||||
|
color = ColListBorder
|
||||||
|
case WindowInput:
|
||||||
|
color = ColInputBorder
|
||||||
|
case WindowHeader:
|
||||||
|
color = ColHeaderBorder
|
||||||
|
case WindowFooter:
|
||||||
|
color = ColFooterBorder
|
||||||
|
case WindowPreview:
|
||||||
|
color = ColPreviewBorder
|
||||||
|
}
|
||||||
|
hw := runeWidth(w.border.top)
|
||||||
|
tcw := runeWidth(w.border.topLeft) + runeWidth(w.border.topRight)
|
||||||
|
bcw := runeWidth(w.border.bottomLeft) + runeWidth(w.border.bottomRight)
|
||||||
|
rem := (w.width - tcw) % hw
|
||||||
|
w.CPrint(color, string(w.border.topLeft)+repeat(w.border.top, (w.width-tcw)/hw)+repeat(' ', rem)+string(w.border.topRight))
|
||||||
|
if !onlyHorizontal {
|
||||||
vw := runeWidth(w.border.left)
|
vw := runeWidth(w.border.left)
|
||||||
for y := 0; y < w.height; y++ {
|
for y := 1; y < w.height-1; y++ {
|
||||||
// Corner rows are already painted by drawHLine above / below.
|
w.Move(y, 0)
|
||||||
if (y == 0 && shape.HasTop()) || (y == w.height-1 && shape.HasBottom()) {
|
w.CPrint(color, string(w.border.left))
|
||||||
continue
|
w.CPrint(color, " ") // Margin
|
||||||
}
|
|
||||||
if hasLeft {
|
w.Move(y, w.width-vw-1)
|
||||||
w.Move(y, 0)
|
w.CPrint(color, " ") // Margin
|
||||||
w.CPrint(color, string(w.border.left)+" ")
|
w.CPrint(color, string(w.border.right))
|
||||||
}
|
|
||||||
if hasRight {
|
|
||||||
w.Move(y, w.width-vw-1)
|
|
||||||
w.CPrint(color, " "+string(w.border.right))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if shape.HasBottom() {
|
w.Move(w.height-1, 0)
|
||||||
var leftCap, rightCap rune
|
rem = (w.width - bcw) % hw
|
||||||
if hasLeft {
|
w.CPrint(color, string(w.border.bottomLeft)+repeat(w.border.bottom, (w.width-bcw)/hw)+repeat(' ', rem)+string(w.border.bottomRight))
|
||||||
leftCap = w.border.bottomLeft
|
|
||||||
}
|
|
||||||
if hasRight {
|
|
||||||
rightCap = w.border.bottomRight
|
|
||||||
}
|
|
||||||
w.drawHLine(w.height-1, w.border.bottom, leftCap, rightCap, color)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *LightWindow) csi(code string) string {
|
func (w *LightWindow) csi(code string) string {
|
||||||
|
|||||||
193
src/tui/tcell.go
193
src/tui/tcell.go
@@ -1017,115 +1017,6 @@ func (w *TcellWindow) DrawHBorder() {
|
|||||||
w.drawBorder(true)
|
w.drawBorder(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// borderStyleFor returns the tcell.Style used to draw borders for `wt`, honoring
|
|
||||||
// whether the window is rendering with colors.
|
|
||||||
func (w *TcellWindow) borderStyleFor(wt WindowType) tcell.Style {
|
|
||||||
if !w.color {
|
|
||||||
return w.normal.style()
|
|
||||||
}
|
|
||||||
return BorderColor(wt).style()
|
|
||||||
}
|
|
||||||
|
|
||||||
// drawHLine fills row `y` with `line` between optional left/right caps.
|
|
||||||
// A zero rune means "no cap"; caps are placed at the very edges of `w`.
|
|
||||||
// tcell has an issue displaying two overlapping wide runes, so the line
|
|
||||||
// stops before the cap position rather than overpainting.
|
|
||||||
func (w *TcellWindow) drawHLine(y int, line, leftCap, rightCap rune, style tcell.Style) {
|
|
||||||
left := w.left
|
|
||||||
right := left + w.width
|
|
||||||
hw := runeWidth(line)
|
|
||||||
lw := 0
|
|
||||||
rw := 0
|
|
||||||
if leftCap != 0 {
|
|
||||||
lw = runeWidth(leftCap)
|
|
||||||
}
|
|
||||||
if rightCap != 0 {
|
|
||||||
rw = runeWidth(rightCap)
|
|
||||||
}
|
|
||||||
for x := left + lw; x <= right-rw-hw; x += hw {
|
|
||||||
_screen.SetContent(x, y, line, nil, style)
|
|
||||||
}
|
|
||||||
if leftCap != 0 {
|
|
||||||
_screen.SetContent(left, y, leftCap, nil, style)
|
|
||||||
}
|
|
||||||
if rightCap != 0 {
|
|
||||||
_screen.SetContent(right-rw, y, rightCap, nil, style)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *TcellWindow) DrawHSeparator(row int, windowType WindowType, useBottom bool) {
|
|
||||||
if w.height == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
shape := w.borderStyle.shape
|
|
||||||
if shape == BorderNone {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
style := w.borderStyleFor(windowType)
|
|
||||||
line := w.borderStyle.top
|
|
||||||
if useBottom {
|
|
||||||
line = w.borderStyle.bottom
|
|
||||||
}
|
|
||||||
var leftCap, rightCap rune
|
|
||||||
if shape.HasLeft() {
|
|
||||||
leftCap = w.borderStyle.leftMid
|
|
||||||
}
|
|
||||||
if shape.HasRight() {
|
|
||||||
rightCap = w.borderStyle.rightMid
|
|
||||||
}
|
|
||||||
w.drawHLine(w.top+row, line, leftCap, rightCap, style)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *TcellWindow) PaintSectionFrame(topContent, bottomContent int, windowType WindowType, edge SectionEdge) {
|
|
||||||
if w.height == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
shape := w.borderStyle.shape
|
|
||||||
if shape == BorderNone {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
style := w.borderStyleFor(windowType)
|
|
||||||
left := w.left
|
|
||||||
right := left + w.width
|
|
||||||
hasLeft := shape.HasLeft()
|
|
||||||
hasRight := shape.HasRight()
|
|
||||||
leftW := runeWidth(w.borderStyle.left)
|
|
||||||
rightW := runeWidth(w.borderStyle.right)
|
|
||||||
// Content rows: overpaint the left and right verticals (+ their 1-char margin) in
|
|
||||||
// the section's color. Inner margin stays at whatever bg the sub-window set.
|
|
||||||
for row := topContent; row <= bottomContent; row++ {
|
|
||||||
y := w.top + row
|
|
||||||
if hasLeft {
|
|
||||||
_screen.SetContent(left, y, w.borderStyle.left, nil, style)
|
|
||||||
_screen.SetContent(left+leftW, y, ' ', nil, style)
|
|
||||||
}
|
|
||||||
if hasRight {
|
|
||||||
_screen.SetContent(right-rightW-1, y, ' ', nil, style)
|
|
||||||
_screen.SetContent(right-rightW, y, w.borderStyle.right, nil, style)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if edge == SectionEdgeTop && shape.HasTop() {
|
|
||||||
var leftCap, rightCap rune
|
|
||||||
if hasLeft {
|
|
||||||
leftCap = w.borderStyle.topLeft
|
|
||||||
}
|
|
||||||
if hasRight {
|
|
||||||
rightCap = w.borderStyle.topRight
|
|
||||||
}
|
|
||||||
w.drawHLine(w.top, w.borderStyle.top, leftCap, rightCap, style)
|
|
||||||
}
|
|
||||||
if edge == SectionEdgeBottom && shape.HasBottom() {
|
|
||||||
var leftCap, rightCap rune
|
|
||||||
if hasLeft {
|
|
||||||
leftCap = w.borderStyle.bottomLeft
|
|
||||||
}
|
|
||||||
if hasRight {
|
|
||||||
rightCap = w.borderStyle.bottomRight
|
|
||||||
}
|
|
||||||
w.drawHLine(w.top+w.height-1, w.borderStyle.bottom, leftCap, rightCap, style)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
|
func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
|
||||||
if w.height == 0 {
|
if w.height == 0 {
|
||||||
return
|
return
|
||||||
@@ -1140,44 +1031,72 @@ func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
|
|||||||
top := w.top
|
top := w.top
|
||||||
bot := top + w.height
|
bot := top + w.height
|
||||||
|
|
||||||
style := w.borderStyleFor(w.windowType)
|
var style tcell.Style
|
||||||
|
if w.color {
|
||||||
hasLeft := shape.HasLeft()
|
switch w.windowType {
|
||||||
hasRight := shape.HasRight()
|
case WindowBase:
|
||||||
|
style = ColBorder.style()
|
||||||
if shape.HasTop() {
|
case WindowList:
|
||||||
var leftCap, rightCap rune
|
style = ColListBorder.style()
|
||||||
if hasLeft {
|
case WindowHeader:
|
||||||
leftCap = w.borderStyle.topLeft
|
style = ColHeaderBorder.style()
|
||||||
|
case WindowFooter:
|
||||||
|
style = ColFooterBorder.style()
|
||||||
|
case WindowInput:
|
||||||
|
style = ColInputBorder.style()
|
||||||
|
case WindowPreview:
|
||||||
|
style = ColPreviewBorder.style()
|
||||||
}
|
}
|
||||||
if hasRight {
|
} else {
|
||||||
rightCap = w.borderStyle.topRight
|
style = w.normal.style()
|
||||||
}
|
|
||||||
w.drawHLine(top, w.borderStyle.top, leftCap, rightCap, style)
|
|
||||||
}
|
}
|
||||||
if shape.HasBottom() {
|
|
||||||
var leftCap, rightCap rune
|
hw := runeWidth(w.borderStyle.top)
|
||||||
if hasLeft {
|
switch shape {
|
||||||
leftCap = w.borderStyle.bottomLeft
|
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderHorizontal, BorderTop:
|
||||||
|
max := right - 2*hw
|
||||||
|
if shape == BorderHorizontal || shape == BorderTop {
|
||||||
|
max = right - hw
|
||||||
}
|
}
|
||||||
if hasRight {
|
// tcell has an issue displaying two overlapping wide runes
|
||||||
rightCap = w.borderStyle.bottomRight
|
// e.g. SetContent( HH )
|
||||||
|
// SetContent( TR )
|
||||||
|
// ==================
|
||||||
|
// ( HH ) => TR is ignored
|
||||||
|
for x := left; x <= max; x += hw {
|
||||||
|
_screen.SetContent(x, top, w.borderStyle.top, nil, style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch shape {
|
||||||
|
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderHorizontal, BorderBottom:
|
||||||
|
max := right - 2*hw
|
||||||
|
if shape == BorderHorizontal || shape == BorderBottom {
|
||||||
|
max = right - hw
|
||||||
|
}
|
||||||
|
for x := left; x <= max; x += hw {
|
||||||
|
_screen.SetContent(x, bot-1, w.borderStyle.bottom, nil, style)
|
||||||
}
|
}
|
||||||
w.drawHLine(bot-1, w.borderStyle.bottom, leftCap, rightCap, style)
|
|
||||||
}
|
}
|
||||||
if !onlyHorizontal {
|
if !onlyHorizontal {
|
||||||
vw := runeWidth(w.borderStyle.right)
|
switch shape {
|
||||||
for y := top; y < bot; y++ {
|
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderVertical, BorderLeft:
|
||||||
// Corner rows are already painted by drawHLine above / below.
|
for y := top; y < bot; y++ {
|
||||||
if (y == top && shape.HasTop()) || (y == bot-1 && shape.HasBottom()) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if hasLeft {
|
|
||||||
_screen.SetContent(left, y, w.borderStyle.left, nil, style)
|
_screen.SetContent(left, y, w.borderStyle.left, nil, style)
|
||||||
}
|
}
|
||||||
if hasRight {
|
}
|
||||||
|
switch shape {
|
||||||
|
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderVertical, BorderRight:
|
||||||
|
vw := runeWidth(w.borderStyle.right)
|
||||||
|
for y := top; y < bot; y++ {
|
||||||
_screen.SetContent(right-vw, y, w.borderStyle.right, nil, style)
|
_screen.SetContent(right-vw, y, w.borderStyle.right, nil, style)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
switch shape {
|
||||||
|
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble:
|
||||||
|
_screen.SetContent(left, top, w.borderStyle.topLeft, nil, style)
|
||||||
|
_screen.SetContent(right-runeWidth(w.borderStyle.topRight), top, w.borderStyle.topRight, nil, style)
|
||||||
|
_screen.SetContent(left, bot-1, w.borderStyle.bottomLeft, nil, style)
|
||||||
|
_screen.SetContent(right-runeWidth(w.borderStyle.bottomRight), bot-1, w.borderStyle.bottomRight, nil, style)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -595,12 +595,11 @@ const (
|
|||||||
BorderBottom
|
BorderBottom
|
||||||
BorderLeft
|
BorderLeft
|
||||||
BorderRight
|
BorderRight
|
||||||
BorderInline
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s BorderShape) HasLeft() bool {
|
func (s BorderShape) HasLeft() bool {
|
||||||
switch s {
|
switch s {
|
||||||
case BorderNone, BorderPhantom, BorderLine, BorderInline, BorderRight, BorderTop, BorderBottom, BorderHorizontal: // No Left
|
case BorderNone, BorderPhantom, BorderLine, BorderRight, BorderTop, BorderBottom, BorderHorizontal: // No Left
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -608,7 +607,7 @@ func (s BorderShape) HasLeft() bool {
|
|||||||
|
|
||||||
func (s BorderShape) HasRight() bool {
|
func (s BorderShape) HasRight() bool {
|
||||||
switch s {
|
switch s {
|
||||||
case BorderNone, BorderPhantom, BorderLine, BorderInline, BorderLeft, BorderTop, BorderBottom, BorderHorizontal: // No right
|
case BorderNone, BorderPhantom, BorderLine, BorderLeft, BorderTop, BorderBottom, BorderHorizontal: // No right
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -616,7 +615,7 @@ func (s BorderShape) HasRight() bool {
|
|||||||
|
|
||||||
func (s BorderShape) HasTop() bool {
|
func (s BorderShape) HasTop() bool {
|
||||||
switch s {
|
switch s {
|
||||||
case BorderNone, BorderPhantom, BorderLine, BorderInline, BorderLeft, BorderRight, BorderBottom, BorderVertical: // No top
|
case BorderNone, BorderPhantom, BorderLine, BorderLeft, BorderRight, BorderBottom, BorderVertical: // No top
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -624,7 +623,7 @@ func (s BorderShape) HasTop() bool {
|
|||||||
|
|
||||||
func (s BorderShape) HasBottom() bool {
|
func (s BorderShape) HasBottom() bool {
|
||||||
switch s {
|
switch s {
|
||||||
case BorderNone, BorderPhantom, BorderLine, BorderInline, BorderLeft, BorderRight, BorderTop, BorderVertical: // No bottom
|
case BorderNone, BorderPhantom, BorderLine, BorderLeft, BorderRight, BorderTop, BorderVertical: // No bottom
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -644,8 +643,6 @@ type BorderStyle struct {
|
|||||||
topRight rune
|
topRight rune
|
||||||
bottomLeft rune
|
bottomLeft rune
|
||||||
bottomRight rune
|
bottomRight rune
|
||||||
leftMid rune
|
|
||||||
rightMid rune
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type BorderCharacter int
|
type BorderCharacter int
|
||||||
@@ -661,9 +658,7 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
|||||||
topLeft: ' ',
|
topLeft: ' ',
|
||||||
topRight: ' ',
|
topRight: ' ',
|
||||||
bottomLeft: ' ',
|
bottomLeft: ' ',
|
||||||
bottomRight: ' ',
|
bottomRight: ' '}
|
||||||
leftMid: ' ',
|
|
||||||
rightMid: ' '}
|
|
||||||
}
|
}
|
||||||
if !unicode {
|
if !unicode {
|
||||||
return BorderStyle{
|
return BorderStyle{
|
||||||
@@ -676,8 +671,6 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
|||||||
topRight: '+',
|
topRight: '+',
|
||||||
bottomLeft: '+',
|
bottomLeft: '+',
|
||||||
bottomRight: '+',
|
bottomRight: '+',
|
||||||
leftMid: '+',
|
|
||||||
rightMid: '+',
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch shape {
|
switch shape {
|
||||||
@@ -692,8 +685,6 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
|||||||
topRight: '┐',
|
topRight: '┐',
|
||||||
bottomLeft: '└',
|
bottomLeft: '└',
|
||||||
bottomRight: '┘',
|
bottomRight: '┘',
|
||||||
leftMid: '├',
|
|
||||||
rightMid: '┤',
|
|
||||||
}
|
}
|
||||||
case BorderBold:
|
case BorderBold:
|
||||||
return BorderStyle{
|
return BorderStyle{
|
||||||
@@ -706,8 +697,6 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
|||||||
topRight: '┓',
|
topRight: '┓',
|
||||||
bottomLeft: '┗',
|
bottomLeft: '┗',
|
||||||
bottomRight: '┛',
|
bottomRight: '┛',
|
||||||
leftMid: '┣',
|
|
||||||
rightMid: '┫',
|
|
||||||
}
|
}
|
||||||
case BorderBlock:
|
case BorderBlock:
|
||||||
// ▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜
|
// ▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜
|
||||||
@@ -723,8 +712,6 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
|||||||
topRight: '▜',
|
topRight: '▜',
|
||||||
bottomLeft: '▙',
|
bottomLeft: '▙',
|
||||||
bottomRight: '▟',
|
bottomRight: '▟',
|
||||||
leftMid: '▌',
|
|
||||||
rightMid: '▐',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case BorderThinBlock:
|
case BorderThinBlock:
|
||||||
@@ -741,8 +728,6 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
|||||||
topRight: '🭾',
|
topRight: '🭾',
|
||||||
bottomLeft: '🭼',
|
bottomLeft: '🭼',
|
||||||
bottomRight: '🭿',
|
bottomRight: '🭿',
|
||||||
leftMid: '▏',
|
|
||||||
rightMid: '▕',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case BorderDouble:
|
case BorderDouble:
|
||||||
@@ -756,8 +741,6 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
|||||||
topRight: '╗',
|
topRight: '╗',
|
||||||
bottomLeft: '╚',
|
bottomLeft: '╚',
|
||||||
bottomRight: '╝',
|
bottomRight: '╝',
|
||||||
leftMid: '╠',
|
|
||||||
rightMid: '╣',
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return BorderStyle{
|
return BorderStyle{
|
||||||
@@ -770,8 +753,6 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
|||||||
topRight: '╮',
|
topRight: '╮',
|
||||||
bottomLeft: '╰',
|
bottomLeft: '╰',
|
||||||
bottomRight: '╯',
|
bottomRight: '╯',
|
||||||
leftMid: '├',
|
|
||||||
rightMid: '┤',
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -793,35 +774,6 @@ const (
|
|||||||
WindowFooter
|
WindowFooter
|
||||||
)
|
)
|
||||||
|
|
||||||
// BorderColor returns the ColorPair used to draw borders for the given WindowType.
|
|
||||||
func BorderColor(wt WindowType) ColorPair {
|
|
||||||
switch wt {
|
|
||||||
case WindowList:
|
|
||||||
return ColListBorder
|
|
||||||
case WindowInput:
|
|
||||||
return ColInputBorder
|
|
||||||
case WindowHeader:
|
|
||||||
return ColHeaderBorder
|
|
||||||
case WindowFooter:
|
|
||||||
return ColFooterBorder
|
|
||||||
case WindowPreview:
|
|
||||||
return ColPreviewBorder
|
|
||||||
}
|
|
||||||
return ColBorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// SectionEdge selects which outer edge of the frame an inline section
|
|
||||||
// should claim when PaintSectionFrame overpaints its adjacent border.
|
|
||||||
// SectionEdgeNone paints only the inner verticals (for sections that
|
|
||||||
// don't touch the outer top or bottom).
|
|
||||||
type SectionEdge int
|
|
||||||
|
|
||||||
const (
|
|
||||||
SectionEdgeNone SectionEdge = iota
|
|
||||||
SectionEdgeTop
|
|
||||||
SectionEdgeBottom
|
|
||||||
)
|
|
||||||
|
|
||||||
type Renderer interface {
|
type Renderer interface {
|
||||||
DefaultTheme() *ColorTheme
|
DefaultTheme() *ColorTheme
|
||||||
Init() error
|
Init() error
|
||||||
@@ -859,19 +811,6 @@ type Window interface {
|
|||||||
|
|
||||||
DrawBorder()
|
DrawBorder()
|
||||||
DrawHBorder()
|
DrawHBorder()
|
||||||
// DrawHSeparator draws an inline horizontal separator at `row` (relative to the
|
|
||||||
// window's top) using the color for `windowType`. The separator is conceptually
|
|
||||||
// the section's inner edge (e.g. the bottom border of an inline header), so the
|
|
||||||
// whole row including junctions carries the section's fg + bg. When useBottom is
|
|
||||||
// true the `bottom` horizontal char is used instead of `top`; for thinblock/block
|
|
||||||
// styles this keeps the thin line bonded to the list content on the opposite side.
|
|
||||||
DrawHSeparator(row int, windowType WindowType, useBottom bool)
|
|
||||||
// PaintSectionFrame overpaints the border cells around the rows [topContent,
|
|
||||||
// bottomContent] (inclusive, relative to the window's top) with the color for
|
|
||||||
// `windowType`. When edge is SectionEdgeTop / SectionEdgeBottom, the
|
|
||||||
// corresponding outer horizontal (+ corners) is also painted, letting the
|
|
||||||
// inline section claim that edge of the outer frame.
|
|
||||||
PaintSectionFrame(topContent, bottomContent int, windowType WindowType, edge SectionEdge)
|
|
||||||
Refresh()
|
Refresh()
|
||||||
FinishFill()
|
FinishFill()
|
||||||
|
|
||||||
@@ -1227,7 +1166,7 @@ func init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, boldify bool, forceBlack bool, hasInputWindow bool, hasHeaderWindow bool, headerInline bool, footerInline bool) {
|
func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, boldify bool, forceBlack bool, hasInputWindow bool, hasHeaderWindow bool) {
|
||||||
if forceBlack {
|
if forceBlack {
|
||||||
theme.Bg = ColorAttr{colBlack, AttrUndefined}
|
theme.Bg = ColorAttr{colBlack, AttrUndefined}
|
||||||
}
|
}
|
||||||
@@ -1361,22 +1300,11 @@ func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, boldify bool, forceBlac
|
|||||||
} else {
|
} else {
|
||||||
theme.HeaderBg = o(theme.Bg, theme.ListBg)
|
theme.HeaderBg = o(theme.Bg, theme.ListBg)
|
||||||
}
|
}
|
||||||
// Inline header/footer borders sit inside the list frame, so default their color
|
theme.HeaderBorder = o(theme.Border, theme.HeaderBorder)
|
||||||
// to the list-border color when the user has not explicitly set it. The inline
|
|
||||||
// separator then matches the surrounding frame.
|
|
||||||
headerBorderFallback := theme.Border
|
|
||||||
if headerInline {
|
|
||||||
headerBorderFallback = theme.ListBorder
|
|
||||||
}
|
|
||||||
theme.HeaderBorder = o(headerBorderFallback, theme.HeaderBorder)
|
|
||||||
theme.HeaderLabel = o(theme.BorderLabel, theme.HeaderLabel)
|
theme.HeaderLabel = o(theme.BorderLabel, theme.HeaderLabel)
|
||||||
|
|
||||||
theme.FooterBg = o(theme.Bg, theme.FooterBg)
|
theme.FooterBg = o(theme.Bg, theme.FooterBg)
|
||||||
footerBorderFallback := theme.Border
|
theme.FooterBorder = o(theme.Border, theme.FooterBorder)
|
||||||
if footerInline {
|
|
||||||
footerBorderFallback = theme.ListBorder
|
|
||||||
}
|
|
||||||
theme.FooterBorder = o(footerBorderFallback, theme.FooterBorder)
|
|
||||||
theme.FooterLabel = o(theme.BorderLabel, theme.FooterLabel)
|
theme.FooterLabel = o(theme.BorderLabel, theme.FooterLabel)
|
||||||
|
|
||||||
if theme.Nomatch.IsUndefined() {
|
if theme.Nomatch.IsUndefined() {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ func TestWrapLine(t *testing.T) {
|
|||||||
t.Errorf("Basic wrap: %v", lines)
|
t.Errorf("Basic wrap: %v", lines)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exact fit - no wrapping needed
|
// Exact fit — no wrapping needed
|
||||||
lines = WrapLine("hello", 0, 5, 8, 2)
|
lines = WrapLine("hello", 0, 5, 8, 2)
|
||||||
if len(lines) != 1 || lines[0].Text != "hello" || lines[0].DisplayWidth != 5 {
|
if len(lines) != 1 || lines[0].Text != "hello" || lines[0].DisplayWidth != 5 {
|
||||||
t.Errorf("Exact fit: %v", lines)
|
t.Errorf("Exact fit: %v", lines)
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"golang.org/x/sys/windows"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type shellType int
|
type shellType int
|
||||||
@@ -21,7 +19,6 @@ const (
|
|||||||
shellTypeUnknown shellType = iota
|
shellTypeUnknown shellType = iota
|
||||||
shellTypeCmd
|
shellTypeCmd
|
||||||
shellTypePowerShell
|
shellTypePowerShell
|
||||||
shellTypePwsh
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var escapeRegex = regexp.MustCompile(`[&|<>()^%!"]`)
|
var escapeRegex = regexp.MustCompile(`[&|<>()^%!"]`)
|
||||||
@@ -49,10 +46,7 @@ func NewExecutor(withShell string) *Executor {
|
|||||||
} else if strings.HasPrefix(basename, "cmd") {
|
} else if strings.HasPrefix(basename, "cmd") {
|
||||||
shellType = shellTypeCmd
|
shellType = shellTypeCmd
|
||||||
args = []string{"/s/c"}
|
args = []string{"/s/c"}
|
||||||
} else if strings.HasPrefix(basename, "pwsh") {
|
} else if strings.HasPrefix(basename, "pwsh") || strings.HasPrefix(basename, "powershell") {
|
||||||
shellType = shellTypePwsh
|
|
||||||
args = []string{"-NoProfile", "-Command"}
|
|
||||||
} else if strings.HasPrefix(basename, "powershell") {
|
|
||||||
shellType = shellTypePowerShell
|
shellType = shellTypePowerShell
|
||||||
args = []string{"-NoProfile", "-Command"}
|
args = []string{"-NoProfile", "-Command"}
|
||||||
} else {
|
} else {
|
||||||
@@ -62,12 +56,8 @@ func NewExecutor(withShell string) *Executor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ExecCommand executes the given command with $SHELL
|
// ExecCommand executes the given command with $SHELL
|
||||||
//
|
// FIXME: setpgid is unused. We set it in the Unix implementation so that we
|
||||||
// On Windows, setpgid controls whether the spawned process is placed in a new
|
// can kill preview process with its child processes at once.
|
||||||
// process group (so that it can be signaled independently, e.g. for previews).
|
|
||||||
// However, we only do this for "pwsh" and non-standard shells, because cmd.exe
|
|
||||||
// and Windows PowerShell ("powershell.exe") don't always exit on Ctrl-Break.
|
|
||||||
//
|
|
||||||
// NOTE: For "powershell", we should ideally set output encoding to UTF8,
|
// NOTE: For "powershell", we should ideally set output encoding to UTF8,
|
||||||
// but it is left as is now because no adverse effect has been observed.
|
// but it is left as is now because no adverse effect has been observed.
|
||||||
func (x *Executor) ExecCommand(command string, setpgid bool) *exec.Cmd {
|
func (x *Executor) ExecCommand(command string, setpgid bool) *exec.Cmd {
|
||||||
@@ -83,31 +73,19 @@ func (x *Executor) ExecCommand(command string, setpgid bool) *exec.Cmd {
|
|||||||
}
|
}
|
||||||
x.shellPath.Store(shell)
|
x.shellPath.Store(shell)
|
||||||
}
|
}
|
||||||
|
|
||||||
var creationFlags uint32
|
|
||||||
// Set new process group for pwsh (PowerShell 7+) and unknown/posix-ish shells
|
|
||||||
if setpgid && (x.shellType == shellTypePwsh || x.shellType == shellTypeUnknown) {
|
|
||||||
creationFlags = windows.CREATE_NEW_PROCESS_GROUP
|
|
||||||
}
|
|
||||||
|
|
||||||
var cmd *exec.Cmd
|
var cmd *exec.Cmd
|
||||||
if x.shellType == shellTypeCmd {
|
if x.shellType == shellTypeCmd {
|
||||||
cmd = exec.Command(shell)
|
cmd = exec.Command(shell)
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
HideWindow: false,
|
HideWindow: false,
|
||||||
CmdLine: fmt.Sprintf(`%s "%s"`, strings.Join(x.args, " "), command),
|
CmdLine: fmt.Sprintf(`%s "%s"`, strings.Join(x.args, " "), command),
|
||||||
CreationFlags: creationFlags,
|
CreationFlags: 0,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
args := x.args
|
cmd = exec.Command(shell, append(x.args, command)...)
|
||||||
if setpgid && x.shellType == shellTypePwsh {
|
|
||||||
// pwsh needs -NonInteractive flag to exit on Ctrl-Break
|
|
||||||
args = append([]string{"-NonInteractive"}, x.args...)
|
|
||||||
}
|
|
||||||
cmd = exec.Command(shell, append(args, command)...)
|
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
HideWindow: false,
|
HideWindow: false,
|
||||||
CreationFlags: creationFlags,
|
CreationFlags: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return cmd
|
return cmd
|
||||||
@@ -178,7 +156,7 @@ func (x *Executor) QuoteEntry(entry string) string {
|
|||||||
fd -H --no-ignore -td -d 4 | fzf --preview ".\eza.exe --color=always --tree --level=3 --icons=always {}" --with-shell "powershell -NoProfile -Command"
|
fd -H --no-ignore -td -d 4 | fzf --preview ".\eza.exe --color=always --tree --level=3 --icons=always {}" --with-shell "powershell -NoProfile -Command"
|
||||||
*/
|
*/
|
||||||
return escapeArg(entry)
|
return escapeArg(entry)
|
||||||
case shellTypePowerShell, shellTypePwsh:
|
case shellTypePowerShell:
|
||||||
escaped := strings.ReplaceAll(entry, `"`, `\"`)
|
escaped := strings.ReplaceAll(entry, `"`, `\"`)
|
||||||
return "'" + strings.ReplaceAll(escaped, "'", "''") + "'"
|
return "'" + strings.ReplaceAll(escaped, "'", "''") + "'"
|
||||||
default:
|
default:
|
||||||
@@ -188,21 +166,6 @@ func (x *Executor) QuoteEntry(entry string) string {
|
|||||||
|
|
||||||
// KillCommand kills the process for the given command
|
// KillCommand kills the process for the given command
|
||||||
func KillCommand(cmd *exec.Cmd) error {
|
func KillCommand(cmd *exec.Cmd) error {
|
||||||
// Safely handle nil command or process.
|
|
||||||
if cmd == nil || cmd.Process == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// If it has its own process group, we can send it Ctrl-Break
|
|
||||||
if cmd.SysProcAttr != nil && cmd.SysProcAttr.CreationFlags&windows.CREATE_NEW_PROCESS_GROUP != 0 {
|
|
||||||
if err := windows.GenerateConsoleCtrlEvent(windows.CTRL_BREAK_EVENT, uint32(cmd.Process.Pid)); err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If it's the same process group, or if sending the console control event
|
|
||||||
// fails (e.g., no console, different console, or process already exited),
|
|
||||||
// fall back to a standard kill. This probably won't *help* if there's I/O
|
|
||||||
// going on, because Wait() will still hang until the I/O finishes unless we
|
|
||||||
// hard-kill the entire process group. But it doesn't hurt to try!
|
|
||||||
return cmd.Process.Kill()
|
return cmd.Process.Kill()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ set -e FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS FZF_DEFAULT_OPTS_FILE FZF_TMUX FZF_T
|
|||||||
set -e FZF_CTRL_T_COMMAND FZF_CTRL_T_OPTS FZF_ALT_C_COMMAND FZF_ALT_C_OPTS FZF_CTRL_R_OPTS
|
set -e FZF_CTRL_T_COMMAND FZF_CTRL_T_OPTS FZF_ALT_C_COMMAND FZF_ALT_C_OPTS FZF_CTRL_R_OPTS
|
||||||
set -e FZF_API_KEY
|
set -e FZF_API_KEY
|
||||||
# Unset completion-specific variables
|
# Unset completion-specific variables
|
||||||
set -e FZF_COMPLETION_OPTS FZF_EXPANSION_OPTS
|
set -e FZF_COMPLETION_TRIGGER FZF_COMPLETION_OPTS
|
||||||
|
|
||||||
set -gx FZF_DEFAULT_OPTS "--no-scrollbar --pointer '>' --marker '>'"
|
set -gx FZF_DEFAULT_OPTS "--no-scrollbar --pointer '>' --marker '>'"
|
||||||
|
set -gx FZF_COMPLETION_TRIGGER '++'
|
||||||
set -gx fish_history fzf_test
|
set -gx fish_history fzf_test
|
||||||
|
|
||||||
# Add fzf to PATH
|
# Add fzf to PATH
|
||||||
|
|||||||
@@ -105,23 +105,6 @@ class Tmux
|
|||||||
go(%W[send-keys -t #{win}] + args.map(&:to_s))
|
go(%W[send-keys -t #{win}] + args.map(&:to_s))
|
||||||
end
|
end
|
||||||
|
|
||||||
# Simulate a mouse click at the given 1-based column and row using the SGR mouse protocol
|
|
||||||
# (xterm mouse mode 1006, which fzf enables). The escape sequence is injected as literal
|
|
||||||
# keystrokes via tmux, and fzf parses it like a real terminal mouse event.
|
|
||||||
#
|
|
||||||
# tmux's own mouse handling intercepts these sequences when `set -g mouse on`, so we toggle
|
|
||||||
# mouse off for the duration of the click and restore the previous state afterwards.
|
|
||||||
def click(col, row, button: 0)
|
|
||||||
prev = go(%w[show-options -gv mouse]).first
|
|
||||||
go(%w[set-option -g mouse off])
|
|
||||||
begin
|
|
||||||
seq = "\e[<#{button};#{col};#{row}M\e[<#{button};#{col};#{row}m"
|
|
||||||
go(%W[send-keys -t #{win} -l #{seq}])
|
|
||||||
ensure
|
|
||||||
go(%W[set-option -g mouse #{prev}]) if prev && !prev.empty?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def paste(str)
|
def paste(str)
|
||||||
system('tmux', 'setb', str, ';', 'pasteb', '-t', win, ';', 'send-keys', '-t', win, 'Enter')
|
system('tmux', 'setb', str, ';', 'pasteb', '-t', win, ';', 'send-keys', '-t', win, 'Enter')
|
||||||
end
|
end
|
||||||
@@ -130,71 +113,6 @@ class Tmux
|
|||||||
go(%W[capture-pane -p -J -t #{win}]).map(&:rstrip).reverse.drop_while(&:empty?).reverse
|
go(%W[capture-pane -p -J -t #{win}]).map(&:rstrip).reverse.drop_while(&:empty?).reverse
|
||||||
end
|
end
|
||||||
|
|
||||||
# Raw pane capture with ANSI escape sequences preserved.
|
|
||||||
def capture_ansi
|
|
||||||
go(%W[capture-pane -p -J -e -t #{win}])
|
|
||||||
end
|
|
||||||
|
|
||||||
# 3-bit ANSI bg code (40..47) -> color name used in --color options.
|
|
||||||
BG_NAMES = %w[black red green yellow blue magenta cyan white].freeze
|
|
||||||
|
|
||||||
# Parse `tmux capture-pane -e` output into per-row bg ranges. Each row is an
|
|
||||||
# array of [col_start, col_end, bg] tuples where bg is one of:
|
|
||||||
# 'default'
|
|
||||||
# 'red' / 'green' / 'blue' / ... (3-bit names)
|
|
||||||
# 'bright-red' / ... (bright variants)
|
|
||||||
# '256:<n>' (256-color fallback)
|
|
||||||
# ANSI state persists across rows, matching real terminal behavior.
|
|
||||||
def bg_ranges
|
|
||||||
raw = go(%W[capture-pane -p -J -e -t #{win}])
|
|
||||||
bg = 'default'
|
|
||||||
raw.map do |row|
|
|
||||||
cells = []
|
|
||||||
i = 0
|
|
||||||
len = row.length
|
|
||||||
while i < len
|
|
||||||
c = row[i]
|
|
||||||
if c == "\e" && row[i + 1] == '['
|
|
||||||
j = i + 2
|
|
||||||
j += 1 while j < len && row[j] != 'm'
|
|
||||||
parts = row[i + 2...j].split(';')
|
|
||||||
k = 0
|
|
||||||
while k < parts.length
|
|
||||||
p = parts[k].to_i
|
|
||||||
case p
|
|
||||||
when 0, 49 then bg = 'default'
|
|
||||||
when 40..47 then bg = BG_NAMES[p - 40]
|
|
||||||
when 100..107 then bg = "bright-#{BG_NAMES[p - 100]}"
|
|
||||||
when 48
|
|
||||||
if parts[k + 1] == '5'
|
|
||||||
bg = "256:#{parts[k + 2]}"
|
|
||||||
k += 2
|
|
||||||
elsif parts[k + 1] == '2'
|
|
||||||
bg = "rgb:#{parts[k + 2]}:#{parts[k + 3]}:#{parts[k + 4]}"
|
|
||||||
k += 4
|
|
||||||
end
|
|
||||||
end
|
|
||||||
k += 1
|
|
||||||
end
|
|
||||||
i = j + 1
|
|
||||||
else
|
|
||||||
cells << bg
|
|
||||||
i += 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
ranges = []
|
|
||||||
start = 0
|
|
||||||
cells.each_with_index do |b, idx|
|
|
||||||
if idx.positive? && b != cells[idx - 1]
|
|
||||||
ranges << [start, idx - 1, cells[idx - 1]]
|
|
||||||
start = idx
|
|
||||||
end
|
|
||||||
end
|
|
||||||
ranges << [start, cells.length - 1, cells.last] unless cells.empty?
|
|
||||||
ranges
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def until(refresh = false, timeout: DEFAULT_TIMEOUT)
|
def until(refresh = false, timeout: DEFAULT_TIMEOUT)
|
||||||
lines = nil
|
lines = nil
|
||||||
begin
|
begin
|
||||||
|
|||||||
@@ -1672,7 +1672,7 @@ class TestCore < TestInteractive
|
|||||||
end
|
end
|
||||||
tmux.send_keys :BSpace, :BSpace, :BSpace
|
tmux.send_keys :BSpace, :BSpace, :BSpace
|
||||||
|
|
||||||
# Reload with shuffled order - cursor should track "555"
|
# Reload with shuffled order — cursor should track "555"
|
||||||
tmux.send_keys 'C-r'
|
tmux.send_keys 'C-r'
|
||||||
tmux.until do |lines|
|
tmux.until do |lines|
|
||||||
assert_equal 1000, lines.match_count
|
assert_equal 1000, lines.match_count
|
||||||
@@ -1694,7 +1694,7 @@ class TestCore < TestInteractive
|
|||||||
tmux.send_keys :Up
|
tmux.send_keys :Up
|
||||||
tmux.until { |lines| assert_includes lines, '> 2 banana' }
|
tmux.until { |lines| assert_includes lines, '> 2 banana' }
|
||||||
|
|
||||||
# Reload - the second field changes, but first field "2" stays
|
# Reload — the second field changes, but first field "2" stays
|
||||||
tmux.send_keys 'C-r'
|
tmux.send_keys 'C-r'
|
||||||
tmux.until do |lines|
|
tmux.until do |lines|
|
||||||
assert_equal 3, lines.match_count
|
assert_equal 3, lines.match_count
|
||||||
@@ -1709,7 +1709,7 @@ class TestCore < TestInteractive
|
|||||||
tmux.send_keys :Up
|
tmux.send_keys :Up
|
||||||
tmux.until { |lines| assert_includes lines, '> beta' }
|
tmux.until { |lines| assert_includes lines, '> beta' }
|
||||||
|
|
||||||
# Reload with completely different items - no match for "beta"
|
# Reload with completely different items — no match for "beta"
|
||||||
# Cursor stays at the same position (second item)
|
# Cursor stays at the same position (second item)
|
||||||
tmux.send_keys 'C-r'
|
tmux.send_keys 'C-r'
|
||||||
tmux.until do |lines|
|
tmux.until do |lines|
|
||||||
@@ -1727,7 +1727,7 @@ class TestCore < TestInteractive
|
|||||||
assert_includes lines[-2], '+T'
|
assert_includes lines[-2], '+T'
|
||||||
end
|
end
|
||||||
|
|
||||||
# Trigger slow reload - should show +T* while blocked
|
# Trigger slow reload — should show +T* while blocked
|
||||||
tmux.send_keys 'C-r'
|
tmux.send_keys 'C-r'
|
||||||
tmux.until { |lines| assert_includes lines[-2], '+T*' }
|
tmux.until { |lines| assert_includes lines[-2], '+T*' }
|
||||||
|
|
||||||
@@ -1769,7 +1769,7 @@ class TestCore < TestInteractive
|
|||||||
assert_includes lines, '> 1'
|
assert_includes lines, '> 1'
|
||||||
end
|
end
|
||||||
|
|
||||||
# Trigger reload - blocked during initial sleep
|
# Trigger reload — blocked during initial sleep
|
||||||
tmux.send_keys 'C-r'
|
tmux.send_keys 'C-r'
|
||||||
tmux.until { |lines| assert_includes lines[-2], '+T*' }
|
tmux.until { |lines| assert_includes lines[-2], '+T*' }
|
||||||
# Match "1" arrives, unblocks before the remaining items load
|
# Match "1" arrives, unblocks before the remaining items load
|
||||||
@@ -1790,7 +1790,7 @@ class TestCore < TestInteractive
|
|||||||
assert_includes lines, '> 1'
|
assert_includes lines, '> 1'
|
||||||
end
|
end
|
||||||
|
|
||||||
# Trigger reload-sync - every observable state must be either:
|
# Trigger reload-sync — every observable state must be either:
|
||||||
# 1. +T* (still blocked), or
|
# 1. +T* (still blocked), or
|
||||||
# 2. final state (count=10, +T without *)
|
# 2. final state (count=10, +T without *)
|
||||||
# Any other combination (e.g. unblocked while count < 10) is a bug.
|
# Any other combination (e.g. unblocked while count < 10) is a bug.
|
||||||
@@ -1835,7 +1835,7 @@ class TestCore < TestInteractive
|
|||||||
tmux.send_keys :Up
|
tmux.send_keys :Up
|
||||||
tmux.until { |lines| assert_includes lines, '> beta' }
|
tmux.until { |lines| assert_includes lines, '> beta' }
|
||||||
|
|
||||||
# Reload with completely different items - no match for "beta"
|
# Reload with completely different items — no match for "beta"
|
||||||
tmux.send_keys 'C-r'
|
tmux.send_keys 'C-r'
|
||||||
tmux.until { |lines| assert_includes lines[-2], '+T*' }
|
tmux.until { |lines| assert_includes lines[-2], '+T*' }
|
||||||
# After stream completes, unblocks with cursor at same position (second item)
|
# After stream completes, unblocks with cursor at same position (second item)
|
||||||
@@ -1857,7 +1857,7 @@ class TestCore < TestInteractive
|
|||||||
tmux.send_keys 'C-t'
|
tmux.send_keys 'C-t'
|
||||||
tmux.until { |lines| assert_includes lines[-2], '+t' }
|
tmux.until { |lines| assert_includes lines[-2], '+t' }
|
||||||
|
|
||||||
# Reload - should track by field "2"
|
# Reload — should track by field "2"
|
||||||
tmux.send_keys 'C-r'
|
tmux.send_keys 'C-r'
|
||||||
tmux.until do |lines|
|
tmux.until do |lines|
|
||||||
assert_equal 3, lines.match_count
|
assert_equal 3, lines.match_count
|
||||||
@@ -1876,7 +1876,7 @@ class TestCore < TestInteractive
|
|||||||
tmux.send_keys :Up, :Up, :Tab
|
tmux.send_keys :Up, :Up, :Tab
|
||||||
tmux.until { |lines| assert_includes lines[-2], '(2)' }
|
tmux.until { |lines| assert_includes lines[-2], '(2)' }
|
||||||
|
|
||||||
# Reload - selections should be preserved by id-nth key
|
# Reload — selections should be preserved by id-nth key
|
||||||
tmux.send_keys 'C-r'
|
tmux.send_keys 'C-r'
|
||||||
tmux.until do |lines|
|
tmux.until do |lines|
|
||||||
assert_equal 3, lines.match_count
|
assert_equal 3, lines.match_count
|
||||||
|
|||||||
@@ -1298,325 +1298,4 @@ class TestLayout < TestInteractive
|
|||||||
tmux.send_keys :Enter
|
tmux.send_keys :Enter
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Locate a word in the currently captured screen and click its first character.
|
|
||||||
# tmux rows/columns are 1-based; capture indices are 0-based.
|
|
||||||
def click_word(word)
|
|
||||||
tmux.capture.each_with_index do |line, idx|
|
|
||||||
col = line.index(word)
|
|
||||||
return tmux.click(col + 1, idx + 1) if col
|
|
||||||
end
|
|
||||||
flunk("word #{word.inspect} not found on screen")
|
|
||||||
end
|
|
||||||
|
|
||||||
# Launch fzf with a click-{header,footer} binding that echoes FZF_CLICK_* into the prompt,
|
|
||||||
# then click each word in `clicks` and assert the resulting L/W values.
|
|
||||||
# `clicks` is an array of [word_to_click, expected_line].
|
|
||||||
def verify_clicks(kind:, opts:, input:, clicks:)
|
|
||||||
var = kind.to_s.upcase # HEADER or FOOTER
|
|
||||||
binding = "click-#{kind}:transform-prompt:" \
|
|
||||||
"echo \"L=$FZF_CLICK_#{var}_LINE W=$FZF_CLICK_#{var}_WORD> \""
|
|
||||||
# --multi makes the info line end in " (0)" so the wait regex is unambiguous.
|
|
||||||
tmux.send_keys %(#{input} | #{FZF} #{opts} --multi --bind '#{binding}'), :Enter
|
|
||||||
# Wait for fzf to fully render before inspecting the screen, otherwise the echoed
|
|
||||||
# command line can shadow click targets.
|
|
||||||
tmux.until { |lines| lines.any_include?(%r{ [0-9]+/[0-9]+ \(0\)}) }
|
|
||||||
clicks.each do |word, line|
|
|
||||||
click_word(word)
|
|
||||||
tmux.until { |lines| assert lines.any_include?("L=#{line} W=#{word}>") }
|
|
||||||
end
|
|
||||||
tmux.send_keys 'Escape'
|
|
||||||
end
|
|
||||||
|
|
||||||
# Header lines (--header-lines) are rendered in reverse display order only under
|
|
||||||
# layout=default; in layout=reverse and layout=reverse-list they keep the input order.
|
|
||||||
# FZF_CLICK_HEADER_LINE reflects the visual row, so the expected value flips.
|
|
||||||
HEADER_CLICKS = [%w[Aaa 1], %w[Bbb 2], %w[Ccc 3]].freeze
|
|
||||||
|
|
||||||
%w[default reverse reverse-list].each do |layout|
|
|
||||||
slug = layout.tr('-', '_')
|
|
||||||
|
|
||||||
# Plain --header with no border around the header section.
|
|
||||||
define_method(:"test_click_header_plain_#{slug}") do
|
|
||||||
verify_clicks(kind: :header,
|
|
||||||
opts: %(--layout=#{layout} --header $'Aaa\\nBbb\\nCcc'),
|
|
||||||
input: 'seq 5',
|
|
||||||
clicks: HEADER_CLICKS)
|
|
||||||
end
|
|
||||||
|
|
||||||
# --header with a framing border (--style full gives --header-border=rounded by default).
|
|
||||||
define_method(:"test_click_header_border_rounded_#{slug}") do
|
|
||||||
verify_clicks(kind: :header,
|
|
||||||
opts: %(--layout=#{layout} --style full --header $'Aaa\\nBbb\\nCcc'),
|
|
||||||
input: 'seq 5',
|
|
||||||
clicks: HEADER_CLICKS)
|
|
||||||
end
|
|
||||||
|
|
||||||
# --header-lines consumed from stdin, with its own framing border.
|
|
||||||
define_method(:"test_click_header_lines_border_rounded_#{slug}") do
|
|
||||||
clicks_hl = if layout == 'default'
|
|
||||||
[%w[Xaa 3], %w[Ybb 2], %w[Zcc 1]]
|
|
||||||
else
|
|
||||||
[%w[Xaa 1], %w[Ybb 2], %w[Zcc 3]]
|
|
||||||
end
|
|
||||||
verify_clicks(kind: :header,
|
|
||||||
opts: %(--layout=#{layout} --style full --header-lines 3),
|
|
||||||
input: "(printf 'Xaa\\nYbb\\nZcc\\n'; seq 5)",
|
|
||||||
clicks: clicks_hl)
|
|
||||||
end
|
|
||||||
|
|
||||||
# --footer with a framing border.
|
|
||||||
define_method(:"test_click_footer_border_rounded_#{slug}") do
|
|
||||||
verify_clicks(kind: :footer,
|
|
||||||
opts: %(--layout=#{layout} --style full --footer $'Foo\\nBar\\nBaz'),
|
|
||||||
input: 'seq 5',
|
|
||||||
clicks: [%w[Foo 1], %w[Bar 2], %w[Baz 3]])
|
|
||||||
end
|
|
||||||
|
|
||||||
# --header and --header-lines combined. Click-header numbering concatenates the two
|
|
||||||
# sections, but the order depends on the layout:
|
|
||||||
# layoutReverse: custom header (1..N), then header-lines (N+1..N+M)
|
|
||||||
# layoutDefault: header-lines (1..M, reversed visually), then custom header (M+1..M+N)
|
|
||||||
# layoutReverseList: header-lines (1..M), then custom header (M+1..M+N)
|
|
||||||
define_method(:"test_click_header_combined_#{slug}") do
|
|
||||||
clicks = case layout
|
|
||||||
when 'reverse'
|
|
||||||
[%w[Aaa 1], %w[Bbb 2], %w[Ccc 3], %w[Xaa 4], %w[Ybb 5], %w[Zcc 6]]
|
|
||||||
when 'default'
|
|
||||||
[%w[Aaa 4], %w[Bbb 5], %w[Ccc 6], %w[Xaa 3], %w[Ybb 2], %w[Zcc 1]]
|
|
||||||
else # reverse-list
|
|
||||||
[%w[Aaa 4], %w[Bbb 5], %w[Ccc 6], %w[Xaa 1], %w[Ybb 2], %w[Zcc 3]]
|
|
||||||
end
|
|
||||||
verify_clicks(kind: :header,
|
|
||||||
opts: %(--layout=#{layout} --header $'Aaa\\nBbb\\nCcc' --header-lines 3),
|
|
||||||
input: "(printf 'Xaa\\nYbb\\nZcc\\n'; seq 5)",
|
|
||||||
clicks: clicks)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Inline header inside a rounded list border.
|
|
||||||
define_method(:"test_click_header_border_inline_#{slug}") do
|
|
||||||
opts = %(--layout=#{layout} --style full --header $'Aaa\\nBbb\\nCcc' --header-border=inline)
|
|
||||||
verify_clicks(kind: :header, opts: opts, input: 'seq 5', clicks: HEADER_CLICKS)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Inline header inside a horizontal list border (top+bottom only, no T-junctions).
|
|
||||||
define_method(:"test_click_header_border_inline_horizontal_list_#{slug}") do
|
|
||||||
opts = %(--layout=#{layout} --style full --list-border=horizontal --header $'Aaa\\nBbb\\nCcc' --header-border=inline)
|
|
||||||
verify_clicks(kind: :header, opts: opts, input: 'seq 5', clicks: HEADER_CLICKS)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Inline header-lines inside a rounded list border.
|
|
||||||
define_method(:"test_click_header_lines_border_inline_#{slug}") do
|
|
||||||
clicks_hl = if layout == 'default'
|
|
||||||
[%w[Xaa 3], %w[Ybb 2], %w[Zcc 1]]
|
|
||||||
else
|
|
||||||
[%w[Xaa 1], %w[Ybb 2], %w[Zcc 3]]
|
|
||||||
end
|
|
||||||
opts = %(--layout=#{layout} --style full --header-lines 3 --header-lines-border=inline)
|
|
||||||
verify_clicks(kind: :header, opts: opts,
|
|
||||||
input: "(printf 'Xaa\\nYbb\\nZcc\\n'; seq 5)",
|
|
||||||
clicks: clicks_hl)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Inline footer inside a rounded list border.
|
|
||||||
define_method(:"test_click_footer_border_inline_#{slug}") do
|
|
||||||
opts = %(--layout=#{layout} --style full --footer $'Foo\\nBar\\nBaz' --footer-border=inline)
|
|
||||||
verify_clicks(kind: :footer, opts: opts, input: 'seq 5',
|
|
||||||
clicks: [%w[Foo 1], %w[Bar 2], %w[Baz 3]])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# An inline section requesting far more rows than the terminal can fit must not
|
|
||||||
# break the layout. The list frame must still render inside the pane with both
|
|
||||||
# corners visible and the prompt line present.
|
|
||||||
def test_inline_header_lines_oversized
|
|
||||||
tmux.send_keys %(seq 10000 | #{FZF} --style full --header-border inline --header-lines 9999), :Enter
|
|
||||||
tmux.until { |lines| lines.any_include?(%r{ [0-9]+/[0-9]+}) }
|
|
||||||
lines = tmux.capture
|
|
||||||
# Rounded (light) and sharp (tcell) default border glyphs.
|
|
||||||
top_corners = /[╭┌]/
|
|
||||||
bottom_corners = /[╰└]/
|
|
||||||
assert(lines.any? { |l| l.match?(top_corners) }, "list frame top missing: #{lines.inspect}")
|
|
||||||
assert(lines.any? { |l| l.match?(bottom_corners) }, "list frame bottom missing: #{lines.inspect}")
|
|
||||||
assert(lines.any? { |l| l.include?('>') }, "prompt missing: #{lines.inspect}")
|
|
||||||
tmux.send_keys 'Escape'
|
|
||||||
end
|
|
||||||
|
|
||||||
# A non-inline section that consumes all available rows must still render without
|
|
||||||
# crashing when another section is inline but has no budget. The inline section's
|
|
||||||
# content is clipped to 0 but the layout proceeds.
|
|
||||||
def test_inline_footer_starved_by_non_inline_header
|
|
||||||
tmux.send_keys %(seq 10000 | #{FZF} --style full --footer-border inline --footer "$(seq 1000)" --header "$(seq 1000)"), :Enter
|
|
||||||
tmux.until { |lines| lines.any_include?(%r{ [0-9]+/[0-9]+}) }
|
|
||||||
lines = tmux.capture
|
|
||||||
assert(lines.any? { |l| l.include?('>') }, "prompt missing: #{lines.inspect}")
|
|
||||||
tmux.send_keys 'Escape'
|
|
||||||
end
|
|
||||||
|
|
||||||
# Without a line-drawing --list-border, --header-border=inline must silently
|
|
||||||
# fall back to the `line` style (documented behavior).
|
|
||||||
def test_inline_falls_back_without_list_border
|
|
||||||
tmux.send_keys %(seq 5 | #{FZF} --list-border=none --header HEADER --header-border=inline), :Enter
|
|
||||||
tmux.until { |lines| lines.any_include?(%r{ [0-9]+/[0-9]+}) }
|
|
||||||
lines = tmux.capture
|
|
||||||
assert(lines.any? { |l| l.include?('HEADER') }, "header missing: #{lines.inspect}")
|
|
||||||
# Neither list frame corners (rounded/sharp) nor T-junction runes appear,
|
|
||||||
# since we've fallen back to a plain line separator.
|
|
||||||
assert(lines.none? { |l| l.match?(/[╭╮╰╯┌┐└┘├┤]/) }, "unexpected frame glyphs: #{lines.inspect}")
|
|
||||||
tmux.send_keys 'Escape'
|
|
||||||
end
|
|
||||||
|
|
||||||
# Regression: when --header-border=inline falls back to `line` because the
|
|
||||||
# list border can't host an inline separator, the header-border color must
|
|
||||||
# inherit from `border`, not `list-border`. The effective shape is `line`,
|
|
||||||
# so color inheritance must match what `line` rendering would use.
|
|
||||||
def test_inline_fallback_does_not_inherit_list_border_color
|
|
||||||
# Marker attribute (bold) on list-border. If HeaderBorder wrongly inherits
|
|
||||||
# from ListBorder, the header separator characters will carry the bold
|
|
||||||
# attribute. --info=hidden and --no-separator strip other separator lines
|
|
||||||
# so the only row of `─` chars is the header separator.
|
|
||||||
tmux.send_keys %(seq 5 | #{FZF} --list-border=none --header HEADER --header-border=inline --info=hidden --no-separator --color=bg:-1,list-border:red:bold), :Enter
|
|
||||||
sep_row = nil
|
|
||||||
tmux.until do |_|
|
|
||||||
sep_row = tmux.capture_ansi.find do |row|
|
|
||||||
stripped = row.gsub(/\e\[[\d;]*m/, '').rstrip
|
|
||||||
stripped.match?(/\A─+\z/)
|
|
||||||
end
|
|
||||||
!sep_row.nil?
|
|
||||||
end
|
|
||||||
# Bold (1) or red fg (31) on the header separator means it inherited from
|
|
||||||
# list-border even though the effective shape is `line` (non-inline).
|
|
||||||
refute_match(/\e\[(?:[\d;]*;)?(?:1|31)(?:;[\d;]*)?m─/, sep_row,
|
|
||||||
"header separator inherited list-border attr: #{sep_row.inspect}")
|
|
||||||
tmux.send_keys 'Escape'
|
|
||||||
end
|
|
||||||
|
|
||||||
# Inline takes precedence over --header-first: the main header stays
|
|
||||||
# inside the list frame instead of moving below the input.
|
|
||||||
def test_inline_header_border_overrides_header_first
|
|
||||||
tmux.send_keys %(seq 5 | #{FZF} --style full --header foo --header-first --header-border inline), :Enter
|
|
||||||
tmux.until do |lines|
|
|
||||||
foo_idx = lines.index { |l| l.match?(/\A│\s+foo\s+│\z/) }
|
|
||||||
input_idx = lines.index { |l| l.match?(/\A│\s+>\s+\d+\/\d+\s+│\z/) }
|
|
||||||
foo_idx && input_idx && foo_idx < input_idx
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# With both sections present, --header-first still moves the main --header
|
|
||||||
# below the input while --header-lines-border=inline keeps header-lines
|
|
||||||
# inside the list frame.
|
|
||||||
def test_inline_header_lines_with_header_first_and_main_header
|
|
||||||
tmux.send_keys %(seq 5 | #{FZF} --style full --header foo --header-lines 1 --header-first --header-lines-border inline), :Enter
|
|
||||||
tmux.until do |lines|
|
|
||||||
one_idx = lines.index { |l| l.match?(/\A│\s+1\s+│\z/) }
|
|
||||||
foo_idx = lines.index { |l| l.match?(/\A│\s+foo\s+│\z/) }
|
|
||||||
input_idx = lines.index { |l| l.match?(/\A│\s+>\s+\d+\/\d+\s+│\z/) }
|
|
||||||
one_idx && foo_idx && input_idx && one_idx < input_idx && input_idx < foo_idx
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# With no main --header, --header-first previously repositioned
|
|
||||||
# header-lines. Inline now takes precedence: header-lines stays inside
|
|
||||||
# the list frame.
|
|
||||||
def test_inline_header_lines_with_header_first_no_main_header
|
|
||||||
tmux.send_keys %(seq 5 | #{FZF} --style full --header-lines 1 --header-first --header-lines-border inline), :Enter
|
|
||||||
tmux.until do |lines|
|
|
||||||
one_idx = lines.index { |l| l.match?(/\A│\s+1\s+│\z/) }
|
|
||||||
input_idx = lines.index { |l| l.match?(/\A│\s+>\s+\d+\/\d+\s+│\z/) }
|
|
||||||
one_idx && input_idx && one_idx < input_idx
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Regression: with --header-border=inline and --header-lines but no
|
|
||||||
# --header, the inline slot was sized for header-lines only. After
|
|
||||||
# change-header added a main header line, resizeIfNeeded tolerated the
|
|
||||||
# too-small slot, so the header-lines line got displaced and disappeared.
|
|
||||||
def test_inline_change_header_grows_slot
|
|
||||||
tmux.send_keys %(seq 5 | #{FZF} --style full --header-lines 1 --header-border inline --bind space:change-header:tada), :Enter
|
|
||||||
tmux.until { |lines| lines.any_include?(/\A│\s+1\s+│\z/) }
|
|
||||||
tmux.send_keys ' '
|
|
||||||
tmux.until do |lines|
|
|
||||||
lines.any_include?(/\A│\s+1\s+│\z/) && lines.any_include?(/\A│\s+tada\s+│\z/)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Invalid inline combinations must be rejected at startup.
|
|
||||||
def test_inline_rejected_on_unsupported_options
|
|
||||||
[
|
|
||||||
['--border=inline', 'inline border is only supported'],
|
|
||||||
['--list-border=inline', 'inline border is only supported'],
|
|
||||||
['--input-border=inline', 'inline border is only supported'],
|
|
||||||
['--preview-window=border-inline --preview :', 'invalid preview window option: border-inline'],
|
|
||||||
['--header-border=inline --header-lines-border=sharp --header-lines=1',
|
|
||||||
'--header-border=inline requires --header-lines-border to be inline or unset']
|
|
||||||
].each do |args, expected|
|
|
||||||
output = `#{FZF} #{args} < /dev/null 2>&1`
|
|
||||||
refute_equal 0, $CHILD_STATUS.exitstatus, "expected non-zero exit for: #{args}"
|
|
||||||
assert_includes output, expected, "wrong error for: #{args}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
# Count rows whose entire width is a single `color` range.
|
|
||||||
def count_full_rows(ranges_by_row, color)
|
|
||||||
ranges_by_row.count { |r| r.length == 1 && r[0][2] == color }
|
|
||||||
end
|
|
||||||
|
|
||||||
# Wait until `tmux.bg_ranges` has at least `count` fully-`color` rows; return them.
|
|
||||||
def wait_for_full_rows(color, count)
|
|
||||||
ranges = nil
|
|
||||||
tmux.until do |_|
|
|
||||||
ranges = tmux.bg_ranges
|
|
||||||
count_full_rows(ranges, color) >= count
|
|
||||||
end
|
|
||||||
ranges
|
|
||||||
end
|
|
||||||
|
|
||||||
public
|
|
||||||
|
|
||||||
# Inline header's entire section (outer edge + content-row verticals + separator)
|
|
||||||
# carries the header-bg color; list rows below carry list-bg.
|
|
||||||
def test_inline_header_bg_color
|
|
||||||
tmux.send_keys %(seq 5 | #{FZF} --list-border --reverse --header HEADER --header-border=inline --color=bg:-1,header-border:white,list-border:white,header-bg:red,list-bg:green), :Enter
|
|
||||||
tmux.until { |lines| lines.any_include?(%r{ [0-9]+/[0-9]+}) }
|
|
||||||
# 3 fully-red rows: top edge, header content, separator.
|
|
||||||
ranges = wait_for_full_rows('red', 3)
|
|
||||||
assert_equal_org(3, count_full_rows(ranges, 'red'))
|
|
||||||
# List rows below (>=5) are fully green.
|
|
||||||
assert_operator count_full_rows(ranges, 'green'), :>=, 5
|
|
||||||
tmux.send_keys 'Escape'
|
|
||||||
end
|
|
||||||
|
|
||||||
# Regression: when --header-lines-border=inline is the only inline section
|
|
||||||
# (no --header-border), the section must still use header-bg, not list-bg.
|
|
||||||
def test_inline_header_lines_bg_without_main_header
|
|
||||||
tmux.send_keys %(seq 5 | #{FZF} --list-border --reverse --header-lines 2 --header-lines-border=inline --color=bg:-1,header-border:white,list-border:white,header-bg:red,list-bg:green), :Enter
|
|
||||||
tmux.until { |lines| lines.any_include?(%r{ [0-9]+/[0-9]+}) }
|
|
||||||
# Top edge + 2 content rows + separator = 4 fully-red rows.
|
|
||||||
ranges = wait_for_full_rows('red', 4)
|
|
||||||
assert_equal_org(4, count_full_rows(ranges, 'red'))
|
|
||||||
tmux.send_keys 'Escape'
|
|
||||||
end
|
|
||||||
|
|
||||||
# Inline footer's entire section carries footer-bg; list rows above carry list-bg.
|
|
||||||
def test_inline_footer_bg_color
|
|
||||||
tmux.send_keys %(seq 5 | #{FZF} --list-border --footer FOOTER --footer-border=inline --color=bg:-1,footer-border:white,list-border:white,footer-bg:blue,list-bg:green), :Enter
|
|
||||||
tmux.until { |lines| lines.any_include?(%r{ [0-9]+/[0-9]+}) }
|
|
||||||
ranges = wait_for_full_rows('blue', 3)
|
|
||||||
assert_equal_org(3, count_full_rows(ranges, 'blue'))
|
|
||||||
tmux.send_keys 'Escape'
|
|
||||||
end
|
|
||||||
|
|
||||||
# The list-label's bg is swapped to match the adjacent inline section so it reads as
|
|
||||||
# part of the section frame rather than a list-colored island on a section-colored edge.
|
|
||||||
def test_list_label_bg_on_inline_section_edge
|
|
||||||
tmux.send_keys %(seq 5 | #{FZF} --list-border --reverse --header HEADER --header-border=inline --list-label=LL --color=bg:-1,header-border:white,list-border:white,header-bg:red,list-bg:green,list-label:yellow:bold), :Enter
|
|
||||||
tmux.until { |lines| lines.any_include?(%r{ [0-9]+/[0-9]+}) }
|
|
||||||
# The label sits on the header-owned top edge, so the entire row must be a
|
|
||||||
# single red run (no green breaks where the label cells are).
|
|
||||||
ranges = wait_for_full_rows('red', 3)
|
|
||||||
assert_operator count_full_rows(ranges, 'red'), :>=, 3
|
|
||||||
tmux.send_keys 'Escape'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class TestServer < TestInteractive
|
|||||||
assert_equal [0, 1], state[:current][:positions]
|
assert_equal [0, 1], state[:current][:positions]
|
||||||
assert_equal state[:current][:positions], state[:current][:positions].sort
|
assert_equal state[:current][:positions], state[:current][:positions].sort
|
||||||
|
|
||||||
# No match - no current item
|
# No match — no current item
|
||||||
Net::HTTP.post(fn.call, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ')
|
Net::HTTP.post(fn.call, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ')
|
||||||
tmux.until { |lines| assert_equal 100, lines.item_count }
|
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||||
tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] }
|
tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] }
|
||||||
|
|||||||
Reference in New Issue
Block a user