mirror of
https://github.com/junegunn/fzf
synced 2026-08-01 18:31:44 +00:00
Compare commits
151 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ce58d08ee3 | |||
| 997a7e5947 | |||
| 88e48619d6 | |||
| 2db14b4308 | |||
| 90c4269d4e | |||
| 6087055305 | |||
| 2f9df91171 | |||
| 12e24d368c | |||
| 55193ee4dc | |||
| ff6a3bbee0 | |||
| dce248ac6d | |||
| 0ff13dcfbe | |||
| 4d6a7757b8 | |||
| b9804f5873 | |||
| 98a3b1fff8 | |||
| 6df5ca17e8 | |||
| 09ca45f7db | |||
| 09fe3a4180 | |||
| b908f7a0ec | |||
| 1a50a3c082 | |||
| fefea8d885 | |||
| 385cccd362 | |||
| 4a684b6c78 | |||
| 4a195e6323 | |||
| 0ecbf3f475 | |||
| 4522868fc0 | |||
| 111a62f1ea | |||
| 33cac3f0e7 | |||
| 74e98cac5c | |||
| c338df02c4 | |||
| 69e9abdab4 | |||
| b6411beaa1 | |||
| b56d614ba2 | |||
| 49ab253555 | |||
| 0e859a18ed | |||
| 880dd20b18 | |||
| 91aa25c863 | |||
| 7e62b34087 | |||
| f9f0014c16 | |||
| ab3b9fef52 | |||
| 7d9724157c | |||
| bc8967632b | |||
| 6360c9261c | |||
| e653628458 | |||
| 28747e5cb6 | |||
| 9725eac314 | |||
| b389616030 | |||
| 25b2248f11 | |||
| 14564e4fc7 | |||
| d01eaa9de3 | |||
| aad805d0d3 | |||
| d1f037059a | |||
| 3f94bcb5bf | |||
| 3c7cbc9d47 | |||
| 28e2a067e5 | |||
| b29e2ee2d1 | |||
| 029b241dbb | |||
| d6ded42026 | |||
| 6eb4b41e34 | |||
| 14b5e1d88c | |||
| 603240122e | |||
| 8d688521fe | |||
| 775129367a | |||
| b3b221854b | |||
| c8cf0992c1 | |||
| 33d8d51c8a | |||
| b473477c22 | |||
| fcc4178bca | |||
| cfc37caabc | |||
| af2a81dc02 | |||
| be5a687281 | |||
| 771e35b972 | |||
| 60a5be1e65 | |||
| 1d5e87f5e4 | |||
| 3db63f5e52 | |||
| 2ab923f3ae | |||
| c3e6d9a8f9 | |||
| 2471edf3ff | |||
| 53a8aeeb72 | |||
| 60b35e748b | |||
| 3f499f055e | |||
| 1df99db0b2 | |||
| 535b610a6b | |||
| 91fab3b3c2 | |||
| b9f2bf64ff | |||
| 07d53cb7e4 | |||
| ead534a1be | |||
| 8a05083503 | |||
| e659b46ff5 | |||
| 991c36453c | |||
| 4d563c6dfa | |||
| 5cb695744f | |||
| c1b259c042 | |||
| 1a0371e2c7 | |||
| aa259fdc19 | |||
| b852dc8a56 | |||
| a0cabe021d | |||
| 8cdfb23df6 | |||
| 4ffde48e2f | |||
| f2b33f038a | |||
| d5913bf86e | |||
| 0e9026b817 | |||
| ab407c4645 | |||
| 91c4bef35f | |||
| bf77206221 | |||
| 0cb1be3f04 | |||
| 01cb38a5fb | |||
| c38c6cad79 | |||
| ba6fc40cfd | |||
| dd46a256c0 | |||
| d19ce0ad8d | |||
| ed7becfb47 | |||
| 9ace1351ff | |||
| e1de29bc40 | |||
| 0df7d10550 | |||
| 91e119a77e | |||
| 3984161f6c | |||
| 91beacf0f4 | |||
| e6ad01fb90 | |||
| ce2200e908 | |||
| 548061dbde | |||
| 8f0c91545d | |||
| 0eefcf348e | |||
| c1f8d18a0c | |||
| 8585969d6d | |||
| 8a943a9b1a | |||
| c87a8eccd4 | |||
| 65df0abf0e | |||
| b51bc6b50e | |||
| febaadbee5 | |||
| 0e67c5aa7a | |||
| 760d1b7c58 | |||
| 9bdacc8df2 | |||
| 8e936ecfa7 | |||
| db2e95b1f2 | |||
| 687074e772 | |||
| 3401c2e0c7 | |||
| e8cb315419 | |||
| f0c4ee4047 | |||
| de0df2422a | |||
| 148b0a94cd | |||
| ca294109c3 | |||
| 9cad2686e9 | |||
| 9a45172232 | |||
| 2a92c7d792 | |||
| f5975cf870 | |||
| a67aa85820 | |||
| c5cabe1691 | |||
| cbed41cd82 | |||
| 6684771cbf | |||
| f5f894ea47 |
@@ -0,0 +1,20 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*.{sh,bash}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
simplify = true
|
||||||
|
binary_next_line = false
|
||||||
|
switch_case_indent = true
|
||||||
|
space_redirects = true
|
||||||
|
function_next_line = false
|
||||||
|
|
||||||
|
# also bash scripts.
|
||||||
|
[{install,uninstall,bin/fzf-preview.sh,bin/fzf-tmux}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
simplify = true
|
||||||
|
binary_next_line = false
|
||||||
|
switch_case_indent = true
|
||||||
|
space_redirects = true
|
||||||
|
function_next_line = false
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
go:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- src/**
|
||||||
|
- main.go
|
||||||
|
- go.mod
|
||||||
|
- go.sum
|
||||||
|
|
||||||
|
shell:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- shell/**
|
||||||
|
|
||||||
|
bash:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- shell/**/*.bash
|
||||||
|
|
||||||
|
zsh:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- shell/**/*.zsh
|
||||||
|
|
||||||
|
fish:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- shell/**/*.fish
|
||||||
|
|
||||||
|
vim:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- plugin/**
|
||||||
|
|
||||||
|
docs:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- '*.md'
|
||||||
|
- doc/**
|
||||||
|
- man/**
|
||||||
|
|
||||||
|
ci:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- .github/**
|
||||||
|
|
||||||
|
build:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- Makefile
|
||||||
|
- .goreleaser.yml
|
||||||
|
- Dockerfile
|
||||||
|
|
||||||
|
test:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- test/**
|
||||||
|
- src/**/*_test.go
|
||||||
|
|
||||||
|
install:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- install
|
||||||
|
- install.ps1
|
||||||
|
- uninstall
|
||||||
@@ -33,12 +33,12 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3
|
uses: github/codeql-action/init@v4
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
|
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v3
|
uses: github/codeql-action/autobuild@v4
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3
|
uses: github/codeql-action/analyze@v4
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
name: Label PRs
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened, synchronize, reopened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
label:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/labeler@v5
|
||||||
|
with:
|
||||||
|
configuration-path: .github/labeler.yml
|
||||||
@@ -23,17 +23,17 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: "1.23"
|
go-version: "1.23"
|
||||||
|
|
||||||
- name: Setup Ruby
|
- name: Setup Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 3.4.1
|
ruby-version: 3.4.6
|
||||||
|
|
||||||
- name: Install packages
|
- name: Install packages
|
||||||
run: sudo apt-get install --yes zsh fish tmux
|
run: sudo apt-get install --yes zsh fish tmux shfmt
|
||||||
|
|
||||||
- name: Install Ruby gems
|
- name: Install Ruby gems
|
||||||
run: bundle install
|
run: bundle install
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: "1.23"
|
go-version: "1.23"
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ jobs:
|
|||||||
ruby-version: 3.0.0
|
ruby-version: 3.0.0
|
||||||
|
|
||||||
- name: Install packages
|
- name: Install packages
|
||||||
run: HOMEBREW_NO_INSTALL_CLEANUP=1 brew install fish zsh tmux
|
run: HOMEBREW_NO_INSTALL_CLEANUP=1 brew install fish zsh tmux shfmt
|
||||||
|
|
||||||
- name: Install Ruby gems
|
- name: Install Ruby gems
|
||||||
run: gem install --no-document minitest:5.14.2 rubocop:1.0.0 rubocop-minitest:0.10.1 rubocop-performance:1.8.1
|
run: gem install --no-document minitest:5.14.2 rubocop:1.0.0 rubocop-minitest:0.10.1 rubocop-performance:1.8.1
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ name: Generate Sponsors README
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: 0 0 * * 0
|
- cron: 0 15 * * 6
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
+9
-4
@@ -22,6 +22,7 @@ builds:
|
|||||||
- loong64
|
- loong64
|
||||||
- ppc64le
|
- ppc64le
|
||||||
- s390x
|
- s390x
|
||||||
|
- riscv64
|
||||||
goarm:
|
goarm:
|
||||||
- "5"
|
- "5"
|
||||||
- "6"
|
- "6"
|
||||||
@@ -39,6 +40,8 @@ builds:
|
|||||||
goarch: arm64
|
goarch: arm64
|
||||||
- goos: openbsd
|
- goos: openbsd
|
||||||
goarch: arm64
|
goarch: arm64
|
||||||
|
- goos: openbsd
|
||||||
|
goarch: riscv64
|
||||||
- goos: android
|
- goos: android
|
||||||
goarch: amd64
|
goarch: amd64
|
||||||
- goos: android
|
- goos: android
|
||||||
@@ -82,12 +85,14 @@ notarize:
|
|||||||
|
|
||||||
archives:
|
archives:
|
||||||
- name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
|
- name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
|
||||||
builds:
|
ids:
|
||||||
- fzf
|
- fzf
|
||||||
format: tar.gz
|
formats:
|
||||||
|
- tar.gz
|
||||||
format_overrides:
|
format_overrides:
|
||||||
- goos: windows
|
- goos: windows
|
||||||
format: zip
|
formats:
|
||||||
|
- zip
|
||||||
files:
|
files:
|
||||||
- non-existent*
|
- non-existent*
|
||||||
|
|
||||||
@@ -99,7 +104,7 @@ release:
|
|||||||
name_template: '{{ .Version }}'
|
name_template: '{{ .Version }}'
|
||||||
|
|
||||||
snapshot:
|
snapshot:
|
||||||
name_template: "{{ .Version }}-devel"
|
version_template: "{{ .Version }}-devel"
|
||||||
|
|
||||||
changelog:
|
changelog:
|
||||||
sort: asc
|
sort: asc
|
||||||
|
|||||||
+3
-2
@@ -1,2 +1,3 @@
|
|||||||
golang 1.23.12
|
golang 1.23
|
||||||
ruby 3.4.1
|
ruby 3.4
|
||||||
|
shfmt 3.12
|
||||||
|
|||||||
+1
-1
@@ -363,7 +363,7 @@ projects, and it will free up memory as you narrow down the results.
|
|||||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||||
INITIAL_QUERY="${*:-}"
|
INITIAL_QUERY="${*:-}"
|
||||||
fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||||
--bind "start:reload:$RG_PREFIX {q}" \
|
--bind "start:reload:$RG_PREFIX {q} || true" \
|
||||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||||
--delimiter : \
|
--delimiter : \
|
||||||
--preview 'bat --color=always {1} --highlight-line {2}' \
|
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||||
|
|||||||
+376
@@ -1,6 +1,382 @@
|
|||||||
CHANGELOG
|
CHANGELOG
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
0.68.0
|
||||||
|
------
|
||||||
|
- Implemented word wrapping in the list section
|
||||||
|
- Added `--wrap=word` (or `--wrap-word`) option and `toggle-wrap-word` action for word-level line wrapping in the list section
|
||||||
|
- Changed default binding of `ctrl-/` and `alt-/` from `toggle-wrap` to `toggle-wrap-word`
|
||||||
|
```sh
|
||||||
|
fzf --wrap=word
|
||||||
|
```
|
||||||
|
- Implemented word wrapping in the preview window
|
||||||
|
- Added `wrap-word` flag for `--preview-window` to enable word-level wrapping
|
||||||
|
- Added `toggle-preview-wrap-word` action
|
||||||
|
```sh
|
||||||
|
fzf --preview 'bat --style=plain --color=always {}' \
|
||||||
|
--preview-window wrap-word \
|
||||||
|
--bind space:toggle-preview-wrap-word
|
||||||
|
```
|
||||||
|
- Added support for underline style variants in `--color`: `underline-double`, `underline-curly`, `underline-dotted`, `underline-dashed`
|
||||||
|
```sh
|
||||||
|
fzf --color 'fg:underline-curly,current-fg:underline-dashed'
|
||||||
|
```
|
||||||
|
- Added support for underline styles (`4:N`) and underline colors (SGR 58/59)
|
||||||
|
```sh
|
||||||
|
# In the list section
|
||||||
|
printf '\e[4:3;58;2;255;0;0mRed curly underline\e[0m\n' | fzf --ansi
|
||||||
|
|
||||||
|
# In the preview window
|
||||||
|
fzf --preview "printf '\e[4:3;58;2;255;0;0mRed curly underline\e[0m\n'"
|
||||||
|
```
|
||||||
|
- Added `--preview-wrap-sign` to set a different wrap indicator for the preview window
|
||||||
|
- Added `alt-gutter` color option (#4602) (@hedgieinsocks)
|
||||||
|
- Added `$FZF_WRAP` environment variable to child processes (`char` or `word` when wrapping is enabled) (#4672) (@bitraid)
|
||||||
|
- fish: Improved command history (CTRL-R) (#4672) (@bitraid)
|
||||||
|
- Enabled syntax highlighting in the list on fish 4.3.3+
|
||||||
|
- Added syntax-highlighted preview window that auto-shows for long or multi-line commands
|
||||||
|
- Added `ALT-ENTER` to reformat and insert selected commands
|
||||||
|
- Improved handling of bulk deletion of selected history entries (`SHIFT-DELETE`)
|
||||||
|
- Added fish completion support (#4605) (@lalvarezt)
|
||||||
|
- zsh: Handle multi-line history selection (#4595) (@LangLangBart)
|
||||||
|
- Bug fixes
|
||||||
|
- Fixed `_fzf_compgen_{path,dir}` to respect `FZF_COMPLETION_{PATH,DIR}_OPTS` (#4592) (@shtse8, @LangLangBart)
|
||||||
|
- Fixed `--preview-window follow` not working correctly with wrapping (#3243, #4258)
|
||||||
|
- Fixed symlinks to directories being returned as files (#4676) (@skk64)
|
||||||
|
- Fixed SIGHUP signal handling (#4668) (@LangLangBart)
|
||||||
|
- Fixed preview process not killed on exit (#4667)
|
||||||
|
- Fixed coloring of items with zero-width characters (#4620)
|
||||||
|
- Fixed `track-current` unset after a combined movement action (#4649)
|
||||||
|
- Fixed `--accept-nth` being ignored in filter mode (#4636) (@charemma)
|
||||||
|
- Fixed display width calculation with `maxWidth` (#4596) (@LangLangBart)
|
||||||
|
- Fixed clearing of the rest of the current line on start (#4652)
|
||||||
|
- Fixed `x-api-key` header not required for GET requests (#4627)
|
||||||
|
- Fixed key reading not cancelled when `execute` triggered via a server request (#4653)
|
||||||
|
- Fixed rebind of readline command `redraw-current-line` (#4635) (@jameslazo)
|
||||||
|
- Fixed `fzf-tmux` `TERM` quoting and added `mktemp` usage (#4664) (@Goofygiraffe06)
|
||||||
|
- Do not allow very long queries in `FuzzyMatchV2` (#4608)
|
||||||
|
|
||||||
|
0.67.0
|
||||||
|
------
|
||||||
|
- Added `--freeze-left=N` option to keep the leftmost N columns always visible.
|
||||||
|
```sh
|
||||||
|
# Keep the file name column fixed and always visible
|
||||||
|
git grep --line-number --color=always -- '' |
|
||||||
|
fzf --ansi --delimiter : --freeze-left 1
|
||||||
|
|
||||||
|
# Can be used with --keep-right
|
||||||
|
git grep --line-number --color=always -- '' |
|
||||||
|
fzf --ansi --delimiter : --freeze-left 1 --keep-right
|
||||||
|
```
|
||||||
|
- Also added `--freeze-right=N` option to keep the rightmost N columns always visible.
|
||||||
|
```sh
|
||||||
|
# Stronger version of --keep-right that always keeps the right-end visible
|
||||||
|
fd | fzf --freeze-right 1
|
||||||
|
|
||||||
|
# Keep the base name always visible
|
||||||
|
fd | fzf --freeze-right 1 --delimiter /
|
||||||
|
|
||||||
|
# Keep both leftmost and rightmost components visible
|
||||||
|
fd | fzf --freeze-left 1 --freeze-right 1 --delimiter /
|
||||||
|
```
|
||||||
|
- Updated `--info=inline` to print the spinner (load indicator).
|
||||||
|
- Bug fixes
|
||||||
|
|
||||||
|
0.66.1
|
||||||
|
------
|
||||||
|
- Bug fixes
|
||||||
|
- Fixed a bug preventing 'ctrl-h' from being bound to an action (#4556)
|
||||||
|
- Fixed `--no-color` / `NO_COLOR` theme (#4561)
|
||||||
|
|
||||||
|
0.66.0
|
||||||
|
------
|
||||||
|
|
||||||
|
### Quick summary
|
||||||
|
|
||||||
|
This version introduces many new features centered around the new "raw" mode.
|
||||||
|
|
||||||
|
| Type | Class | Name | Description |
|
||||||
|
| :-- | :-- | :-- | :-- |
|
||||||
|
| New | Option | `--raw` | Enable raw mode by default |
|
||||||
|
| New | Option | `--gutter CHAR` | Set the gutter column character |
|
||||||
|
| New | Option | `--gutter-raw CHAR` | Set the gutter column character in raw mode |
|
||||||
|
| Enhancement | Option | `--listen SOCKET` | Added support for Unix domain sockets |
|
||||||
|
| New | Action | `toggle-raw` | Toggle raw mode |
|
||||||
|
| New | Action | `enable-raw` | Enable raw mode |
|
||||||
|
| New | Action | `disable-raw` | Disable raw mode |
|
||||||
|
| New | Action | `up-match` | Move up to the matching item |
|
||||||
|
| New | Action | `down-match` | Move down to the matching item |
|
||||||
|
| New | Action | `best` | Move to the matching item with the best score |
|
||||||
|
| New | Color | `nomatch` | Color for non-matching items in raw mode |
|
||||||
|
| New | Env Var | `FZF_RAW` | Matching status in raw mode (0, 1, or undefined) |
|
||||||
|
| New | Env Var | `FZF_DIRECTION` | `up` or `down` depending on the layout |
|
||||||
|
| New | Env Var | `FZF_SOCK` | Path to the Unix domain socket fzf is listening on |
|
||||||
|
| Enhancement | Key | `CTRL-N` | `down` -> `down-match` |
|
||||||
|
| Enhancement | Key | `CTRL-P` | `up` -> `up-match` |
|
||||||
|
| Enhancement | Shell | `CTRL-R` binding | Toggle raw mode with `ALT-R` |
|
||||||
|
| Enhancement | Shell | `CTRL-R` binding | Opt-out with an empty `FZF_CTRL_R_COMMAND` |
|
||||||
|
|
||||||
|
### 1. Introducing "raw" mode
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This version introduces a new "raw" mode (named so because it shows the list
|
||||||
|
"unfiltered"). In raw mode, non-matching items stay in their original positions,
|
||||||
|
but appear dimmed. This allows you to see the surrounding items of a match and
|
||||||
|
better understand the context of it. You can enable raw mode by default with
|
||||||
|
`--raw`, but it's often more useful when toggled dynamically with the
|
||||||
|
`toggle-raw` action.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
tree | fzf --reverse --bind alt-r:toggle-raw
|
||||||
|
```
|
||||||
|
|
||||||
|
While non-matching items are displayed in a dimmed color, they are treated just
|
||||||
|
like matching items, so you place the cursor on them and perform any action. If
|
||||||
|
you prefer to navigate only through matching items, use the `down-match` and
|
||||||
|
`up-match` actions, which are from now on bound to `CTRL-N` and `CTRL-P`
|
||||||
|
respectively, and also to `ALT-DOWN` and `ALT-UP`.
|
||||||
|
|
||||||
|
| Key | Action | With `--history` |
|
||||||
|
| :-- | :-- | :-- |
|
||||||
|
| `down` | `down` | |
|
||||||
|
| `up` | `up` | |
|
||||||
|
| `ctrl-j` | `down` | |
|
||||||
|
| `ctrl-k` | `up` | |
|
||||||
|
| `ctrl-n` | `down-match` | `next-history` |
|
||||||
|
| `ctrl-p` | `up-match` | `prev-history` |
|
||||||
|
| `alt-down` | `down-match` | |
|
||||||
|
| `alt-up` | `up-match` | |
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> `CTRL-N` and `CTRL-P` are bound to `next-history` and `prev-history` when
|
||||||
|
> `--history` option is enabled, so in that case, you'll need to manually bind
|
||||||
|
> them, or use `ALT-DOWN` and `ALT-UP` instead.
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> `up-match` and `down-match` are equivalent to `up` and `down` when not in
|
||||||
|
> raw mode, so you can safely bind them to `up` and `arrow` keys if you prefer.
|
||||||
|
> ```sh
|
||||||
|
> fzf --bind up:up-match,down:down-match
|
||||||
|
> ```
|
||||||
|
|
||||||
|
#### Customizing the behavior
|
||||||
|
|
||||||
|
In raw mode, the input list is presented in its original order, unfiltered, and
|
||||||
|
your cursor will not move to the matching item automatically. Here are ways to
|
||||||
|
customize the behavior.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# When the result list is updated, move the cursor to the item with the best score
|
||||||
|
# (assuming sorting is not disabled)
|
||||||
|
fzf --raw --bind result:best
|
||||||
|
|
||||||
|
# Move to the first matching item in the original list
|
||||||
|
# - $FZF_RAW is set to 0 when raw mode is enabled and the current item is a non-match
|
||||||
|
# - $FZF_DIRECTION is set to either 'up' or 'down' depending on the layout direction
|
||||||
|
fzf --raw --bind 'result:first+transform:[[ $FZF_RAW = 0 ]] && echo $FZF_DIRECTION-match'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Customizing the look
|
||||||
|
|
||||||
|
##### Gutter
|
||||||
|
|
||||||
|
To make the mode visually distinct, the gutter column is rendered in a dashed
|
||||||
|
line using `▖` character. But you can customize it with the `--gutter-raw CHAR`
|
||||||
|
option.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Use a thinner gutter instead of the default dashed line
|
||||||
|
fzf --bind alt-r:toggle-raw --gutter-raw ▎
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Color and style of non-matching items
|
||||||
|
|
||||||
|
Non-matching items are displayed in a dimmed color by default, but you can
|
||||||
|
change it with the `--color nomatch:...` option.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
fzf --raw --color nomatch:red
|
||||||
|
fzf --raw --color nomatch:red:dim
|
||||||
|
fzf --raw --color nomatch:red:dim:strikethrough
|
||||||
|
fzf --raw --color nomatch:red:dim:strikethrough:italic
|
||||||
|
```
|
||||||
|
|
||||||
|
For colored input, dimming alone may not be enough, and you may prefer to remove
|
||||||
|
colors entirely. For that case, a new special style attribute `strip` has been
|
||||||
|
added.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
fd --color always | fzf --ansi --raw --color nomatch:dim:strip:strikethrough
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Conditional actions for raw mode
|
||||||
|
|
||||||
|
You may want to perform different actions depending on whether the current item
|
||||||
|
is a match or not. For that, fzf now exports `$FZF_RAW` environment variable.
|
||||||
|
|
||||||
|
It's:
|
||||||
|
|
||||||
|
- Undefined if raw mode is disabled
|
||||||
|
- `1` if the current item is a match
|
||||||
|
- `0` otherwise
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Do not allow selecting non-matching items
|
||||||
|
fzf --raw --bind 'enter:transform:[[ ${FZF_RAW-1} = 1 ]] && echo accept || echo bell'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Leveraging raw mode in shell integration
|
||||||
|
|
||||||
|
The `CTRL-R` binding (command history) now lets you toggle raw mode with `ALT-R`.
|
||||||
|
|
||||||
|
### 2. Style changes
|
||||||
|
|
||||||
|
The screenshot on the right shows the updated gutter style:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This version includes a few minor updates to fzf's classic visual style:
|
||||||
|
|
||||||
|
- The gutter column is now narrower, rendered with the left-half block character (`▌`).
|
||||||
|
- Markers no longer use background colors.
|
||||||
|
- The `--color base16` theme (alias: `16`) has been updated for better compatibility with both dark and light themes.
|
||||||
|
|
||||||
|
### 3. `--listen` now supports Unix domain sockets
|
||||||
|
|
||||||
|
If an argument to `--listen` ends with `.sock`, fzf will listen on a Unix
|
||||||
|
domain socket at the specified path.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
fzf --listen /tmp/fzf.sock --no-tmux
|
||||||
|
|
||||||
|
# GET
|
||||||
|
curl --unix-socket /tmp/fzf.sock http
|
||||||
|
|
||||||
|
# POST
|
||||||
|
curl --unix-socket /tmp/fzf.sock http -d up
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that any existing file at the given path will be removed before creating
|
||||||
|
the socket, so avoid using an important file path.
|
||||||
|
|
||||||
|
### 4. Added options
|
||||||
|
|
||||||
|
#### `--gutter CHAR`
|
||||||
|
|
||||||
|
The gutter column can now be customized using `--gutter CHAR` and styled with
|
||||||
|
`--color gutter:...`. Examples:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Right-aligned gutter
|
||||||
|
fzf --gutter '▐'
|
||||||
|
|
||||||
|
# Even thinner gutter
|
||||||
|
fzf --gutter '▎'
|
||||||
|
|
||||||
|
# Yellow checker pattern
|
||||||
|
fzf --gutter '▚' --color gutter:yellow
|
||||||
|
|
||||||
|
# Classic style
|
||||||
|
fzf --gutter ' ' --color gutter:reverse
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `--gutter-raw CHAR`
|
||||||
|
|
||||||
|
As noted above, the `--gutter-raw CHAR` option was also added for customizing the gutter column in raw mode.
|
||||||
|
|
||||||
|
### 5. Added actions
|
||||||
|
|
||||||
|
The following actions were introduced to support working with raw mode:
|
||||||
|
|
||||||
|
| Action | Description |
|
||||||
|
| :-- | :-- |
|
||||||
|
| `toggle-raw` | Toggle raw mode |
|
||||||
|
| `enable-raw` | Enable raw mode |
|
||||||
|
| `disable-raw` | Disable raw mode |
|
||||||
|
| `up-match` | Move up to the matching item; identical to `up` if raw mode is disabled |
|
||||||
|
| `down-match` | Move down to the matching item; identical to `down` if raw mode is disabled |
|
||||||
|
| `best` | Move to the matching item with the best score; identical to `first` if raw mode is disabled |
|
||||||
|
|
||||||
|
### 6. Added environment variables
|
||||||
|
|
||||||
|
#### `$FZF_DIRECTION`
|
||||||
|
|
||||||
|
`$FZF_DIRECTION` is now exported to child processes, indicating the list direction of the current layout:
|
||||||
|
|
||||||
|
- `up` for the default layout
|
||||||
|
- `down` for `reverse` or `reverse-list`
|
||||||
|
|
||||||
|
This simplifies writing transform actions involving layout-dependent actions
|
||||||
|
like `{up,down}-match`, `{up,down}-selected`, and `toggle+{up,down}`.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
fzf --raw --bind 'result:first+transform:[[ $FZF_RAW = 0 ]] && echo $FZF_DIRECTION-match'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `$FZF_SOCK`
|
||||||
|
|
||||||
|
When fzf is listening on a Unix domain socket using `--listen`, the path to the
|
||||||
|
socket is exported as `$FZF_SOCK`, analogous to `$FZF_PORT` for TCP sockets.
|
||||||
|
|
||||||
|
#### `$FZF_RAW`
|
||||||
|
|
||||||
|
As described above, `$FZF_RAW` is now exported to child processes in raw mode,
|
||||||
|
indicating whether the current item is a match (`1`) or not (`0`). It is not
|
||||||
|
defined when not in raw mode.
|
||||||
|
|
||||||
|
#### `$FZF_CTRL_R_COMMAND`
|
||||||
|
|
||||||
|
You can opt-out `CTRL-R` binding from the shell integration by setting
|
||||||
|
`FZF_CTRL_R_COMMAND` to an empty string. Setting it to any other value is not
|
||||||
|
supported and will result in a warning.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Disable the CTRL-R binding from the shell integration
|
||||||
|
FZF_CTRL_R_COMMAND= eval "$(fzf --bash)"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Added key support for `--bind`
|
||||||
|
|
||||||
|
Pull request [#3996](https://github.com/junegunn/fzf/pull/3996) added support
|
||||||
|
for many additional keys for `--bind` option, such as `ctrl-backspace`.
|
||||||
|
|
||||||
|
### 8. Breaking changes
|
||||||
|
|
||||||
|
#### Hiding the gutter column
|
||||||
|
|
||||||
|
In the previous versions, the recommended way to hide the gutter column was to
|
||||||
|
set `--color gutter:-1`. That's because the gutter column was just a space
|
||||||
|
character, reversed. But now that it's using a visible character (`▌`), applying
|
||||||
|
the default color is no longer enough to hide it. Instead, you can set it to
|
||||||
|
a space character.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Hide the gutter column
|
||||||
|
fzf --gutter ' '
|
||||||
|
|
||||||
|
# Classic style
|
||||||
|
fzf --gutter ' ' --color gutter:reverse
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `--color` option
|
||||||
|
|
||||||
|
In the previous versions, some elements had default style attributes applied and
|
||||||
|
you would have to explicitly unset them with `regular` attribute if you wanted
|
||||||
|
to reset them. This is no longer needed now, as the default style attributes
|
||||||
|
are applied only when you do not specify any color or style for that element.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# No 'dim', just red and italic.
|
||||||
|
fzf --ghost 'Type to search' --color ghost:red:italic
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Compatibility changes
|
||||||
|
|
||||||
|
Starting with this release, fzf is built with Go 1.23. Support for some old OS versions has been dropped.
|
||||||
|
|
||||||
|
See https://go.dev/wiki/MinimumRequirements.
|
||||||
|
|
||||||
0.65.2
|
0.65.2
|
||||||
------
|
------
|
||||||
- Bug fixes and improvements
|
- Bug fixes and improvements
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2013-2025 Junegunn Choi
|
Copyright (c) 2013-2026 Junegunn Choi
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
GO ?= go
|
GO ?= go
|
||||||
|
DOCKER ?= docker
|
||||||
GOOS ?= $(shell $(GO) env GOOS)
|
GOOS ?= $(shell $(GO) env GOOS)
|
||||||
|
|
||||||
MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST)))
|
MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST)))
|
||||||
ROOT_DIR := $(shell dirname $(MAKEFILE))
|
ROOT_DIR := $(shell dirname $(MAKEFILE))
|
||||||
SOURCES := $(wildcard *.go src/*.go src/*/*.go shell/*sh man/man1/*.1) $(MAKEFILE)
|
SOURCES := $(wildcard *.go src/*.go src/*/*.go shell/*sh man/man1/*.1) $(MAKEFILE)
|
||||||
|
|
||||||
|
BASH_SCRIPTS := $(ROOT_DIR)/bin/fzf-preview.sh \
|
||||||
|
$(ROOT_DIR)/bin/fzf-tmux \
|
||||||
|
$(ROOT_DIR)/install \
|
||||||
|
$(ROOT_DIR)/uninstall \
|
||||||
|
$(ROOT_DIR)/shell/common.sh \
|
||||||
|
$(ROOT_DIR)/shell/update.sh \
|
||||||
|
$(ROOT_DIR)/shell/completion.bash \
|
||||||
|
$(ROOT_DIR)/shell/key-bindings.bash
|
||||||
|
|
||||||
ifdef FZF_VERSION
|
ifdef FZF_VERSION
|
||||||
VERSION := $(FZF_VERSION)
|
VERSION := $(FZF_VERSION)
|
||||||
else
|
else
|
||||||
@@ -88,9 +98,14 @@ itest:
|
|||||||
bench:
|
bench:
|
||||||
cd src && SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" -run=Bench -bench=. -benchmem
|
cd src && SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" -run=Bench -bench=. -benchmem
|
||||||
|
|
||||||
lint: $(SOURCES) test/*.rb test/lib/*.rb
|
lint: $(SOURCES) test/*.rb test/lib/*.rb ${BASH_SCRIPTS}
|
||||||
[ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1)
|
[ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1)
|
||||||
bundle exec rubocop -a --require rubocop-minitest --require rubocop-performance
|
bundle exec rubocop -a --require rubocop-minitest --require rubocop-performance
|
||||||
|
shell/update.sh --check ${BASH_SCRIPTS}
|
||||||
|
|
||||||
|
fmt: $(SOURCES) $(BASH_SCRIPTS)
|
||||||
|
gofmt -s -w src
|
||||||
|
shell/update.sh ${BASH_SCRIPTS}
|
||||||
|
|
||||||
install: bin/fzf
|
install: bin/fzf
|
||||||
|
|
||||||
@@ -178,15 +193,15 @@ bin/fzf: target/$(BINARY) | bin
|
|||||||
cp -f target/$(BINARY) bin/fzf
|
cp -f target/$(BINARY) bin/fzf
|
||||||
|
|
||||||
docker:
|
docker:
|
||||||
docker build -t fzf-ubuntu .
|
$(DOCKER) build -t fzf-ubuntu .
|
||||||
docker run -it fzf-ubuntu tmux
|
$(DOCKER) run -it fzf-ubuntu tmux
|
||||||
|
|
||||||
docker-test:
|
docker-test:
|
||||||
docker build -t fzf-ubuntu .
|
$(DOCKER) build -t fzf-ubuntu .
|
||||||
docker run -it fzf-ubuntu
|
$(DOCKER) run -it fzf-ubuntu
|
||||||
|
|
||||||
update:
|
update:
|
||||||
$(GO) get -u
|
$(GO) get -u
|
||||||
$(GO) mod tidy
|
$(GO) mod tidy
|
||||||
|
|
||||||
.PHONY: all generate build release test itest bench lint install clean docker docker-test update
|
.PHONY: all generate build release test itest bench lint install clean docker docker-test update fmt
|
||||||
|
|||||||
+1
-1
@@ -493,4 +493,4 @@ autocmd FileType fzf set laststatus=0 noshowmode noruler
|
|||||||
|
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2013-2025 Junegunn Choi
|
Copyright (c) 2013-2026 Junegunn Choi
|
||||||
|
|||||||
+2
-2
@@ -49,9 +49,9 @@ if [[ ! $type =~ image/ ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
|
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
|
||||||
if [[ $dim = x ]]; then
|
if [[ $dim == x ]]; then
|
||||||
dim=$(stty size < /dev/tty | awk '{print $2 "x" $1}')
|
dim=$(stty size < /dev/tty | awk '{print $2 "x" $1}')
|
||||||
elif ! [[ $KITTY_WINDOW_ID ]] && (( FZF_PREVIEW_TOP + FZF_PREVIEW_LINES == $(stty size < /dev/tty | awk '{print $1}') )); then
|
elif ! [[ $KITTY_WINDOW_ID ]] && ((FZF_PREVIEW_TOP + FZF_PREVIEW_LINES == $(stty size < /dev/tty | awk '{print $1}'))); then
|
||||||
# Avoid scrolling issue when the Sixel image touches the bottom of the screen
|
# Avoid scrolling issue when the Sixel image touches the bottom of the screen
|
||||||
# * https://github.com/junegunn/fzf/issues/2544
|
# * https://github.com/junegunn/fzf/issues/2544
|
||||||
dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1))
|
dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1))
|
||||||
|
|||||||
+50
-48
@@ -8,7 +8,7 @@ fail() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fzf="$(command which fzf)" || fzf="$(dirname "$0")/fzf"
|
fzf="$(command which fzf)" || fzf="$(dirname "$0")/fzf"
|
||||||
[[ -x "$fzf" ]] || fail 'fzf executable not found'
|
[[ -x $fzf ]] || fail 'fzf executable not found'
|
||||||
|
|
||||||
args=()
|
args=()
|
||||||
opt=""
|
opt=""
|
||||||
@@ -16,8 +16,8 @@ skip=""
|
|||||||
swap=""
|
swap=""
|
||||||
close=""
|
close=""
|
||||||
term=""
|
term=""
|
||||||
[[ -n "$LINES" ]] && lines=$LINES || lines=$(tput lines) || lines=$(tmux display-message -p "#{pane_height}")
|
[[ -n $LINES ]] && lines=$LINES || lines=$(tput lines) || lines=$(tmux display-message -p "#{pane_height}")
|
||||||
[[ -n "$COLUMNS" ]] && columns=$COLUMNS || columns=$(tput cols) || columns=$(tmux display-message -p "#{pane_width}")
|
[[ -n $COLUMNS ]] && columns=$COLUMNS || columns=$(tput cols) || columns=$(tmux display-message -p "#{pane_width}")
|
||||||
|
|
||||||
tmux_version=$(tmux -V | sed 's/[^0-9.]//g')
|
tmux_version=$(tmux -V | sed 's/[^0-9.]//g')
|
||||||
tmux_32=$(awk '{print ($1 >= 3.2)}' <<< "$tmux_version" 2> /dev/null || bc -l <<< "$tmux_version >= 3.2")
|
tmux_32=$(awk '{print ($1 >= 3.2)}' <<< "$tmux_version" 2> /dev/null || bc -l <<< "$tmux_version >= 3.2")
|
||||||
@@ -47,7 +47,7 @@ help() {
|
|||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
arg="$1"
|
arg="$1"
|
||||||
shift
|
shift
|
||||||
[[ -z "$skip" ]] && case "$arg" in
|
[[ -z $skip ]] && case "$arg" in
|
||||||
-)
|
-)
|
||||||
term=1
|
term=1
|
||||||
;;
|
;;
|
||||||
@@ -58,19 +58,19 @@ while [[ $# -gt 0 ]]; do
|
|||||||
echo "fzf-tmux (with fzf $("$fzf" --version))"
|
echo "fzf-tmux (with fzf $("$fzf" --version))"
|
||||||
exit
|
exit
|
||||||
;;
|
;;
|
||||||
-p*|-w*|-h*|-x*|-y*|-d*|-u*|-r*|-l*)
|
-p* | -w* | -h* | -x* | -y* | -d* | -u* | -r* | -l*)
|
||||||
if [[ "$arg" =~ ^-[pwhxy] ]]; then
|
if [[ $arg =~ ^-[pwhxy] ]]; then
|
||||||
[[ "$opt" =~ "-E" ]] || opt="-E"
|
[[ $opt =~ "-E" ]] || opt="-E"
|
||||||
elif [[ "$arg" =~ ^.[lr] ]]; then
|
elif [[ $arg =~ ^.[lr] ]]; then
|
||||||
opt="-h"
|
opt="-h"
|
||||||
if [[ "$arg" =~ ^.l ]]; then
|
if [[ $arg =~ ^.l ]]; then
|
||||||
opt="$opt -d"
|
opt="$opt -d"
|
||||||
swap="; swap-pane -D ; select-pane -L"
|
swap="; swap-pane -D ; select-pane -L"
|
||||||
close="; tmux swap-pane -D"
|
close="; tmux swap-pane -D"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
opt=""
|
opt=""
|
||||||
if [[ "$arg" =~ ^.u ]]; then
|
if [[ $arg =~ ^.u ]]; then
|
||||||
opt="$opt -d"
|
opt="$opt -d"
|
||||||
swap="; swap-pane -D ; select-pane -U"
|
swap="; swap-pane -D ; select-pane -U"
|
||||||
close="; tmux swap-pane -D"
|
close="; tmux swap-pane -D"
|
||||||
@@ -79,7 +79,7 @@ while [[ $# -gt 0 ]]; do
|
|||||||
if [[ ${#arg} -gt 2 ]]; then
|
if [[ ${#arg} -gt 2 ]]; then
|
||||||
size="${arg:2}"
|
size="${arg:2}"
|
||||||
else
|
else
|
||||||
if [[ "$1" =~ ^[0-9%,]+$ ]] || [[ "$1" =~ ^[A-Z]$ ]]; then
|
if [[ $1 =~ ^[0-9%,]+$ ]] || [[ $1 =~ ^[A-Z]$ ]]; then
|
||||||
size="$1"
|
size="$1"
|
||||||
shift
|
shift
|
||||||
else
|
else
|
||||||
@@ -87,37 +87,37 @@ while [[ $# -gt 0 ]]; do
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$arg" =~ ^-p ]]; then
|
if [[ $arg =~ ^-p ]]; then
|
||||||
if [[ -n "$size" ]]; then
|
if [[ -n $size ]]; then
|
||||||
w=${size%%,*}
|
w=${size%%,*}
|
||||||
h=${size##*,}
|
h=${size##*,}
|
||||||
opt="$opt -w$w -h$h"
|
opt="$opt -w$w -h$h"
|
||||||
fi
|
fi
|
||||||
elif [[ "$arg" =~ ^-[whxy] ]]; then
|
elif [[ $arg =~ ^-[whxy] ]]; then
|
||||||
opt="$opt ${arg:0:2}$size"
|
opt="$opt ${arg:0:2}$size"
|
||||||
elif [[ "$size" =~ %$ ]]; then
|
elif [[ $size =~ %$ ]]; then
|
||||||
size=${size:0:((${#size}-1))}
|
size=${size:0:${#size}-1}
|
||||||
if [[ $tmux_32 = 1 ]]; then
|
if [[ $tmux_32 == 1 ]]; then
|
||||||
if [[ -n "$swap" ]]; then
|
if [[ -n $swap ]]; then
|
||||||
opt="$opt -l $(( 100 - size ))%"
|
opt="$opt -l $((100 - size))%"
|
||||||
else
|
else
|
||||||
opt="$opt -l $size%"
|
opt="$opt -l $size%"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
if [[ -n "$swap" ]]; then
|
if [[ -n $swap ]]; then
|
||||||
opt="$opt -p $(( 100 - size ))"
|
opt="$opt -p $((100 - size))"
|
||||||
else
|
else
|
||||||
opt="$opt -p $size"
|
opt="$opt -p $size"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
if [[ -n "$swap" ]]; then
|
if [[ -n $swap ]]; then
|
||||||
if [[ "$arg" =~ ^.l ]]; then
|
if [[ $arg =~ ^.l ]]; then
|
||||||
max=$columns
|
max=$columns
|
||||||
else
|
else
|
||||||
max=$lines
|
max=$lines
|
||||||
fi
|
fi
|
||||||
size=$(( max - size ))
|
size=$((max - size))
|
||||||
[[ $size -lt 0 ]] && size=0
|
[[ $size -lt 0 ]] && size=0
|
||||||
opt="$opt -l $size"
|
opt="$opt -l $size"
|
||||||
else
|
else
|
||||||
@@ -135,10 +135,10 @@ while [[ $# -gt 0 ]]; do
|
|||||||
args+=("$arg")
|
args+=("$arg")
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
[[ -n "$skip" ]] && args+=("$arg")
|
[[ -n $skip ]] && args+=("$arg")
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ -z "$TMUX" ]]; then
|
if [[ -z $TMUX ]]; then
|
||||||
"$fzf" "${args[@]}"
|
"$fzf" "${args[@]}"
|
||||||
exit $?
|
exit $?
|
||||||
fi
|
fi
|
||||||
@@ -149,7 +149,7 @@ fi
|
|||||||
args=("${args[@]}" "--no-height" "--bind=ctrl-z:ignore" "--no-tmux")
|
args=("${args[@]}" "--no-height" "--bind=ctrl-z:ignore" "--no-tmux")
|
||||||
|
|
||||||
# Handle zoomed tmux pane without popup options by moving it to a temp window
|
# Handle zoomed tmux pane without popup options by moving it to a temp window
|
||||||
if [[ ! "$opt" =~ "-E" ]] && tmux list-panes -F '#F' | grep -q Z; then
|
if [[ ! $opt =~ "-E" ]] && tmux list-panes -F '#F' | grep -q Z; then
|
||||||
zoomed_without_popup=1
|
zoomed_without_popup=1
|
||||||
original_window=$(tmux display-message -p "#{window_id}")
|
original_window=$(tmux display-message -p "#{window_id}")
|
||||||
tmp_window=$(tmux new-window -d -P -F "#{window_id}" "bash -c 'while :; do for c in \\| / - '\\;' do sleep 0.2; printf \"\\r\$c fzf-tmux is running\\r\"; done; done'")
|
tmp_window=$(tmux new-window -d -P -F "#{window_id}" "bash -c 'while :; do for c in \\| / - '\\;' do sleep 0.2; printf \"\\r\$c fzf-tmux is running\\r\"; done; done'")
|
||||||
@@ -159,28 +159,27 @@ fi
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Clean up named pipes on exit
|
# Clean up named pipes on exit
|
||||||
id=$RANDOM
|
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/fzf-tmux-XXXXXX")
|
||||||
argsf="${TMPDIR:-/tmp}/fzf-args-$id"
|
argsf="$tmpdir/args"
|
||||||
fifo1="${TMPDIR:-/tmp}/fzf-fifo1-$id"
|
fifo1="$tmpdir/fifo1"
|
||||||
fifo2="${TMPDIR:-/tmp}/fzf-fifo2-$id"
|
fifo2="$tmpdir/fifo2"
|
||||||
fifo3="${TMPDIR:-/tmp}/fzf-fifo3-$id"
|
fifo3="$tmpdir/fifo3"
|
||||||
if tmux_win_opts=$(tmux show-options -p remain-on-exit \; show-options -p synchronize-panes 2> /dev/null); then
|
if tmux_win_opts=$(tmux show-options -p remain-on-exit \; show-options -p synchronize-panes 2> /dev/null); then
|
||||||
tmux_win_opts=( $(sed '/ off/d; s/synchronize-panes/set-option -p synchronize-panes/; s/remain-on-exit/set-option -p remain-on-exit/; s/$/ \\;/' <<< "$tmux_win_opts") )
|
tmux_win_opts=($(sed '/ off/d; s/synchronize-panes/set-option -p synchronize-panes/; s/remain-on-exit/set-option -p remain-on-exit/; s/$/ \\;/' <<< "$tmux_win_opts"))
|
||||||
tmux_off_opts='; set-option -p synchronize-panes off ; set-option -p remain-on-exit off'
|
tmux_off_opts='; set-option -p synchronize-panes off ; set-option -p remain-on-exit off'
|
||||||
else
|
else
|
||||||
tmux_win_opts=( $(tmux show-window-options remain-on-exit \; show-window-options synchronize-panes | sed '/ off/d; s/^/set-window-option /; s/$/ \\;/') )
|
tmux_win_opts=($(tmux show-window-options remain-on-exit \; show-window-options synchronize-panes | sed '/ off/d; s/^/set-window-option /; s/$/ \\;/'))
|
||||||
tmux_off_opts='; set-window-option synchronize-panes off ; set-window-option remain-on-exit off'
|
tmux_off_opts='; set-window-option synchronize-panes off ; set-window-option remain-on-exit off'
|
||||||
fi
|
fi
|
||||||
cleanup() {
|
cleanup() {
|
||||||
\rm -f $argsf $fifo1 $fifo2 $fifo3
|
\rm -rf "$tmpdir"
|
||||||
|
|
||||||
# Restore tmux window options
|
# Restore tmux window options
|
||||||
if [[ "${#tmux_win_opts[@]}" -gt 1 ]]; then
|
if [[ ${#tmux_win_opts[@]} -gt 1 ]]; then
|
||||||
eval "tmux ${tmux_win_opts[*]}"
|
eval "tmux ${tmux_win_opts[*]}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Remove temp window if we were zoomed without popup options
|
# Remove temp window if we were zoomed without popup options
|
||||||
if [[ -n "$zoomed_without_popup" ]]; then
|
if [[ -n $zoomed_without_popup ]]; then
|
||||||
tmux display-message -p "#{window_id}" > /dev/null
|
tmux display-message -p "#{window_id}" > /dev/null
|
||||||
tmux swap-pane -t $original_window \; \
|
tmux swap-pane -t $original_window \; \
|
||||||
select-window -t $original_window \; \
|
select-window -t $original_window \; \
|
||||||
@@ -196,11 +195,11 @@ cleanup() {
|
|||||||
trap 'cleanup 1' SIGUSR1
|
trap 'cleanup 1' SIGUSR1
|
||||||
trap 'cleanup' EXIT
|
trap 'cleanup' EXIT
|
||||||
|
|
||||||
envs="export TERM=$TERM "
|
envs="export TERM=$(printf %q "$TERM") "
|
||||||
if [[ "$opt" =~ "-E" ]]; then
|
if [[ $opt =~ "-E" ]]; then
|
||||||
if [[ $tmux_version = 3.2 ]]; then
|
if [[ $tmux_version == 3.2 ]]; then
|
||||||
FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS"
|
FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS"
|
||||||
elif [[ $tmux_32 = 1 ]]; then
|
elif [[ $tmux_32 == 1 ]]; then
|
||||||
FZF_DEFAULT_OPTS="--border $FZF_DEFAULT_OPTS"
|
FZF_DEFAULT_OPTS="--border $FZF_DEFAULT_OPTS"
|
||||||
opt="-B $opt"
|
opt="-B $opt"
|
||||||
else
|
else
|
||||||
@@ -211,8 +210,8 @@ fi
|
|||||||
envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")"
|
envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")"
|
||||||
envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")"
|
envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")"
|
||||||
envs="$envs FZF_DEFAULT_OPTS_FILE=$(printf %q "$FZF_DEFAULT_OPTS_FILE")"
|
envs="$envs FZF_DEFAULT_OPTS_FILE=$(printf %q "$FZF_DEFAULT_OPTS_FILE")"
|
||||||
[[ -n "$RUNEWIDTH_EASTASIAN" ]] && envs="$envs RUNEWIDTH_EASTASIAN=$(printf %q "$RUNEWIDTH_EASTASIAN")"
|
[[ -n $RUNEWIDTH_EASTASIAN ]] && envs="$envs RUNEWIDTH_EASTASIAN=$(printf %q "$RUNEWIDTH_EASTASIAN")"
|
||||||
[[ -n "$BAT_THEME" ]] && envs="$envs BAT_THEME=$(printf %q "$BAT_THEME")"
|
[[ -n $BAT_THEME ]] && envs="$envs BAT_THEME=$(printf %q "$BAT_THEME")"
|
||||||
echo "$envs;" > "$argsf"
|
echo "$envs;" > "$argsf"
|
||||||
|
|
||||||
# Build arguments to fzf
|
# Build arguments to fzf
|
||||||
@@ -224,9 +223,9 @@ close="; trap - EXIT SIGINT SIGTERM $close"
|
|||||||
|
|
||||||
export TMUX=$(cut -d , -f 1,2 <<< "$TMUX")
|
export TMUX=$(cut -d , -f 1,2 <<< "$TMUX")
|
||||||
mkfifo -m o+w $fifo2
|
mkfifo -m o+w $fifo2
|
||||||
if [[ "$opt" =~ "-E" ]]; then
|
if [[ $opt =~ "-E" ]]; then
|
||||||
cat $fifo2 &
|
cat $fifo2 &
|
||||||
if [[ -n "$term" ]] || [[ -t 0 ]]; then
|
if [[ -n $term ]] || [[ -t 0 ]]; then
|
||||||
cat <<< "\"$fzf\" $opts > $fifo2; out=\$? $close; exit \$out" >> $argsf
|
cat <<< "\"$fzf\" $opts > $fifo2; out=\$? $close; exit \$out" >> $argsf
|
||||||
else
|
else
|
||||||
mkfifo $fifo1
|
mkfifo $fifo1
|
||||||
@@ -239,7 +238,7 @@ if [[ "$opt" =~ "-E" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
mkfifo -m o+w $fifo3
|
mkfifo -m o+w $fifo3
|
||||||
if [[ -n "$term" ]] || [[ -t 0 ]]; then
|
if [[ -n $term ]] || [[ -t 0 ]]; then
|
||||||
cat <<< "\"$fzf\" $opts > $fifo2; echo \$? > $fifo3 $close" >> $argsf
|
cat <<< "\"$fzf\" $opts > $fifo2; echo \$? > $fifo3 $close" >> $argsf
|
||||||
else
|
else
|
||||||
mkfifo $fifo1
|
mkfifo $fifo1
|
||||||
@@ -249,6 +248,9 @@ fi
|
|||||||
tmux \
|
tmux \
|
||||||
split-window -c "$PWD" $opt "bash -c 'exec -a fzf bash $argsf'" $swap \
|
split-window -c "$PWD" $opt "bash -c 'exec -a fzf bash $argsf'" $swap \
|
||||||
$tmux_off_opts \
|
$tmux_off_opts \
|
||||||
> /dev/null 2>&1 || { "$fzf" "${args[@]}"; exit $?; }
|
> /dev/null 2>&1 || {
|
||||||
|
"$fzf" "${args[@]}"
|
||||||
|
exit $?
|
||||||
|
}
|
||||||
cat $fifo2
|
cat $fifo2
|
||||||
exit "$(cat $fifo3)"
|
exit "$(cat $fifo3)"
|
||||||
|
|||||||
+1
-1
@@ -503,7 +503,7 @@ LICENSE *fzf-license*
|
|||||||
|
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2013-2025 Junegunn Choi
|
Copyright (c) 2013-2026 Junegunn Choi
|
||||||
|
|
||||||
==============================================================================
|
==============================================================================
|
||||||
vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap:
|
vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
module github.com/junegunn/fzf
|
module github.com/junegunn/fzf
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/charlievieth/fastwalk v1.0.13
|
github.com/charlievieth/fastwalk v1.0.14
|
||||||
github.com/gdamore/tcell/v2 v2.9.0
|
github.com/gdamore/tcell/v2 v2.9.0
|
||||||
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741
|
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741
|
||||||
github.com/mattn/go-isatty v0.0.20
|
github.com/mattn/go-isatty v0.0.20
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
github.com/charlievieth/fastwalk v1.0.13 h1:rCdesaKpxBft4jdNqKbJtTS23Dfhem3eEPE0jfj//xc=
|
github.com/charlievieth/fastwalk v1.0.14 h1:3Eh5uaFGwHZd8EGwTjJnSpBkfwfsak9h6ICgnWlhAyg=
|
||||||
github.com/charlievieth/fastwalk v1.0.13/go.mod h1:diVcUreiU1aQ4/Wu3NbxxH4/KYdKpLDojrQ1Bb2KgNY=
|
github.com/charlievieth/fastwalk v1.0.14/go.mod h1:diVcUreiU1aQ4/Wu3NbxxH4/KYdKpLDojrQ1Bb2KgNY=
|
||||||
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
|
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
|
||||||
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
|
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
|
||||||
github.com/gdamore/tcell/v2 v2.9.0 h1:N6t+eqK7/xwtRPwxzs1PXeRWnm0H9l02CrgJ7DLn1ys=
|
github.com/gdamore/tcell/v2 v2.9.0 h1:N6t+eqK7/xwtRPwxzs1PXeRWnm0H9l02CrgJ7DLn1ys=
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
version=0.65.2
|
version=0.68.0
|
||||||
auto_completion=
|
auto_completion=
|
||||||
key_bindings=
|
key_bindings=
|
||||||
update_config=2
|
update_config=2
|
||||||
@@ -46,16 +46,16 @@ for opt in "$@"; do
|
|||||||
prefix_expand=${XDG_CONFIG_HOME:-$HOME/.config}/fzf/fzf
|
prefix_expand=${XDG_CONFIG_HOME:-$HOME/.config}/fzf/fzf
|
||||||
mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/fzf"
|
mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/fzf"
|
||||||
;;
|
;;
|
||||||
--key-bindings) key_bindings=1 ;;
|
--key-bindings) key_bindings=1 ;;
|
||||||
--no-key-bindings) key_bindings=0 ;;
|
--no-key-bindings) key_bindings=0 ;;
|
||||||
--completion) auto_completion=1 ;;
|
--completion) auto_completion=1 ;;
|
||||||
--no-completion) auto_completion=0 ;;
|
--no-completion) auto_completion=0 ;;
|
||||||
--update-rc) update_config=1 ;;
|
--update-rc) update_config=1 ;;
|
||||||
--no-update-rc) update_config=0 ;;
|
--no-update-rc) update_config=0 ;;
|
||||||
--bin) ;;
|
--bin) ;;
|
||||||
--no-bash) shells=${shells/bash/} ;;
|
--no-bash) shells=${shells/bash/} ;;
|
||||||
--no-zsh) shells=${shells/zsh/} ;;
|
--no-zsh) shells=${shells/zsh/} ;;
|
||||||
--no-fish) shells=${shells/fish/} ;;
|
--no-fish) shells=${shells/fish/} ;;
|
||||||
*)
|
*)
|
||||||
echo "unknown option: $opt"
|
echo "unknown option: $opt"
|
||||||
help
|
help
|
||||||
@@ -104,7 +104,7 @@ check_binary() {
|
|||||||
|
|
||||||
link_fzf_in_path() {
|
link_fzf_in_path() {
|
||||||
if which_fzf="$(command -v fzf)"; then
|
if which_fzf="$(command -v fzf)"; then
|
||||||
echo " - Found in \$PATH"
|
echo ' - Found in $PATH'
|
||||||
echo " - Creating symlink: bin/fzf -> $which_fzf"
|
echo " - Creating symlink: bin/fzf -> $which_fzf"
|
||||||
(cd "$fzf_base"/bin && rm -f fzf && ln -sf "$which_fzf" fzf)
|
(cd "$fzf_base"/bin && rm -f fzf && ln -sf "$which_fzf" fzf)
|
||||||
check_binary && return
|
check_binary && return
|
||||||
@@ -114,22 +114,22 @@ link_fzf_in_path() {
|
|||||||
|
|
||||||
try_curl() {
|
try_curl() {
|
||||||
command -v curl > /dev/null &&
|
command -v curl > /dev/null &&
|
||||||
if [[ $1 =~ tar.gz$ ]]; then
|
if [[ $1 =~ tar.gz$ ]]; then
|
||||||
curl -fL $1 | tar --no-same-owner -xzf -
|
curl -fL $1 | tar --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"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
try_wget() {
|
try_wget() {
|
||||||
command -v wget > /dev/null &&
|
command -v wget > /dev/null &&
|
||||||
if [[ $1 =~ tar.gz$ ]]; then
|
if [[ $1 =~ tar.gz$ ]]; then
|
||||||
wget -O - $1 | tar --no-same-owner -xzf -
|
wget -O - $1 | tar --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"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
download() {
|
download() {
|
||||||
@@ -164,29 +164,30 @@ download() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Try to download binary executable
|
# Try to download binary executable
|
||||||
archi=$(uname -smo 2>/dev/null || uname -sm)
|
archi=$(uname -smo 2> /dev/null || uname -sm)
|
||||||
binary_available=1
|
binary_available=1
|
||||||
binary_error=""
|
binary_error=""
|
||||||
case "$archi" in
|
case "$archi" in
|
||||||
Darwin\ arm64*) download fzf-$version-darwin_arm64.tar.gz ;;
|
Darwin\ arm64*) download fzf-$version-darwin_arm64.tar.gz ;;
|
||||||
Darwin\ x86_64*) download fzf-$version-darwin_amd64.tar.gz ;;
|
Darwin\ x86_64*) download fzf-$version-darwin_amd64.tar.gz ;;
|
||||||
Linux\ armv5*) download fzf-$version-linux_armv5.tar.gz ;;
|
Linux\ armv5*) download fzf-$version-linux_armv5.tar.gz ;;
|
||||||
Linux\ armv6*) download fzf-$version-linux_armv6.tar.gz ;;
|
Linux\ armv6*) download fzf-$version-linux_armv6.tar.gz ;;
|
||||||
Linux\ armv7*) download fzf-$version-linux_armv7.tar.gz ;;
|
Linux\ armv7*) download fzf-$version-linux_armv7.tar.gz ;;
|
||||||
Linux\ armv8*) download fzf-$version-linux_arm64.tar.gz ;;
|
Linux\ armv8*) download fzf-$version-linux_arm64.tar.gz ;;
|
||||||
Linux\ aarch64\ Android) download fzf-$version-android_arm64.tar.gz ;;
|
Linux\ aarch64\ Android) download fzf-$version-android_arm64.tar.gz ;;
|
||||||
Linux\ aarch64*) download fzf-$version-linux_arm64.tar.gz ;;
|
Linux\ aarch64*) download fzf-$version-linux_arm64.tar.gz ;;
|
||||||
Linux\ loongarch64*) download fzf-$version-linux_loong64.tar.gz ;;
|
Linux\ loongarch64*) download fzf-$version-linux_loong64.tar.gz ;;
|
||||||
Linux\ ppc64le*) download fzf-$version-linux_ppc64le.tar.gz ;;
|
Linux\ riscv64*) download fzf-$version-linux_riscv64.tar.gz ;;
|
||||||
Linux\ *64*) download fzf-$version-linux_amd64.tar.gz ;;
|
Linux\ ppc64le*) download fzf-$version-linux_ppc64le.tar.gz ;;
|
||||||
Linux\ s390x*) download fzf-$version-linux_s390x.tar.gz ;;
|
Linux\ *64*) download fzf-$version-linux_amd64.tar.gz ;;
|
||||||
FreeBSD\ *64*) download fzf-$version-freebsd_amd64.tar.gz ;;
|
Linux\ s390x*) download fzf-$version-linux_s390x.tar.gz ;;
|
||||||
OpenBSD\ *64*) download fzf-$version-openbsd_amd64.tar.gz ;;
|
FreeBSD\ *64*) download fzf-$version-freebsd_amd64.tar.gz ;;
|
||||||
CYGWIN*\ *64*) download fzf-$version-windows_amd64.zip ;;
|
OpenBSD\ *64*) download fzf-$version-openbsd_amd64.tar.gz ;;
|
||||||
MINGW*\ *64*) download fzf-$version-windows_amd64.zip ;;
|
CYGWIN*\ *64*) download fzf-$version-windows_amd64.zip ;;
|
||||||
MSYS*\ *64*) download fzf-$version-windows_amd64.zip ;;
|
MINGW*\ *64*) download fzf-$version-windows_amd64.zip ;;
|
||||||
Windows*\ *64*) download fzf-$version-windows_amd64.zip ;;
|
MSYS*\ *64*) download fzf-$version-windows_amd64.zip ;;
|
||||||
*) binary_available=0 binary_error=1 ;;
|
Windows*\ *64*) download fzf-$version-windows_amd64.zip ;;
|
||||||
|
*) binary_available=0 binary_error=1 ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
cd "$fzf_base"
|
cd "$fzf_base"
|
||||||
@@ -215,7 +216,7 @@ if [ -n "$binary_error" ]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[[ "$*" =~ "--bin" ]] && exit 0
|
[[ $* =~ "--bin" ]] && exit 0
|
||||||
|
|
||||||
for s in $shells; do
|
for s in $shells; do
|
||||||
if ! command -v "$s" > /dev/null; then
|
if ! command -v "$s" > /dev/null; then
|
||||||
@@ -242,16 +243,16 @@ fi
|
|||||||
|
|
||||||
echo
|
echo
|
||||||
for shell in $shells; do
|
for shell in $shells; do
|
||||||
[[ "$shell" = fish ]] && continue
|
fzf_completion="source \"$fzf_base/shell/completion.${shell}\""
|
||||||
|
fzf_key_bindings="source \"$fzf_base/shell/key-bindings.${shell}\""
|
||||||
|
[[ $shell == fish ]] && continue
|
||||||
src=${prefix_expand}.${shell}
|
src=${prefix_expand}.${shell}
|
||||||
echo -n "Generate $src ... "
|
echo -n "Generate $src ... "
|
||||||
|
|
||||||
fzf_completion="source \"$fzf_base/shell/completion.${shell}\""
|
|
||||||
if [ $auto_completion -eq 0 ]; then
|
if [ $auto_completion -eq 0 ]; then
|
||||||
fzf_completion="# $fzf_completion"
|
fzf_completion="# $fzf_completion"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
fzf_key_bindings="source \"$fzf_base/shell/key-bindings.${shell}\""
|
|
||||||
if [ $key_bindings -eq 0 ]; then
|
if [ $key_bindings -eq 0 ]; then
|
||||||
fzf_key_bindings="# $fzf_key_bindings"
|
fzf_key_bindings="# $fzf_key_bindings"
|
||||||
fi
|
fi
|
||||||
@@ -266,7 +267,7 @@ fi
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
if [[ $auto_completion -eq 1 ]] && [[ $key_bindings -eq 1 ]]; then
|
if [[ $auto_completion -eq 1 ]] && [[ $key_bindings -eq 1 ]]; then
|
||||||
if [[ "$shell" = zsh ]]; then
|
if [[ $shell == zsh ]]; then
|
||||||
echo "source <(fzf --$shell)" >> "$src"
|
echo "source <(fzf --$shell)" >> "$src"
|
||||||
else
|
else
|
||||||
echo "eval \"\$(fzf --$shell)\"" >> "$src"
|
echo "eval \"\$(fzf --$shell)\"" >> "$src"
|
||||||
@@ -286,7 +287,7 @@ EOF
|
|||||||
done
|
done
|
||||||
|
|
||||||
# fish
|
# fish
|
||||||
if [[ "$shells" =~ fish ]]; then
|
if [[ $shells =~ fish ]]; then
|
||||||
echo -n "Update fish_user_paths ... "
|
echo -n "Update fish_user_paths ... "
|
||||||
fish << EOF
|
fish << EOF
|
||||||
echo \$fish_user_paths | \grep "$fzf_base"/bin > /dev/null
|
echo \$fish_user_paths | \grep "$fzf_base"/bin > /dev/null
|
||||||
@@ -301,15 +302,16 @@ append_line() {
|
|||||||
line="$2"
|
line="$2"
|
||||||
file="$3"
|
file="$3"
|
||||||
pat="${4:-}"
|
pat="${4:-}"
|
||||||
|
at_lno="${5:-}"
|
||||||
lines=""
|
lines=""
|
||||||
|
|
||||||
echo "Update $file:"
|
echo "Update $file:"
|
||||||
echo " - $line"
|
echo " - $line"
|
||||||
if [ -f "$file" ]; then
|
if [ -f "$file" ]; then
|
||||||
if [ $# -lt 4 ]; then
|
if [[ -n $pat ]]; then
|
||||||
lines=$(\grep -nF "$line" "$file")
|
|
||||||
else
|
|
||||||
lines=$(\grep -nF "$pat" "$file")
|
lines=$(\grep -nF "$pat" "$file")
|
||||||
|
else
|
||||||
|
lines=$(\grep -nF "${line#"${line%%[![:space:]]*}"}" "$file")
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -318,17 +320,21 @@ append_line() {
|
|||||||
sed 's/^/ Line /' <<< "$lines"
|
sed 's/^/ Line /' <<< "$lines"
|
||||||
|
|
||||||
update=0
|
update=0
|
||||||
if ! \grep -qv "^[0-9]*:[[:space:]]*#" <<< "$lines" ; then
|
if ! \grep -qv "^[0-9]*:[[:space:]]*#" <<< "$lines"; then
|
||||||
echo " - But they all seem to be commented"
|
echo " - But they all seem to be commented"
|
||||||
ask " - Continue modifying $file?"
|
ask " - Continue modifying $file?"
|
||||||
update=$?
|
update=$?
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
if [ "$update" -eq 1 ]; then
|
if [ "$update" -eq 1 ]; then
|
||||||
[ -f "$file" ] && echo >> "$file"
|
if [[ -z $at_lno ]]; then
|
||||||
echo "$line" >> "$file"
|
[ -f "$file" ] && echo >> "$file"
|
||||||
|
echo "$line" >> "$file"
|
||||||
|
else
|
||||||
|
sed -i.~fzf_bak "${at_lno}a\\"$'\n'"$line" "$file" && rm "$file.~fzf_bak"
|
||||||
|
fi
|
||||||
echo " + Added"
|
echo " + Added"
|
||||||
else
|
else
|
||||||
echo " ~ Skipped"
|
echo " ~ Skipped"
|
||||||
@@ -356,43 +362,84 @@ if [ $update_config -eq 2 ]; then
|
|||||||
fi
|
fi
|
||||||
echo
|
echo
|
||||||
for shell in $shells; do
|
for shell in $shells; do
|
||||||
[[ "$shell" = fish ]] && continue
|
[[ $shell == fish ]] && continue
|
||||||
[ $shell = zsh ] && dest=${ZDOTDIR:-~}/.zshrc || dest=~/.bashrc
|
[ $shell = zsh ] && dest=${ZDOTDIR:-~}/.zshrc || dest=~/.bashrc
|
||||||
append_line $update_config "[ -f ${prefix}.${shell} ] && source ${prefix}.${shell}" "$dest" "${prefix}.${shell}"
|
append_line $update_config "[ -f ${prefix}.${shell} ] && source ${prefix}.${shell}" "$dest" "${prefix}.${shell}"
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ $key_bindings -eq 1 ] && [[ "$shells" =~ fish ]]; then
|
if [[ $shells =~ fish ]]; then
|
||||||
bind_file="${fish_dir}/functions/fish_user_key_bindings.fish"
|
bind_file="${fish_dir}/functions/fish_user_key_bindings.fish"
|
||||||
if [ ! -e "$bind_file" ]; then
|
if [ ! -e "$bind_file" ]; then
|
||||||
mkdir -p "${fish_dir}/functions"
|
if [[ $key_bindings -eq 1 || $auto_completion -eq 1 ]]; then
|
||||||
create_file "$bind_file" \
|
mkdir -p "${fish_dir}/functions"
|
||||||
'function fish_user_key_bindings' \
|
if [[ $key_bindings -eq 1 && $auto_completion -eq 1 ]]; then
|
||||||
' fzf --fish | source' \
|
create_file "$bind_file" \
|
||||||
'end'
|
'function fish_user_key_bindings' \
|
||||||
|
' fzf --fish | source' \
|
||||||
|
'end'
|
||||||
|
elif [[ $key_bindings -eq 1 ]]; then
|
||||||
|
create_file "$bind_file" \
|
||||||
|
'function fish_user_key_bindings' \
|
||||||
|
" $fzf_key_bindings" \
|
||||||
|
'end'
|
||||||
|
elif [[ $auto_completion -eq 1 ]]; then
|
||||||
|
create_file "$bind_file" \
|
||||||
|
'function fish_user_key_bindings' \
|
||||||
|
" $fzf_completion" \
|
||||||
|
'end'
|
||||||
|
fi
|
||||||
|
lno_func=$(\grep -nF "function fish_user_key_bindings" "$bind_file" | sed 's/:.*//' | tr '\n' ' ')
|
||||||
|
else
|
||||||
|
lno_func=0
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
echo "Check $bind_file:"
|
echo "Check $bind_file:"
|
||||||
lno=$(\grep -nF "fzf_key_bindings" "$bind_file" | sed 's/:.*//' | tr '\n' ' ')
|
lno_func=$(\grep -nF "function fish_user_key_bindings" "$bind_file" | sed 's/:.*//' | tr '\n' ' ')
|
||||||
if [[ -n $lno ]]; then
|
if [[ -z $lno_func ]]; then
|
||||||
echo " ** Found 'fzf_key_bindings' in line #$lno"
|
echo -e "function fish_user_key_bindings\nend" >> "$bind_file"
|
||||||
echo " ** You have to replace the line to 'fzf --fish | source'"
|
lno_func=$(\grep -nF "function fish_user_key_bindings" "$bind_file" | sed 's/:.*//' | tr '\n' ' ')
|
||||||
|
fi
|
||||||
|
lno_keys=$(\grep -nF "fzf_key_bindings" "$bind_file" | sed 's/:.*//' | tr '\n' ' ')
|
||||||
|
if [[ -n $lno_keys ]]; then
|
||||||
|
echo " ** Found 'fzf_key_bindings' in line #$lno_keys"
|
||||||
|
if [[ $key_bindings -eq 1 && $auto_completion -eq 1 ]]; then
|
||||||
|
echo " ** You have to replace the line to 'fzf --fish | source'"
|
||||||
|
elif [[ $key_bindings -eq 1 ]]; then
|
||||||
|
echo " ** You have to replace the line to '$fzf_key_bindings'"
|
||||||
|
else
|
||||||
|
echo " ** You have to remove the line"
|
||||||
|
fi
|
||||||
echo
|
echo
|
||||||
else
|
else
|
||||||
echo " - Clear"
|
echo " - Clear"
|
||||||
echo
|
echo
|
||||||
append_line $update_config "fzf --fish | source" "$bind_file"
|
if [[ $key_bindings -eq 1 && $auto_completion -eq 1 ]]; then
|
||||||
|
sed -i.~fzf_bak "\#$fzf_completion#d" "$bind_file" && rm "$bind_file.~fzf_bak"
|
||||||
|
sed -i.~fzf_bak "\#$fzf_key_bindings#d" "$bind_file" && rm "$bind_file.~fzf_bak"
|
||||||
|
append_line $update_config " fzf --fish | source" "$bind_file" "" "$lno_func"
|
||||||
|
else
|
||||||
|
sed -i.~fzf_bak '/fzf --fish \| source/d' "$bind_file" && rm "$bind_file.~fzf_bak"
|
||||||
|
if [[ $key_bindings -eq 1 ]]; then
|
||||||
|
sed -i.~fzf_bak "\#$fzf_completion#d" "$bind_file" && rm "$bind_file.~fzf_bak"
|
||||||
|
append_line $update_config " $fzf_key_bindings" "$bind_file" "" "$lno_func"
|
||||||
|
elif [[ $auto_completion -eq 1 ]]; then
|
||||||
|
sed -i.~fzf_bak "\#$fzf_key_bindings#d" "$bind_file" && rm "$bind_file.~fzf_bak"
|
||||||
|
append_line $update_config " $fzf_completion" "$bind_file" "" "$lno_func"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ $update_config -eq 1 ]; then
|
if [ $update_config -eq 1 ]; then
|
||||||
echo 'Finished. Restart your shell or reload config file.'
|
echo 'Finished. Restart your shell or reload config file.'
|
||||||
if [[ "$shells" =~ bash ]]; then
|
if [[ $shells =~ bash ]]; then
|
||||||
echo -n ' source ~/.bashrc # bash'
|
echo -n ' source ~/.bashrc # bash'
|
||||||
[[ "$archi" =~ Darwin ]] && echo -n ' (.bashrc should be loaded from .bash_profile)'
|
[[ $archi =~ Darwin ]] && echo -n ' (.bashrc should be loaded from .bash_profile)'
|
||||||
echo
|
echo
|
||||||
fi
|
fi
|
||||||
[[ "$shells" =~ zsh ]] && echo " source ${ZDOTDIR:-~}/.zshrc # zsh"
|
[[ $shells =~ zsh ]] && echo " source ${ZDOTDIR:-~}/.zshrc # zsh"
|
||||||
[[ "$shells" =~ fish ]] && [ $key_bindings -eq 1 ] && echo ' fzf_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
-1
@@ -1,4 +1,4 @@
|
|||||||
$version="0.65.2"
|
$version="0.68.0"
|
||||||
|
|
||||||
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/junegunn/fzf/src/protector"
|
"github.com/junegunn/fzf/src/protector"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "0.65"
|
var version = "0.68"
|
||||||
var revision = "devel"
|
var revision = "devel"
|
||||||
|
|
||||||
//go:embed shell/key-bindings.bash
|
//go:embed shell/key-bindings.bash
|
||||||
@@ -29,6 +29,9 @@ var zshCompletion []byte
|
|||||||
//go:embed shell/key-bindings.fish
|
//go:embed shell/key-bindings.fish
|
||||||
var fishKeyBindings []byte
|
var fishKeyBindings []byte
|
||||||
|
|
||||||
|
//go:embed shell/completion.fish
|
||||||
|
var fishCompletion []byte
|
||||||
|
|
||||||
//go:embed man/man1/fzf.1
|
//go:embed man/man1/fzf.1
|
||||||
var manPage []byte
|
var manPage []byte
|
||||||
|
|
||||||
@@ -65,7 +68,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
if options.Fish {
|
if options.Fish {
|
||||||
printScript("key-bindings.fish", fishKeyBindings)
|
printScript("key-bindings.fish", fishKeyBindings)
|
||||||
fmt.Println("fzf_key_bindings")
|
printScript("completion.fish", fishCompletion)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if options.Help {
|
if options.Help {
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
.ig
|
.ig
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2013-2025 Junegunn Choi
|
Copyright (c) 2013-2026 Junegunn Choi
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
@@ -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 "Aug 2025" "fzf 0.65.2" "fzf\-tmux - open fzf in tmux split pane"
|
.TH fzf\-tmux 1 "Feb 2026" "fzf 0.68.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
|
||||||
|
|||||||
+102
-29
@@ -1,7 +1,7 @@
|
|||||||
.ig
|
.ig
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2013-2025 Junegunn Choi
|
Copyright (c) 2013-2026 Junegunn Choi
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
@@ -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 "Aug 2025" "fzf 0.65.2" "fzf - a command-line fuzzy finder"
|
.TH fzf 1 "Feb 2026" "fzf 0.68.0" "fzf - a command-line fuzzy finder"
|
||||||
|
|
||||||
.SH NAME
|
.SH NAME
|
||||||
fzf - a command-line fuzzy finder
|
fzf - a command-line fuzzy finder
|
||||||
@@ -246,11 +246,11 @@ color mappings. Each entry is separated by a comma and/or whitespaces.
|
|||||||
|
|
||||||
.RS
|
.RS
|
||||||
.B BASE SCHEME:
|
.B BASE SCHEME:
|
||||||
(default: \fBdark\fR on 256-color terminal, otherwise \fB16\fR; If \fBNO_COLOR\fR is set, \fBbw\fR)
|
(default: \fBdark\fR on 256-color terminal, otherwise \fBbase16\fR; If \fBNO_COLOR\fR is set, \fBbw\fR)
|
||||||
|
|
||||||
\fBdark \fRColor scheme for dark 256-color terminal
|
\fBdark \fRColor scheme for dark terminal
|
||||||
\fBlight \fRColor scheme for light 256-color terminal
|
\fBlight \fRColor scheme for light terminal
|
||||||
\fB16 \fRColor scheme for 16-color terminal
|
\fBbase16 \fRColor scheme using base 16 colors (alias: \fB16\fR)
|
||||||
\fBbw \fRNo colors (equivalent to \fB\-\-no\-color\fR)
|
\fBbw \fRNo colors (equivalent to \fB\-\-no\-color\fR)
|
||||||
|
|
||||||
.B COLOR NAMES:
|
.B COLOR NAMES:
|
||||||
@@ -272,6 +272,7 @@ color mappings. Each entry is separated by a comma and/or whitespaces.
|
|||||||
\fBgutter \fRGutter on the left
|
\fBgutter \fRGutter on the left
|
||||||
\fBcurrent\-hl (hl+) \fRHighlighted substrings (current line)
|
\fBcurrent\-hl (hl+) \fRHighlighted substrings (current line)
|
||||||
\fBalt\-bg \fRAlternate background color to create striped lines
|
\fBalt\-bg \fRAlternate background color to create striped lines
|
||||||
|
\fBalt\-gutter \fRAlternate gutter color to create the striped pattern
|
||||||
\fBquery (input\-fg) \fRQuery string
|
\fBquery (input\-fg) \fRQuery string
|
||||||
\fBghost \fRGhost text (\fB\-\-ghost\fR, \fBdim\fR applied by default)
|
\fBghost \fRGhost text (\fB\-\-ghost\fR, \fBdim\fR applied by default)
|
||||||
\fBdisabled \fRQuery string when search is disabled (\fB\-\-disabled\fR)
|
\fBdisabled \fRQuery string when search is disabled (\fB\-\-disabled\fR)
|
||||||
@@ -299,6 +300,7 @@ color mappings. Each entry is separated by a comma and/or whitespaces.
|
|||||||
\fBheader (header\-fg) \fRHeader
|
\fBheader (header\-fg) \fRHeader
|
||||||
\fBfooter (footer\-fg) \fRFooter
|
\fBfooter (footer\-fg) \fRFooter
|
||||||
\fBnth \fRParts of the line specified by \fB\-\-nth\fR (only supports attributes)
|
\fBnth \fRParts of the line specified by \fB\-\-nth\fR (only supports attributes)
|
||||||
|
\fBnomatch \fRNon-matching items in raw mode (default: \fBdim\fR)
|
||||||
|
|
||||||
.B ANSI COLORS:
|
.B ANSI COLORS:
|
||||||
\fB\-1 \fRDefault terminal foreground/background color
|
\fB\-1 \fRDefault terminal foreground/background color
|
||||||
@@ -324,9 +326,14 @@ color mappings. Each entry is separated by a comma and/or whitespaces.
|
|||||||
\fB#rrggbb \fR24-bit colors
|
\fB#rrggbb \fR24-bit colors
|
||||||
|
|
||||||
.B ANSI ATTRIBUTES: (Only applies to foreground colors)
|
.B ANSI ATTRIBUTES: (Only applies to foreground colors)
|
||||||
\fBregular \fRClears previously set attributes; should precede the other ones
|
\fBregular \fRClear previously set attributes; should precede the other ones
|
||||||
|
\fBstrip \fRRemove colors
|
||||||
\fBbold\fR
|
\fBbold\fR
|
||||||
\fBunderline\fR
|
\fBunderline\fR
|
||||||
|
\fBunderline-double\fR
|
||||||
|
\fBunderline-curly\fR
|
||||||
|
\fBunderline-dotted\fR
|
||||||
|
\fBunderline-dashed\fR
|
||||||
\fBreverse\fR
|
\fBreverse\fR
|
||||||
\fBdim\fR
|
\fBdim\fR
|
||||||
\fBitalic\fR
|
\fBitalic\fR
|
||||||
@@ -586,8 +593,11 @@ Highlight the whole current line
|
|||||||
.B "\-\-cycle"
|
.B "\-\-cycle"
|
||||||
Enable cyclic scroll
|
Enable cyclic scroll
|
||||||
.TP
|
.TP
|
||||||
.B "\-\-wrap"
|
.BI "\-\-wrap" "[=MODE]"
|
||||||
Enable line wrap
|
Enable line wrap. \fIMODE\fR can be \fBchar\fR (default) or \fBword\fR.
|
||||||
|
\fBword\fR mode wraps lines at word boundaries (spaces and tabs) instead of
|
||||||
|
at arbitrary character positions. \fB\-\-wrap\-word\fR is a synonym for
|
||||||
|
\fB\-\-wrap=word\fR.
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-wrap\-sign" "=INDICATOR"
|
.BI "\-\-wrap\-sign" "=INDICATOR"
|
||||||
Indicator for wrapped lines. The default is '↳ ' or '> ' depending on
|
Indicator for wrapped lines. The default is '↳ ' or '> ' depending on
|
||||||
@@ -596,6 +606,9 @@ Indicator for wrapped lines. The default is '↳ ' or '> ' depending on
|
|||||||
.B "\-\-no\-multi\-line"
|
.B "\-\-no\-multi\-line"
|
||||||
Disable multi-line display of items when using \fB\-\-read0\fR
|
Disable multi-line display of items when using \fB\-\-read0\fR
|
||||||
.TP
|
.TP
|
||||||
|
.B "\-\-raw"
|
||||||
|
Enable raw mode where non-matching items are also displayed in a dimmed color.
|
||||||
|
.TP
|
||||||
.B "\-\-track"
|
.B "\-\-track"
|
||||||
Make fzf track the current selection when the result list is updated.
|
Make fzf track the current selection when the result list is updated.
|
||||||
This can be useful when browsing logs using fzf with sorting disabled. It is
|
This can be useful when browsing logs using fzf with sorting disabled. It is
|
||||||
@@ -624,9 +637,16 @@ Render empty lines between each item
|
|||||||
The given string will be repeated to draw a horizontal line on each gap
|
The given string will be repeated to draw a horizontal line on each gap
|
||||||
(default: '┈' or '\-' depending on \fB\-\-no\-unicode\fR).
|
(default: '┈' or '\-' depending on \fB\-\-no\-unicode\fR).
|
||||||
.TP
|
.TP
|
||||||
|
.BI "\-\-freeze\-left=" "N"
|
||||||
|
Number of fields to freeze on the left.
|
||||||
|
.TP
|
||||||
|
.BI "\-\-freeze\-right=" "N"
|
||||||
|
Number of fields to freeze on the right.
|
||||||
|
.TP
|
||||||
.B "\-\-keep\-right"
|
.B "\-\-keep\-right"
|
||||||
Keep the right end of the line visible when it's too long. Effective only when
|
Keep the right end of the line visible when it's too long. Effective only when
|
||||||
the query string is empty.
|
the query string is empty. Use \fB\-\-freeze\-right=1\fR instead if you want
|
||||||
|
the last field to be always visible even with a non-empty query.
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-scroll\-off=" "LINES"
|
.BI "\-\-scroll\-off=" "LINES"
|
||||||
Number of screen lines to keep above or below when scrolling to the top or to
|
Number of screen lines to keep above or below when scrolling to the top or to
|
||||||
@@ -643,6 +663,12 @@ on the center of the screen.
|
|||||||
.BI "\-\-jump\-labels=" "CHARS"
|
.BI "\-\-jump\-labels=" "CHARS"
|
||||||
Label characters for \fBjump\fR mode.
|
Label characters for \fBjump\fR mode.
|
||||||
.TP
|
.TP
|
||||||
|
.BI "\-\-gutter=" "CHAR"
|
||||||
|
Character used for the gutter column (default: '▌' unless \fB\-\-no\-unicode\fR is given)
|
||||||
|
.TP
|
||||||
|
.BI "\-\-gutter\-raw=" "CHAR"
|
||||||
|
Character used for the gutter column in raw mode (default: '▖' unless \fB\-\-no\-unicode\fR is given)
|
||||||
|
.TP
|
||||||
.BI "\-\-pointer=" "STR"
|
.BI "\-\-pointer=" "STR"
|
||||||
Pointer to the current line (default: '▌' or '>' depending on \fB\-\-no\-unicode\fR)
|
Pointer to the current line (default: '▌' or '>' depending on \fB\-\-no\-unicode\fR)
|
||||||
.TP
|
.TP
|
||||||
@@ -722,7 +748,7 @@ ENVIRONMENT VARIABLES EXPORTED TO CHILD PROCESSES.
|
|||||||
|
|
||||||
e.g.
|
e.g.
|
||||||
\fB# Prepend the current cursor position in yellow
|
\fB# Prepend the current cursor position in yellow
|
||||||
fzf \-\-info\-command='echo \-e "\\x1b[33;1m$FZF_POS\\x1b[m/$FZF_INFO 💛"'\fR
|
fzf \-\-info\-command='printf "\\x1b[33;1m$FZF_POS\\x1b[m/$FZF_INFO 💛"'\fR
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B "\-\-no\-info"
|
.B "\-\-no\-info"
|
||||||
@@ -822,6 +848,9 @@ e.g.
|
|||||||
# This won't work properly without 'f' flag due to ARG_MAX limit.
|
# This won't work properly without 'f' flag due to ARG_MAX limit.
|
||||||
seq 100000 | fzf \-\-preview "awk '{sum+=\\$1} END {print sum}' {*f}"\fR
|
seq 100000 | fzf \-\-preview "awk '{sum+=\\$1} END {print sum}' {*f}"\fR
|
||||||
|
|
||||||
|
\fB# Use {+f} to get the selected items as a line-separated list
|
||||||
|
seq 100 | fzf \-\-multi \-\-bind 'enter:become:cat {+f}'\fR
|
||||||
|
|
||||||
Also,
|
Also,
|
||||||
|
|
||||||
* \fB{q}\fR is replaced to the current query string
|
* \fB{q}\fR is replaced to the current query string
|
||||||
@@ -890,6 +919,11 @@ Should be used with one of the following \fB\-\-preview\-window\fR options.
|
|||||||
.B * border\-bottom
|
.B * border\-bottom
|
||||||
.br
|
.br
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-\-preview\-wrap\-sign" =INDICATOR
|
||||||
|
Indicator for wrapped lines in the preview window. If not set, the value of
|
||||||
|
\fB\-\-wrap\-sign\fR is used.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-preview\-label\-pos" [=N[:top|bottom]]
|
.BI "\-\-preview\-label\-pos" [=N[:top|bottom]]
|
||||||
Position of the border label on the border line of the preview window. Specify
|
Position of the border label on the border line of the preview window. Specify
|
||||||
@@ -900,7 +934,7 @@ default value 0 (or \fBcenter\fR) will put the label at the center of the
|
|||||||
border line.
|
border line.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-preview\-window=" "[POSITION][,SIZE[%]][,border\-STYLE][,[no]wrap][,[no]follow][,[no]cycle][,[no]info][,[no]hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]"
|
.BI "\-\-preview\-window=" "[POSITION][,SIZE[%]][,border\-STYLE][,[no]wrap][,wrap\-word][,[no]follow][,[no]cycle][,[no]info][,[no]hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]"
|
||||||
|
|
||||||
.RS
|
.RS
|
||||||
.B POSITION: (default: right)
|
.B POSITION: (default: right)
|
||||||
@@ -918,7 +952,8 @@ default until \fBtoggle\-preview\fR action is triggered.
|
|||||||
execute the command in the background.
|
execute the command in the background.
|
||||||
|
|
||||||
* Long lines are truncated by default. Line wrap can be enabled with
|
* Long lines are truncated by default. Line wrap can be enabled with
|
||||||
\fBwrap\fR flag.
|
\fBwrap\fR flag. \fBwrap\-word\fR flag enables word-level wrapping, which
|
||||||
|
breaks lines at word boundaries instead of mid-word.
|
||||||
|
|
||||||
* Preview window will automatically scroll to the bottom when \fBfollow\fR
|
* Preview window will automatically scroll to the bottom when \fBfollow\fR
|
||||||
flag is set, similarly to how \fBtail \-f\fR works.
|
flag is set, similarly to how \fBtail \-f\fR works.
|
||||||
@@ -1125,19 +1160,25 @@ On Windows, the default value is \fBcmd /s/c\fR when \fB$SHELL\fR is not
|
|||||||
set.
|
set.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B "\-\-listen[=[ADDR:]PORT]" "\-\-listen\-unsafe[=[ADDR:]PORT]"
|
.B "\-\-listen[=SOCKET_PATH|[ADDR:]PORT]" "\-\-listen\-unsafe[=[ADDR:]PORT]"
|
||||||
Start HTTP server and listen on the given address. It allows external processes
|
Start HTTP server and listen on the given address or Unix socket. It allows
|
||||||
to send actions to perform via POST method.
|
external processes to send actions to perform via POST method and query the
|
||||||
|
program state via GET method. For the argument to be recognized as a socket
|
||||||
|
path, it must have \fB.sock\fR extension.
|
||||||
|
|
||||||
- If the port number is omitted or given as 0, fzf will automatically choose
|
- If the port number is omitted or given as 0, fzf will automatically choose
|
||||||
a port and export it as \fBFZF_PORT\fR environment variable to the child processes
|
a port and export it as \fBFZF_PORT\fR environment variable to the child processes.
|
||||||
|
|
||||||
|
- If a Unix socket path is given, fzf will create a Unix domain socket at the
|
||||||
|
given path. The existing file will be removed. The path to the socket file
|
||||||
|
is exported as \fBFZF_SOCK\fR environment variable.
|
||||||
|
|
||||||
- If \fBFZF_API_KEY\fR environment variable is set, the server would require
|
- If \fBFZF_API_KEY\fR environment variable is set, the server would require
|
||||||
sending an API key with the same value in the \fBx\-api\-key\fR HTTP header
|
sending an API key with the same value in the \fBx\-api\-key\fR HTTP header.
|
||||||
|
|
||||||
- \fBFZF_API_KEY\fR is required for a non-localhost listen address
|
- \fBFZF_API_KEY\fR is required for a non-localhost listen address.
|
||||||
|
|
||||||
- To allow remote process execution, use \fB\-\-listen\-unsafe\fR
|
- To allow remote process execution, use \fB\-\-listen\-unsafe\fR.
|
||||||
|
|
||||||
e.g.
|
e.g.
|
||||||
\fB# Start HTTP server on port 6266
|
\fB# Start HTTP server on port 6266
|
||||||
@@ -1176,6 +1217,18 @@ e.g.
|
|||||||
'
|
'
|
||||||
\fR
|
\fR
|
||||||
|
|
||||||
|
Here is an example script that uses a Unix socket instead of a TCP port.
|
||||||
|
|
||||||
|
\fB
|
||||||
|
fzf --listen=/tmp/fzf.sock
|
||||||
|
|
||||||
|
# GET
|
||||||
|
curl --unix-socket /tmp/fzf.sock http
|
||||||
|
|
||||||
|
# POST
|
||||||
|
curl --unix-socket /tmp/fzf.sock http -d up
|
||||||
|
\fR
|
||||||
|
|
||||||
.SS DIRECTORY TRAVERSAL
|
.SS DIRECTORY TRAVERSAL
|
||||||
.TP
|
.TP
|
||||||
.B "\-\-walker=[file][,dir][,follow][,hidden]"
|
.B "\-\-walker=[file][,dir][,follow][,hidden]"
|
||||||
@@ -1327,6 +1380,8 @@ fzf exports the following environment variables to its child processes.
|
|||||||
.br
|
.br
|
||||||
.BR FZF_COLUMNS " Number of columns fzf takes up excluding padding and margin"
|
.BR FZF_COLUMNS " Number of columns fzf takes up excluding padding and margin"
|
||||||
.br
|
.br
|
||||||
|
.BR FZF_DIRECTION " Direction of the list (\fBup\fR or \fBdown\fR)"
|
||||||
|
.br
|
||||||
.BR FZF_TOTAL_COUNT " Total number of items"
|
.BR FZF_TOTAL_COUNT " Total number of items"
|
||||||
.br
|
.br
|
||||||
.BR FZF_MATCH_COUNT " Number of matched items"
|
.BR FZF_MATCH_COUNT " Number of matched items"
|
||||||
@@ -1335,6 +1390,8 @@ fzf exports the following environment variables to its child processes.
|
|||||||
.br
|
.br
|
||||||
.BR FZF_POS " Vertical position of the cursor in the list starting from 1"
|
.BR FZF_POS " Vertical position of the cursor in the list starting from 1"
|
||||||
.br
|
.br
|
||||||
|
.BR FZF_WRAP " The line wrapping mode (char, word) when enabled"
|
||||||
|
.br
|
||||||
.BR FZF_QUERY " Current query string"
|
.BR FZF_QUERY " Current query string"
|
||||||
.br
|
.br
|
||||||
.BR FZF_INPUT_STATE " Current input state (enabled, disabled, hidden)"
|
.BR FZF_INPUT_STATE " Current input state (enabled, disabled, hidden)"
|
||||||
@@ -1363,6 +1420,8 @@ fzf exports the following environment variables to its child processes.
|
|||||||
.br
|
.br
|
||||||
.BR FZF_PORT " Port number when \-\-listen option is used"
|
.BR FZF_PORT " Port number when \-\-listen option is used"
|
||||||
.br
|
.br
|
||||||
|
.BR FZF_SOCK " Unix socket path when \-\-listen option is used"
|
||||||
|
.br
|
||||||
.BR FZF_PREVIEW_TOP " Top position of the preview window"
|
.BR FZF_PREVIEW_TOP " Top position of the preview window"
|
||||||
.br
|
.br
|
||||||
.BR FZF_PREVIEW_LEFT " Left position of the preview window"
|
.BR FZF_PREVIEW_LEFT " Left position of the preview window"
|
||||||
@@ -1370,6 +1429,8 @@ fzf exports the following environment variables to its child processes.
|
|||||||
.BR FZF_PREVIEW_LINES " Number of lines in the preview window"
|
.BR FZF_PREVIEW_LINES " Number of lines in the preview window"
|
||||||
.br
|
.br
|
||||||
.BR FZF_PREVIEW_COLUMNS " Number of columns in the preview window"
|
.BR FZF_PREVIEW_COLUMNS " Number of columns in the preview window"
|
||||||
|
.br
|
||||||
|
.BR FZF_RAW " Only in raw mode. 1 if the current item matches, 0 otherwise"
|
||||||
|
|
||||||
.SH EXTENDED SEARCH MODE
|
.SH EXTENDED SEARCH MODE
|
||||||
|
|
||||||
@@ -1453,7 +1514,7 @@ e.g.
|
|||||||
.br
|
.br
|
||||||
\fIctrl\-/\fR (\fIctrl\-_\fR)
|
\fIctrl\-/\fR (\fIctrl\-_\fR)
|
||||||
.br
|
.br
|
||||||
\fIctrl\-alt\-[a\-z]\fR
|
\fIctrl\-alt\-[a\-z]\fR (\fIctrl\-alt\-h\fR is \fIctrl\-alt\-backspace\fR on non-Windows)
|
||||||
.br
|
.br
|
||||||
\fIalt\-[*]\fR (Any case-sensitive single character is allowed)
|
\fIalt\-[*]\fR (Any case-sensitive single character is allowed)
|
||||||
.br
|
.br
|
||||||
@@ -1583,7 +1644,7 @@ e.g.
|
|||||||
.br
|
.br
|
||||||
\fIctrl\-alt\-end\fR
|
\fIctrl\-alt\-end\fR
|
||||||
.br
|
.br
|
||||||
\fIctrl\-alt\-backspace\fR (\fIctrl\-alt\-bspace\fR \fIctrl\-alt\-bs\fR)
|
\fIctrl\-alt\-backspace\fR (\fIctrl\-alt\-bspace\fR \fIctrl\-alt\-bs\fR) (\fIctrl\-alt\-h\fR (non-Windows))
|
||||||
.br
|
.br
|
||||||
\fIctrl\-alt\-delete\fR
|
\fIctrl\-alt\-delete\fR
|
||||||
.br
|
.br
|
||||||
@@ -1814,11 +1875,13 @@ A key or an event can be bound to one or more of the following actions.
|
|||||||
\fBbecome(...)\fR (replace fzf process with the specified command; see below for the details)
|
\fBbecome(...)\fR (replace fzf process with the specified command; see below for the details)
|
||||||
\fBbeginning\-of\-line\fR \fIctrl\-a home\fR
|
\fBbeginning\-of\-line\fR \fIctrl\-a home\fR
|
||||||
\fBbell\fR (ring the terminal bell)
|
\fBbell\fR (ring the terminal bell)
|
||||||
|
\fBbest\fR (move to the best match; same as \fBfirst\fR if raw mode is disabled)
|
||||||
\fBbg\-cancel\fR (cancel background transform processes)
|
\fBbg\-cancel\fR (cancel background transform processes)
|
||||||
\fBcancel\fR (clear query string if not empty, abort fzf otherwise)
|
\fBcancel\fR (clear query string if not empty, abort fzf otherwise)
|
||||||
\fBchange\-border\-label(...)\fR (change \fB\-\-border\-label\fR to the given string)
|
\fBchange\-border\-label(...)\fR (change \fB\-\-border\-label\fR to the given string)
|
||||||
\fBchange\-ghost(...)\fR (change ghost text to the given string)
|
\fBchange\-ghost(...)\fR (change ghost text to the given string)
|
||||||
\fBchange\-header(...)\fR (change header to the given string; doesn't affect \fB\-\-header\-lines\fR)
|
\fBchange\-header(...)\fR (change header to the given string; doesn't affect \fB\-\-header\-lines\fR)
|
||||||
|
\fBchange\-header\-lines(N)\fR (change the number of \fB\-\-header\-lines\fR)
|
||||||
\fBchange\-header\-label(...)\fR (change \fB\-\-header\-label\fR to the given string)
|
\fBchange\-header\-label(...)\fR (change \fB\-\-header\-label\fR to the given string)
|
||||||
\fBchange\-input\-label(...)\fR (change \fB\-\-input\-label\fR to the given string)
|
\fBchange\-input\-label(...)\fR (change \fB\-\-input\-label\fR to the given string)
|
||||||
\fBchange\-list\-label(...)\fR (change \fB\-\-list\-label\fR to the given string)
|
\fBchange\-list\-label(...)\fR (change \fB\-\-list\-label\fR to the given string)
|
||||||
@@ -1838,9 +1901,13 @@ A key or an event can be bound to one or more of the following actions.
|
|||||||
\fBdelete\-char\fR \fIdel\fR
|
\fBdelete\-char\fR \fIdel\fR
|
||||||
\fBdelete\-char/eof\fR \fIctrl\-d\fR (same as \fBdelete\-char\fR except aborts fzf if query is empty)
|
\fBdelete\-char/eof\fR \fIctrl\-d\fR (same as \fBdelete\-char\fR except aborts fzf if query is empty)
|
||||||
\fBdeselect\fR
|
\fBdeselect\fR
|
||||||
\fBdeselect\-all\fR (deselect all matches; to also clear non-matched selections, use \fBclear\-multi\fR)
|
\fBdeselect\-all\fR (deselect all matches; to also clear non-matching selections, use \fBclear\-multi\fR)
|
||||||
|
\fBdisable\-raw\fR (disable raw mode)
|
||||||
\fBdisable\-search\fR (disable search functionality)
|
\fBdisable\-search\fR (disable search functionality)
|
||||||
\fBdown\fR \fIctrl\-j ctrl\-n down\fR
|
\fBdown\fR \fIctrl\-j down\fR
|
||||||
|
\fBdown\-match\fR \fIctrl\-n\fR \fIalt\-down\fR (move to the match below the cursor)
|
||||||
|
\fBdown\-selected\fR (move to the selected item below the cursor)
|
||||||
|
\fBenable\-raw\fR (enable raw mode)
|
||||||
\fBenable\-search\fR (enable search functionality)
|
\fBenable\-search\fR (enable search functionality)
|
||||||
\fBend\-of\-line\fR \fIctrl\-e end\fR
|
\fBend\-of\-line\fR \fIctrl\-e end\fR
|
||||||
\fBexclude\fR (exclude the current item from the result)
|
\fBexclude\fR (exclude the current item from the result)
|
||||||
@@ -1858,7 +1925,7 @@ A key or an event can be bound to one or more of the following actions.
|
|||||||
\fBkill\-word\fR \fIalt\-d\fR
|
\fBkill\-word\fR \fIalt\-d\fR
|
||||||
\fBlast\fR (move to the last match; same as \fBpos(\-1)\fR)
|
\fBlast\fR (move to the last match; same as \fBpos(\-1)\fR)
|
||||||
\fBnext\-history\fR (\fIctrl\-n\fR on \fB\-\-history\fR)
|
\fBnext\-history\fR (\fIctrl\-n\fR on \fB\-\-history\fR)
|
||||||
\fBnext\-selected\fR (move to the next selected item)
|
\fBnext\-selected\fR (synonym to \fBdown\-selected\fR)
|
||||||
\fBpage\-down\fR \fIpgdn\fR
|
\fBpage\-down\fR \fIpgdn\fR
|
||||||
\fBpage\-up\fR \fIpgup\fR
|
\fBpage\-up\fR \fIpgup\fR
|
||||||
\fBhalf\-page\-down\fR
|
\fBhalf\-page\-down\fR
|
||||||
@@ -1871,7 +1938,7 @@ A key or an event can be bound to one or more of the following actions.
|
|||||||
\fBoffset\-middle\fR (place the current item is in the middle of the screen)
|
\fBoffset\-middle\fR (place the current item is in the middle of the screen)
|
||||||
\fBpos(...)\fR (move cursor to the numeric position; negative number to count from the end)
|
\fBpos(...)\fR (move cursor to the numeric position; negative number to count from the end)
|
||||||
\fBprev\-history\fR (\fIctrl\-p\fR on \fB\-\-history\fR)
|
\fBprev\-history\fR (\fIctrl\-p\fR on \fB\-\-history\fR)
|
||||||
\fBprev\-selected\fR (move to the previous selected item)
|
\fBprev\-selected\fR (synonym to \fBup\-selected\fR)
|
||||||
\fBpreview(...)\fR (see below for the details)
|
\fBpreview(...)\fR (see below for the details)
|
||||||
\fBpreview\-down\fR \fIshift\-down\fR
|
\fBpreview\-down\fR \fIshift\-down\fR
|
||||||
\fBpreview\-up\fR \fIshift\-up\fR
|
\fBpreview\-up\fR \fIshift\-up\fR
|
||||||
@@ -1906,11 +1973,14 @@ A key or an event can be bound to one or more of the following actions.
|
|||||||
\fBtoggle\-multi\-line\fR
|
\fBtoggle\-multi\-line\fR
|
||||||
\fBtoggle\-preview\fR
|
\fBtoggle\-preview\fR
|
||||||
\fBtoggle\-preview\-wrap\fR
|
\fBtoggle\-preview\-wrap\fR
|
||||||
|
\fBtoggle\-preview\-wrap\-word\fR
|
||||||
|
\fBtoggle\-raw\fR (toggle raw mode for displaying non-matching items)
|
||||||
\fBtoggle\-search\fR (toggle search functionality)
|
\fBtoggle\-search\fR (toggle search functionality)
|
||||||
\fBtoggle\-sort\fR
|
\fBtoggle\-sort\fR
|
||||||
\fBtoggle\-track\fR (toggle global tracking option (\fB\-\-track\fR))
|
\fBtoggle\-track\fR (toggle global tracking option (\fB\-\-track\fR))
|
||||||
\fBtoggle\-track\-current\fR (toggle tracking of the current item)
|
\fBtoggle\-track\-current\fR (toggle tracking of the current item)
|
||||||
\fBtoggle\-wrap\fR \fIctrl\-/\fR \fIalt\-/\fR
|
\fBtoggle\-wrap\fR
|
||||||
|
\fBtoggle\-wrap\-word\fR \fIctrl\-/\fR \fIalt\-/\fR
|
||||||
\fBtoggle+down\fR \fIctrl\-i (tab)\fR
|
\fBtoggle+down\fR \fIctrl\-i (tab)\fR
|
||||||
\fBtoggle+up\fR \fIbtab (shift\-tab)\fR
|
\fBtoggle+up\fR \fIbtab (shift\-tab)\fR
|
||||||
\fBtrack\-current\fR (track the current item; automatically disabled if focus changes)
|
\fBtrack\-current\fR (track the current item; automatically disabled if focus changes)
|
||||||
@@ -1918,6 +1988,7 @@ A key or an event can be bound to one or more of the following actions.
|
|||||||
\fBtransform\-border\-label(...)\fR (transform border label using an external command)
|
\fBtransform\-border\-label(...)\fR (transform border label using an external command)
|
||||||
\fBtransform\-ghost(...)\fR (transform ghost text using an external command)
|
\fBtransform\-ghost(...)\fR (transform ghost text using an external command)
|
||||||
\fBtransform\-header(...)\fR (transform header using an external command)
|
\fBtransform\-header(...)\fR (transform header using an external command)
|
||||||
|
\fBtransform\-header\-lines(...)\fR (transform the number of \fB\-\-header\-lines\fR using an external command)
|
||||||
\fBtransform\-header\-label(...)\fR (transform header label using an external command)
|
\fBtransform\-header\-label(...)\fR (transform header label using an external command)
|
||||||
\fBtransform\-input\-label(...)\fR (transform input label using an external command)
|
\fBtransform\-input\-label(...)\fR (transform input label using an external command)
|
||||||
\fBtransform\-list\-label(...)\fR (transform list label using an external command)
|
\fBtransform\-list\-label(...)\fR (transform list label using an external command)
|
||||||
@@ -1932,7 +2003,9 @@ A key or an event can be bound to one or more of the following actions.
|
|||||||
\fBunix\-line\-discard\fR \fIctrl\-u\fR
|
\fBunix\-line\-discard\fR \fIctrl\-u\fR
|
||||||
\fBunix\-word\-rubout\fR \fIctrl\-w\fR
|
\fBunix\-word\-rubout\fR \fIctrl\-w\fR
|
||||||
\fBuntrack\-current\fR (stop tracking the current item; no-op if global tracking is enabled)
|
\fBuntrack\-current\fR (stop tracking the current item; no-op if global tracking is enabled)
|
||||||
\fBup\fR \fIctrl\-k ctrl\-p up\fR
|
\fBup\fR \fIctrl\-k up\fR
|
||||||
|
\fBup\-match\fR \fIctrl\-p\fR \fIalt\-up\fR (move to the match above the cursor)
|
||||||
|
\fBup\-selected\fR (move to the selected item above the cursor)
|
||||||
\fByank\fR \fIctrl\-y\fR
|
\fByank\fR \fIctrl\-y\fR
|
||||||
|
|
||||||
Each \fBtransform*\fR action has a corresponding \fBbg\-transform*\fR
|
Each \fBtransform*\fR action has a corresponding \fBbg\-transform*\fR
|
||||||
@@ -2055,7 +2128,7 @@ payload of HTTP POST request to the \fB\-\-listen\fR server.
|
|||||||
|
|
||||||
e.g.
|
e.g.
|
||||||
\fB# Disallow selecting an empty line
|
\fB# Disallow selecting an empty line
|
||||||
echo \-e "1. Hello\\n2. Goodbye\\n\\n3. Exit" |
|
printf "1. Hello\\n2. Goodbye\\n\\n3. Exit" |
|
||||||
fzf \-\-height '~100%' \-\-reverse \-\-header 'Select one' \\
|
fzf \-\-height '~100%' \-\-reverse \-\-header 'Select one' \\
|
||||||
\-\-bind 'enter:transform:[[ \-n {} ]] &&
|
\-\-bind 'enter:transform:[[ \-n {} ]] &&
|
||||||
echo accept ||
|
echo accept ||
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
" Copyright (c) 2013-2025 Junegunn Choi
|
" Copyright (c) 2013-2026 Junegunn Choi
|
||||||
"
|
"
|
||||||
" MIT License
|
" MIT License
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -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
|
||||||
+5
-3
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
__fzf_defaults() {
|
__fzf_defaults() {
|
||||||
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||||
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||||
@@ -22,12 +21,15 @@ __fzf_exec_awk() {
|
|||||||
# modern point of view. To use a standard-conforming version in Solaris,
|
# modern point of view. To use a standard-conforming version in Solaris,
|
||||||
# one needs to explicitly use /usr/xpg4/bin/awk.
|
# one needs to explicitly use /usr/xpg4/bin/awk.
|
||||||
__fzf_awk=/usr/xpg4/bin/awk
|
__fzf_awk=/usr/xpg4/bin/awk
|
||||||
elif command -v mawk >/dev/null 2>&1; then
|
elif command -v mawk > /dev/null 2>&1; then
|
||||||
# choose the faster mawk if: it's installed && build date >= 20230322 &&
|
# choose the faster mawk if: it's installed && build date >= 20230322 &&
|
||||||
# version >= 1.3.4
|
# version >= 1.3.4
|
||||||
local n x y z d
|
local n x y z d
|
||||||
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null)
|
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null)
|
||||||
[[ $n == mawk ]] && (( d >= 20230302 && (x * 1000 + y) * 1000 + z >= 1003004 )) && __fzf_awk=mawk
|
[[ $n == mawk ]] &&
|
||||||
|
(((x * 1000 + y) * 1000 + z >= 1003004)) 2> /dev/null &&
|
||||||
|
((d >= 20230302)) 2> /dev/null &&
|
||||||
|
__fzf_awk=mawk
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
# Note: macOS awk has a quirk that it stops processing at all when it sees
|
# Note: macOS awk has a quirk that it stops processing at all when it sees
|
||||||
|
|||||||
+134
-80
@@ -31,9 +31,10 @@ if [[ $- =~ i ]]; then
|
|||||||
|
|
||||||
###########################################################
|
###########################################################
|
||||||
|
|
||||||
|
#----BEGIN shfmt
|
||||||
#----BEGIN INCLUDE common.sh
|
#----BEGIN INCLUDE common.sh
|
||||||
# NOTE: Do not directly edit this section, which is copied from "common.sh".
|
# NOTE: Do not directly edit this section, which is copied from "common.sh".
|
||||||
# To modify it, one can edit "common.sh" and run "./update-common.sh" to apply
|
# To modify it, one can edit "common.sh" and run "./update.sh" to apply
|
||||||
# the changes. See code comments in "common.sh" for the implementation details.
|
# the changes. See code comments in "common.sh" for the implementation details.
|
||||||
|
|
||||||
__fzf_defaults() {
|
__fzf_defaults() {
|
||||||
@@ -47,10 +48,13 @@ __fzf_exec_awk() {
|
|||||||
__fzf_awk=awk
|
__fzf_awk=awk
|
||||||
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
|
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
|
||||||
__fzf_awk=/usr/xpg4/bin/awk
|
__fzf_awk=/usr/xpg4/bin/awk
|
||||||
elif command -v mawk >/dev/null 2>&1; then
|
elif command -v mawk > /dev/null 2>&1; then
|
||||||
local n x y z d
|
local n x y z d
|
||||||
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null)
|
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null)
|
||||||
[[ $n == mawk ]] && (( d >= 20230302 && (x * 1000 + y) * 1000 + z >= 1003004 )) && __fzf_awk=mawk
|
[[ $n == mawk ]] &&
|
||||||
|
(((x * 1000 + y) * 1000 + z >= 1003004)) 2> /dev/null &&
|
||||||
|
((d >= 20230302)) 2> /dev/null &&
|
||||||
|
__fzf_awk=mawk
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
LC_ALL=C exec "$__fzf_awk" "$@"
|
LC_ALL=C exec "$__fzf_awk" "$@"
|
||||||
@@ -58,9 +62,9 @@ __fzf_exec_awk() {
|
|||||||
#----END INCLUDE
|
#----END INCLUDE
|
||||||
|
|
||||||
__fzf_comprun() {
|
__fzf_comprun() {
|
||||||
if [[ "$(type -t _fzf_comprun 2>&1)" = function ]]; then
|
if [[ "$(type -t _fzf_comprun 2>&1)" == function ]]; then
|
||||||
_fzf_comprun "$@"
|
_fzf_comprun "$@"
|
||||||
elif [[ -n "${TMUX_PANE-}" ]] && { [[ "${FZF_TMUX:-0}" != 0 ]] || [[ -n "${FZF_TMUX_OPTS-}" ]]; }; then
|
elif [[ -n ${TMUX_PANE-} ]] && { [[ ${FZF_TMUX:-0} != 0 ]] || [[ -n ${FZF_TMUX_OPTS-} ]]; }; then
|
||||||
shift
|
shift
|
||||||
fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- "$@"
|
fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- "$@"
|
||||||
else
|
else
|
||||||
@@ -72,13 +76,13 @@ __fzf_comprun() {
|
|||||||
__fzf_orig_completion() {
|
__fzf_orig_completion() {
|
||||||
local l comp f cmd
|
local l comp f cmd
|
||||||
while read -r l; do
|
while read -r l; do
|
||||||
if [[ "$l" =~ ^(.*\ -F)\ *([^ ]*).*\ ([^ ]*)$ ]]; then
|
if [[ $l =~ ^(.*\ -F)\ *([^ ]*).*\ ([^ ]*)$ ]]; then
|
||||||
comp="${BASH_REMATCH[1]}"
|
comp="${BASH_REMATCH[1]}"
|
||||||
f="${BASH_REMATCH[2]}"
|
f="${BASH_REMATCH[2]}"
|
||||||
cmd="${BASH_REMATCH[3]}"
|
cmd="${BASH_REMATCH[3]}"
|
||||||
[[ "$f" = _fzf_* ]] && continue
|
[[ $f == _fzf_* ]] && continue
|
||||||
printf -v "_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}" "%s" "${comp} %s ${cmd} #${f}"
|
printf -v "_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}" "%s" "${comp} %s ${cmd} #${f}"
|
||||||
if [[ "$l" = *" -o nospace "* ]] && [[ ! "${__fzf_nospace_commands-}" = *" $cmd "* ]]; then
|
if [[ $l == *" -o nospace "* ]] && [[ ${__fzf_nospace_commands-} != *" $cmd "* ]]; then
|
||||||
__fzf_nospace_commands="${__fzf_nospace_commands-} $cmd "
|
__fzf_nospace_commands="${__fzf_nospace_commands-} $cmd "
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -114,12 +118,13 @@ _fzf_opts_completion() {
|
|||||||
local cur prev opts
|
local cur prev opts
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
prev="${COMP_WORDS[COMP_CWORD - 1]}"
|
||||||
opts="
|
opts="
|
||||||
+c --no-color
|
+c --no-color
|
||||||
+i --no-ignore-case
|
+i --no-ignore-case
|
||||||
+s --no-sort
|
+s --no-sort
|
||||||
+x --no-extended
|
+x --no-extended
|
||||||
|
--accept-nth
|
||||||
--ansi
|
--ansi
|
||||||
--bash
|
--bash
|
||||||
--bind
|
--bind
|
||||||
@@ -133,56 +138,88 @@ _fzf_opts_completion() {
|
|||||||
--expect
|
--expect
|
||||||
--filepath-word
|
--filepath-word
|
||||||
--fish
|
--fish
|
||||||
|
--footer
|
||||||
|
--footer-border
|
||||||
|
--footer-label
|
||||||
|
--footer-label-pos
|
||||||
|
--freeze-left
|
||||||
|
--freeze-right
|
||||||
|
--gap
|
||||||
|
--gap-line
|
||||||
|
--ghost
|
||||||
|
--gutter
|
||||||
|
--gutter-raw
|
||||||
--header
|
--header
|
||||||
|
--header-border
|
||||||
--header-first
|
--header-first
|
||||||
|
--header-label
|
||||||
|
--header-label-pos
|
||||||
--header-lines
|
--header-lines
|
||||||
|
--header-lines-border
|
||||||
--height
|
--height
|
||||||
--highlight-line
|
--highlight-line
|
||||||
--history
|
--history
|
||||||
--history-size
|
--history-size
|
||||||
--hscroll-off
|
--hscroll-off
|
||||||
--info
|
--info
|
||||||
|
--info-command
|
||||||
|
--input-border
|
||||||
|
--input-label
|
||||||
|
--input-label-pos
|
||||||
--jump-labels
|
--jump-labels
|
||||||
--keep-right
|
--keep-right
|
||||||
--layout
|
--layout
|
||||||
--listen
|
--listen
|
||||||
--listen-unsafe
|
--listen-unsafe
|
||||||
|
--list-border
|
||||||
|
--list-label
|
||||||
|
--list-label-pos
|
||||||
--literal
|
--literal
|
||||||
--man
|
--man
|
||||||
--margin
|
--margin
|
||||||
--marker
|
--marker
|
||||||
|
--marker-multi-line
|
||||||
--min-height
|
--min-height
|
||||||
--no-bold
|
--no-bold
|
||||||
--no-clear
|
|
||||||
--no-hscroll
|
--no-hscroll
|
||||||
--no-mouse
|
--no-input
|
||||||
|
--no-multi-line
|
||||||
--no-scrollbar
|
--no-scrollbar
|
||||||
--no-separator
|
--no-separator
|
||||||
--no-unicode
|
|
||||||
--padding
|
--padding
|
||||||
--pointer
|
--pointer
|
||||||
--preview
|
--preview
|
||||||
|
--preview-border
|
||||||
--preview-label
|
--preview-label
|
||||||
--preview-label-pos
|
--preview-label-pos
|
||||||
--preview-window
|
--preview-window
|
||||||
--print-query
|
--print-query
|
||||||
--print0
|
--print0
|
||||||
--prompt
|
--prompt
|
||||||
|
--raw
|
||||||
--read0
|
--read0
|
||||||
--reverse
|
|
||||||
--scheme
|
--scheme
|
||||||
--scroll-off
|
--scroll-off
|
||||||
|
--scrollbar
|
||||||
--separator
|
--separator
|
||||||
|
--smart-case
|
||||||
|
--style
|
||||||
--sync
|
--sync
|
||||||
--tabstop
|
--tabstop
|
||||||
--tac
|
--tac
|
||||||
|
--tail
|
||||||
--tiebreak
|
--tiebreak
|
||||||
--tmux
|
--tmux
|
||||||
--track
|
--track
|
||||||
--version
|
--version
|
||||||
|
--walker
|
||||||
|
--walker-root
|
||||||
|
--walker-skip
|
||||||
--with-nth
|
--with-nth
|
||||||
--with-shell
|
--with-shell
|
||||||
--wrap
|
--wrap
|
||||||
|
--wrap-sign
|
||||||
|
--preview-wrap-sign
|
||||||
--zsh
|
--zsh
|
||||||
-0 --exit-0
|
-0 --exit-0
|
||||||
-1 --select-1
|
-1 --select-1
|
||||||
@@ -197,32 +234,41 @@ _fzf_opts_completion() {
|
|||||||
--"
|
--"
|
||||||
|
|
||||||
case "${prev}" in
|
case "${prev}" in
|
||||||
--scheme)
|
--scheme)
|
||||||
COMPREPLY=( $(compgen -W "default path history" -- "$cur") )
|
COMPREPLY=($(compgen -W "default path history" -- "$cur"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
--tiebreak)
|
--tiebreak)
|
||||||
COMPREPLY=( $(compgen -W "length chunk begin end index" -- "$cur") )
|
COMPREPLY=($(compgen -W "length chunk pathname begin end index" -- "$cur"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
--color)
|
--color)
|
||||||
COMPREPLY=( $(compgen -W "dark light 16 bw no" -- "$cur") )
|
COMPREPLY=($(compgen -W "dark light base16 16 bw no" -- "$cur"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
--layout)
|
--layout)
|
||||||
COMPREPLY=( $(compgen -W "default reverse reverse-list" -- "$cur") )
|
COMPREPLY=($(compgen -W "default reverse reverse-list" -- "$cur"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
--info)
|
--info)
|
||||||
COMPREPLY=( $(compgen -W "default right hidden inline inline-right" -- "$cur") )
|
COMPREPLY=($(compgen -W "default right hidden inline inline-right" -- "$cur"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
--preview-window)
|
--wrap)
|
||||||
COMPREPLY=( $(compgen -W "
|
COMPREPLY=($(compgen -W "char word" -- "$cur"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
--style)
|
||||||
|
COMPREPLY=($(compgen -W "default minimal full" -- "$cur"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
--preview-window)
|
||||||
|
COMPREPLY=($(compgen -W "
|
||||||
default
|
default
|
||||||
hidden
|
hidden
|
||||||
nohidden
|
nohidden
|
||||||
wrap
|
wrap
|
||||||
|
wrap-word
|
||||||
nowrap
|
nowrap
|
||||||
cycle
|
cycle
|
||||||
nocycle
|
nocycle
|
||||||
@@ -231,6 +277,7 @@ _fzf_opts_completion() {
|
|||||||
left
|
left
|
||||||
right
|
right
|
||||||
rounded border border-rounded
|
rounded border border-rounded
|
||||||
|
border-line
|
||||||
sharp border-sharp
|
sharp border-sharp
|
||||||
border-bold
|
border-bold
|
||||||
border-block
|
border-block
|
||||||
@@ -244,21 +291,23 @@ _fzf_opts_completion() {
|
|||||||
border-left
|
border-left
|
||||||
border-right
|
border-right
|
||||||
follow
|
follow
|
||||||
nofollow" -- "$cur") )
|
nofollow
|
||||||
return 0
|
info
|
||||||
;;
|
noinfo" -- "$cur"))
|
||||||
--border)
|
return 0
|
||||||
COMPREPLY=( $(compgen -W "rounded sharp bold block thinblock double horizontal vertical top bottom left right none" -- "$cur") )
|
;;
|
||||||
return 0
|
--border | --list-border | --header-border | --header-lines-border | --footer-border | --input-border | --preview-border)
|
||||||
;;
|
COMPREPLY=($(compgen -W "line rounded sharp bold block thinblock double horizontal vertical top bottom left right none" -- "$cur"))
|
||||||
--border-label-pos|--preview-label-pos)
|
return 0
|
||||||
COMPREPLY=( $(compgen -W "center bottom top" -- "$cur") )
|
;;
|
||||||
return 0
|
--border-label-pos | --preview-label-pos | --list-label-pos | --header-label-pos | --footer-label-pos | --input-label-pos)
|
||||||
;;
|
COMPREPLY=($(compgen -W "center bottom top" -- "$cur"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
if [[ "$cur" =~ ^-|\+ ]]; then
|
if [[ $cur =~ ^-|\+ ]]; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "$cur") )
|
COMPREPLY=($(compgen -W "${opts}" -- "$cur"))
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -272,7 +321,7 @@ _fzf_handle_dynamic_completion() {
|
|||||||
orig_cmd="$1"
|
orig_cmd="$1"
|
||||||
if __fzf_orig_completion_get_orig_func "$cmd"; then
|
if __fzf_orig_completion_get_orig_func "$cmd"; then
|
||||||
"$REPLY" "$@"
|
"$REPLY" "$@"
|
||||||
elif [[ -n "${_fzf_completion_loader-}" ]]; then
|
elif [[ -n ${_fzf_completion_loader-} ]]; then
|
||||||
orig_complete=$(complete -p "$orig_cmd" 2> /dev/null)
|
orig_complete=$(complete -p "$orig_cmd" 2> /dev/null)
|
||||||
$_fzf_completion_loader "$@"
|
$_fzf_completion_loader "$@"
|
||||||
ret=$?
|
ret=$?
|
||||||
@@ -286,7 +335,7 @@ _fzf_handle_dynamic_completion() {
|
|||||||
__fzf_orig_completion_instantiate "$cmd" "${BASH_REMATCH[1]}" &&
|
__fzf_orig_completion_instantiate "$cmd" "${BASH_REMATCH[1]}" &&
|
||||||
orig_complete=$REPLY
|
orig_complete=$REPLY
|
||||||
|
|
||||||
if [[ "${__fzf_nospace_commands-}" = *" $orig_cmd "* ]]; then
|
if [[ ${__fzf_nospace_commands-} == *" $orig_cmd "* ]]; then
|
||||||
eval "${orig_complete/ -F / -o nospace -F }"
|
eval "${orig_complete/ -F / -o nospace -F }"
|
||||||
else
|
else
|
||||||
eval "$orig_complete"
|
eval "$orig_complete"
|
||||||
@@ -306,30 +355,33 @@ __fzf_generic_path_completion() {
|
|||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
||||||
[[ $COMP_CWORD -ge 0 ]] && cur="${COMP_WORDS[COMP_CWORD]}"
|
[[ $COMP_CWORD -ge 0 ]] && cur="${COMP_WORDS[COMP_CWORD]}"
|
||||||
if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
|
if [[ $cur == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
|
||||||
base=${cur:0:${#cur}-${#trigger}}
|
base=${cur:0:${#cur}-${#trigger}}
|
||||||
eval "base=$base" 2> /dev/null || return
|
eval "base=$base" 2> /dev/null || return
|
||||||
|
|
||||||
dir=
|
dir=
|
||||||
[[ $base = *"/"* ]] && dir="$base"
|
[[ $base == *"/"* ]] && dir="$base"
|
||||||
while true; do
|
while true; do
|
||||||
if [[ -z "$dir" ]] || [[ -d "$dir" ]]; then
|
if [[ -z $dir ]] || [[ -d $dir ]]; then
|
||||||
leftover=${base/#"$dir"}
|
leftover=${base/#"$dir"/}
|
||||||
leftover=${leftover/#\/}
|
leftover=${leftover/#\//}
|
||||||
[[ -z "$dir" ]] && dir='.'
|
[[ -z $dir ]] && dir='.'
|
||||||
[[ "$dir" != "/" ]] && dir="${dir/%\//}"
|
[[ $dir != "/" ]] && dir="${dir/%\//}"
|
||||||
matches=$(
|
matches=$(
|
||||||
export FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-} $2")
|
export FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-} $2")
|
||||||
unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
|
unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
|
||||||
|
if [[ $1 =~ dir ]]; then
|
||||||
|
eval "rest=(${FZF_COMPLETION_DIR_OPTS-})"
|
||||||
|
else
|
||||||
|
eval "rest=(${FZF_COMPLETION_PATH_OPTS-})"
|
||||||
|
fi
|
||||||
if declare -F "$1" > /dev/null; then
|
if declare -F "$1" > /dev/null; then
|
||||||
eval "$1 $(printf %q "$dir")" | __fzf_comprun "$4" -q "$leftover"
|
eval "$1 $(printf %q "$dir")" | __fzf_comprun "$4" -q "$leftover" "${rest[@]}"
|
||||||
else
|
else
|
||||||
if [[ $1 =~ dir ]]; then
|
if [[ $1 =~ dir ]]; then
|
||||||
walker=dir,follow
|
walker=dir,follow
|
||||||
eval "rest=(${FZF_COMPLETION_DIR_OPTS-})"
|
|
||||||
else
|
else
|
||||||
walker=file,dir,follow,hidden
|
walker=file,dir,follow,hidden
|
||||||
eval "rest=(${FZF_COMPLETION_PATH_OPTS-})"
|
|
||||||
fi
|
fi
|
||||||
__fzf_comprun "$4" -q "$leftover" --walker "$walker" --walker-root="$dir" "${rest[@]}"
|
__fzf_comprun "$4" -q "$leftover" --walker "$walker" --walker-root="$dir" "${rest[@]}"
|
||||||
fi | while read -r item; do
|
fi | while read -r item; do
|
||||||
@@ -337,11 +389,11 @@ __fzf_generic_path_completion() {
|
|||||||
done
|
done
|
||||||
)
|
)
|
||||||
matches=${matches% }
|
matches=${matches% }
|
||||||
[[ -z "$3" ]] && [[ "${__fzf_nospace_commands-}" = *" ${COMP_WORDS[0]} "* ]] && matches="$matches "
|
[[ -z $3 ]] && [[ ${__fzf_nospace_commands-} == *" ${COMP_WORDS[0]} "* ]] && matches="$matches "
|
||||||
if [[ -n "$matches" ]]; then
|
if [[ -n $matches ]]; then
|
||||||
COMPREPLY=( "$matches" )
|
COMPREPLY=("$matches")
|
||||||
else
|
else
|
||||||
COMPREPLY=( "$cur" )
|
COMPREPLY=("$cur")
|
||||||
fi
|
fi
|
||||||
# To redraw line after fzf closes (printf '\e[5n')
|
# To redraw line after fzf closes (printf '\e[5n')
|
||||||
bind '"\e[0n": redraw-current-line' 2> /dev/null
|
bind '"\e[0n": redraw-current-line' 2> /dev/null
|
||||||
@@ -349,7 +401,7 @@ __fzf_generic_path_completion() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
dir=$(command dirname "$dir")
|
dir=$(command dirname "$dir")
|
||||||
[[ "$dir" =~ /$ ]] || dir="$dir"/
|
[[ $dir =~ /$ ]] || dir="$dir"/
|
||||||
done
|
done
|
||||||
else
|
else
|
||||||
shift
|
shift
|
||||||
@@ -365,15 +417,15 @@ _fzf_complete() {
|
|||||||
args=("$@")
|
args=("$@")
|
||||||
sep=
|
sep=
|
||||||
for i in "${!args[@]}"; do
|
for i in "${!args[@]}"; do
|
||||||
if [[ "${args[$i]}" = -- ]]; then
|
if [[ ${args[$i]} == -- ]]; then
|
||||||
sep=$i
|
sep=$i
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
if [[ -n "$sep" ]]; then
|
if [[ -n $sep ]]; then
|
||||||
str_arg=
|
str_arg=
|
||||||
rest=("${args[@]:$((sep + 1)):${#args[@]}}")
|
rest=("${args[@]:$((sep + 1)):${#args[@]}}")
|
||||||
args=("${args[@]:0:$sep}")
|
args=("${args[@]:0:sep}")
|
||||||
else
|
else
|
||||||
str_arg=$1
|
str_arg=$1
|
||||||
args=()
|
args=()
|
||||||
@@ -388,15 +440,16 @@ _fzf_complete() {
|
|||||||
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
||||||
cmd="${COMP_WORDS[0]}"
|
cmd="${COMP_WORDS[0]}"
|
||||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||||
if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
|
if [[ $cur == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
|
||||||
cur=${cur:0:${#cur}-${#trigger}}
|
cur=${cur:0:${#cur}-${#trigger}}
|
||||||
|
|
||||||
selected=$(
|
selected=$(
|
||||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse" "${FZF_COMPLETION_OPTS-} $str_arg") \
|
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse" "${FZF_COMPLETION_OPTS-} $str_arg") \
|
||||||
FZF_DEFAULT_OPTS_FILE='' \
|
FZF_DEFAULT_OPTS_FILE='' \
|
||||||
__fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | eval "$post" | command tr '\n' ' ')
|
__fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | eval "$post" | command tr '\n' ' '
|
||||||
|
)
|
||||||
selected=${selected% } # Strip trailing space not to repeat "-o nospace"
|
selected=${selected% } # Strip trailing space not to repeat "-o nospace"
|
||||||
if [[ -n "$selected" ]]; then
|
if [[ -n $selected ]]; then
|
||||||
COMPREPLY=("$selected")
|
COMPREPLY=("$selected")
|
||||||
else
|
else
|
||||||
COMPREPLY=("$cur")
|
COMPREPLY=("$cur")
|
||||||
@@ -454,10 +507,10 @@ _fzf_proc_completion() {
|
|||||||
'
|
'
|
||||||
_fzf_complete -m --header-lines=1 --no-preview --wrap --color fg:dim,nth:regular \
|
_fzf_complete -m --header-lines=1 --no-preview --wrap --color fg:dim,nth:regular \
|
||||||
--bind "click-header:transform:$transformer" -- "$@" < <(
|
--bind "click-header:transform:$transformer" -- "$@" < <(
|
||||||
command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
|
command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
|
||||||
command ps -eo user,pid,ppid,time,args 2> /dev/null || # For BusyBox
|
command ps -eo user,pid,ppid,time,args 2> /dev/null || # For BusyBox
|
||||||
command ps --everyone --full --windows # For cygwin
|
command ps --everyone --full --windows # For cygwin
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
_fzf_proc_completion_post() {
|
_fzf_proc_completion_post() {
|
||||||
@@ -540,12 +593,12 @@ _fzf_host_completion() {
|
|||||||
# > and the third argument ($3) is the word preceding the word being completed on the current command line.
|
# > and the third argument ($3) is the word preceding the word being completed on the current command line.
|
||||||
_fzf_complete_ssh() {
|
_fzf_complete_ssh() {
|
||||||
case $3 in
|
case $3 in
|
||||||
-i|-F|-E)
|
-i | -F | -E)
|
||||||
_fzf_path_completion "$@"
|
_fzf_path_completion "$@"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
local user=
|
local user=
|
||||||
[[ "$2" =~ '@' ]] && user="${2%%@*}@"
|
[[ $2 =~ '@' ]] && user="${2%%@*}@"
|
||||||
_fzf_complete +m -- "$@" < <(__fzf_list_hosts | __fzf_exec_awk -v user="$user" '{print user $0}')
|
_fzf_complete +m -- "$@" < <(__fzf_list_hosts | __fzf_exec_awk -v user="$user" '{print user $0}')
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -676,12 +729,13 @@ _fzf_setup_completion() {
|
|||||||
__fzf_orig_completion < <(complete -p "$@" 2> /dev/null)
|
__fzf_orig_completion < <(complete -p "$@" 2> /dev/null)
|
||||||
for cmd in "$@"; do
|
for cmd in "$@"; do
|
||||||
case "$kind" in
|
case "$kind" in
|
||||||
dir) __fzf_defc "$cmd" "$fn" "-o nospace -o dirnames" ;;
|
dir) __fzf_defc "$cmd" "$fn" "-o nospace -o dirnames" ;;
|
||||||
var) __fzf_defc "$cmd" "$fn" "-o default -o nospace -v" ;;
|
var) __fzf_defc "$cmd" "$fn" "-o default -o nospace -v" ;;
|
||||||
alias) __fzf_defc "$cmd" "$fn" "-a" ;;
|
alias) __fzf_defc "$cmd" "$fn" "-a" ;;
|
||||||
*) __fzf_defc "$cmd" "$fn" "-o default -o bashdefault" ;;
|
*) __fzf_defc "$cmd" "$fn" "-o default -o bashdefault" ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
#----END shfmt
|
||||||
|
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -0,0 +1,241 @@
|
|||||||
|
# ____ ____
|
||||||
|
# / __/___ / __/
|
||||||
|
# / /_/_ / / /_
|
||||||
|
# / __/ / /_/ __/
|
||||||
|
# /_/ /___/_/ completion.fish
|
||||||
|
#
|
||||||
|
# - $FZF_COMPLETION_OPTS (default: empty)
|
||||||
|
|
||||||
|
function fzf_completion_setup
|
||||||
|
|
||||||
|
#----BEGIN INCLUDE common.fish
|
||||||
|
# NOTE: Do not directly edit this section, which is copied from "common.fish".
|
||||||
|
# To modify it, one can edit "common.fish" and run "./update.sh" to apply
|
||||||
|
# the changes. See code comments in "common.fish" for the implementation details.
|
||||||
|
|
||||||
|
function __fzf_defaults
|
||||||
|
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
|
||||||
|
string join ' ' -- \
|
||||||
|
"--height $FZF_TMUX_HEIGHT --min-height=20+ --bind=ctrl-z:ignore" $argv[1] \
|
||||||
|
(test -r "$FZF_DEFAULT_OPTS_FILE"; and string join -- ' ' <$FZF_DEFAULT_OPTS_FILE) \
|
||||||
|
$FZF_DEFAULT_OPTS $argv[2..-1]
|
||||||
|
end
|
||||||
|
|
||||||
|
function __fzfcmd
|
||||||
|
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
|
||||||
|
if test -n "$FZF_TMUX_OPTS"
|
||||||
|
echo "fzf-tmux $FZF_TMUX_OPTS -- "
|
||||||
|
else if test "$FZF_TMUX" = "1"
|
||||||
|
echo "fzf-tmux -d$FZF_TMUX_HEIGHT -- "
|
||||||
|
else
|
||||||
|
echo "fzf"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function __fzf_cmd_tokens -d 'Return command line tokens, skipping leading env assignments and command prefixes'
|
||||||
|
set -l tokens
|
||||||
|
if test (string match -r -- '^\d+' $version) -ge 4
|
||||||
|
set -- tokens (commandline -xpc)
|
||||||
|
else
|
||||||
|
set -- tokens (commandline -opc)
|
||||||
|
end
|
||||||
|
|
||||||
|
set -l -- var_count 0
|
||||||
|
for i in $tokens
|
||||||
|
if string match -qr -- '^[\w]+=' $i
|
||||||
|
set var_count (math $var_count + 1)
|
||||||
|
else
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
set -e -- tokens[0..$var_count]
|
||||||
|
|
||||||
|
while true
|
||||||
|
switch "$tokens[1]"
|
||||||
|
case builtin command
|
||||||
|
set -e -- tokens[1]
|
||||||
|
test "$tokens[1]" = "--"; and set -e -- tokens[1]
|
||||||
|
case env
|
||||||
|
set -e -- tokens[1]
|
||||||
|
test "$tokens[1]" = "--"; and set -e -- tokens[1]
|
||||||
|
while string match -qr -- '^[\w]+=' "$tokens[1]"
|
||||||
|
set -e -- tokens[1]
|
||||||
|
end
|
||||||
|
case '*'
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
string escape -n -- $tokens
|
||||||
|
end
|
||||||
|
|
||||||
|
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
|
||||||
|
set -l fzf_query ''
|
||||||
|
set -l prefix ''
|
||||||
|
set -l dir '.'
|
||||||
|
|
||||||
|
set -l -- fish_major (string match -r -- '^\d+' $version)
|
||||||
|
set -l -- fish_minor (string match -r -- '^\d+\.(\d+)' $version)[2]
|
||||||
|
|
||||||
|
set -l -- match_regex '(?<fzf_query>[\s\S]*?(?=\n?$)$)'
|
||||||
|
set -l -- prefix_regex '^-[^\s=]+=|^-(?!-)\S'
|
||||||
|
if test "$fish_major" -eq 3 -a "$fish_minor" -lt 3
|
||||||
|
or string match -q -v -- '* -- *' (string sub -l (commandline -Cp) -- (commandline -p))
|
||||||
|
set -- match_regex "(?<prefix>$prefix_regex)?$match_regex"
|
||||||
|
end
|
||||||
|
|
||||||
|
if test "$fish_major" -ge 4
|
||||||
|
string match -q -r -- $match_regex (commandline --current-token --tokens-expanded | string collect -N)
|
||||||
|
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||||
|
string match -q -r -- $match_regex (commandline --current-token --tokenize | string collect -N)
|
||||||
|
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)' '')
|
||||||
|
else
|
||||||
|
set -l -- cl_token (commandline --current-token --tokenize | string collect -N)
|
||||||
|
set -- prefix (string match -r -- $prefix_regex $cl_token)
|
||||||
|
set -- fzf_query (string replace -- "$prefix" '' $cl_token | string collect -N)
|
||||||
|
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)|\\\n\\\n$' '')
|
||||||
|
end
|
||||||
|
|
||||||
|
if test -n "$fzf_query"
|
||||||
|
if test \( "$fish_major" -ge 4 \) -o \( "$fish_major" -eq 3 -a "$fish_minor" -ge 5 \)
|
||||||
|
set -- fzf_query (path normalize -- $fzf_query)
|
||||||
|
set -- dir $fzf_query
|
||||||
|
while not path is -d $dir
|
||||||
|
set -- dir (path dirname $dir)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||||
|
string match -q -r -- '(?<fzf_query>^[\s\S]*?(?=\n?$)$)' \
|
||||||
|
(string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
|
||||||
|
else
|
||||||
|
set -- fzf_query (string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
|
||||||
|
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r '\\\n$' '')
|
||||||
|
end
|
||||||
|
set -- dir $fzf_query
|
||||||
|
while not test -d "$dir"
|
||||||
|
set -- dir (dirname -z -- "$dir" | string split0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not string match -q -- '.' $dir; or string match -q -r -- '^\./|^\.$' $fzf_query
|
||||||
|
if test "$fish_major" -ge 4
|
||||||
|
string match -q -r -- '^'(string escape --style=regex -- $dir)'/?(?<fzf_query>[\s\S]*)' $fzf_query
|
||||||
|
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||||
|
string match -q -r -- '^/?(?<fzf_query>[\s\S]*?(?=\n?$)$)' \
|
||||||
|
(string replace -- "$dir" '' $fzf_query | string collect -N)
|
||||||
|
else
|
||||||
|
set -- fzf_query (string replace -- "$dir" '' $fzf_query | string collect -N)
|
||||||
|
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^/?|\\\n$' '')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
string escape -n -- "$dir" "$fzf_query" "$prefix"
|
||||||
|
end
|
||||||
|
#----END INCLUDE
|
||||||
|
|
||||||
|
# Use complete builtin for specific commands
|
||||||
|
function __fzf_complete_native
|
||||||
|
set -l -- token (commandline -t)
|
||||||
|
set -l -- completions (eval complete -C \"$argv[1]\")
|
||||||
|
test -n "$completions"; or begin commandline -f repaint; return; end
|
||||||
|
|
||||||
|
# Calculate tabstop based on longest completion item (sample first 500 for performance)
|
||||||
|
set -l -- tabstop 20
|
||||||
|
set -l -- sample_size (math "min(500, "(count $completions)")")
|
||||||
|
for c in $completions[1..$sample_size]
|
||||||
|
set -l -- len (string length -V -- (string split -- \t $c))
|
||||||
|
test -n "$len[2]" -a "$len[1]" -gt "$tabstop"
|
||||||
|
and set -- tabstop $len[1]
|
||||||
|
end
|
||||||
|
# limit to 120 to prevent long lines
|
||||||
|
set -- tabstop (math "min($tabstop + 4, 120)")
|
||||||
|
|
||||||
|
set -l result
|
||||||
|
set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults \
|
||||||
|
"--reverse --delimiter=\\t --nth=1 --tabstop=$tabstop --color=fg:dim,nth:regular" \
|
||||||
|
$FZF_COMPLETION_OPTS $argv[2..-1] --accept-nth=1 --read0 --print0)
|
||||||
|
set -- result (string join0 -- $completions | eval (__fzfcmd) | string split0)
|
||||||
|
and begin
|
||||||
|
set -l -- tail ' '
|
||||||
|
# Append / to bare ~username results (fish omits it unlike other shells)
|
||||||
|
set -- result (string replace -r -- '^(~\w+)\s?$' '$1/' $result)
|
||||||
|
# Don't add trailing space if single result is a directory
|
||||||
|
test (count $result) -eq 1
|
||||||
|
and string match -q -- '*/' "$result"; and set -- tail ''
|
||||||
|
|
||||||
|
set -l -- result (string escape -n -- $result)
|
||||||
|
|
||||||
|
string match -q -- '~*' "$token"
|
||||||
|
and set result (string replace -r -- '^\\\\~' '~' $result)
|
||||||
|
|
||||||
|
string match -q -- '$*' "$token"
|
||||||
|
and set result (string replace -r -- '^\\\\\$' '\$' $result)
|
||||||
|
|
||||||
|
commandline -rt -- (string join ' ' -- $result)$tail
|
||||||
|
end
|
||||||
|
commandline -f repaint
|
||||||
|
end
|
||||||
|
|
||||||
|
function _fzf_complete
|
||||||
|
set -l -- args (string escape -- $argv | string join ' ' | string split -- ' -- ')
|
||||||
|
set -l -- post_func (status function)_(string split -- ' ' $args[2])[1]_post
|
||||||
|
set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults --reverse $FZF_COMPLETION_OPTS $args[1])
|
||||||
|
set -lx FZF_DEFAULT_OPTS_FILE
|
||||||
|
set -lx FZF_DEFAULT_COMMAND
|
||||||
|
set -l -- fzf_query (commandline -t | string escape)
|
||||||
|
set -l result
|
||||||
|
eval (__fzfcmd) --query=$fzf_query | while read -l r; set -a -- result $r; end
|
||||||
|
and if functions -q $post_func
|
||||||
|
commandline -rt -- (string collect -- $result | eval $post_func $args[2] | string join ' ')' '
|
||||||
|
else
|
||||||
|
commandline -rt -- (string join -- ' ' (string escape -- $result))' '
|
||||||
|
end
|
||||||
|
commandline -f repaint
|
||||||
|
end
|
||||||
|
|
||||||
|
# Kill completion (process selection)
|
||||||
|
function _fzf_complete_kill
|
||||||
|
set -l -- fzf_query (commandline -t | string escape)
|
||||||
|
set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults --reverse $FZF_COMPLETION_OPTS \
|
||||||
|
--accept-nth=2 -m --header-lines=1 --no-preview --wrap)
|
||||||
|
set -lx FZF_DEFAULT_OPTS_FILE
|
||||||
|
if type -q ps
|
||||||
|
set -l -- ps_cmd 'begin command ps -eo user,pid,ppid,start,time,command 2>/dev/null;' \
|
||||||
|
'or command ps -eo user,pid,ppid,time,args 2>/dev/null;' \
|
||||||
|
'or command ps --everyone --full --windows 2>/dev/null; end'
|
||||||
|
set -l -- result (eval $ps_cmd \| (__fzfcmd) --query=$fzf_query)
|
||||||
|
and commandline -rt -- (string join ' ' -- $result)" "
|
||||||
|
else
|
||||||
|
__fzf_complete_native "kill " --multi --query=$fzf_query
|
||||||
|
end
|
||||||
|
commandline -f repaint
|
||||||
|
end
|
||||||
|
|
||||||
|
# Main completion function
|
||||||
|
function fzf-completion
|
||||||
|
set -l -- tokens (__fzf_cmd_tokens)
|
||||||
|
set -l -- current_token (commandline -t)
|
||||||
|
set -l -- cmd_name $tokens[1]
|
||||||
|
|
||||||
|
# Route to appropriate completion function
|
||||||
|
if test -n "$tokens"; and functions -q _fzf_complete_$cmd_name
|
||||||
|
_fzf_complete_$cmd_name $tokens
|
||||||
|
else
|
||||||
|
set -l -- fzf_opt --query=$current_token --multi
|
||||||
|
__fzf_complete_native "$tokens $current_token" $fzf_opt
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Bind Shift-Tab to fzf-completion (Tab retains native Fish behavior)
|
||||||
|
if test (string match -r -- '^\d+' $version) -ge 4
|
||||||
|
bind shift-tab fzf-completion
|
||||||
|
bind -M insert shift-tab fzf-completion
|
||||||
|
else
|
||||||
|
bind -k btab fzf-completion
|
||||||
|
bind -M insert -k btab fzf-completion
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Run setup
|
||||||
|
fzf_completion_setup
|
||||||
+12
-6
@@ -98,7 +98,7 @@ if [[ -o interactive ]]; then
|
|||||||
|
|
||||||
#----BEGIN INCLUDE common.sh
|
#----BEGIN INCLUDE common.sh
|
||||||
# NOTE: Do not directly edit this section, which is copied from "common.sh".
|
# NOTE: Do not directly edit this section, which is copied from "common.sh".
|
||||||
# To modify it, one can edit "common.sh" and run "./update-common.sh" to apply
|
# To modify it, one can edit "common.sh" and run "./update.sh" to apply
|
||||||
# the changes. See code comments in "common.sh" for the implementation details.
|
# the changes. See code comments in "common.sh" for the implementation details.
|
||||||
|
|
||||||
__fzf_defaults() {
|
__fzf_defaults() {
|
||||||
@@ -112,10 +112,13 @@ __fzf_exec_awk() {
|
|||||||
__fzf_awk=awk
|
__fzf_awk=awk
|
||||||
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
|
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
|
||||||
__fzf_awk=/usr/xpg4/bin/awk
|
__fzf_awk=/usr/xpg4/bin/awk
|
||||||
elif command -v mawk >/dev/null 2>&1; then
|
elif command -v mawk > /dev/null 2>&1; then
|
||||||
local n x y z d
|
local n x y z d
|
||||||
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null)
|
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null)
|
||||||
[[ $n == mawk ]] && (( d >= 20230302 && (x * 1000 + y) * 1000 + z >= 1003004 )) && __fzf_awk=mawk
|
[[ $n == mawk ]] &&
|
||||||
|
(((x * 1000 + y) * 1000 + z >= 1003004)) 2> /dev/null &&
|
||||||
|
((d >= 20230302)) 2> /dev/null &&
|
||||||
|
__fzf_awk=mawk
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
LC_ALL=C exec "$__fzf_awk" "$@"
|
LC_ALL=C exec "$__fzf_awk" "$@"
|
||||||
@@ -171,15 +174,18 @@ __fzf_generic_path_completion() {
|
|||||||
export FZF_DEFAULT_OPTS
|
export FZF_DEFAULT_OPTS
|
||||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-}")
|
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-}")
|
||||||
unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
|
unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
|
||||||
|
if [[ $compgen =~ dir ]]; then
|
||||||
|
rest=${FZF_COMPLETION_DIR_OPTS-}
|
||||||
|
else
|
||||||
|
rest=${FZF_COMPLETION_PATH_OPTS-}
|
||||||
|
fi
|
||||||
if declare -f "$compgen" > /dev/null; then
|
if declare -f "$compgen" > /dev/null; then
|
||||||
eval "$compgen $(printf %q "$dir")" | __fzf_comprun "$cmd_word" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover"
|
eval "$compgen $(printf %q "$dir")" | __fzf_comprun "$cmd_word" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" ${(Q)${(Z+n+)rest}}
|
||||||
else
|
else
|
||||||
if [[ $compgen =~ dir ]]; then
|
if [[ $compgen =~ dir ]]; then
|
||||||
walker=dir,follow
|
walker=dir,follow
|
||||||
rest=${FZF_COMPLETION_DIR_OPTS-}
|
|
||||||
else
|
else
|
||||||
walker=file,dir,follow,hidden
|
walker=file,dir,follow,hidden
|
||||||
rest=${FZF_COMPLETION_PATH_OPTS-}
|
|
||||||
fi
|
fi
|
||||||
__fzf_comprun "$cmd_word" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" --walker "$walker" --walker-root="$dir" ${(Q)${(Z+n+)rest}} < /dev/tty
|
__fzf_comprun "$cmd_word" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" --walker "$walker" --walker-root="$dir" ${(Q)${(Z+n+)rest}} < /dev/tty
|
||||||
fi | while read -r item; do
|
fi | while read -r item; do
|
||||||
|
|||||||
+47
-31
@@ -7,6 +7,7 @@
|
|||||||
# - $FZF_TMUX_OPTS
|
# - $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_OPTS
|
# - $FZF_CTRL_R_OPTS
|
||||||
# - $FZF_ALT_C_COMMAND
|
# - $FZF_ALT_C_COMMAND
|
||||||
# - $FZF_ALT_C_OPTS
|
# - $FZF_ALT_C_OPTS
|
||||||
@@ -17,9 +18,10 @@ if [[ $- =~ i ]]; then
|
|||||||
# Key bindings
|
# Key bindings
|
||||||
# ------------
|
# ------------
|
||||||
|
|
||||||
|
#----BEGIN shfmt
|
||||||
#----BEGIN INCLUDE common.sh
|
#----BEGIN INCLUDE common.sh
|
||||||
# NOTE: Do not directly edit this section, which is copied from "common.sh".
|
# NOTE: Do not directly edit this section, which is copied from "common.sh".
|
||||||
# To modify it, one can edit "common.sh" and run "./update-common.sh" to apply
|
# To modify it, one can edit "common.sh" and run "./update.sh" to apply
|
||||||
# the changes. See code comments in "common.sh" for the implementation details.
|
# the changes. See code comments in "common.sh" for the implementation details.
|
||||||
|
|
||||||
__fzf_defaults() {
|
__fzf_defaults() {
|
||||||
@@ -33,10 +35,13 @@ __fzf_exec_awk() {
|
|||||||
__fzf_awk=awk
|
__fzf_awk=awk
|
||||||
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
|
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
|
||||||
__fzf_awk=/usr/xpg4/bin/awk
|
__fzf_awk=/usr/xpg4/bin/awk
|
||||||
elif command -v mawk >/dev/null 2>&1; then
|
elif command -v mawk > /dev/null 2>&1; then
|
||||||
local n x y z d
|
local n x y z d
|
||||||
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null)
|
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null)
|
||||||
[[ $n == mawk ]] && (( d >= 20230302 && (x * 1000 + y) * 1000 + z >= 1003004 )) && __fzf_awk=mawk
|
[[ $n == mawk ]] &&
|
||||||
|
(((x * 1000 + y) * 1000 + z >= 1003004)) 2> /dev/null &&
|
||||||
|
((d >= 20230302)) 2> /dev/null &&
|
||||||
|
__fzf_awk=mawk
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
LC_ALL=C exec "$__fzf_awk" "$@"
|
LC_ALL=C exec "$__fzf_awk" "$@"
|
||||||
@@ -45,30 +50,30 @@ __fzf_exec_awk() {
|
|||||||
|
|
||||||
__fzf_select__() {
|
__fzf_select__() {
|
||||||
FZF_DEFAULT_COMMAND=${FZF_CTRL_T_COMMAND:-} \
|
FZF_DEFAULT_COMMAND=${FZF_CTRL_T_COMMAND:-} \
|
||||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path" "${FZF_CTRL_T_OPTS-} -m") \
|
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path" "${FZF_CTRL_T_OPTS-} -m") \
|
||||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) "$@" |
|
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) "$@" |
|
||||||
while read -r item; do
|
while read -r item; do
|
||||||
printf '%q ' "$item" # escape special chars
|
printf '%q ' "$item" # escape special chars
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
__fzfcmd() {
|
__fzfcmd() {
|
||||||
[[ -n "${TMUX_PANE-}" ]] && { [[ "${FZF_TMUX:-0}" != 0 ]] || [[ -n "${FZF_TMUX_OPTS-}" ]]; } &&
|
[[ -n ${TMUX_PANE-} ]] && { [[ ${FZF_TMUX:-0} != 0 ]] || [[ -n ${FZF_TMUX_OPTS-} ]]; } &&
|
||||||
echo "fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- " || echo "fzf"
|
echo "fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- " || echo "fzf"
|
||||||
}
|
}
|
||||||
|
|
||||||
fzf-file-widget() {
|
fzf-file-widget() {
|
||||||
local selected="$(__fzf_select__ "$@")"
|
local selected="$(__fzf_select__ "$@")"
|
||||||
READLINE_LINE="${READLINE_LINE:0:$READLINE_POINT}$selected${READLINE_LINE:$READLINE_POINT}"
|
READLINE_LINE="${READLINE_LINE:0:READLINE_POINT}$selected${READLINE_LINE:READLINE_POINT}"
|
||||||
READLINE_POINT=$(( READLINE_POINT + ${#selected} ))
|
READLINE_POINT=$((READLINE_POINT + ${#selected}))
|
||||||
}
|
}
|
||||||
|
|
||||||
__fzf_cd__() {
|
__fzf_cd__() {
|
||||||
local dir
|
local dir
|
||||||
dir=$(
|
dir=$(
|
||||||
FZF_DEFAULT_COMMAND=${FZF_ALT_C_COMMAND:-} \
|
FZF_DEFAULT_COMMAND=${FZF_ALT_C_COMMAND:-} \
|
||||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=dir,follow,hidden --scheme=path" "${FZF_ALT_C_OPTS-} +m") \
|
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=dir,follow,hidden --scheme=path" "${FZF_ALT_C_OPTS-} +m") \
|
||||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd)
|
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd)
|
||||||
) && printf 'builtin cd -- %q' "$(builtin unset CDPATH && builtin cd -- "$dir" && builtin pwd)"
|
) && printf 'builtin cd -- %q' "$(builtin unset CDPATH && builtin cd -- "$dir" && builtin pwd)"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,11 +85,11 @@ if command -v perl > /dev/null; then
|
|||||||
set +o pipefail
|
set +o pipefail
|
||||||
builtin fc -lnr -2147483648 |
|
builtin fc -lnr -2147483648 |
|
||||||
last_hist=$(HISTTIMEFORMAT='' builtin history 1) command perl -n -l0 -e "$script" |
|
last_hist=$(HISTTIMEFORMAT='' builtin history 1) command perl -n -l0 -e "$script" |
|
||||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '"$'\t'"↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \
|
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,alt-r:toggle-raw --wrap-sign '"$'\t'"↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \
|
||||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
|
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
|
||||||
) || return
|
) || return
|
||||||
READLINE_LINE=$(command perl -pe 's/^\d*\t//' <<< "$output")
|
READLINE_LINE=$(command perl -pe 's/^\d*\t//' <<< "$output")
|
||||||
if [[ -z "$READLINE_POINT" ]]; then
|
if [[ -z $READLINE_POINT ]]; then
|
||||||
echo "$READLINE_LINE"
|
echo "$READLINE_LINE"
|
||||||
else
|
else
|
||||||
READLINE_POINT=0x7fffffff
|
READLINE_POINT=0x7fffffff
|
||||||
@@ -93,7 +98,7 @@ if command -v perl > /dev/null; then
|
|||||||
else # awk - fallback for POSIX systems
|
else # awk - fallback for POSIX systems
|
||||||
__fzf_history__() {
|
__fzf_history__() {
|
||||||
local output script
|
local output script
|
||||||
[[ $(HISTTIMEFORMAT='' builtin history 1) =~ [[:digit:]]+ ]] # how many history entries
|
[[ $(HISTTIMEFORMAT='' builtin history 1) =~ [[:digit:]]+ ]] # how many history entries
|
||||||
script='function P(b) { ++n; sub(/^[ *]/, "", b); if (!seen[b]++) { printf "%d\t%s%c", '$((BASH_REMATCH + 1))' - n, b, 0 } }
|
script='function P(b) { ++n; sub(/^[ *]/, "", b); if (!seen[b]++) { printf "%d\t%s%c", '$((BASH_REMATCH + 1))' - n, b, 0 } }
|
||||||
NR==1 { b = substr($0, 2); next }
|
NR==1 { b = substr($0, 2); next }
|
||||||
/^\t/ { P(b); b = substr($0, 2); next }
|
/^\t/ { P(b); b = substr($0, 2); next }
|
||||||
@@ -101,13 +106,13 @@ else # awk - fallback for POSIX systems
|
|||||||
END { if (NR) P(b) }'
|
END { if (NR) P(b) }'
|
||||||
output=$(
|
output=$(
|
||||||
set +o pipefail
|
set +o pipefail
|
||||||
builtin fc -lnr -2147483648 2> /dev/null | # ( $'\t '<lines>$'\n' )* ; <lines> ::= [^\n]* ( $'\n'<lines> )*
|
builtin fc -lnr -2147483648 2> /dev/null | # ( $'\t '<lines>$'\n' )* ; <lines> ::= [^\n]* ( $'\n'<lines> )*
|
||||||
__fzf_exec_awk "$script" | # ( <counter>$'\t'<lines>$'\000' )*
|
__fzf_exec_awk "$script" | # ( <counter>$'\t'<lines>$'\000' )*
|
||||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '"$'\t'"↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \
|
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,alt-r:toggle-raw --wrap-sign '"$'\t'"↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \
|
||||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
|
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
|
||||||
) || return
|
) || return
|
||||||
READLINE_LINE=${output#*$'\t'}
|
READLINE_LINE=${output#*$'\t'}
|
||||||
if [[ -z "$READLINE_POINT" ]]; then
|
if [[ -z $READLINE_POINT ]]; then
|
||||||
echo "$READLINE_LINE"
|
echo "$READLINE_LINE"
|
||||||
else
|
else
|
||||||
READLINE_POINT=0x7fffffff
|
READLINE_POINT=0x7fffffff
|
||||||
@@ -116,43 +121,54 @@ else # awk - fallback for POSIX systems
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Required to refresh the prompt after fzf
|
# Required to refresh the prompt after fzf
|
||||||
bind -m emacs-standard '"\er": redraw-current-line'
|
bind -m emacs-standard '"\C-\e(": redraw-current-line'
|
||||||
|
|
||||||
bind -m vi-command '"\C-z": emacs-editing-mode'
|
bind -m vi-command '"\C-z": emacs-editing-mode'
|
||||||
bind -m vi-insert '"\C-z": emacs-editing-mode'
|
bind -m vi-insert '"\C-z": emacs-editing-mode'
|
||||||
bind -m emacs-standard '"\C-z": vi-editing-mode'
|
bind -m emacs-standard '"\C-z": vi-editing-mode'
|
||||||
|
|
||||||
if (( BASH_VERSINFO[0] < 4 )); then
|
if ((BASH_VERSINFO[0] < 4)); then
|
||||||
# CTRL-T - Paste the selected file path into the command line
|
# CTRL-T - Paste the selected file path into the command line
|
||||||
if [[ "${FZF_CTRL_T_COMMAND-x}" != "" ]]; then
|
if [[ ${FZF_CTRL_T_COMMAND-x} != "" ]]; then
|
||||||
bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__fzf_select__`\e\C-e\er\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f\C-y\ey\C-_"'
|
bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__fzf_select__`\e\C-e\C-\e(\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f\C-y\ey\C-_"'
|
||||||
bind -m vi-command '"\C-t": "\C-z\C-t\C-z"'
|
bind -m vi-command '"\C-t": "\C-z\C-t\C-z"'
|
||||||
bind -m vi-insert '"\C-t": "\C-z\C-t\C-z"'
|
bind -m vi-insert '"\C-t": "\C-z\C-t\C-z"'
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# CTRL-R - Paste the selected command from history into the command line
|
# CTRL-R - Paste the selected command from history into the command line
|
||||||
bind -m emacs-standard '"\C-r": "\C-e \C-u\C-y\ey\C-u`__fzf_history__`\e\C-e\er"'
|
if [[ ${FZF_CTRL_R_COMMAND-x} != "" ]]; then
|
||||||
bind -m vi-command '"\C-r": "\C-z\C-r\C-z"'
|
if [[ -n ${FZF_CTRL_R_COMMAND-} ]]; then
|
||||||
bind -m vi-insert '"\C-r": "\C-z\C-r\C-z"'
|
echo "warning: FZF_CTRL_R_COMMAND is set to a custom command, but custom commands are not yet supported for CTRL-R" >&2
|
||||||
|
fi
|
||||||
|
bind -m emacs-standard '"\C-r": "\C-e \C-u\C-y\ey\C-u`__fzf_history__`\e\C-e\C-\e("'
|
||||||
|
bind -m vi-command '"\C-r": "\C-z\C-r\C-z"'
|
||||||
|
bind -m vi-insert '"\C-r": "\C-z\C-r\C-z"'
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
# CTRL-T - Paste the selected file path into the command line
|
# CTRL-T - Paste the selected file path into the command line
|
||||||
if [[ "${FZF_CTRL_T_COMMAND-x}" != "" ]]; then
|
if [[ ${FZF_CTRL_T_COMMAND-x} != "" ]]; then
|
||||||
bind -m emacs-standard -x '"\C-t": fzf-file-widget'
|
bind -m emacs-standard -x '"\C-t": fzf-file-widget'
|
||||||
bind -m vi-command -x '"\C-t": fzf-file-widget'
|
bind -m vi-command -x '"\C-t": fzf-file-widget'
|
||||||
bind -m vi-insert -x '"\C-t": fzf-file-widget'
|
bind -m vi-insert -x '"\C-t": fzf-file-widget'
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# CTRL-R - Paste the selected command from history into the command line
|
# CTRL-R - Paste the selected command from history into the command line
|
||||||
bind -m emacs-standard -x '"\C-r": __fzf_history__'
|
if [[ ${FZF_CTRL_R_COMMAND-x} != "" ]]; then
|
||||||
bind -m vi-command -x '"\C-r": __fzf_history__'
|
if [[ -n ${FZF_CTRL_R_COMMAND-} ]]; then
|
||||||
bind -m vi-insert -x '"\C-r": __fzf_history__'
|
echo "warning: FZF_CTRL_R_COMMAND is set to a custom command, but custom commands are not yet supported for CTRL-R" >&2
|
||||||
|
fi
|
||||||
|
bind -m emacs-standard -x '"\C-r": __fzf_history__'
|
||||||
|
bind -m vi-command -x '"\C-r": __fzf_history__'
|
||||||
|
bind -m vi-insert -x '"\C-r": __fzf_history__'
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ALT-C - cd into the selected directory
|
# ALT-C - cd into the selected directory
|
||||||
if [[ "${FZF_ALT_C_COMMAND-x}" != "" ]]; then
|
if [[ ${FZF_ALT_C_COMMAND-x} != "" ]]; then
|
||||||
bind -m emacs-standard '"\ec": " \C-b\C-k \C-u`__fzf_cd__`\e\C-e\er\C-m\C-y\C-h\e \C-y\ey\C-x\C-x\C-d\C-y\ey\C-_"'
|
bind -m emacs-standard '"\ec": " \C-b\C-k \C-u`__fzf_cd__`\e\C-e\C-\e(\C-m\C-y\C-h\e \C-y\ey\C-x\C-x\C-d\C-y\ey\C-_"'
|
||||||
bind -m vi-command '"\ec": "\C-z\ec\C-z"'
|
bind -m vi-command '"\ec": "\C-z\ec\C-z"'
|
||||||
bind -m vi-insert '"\ec": "\C-z\ec\C-z"'
|
bind -m vi-insert '"\ec": "\C-z\ec\C-z"'
|
||||||
fi
|
fi
|
||||||
|
#----END shfmt
|
||||||
|
|
||||||
fi
|
fi
|
||||||
|
|||||||
+97
-41
@@ -7,6 +7,7 @@
|
|||||||
# - $FZF_TMUX_OPTS
|
# - $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_OPTS
|
# - $FZF_CTRL_R_OPTS
|
||||||
# - $FZF_ALT_C_COMMAND
|
# - $FZF_ALT_C_COMMAND
|
||||||
# - $FZF_ALT_C_OPTS
|
# - $FZF_ALT_C_OPTS
|
||||||
@@ -20,8 +21,8 @@
|
|||||||
function fzf_key_bindings
|
function fzf_key_bindings
|
||||||
|
|
||||||
# Check fish version
|
# Check fish version
|
||||||
set -l fish_ver (string match -r '^(\d+).(\d+)' $version 2> /dev/null; or echo 0\n0\n0)
|
if set -l -- fish_ver (string match -r '^(\d+)\.(\d+)' $version 2>/dev/null)
|
||||||
if test \( "$fish_ver[2]" -lt 3 \) -o \( "$fish_ver[2]" -eq 3 -a "$fish_ver[3]" -lt 1 \)
|
and test "$fish_ver[2]" -lt 3 -o "$fish_ver[2]" -eq 3 -a "$fish_ver[3]" -lt 1
|
||||||
echo "This script requires fish version 3.1b1 or newer." >&2
|
echo "This script requires fish version 3.1b1 or newer." >&2
|
||||||
return 1
|
return 1
|
||||||
else if not type -q fzf
|
else if not type -q fzf
|
||||||
@@ -29,9 +30,12 @@ function fzf_key_bindings
|
|||||||
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] \
|
||||||
@@ -50,17 +54,51 @@ 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 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_major (string match -r -- '^\d+' $version)
|
||||||
set -l -- fish_minor (string match -r -- '^\d+\.(\d+)' $version)[2]
|
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 -- match_regex '(?<fzf_query>[\s\S]*?(?=\n?$)$)'
|
||||||
set -l -- prefix_regex '^-[^\s=]+=|^-(?!-)\S'
|
set -l -- prefix_regex '^-[^\s=]+=|^-(?!-)\S'
|
||||||
if test "$fish_major" -eq 3 -a "$fish_minor" -lt 3
|
if test "$fish_major" -eq 3 -a "$fish_minor" -lt 3
|
||||||
@@ -68,16 +106,12 @@ function fzf_key_bindings
|
|||||||
set -- match_regex "(?<prefix>$prefix_regex)?$match_regex"
|
set -- match_regex "(?<prefix>$prefix_regex)?$match_regex"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Set $prefix and expanded $fzf_query with preserved trailing newlines.
|
|
||||||
if test "$fish_major" -ge 4
|
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)
|
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
|
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)
|
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
|
else
|
||||||
# fish older than v3.2.0 (v3.1b1 - v3.1.2)
|
|
||||||
set -l -- cl_token (commandline --current-token --tokenize | string collect -N)
|
set -l -- cl_token (commandline --current-token --tokenize | string collect -N)
|
||||||
set -- prefix (string match -r -- $prefix_regex $cl_token)
|
set -- prefix (string match -r -- $prefix_regex $cl_token)
|
||||||
set -- fzf_query (string replace -- "$prefix" '' $cl_token | string collect -N)
|
set -- fzf_query (string replace -- "$prefix" '' $cl_token | string collect -N)
|
||||||
@@ -85,22 +119,17 @@ function fzf_key_bindings
|
|||||||
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 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 -- 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
|
||||||
# fish older than v3.5.0 (v3.1b1 - v3.4.1)
|
|
||||||
if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
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 match -q -r -- '(?<fzf_query>^[\s\S]*?(?=\n?$)$)' \
|
||||||
(string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
|
(string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
|
||||||
else
|
else
|
||||||
# fish v3.1b1 - v3.1.2
|
|
||||||
set -- fzf_query (string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
|
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$' '')
|
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r '\\\n$' '')
|
||||||
end
|
end
|
||||||
@@ -111,16 +140,12 @@ function fzf_key_bindings
|
|||||||
end
|
end
|
||||||
|
|
||||||
if not string match -q -- '.' $dir; or string match -q -r -- '^\./|^\.$' $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 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
|
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
|
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 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
|
else
|
||||||
# fish older than v3.2.0 (v3.1b1 - v3.1.2)
|
|
||||||
set -- fzf_query (string replace -- "$dir" '' $fzf_query | string collect -N)
|
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$' '')
|
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^/?|\\\n$' '')
|
||||||
end
|
end
|
||||||
@@ -129,6 +154,7 @@ function fzf_key_bindings
|
|||||||
|
|
||||||
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"
|
||||||
@@ -139,13 +165,13 @@ function fzf_key_bindings
|
|||||||
|
|
||||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults \
|
set -lx FZF_DEFAULT_OPTS (__fzf_defaults \
|
||||||
"--reverse --walker=file,dir,follow,hidden --scheme=path" \
|
"--reverse --walker=file,dir,follow,hidden --scheme=path" \
|
||||||
"$FZF_CTRL_T_OPTS --multi --print0")
|
"--multi $FZF_CTRL_T_OPTS --print0")
|
||||||
|
|
||||||
set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND"
|
set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND"
|
||||||
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 -- $result))' '
|
and commandline -rt -- (string join -- ' ' $prefix(string escape --no-quoted -- $result))' '
|
||||||
|
|
||||||
commandline -f repaint
|
commandline -f repaint
|
||||||
end
|
end
|
||||||
@@ -156,24 +182,46 @@ function fzf_key_bindings
|
|||||||
set -l -- total_lines (count $command_line)
|
set -l -- total_lines (count $command_line)
|
||||||
set -l -- fzf_query (string escape -- $command_line[$current_line])
|
set -l -- fzf_query (string escape -- $command_line[$current_line])
|
||||||
|
|
||||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults '' \
|
set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults '' \
|
||||||
'--nth=2..,.. --scheme=history --multi --wrap-sign="\t↳ "' \
|
'--nth=2..,.. --scheme=history --multi --no-multi-line --no-wrap --wrap-sign="\t\t\t↳ " --preview-wrap-sign="↳ "' \
|
||||||
'--bind=\'shift-delete:execute-silent(eval history delete --exact --case-sensitive -- (string escape -n -- {+} | string replace -r -a "^\d*\\\\\\t|(?<=\\\\\\n)\\\\\\t" ""))+reload(eval $FZF_DEFAULT_COMMAND)\'' \
|
'--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=ctrl-r:toggle-sort --highlight-line $FZF_CTRL_R_OPTS" \
|
'--bind="alt-enter:become(string join0 -- (string collect -- {+2..} | fish_indent -i))"' \
|
||||||
'--accept-nth=2.. --read0 --print0 --with-shell='(status fish-path)\\ -c)
|
"--bind=ctrl-r:toggle-sort,alt-r:toggle-raw --highlight-line $FZF_CTRL_R_OPTS" \
|
||||||
|
'--accept-nth=2.. --delimiter="\t" --tabstop=4 --read0 --print0 --with-shell='(status fish-path)\\ -c)
|
||||||
|
|
||||||
|
# Add dynamic preview options if preview command isn't already set by user
|
||||||
|
if string match -qvr -- '--preview[= ]' "$FZF_DEFAULT_OPTS"
|
||||||
|
# Convert the highlighted timestamp using the date command if available
|
||||||
|
set -l -- date_cmd '{1}'
|
||||||
|
if type -q date
|
||||||
|
if date -d @0 '+%s' 2>/dev/null | string match -q 0
|
||||||
|
# GNU date
|
||||||
|
set -- date_cmd '(date -d @{1} \\"+%F %a %T\\")'
|
||||||
|
else if date -r 0 '+%s' 2>/dev/null | string match -q 0
|
||||||
|
# BSD date
|
||||||
|
set -- date_cmd '(date -r {1} \\"+%F %a %T\\")'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Prepend the options to allow user customizations
|
||||||
|
set -p -- FZF_DEFAULT_OPTS \
|
||||||
|
'--bind="focus,resize:bg-transform:if test \\"$FZF_COLUMNS\\" -gt 100 -a \\\\( \\"$FZF_SELECT_COUNT\\" -gt 0 -o \\\\( -z \\"$FZF_WRAP\\" -a (string length -- {}) -gt (math $FZF_COLUMNS - 4) \\\\) -o (string collect -- {2..} | fish_indent | count) -gt 1 \\\\); echo show-preview; else echo hide-preview; end"' \
|
||||||
|
'--preview="string collect -- (test \\"$FZF_SELECT_COUNT\\" -gt 0; and string collect -- {+2..}) \\"\\n# \\"'$date_cmd' {2..} | fish_indent --ansi"' \
|
||||||
|
'--preview-window="right,50%,wrap-word,follow,info,hidden"'
|
||||||
|
end
|
||||||
|
|
||||||
set -lx FZF_DEFAULT_OPTS_FILE
|
set -lx FZF_DEFAULT_OPTS_FILE
|
||||||
set -lx FZF_DEFAULT_COMMAND
|
|
||||||
|
|
||||||
if type -q perl
|
set -lx -- FZF_DEFAULT_COMMAND 'builtin history -z --show-time="%s%t"'
|
||||||
set -a FZF_DEFAULT_OPTS '--tac'
|
|
||||||
set FZF_DEFAULT_COMMAND 'builtin history -z --reverse | command perl -0 -pe \'s/^/$.\t/g; s/\n/\n\t/gm\''
|
# Enable syntax highlighting colors on fish v4.3.3 and newer
|
||||||
else
|
if set -l -- v (string match -r -- '^(\d+)\.(\d+)(?:\.(\d+))?' $version)
|
||||||
set FZF_DEFAULT_COMMAND \
|
and test "$v[2]" -gt 4 -o "$v[2]" -eq 4 -a \
|
||||||
'set -l h (builtin history -z --reverse | string split0);' \
|
\( "$v[3]" -gt 3 -o "$v[3]" -eq 3 -a \
|
||||||
'for i in (seq (count $h) -1 1);' \
|
\( -n "$v[4]" -a "$v[4]" -ge 3 \) \)
|
||||||
'string join0 -- $i\t(string replace -a -- \n \n\t $h[$i] | string collect);' \
|
|
||||||
'end'
|
set -a -- FZF_DEFAULT_OPTS '--ansi'
|
||||||
|
set -a -- FZF_DEFAULT_COMMAND '--color=always'
|
||||||
end
|
end
|
||||||
|
|
||||||
# Merge history from other sessions before searching
|
# Merge history from other sessions before searching
|
||||||
@@ -181,11 +229,11 @@ function fzf_key_bindings
|
|||||||
|
|
||||||
if set -l result (eval $FZF_DEFAULT_COMMAND \| (__fzfcmd) --query=$fzf_query | string split0)
|
if set -l result (eval $FZF_DEFAULT_COMMAND \| (__fzfcmd) --query=$fzf_query | string split0)
|
||||||
if test "$total_lines" -eq 1
|
if test "$total_lines" -eq 1
|
||||||
commandline -- (string replace -a -- \n\t \n $result)
|
commandline -- $result
|
||||||
else
|
else
|
||||||
set -l a (math $current_line - 1)
|
set -l a (math $current_line - 1)
|
||||||
set -l b (math $current_line + 1)
|
set -l b (math $current_line + 1)
|
||||||
commandline -- $command_line[1..$a] (string replace -a -- \n\t \n $result)
|
commandline -- $command_line[1..$a] $result
|
||||||
commandline -a -- '' $command_line[$b..-1]
|
commandline -a -- '' $command_line[$b..-1]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -214,8 +262,13 @@ function fzf_key_bindings
|
|||||||
commandline -f repaint
|
commandline -f repaint
|
||||||
end
|
end
|
||||||
|
|
||||||
bind \cr fzf-history-widget
|
if not set -q FZF_CTRL_R_COMMAND; or test -n "$FZF_CTRL_R_COMMAND"
|
||||||
bind -M insert \cr fzf-history-widget
|
if test -n "$FZF_CTRL_R_COMMAND"
|
||||||
|
echo "warning: FZF_CTRL_R_COMMAND is set to a custom command, but custom commands are not yet supported for CTRL-R" >&2
|
||||||
|
end
|
||||||
|
bind \cr fzf-history-widget
|
||||||
|
bind -M insert \cr fzf-history-widget
|
||||||
|
end
|
||||||
|
|
||||||
if not set -q FZF_CTRL_T_COMMAND; or test -n "$FZF_CTRL_T_COMMAND"
|
if not set -q FZF_CTRL_T_COMMAND; or test -n "$FZF_CTRL_T_COMMAND"
|
||||||
bind \ct fzf-file-widget
|
bind \ct fzf-file-widget
|
||||||
@@ -228,3 +281,6 @@ function fzf_key_bindings
|
|||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Run setup
|
||||||
|
fzf_key_bindings
|
||||||
|
|||||||
+49
-13
@@ -7,6 +7,7 @@
|
|||||||
# - $FZF_TMUX_OPTS
|
# - $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_OPTS
|
# - $FZF_CTRL_R_OPTS
|
||||||
# - $FZF_ALT_C_COMMAND
|
# - $FZF_ALT_C_COMMAND
|
||||||
# - $FZF_ALT_C_OPTS
|
# - $FZF_ALT_C_OPTS
|
||||||
@@ -40,7 +41,7 @@ if [[ -o interactive ]]; then
|
|||||||
|
|
||||||
#----BEGIN INCLUDE common.sh
|
#----BEGIN INCLUDE common.sh
|
||||||
# NOTE: Do not directly edit this section, which is copied from "common.sh".
|
# NOTE: Do not directly edit this section, which is copied from "common.sh".
|
||||||
# To modify it, one can edit "common.sh" and run "./update-common.sh" to apply
|
# To modify it, one can edit "common.sh" and run "./update.sh" to apply
|
||||||
# the changes. See code comments in "common.sh" for the implementation details.
|
# the changes. See code comments in "common.sh" for the implementation details.
|
||||||
|
|
||||||
__fzf_defaults() {
|
__fzf_defaults() {
|
||||||
@@ -54,10 +55,13 @@ __fzf_exec_awk() {
|
|||||||
__fzf_awk=awk
|
__fzf_awk=awk
|
||||||
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
|
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
|
||||||
__fzf_awk=/usr/xpg4/bin/awk
|
__fzf_awk=/usr/xpg4/bin/awk
|
||||||
elif command -v mawk >/dev/null 2>&1; then
|
elif command -v mawk > /dev/null 2>&1; then
|
||||||
local n x y z d
|
local n x y z d
|
||||||
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null)
|
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null)
|
||||||
[[ $n == mawk ]] && (( d >= 20230302 && (x * 1000 + y) * 1000 + z >= 1003004 )) && __fzf_awk=mawk
|
[[ $n == mawk ]] &&
|
||||||
|
(((x * 1000 + y) * 1000 + z >= 1003004)) 2> /dev/null &&
|
||||||
|
((d >= 20230302)) 2> /dev/null &&
|
||||||
|
__fzf_awk=mawk
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
LC_ALL=C exec "$__fzf_awk" "$@"
|
LC_ALL=C exec "$__fzf_awk" "$@"
|
||||||
@@ -124,25 +128,52 @@ fi
|
|||||||
|
|
||||||
# CTRL-R - Paste the selected command from history into the command line
|
# CTRL-R - Paste the selected command from history into the command line
|
||||||
fzf-history-widget() {
|
fzf-history-widget() {
|
||||||
local selected
|
local selected extracted_with_perl=0
|
||||||
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases noglob nobash_rematch 2> /dev/null
|
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases no_glob no_ksharrays extendedglob 2> /dev/null
|
||||||
# Ensure the module is loaded if not already, and the required features, such
|
# Ensure the module is loaded if not already, and the required features, such
|
||||||
# as the associative 'history' array, which maps event numbers to full history
|
# as the associative 'history' array, which maps event numbers to full history
|
||||||
# lines, are set. Also, make sure Perl is installed for multi-line output.
|
# lines, are set. Also, make sure Perl is installed for multi-line output.
|
||||||
if zmodload -F zsh/parameter p:{commands,history} 2>/dev/null && (( ${+commands[perl]} )); then
|
if zmodload -F zsh/parameter p:{commands,history} 2>/dev/null && (( ${+commands[perl]} )); then
|
||||||
selected="$(printf '%s\t%s\000' "${(kv)history[@]}" |
|
selected="$(printf '%s\t%s\000' "${(kv)history[@]}" |
|
||||||
perl -0 -ne 'if (!$seen{(/^\s*[0-9]+\**\t(.*)/s, $1)}++) { s/\n/\n\t/g; print; }' |
|
perl -0 -ne 'if (!$seen{(/^\s*[0-9]+\**\t(.*)/s, $1)}++) { s/\n/\n\t/g; print; }' |
|
||||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '\t↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m --read0") \
|
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,alt-r:toggle-raw --wrap-sign '\t↳ ' --highlight-line --multi ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} --read0") \
|
||||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
|
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
|
||||||
|
extracted_with_perl=1
|
||||||
else
|
else
|
||||||
selected="$(fc -rl 1 | __fzf_exec_awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' |
|
selected="$(fc -rl 1 | __fzf_exec_awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' |
|
||||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '\t↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m") \
|
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,alt-r:toggle-raw --wrap-sign '\t↳ ' --highlight-line --multi ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER}") \
|
||||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
|
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
|
||||||
fi
|
fi
|
||||||
local ret=$?
|
local ret=$?
|
||||||
|
local -a cmds
|
||||||
|
# Avoid leaking auto assigned values when using backreferences '(#b)'
|
||||||
|
local -a mbegin mend match
|
||||||
if [ -n "$selected" ]; then
|
if [ -n "$selected" ]; then
|
||||||
if [[ $(__fzf_exec_awk '{print $1; exit}' <<< "$selected") =~ ^[1-9][0-9]* ]]; then
|
# Heuristic to check if the selected value is from history or a custom query
|
||||||
zle vi-fetch-history -n $MATCH
|
if ((( extracted_with_perl )) && [[ $selected == <->$'\t'* ]]) ||
|
||||||
|
((( ! extracted_with_perl )) && [[ $selected == [[:blank:]]#<->( |\* )* ]]); then
|
||||||
|
# Split at newlines
|
||||||
|
for line in ${(ps:\n:)selected}; do
|
||||||
|
if (( extracted_with_perl )); then
|
||||||
|
if [[ $line == (#b)(<->)(#B)$'\t'* ]]; then
|
||||||
|
(( ${+history[${match[1]}]} )) && cmds+=("${history[${match[1]}]}")
|
||||||
|
fi
|
||||||
|
elif [[ $line == [[:blank:]]#(#b)(<->)(#B)( |\* )* ]]; then
|
||||||
|
# Avoid $history array: lags behind 'fc' on foreign commands (*)
|
||||||
|
# https://zsh.org/mla/users/2024/msg00692.html
|
||||||
|
# Push BUFFER onto stack; fetch and save history entry from BUFFER; restore
|
||||||
|
zle .push-line
|
||||||
|
zle vi-fetch-history -n ${match[1]}
|
||||||
|
(( ${#BUFFER} )) && cmds+=("${BUFFER}")
|
||||||
|
BUFFER=""
|
||||||
|
zle .get-line
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if (( ${#cmds[@]} )); then
|
||||||
|
# Join by newline after stripping trailing newlines from each command
|
||||||
|
BUFFER="${(pj:\n:)${(@)cmds%%$'\n'#}}"
|
||||||
|
CURSOR=${#BUFFER}
|
||||||
|
fi
|
||||||
else # selected is a custom query, not from history
|
else # selected is a custom query, not from history
|
||||||
LBUFFER="$selected"
|
LBUFFER="$selected"
|
||||||
fi
|
fi
|
||||||
@@ -150,10 +181,15 @@ fzf-history-widget() {
|
|||||||
zle reset-prompt
|
zle reset-prompt
|
||||||
return $ret
|
return $ret
|
||||||
}
|
}
|
||||||
zle -N fzf-history-widget
|
if [[ ${FZF_CTRL_R_COMMAND-x} != "" ]]; then
|
||||||
bindkey -M emacs '^R' fzf-history-widget
|
if [[ -n ${FZF_CTRL_R_COMMAND-} ]]; then
|
||||||
bindkey -M vicmd '^R' fzf-history-widget
|
echo "warning: FZF_CTRL_R_COMMAND is set to a custom command, but custom commands are not yet supported for CTRL-R" >&2
|
||||||
bindkey -M viins '^R' fzf-history-widget
|
fi
|
||||||
|
zle -N fzf-history-widget
|
||||||
|
bindkey -M emacs '^R' fzf-history-widget
|
||||||
|
bindkey -M vicmd '^R' fzf-history-widget
|
||||||
|
bindkey -M viins '^R' fzf-history-widget
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
} always {
|
} always {
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
# This script applies the contents of "common.sh" to the other files.
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Go to the directory that contains this script
|
|
||||||
dir=${0%"${0##*/}"}
|
|
||||||
if [ -n "$dir" ]; then
|
|
||||||
cd "$dir"
|
|
||||||
fi
|
|
||||||
|
|
||||||
update() {
|
|
||||||
{
|
|
||||||
sed -n '1,/^#----BEGIN INCLUDE common\.sh/p' "$1"
|
|
||||||
cat <<EOF
|
|
||||||
# NOTE: Do not directly edit this section, which is copied from "common.sh".
|
|
||||||
# To modify it, one can edit "common.sh" and run "./update-common.sh" to apply
|
|
||||||
# the changes. See code comments in "common.sh" for the implementation details.
|
|
||||||
EOF
|
|
||||||
grep -v '^[[:blank:]]*#' common.sh # remove code comments in common.sh
|
|
||||||
sed -n '/^#----END INCLUDE/,$p' "$1"
|
|
||||||
} > "$1.part"
|
|
||||||
|
|
||||||
mv -f "$1.part" "$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
update completion.bash
|
|
||||||
update completion.zsh
|
|
||||||
update key-bindings.bash
|
|
||||||
update key-bindings.zsh
|
|
||||||
Executable
+70
@@ -0,0 +1,70 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# This script applies the contents of "common.sh" to the other files.
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
dir=${0%"${0##*/}"}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
{
|
||||||
|
sed -n "1,/^#----BEGIN INCLUDE $1/p" "$2"
|
||||||
|
cat << EOF
|
||||||
|
# NOTE: Do not directly edit this section, which is copied from "$1".
|
||||||
|
# To modify it, one can edit "$1" and run "./update.sh" to apply
|
||||||
|
# the changes. See code comments in "$1" for the implementation details.
|
||||||
|
EOF
|
||||||
|
echo
|
||||||
|
grep -v '^[[:blank:]]*#' "$dir/$1" # remove code comments from the common file
|
||||||
|
sed -n '/^#----END INCLUDE/,$p' "$2"
|
||||||
|
} > "$2.part"
|
||||||
|
|
||||||
|
mv -f "$2.part" "$2"
|
||||||
|
}
|
||||||
|
|
||||||
|
update "common.sh" "$dir/completion.bash"
|
||||||
|
update "common.sh" "$dir/completion.zsh"
|
||||||
|
update "common.sh" "$dir/key-bindings.bash"
|
||||||
|
update "common.sh" "$dir/key-bindings.zsh"
|
||||||
|
update "common.fish" "$dir/completion.fish"
|
||||||
|
update "common.fish" "$dir/key-bindings.fish"
|
||||||
|
|
||||||
|
# Check if --check is in ARGV
|
||||||
|
check=0
|
||||||
|
rest=()
|
||||||
|
for arg in "$@"; do
|
||||||
|
case $arg in
|
||||||
|
--check) check=1 ;;
|
||||||
|
*) rest+=("$arg") ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
fmt() {
|
||||||
|
if ! grep -q "^#----BEGIN shfmt" "$1"; then
|
||||||
|
if [[ $check == 1 ]]; then
|
||||||
|
shfmt -d "$1"
|
||||||
|
return $?
|
||||||
|
else
|
||||||
|
shfmt -w "$1"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sed -n '1,/^#----BEGIN shfmt/p' "$1" | sed '$d'
|
||||||
|
sed -n '/^#----BEGIN shfmt/,/^#----END shfmt/p' "$1" | shfmt --filename "$1"
|
||||||
|
sed -n '/^#----END shfmt/,$p' "$1" | sed '1d'
|
||||||
|
} > "$1.part"
|
||||||
|
|
||||||
|
if [[ $check == 1 ]]; then
|
||||||
|
diff -q "$1" "$1.part"
|
||||||
|
ret=$?
|
||||||
|
rm -f "$1.part"
|
||||||
|
return $ret
|
||||||
|
fi
|
||||||
|
|
||||||
|
mv -f "$1.part" "$1"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
for file in "${rest[@]}"; do
|
||||||
|
fmt "$file" || exit $?
|
||||||
|
done
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2013-2025 Junegunn Choi
|
Copyright (c) 2013-2026 Junegunn Choi
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
+155
-144
@@ -30,153 +30,164 @@ func _() {
|
|||||||
_ = x[actChangeBorderLabel-19]
|
_ = x[actChangeBorderLabel-19]
|
||||||
_ = x[actChangeGhost-20]
|
_ = x[actChangeGhost-20]
|
||||||
_ = x[actChangeHeader-21]
|
_ = x[actChangeHeader-21]
|
||||||
_ = x[actChangeFooter-22]
|
_ = x[actChangeHeaderLines-22]
|
||||||
_ = x[actChangeHeaderLabel-23]
|
_ = x[actChangeFooter-23]
|
||||||
_ = x[actChangeFooterLabel-24]
|
_ = x[actChangeHeaderLabel-24]
|
||||||
_ = x[actChangeInputLabel-25]
|
_ = x[actChangeFooterLabel-25]
|
||||||
_ = x[actChangeListLabel-26]
|
_ = x[actChangeInputLabel-26]
|
||||||
_ = x[actChangeMulti-27]
|
_ = x[actChangeListLabel-27]
|
||||||
_ = x[actChangeNth-28]
|
_ = x[actChangeMulti-28]
|
||||||
_ = x[actChangePointer-29]
|
_ = x[actChangeNth-29]
|
||||||
_ = x[actChangePreview-30]
|
_ = x[actChangePointer-30]
|
||||||
_ = x[actChangePreviewLabel-31]
|
_ = x[actChangePreview-31]
|
||||||
_ = x[actChangePreviewWindow-32]
|
_ = x[actChangePreviewLabel-32]
|
||||||
_ = x[actChangePrompt-33]
|
_ = x[actChangePreviewWindow-33]
|
||||||
_ = x[actChangeQuery-34]
|
_ = x[actChangePrompt-34]
|
||||||
_ = x[actClearScreen-35]
|
_ = x[actChangeQuery-35]
|
||||||
_ = x[actClearQuery-36]
|
_ = x[actClearScreen-36]
|
||||||
_ = x[actClearSelection-37]
|
_ = x[actClearQuery-37]
|
||||||
_ = x[actClose-38]
|
_ = x[actClearSelection-38]
|
||||||
_ = x[actDeleteChar-39]
|
_ = x[actClose-39]
|
||||||
_ = x[actDeleteCharEof-40]
|
_ = x[actDeleteChar-40]
|
||||||
_ = x[actEndOfLine-41]
|
_ = x[actDeleteCharEof-41]
|
||||||
_ = x[actFatal-42]
|
_ = x[actEndOfLine-42]
|
||||||
_ = x[actForwardChar-43]
|
_ = x[actFatal-43]
|
||||||
_ = x[actForwardWord-44]
|
_ = x[actForwardChar-44]
|
||||||
_ = x[actForwardSubWord-45]
|
_ = x[actForwardWord-45]
|
||||||
_ = x[actKillLine-46]
|
_ = x[actForwardSubWord-46]
|
||||||
_ = x[actKillWord-47]
|
_ = x[actKillLine-47]
|
||||||
_ = x[actKillSubWord-48]
|
_ = x[actKillWord-48]
|
||||||
_ = x[actUnixLineDiscard-49]
|
_ = x[actKillSubWord-49]
|
||||||
_ = x[actUnixWordRubout-50]
|
_ = x[actUnixLineDiscard-50]
|
||||||
_ = x[actYank-51]
|
_ = x[actUnixWordRubout-51]
|
||||||
_ = x[actBackwardKillWord-52]
|
_ = x[actYank-52]
|
||||||
_ = x[actBackwardKillSubWord-53]
|
_ = x[actBackwardKillWord-53]
|
||||||
_ = x[actSelectAll-54]
|
_ = x[actBackwardKillSubWord-54]
|
||||||
_ = x[actDeselectAll-55]
|
_ = x[actSelectAll-55]
|
||||||
_ = x[actToggle-56]
|
_ = x[actDeselectAll-56]
|
||||||
_ = x[actToggleSearch-57]
|
_ = x[actToggle-57]
|
||||||
_ = x[actToggleAll-58]
|
_ = x[actToggleSearch-58]
|
||||||
_ = x[actToggleDown-59]
|
_ = x[actToggleAll-59]
|
||||||
_ = x[actToggleUp-60]
|
_ = x[actToggleDown-60]
|
||||||
_ = x[actToggleIn-61]
|
_ = x[actToggleUp-61]
|
||||||
_ = x[actToggleOut-62]
|
_ = x[actToggleIn-62]
|
||||||
_ = x[actToggleTrack-63]
|
_ = x[actToggleOut-63]
|
||||||
_ = x[actToggleTrackCurrent-64]
|
_ = x[actToggleTrack-64]
|
||||||
_ = x[actToggleHeader-65]
|
_ = x[actToggleTrackCurrent-65]
|
||||||
_ = x[actToggleWrap-66]
|
_ = x[actToggleHeader-66]
|
||||||
_ = x[actToggleMultiLine-67]
|
_ = x[actToggleWrap-67]
|
||||||
_ = x[actToggleHscroll-68]
|
_ = x[actToggleWrapWord-68]
|
||||||
_ = x[actTrackCurrent-69]
|
_ = x[actToggleMultiLine-69]
|
||||||
_ = x[actToggleInput-70]
|
_ = x[actToggleHscroll-70]
|
||||||
_ = x[actHideInput-71]
|
_ = x[actToggleRaw-71]
|
||||||
_ = x[actShowInput-72]
|
_ = x[actEnableRaw-72]
|
||||||
_ = x[actUntrackCurrent-73]
|
_ = x[actDisableRaw-73]
|
||||||
_ = x[actDown-74]
|
_ = x[actTrackCurrent-74]
|
||||||
_ = x[actUp-75]
|
_ = x[actToggleInput-75]
|
||||||
_ = x[actPageUp-76]
|
_ = x[actHideInput-76]
|
||||||
_ = x[actPageDown-77]
|
_ = x[actShowInput-77]
|
||||||
_ = x[actPosition-78]
|
_ = x[actUntrackCurrent-78]
|
||||||
_ = x[actHalfPageUp-79]
|
_ = x[actDown-79]
|
||||||
_ = x[actHalfPageDown-80]
|
_ = x[actDownMatch-80]
|
||||||
_ = x[actOffsetUp-81]
|
_ = x[actUp-81]
|
||||||
_ = x[actOffsetDown-82]
|
_ = x[actUpMatch-82]
|
||||||
_ = x[actOffsetMiddle-83]
|
_ = x[actPageUp-83]
|
||||||
_ = x[actJump-84]
|
_ = x[actPageDown-84]
|
||||||
_ = x[actJumpAccept-85]
|
_ = x[actPosition-85]
|
||||||
_ = x[actPrintQuery-86]
|
_ = x[actHalfPageUp-86]
|
||||||
_ = x[actRefreshPreview-87]
|
_ = x[actHalfPageDown-87]
|
||||||
_ = x[actReplaceQuery-88]
|
_ = x[actOffsetUp-88]
|
||||||
_ = x[actToggleSort-89]
|
_ = x[actOffsetDown-89]
|
||||||
_ = x[actShowPreview-90]
|
_ = x[actOffsetMiddle-90]
|
||||||
_ = x[actHidePreview-91]
|
_ = x[actJump-91]
|
||||||
_ = x[actTogglePreview-92]
|
_ = x[actJumpAccept-92]
|
||||||
_ = x[actTogglePreviewWrap-93]
|
_ = x[actPrintQuery-93]
|
||||||
_ = x[actTransform-94]
|
_ = x[actRefreshPreview-94]
|
||||||
_ = x[actTransformBorderLabel-95]
|
_ = x[actReplaceQuery-95]
|
||||||
_ = x[actTransformGhost-96]
|
_ = x[actToggleSort-96]
|
||||||
_ = x[actTransformHeader-97]
|
_ = x[actShowPreview-97]
|
||||||
_ = x[actTransformFooter-98]
|
_ = x[actHidePreview-98]
|
||||||
_ = x[actTransformHeaderLabel-99]
|
_ = x[actTogglePreview-99]
|
||||||
_ = x[actTransformFooterLabel-100]
|
_ = x[actTogglePreviewWrap-100]
|
||||||
_ = x[actTransformInputLabel-101]
|
_ = x[actTogglePreviewWrapWord-101]
|
||||||
_ = x[actTransformListLabel-102]
|
_ = x[actTransform-102]
|
||||||
_ = x[actTransformNth-103]
|
_ = x[actTransformBorderLabel-103]
|
||||||
_ = x[actTransformPointer-104]
|
_ = x[actTransformGhost-104]
|
||||||
_ = x[actTransformPreviewLabel-105]
|
_ = x[actTransformHeader-105]
|
||||||
_ = x[actTransformPrompt-106]
|
_ = x[actTransformHeaderLines-106]
|
||||||
_ = x[actTransformQuery-107]
|
_ = x[actTransformFooter-107]
|
||||||
_ = x[actTransformSearch-108]
|
_ = x[actTransformHeaderLabel-108]
|
||||||
_ = x[actTrigger-109]
|
_ = x[actTransformFooterLabel-109]
|
||||||
_ = x[actBgTransform-110]
|
_ = x[actTransformInputLabel-110]
|
||||||
_ = x[actBgTransformBorderLabel-111]
|
_ = x[actTransformListLabel-111]
|
||||||
_ = x[actBgTransformGhost-112]
|
_ = x[actTransformNth-112]
|
||||||
_ = x[actBgTransformHeader-113]
|
_ = x[actTransformPointer-113]
|
||||||
_ = x[actBgTransformFooter-114]
|
_ = x[actTransformPreviewLabel-114]
|
||||||
_ = x[actBgTransformHeaderLabel-115]
|
_ = x[actTransformPrompt-115]
|
||||||
_ = x[actBgTransformFooterLabel-116]
|
_ = x[actTransformQuery-116]
|
||||||
_ = x[actBgTransformInputLabel-117]
|
_ = x[actTransformSearch-117]
|
||||||
_ = x[actBgTransformListLabel-118]
|
_ = x[actTrigger-118]
|
||||||
_ = x[actBgTransformNth-119]
|
_ = x[actBgTransform-119]
|
||||||
_ = x[actBgTransformPointer-120]
|
_ = x[actBgTransformBorderLabel-120]
|
||||||
_ = x[actBgTransformPreviewLabel-121]
|
_ = x[actBgTransformGhost-121]
|
||||||
_ = x[actBgTransformPrompt-122]
|
_ = x[actBgTransformHeader-122]
|
||||||
_ = x[actBgTransformQuery-123]
|
_ = x[actBgTransformHeaderLines-123]
|
||||||
_ = x[actBgTransformSearch-124]
|
_ = x[actBgTransformFooter-124]
|
||||||
_ = x[actBgCancel-125]
|
_ = x[actBgTransformHeaderLabel-125]
|
||||||
_ = x[actSearch-126]
|
_ = x[actBgTransformFooterLabel-126]
|
||||||
_ = x[actPreview-127]
|
_ = x[actBgTransformInputLabel-127]
|
||||||
_ = x[actPreviewTop-128]
|
_ = x[actBgTransformListLabel-128]
|
||||||
_ = x[actPreviewBottom-129]
|
_ = x[actBgTransformNth-129]
|
||||||
_ = x[actPreviewUp-130]
|
_ = x[actBgTransformPointer-130]
|
||||||
_ = x[actPreviewDown-131]
|
_ = x[actBgTransformPreviewLabel-131]
|
||||||
_ = x[actPreviewPageUp-132]
|
_ = x[actBgTransformPrompt-132]
|
||||||
_ = x[actPreviewPageDown-133]
|
_ = x[actBgTransformQuery-133]
|
||||||
_ = x[actPreviewHalfPageUp-134]
|
_ = x[actBgTransformSearch-134]
|
||||||
_ = x[actPreviewHalfPageDown-135]
|
_ = x[actBgCancel-135]
|
||||||
_ = x[actPrevHistory-136]
|
_ = x[actSearch-136]
|
||||||
_ = x[actPrevSelected-137]
|
_ = x[actPreview-137]
|
||||||
_ = x[actPrint-138]
|
_ = x[actPreviewTop-138]
|
||||||
_ = x[actPut-139]
|
_ = x[actPreviewBottom-139]
|
||||||
_ = x[actNextHistory-140]
|
_ = x[actPreviewUp-140]
|
||||||
_ = x[actNextSelected-141]
|
_ = x[actPreviewDown-141]
|
||||||
_ = x[actExecute-142]
|
_ = x[actPreviewPageUp-142]
|
||||||
_ = x[actExecuteSilent-143]
|
_ = x[actPreviewPageDown-143]
|
||||||
_ = x[actExecuteMulti-144]
|
_ = x[actPreviewHalfPageUp-144]
|
||||||
_ = x[actSigStop-145]
|
_ = x[actPreviewHalfPageDown-145]
|
||||||
_ = x[actFirst-146]
|
_ = x[actPrevHistory-146]
|
||||||
_ = x[actLast-147]
|
_ = x[actPrevSelected-147]
|
||||||
_ = x[actReload-148]
|
_ = x[actPrint-148]
|
||||||
_ = x[actReloadSync-149]
|
_ = x[actPut-149]
|
||||||
_ = x[actDisableSearch-150]
|
_ = x[actNextHistory-150]
|
||||||
_ = x[actEnableSearch-151]
|
_ = x[actNextSelected-151]
|
||||||
_ = x[actSelect-152]
|
_ = x[actExecute-152]
|
||||||
_ = x[actDeselect-153]
|
_ = x[actExecuteSilent-153]
|
||||||
_ = x[actUnbind-154]
|
_ = x[actExecuteMulti-154]
|
||||||
_ = x[actRebind-155]
|
_ = x[actSigStop-155]
|
||||||
_ = x[actToggleBind-156]
|
_ = x[actBest-156]
|
||||||
_ = x[actBecome-157]
|
_ = x[actFirst-157]
|
||||||
_ = x[actShowHeader-158]
|
_ = x[actLast-158]
|
||||||
_ = x[actHideHeader-159]
|
_ = x[actReload-159]
|
||||||
_ = x[actBell-160]
|
_ = x[actReloadSync-160]
|
||||||
_ = x[actExclude-161]
|
_ = x[actDisableSearch-161]
|
||||||
_ = x[actExcludeMulti-162]
|
_ = x[actEnableSearch-162]
|
||||||
_ = x[actAsync-163]
|
_ = x[actSelect-163]
|
||||||
|
_ = x[actDeselect-164]
|
||||||
|
_ = x[actUnbind-165]
|
||||||
|
_ = x[actRebind-166]
|
||||||
|
_ = x[actToggleBind-167]
|
||||||
|
_ = x[actBecome-168]
|
||||||
|
_ = x[actShowHeader-169]
|
||||||
|
_ = x[actHideHeader-170]
|
||||||
|
_ = x[actBell-171]
|
||||||
|
_ = x[actExclude-172]
|
||||||
|
_ = x[actExcludeMulti-173]
|
||||||
|
_ = x[actAsync-174]
|
||||||
}
|
}
|
||||||
|
|
||||||
const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactBackwardSubWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactForwardSubWordactKillLineactKillWordactKillSubWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactBackwardKillSubWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactTriggeractBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactBgCancelactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsync"
|
const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactBackwardSubWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeHeaderLinesactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactForwardSubWordactKillLineactKillWordactKillSubWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactBackwardKillSubWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleWrapWordactToggleMultiLineactToggleHscrollactToggleRawactEnableRawactDisableRawactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactDownMatchactUpactUpMatchactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTogglePreviewWrapWordactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformHeaderLinesactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactTriggeractBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformHeaderLinesactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactBgCancelactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactBestactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsync"
|
||||||
|
|
||||||
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 57, 77, 84, 92, 110, 118, 127, 144, 165, 180, 201, 225, 240, 258, 267, 287, 301, 316, 331, 351, 371, 390, 408, 422, 434, 450, 466, 487, 509, 524, 538, 552, 565, 582, 590, 603, 619, 631, 639, 653, 667, 684, 695, 706, 720, 738, 755, 762, 781, 803, 815, 829, 838, 853, 865, 878, 889, 900, 912, 926, 947, 962, 975, 993, 1009, 1024, 1038, 1050, 1062, 1079, 1086, 1091, 1100, 1111, 1122, 1135, 1150, 1161, 1174, 1189, 1196, 1209, 1222, 1239, 1254, 1267, 1281, 1295, 1311, 1331, 1343, 1366, 1383, 1401, 1419, 1442, 1465, 1487, 1508, 1523, 1542, 1566, 1584, 1601, 1619, 1629, 1643, 1668, 1687, 1707, 1727, 1752, 1777, 1801, 1824, 1841, 1862, 1888, 1908, 1927, 1947, 1958, 1967, 1977, 1990, 2006, 2018, 2032, 2048, 2066, 2086, 2108, 2122, 2137, 2145, 2151, 2165, 2180, 2190, 2206, 2221, 2231, 2239, 2246, 2255, 2268, 2284, 2299, 2308, 2319, 2328, 2337, 2350, 2359, 2372, 2385, 2392, 2402, 2417, 2425}
|
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 57, 77, 84, 92, 110, 118, 127, 144, 165, 180, 201, 225, 240, 258, 267, 287, 301, 316, 336, 351, 371, 391, 410, 428, 442, 454, 470, 486, 507, 529, 544, 558, 572, 585, 602, 610, 623, 639, 651, 659, 673, 687, 704, 715, 726, 740, 758, 775, 782, 801, 823, 835, 849, 858, 873, 885, 898, 909, 920, 932, 946, 967, 982, 995, 1012, 1030, 1046, 1058, 1070, 1083, 1098, 1112, 1124, 1136, 1153, 1160, 1172, 1177, 1187, 1196, 1207, 1218, 1231, 1246, 1257, 1270, 1285, 1292, 1305, 1318, 1335, 1350, 1363, 1377, 1391, 1407, 1427, 1451, 1463, 1486, 1503, 1521, 1544, 1562, 1585, 1608, 1630, 1651, 1666, 1685, 1709, 1727, 1744, 1762, 1772, 1786, 1811, 1830, 1850, 1875, 1895, 1920, 1945, 1969, 1992, 2009, 2030, 2056, 2076, 2095, 2115, 2126, 2135, 2145, 2158, 2174, 2186, 2200, 2216, 2234, 2254, 2276, 2290, 2305, 2313, 2319, 2333, 2348, 2358, 2374, 2389, 2399, 2406, 2414, 2421, 2430, 2443, 2459, 2474, 2483, 2494, 2503, 2512, 2525, 2534, 2547, 2560, 2567, 2577, 2592, 2600}
|
||||||
|
|
||||||
func (i actionType) String() string {
|
func (i actionType) String() string {
|
||||||
if i < 0 || i >= actionType(len(_actionType_index)-1) {
|
if i < 0 || i >= actionType(len(_actionType_index)-1) {
|
||||||
|
|||||||
+11
-9
@@ -365,7 +365,7 @@ func asciiFuzzyIndex(input *util.Chars, pattern []rune, caseSensitive bool) (int
|
|||||||
|
|
||||||
firstIdx, idx, lastIdx := 0, 0, 0
|
firstIdx, idx, lastIdx := 0, 0, 0
|
||||||
var b byte
|
var b byte
|
||||||
for pidx := 0; pidx < len(pattern); pidx++ {
|
for pidx := range pattern {
|
||||||
b = byte(pattern[pidx])
|
b = byte(pattern[pidx])
|
||||||
idx = trySkip(input, caseSensitive, b, idx)
|
idx = trySkip(input, caseSensitive, b, idx)
|
||||||
if idx < 0 {
|
if idx < 0 {
|
||||||
@@ -445,7 +445,9 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
|||||||
|
|
||||||
// Since O(nm) algorithm can be prohibitively expensive for large input,
|
// Since O(nm) algorithm can be prohibitively expensive for large input,
|
||||||
// we fall back to the greedy algorithm.
|
// we fall back to the greedy algorithm.
|
||||||
if slab != nil && N*M > cap(slab.I16) {
|
// Also, we should not allow a very long pattern to avoid 16-bit integer
|
||||||
|
// overflow in the score matrix. 1000 is a safe limit.
|
||||||
|
if slab != nil && N*M > cap(slab.I16) || M > 1000 {
|
||||||
return FuzzyMatchV1(caseSensitive, normalize, forward, input, pattern, withPos, slab)
|
return FuzzyMatchV1(caseSensitive, normalize, forward, input, pattern, withPos, slab)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -501,7 +503,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
|||||||
if pidx < M {
|
if pidx < M {
|
||||||
F[pidx] = int32(off)
|
F[pidx] = int32(off)
|
||||||
pidx++
|
pidx++
|
||||||
pchar = pattern[util.Min(pidx, M-1)]
|
pchar = pattern[min(pidx, M-1)]
|
||||||
}
|
}
|
||||||
lastIdx = off
|
lastIdx = off
|
||||||
}
|
}
|
||||||
@@ -519,9 +521,9 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
|||||||
inGap = false
|
inGap = false
|
||||||
} else {
|
} else {
|
||||||
if inGap {
|
if inGap {
|
||||||
H0[off] = util.Max16(prevH0+scoreGapExtension, 0)
|
H0[off] = max(prevH0+scoreGapExtension, 0)
|
||||||
} else {
|
} else {
|
||||||
H0[off] = util.Max16(prevH0+scoreGapStart, 0)
|
H0[off] = max(prevH0+scoreGapStart, 0)
|
||||||
}
|
}
|
||||||
C0[off] = 0
|
C0[off] = 0
|
||||||
inGap = true
|
inGap = true
|
||||||
@@ -587,7 +589,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
|||||||
if b >= bonusBoundary && b > fb {
|
if b >= bonusBoundary && b > fb {
|
||||||
consecutive = 1
|
consecutive = 1
|
||||||
} else {
|
} else {
|
||||||
b = util.Max16(b, util.Max16(bonusConsecutive, fb))
|
b = max(b, bonusConsecutive, fb)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if s1+b < s2 {
|
if s1+b < s2 {
|
||||||
@@ -600,7 +602,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
|||||||
Csub[off] = consecutive
|
Csub[off] = consecutive
|
||||||
|
|
||||||
inGap = s1 < s2
|
inGap = s1 < s2
|
||||||
score := util.Max16(util.Max16(s1, s2), 0)
|
score := max(s1, s2, 0)
|
||||||
if pidx == M-1 && (forward && score > maxScore || !forward && score >= maxScore) {
|
if pidx == M-1 && (forward && score > maxScore || !forward && score >= maxScore) {
|
||||||
maxScore, maxScorePos = score, col
|
maxScore, maxScorePos = score, col
|
||||||
}
|
}
|
||||||
@@ -684,7 +686,7 @@ func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, patter
|
|||||||
if bonus >= bonusBoundary && bonus > firstBonus {
|
if bonus >= bonusBoundary && bonus > firstBonus {
|
||||||
firstBonus = bonus
|
firstBonus = bonus
|
||||||
}
|
}
|
||||||
bonus = util.Max16(util.Max16(bonus, firstBonus), bonusConsecutive)
|
bonus = max(bonus, firstBonus, bonusConsecutive)
|
||||||
}
|
}
|
||||||
if pidx == 0 {
|
if pidx == 0 {
|
||||||
score += int(bonus * bonusFirstCharMultiplier)
|
score += int(bonus * bonusFirstCharMultiplier)
|
||||||
@@ -726,7 +728,7 @@ func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.C
|
|||||||
lenRunes := text.Length()
|
lenRunes := text.Length()
|
||||||
lenPattern := len(pattern)
|
lenPattern := len(pattern)
|
||||||
|
|
||||||
for index := 0; index < lenRunes; index++ {
|
for index := range lenRunes {
|
||||||
char := text.Get(indexAt(index, lenRunes, forward))
|
char := text.Get(indexAt(index, lenRunes, forward))
|
||||||
// This is considerably faster than blindly applying strings.ToLower to the
|
// This is considerably faster than blindly applying strings.ToLower to the
|
||||||
// whole string
|
// whole string
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ func TestFuzzyMatch(t *testing.T) {
|
|||||||
scoreGapStart*2+scoreGapExtension*2)
|
scoreGapStart*2+scoreGapExtension*2)
|
||||||
assertMatch(t, fn, true, forward, "FooBar Baz", "FooB", 0, 4,
|
assertMatch(t, fn, true, forward, "FooBar Baz", "FooB", 0, 4,
|
||||||
scoreMatch*4+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+int(bonusBoundaryWhite)*2+
|
scoreMatch*4+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+int(bonusBoundaryWhite)*2+
|
||||||
util.Max(bonusCamel123, int(bonusBoundaryWhite)))
|
max(bonusCamel123, int(bonusBoundaryWhite)))
|
||||||
|
|
||||||
// Consecutive bonus updated
|
// Consecutive bonus updated
|
||||||
assertMatch(t, fn, true, forward, "foo-bar", "o-ba", 2, 6,
|
assertMatch(t, fn, true, forward, "foo-bar", "o-ba", 2, 6,
|
||||||
|
|||||||
+81
-17
@@ -22,20 +22,21 @@ type url struct {
|
|||||||
type ansiState struct {
|
type ansiState struct {
|
||||||
fg tui.Color
|
fg tui.Color
|
||||||
bg tui.Color
|
bg tui.Color
|
||||||
|
ul tui.Color
|
||||||
attr tui.Attr
|
attr tui.Attr
|
||||||
lbg tui.Color
|
lbg tui.Color
|
||||||
url *url
|
url *url
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ansiState) colored() bool {
|
func (s *ansiState) colored() bool {
|
||||||
return s.fg != -1 || s.bg != -1 || s.attr > 0 || s.lbg >= 0 || s.url != nil
|
return s.fg != -1 || s.bg != -1 || s.ul != -1 || s.attr > 0 || s.lbg >= 0 || s.url != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ansiState) equals(t *ansiState) bool {
|
func (s *ansiState) equals(t *ansiState) bool {
|
||||||
if t == nil {
|
if t == nil {
|
||||||
return !s.colored()
|
return !s.colored()
|
||||||
}
|
}
|
||||||
return s.fg == t.fg && s.bg == t.bg && s.attr == t.attr && s.lbg == t.lbg && s.url == t.url
|
return s.fg == t.fg && s.bg == t.bg && s.ul == t.ul && s.attr == t.attr && s.lbg == t.lbg && s.url == t.url
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ansiState) ToString() string {
|
func (s *ansiState) ToString() string {
|
||||||
@@ -54,7 +55,18 @@ func (s *ansiState) ToString() string {
|
|||||||
ret += "3;"
|
ret += "3;"
|
||||||
}
|
}
|
||||||
if s.attr&tui.Underline > 0 {
|
if s.attr&tui.Underline > 0 {
|
||||||
ret += "4;"
|
switch s.attr.UnderlineStyle() {
|
||||||
|
case tui.UlStyleDouble:
|
||||||
|
ret += "4:2;"
|
||||||
|
case tui.UlStyleCurly:
|
||||||
|
ret += "4:3;"
|
||||||
|
case tui.UlStyleDotted:
|
||||||
|
ret += "4:4;"
|
||||||
|
case tui.UlStyleDashed:
|
||||||
|
ret += "4:5;"
|
||||||
|
default:
|
||||||
|
ret += "4;"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if s.attr&tui.Blink > 0 {
|
if s.attr&tui.Blink > 0 {
|
||||||
ret += "5;"
|
ret += "5;"
|
||||||
@@ -66,6 +78,9 @@ func (s *ansiState) ToString() string {
|
|||||||
ret += "9;"
|
ret += "9;"
|
||||||
}
|
}
|
||||||
ret += toAnsiString(s.fg, 30) + toAnsiString(s.bg, 40)
|
ret += toAnsiString(s.fg, 30) + toAnsiString(s.bg, 40)
|
||||||
|
if s.ul != -1 {
|
||||||
|
ret += toAnsiStringUl(s.ul)
|
||||||
|
}
|
||||||
|
|
||||||
ret = "\x1b[" + strings.TrimSuffix(ret, ";") + "m"
|
ret = "\x1b[" + strings.TrimSuffix(ret, ";") + "m"
|
||||||
if s.url != nil {
|
if s.url != nil {
|
||||||
@@ -74,6 +89,20 @@ func (s *ansiState) ToString() string {
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toAnsiStringUl(color tui.Color) string {
|
||||||
|
col := int(color)
|
||||||
|
if col < 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if col >= (1 << 24) {
|
||||||
|
r := strconv.Itoa((col >> 16) & 0xff)
|
||||||
|
g := strconv.Itoa((col >> 8) & 0xff)
|
||||||
|
b := strconv.Itoa(col & 0xff)
|
||||||
|
return "58;2;" + r + ";" + g + ";" + b + ";"
|
||||||
|
}
|
||||||
|
return "58;5;" + strconv.Itoa(col) + ";"
|
||||||
|
}
|
||||||
|
|
||||||
func toAnsiString(color tui.Color, offset int) string {
|
func toAnsiString(color tui.Color, offset int) string {
|
||||||
col := int(color)
|
col := int(color)
|
||||||
ret := ""
|
ret := ""
|
||||||
@@ -338,15 +367,19 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
|
|||||||
return trimmed, nil, state
|
return trimmed, nil, state
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseAnsiCode(s string) (int, string) {
|
func parseAnsiCode(s string) (int, byte, string) {
|
||||||
var remaining string
|
var remaining string
|
||||||
var i int
|
var sep byte
|
||||||
// Faster than strings.IndexAny(";:")
|
// Find the first separator (either ; or :)
|
||||||
i = strings.IndexByte(s, ';')
|
i := -1
|
||||||
if i < 0 {
|
for j := 0; j < len(s); j++ {
|
||||||
i = strings.IndexByte(s, ':')
|
if s[j] == ';' || s[j] == ':' {
|
||||||
|
i = j
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if i >= 0 {
|
if i >= 0 {
|
||||||
|
sep = s[i]
|
||||||
remaining = s[i+1:]
|
remaining = s[i+1:]
|
||||||
s = s[:i]
|
s = s[:i]
|
||||||
}
|
}
|
||||||
@@ -358,14 +391,14 @@ func parseAnsiCode(s string) (int, string) {
|
|||||||
for _, ch := range stringBytes(s) {
|
for _, ch := range stringBytes(s) {
|
||||||
ch -= '0'
|
ch -= '0'
|
||||||
if ch > 9 {
|
if ch > 9 {
|
||||||
return -1, remaining
|
return -1, sep, remaining
|
||||||
}
|
}
|
||||||
code = code*10 + int(ch)
|
code = code*10 + int(ch)
|
||||||
}
|
}
|
||||||
return code, remaining
|
return code, sep, remaining
|
||||||
}
|
}
|
||||||
|
|
||||||
return -1, remaining
|
return -1, sep, remaining
|
||||||
}
|
}
|
||||||
|
|
||||||
func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
||||||
@@ -373,14 +406,14 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
|||||||
if prevState != nil {
|
if prevState != nil {
|
||||||
return *prevState
|
return *prevState
|
||||||
}
|
}
|
||||||
return ansiState{-1, -1, 0, -1, nil}
|
return ansiState{-1, -1, -1, 0, -1, nil}
|
||||||
}
|
}
|
||||||
|
|
||||||
var state ansiState
|
var state ansiState
|
||||||
if prevState == nil {
|
if prevState == nil {
|
||||||
state = ansiState{-1, -1, 0, -1, nil}
|
state = ansiState{-1, -1, -1, 0, -1, nil}
|
||||||
} else {
|
} else {
|
||||||
state = ansiState{prevState.fg, prevState.bg, prevState.attr, prevState.lbg, prevState.url}
|
state = ansiState{prevState.fg, prevState.bg, prevState.ul, prevState.attr, prevState.lbg, prevState.url}
|
||||||
}
|
}
|
||||||
if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
|
if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
|
||||||
if prevState != nil && (strings.HasSuffix(ansiCode, "0K") || strings.HasSuffix(ansiCode, "[K")) {
|
if prevState != nil && (strings.HasSuffix(ansiCode, "0K") || strings.HasSuffix(ansiCode, "[K")) {
|
||||||
@@ -405,6 +438,7 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
|||||||
reset := func() {
|
reset := func() {
|
||||||
state.fg = -1
|
state.fg = -1
|
||||||
state.bg = -1
|
state.bg = -1
|
||||||
|
state.ul = -1
|
||||||
state.attr = 0
|
state.attr = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,7 +454,8 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
|||||||
count := 0
|
count := 0
|
||||||
for len(ansiCode) != 0 {
|
for len(ansiCode) != 0 {
|
||||||
var num int
|
var num int
|
||||||
if num, ansiCode = parseAnsiCode(ansiCode); num != -1 {
|
var sep byte
|
||||||
|
if num, sep, ansiCode = parseAnsiCode(ansiCode); num != -1 {
|
||||||
count++
|
count++
|
||||||
switch state256 {
|
switch state256 {
|
||||||
case 0:
|
case 0:
|
||||||
@@ -431,10 +466,15 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
|||||||
case 48:
|
case 48:
|
||||||
ptr = &state.bg
|
ptr = &state.bg
|
||||||
state256++
|
state256++
|
||||||
|
case 58:
|
||||||
|
ptr = &state.ul
|
||||||
|
state256++
|
||||||
case 39:
|
case 39:
|
||||||
state.fg = -1
|
state.fg = -1
|
||||||
case 49:
|
case 49:
|
||||||
state.bg = -1
|
state.bg = -1
|
||||||
|
case 59:
|
||||||
|
state.ul = -1
|
||||||
case 1:
|
case 1:
|
||||||
state.attr = state.attr | tui.Bold
|
state.attr = state.attr | tui.Bold
|
||||||
case 2:
|
case 2:
|
||||||
@@ -442,7 +482,30 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
|||||||
case 3:
|
case 3:
|
||||||
state.attr = state.attr | tui.Italic
|
state.attr = state.attr | tui.Italic
|
||||||
case 4:
|
case 4:
|
||||||
state.attr = state.attr | tui.Underline
|
if sep == ':' {
|
||||||
|
// SGR 4:N — underline style sub-parameter
|
||||||
|
var subNum int
|
||||||
|
subNum, _, ansiCode = parseAnsiCode(ansiCode)
|
||||||
|
state.attr = state.attr &^ tui.UnderlineStyleMask
|
||||||
|
switch subNum {
|
||||||
|
case 0:
|
||||||
|
state.attr = state.attr &^ tui.Underline
|
||||||
|
case 1:
|
||||||
|
state.attr = state.attr | tui.Underline
|
||||||
|
case 2:
|
||||||
|
state.attr = state.attr | tui.Underline | tui.UlStyleDouble
|
||||||
|
case 3:
|
||||||
|
state.attr = state.attr | tui.Underline | tui.UlStyleCurly
|
||||||
|
case 4:
|
||||||
|
state.attr = state.attr | tui.Underline | tui.UlStyleDotted
|
||||||
|
case 5:
|
||||||
|
state.attr = state.attr | tui.Underline | tui.UlStyleDashed
|
||||||
|
default:
|
||||||
|
state.attr = state.attr | tui.Underline
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state.attr = state.attr | tui.Underline
|
||||||
|
}
|
||||||
case 5:
|
case 5:
|
||||||
state.attr = state.attr | tui.Blink
|
state.attr = state.attr | tui.Blink
|
||||||
case 7:
|
case 7:
|
||||||
@@ -456,6 +519,7 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
|||||||
state.attr = state.attr &^ tui.Italic
|
state.attr = state.attr &^ tui.Italic
|
||||||
case 24: // tput rmul
|
case 24: // tput rmul
|
||||||
state.attr = state.attr &^ tui.Underline
|
state.attr = state.attr &^ tui.Underline
|
||||||
|
state.attr = state.attr &^ tui.UnderlineStyleMask
|
||||||
case 25:
|
case 25:
|
||||||
state.attr = state.attr &^ tui.Blink
|
state.attr = state.attr &^ tui.Blink
|
||||||
case 27:
|
case 27:
|
||||||
|
|||||||
+127
-21
@@ -41,7 +41,7 @@ func testParserReference(t testing.TB, str string) {
|
|||||||
|
|
||||||
equal := len(got) == len(exp)
|
equal := len(got) == len(exp)
|
||||||
if equal {
|
if equal {
|
||||||
for i := 0; i < len(got); i++ {
|
for i := range got {
|
||||||
if got[i] != exp[i] {
|
if got[i] != exp[i] {
|
||||||
equal = false
|
equal = false
|
||||||
break
|
break
|
||||||
@@ -167,9 +167,9 @@ func TestNextAnsiEscapeSequence_Fuzz_Random(t *testing.T) {
|
|||||||
randomString := func(rr *rand.Rand) string {
|
randomString := func(rr *rand.Rand) string {
|
||||||
numChars := rand.Intn(50)
|
numChars := rand.Intn(50)
|
||||||
codePoints := make([]rune, numChars)
|
codePoints := make([]rune, numChars)
|
||||||
for i := 0; i < len(codePoints); i++ {
|
for i := range codePoints {
|
||||||
var r rune
|
var r rune
|
||||||
for n := 0; n < 1000; n++ {
|
for range 1000 {
|
||||||
r = rune(rr.Intn(utf8.MaxRune))
|
r = rune(rr.Intn(utf8.MaxRune))
|
||||||
// Allow 10% of runes to be invalid
|
// Allow 10% of runes to be invalid
|
||||||
if utf8.ValidRune(r) || rr.Float64() < 0.10 {
|
if utf8.ValidRune(r) || rr.Float64() < 0.10 {
|
||||||
@@ -182,7 +182,7 @@ func TestNextAnsiEscapeSequence_Fuzz_Random(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rr := rand.New(rand.NewSource(1))
|
rr := rand.New(rand.NewSource(1))
|
||||||
for i := 0; i < 100_000; i++ {
|
for range 100_000 {
|
||||||
testParserReference(t, randomString(rr))
|
testParserReference(t, randomString(rr))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,10 +369,10 @@ func TestAnsiCodeStringConversion(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert("\x1b[m", nil, "")
|
assert("\x1b[m", nil, "")
|
||||||
assert("\x1b[m", &ansiState{attr: tui.Blink, lbg: -1}, "")
|
assert("\x1b[m", &ansiState{attr: tui.Blink, ul: -1, lbg: -1}, "")
|
||||||
assert("\x1b[0m", &ansiState{fg: 4, bg: 4, lbg: -1}, "")
|
assert("\x1b[0m", &ansiState{fg: 4, bg: 4, ul: -1, lbg: -1}, "")
|
||||||
assert("\x1b[;m", &ansiState{fg: 4, bg: 4, lbg: -1}, "")
|
assert("\x1b[;m", &ansiState{fg: 4, bg: 4, ul: -1, lbg: -1}, "")
|
||||||
assert("\x1b[;;m", &ansiState{fg: 4, bg: 4, lbg: -1}, "")
|
assert("\x1b[;;m", &ansiState{fg: 4, bg: 4, ul: -1, lbg: -1}, "")
|
||||||
|
|
||||||
assert("\x1b[31m", nil, "\x1b[31;49m")
|
assert("\x1b[31m", nil, "\x1b[31;49m")
|
||||||
assert("\x1b[41m", nil, "\x1b[39;41m")
|
assert("\x1b[41m", nil, "\x1b[39;41m")
|
||||||
@@ -380,36 +380,142 @@ func TestAnsiCodeStringConversion(t *testing.T) {
|
|||||||
assert("\x1b[92m", nil, "\x1b[92;49m")
|
assert("\x1b[92m", nil, "\x1b[92;49m")
|
||||||
assert("\x1b[102m", nil, "\x1b[39;102m")
|
assert("\x1b[102m", nil, "\x1b[39;102m")
|
||||||
|
|
||||||
assert("\x1b[31m", &ansiState{fg: 4, bg: 4, lbg: -1}, "\x1b[31;44m")
|
assert("\x1b[31m", &ansiState{fg: 4, bg: 4, ul: -1, lbg: -1}, "\x1b[31;44m")
|
||||||
assert("\x1b[1;2;31m", &ansiState{fg: 2, bg: -1, attr: tui.Reverse, lbg: -1}, "\x1b[1;2;7;31;49m")
|
assert("\x1b[1;2;31m", &ansiState{fg: 2, bg: -1, ul: -1, attr: tui.Reverse, lbg: -1}, "\x1b[1;2;7;31;49m")
|
||||||
assert("\x1b[38;5;100;48;5;200m", nil, "\x1b[38;5;100;48;5;200m")
|
assert("\x1b[38;5;100;48;5;200m", nil, "\x1b[38;5;100;48;5;200m")
|
||||||
assert("\x1b[38:5:100:48:5:200m", nil, "\x1b[38;5;100;48;5;200m")
|
assert("\x1b[38:5:100:48:5:200m", nil, "\x1b[38;5;100;48;5;200m")
|
||||||
assert("\x1b[48;5;100;38;5;200m", nil, "\x1b[38;5;200;48;5;100m")
|
assert("\x1b[48;5;100;38;5;200m", nil, "\x1b[38;5;200;48;5;100m")
|
||||||
assert("\x1b[48;5;100;38;2;10;20;30;1m", nil, "\x1b[1;38;2;10;20;30;48;5;100m")
|
assert("\x1b[48;5;100;38;2;10;20;30;1m", nil, "\x1b[1;38;2;10;20;30;48;5;100m")
|
||||||
assert("\x1b[48;5;100;38;2;10;20;30;7m",
|
assert("\x1b[48;5;100;38;2;10;20;30;7m",
|
||||||
&ansiState{attr: tui.Dim | tui.Italic, fg: 1, bg: 1},
|
&ansiState{attr: tui.Dim | tui.Italic, fg: 1, bg: 1, ul: -1},
|
||||||
"\x1b[2;3;7;38;2;10;20;30;48;5;100m")
|
"\x1b[2;3;7;38;2;10;20;30;48;5;100m")
|
||||||
|
|
||||||
|
// Underline styles
|
||||||
|
assert("\x1b[4:3m", nil, "\x1b[4:3;39;49m")
|
||||||
|
assert("\x1b[4:2m", nil, "\x1b[4:2;39;49m")
|
||||||
|
assert("\x1b[4:4m", nil, "\x1b[4:4;39;49m")
|
||||||
|
assert("\x1b[4:5m", nil, "\x1b[4:5;39;49m")
|
||||||
|
assert("\x1b[4:1m", nil, "\x1b[4;39;49m")
|
||||||
|
|
||||||
|
// Underline color (256-color)
|
||||||
|
assert("\x1b[4;58;5;100m", nil, "\x1b[4;39;49;58;5;100m")
|
||||||
|
// Underline color (24-bit)
|
||||||
|
assert("\x1b[4;58;2;255;0;128m", nil, "\x1b[4;39;49;58;2;255;0;128m")
|
||||||
|
// Curly underline + underline color
|
||||||
|
assert("\x1b[4:3;58;2;255;0;0m", nil, "\x1b[4:3;39;49;58;2;255;0;0m")
|
||||||
|
// SGR 59 resets underline color
|
||||||
|
assert("\x1b[59m", &ansiState{fg: 1, bg: -1, ul: 100, lbg: -1}, "\x1b[31;49m")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseAnsiCode(t *testing.T) {
|
func TestParseAnsiCode(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
In, Exp string
|
In string
|
||||||
N int
|
Exp string
|
||||||
|
N int
|
||||||
|
Sep byte
|
||||||
}{
|
}{
|
||||||
{"123", "", 123},
|
{"123", "", 123, 0},
|
||||||
{"1a", "", -1},
|
{"1a", "", -1, 0},
|
||||||
{"1a;12", "12", -1},
|
{"1a;12", "12", -1, ';'},
|
||||||
{"12;a", "a", 12},
|
{"12;a", "a", 12, ';'},
|
||||||
{"-2", "", -1},
|
{"-2", "", -1, 0},
|
||||||
|
// Colon sub-parameters: earliest separator wins (@shtse8)
|
||||||
|
{"4:3", "3", 4, ':'},
|
||||||
|
{"4:3;31", "3;31", 4, ':'},
|
||||||
|
{"38:2:255:0:0", "2:255:0:0", 38, ':'},
|
||||||
|
{"58:5:200", "5:200", 58, ':'},
|
||||||
|
// Semicolon before colon
|
||||||
|
{"4;38:2:0:0:0", "38:2:0:0:0", 4, ';'},
|
||||||
}
|
}
|
||||||
for _, x := range tests {
|
for _, x := range tests {
|
||||||
n, s := parseAnsiCode(x.In)
|
n, sep, s := parseAnsiCode(x.In)
|
||||||
if n != x.N || s != x.Exp {
|
if n != x.N || s != x.Exp || sep != x.Sep {
|
||||||
t.Fatalf("%q: got: (%d %q) want: (%d %q)", x.In, n, s, x.N, x.Exp)
|
t.Fatalf("%q: got: (%d %q %q) want: (%d %q %q)", x.In, n, s, string(sep), x.N, x.Exp, string(x.Sep))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test cases adapted from @shtse8 (PR #4678)
|
||||||
|
func TestInterpretCodeUnderlineStyles(t *testing.T) {
|
||||||
|
// 4:0 = no underline
|
||||||
|
state := interpretCode("\x1b[4:0m", nil)
|
||||||
|
if state.attr&tui.Underline != 0 {
|
||||||
|
t.Error("4:0 should not set underline")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4:1 = single underline
|
||||||
|
state = interpretCode("\x1b[4:1m", nil)
|
||||||
|
if state.attr&tui.Underline == 0 {
|
||||||
|
t.Error("4:1 should set underline")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4:3 = curly underline
|
||||||
|
state = interpretCode("\x1b[4:3m", nil)
|
||||||
|
if state.attr&tui.Underline == 0 {
|
||||||
|
t.Error("4:3 should set underline")
|
||||||
|
}
|
||||||
|
if state.attr.UnderlineStyle() != tui.UlStyleCurly {
|
||||||
|
t.Error("4:3 should set curly underline style")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4:3 should NOT set italic (3 is a sub-param, not SGR 3)
|
||||||
|
if state.attr&tui.Italic != 0 {
|
||||||
|
t.Error("4:3 should not set italic")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4:2;31 = double underline + red fg
|
||||||
|
state = interpretCode("\x1b[4:2;31m", nil)
|
||||||
|
if state.attr&tui.Underline == 0 {
|
||||||
|
t.Error("4:2;31 should set underline")
|
||||||
|
}
|
||||||
|
if state.fg != 1 {
|
||||||
|
t.Errorf("4:2;31 should set fg to red (1), got %d", state.fg)
|
||||||
|
}
|
||||||
|
if state.attr&tui.Dim != 0 {
|
||||||
|
t.Error("4:2;31 should not set dim")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plain 4 still works
|
||||||
|
state = interpretCode("\x1b[4m", nil)
|
||||||
|
if state.attr&tui.Underline == 0 {
|
||||||
|
t.Error("4 should set underline")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4;2 (semicolon) = underline + dim
|
||||||
|
state = interpretCode("\x1b[4;2m", nil)
|
||||||
|
if state.attr&tui.Underline == 0 {
|
||||||
|
t.Error("4;2 should set underline")
|
||||||
|
}
|
||||||
|
if state.attr&tui.Dim == 0 {
|
||||||
|
t.Error("4;2 should set dim")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test cases adapted from @shtse8 (PR #4678)
|
||||||
|
func TestInterpretCodeUnderlineColor(t *testing.T) {
|
||||||
|
// 58:2:R:G:B should not affect fg or bg
|
||||||
|
state := interpretCode("\x1b[58:2:255:0:0m", nil)
|
||||||
|
if state.fg != -1 || state.bg != -1 {
|
||||||
|
t.Errorf("58:2:R:G:B should not affect fg/bg, got fg=%d bg=%d", state.fg, state.bg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 58:5:200 should not affect fg or bg
|
||||||
|
state = interpretCode("\x1b[58:5:200m", nil)
|
||||||
|
if state.fg != -1 || state.bg != -1 {
|
||||||
|
t.Errorf("58:5:N should not affect fg/bg, got fg=%d bg=%d", state.fg, state.bg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 58:2:R:G:B combined with 38:2:R:G:B should only set fg
|
||||||
|
state = interpretCode("\x1b[58:2:255:0:0;38:2:0:255:0m", nil)
|
||||||
|
expectedFg := tui.Color(1<<24 | 0<<16 | 255<<8 | 0)
|
||||||
|
if state.fg != expectedFg {
|
||||||
|
t.Errorf("expected fg=%d, got %d", expectedFg, state.fg)
|
||||||
|
}
|
||||||
|
if state.bg != -1 {
|
||||||
|
t.Errorf("bg should be -1, got %d", state.bg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// kernel/bpf/preload/iterators/README
|
// kernel/bpf/preload/iterators/README
|
||||||
const ansiBenchmarkString = "\x1b[38;5;81m\x1b[01;31m\x1b[Kkernel/\x1b[0m\x1b[38:5:81mbpf/" +
|
const ansiBenchmarkString = "\x1b[38;5;81m\x1b[01;31m\x1b[Kkernel/\x1b[0m\x1b[38:5:81mbpf/" +
|
||||||
"\x1b[0m\x1b[38:5:81mpreload/\x1b[0m\x1b[38;5;81miterators/" +
|
"\x1b[0m\x1b[38:5:81mpreload/\x1b[0m\x1b[38;5;81miterators/" +
|
||||||
|
|||||||
@@ -52,6 +52,20 @@ func (cl *ChunkList) lastChunk() *Chunk {
|
|||||||
return cl.chunks[len(cl.chunks)-1]
|
return cl.chunks[len(cl.chunks)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetItems returns the first n items from the given chunks
|
||||||
|
func GetItems(chunks []*Chunk, n int) []Item {
|
||||||
|
items := make([]Item, 0, n)
|
||||||
|
for _, chunk := range chunks {
|
||||||
|
for i := 0; i < chunk.count && len(items) < n; i++ {
|
||||||
|
items = append(items, chunk.items[i])
|
||||||
|
}
|
||||||
|
if len(items) >= n {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
// CountItems returns the total number of Items
|
// CountItems returns the total number of Items
|
||||||
func CountItems(cs []*Chunk) int {
|
func CountItems(cs []*Chunk) int {
|
||||||
if len(cs) == 0 {
|
if len(cs) == 0 {
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func TestChunkList(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add more data
|
// Add more data
|
||||||
for i := 0; i < chunkSize*2; i++ {
|
for i := range chunkSize * 2 {
|
||||||
cl.Push(fmt.Appendf(nil, "item %d", i))
|
cl.Push(fmt.Appendf(nil, "item %d", i))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ func TestChunkListTail(t *testing.T) {
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
total := chunkSize*2 + chunkSize/2
|
total := chunkSize*2 + chunkSize/2
|
||||||
for i := 0; i < total; i++ {
|
for i := range total {
|
||||||
cl.Push(fmt.Appendf(nil, "item %d", i))
|
cl.Push(fmt.Appendf(nil, "item %d", i))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-2
@@ -39,7 +39,7 @@ const (
|
|||||||
progressMinDuration = 200 * time.Millisecond
|
progressMinDuration = 200 * time.Millisecond
|
||||||
|
|
||||||
// Capacity of each chunk
|
// Capacity of each chunk
|
||||||
chunkSize int = 100
|
chunkSize int = 1000
|
||||||
|
|
||||||
// Pre-allocated memory slices to minimize GC
|
// Pre-allocated memory slices to minimize GC
|
||||||
slab16Size int = 100 * 1024 // 200KB * 32 = 12.8MB
|
slab16Size int = 100 * 1024 // 200KB * 32 = 12.8MB
|
||||||
@@ -65,7 +65,6 @@ const (
|
|||||||
EvtSearchNew
|
EvtSearchNew
|
||||||
EvtSearchProgress
|
EvtSearchProgress
|
||||||
EvtSearchFin
|
EvtSearchFin
|
||||||
EvtHeader
|
|
||||||
EvtReady
|
EvtReady
|
||||||
EvtQuit
|
EvtQuit
|
||||||
)
|
)
|
||||||
|
|||||||
+105
-51
@@ -2,6 +2,8 @@
|
|||||||
package fzf
|
package fzf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -16,7 +18,6 @@ Reader -> EvtReadNew -> Matcher (restart)
|
|||||||
Terminal -> EvtSearchNew:bool -> Matcher (restart)
|
Terminal -> EvtSearchNew:bool -> Matcher (restart)
|
||||||
Matcher -> EvtSearchProgress -> Terminal (update info)
|
Matcher -> EvtSearchProgress -> Terminal (update info)
|
||||||
Matcher -> EvtSearchFin -> Terminal (update list)
|
Matcher -> EvtSearchFin -> Terminal (update list)
|
||||||
Matcher -> EvtHeader -> Terminal (update header)
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
type revision struct {
|
type revision struct {
|
||||||
@@ -37,6 +38,18 @@ func (r revision) compatible(other revision) bool {
|
|||||||
return r.major == other.major
|
return r.major == other.major
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildItemTransformer(opts *Options) func(*Item) string {
|
||||||
|
if opts.AcceptNth != nil {
|
||||||
|
fn := opts.AcceptNth(opts.Delimiter)
|
||||||
|
return func(item *Item) string {
|
||||||
|
return item.acceptNth(opts.Ansi, opts.Delimiter, fn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return func(item *Item) string {
|
||||||
|
return item.AsString(opts.Ansi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Run starts fzf
|
// Run starts fzf
|
||||||
func Run(opts *Options) (int, error) {
|
func Run(opts *Options) (int, error) {
|
||||||
if opts.Filter == nil {
|
if opts.Filter == nil {
|
||||||
@@ -100,14 +113,8 @@ func Run(opts *Options) (int, error) {
|
|||||||
cache := NewChunkCache()
|
cache := NewChunkCache()
|
||||||
var chunkList *ChunkList
|
var chunkList *ChunkList
|
||||||
var itemIndex int32
|
var itemIndex int32
|
||||||
header := make([]string, 0, opts.HeaderLines)
|
|
||||||
if opts.WithNth == nil {
|
if opts.WithNth == nil {
|
||||||
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
||||||
if len(header) < opts.HeaderLines {
|
|
||||||
header = append(header, byteString(data))
|
|
||||||
eventBox.Set(EvtHeader, header)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
item.text, item.colors = ansiProcessor(data)
|
item.text, item.colors = ansiProcessor(data)
|
||||||
item.text.Index = itemIndex
|
item.text.Index = itemIndex
|
||||||
itemIndex++
|
itemIndex++
|
||||||
@@ -134,11 +141,6 @@ func Run(opts *Options) (int, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
transformed := nthTransformer(tokens, itemIndex)
|
transformed := nthTransformer(tokens, itemIndex)
|
||||||
if len(header) < opts.HeaderLines {
|
|
||||||
header = append(header, transformed)
|
|
||||||
eventBox.Set(EvtHeader, header)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
item.text, item.colors = ansiProcessor(stringBytes(transformed))
|
item.text, item.colors = ansiProcessor(stringBytes(transformed))
|
||||||
|
|
||||||
// We should not trim trailing whitespaces with background colors
|
// We should not trim trailing whitespaces with background colors
|
||||||
@@ -146,7 +148,7 @@ func Run(opts *Options) (int, error) {
|
|||||||
if item.colors != nil {
|
if item.colors != nil {
|
||||||
for _, ansi := range *item.colors {
|
for _, ansi := range *item.colors {
|
||||||
if ansi.color.bg >= 0 {
|
if ansi.color.bg >= 0 {
|
||||||
maxColorOffset = util.Max32(maxColorOffset, ansi.offset[1])
|
maxColorOffset = max(maxColorOffset, ansi.offset[1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,7 +182,7 @@ func Run(opts *Options) (int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reader
|
// Reader
|
||||||
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
|
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync && opts.Bench == 0
|
||||||
var reader *Reader
|
var reader *Reader
|
||||||
if !streamingFilter {
|
if !streamingFilter {
|
||||||
reader = NewReader(func(data []byte) bool {
|
reader = NewReader(func(data []byte) bool {
|
||||||
@@ -223,18 +225,17 @@ func Run(opts *Options) (int, error) {
|
|||||||
denylist = make(map[int32]struct{})
|
denylist = make(map[int32]struct{})
|
||||||
denyMutex.Unlock()
|
denyMutex.Unlock()
|
||||||
}
|
}
|
||||||
|
headerLines := int32(opts.HeaderLines)
|
||||||
|
headerUpdated := false
|
||||||
patternBuilder := func(runes []rune) *Pattern {
|
patternBuilder := func(runes []rune) *Pattern {
|
||||||
denyMutex.Lock()
|
denyMutex.Lock()
|
||||||
denylistCopy := make(map[int32]struct{})
|
denylistCopy := maps.Clone(denylist)
|
||||||
for k, v := range denylist {
|
|
||||||
denylistCopy[k] = v
|
|
||||||
}
|
|
||||||
denyMutex.Unlock()
|
denyMutex.Unlock()
|
||||||
return BuildPattern(cache, patternCache,
|
return BuildPattern(cache, patternCache,
|
||||||
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
|
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
|
||||||
opts.Filter == nil, nth, opts.Delimiter, inputRevision, runes, denylistCopy)
|
opts.Filter == nil, nth, opts.Delimiter, inputRevision, runes, denylistCopy, headerLines)
|
||||||
}
|
}
|
||||||
matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision)
|
matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision, opts.Threads)
|
||||||
|
|
||||||
// Filtering mode
|
// Filtering mode
|
||||||
if opts.Filter != nil {
|
if opts.Filter != nil {
|
||||||
@@ -245,6 +246,8 @@ func Run(opts *Options) (int, error) {
|
|||||||
pattern := patternBuilder([]rune(*opts.Filter))
|
pattern := patternBuilder([]rune(*opts.Filter))
|
||||||
matcher.sort = pattern.sortable
|
matcher.sort = pattern.sortable
|
||||||
|
|
||||||
|
transformer := buildItemTransformer(opts)
|
||||||
|
|
||||||
found := false
|
found := false
|
||||||
if streamingFilter {
|
if streamingFilter {
|
||||||
slab := util.MakeSlab(slab16Size, slab32Size)
|
slab := util.MakeSlab(slab16Size, slab32Size)
|
||||||
@@ -253,9 +256,12 @@ func Run(opts *Options) (int, error) {
|
|||||||
func(runes []byte) bool {
|
func(runes []byte) bool {
|
||||||
item := Item{}
|
item := Item{}
|
||||||
if chunkList.trans(&item, runes) {
|
if chunkList.trans(&item, runes) {
|
||||||
|
if item.Index() < headerLines {
|
||||||
|
return false
|
||||||
|
}
|
||||||
mutex.Lock()
|
mutex.Lock()
|
||||||
if result, _, _ := pattern.MatchItem(&item, false, slab); result != nil {
|
if result, _, _ := pattern.MatchItem(&item, false, slab); result.item != nil {
|
||||||
opts.Printer(item.text.ToString())
|
opts.Printer(transformer(&item))
|
||||||
found = true
|
found = true
|
||||||
}
|
}
|
||||||
mutex.Unlock()
|
mutex.Unlock()
|
||||||
@@ -269,11 +275,51 @@ func Run(opts *Options) (int, error) {
|
|||||||
|
|
||||||
// NOTE: Streaming filter is inherently not compatible with --tail
|
// NOTE: Streaming filter is inherently not compatible with --tail
|
||||||
snapshot, _, _ := chunkList.Snapshot(opts.Tail)
|
snapshot, _, _ := chunkList.Snapshot(opts.Tail)
|
||||||
merger, _ := matcher.scan(MatchRequest{
|
|
||||||
|
if opts.Bench > 0 {
|
||||||
|
// Benchmark mode: repeat scan for the given duration
|
||||||
|
totalItems := CountItems(snapshot)
|
||||||
|
var matchCount int
|
||||||
|
var times []time.Duration
|
||||||
|
deadline := time.Now().Add(opts.Bench)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
cache.Clear()
|
||||||
|
start := time.Now()
|
||||||
|
result := matcher.scan(MatchRequest{
|
||||||
|
chunks: snapshot,
|
||||||
|
pattern: pattern})
|
||||||
|
times = append(times, time.Since(start))
|
||||||
|
matchCount = result.merger.Length()
|
||||||
|
}
|
||||||
|
// Print stats
|
||||||
|
var total time.Duration
|
||||||
|
minD, maxD := times[0], times[0]
|
||||||
|
for _, d := range times {
|
||||||
|
total += d
|
||||||
|
if d < minD {
|
||||||
|
minD = d
|
||||||
|
}
|
||||||
|
if d > maxD {
|
||||||
|
maxD = d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
avg := total / time.Duration(len(times))
|
||||||
|
selectivity := float64(matchCount) / float64(totalItems) * 100
|
||||||
|
fmt.Printf(" %d iterations avg: %.2fms min: %.2fms max: %.2fms total: %.2fs items: %d matches: %d (%.2f%%)\n",
|
||||||
|
len(times),
|
||||||
|
float64(avg.Microseconds())/1000,
|
||||||
|
float64(minD.Microseconds())/1000,
|
||||||
|
float64(maxD.Microseconds())/1000,
|
||||||
|
total.Seconds(),
|
||||||
|
totalItems, matchCount, selectivity)
|
||||||
|
return ExitOk, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := matcher.scan(MatchRequest{
|
||||||
chunks: snapshot,
|
chunks: snapshot,
|
||||||
pattern: pattern})
|
pattern: pattern})
|
||||||
for i := 0; i < merger.Length(); i++ {
|
for i := 0; i < result.merger.Length(); i++ {
|
||||||
opts.Printer(merger.Get(i).item.AsString(opts.Ansi))
|
opts.Printer(transformer(result.merger.Get(i).item))
|
||||||
found = true
|
found = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -318,10 +364,11 @@ func Run(opts *Options) (int, error) {
|
|||||||
query := []rune{}
|
query := []rune{}
|
||||||
determine := func(final bool) {
|
determine := func(final bool) {
|
||||||
if heightUnknown {
|
if heightUnknown {
|
||||||
if total >= maxFit || final {
|
items := max(0, total-int(headerLines))
|
||||||
|
if items >= maxFit || final {
|
||||||
deferred = false
|
deferred = false
|
||||||
heightUnknown = false
|
heightUnknown = false
|
||||||
terminal.startChan <- fitpad{util.Min(total, maxFit), padHeight}
|
terminal.startChan <- fitpad{min(items, maxFit), padHeight}
|
||||||
}
|
}
|
||||||
} else if deferred {
|
} else if deferred {
|
||||||
deferred = false
|
deferred = false
|
||||||
@@ -337,11 +384,11 @@ func Run(opts *Options) (int, error) {
|
|||||||
clearDenylist()
|
clearDenylist()
|
||||||
}
|
}
|
||||||
reading = true
|
reading = true
|
||||||
|
headerUpdated = false
|
||||||
startTick = ticks
|
startTick = ticks
|
||||||
chunkList.Clear()
|
chunkList.Clear()
|
||||||
itemIndex = 0
|
itemIndex = 0
|
||||||
inputRevision.bumpMajor()
|
inputRevision.bumpMajor()
|
||||||
header = make([]string, 0, opts.HeaderLines)
|
|
||||||
readyChan := make(chan bool)
|
readyChan := make(chan bool)
|
||||||
go reader.restart(command, environ, readyChan)
|
go reader.restart(command, environ, readyChan)
|
||||||
<-readyChan
|
<-readyChan
|
||||||
@@ -399,7 +446,11 @@ func Run(opts *Options) (int, error) {
|
|||||||
snapshotRevision = inputRevision
|
snapshotRevision = inputRevision
|
||||||
}
|
}
|
||||||
total = count
|
total = count
|
||||||
terminal.UpdateCount(total, !reading, value.(*string))
|
terminal.UpdateCount(max(0, total-int(headerLines)), !reading, value.(*string))
|
||||||
|
if headerLines > 0 && !headerUpdated {
|
||||||
|
terminal.UpdateHeader(GetItems(snapshot, int(headerLines)))
|
||||||
|
headerUpdated = int32(total) >= headerLines
|
||||||
|
}
|
||||||
if heightUnknown && !deferred {
|
if heightUnknown && !deferred {
|
||||||
determine(!reading)
|
determine(!reading)
|
||||||
}
|
}
|
||||||
@@ -409,6 +460,7 @@ func Run(opts *Options) (int, error) {
|
|||||||
var command *commandSpec
|
var command *commandSpec
|
||||||
var environ []string
|
var environ []string
|
||||||
var changed bool
|
var changed bool
|
||||||
|
headerLinesChanged := false
|
||||||
switch val := value.(type) {
|
switch val := value.(type) {
|
||||||
case searchRequest:
|
case searchRequest:
|
||||||
sort = val.sort
|
sort = val.sort
|
||||||
@@ -429,6 +481,12 @@ func Run(opts *Options) (int, error) {
|
|||||||
nth = *val.nth
|
nth = *val.nth
|
||||||
bump = true
|
bump = true
|
||||||
}
|
}
|
||||||
|
if val.headerLines != nil {
|
||||||
|
headerLines = int32(*val.headerLines)
|
||||||
|
headerUpdated = false
|
||||||
|
headerLinesChanged = true
|
||||||
|
bump = true
|
||||||
|
}
|
||||||
if bump {
|
if bump {
|
||||||
patternCache = make(map[string]*Pattern)
|
patternCache = make(map[string]*Pattern)
|
||||||
cache.Clear()
|
cache.Clear()
|
||||||
@@ -465,6 +523,14 @@ func Run(opts *Options) (int, error) {
|
|||||||
snapshotRevision = inputRevision
|
snapshotRevision = inputRevision
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if headerLinesChanged {
|
||||||
|
terminal.UpdateCount(max(0, total-int(headerLines)), !reading, nil)
|
||||||
|
if headerLines > 0 {
|
||||||
|
terminal.UpdateHeader(GetItems(snapshot, int(headerLines)))
|
||||||
|
} else {
|
||||||
|
terminal.UpdateHeader(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
matcher.Reset(snapshot, input(), true, !reading, sort, snapshotRevision)
|
matcher.Reset(snapshot, input(), true, !reading, sort, snapshotRevision)
|
||||||
delay = false
|
delay = false
|
||||||
|
|
||||||
@@ -474,19 +540,15 @@ func Run(opts *Options) (int, error) {
|
|||||||
terminal.UpdateProgress(val)
|
terminal.UpdateProgress(val)
|
||||||
}
|
}
|
||||||
|
|
||||||
case EvtHeader:
|
|
||||||
headerPadded := make([]string, opts.HeaderLines)
|
|
||||||
copy(headerPadded, value.([]string))
|
|
||||||
terminal.UpdateHeader(headerPadded)
|
|
||||||
|
|
||||||
case EvtSearchFin:
|
case EvtSearchFin:
|
||||||
switch val := value.(type) {
|
switch val := value.(type) {
|
||||||
case *Merger:
|
case MatchResult:
|
||||||
|
merger := val.merger
|
||||||
if deferred {
|
if deferred {
|
||||||
count := val.Length()
|
count := merger.Length()
|
||||||
if opts.Select1 && count > 1 || opts.Exit0 && !opts.Select1 && count > 0 {
|
if opts.Select1 && count > 1 || opts.Exit0 && !opts.Select1 && count > 0 {
|
||||||
determine(val.final)
|
determine(merger.final)
|
||||||
} else if val.final {
|
} else if merger.final {
|
||||||
if opts.Exit0 && count == 0 || opts.Select1 && count == 1 {
|
if opts.Exit0 && count == 0 || opts.Select1 && count == 1 {
|
||||||
if opts.PrintQuery {
|
if opts.PrintQuery {
|
||||||
opts.Printer(opts.Query)
|
opts.Printer(opts.Query)
|
||||||
@@ -494,17 +556,9 @@ func Run(opts *Options) (int, error) {
|
|||||||
if len(opts.Expect) > 0 {
|
if len(opts.Expect) > 0 {
|
||||||
opts.Printer("")
|
opts.Printer("")
|
||||||
}
|
}
|
||||||
transformer := func(item *Item) string {
|
transformer := buildItemTransformer(opts)
|
||||||
return item.AsString(opts.Ansi)
|
for i := range count {
|
||||||
}
|
opts.Printer(transformer(merger.Get(i).item))
|
||||||
if opts.AcceptNth != nil {
|
|
||||||
fn := opts.AcceptNth(opts.Delimiter)
|
|
||||||
transformer = func(item *Item) string {
|
|
||||||
return item.acceptNth(opts.Ansi, opts.Delimiter, fn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i := 0; i < count; i++ {
|
|
||||||
opts.Printer(transformer(val.Get(i).item))
|
|
||||||
}
|
}
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
exitCode = ExitNoMatch
|
exitCode = ExitNoMatch
|
||||||
@@ -512,7 +566,7 @@ func Run(opts *Options) (int, error) {
|
|||||||
stop = true
|
stop = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
determine(val.final)
|
determine(merger.final)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
terminal.UpdateList(val)
|
terminal.UpdateList(val)
|
||||||
@@ -525,7 +579,7 @@ func Run(opts *Options) (int, error) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
if delay && reading {
|
if delay && reading {
|
||||||
dur := util.DurWithin(
|
dur := util.Constrain(
|
||||||
time.Duration(ticks-startTick)*coordinatorDelayStep,
|
time.Duration(ticks-startTick)*coordinatorDelayStep,
|
||||||
0, coordinatorDelayMax)
|
0, coordinatorDelayMax)
|
||||||
time.Sleep(dur)
|
time.Sleep(dur)
|
||||||
|
|||||||
+1
-1
@@ -38,7 +38,7 @@ func TestHistory(t *testing.T) {
|
|||||||
if len(h.lines) != maxHistory+1 {
|
if len(h.lines) != maxHistory+1 {
|
||||||
t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines))
|
t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines))
|
||||||
}
|
}
|
||||||
for i := 0; i < maxHistory; i++ {
|
for i := range maxHistory {
|
||||||
if h.lines[i] != "foobar" {
|
if h.lines[i] != "foobar" {
|
||||||
t.Error("Expected: foobar, actual: " + h.lines[i])
|
t.Error("Expected: foobar, actual: " + h.lines[i])
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-29
@@ -3,7 +3,6 @@ package fzf
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sort"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -19,6 +18,20 @@ type MatchRequest struct {
|
|||||||
revision revision
|
revision revision
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MatchResult struct {
|
||||||
|
merger *Merger
|
||||||
|
passMerger *Merger
|
||||||
|
cancelled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mr MatchResult) cacheable() bool {
|
||||||
|
return mr.merger != nil && mr.merger.cacheable()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mr MatchResult) final() bool {
|
||||||
|
return mr.merger != nil && mr.merger.final
|
||||||
|
}
|
||||||
|
|
||||||
// Matcher is responsible for performing search
|
// Matcher is responsible for performing search
|
||||||
type Matcher struct {
|
type Matcher struct {
|
||||||
cache *ChunkCache
|
cache *ChunkCache
|
||||||
@@ -29,7 +42,8 @@ type Matcher struct {
|
|||||||
reqBox *util.EventBox
|
reqBox *util.EventBox
|
||||||
partitions int
|
partitions int
|
||||||
slab []*util.Slab
|
slab []*util.Slab
|
||||||
mergerCache map[string]*Merger
|
sortBuf [][]Result
|
||||||
|
mergerCache map[string]MatchResult
|
||||||
revision revision
|
revision revision
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,8 +54,11 @@ const (
|
|||||||
|
|
||||||
// NewMatcher returns a new Matcher
|
// NewMatcher returns a new Matcher
|
||||||
func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern,
|
func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern,
|
||||||
sort bool, tac bool, eventBox *util.EventBox, revision revision) *Matcher {
|
sort bool, tac bool, eventBox *util.EventBox, revision revision, threads int) *Matcher {
|
||||||
partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
|
partitions := min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
|
||||||
|
if threads > 0 {
|
||||||
|
partitions = threads
|
||||||
|
}
|
||||||
return &Matcher{
|
return &Matcher{
|
||||||
cache: cache,
|
cache: cache,
|
||||||
patternBuilder: patternBuilder,
|
patternBuilder: patternBuilder,
|
||||||
@@ -51,7 +68,8 @@ func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern,
|
|||||||
reqBox: util.NewEventBox(),
|
reqBox: util.NewEventBox(),
|
||||||
partitions: partitions,
|
partitions: partitions,
|
||||||
slab: make([]*util.Slab, partitions),
|
slab: make([]*util.Slab, partitions),
|
||||||
mergerCache: make(map[string]*Merger),
|
sortBuf: make([][]Result, partitions),
|
||||||
|
mergerCache: make(map[string]MatchResult),
|
||||||
revision: revision}
|
revision: revision}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,43 +103,42 @@ func (m *Matcher) Loop() {
|
|||||||
cacheCleared := false
|
cacheCleared := false
|
||||||
if request.sort != m.sort || request.revision != m.revision {
|
if request.sort != m.sort || request.revision != m.revision {
|
||||||
m.sort = request.sort
|
m.sort = request.sort
|
||||||
m.revision = request.revision
|
m.mergerCache = make(map[string]MatchResult)
|
||||||
m.mergerCache = make(map[string]*Merger)
|
|
||||||
if !request.revision.compatible(m.revision) {
|
if !request.revision.compatible(m.revision) {
|
||||||
m.cache.Clear()
|
m.cache.Clear()
|
||||||
}
|
}
|
||||||
|
m.revision = request.revision
|
||||||
cacheCleared = true
|
cacheCleared = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restart search
|
// Restart search
|
||||||
patternString := request.pattern.AsString()
|
patternString := request.pattern.AsString()
|
||||||
var merger *Merger
|
var result MatchResult
|
||||||
cancelled := false
|
|
||||||
count := CountItems(request.chunks)
|
count := CountItems(request.chunks)
|
||||||
|
|
||||||
if !cacheCleared {
|
if !cacheCleared {
|
||||||
if count == prevCount {
|
if count == prevCount {
|
||||||
// Look up mergerCache
|
// Look up mergerCache
|
||||||
if cached, found := m.mergerCache[patternString]; found && cached.final == request.final {
|
if cached, found := m.mergerCache[patternString]; found && cached.final() == request.final {
|
||||||
merger = cached
|
result = cached
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Invalidate mergerCache
|
// Invalidate mergerCache
|
||||||
prevCount = count
|
prevCount = count
|
||||||
m.mergerCache = make(map[string]*Merger)
|
m.mergerCache = make(map[string]MatchResult)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if merger == nil {
|
if result.merger == nil {
|
||||||
merger, cancelled = m.scan(request)
|
result = m.scan(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cancelled {
|
if !result.cancelled {
|
||||||
if merger.cacheable() {
|
if result.cacheable() {
|
||||||
m.mergerCache[patternString] = merger
|
m.mergerCache[patternString] = result
|
||||||
}
|
}
|
||||||
merger.final = request.final
|
result.merger.final = request.final
|
||||||
m.eventBox.Set(EvtSearchFin, merger)
|
m.eventBox.Set(EvtSearchFin, result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,16 +169,18 @@ type partialResult struct {
|
|||||||
matches []Result
|
matches []Result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
func (m *Matcher) scan(request MatchRequest) MatchResult {
|
||||||
startedAt := time.Now()
|
startedAt := time.Now()
|
||||||
|
|
||||||
numChunks := len(request.chunks)
|
numChunks := len(request.chunks)
|
||||||
if numChunks == 0 {
|
if numChunks == 0 {
|
||||||
return EmptyMerger(request.revision), false
|
m := EmptyMerger(request.revision)
|
||||||
|
return MatchResult{m, m, false}
|
||||||
}
|
}
|
||||||
pattern := request.pattern
|
pattern := request.pattern
|
||||||
|
passMerger := PassMerger(&request.chunks, m.tac, request.revision, pattern.startIndex)
|
||||||
if pattern.IsEmpty() {
|
if pattern.IsEmpty() {
|
||||||
return PassMerger(&request.chunks, m.tac, request.revision), false
|
return MatchResult{passMerger, passMerger, false}
|
||||||
}
|
}
|
||||||
|
|
||||||
minIndex := request.chunks[0].items[0].Index()
|
minIndex := request.chunks[0].items[0].Index()
|
||||||
@@ -197,11 +216,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
|||||||
sliceMatches = append(sliceMatches, matches...)
|
sliceMatches = append(sliceMatches, matches...)
|
||||||
}
|
}
|
||||||
if m.sort && request.pattern.sortable {
|
if m.sort && request.pattern.sortable {
|
||||||
if m.tac {
|
m.sortBuf[idx] = radixSortResults(sliceMatches, m.tac, m.sortBuf[idx])
|
||||||
sort.Sort(ByRelevanceTac(sliceMatches))
|
|
||||||
} else {
|
|
||||||
sort.Sort(ByRelevance(sliceMatches))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
resultChan <- partialResult{idx, sliceMatches}
|
resultChan <- partialResult{idx, sliceMatches}
|
||||||
}(idx, m.slab[idx], chunks)
|
}(idx, m.slab[idx], chunks)
|
||||||
@@ -224,7 +239,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if m.reqBox.Peek(reqReset) {
|
if m.reqBox.Peek(reqReset) {
|
||||||
return nil, wait()
|
return MatchResult{nil, nil, wait()}
|
||||||
}
|
}
|
||||||
|
|
||||||
if time.Since(startedAt) > progressMinDuration {
|
if time.Since(startedAt) > progressMinDuration {
|
||||||
@@ -237,7 +252,8 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
|||||||
partialResult := <-resultChan
|
partialResult := <-resultChan
|
||||||
partialResults[partialResult.index] = partialResult.matches
|
partialResults[partialResult.index] = partialResult.matches
|
||||||
}
|
}
|
||||||
return NewMerger(pattern, partialResults, m.sort && request.pattern.sortable, m.tac, request.revision, minIndex, maxIndex), false
|
merger := NewMerger(pattern, partialResults, m.sort && request.pattern.sortable, m.tac, request.revision, minIndex, maxIndex)
|
||||||
|
return MatchResult{merger, passMerger, false}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset is called to interrupt/signal the ongoing search
|
// Reset is called to interrupt/signal the ongoing search
|
||||||
|
|||||||
+37
-23
@@ -10,42 +10,46 @@ func EmptyMerger(revision revision) *Merger {
|
|||||||
// Merger holds a set of locally sorted lists of items and provides the view of
|
// Merger holds a set of locally sorted lists of items and provides the view of
|
||||||
// a single, globally-sorted list
|
// a single, globally-sorted list
|
||||||
type Merger struct {
|
type Merger struct {
|
||||||
pattern *Pattern
|
pattern *Pattern
|
||||||
lists [][]Result
|
lists [][]Result
|
||||||
merged []Result
|
merged []Result
|
||||||
chunks *[]*Chunk
|
chunks *[]*Chunk
|
||||||
cursors []int
|
cursors []int
|
||||||
sorted bool
|
sorted bool
|
||||||
tac bool
|
tac bool
|
||||||
final bool
|
final bool
|
||||||
count int
|
count int
|
||||||
pass bool
|
pass bool
|
||||||
revision revision
|
startIndex int
|
||||||
minIndex int32
|
revision revision
|
||||||
maxIndex int32
|
minIndex int32
|
||||||
|
maxIndex int32
|
||||||
}
|
}
|
||||||
|
|
||||||
// PassMerger returns a new Merger that simply returns the items in the
|
// PassMerger returns a new Merger that simply returns the items in the
|
||||||
// original order
|
// original order. startIndex items are skipped from the beginning.
|
||||||
func PassMerger(chunks *[]*Chunk, tac bool, revision revision) *Merger {
|
func PassMerger(chunks *[]*Chunk, tac bool, revision revision, startIndex int32) *Merger {
|
||||||
var minIndex, maxIndex int32
|
var minIndex, maxIndex int32
|
||||||
if len(*chunks) > 0 {
|
if len(*chunks) > 0 {
|
||||||
minIndex = (*chunks)[0].items[0].Index()
|
minIndex = (*chunks)[0].items[0].Index()
|
||||||
maxIndex = (*chunks)[len(*chunks)-1].lastIndex(minIndex)
|
maxIndex = (*chunks)[len(*chunks)-1].lastIndex(minIndex)
|
||||||
}
|
}
|
||||||
|
si := int(startIndex)
|
||||||
mg := Merger{
|
mg := Merger{
|
||||||
pattern: nil,
|
pattern: nil,
|
||||||
chunks: chunks,
|
chunks: chunks,
|
||||||
tac: tac,
|
tac: tac,
|
||||||
count: 0,
|
count: 0,
|
||||||
pass: true,
|
pass: true,
|
||||||
revision: revision,
|
startIndex: si,
|
||||||
minIndex: minIndex,
|
revision: revision,
|
||||||
maxIndex: maxIndex}
|
minIndex: minIndex + startIndex,
|
||||||
|
maxIndex: maxIndex}
|
||||||
|
|
||||||
for _, chunk := range *mg.chunks {
|
for _, chunk := range *mg.chunks {
|
||||||
mg.count += chunk.count
|
mg.count += chunk.count
|
||||||
}
|
}
|
||||||
|
mg.count = max(0, mg.count-si)
|
||||||
return &mg
|
return &mg
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +117,7 @@ func (mg *Merger) Get(idx int) Result {
|
|||||||
if mg.tac {
|
if mg.tac {
|
||||||
idx = mg.count - idx - 1
|
idx = mg.count - idx - 1
|
||||||
}
|
}
|
||||||
|
idx += mg.startIndex
|
||||||
firstChunk := (*mg.chunks)[0]
|
firstChunk := (*mg.chunks)[0]
|
||||||
if firstChunk.count < chunkSize && idx >= firstChunk.count {
|
if firstChunk.count < chunkSize && idx >= firstChunk.count {
|
||||||
idx -= firstChunk.count
|
idx -= firstChunk.count
|
||||||
@@ -141,6 +146,15 @@ func (mg *Merger) Get(idx int) Result {
|
|||||||
panic(fmt.Sprintf("Index out of bounds (unsorted, %d/%d)", idx, mg.count))
|
panic(fmt.Sprintf("Index out of bounds (unsorted, %d/%d)", idx, mg.count))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (mg *Merger) ToMap() map[int32]Result {
|
||||||
|
ret := make(map[int32]Result, mg.count)
|
||||||
|
for i := 0; i < mg.count; i++ {
|
||||||
|
result := mg.Get(i)
|
||||||
|
ret[result.Index()] = result
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
func (mg *Merger) cacheable() bool {
|
func (mg *Merger) cacheable() bool {
|
||||||
return mg.count < mergerCacheMax
|
return mg.count < mergerCacheMax
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -34,11 +34,11 @@ func buildLists(partiallySorted bool) ([][]Result, []Result) {
|
|||||||
numLists := 4
|
numLists := 4
|
||||||
lists := make([][]Result, numLists)
|
lists := make([][]Result, numLists)
|
||||||
cnt := 0
|
cnt := 0
|
||||||
for i := 0; i < numLists; i++ {
|
for i := range numLists {
|
||||||
numResults := rand.Int() % 20
|
numResults := rand.Int() % 20
|
||||||
cnt += numResults
|
cnt += numResults
|
||||||
lists[i] = make([]Result, numResults)
|
lists[i] = make([]Result, numResults)
|
||||||
for j := 0; j < numResults; j++ {
|
for j := range numResults {
|
||||||
item := randResult()
|
item := randResult()
|
||||||
lists[i][j] = item
|
lists[i][j] = item
|
||||||
}
|
}
|
||||||
@@ -60,7 +60,7 @@ func TestMergerUnsorted(t *testing.T) {
|
|||||||
// Not sorted: same order
|
// Not sorted: same order
|
||||||
mg := NewMerger(nil, lists, false, false, revision{}, 0, 0)
|
mg := NewMerger(nil, lists, false, false, revision{}, 0, 0)
|
||||||
assert(t, cnt == mg.Length(), "Invalid Length")
|
assert(t, cnt == mg.Length(), "Invalid Length")
|
||||||
for i := 0; i < cnt; i++ {
|
for i := range cnt {
|
||||||
assert(t, items[i] == mg.Get(i), "Invalid Get")
|
assert(t, items[i] == mg.Get(i), "Invalid Get")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,7 +73,7 @@ func TestMergerSorted(t *testing.T) {
|
|||||||
mg := NewMerger(nil, lists, true, false, revision{}, 0, 0)
|
mg := NewMerger(nil, lists, true, false, revision{}, 0, 0)
|
||||||
assert(t, cnt == mg.Length(), "Invalid Length")
|
assert(t, cnt == mg.Length(), "Invalid Length")
|
||||||
sort.Sort(ByRelevance(items))
|
sort.Sort(ByRelevance(items))
|
||||||
for i := 0; i < cnt; i++ {
|
for i := range cnt {
|
||||||
if items[i] != mg.Get(i) {
|
if items[i] != mg.Get(i) {
|
||||||
t.Error("Not sorted", items[i], mg.Get(i))
|
t.Error("Not sorted", items[i], mg.Get(i))
|
||||||
}
|
}
|
||||||
|
|||||||
+223
-54
@@ -3,10 +3,12 @@ package fzf
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
"github.com/junegunn/fzf/src/algo"
|
"github.com/junegunn/fzf/src/algo"
|
||||||
@@ -59,7 +61,7 @@ Usage: fzf [options]
|
|||||||
|
|
||||||
GLOBAL STYLE
|
GLOBAL STYLE
|
||||||
--style=PRESET Apply a style preset [default|minimal|full[:BORDER_STYLE]
|
--style=PRESET Apply a style preset [default|minimal|full[:BORDER_STYLE]
|
||||||
--color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors
|
--color=COLSPEC Base scheme (dark|light|base16|bw) and/or custom colors
|
||||||
--no-color Disable colors
|
--no-color Disable colors
|
||||||
--no-bold Do not use bold text
|
--no-bold Do not use bold text
|
||||||
|
|
||||||
@@ -94,14 +96,17 @@ Usage: fzf [options]
|
|||||||
-m, --multi[=MAX] Enable multi-select with tab/shift-tab
|
-m, --multi[=MAX] Enable multi-select with tab/shift-tab
|
||||||
--highlight-line Highlight the whole current line
|
--highlight-line Highlight the whole current line
|
||||||
--cycle Enable cyclic scroll
|
--cycle Enable cyclic scroll
|
||||||
--wrap Enable line wrap
|
--wrap[=MODE] Enable line wrap (char|word, default: char)
|
||||||
--wrap-sign=STR Indicator for wrapped lines
|
--wrap-sign=STR Indicator for wrapped lines
|
||||||
--no-multi-line Disable multi-line display of items when using --read0
|
--no-multi-line Disable multi-line display of items when using --read0
|
||||||
|
--raw Enable raw mode (show non-matching items)
|
||||||
--track Track the current selection when the result is updated
|
--track Track the current selection when the result is updated
|
||||||
--tac Reverse the order of the input
|
--tac Reverse the order of the input
|
||||||
--gap[=N] Render empty lines between each item
|
--gap[=N] Render empty lines between each item
|
||||||
--gap-line[=STR] Draw horizontal line on each gap using the string
|
--gap-line[=STR] Draw horizontal line on each gap using the string
|
||||||
(default: '┈' or '-')
|
(default: '┈' or '-')
|
||||||
|
--freeze-left=N Number of fields to freeze on the left
|
||||||
|
--freeze-right=N Number of fields to freeze on the right
|
||||||
--keep-right Keep the right end of the line visible on overflow
|
--keep-right Keep the right end of the line visible on overflow
|
||||||
--scroll-off=LINES Number of screen lines to keep above or below when
|
--scroll-off=LINES Number of screen lines to keep above or below when
|
||||||
scrolling to the top or to the bottom (default: 0)
|
scrolling to the top or to the bottom (default: 0)
|
||||||
@@ -109,6 +114,8 @@ Usage: fzf [options]
|
|||||||
--hscroll-off=COLS Number of screen columns to keep to the right of the
|
--hscroll-off=COLS Number of screen columns to keep to the right of the
|
||||||
highlighted substring (default: 10)
|
highlighted substring (default: 10)
|
||||||
--jump-labels=CHARS Label characters for jump mode
|
--jump-labels=CHARS Label characters for jump mode
|
||||||
|
--gutter=CHAR Character used for the gutter column (default: '▌')
|
||||||
|
--gutter-raw=CHAR Character used for the gutter column in raw mode (default: '▖')
|
||||||
--pointer=STR Pointer to the current line (default: '▌' or '>')
|
--pointer=STR Pointer to the current line (default: '▌' or '>')
|
||||||
--marker=STR Multi-select marker (default: '┃' or '>')
|
--marker=STR Multi-select marker (default: '┃' or '>')
|
||||||
--marker-multi-line=STR Multi-select marker for multi-line entries;
|
--marker-multi-line=STR Multi-select marker for multi-line entries;
|
||||||
@@ -151,7 +158,7 @@ Usage: fzf [options]
|
|||||||
--preview=COMMAND Command to preview highlighted line ({})
|
--preview=COMMAND Command to preview highlighted line ({})
|
||||||
--preview-window=OPT Preview window layout (default: right:50%)
|
--preview-window=OPT Preview window layout (default: right:50%)
|
||||||
[up|down|left|right][,SIZE[%]]
|
[up|down|left|right][,SIZE[%]]
|
||||||
[,[no]wrap][,[no]cycle][,[no]follow][,[no]info]
|
[,[no]wrap[-word]][,[no]cycle][,[no]follow][,[no]info]
|
||||||
[,[no]hidden][,border-STYLE]
|
[,[no]hidden][,border-STYLE]
|
||||||
[,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES]
|
[,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES]
|
||||||
[,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]
|
[,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]
|
||||||
@@ -161,6 +168,7 @@ Usage: fzf [options]
|
|||||||
--preview-label=LABEL
|
--preview-label=LABEL
|
||||||
--preview-label-pos=N Same as --border-label and --border-label-pos,
|
--preview-label-pos=N Same as --border-label and --border-label-pos,
|
||||||
but for preview window
|
but for preview window
|
||||||
|
--preview-wrap-sign=STR Indicator for wrapped lines in the preview window
|
||||||
|
|
||||||
HEADER
|
HEADER
|
||||||
--header=STR String to print as header
|
--header=STR String to print as header
|
||||||
@@ -202,8 +210,10 @@ Usage: fzf [options]
|
|||||||
|
|
||||||
ADVANCED
|
ADVANCED
|
||||||
--with-shell=STR Shell command and flags to start child processes with
|
--with-shell=STR Shell command and flags to start child processes with
|
||||||
--listen[=[ADDR:]PORT] Start HTTP server to receive actions (POST /)
|
--listen[=[ADDR:]PORT] Start HTTP server to receive actions via TCP
|
||||||
(To allow remote process execution, use --listen-unsafe)
|
(To allow remote process execution, use --listen-unsafe)
|
||||||
|
--listen=SOCKET_PATH Start HTTP server to receive actions via Unix domain socket
|
||||||
|
(Path should end with .sock)
|
||||||
|
|
||||||
DIRECTORY TRAVERSAL (Only used when $FZF_DEFAULT_COMMAND is not set)
|
DIRECTORY TRAVERSAL (Only used when $FZF_DEFAULT_COMMAND is not set)
|
||||||
--walker=OPTS [file][,dir][,follow][,hidden] (default: file,follow,hidden)
|
--walker=OPTS [file][,dir][,follow][,hidden] (default: file,follow,hidden)
|
||||||
@@ -212,8 +222,8 @@ Usage: fzf [options]
|
|||||||
(default: .git,node_modules)
|
(default: .git,node_modules)
|
||||||
|
|
||||||
HISTORY
|
HISTORY
|
||||||
--history=FILE History file
|
--history=FILE File to store fzf search history (*not* shell command history)
|
||||||
--history-size=N Maximum number of history entries (default: 1000)
|
--history-size=N Maximum number of entries to keep in the file (default: 1000)
|
||||||
|
|
||||||
SHELL INTEGRATION
|
SHELL INTEGRATION
|
||||||
--bash Print script to set up Bash shell integration
|
--bash Print script to set up Bash shell integration
|
||||||
@@ -284,14 +294,32 @@ func defaultMargin() [4]sizeSpec {
|
|||||||
return [4]sizeSpec{}
|
return [4]sizeSpec{}
|
||||||
}
|
}
|
||||||
|
|
||||||
type trackOption int
|
type trackOption struct {
|
||||||
|
enabled bool
|
||||||
|
index int32
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
var (
|
||||||
trackDisabled trackOption = iota
|
trackDisabled = trackOption{false, minItem.Index()}
|
||||||
trackEnabled
|
trackEnabled = trackOption{true, minItem.Index()}
|
||||||
trackCurrent
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (t trackOption) Disabled() bool {
|
||||||
|
return !t.enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t trackOption) Global() bool {
|
||||||
|
return t.enabled && t.index == minItem.Index()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t trackOption) Current() bool {
|
||||||
|
return t.enabled && t.index != minItem.Index()
|
||||||
|
}
|
||||||
|
|
||||||
|
func trackCurrent(index int32) trackOption {
|
||||||
|
return trackOption{true, index}
|
||||||
|
}
|
||||||
|
|
||||||
type windowPosition int
|
type windowPosition int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -341,6 +369,7 @@ type previewOpts struct {
|
|||||||
scroll string
|
scroll string
|
||||||
hidden bool
|
hidden bool
|
||||||
wrap bool
|
wrap bool
|
||||||
|
wrapWord bool
|
||||||
cycle bool
|
cycle bool
|
||||||
follow bool
|
follow bool
|
||||||
info bool
|
info bool
|
||||||
@@ -517,7 +546,7 @@ func (o *previewOpts) compare(active *previewOpts, b *previewOpts) previewOptsCo
|
|||||||
return previewOptsDifferentLayout
|
return previewOptsDifferentLayout
|
||||||
}
|
}
|
||||||
|
|
||||||
if a.wrap == b.wrap && a.headerLines == b.headerLines && a.info == b.info && a.scroll == b.scroll {
|
if a.wrap == b.wrap && a.wrapWord == b.wrapWord && a.headerLines == b.headerLines && a.info == b.info && a.scroll == b.scroll {
|
||||||
return previewOptsSame
|
return previewOptsSame
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -556,10 +585,13 @@ type Options struct {
|
|||||||
Case Case
|
Case Case
|
||||||
Normalize bool
|
Normalize bool
|
||||||
Nth []Range
|
Nth []Range
|
||||||
|
FreezeLeft int
|
||||||
|
FreezeRight int
|
||||||
WithNth func(Delimiter) func([]Token, int32) string
|
WithNth func(Delimiter) func([]Token, int32) string
|
||||||
AcceptNth func(Delimiter) func([]Token, int32) string
|
AcceptNth func(Delimiter) func([]Token, int32) string
|
||||||
Delimiter Delimiter
|
Delimiter Delimiter
|
||||||
Sort int
|
Sort int
|
||||||
|
Raw bool
|
||||||
Track trackOption
|
Track trackOption
|
||||||
Tac bool
|
Tac bool
|
||||||
Tail int
|
Tail int
|
||||||
@@ -567,6 +599,7 @@ type Options struct {
|
|||||||
Multi int
|
Multi int
|
||||||
Ansi bool
|
Ansi bool
|
||||||
Mouse bool
|
Mouse bool
|
||||||
|
BaseTheme *tui.ColorTheme
|
||||||
Theme *tui.ColorTheme
|
Theme *tui.ColorTheme
|
||||||
Black bool
|
Black bool
|
||||||
Bold bool
|
Bold bool
|
||||||
@@ -575,7 +608,9 @@ type Options struct {
|
|||||||
Layout layoutType
|
Layout layoutType
|
||||||
Cycle bool
|
Cycle bool
|
||||||
Wrap bool
|
Wrap bool
|
||||||
|
WrapWord bool
|
||||||
WrapSign *string
|
WrapSign *string
|
||||||
|
PreviewWrapSign *string
|
||||||
MultiLine bool
|
MultiLine bool
|
||||||
CursorLine bool
|
CursorLine bool
|
||||||
KeepRight bool
|
KeepRight bool
|
||||||
@@ -590,6 +625,8 @@ type Options struct {
|
|||||||
Separator *string
|
Separator *string
|
||||||
JumpLabels string
|
JumpLabels string
|
||||||
Prompt string
|
Prompt string
|
||||||
|
Gutter *string
|
||||||
|
GutterRaw *string
|
||||||
Pointer *string
|
Pointer *string
|
||||||
Marker *string
|
Marker *string
|
||||||
MarkerMulti *[3]string
|
MarkerMulti *[3]string
|
||||||
@@ -641,6 +678,8 @@ type Options struct {
|
|||||||
WalkerSkip []string
|
WalkerSkip []string
|
||||||
Version bool
|
Version bool
|
||||||
Help bool
|
Help bool
|
||||||
|
Threads int
|
||||||
|
Bench time.Duration
|
||||||
CPUProfile string
|
CPUProfile string
|
||||||
MEMProfile string
|
MEMProfile string
|
||||||
BlockProfile string
|
BlockProfile string
|
||||||
@@ -659,15 +698,22 @@ func filterNonEmpty(input []string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func defaultPreviewOpts(command string) previewOpts {
|
func defaultPreviewOpts(command string) previewOpts {
|
||||||
return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, true, defaultBorderShape, 0, 0, nil}
|
return previewOpts{
|
||||||
|
command: command,
|
||||||
|
position: posRight,
|
||||||
|
size: sizeSpec{50, true},
|
||||||
|
info: true,
|
||||||
|
border: defaultBorderShape,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultOptions() *Options {
|
func defaultOptions() *Options {
|
||||||
var theme *tui.ColorTheme
|
var theme, baseTheme *tui.ColorTheme
|
||||||
if os.Getenv("NO_COLOR") != "" {
|
if os.Getenv("NO_COLOR") != "" {
|
||||||
theme = tui.NoColorTheme()
|
theme = tui.NoColorTheme
|
||||||
|
baseTheme = tui.NoColorTheme
|
||||||
} else {
|
} else {
|
||||||
theme = tui.EmptyTheme()
|
theme = tui.EmptyTheme
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Options{
|
return &Options{
|
||||||
@@ -693,12 +739,14 @@ func defaultOptions() *Options {
|
|||||||
Ansi: false,
|
Ansi: false,
|
||||||
Mouse: true,
|
Mouse: true,
|
||||||
Theme: theme,
|
Theme: theme,
|
||||||
|
BaseTheme: baseTheme,
|
||||||
Black: false,
|
Black: false,
|
||||||
Bold: true,
|
Bold: true,
|
||||||
MinHeight: -10,
|
MinHeight: -10,
|
||||||
Layout: layoutDefault,
|
Layout: layoutDefault,
|
||||||
Cycle: false,
|
Cycle: false,
|
||||||
Wrap: false,
|
Wrap: false,
|
||||||
|
WrapWord: false,
|
||||||
MultiLine: true,
|
MultiLine: true,
|
||||||
KeepRight: false,
|
KeepRight: false,
|
||||||
Hscroll: true,
|
Hscroll: true,
|
||||||
@@ -710,6 +758,8 @@ func defaultOptions() *Options {
|
|||||||
Separator: nil,
|
Separator: nil,
|
||||||
JumpLabels: defaultJumpLabels,
|
JumpLabels: defaultJumpLabels,
|
||||||
Prompt: "> ",
|
Prompt: "> ",
|
||||||
|
Gutter: nil,
|
||||||
|
GutterRaw: nil,
|
||||||
Pointer: nil,
|
Pointer: nil,
|
||||||
Marker: nil,
|
Marker: nil,
|
||||||
MarkerMulti: nil,
|
MarkerMulti: nil,
|
||||||
@@ -1199,11 +1249,20 @@ func parseKeyChords(str string, message string) (map[tui.Event]string, []tui.Eve
|
|||||||
default:
|
default:
|
||||||
runes := []rune(key)
|
runes := []rune(key)
|
||||||
if len(key) == 10 && strings.HasPrefix(lkey, "ctrl-alt-") && isAlphabet(lkey[9]) {
|
if len(key) == 10 && strings.HasPrefix(lkey, "ctrl-alt-") && isAlphabet(lkey[9]) {
|
||||||
evt := tui.CtrlAltKey(rune(key[9]))
|
r := rune(lkey[9])
|
||||||
|
evt := tui.CtrlAltKey(r)
|
||||||
|
if r == 'h' && !util.IsWindows() {
|
||||||
|
evt = tui.CtrlAltBackspace.AsEvent()
|
||||||
|
}
|
||||||
chords[evt] = key
|
chords[evt] = key
|
||||||
list = append(list, evt)
|
list = append(list, evt)
|
||||||
} else if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) {
|
} else if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) {
|
||||||
add(tui.EventType(tui.CtrlA.Int() + int(lkey[5]) - 'a'))
|
evt := tui.EventType(tui.CtrlA.Int() + int(lkey[5]) - 'a')
|
||||||
|
r := rune(lkey[5])
|
||||||
|
if r == 'h' && !util.IsWindows() {
|
||||||
|
evt = tui.CtrlBackspace
|
||||||
|
}
|
||||||
|
add(evt)
|
||||||
} else if len(runes) == 5 && strings.HasPrefix(lkey, "alt-") {
|
} else if len(runes) == 5 && strings.HasPrefix(lkey, "alt-") {
|
||||||
r := runes[4]
|
r := runes[4]
|
||||||
switch r {
|
switch r {
|
||||||
@@ -1308,8 +1367,9 @@ func dupeTheme(theme *tui.ColorTheme) *tui.ColorTheme {
|
|||||||
return &dupe
|
return &dupe
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, error) {
|
func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, *tui.ColorTheme, error) {
|
||||||
var err error
|
var err error
|
||||||
|
var baseTheme *tui.ColorTheme
|
||||||
theme := dupeTheme(defaultTheme)
|
theme := dupeTheme(defaultTheme)
|
||||||
rrggbb := regexp.MustCompile("^#[0-9a-fA-F]{6}$")
|
rrggbb := regexp.MustCompile("^#[0-9a-fA-F]{6}$")
|
||||||
comma := regexp.MustCompile(`[\s,]+`)
|
comma := regexp.MustCompile(`[\s,]+`)
|
||||||
@@ -1320,13 +1380,17 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, erro
|
|||||||
}
|
}
|
||||||
switch str {
|
switch str {
|
||||||
case "dark":
|
case "dark":
|
||||||
|
baseTheme = tui.Dark256
|
||||||
theme = dupeTheme(tui.Dark256)
|
theme = dupeTheme(tui.Dark256)
|
||||||
case "light":
|
case "light":
|
||||||
|
baseTheme = tui.Light256
|
||||||
theme = dupeTheme(tui.Light256)
|
theme = dupeTheme(tui.Light256)
|
||||||
case "16":
|
case "base16", "16":
|
||||||
|
baseTheme = tui.Default16
|
||||||
theme = dupeTheme(tui.Default16)
|
theme = dupeTheme(tui.Default16)
|
||||||
case "bw", "no":
|
case "bw", "no":
|
||||||
theme = tui.NoColorTheme()
|
baseTheme = tui.NoColorTheme
|
||||||
|
theme = dupeTheme(tui.NoColorTheme)
|
||||||
default:
|
default:
|
||||||
fail := func() {
|
fail := func() {
|
||||||
// Let the code proceed to simplify the error handling
|
// Let the code proceed to simplify the error handling
|
||||||
@@ -1351,10 +1415,20 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, erro
|
|||||||
cattr.Attr |= tui.Bold
|
cattr.Attr |= tui.Bold
|
||||||
case "dim":
|
case "dim":
|
||||||
cattr.Attr |= tui.Dim
|
cattr.Attr |= tui.Dim
|
||||||
|
case "strip":
|
||||||
|
cattr.Attr |= tui.Strip
|
||||||
case "italic":
|
case "italic":
|
||||||
cattr.Attr |= tui.Italic
|
cattr.Attr |= tui.Italic
|
||||||
case "underline":
|
case "underline":
|
||||||
cattr.Attr |= tui.Underline
|
cattr.Attr |= tui.Underline
|
||||||
|
case "underline-double":
|
||||||
|
cattr.Attr |= tui.Underline | tui.UlStyleDouble
|
||||||
|
case "underline-curly":
|
||||||
|
cattr.Attr |= tui.Underline | tui.UlStyleCurly
|
||||||
|
case "underline-dotted":
|
||||||
|
cattr.Attr |= tui.Underline | tui.UlStyleDotted
|
||||||
|
case "underline-dashed":
|
||||||
|
cattr.Attr |= tui.Underline | tui.UlStyleDashed
|
||||||
case "blink":
|
case "blink":
|
||||||
cattr.Attr |= tui.Blink
|
cattr.Attr |= tui.Blink
|
||||||
case "reverse":
|
case "reverse":
|
||||||
@@ -1438,8 +1512,12 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, erro
|
|||||||
mergeAttr(&theme.SelectedBg)
|
mergeAttr(&theme.SelectedBg)
|
||||||
case "nth":
|
case "nth":
|
||||||
mergeAttr(&theme.Nth)
|
mergeAttr(&theme.Nth)
|
||||||
|
case "nomatch":
|
||||||
|
mergeAttr(&theme.Nomatch)
|
||||||
case "gutter":
|
case "gutter":
|
||||||
mergeAttr(&theme.Gutter)
|
mergeAttr(&theme.Gutter)
|
||||||
|
case "alt-gutter":
|
||||||
|
mergeAttr(&theme.AltGutter)
|
||||||
case "hl":
|
case "hl":
|
||||||
mergeAttr(&theme.Match)
|
mergeAttr(&theme.Match)
|
||||||
case "current-hl", "hl+":
|
case "current-hl", "hl+":
|
||||||
@@ -1503,7 +1581,7 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, erro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return theme, err
|
return baseTheme, theme, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseWalkerOpts(str string) (walkerOpts, error) {
|
func parseWalkerOpts(str string) (walkerOpts, error) {
|
||||||
@@ -1551,7 +1629,7 @@ const (
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
executeRegexp = regexp.MustCompile(
|
executeRegexp = regexp.MustCompile(
|
||||||
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|bg-transform|transform)-(?:query|prompt|(?:border|list|preview|input|header|footer)-label|header|footer|search|nth|pointer|ghost)|bg-transform|transform|change-(?:preview-window|preview|multi)|(?:re|un|toggle-)bind|pos|put|print|search|trigger)`)
|
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|bg-transform|transform)-(?:query|prompt|(?:border|list|preview|input|header|footer)-label|header-lines|header|footer|search|nth|pointer|ghost)|bg-transform|transform|change-(?:preview-window|preview|multi)|(?:re|un|toggle-)bind|pos|put|print|search|trigger)`)
|
||||||
splitRegexp = regexp.MustCompile("[,:]+")
|
splitRegexp = regexp.MustCompile("[,:]+")
|
||||||
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
|
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
|
||||||
}
|
}
|
||||||
@@ -1733,10 +1811,18 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
|||||||
appendAction(actToggleHeader)
|
appendAction(actToggleHeader)
|
||||||
case "toggle-wrap":
|
case "toggle-wrap":
|
||||||
appendAction(actToggleWrap)
|
appendAction(actToggleWrap)
|
||||||
|
case "toggle-wrap-word":
|
||||||
|
appendAction(actToggleWrapWord)
|
||||||
case "toggle-multi-line":
|
case "toggle-multi-line":
|
||||||
appendAction(actToggleMultiLine)
|
appendAction(actToggleMultiLine)
|
||||||
case "toggle-hscroll":
|
case "toggle-hscroll":
|
||||||
appendAction(actToggleHscroll)
|
appendAction(actToggleHscroll)
|
||||||
|
case "toggle-raw":
|
||||||
|
appendAction(actToggleRaw)
|
||||||
|
case "enable-raw":
|
||||||
|
appendAction(actEnableRaw)
|
||||||
|
case "disable-raw":
|
||||||
|
appendAction(actDisableRaw)
|
||||||
case "show-header":
|
case "show-header":
|
||||||
appendAction(actShowHeader)
|
appendAction(actShowHeader)
|
||||||
case "hide-header":
|
case "hide-header":
|
||||||
@@ -1757,12 +1843,18 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
|||||||
appendAction(actToggle)
|
appendAction(actToggle)
|
||||||
case "down":
|
case "down":
|
||||||
appendAction(actDown)
|
appendAction(actDown)
|
||||||
|
case "down-match":
|
||||||
|
appendAction(actDownMatch)
|
||||||
case "up":
|
case "up":
|
||||||
appendAction(actUp)
|
appendAction(actUp)
|
||||||
|
case "up-match":
|
||||||
|
appendAction(actUpMatch)
|
||||||
case "first", "top":
|
case "first", "top":
|
||||||
appendAction(actFirst)
|
appendAction(actFirst)
|
||||||
case "last":
|
case "last":
|
||||||
appendAction(actLast)
|
appendAction(actLast)
|
||||||
|
case "best":
|
||||||
|
appendAction(actBest)
|
||||||
case "page-up":
|
case "page-up":
|
||||||
appendAction(actPageUp)
|
appendAction(actPageUp)
|
||||||
case "page-down":
|
case "page-down":
|
||||||
@@ -1775,9 +1867,9 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
|||||||
appendAction(actPrevHistory)
|
appendAction(actPrevHistory)
|
||||||
case "next-history":
|
case "next-history":
|
||||||
appendAction(actNextHistory)
|
appendAction(actNextHistory)
|
||||||
case "prev-selected":
|
case "up-selected", "prev-selected":
|
||||||
appendAction(actPrevSelected)
|
appendAction(actPrevSelected)
|
||||||
case "next-selected":
|
case "down-selected", "next-selected":
|
||||||
appendAction(actNextSelected)
|
appendAction(actNextSelected)
|
||||||
case "show-preview":
|
case "show-preview":
|
||||||
appendAction(actShowPreview)
|
appendAction(actShowPreview)
|
||||||
@@ -1787,6 +1879,8 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
|||||||
appendAction(actTogglePreview)
|
appendAction(actTogglePreview)
|
||||||
case "toggle-preview-wrap":
|
case "toggle-preview-wrap":
|
||||||
appendAction(actTogglePreviewWrap)
|
appendAction(actTogglePreviewWrap)
|
||||||
|
case "toggle-preview-wrap-word":
|
||||||
|
appendAction(actTogglePreviewWrapWord)
|
||||||
case "toggle-sort":
|
case "toggle-sort":
|
||||||
appendAction(actToggleSort)
|
appendAction(actToggleSort)
|
||||||
case "offset-up":
|
case "offset-up":
|
||||||
@@ -1946,6 +2040,8 @@ func isExecuteAction(str string) actionType {
|
|||||||
return actPreview
|
return actPreview
|
||||||
case "change-header":
|
case "change-header":
|
||||||
return actChangeHeader
|
return actChangeHeader
|
||||||
|
case "change-header-lines":
|
||||||
|
return actChangeHeaderLines
|
||||||
case "change-footer":
|
case "change-footer":
|
||||||
return actChangeFooter
|
return actChangeFooter
|
||||||
case "change-list-label":
|
case "change-list-label":
|
||||||
@@ -2006,6 +2102,8 @@ func isExecuteAction(str string) actionType {
|
|||||||
return actTransformFooter
|
return actTransformFooter
|
||||||
case "transform-header":
|
case "transform-header":
|
||||||
return actTransformHeader
|
return actTransformHeader
|
||||||
|
case "transform-header-lines":
|
||||||
|
return actTransformHeaderLines
|
||||||
case "transform-ghost":
|
case "transform-ghost":
|
||||||
return actTransformGhost
|
return actTransformGhost
|
||||||
case "transform-nth":
|
case "transform-nth":
|
||||||
@@ -2036,6 +2134,8 @@ func isExecuteAction(str string) actionType {
|
|||||||
return actBgTransformFooter
|
return actBgTransformFooter
|
||||||
case "bg-transform-header":
|
case "bg-transform-header":
|
||||||
return actBgTransformHeader
|
return actBgTransformHeader
|
||||||
|
case "bg-transform-header-lines":
|
||||||
|
return actBgTransformHeaderLines
|
||||||
case "bg-transform-ghost":
|
case "bg-transform-ghost":
|
||||||
return actBgTransformGhost
|
return actBgTransformGhost
|
||||||
case "bg-transform-nth":
|
case "bg-transform-nth":
|
||||||
@@ -2198,8 +2298,13 @@ func parsePreviewWindowImpl(opts *previewOpts, input string) error {
|
|||||||
opts.hidden = false
|
opts.hidden = false
|
||||||
case "wrap":
|
case "wrap":
|
||||||
opts.wrap = true
|
opts.wrap = true
|
||||||
|
opts.wrapWord = false
|
||||||
|
case "wrap-word":
|
||||||
|
opts.wrap = true
|
||||||
|
opts.wrapWord = true
|
||||||
case "nowrap":
|
case "nowrap":
|
||||||
opts.wrap = false
|
opts.wrap = false
|
||||||
|
opts.wrapWord = false
|
||||||
case "cycle":
|
case "cycle":
|
||||||
opts.cycle = true
|
opts.cycle = true
|
||||||
case "nocycle":
|
case "nocycle":
|
||||||
@@ -2600,9 +2705,7 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for k, v := range chords {
|
maps.Copy(opts.Expect, chords)
|
||||||
opts.Expect[k] = v
|
|
||||||
}
|
|
||||||
case "--no-expect":
|
case "--no-expect":
|
||||||
opts.Expect = make(map[tui.Event]string)
|
opts.Expect = make(map[tui.Event]string)
|
||||||
case "--enabled", "--no-phony":
|
case "--enabled", "--no-phony":
|
||||||
@@ -2630,11 +2733,15 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
|||||||
case "--color":
|
case "--color":
|
||||||
_, spec := optionalNextString()
|
_, spec := optionalNextString()
|
||||||
if len(spec) == 0 {
|
if len(spec) == 0 {
|
||||||
opts.Theme = tui.EmptyTheme()
|
opts.Theme = tui.EmptyTheme
|
||||||
} else {
|
} else {
|
||||||
if opts.Theme, err = parseTheme(opts.Theme, spec); err != nil {
|
var baseTheme *tui.ColorTheme
|
||||||
|
if baseTheme, opts.Theme, err = parseTheme(opts.Theme, spec); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if baseTheme != nil {
|
||||||
|
opts.BaseTheme = baseTheme
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case "--toggle-sort":
|
case "--toggle-sort":
|
||||||
str, err := nextString("key name required")
|
str, err := nextString("key name required")
|
||||||
@@ -2658,6 +2765,14 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
|||||||
if opts.Nth, err = splitNth(str); err != nil {
|
if opts.Nth, err = splitNth(str); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
case "--freeze-left":
|
||||||
|
if opts.FreezeLeft, err = nextInt("number of fields required"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "--freeze-right":
|
||||||
|
if opts.FreezeRight, err = nextInt("number of fields required"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
case "--with-nth":
|
case "--with-nth":
|
||||||
str, err := nextString("nth expression required")
|
str, err := nextString("nth expression required")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2680,6 +2795,10 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
|||||||
}
|
}
|
||||||
case "+s", "--no-sort":
|
case "+s", "--no-sort":
|
||||||
opts.Sort = 0
|
opts.Sort = 0
|
||||||
|
case "--raw":
|
||||||
|
opts.Raw = true
|
||||||
|
case "--no-raw":
|
||||||
|
opts.Raw = false
|
||||||
case "--track":
|
case "--track":
|
||||||
opts.Track = trackEnabled
|
opts.Track = trackEnabled
|
||||||
case "--no-track":
|
case "--no-track":
|
||||||
@@ -2716,7 +2835,8 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
|||||||
case "--no-mouse":
|
case "--no-mouse":
|
||||||
opts.Mouse = false
|
opts.Mouse = false
|
||||||
case "+c", "--no-color":
|
case "+c", "--no-color":
|
||||||
opts.Theme = tui.NoColorTheme()
|
opts.BaseTheme = tui.NoColorTheme
|
||||||
|
opts.Theme = tui.NoColorTheme
|
||||||
case "+2", "--no-256":
|
case "+2", "--no-256":
|
||||||
opts.Theme = tui.Default16
|
opts.Theme = tui.Default16
|
||||||
case "--black":
|
case "--black":
|
||||||
@@ -2748,9 +2868,29 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
|||||||
case "--no-cycle":
|
case "--no-cycle":
|
||||||
opts.Cycle = false
|
opts.Cycle = false
|
||||||
case "--wrap":
|
case "--wrap":
|
||||||
opts.Wrap = true
|
given, str := optionalNextString()
|
||||||
|
if given {
|
||||||
|
switch str {
|
||||||
|
case "char":
|
||||||
|
opts.Wrap = true
|
||||||
|
opts.WrapWord = false
|
||||||
|
case "word":
|
||||||
|
opts.Wrap = true
|
||||||
|
opts.WrapWord = true
|
||||||
|
default:
|
||||||
|
return errors.New("invalid wrap mode: " + str + " (expected: char or word)")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
opts.Wrap = true
|
||||||
|
}
|
||||||
case "--no-wrap":
|
case "--no-wrap":
|
||||||
opts.Wrap = false
|
opts.Wrap = false
|
||||||
|
opts.WrapWord = false
|
||||||
|
case "--wrap-word":
|
||||||
|
opts.Wrap = true
|
||||||
|
opts.WrapWord = true
|
||||||
|
case "--no-wrap-word":
|
||||||
|
opts.WrapWord = false
|
||||||
case "--wrap-sign":
|
case "--wrap-sign":
|
||||||
str, err := nextString("wrap sign required")
|
str, err := nextString("wrap sign required")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2857,6 +2997,20 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
case "--gutter":
|
||||||
|
str, err := nextString("gutter character required")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
str = firstLine(str)
|
||||||
|
opts.Gutter = &str
|
||||||
|
case "--gutter-raw":
|
||||||
|
str, err := nextString("gutter character for raw mode required")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
str = firstLine(str)
|
||||||
|
opts.GutterRaw = &str
|
||||||
case "--pointer":
|
case "--pointer":
|
||||||
str, err := nextString("pointer sign required")
|
str, err := nextString("pointer sign required")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2970,6 +3124,12 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
|||||||
if opts.Preview.border, err = parseBorder(arg, !hasArg); err != nil {
|
if opts.Preview.border, err = parseBorder(arg, !hasArg); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
case "--preview-wrap-sign":
|
||||||
|
str, err := nextString("preview wrap sign required")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
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 {
|
||||||
@@ -3216,6 +3376,23 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
opts.WalkerSkip = filterNonEmpty(strings.Split(str, ","))
|
opts.WalkerSkip = filterNonEmpty(strings.Split(str, ","))
|
||||||
|
case "--threads":
|
||||||
|
if opts.Threads, err = nextInt("number of threads required"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if opts.Threads < 0 {
|
||||||
|
return errors.New("--threads must be a positive integer")
|
||||||
|
}
|
||||||
|
case "--bench":
|
||||||
|
str, err := nextString("duration required (e.g. 3s, 500ms)")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dur, err := time.ParseDuration(str)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("invalid duration for --bench: " + str)
|
||||||
|
}
|
||||||
|
opts.Bench = dur
|
||||||
case "--profile-cpu":
|
case "--profile-cpu":
|
||||||
if opts.CPUProfile, err = nextString("file path required: cpu"); err != nil {
|
if opts.CPUProfile, err = nextString("file path required: cpu"); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -3282,6 +3459,10 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
|||||||
return errors.New("empty jump labels")
|
return errors.New("empty jump labels")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.FreezeLeft < 0 || opts.FreezeRight < 0 {
|
||||||
|
return errors.New("number of fields to freeze must be a non-negative integer")
|
||||||
|
}
|
||||||
|
|
||||||
if validateJumpLabels {
|
if validateJumpLabels {
|
||||||
for _, r := range opts.JumpLabels {
|
for _, r := range opts.JumpLabels {
|
||||||
if r < 32 || r > 126 {
|
if r < 32 || r > 126 {
|
||||||
@@ -3355,26 +3536,31 @@ func applyPreset(opts *Options, preset string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateSign(sign string, signOptName string) error {
|
func validateSign(sign string, signOptName string, maxWidth int) error {
|
||||||
if uniseg.StringWidth(sign) > 2 {
|
if uniseg.StringWidth(sign) > maxWidth {
|
||||||
return fmt.Errorf("%v display width should be up to 2", signOptName)
|
return fmt.Errorf("%v display width should be up to %d", signOptName, maxWidth)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateOptions(opts *Options) error {
|
func validateOptions(opts *Options) error {
|
||||||
if opts.Pointer != nil {
|
if opts.Pointer != nil {
|
||||||
if err := validateSign(*opts.Pointer, "pointer"); err != nil {
|
if err := validateSign(*opts.Pointer, "pointer", 2); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.Marker != nil {
|
if opts.Marker != nil {
|
||||||
if err := validateSign(*opts.Marker, "marker"); err != nil {
|
if err := validateSign(*opts.Marker, "marker", 2); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.Gutter != nil && uniseg.StringWidth(*opts.Gutter) != 1 ||
|
||||||
|
opts.GutterRaw != nil && uniseg.StringWidth(*opts.GutterRaw) != 1 {
|
||||||
|
return errors.New("gutter display width should be 1")
|
||||||
|
}
|
||||||
|
|
||||||
if opts.Scrollbar != nil {
|
if opts.Scrollbar != nil {
|
||||||
runes := []rune(*opts.Scrollbar)
|
runes := []rune(*opts.Scrollbar)
|
||||||
if len(runes) > 2 {
|
if len(runes) > 2 {
|
||||||
@@ -3574,23 +3760,6 @@ func postProcessOptions(opts *Options) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.Bold {
|
|
||||||
theme := opts.Theme
|
|
||||||
boldify := func(c tui.ColorAttr) tui.ColorAttr {
|
|
||||||
dup := c
|
|
||||||
if (c.Attr & tui.AttrRegular) == 0 {
|
|
||||||
dup.Attr |= tui.BoldForce
|
|
||||||
}
|
|
||||||
return dup
|
|
||||||
}
|
|
||||||
theme.Current = boldify(theme.Current)
|
|
||||||
theme.CurrentMatch = boldify(theme.CurrentMatch)
|
|
||||||
theme.Prompt = boldify(theme.Prompt)
|
|
||||||
theme.Input = boldify(theme.Input)
|
|
||||||
theme.Cursor = boldify(theme.Cursor)
|
|
||||||
theme.Spinner = boldify(theme.Spinner)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If --height option is not supported on the platform, just ignore it
|
// If --height option is not supported on the platform, just ignore it
|
||||||
if !tui.IsLightRendererSupported() && opts.Height.size > 0 {
|
if !tui.IsLightRendererSupported() && opts.Height.size > 0 {
|
||||||
opts.Height = heightSpec{}
|
opts.Height = heightSpec{}
|
||||||
|
|||||||
+72
-7
@@ -300,8 +300,12 @@ func TestBind(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestColorSpec(t *testing.T) {
|
func TestColorSpec(t *testing.T) {
|
||||||
|
var base *tui.ColorTheme
|
||||||
theme := tui.Dark256
|
theme := tui.Dark256
|
||||||
dark, _ := parseTheme(theme, "dark")
|
base, dark, _ := parseTheme(theme, "dark")
|
||||||
|
if *dark != *base {
|
||||||
|
t.Errorf("incorrect base theme returned")
|
||||||
|
}
|
||||||
if *dark != *theme {
|
if *dark != *theme {
|
||||||
t.Errorf("colors should be equivalent")
|
t.Errorf("colors should be equivalent")
|
||||||
}
|
}
|
||||||
@@ -309,7 +313,10 @@ func TestColorSpec(t *testing.T) {
|
|||||||
t.Errorf("point should not be equivalent")
|
t.Errorf("point should not be equivalent")
|
||||||
}
|
}
|
||||||
|
|
||||||
light, _ := parseTheme(theme, "dark,light")
|
base, light, _ := parseTheme(theme, "dark,light")
|
||||||
|
if *light != *base {
|
||||||
|
t.Errorf("incorrect base theme returned")
|
||||||
|
}
|
||||||
if *light == *theme {
|
if *light == *theme {
|
||||||
t.Errorf("should not be equivalent")
|
t.Errorf("should not be equivalent")
|
||||||
}
|
}
|
||||||
@@ -320,7 +327,7 @@ func TestColorSpec(t *testing.T) {
|
|||||||
t.Errorf("point should not be equivalent")
|
t.Errorf("point should not be equivalent")
|
||||||
}
|
}
|
||||||
|
|
||||||
customized, _ := parseTheme(theme, "fg:231,bg:232")
|
_, customized, _ := parseTheme(theme, "fg:231,bg:232")
|
||||||
if customized.Fg.Color != 231 || customized.Bg.Color != 232 {
|
if customized.Fg.Color != 231 || customized.Bg.Color != 232 {
|
||||||
t.Errorf("color not customized")
|
t.Errorf("color not customized")
|
||||||
}
|
}
|
||||||
@@ -333,7 +340,7 @@ func TestColorSpec(t *testing.T) {
|
|||||||
t.Errorf("colors should now be equivalent: %v, %v", tui.Dark256, customized)
|
t.Errorf("colors should now be equivalent: %v, %v", tui.Dark256, customized)
|
||||||
}
|
}
|
||||||
|
|
||||||
customized, _ = parseTheme(theme, "fg:231,dark bg:232")
|
_, customized, _ = parseTheme(theme, "fg:231,dark bg:232")
|
||||||
if customized.Fg != tui.Dark256.Fg || customized.Bg == tui.Dark256.Bg {
|
if customized.Fg != tui.Dark256.Fg || customized.Bg == tui.Dark256.Bg {
|
||||||
t.Errorf("color not customized")
|
t.Errorf("color not customized")
|
||||||
}
|
}
|
||||||
@@ -350,8 +357,8 @@ func TestDefaultCtrlNP(t *testing.T) {
|
|||||||
t.Error()
|
t.Error()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
check([]string{}, tui.CtrlN, actDown)
|
check([]string{}, tui.CtrlN, actDownMatch)
|
||||||
check([]string{}, tui.CtrlP, actUp)
|
check([]string{}, tui.CtrlP, actUpMatch)
|
||||||
|
|
||||||
check([]string{"--bind=ctrl-n:accept"}, tui.CtrlN, actAccept)
|
check([]string{"--bind=ctrl-n:accept"}, tui.CtrlN, actAccept)
|
||||||
check([]string{"--bind=ctrl-p:accept"}, tui.CtrlP, actAccept)
|
check([]string{"--bind=ctrl-p:accept"}, tui.CtrlP, actAccept)
|
||||||
@@ -441,6 +448,64 @@ func TestPreviewOpts(t *testing.T) {
|
|||||||
opts.Preview.size.size == 70) {
|
opts.Preview.size.size == 70) {
|
||||||
t.Error(opts.Preview)
|
t.Error(opts.Preview)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wrap-word tests
|
||||||
|
opts = optsFor("--preview-window=wrap-word")
|
||||||
|
if !(opts.Preview.wrap == true && opts.Preview.wrapWord == true) {
|
||||||
|
t.Errorf("wrap-word: wrap=%v, wrapWord=%v", opts.Preview.wrap, opts.Preview.wrapWord)
|
||||||
|
}
|
||||||
|
opts = optsFor("--preview-window=wrap-word,nowrap")
|
||||||
|
if !(opts.Preview.wrap == false && opts.Preview.wrapWord == false) {
|
||||||
|
t.Errorf("wrap-word,nowrap: wrap=%v, wrapWord=%v", opts.Preview.wrap, opts.Preview.wrapWord)
|
||||||
|
}
|
||||||
|
opts = optsFor("--preview-window=wrap-word,wrap")
|
||||||
|
if !(opts.Preview.wrap == true && opts.Preview.wrapWord == false) {
|
||||||
|
t.Errorf("wrap-word,wrap: wrap=%v, wrapWord=%v", opts.Preview.wrap, opts.Preview.wrapWord)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPreviewWrapSign(t *testing.T) {
|
||||||
|
// Default: no preview wrap sign override
|
||||||
|
opts := optsFor()
|
||||||
|
if opts.PreviewWrapSign != nil {
|
||||||
|
t.Errorf("expected nil PreviewWrapSign, got %v", *opts.PreviewWrapSign)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --preview-wrap-sign sets PreviewWrapSign
|
||||||
|
opts = optsFor("--preview-wrap-sign", ">> ")
|
||||||
|
if opts.PreviewWrapSign == nil || *opts.PreviewWrapSign != ">> " {
|
||||||
|
t.Errorf("expected '>> ', got %v", opts.PreviewWrapSign)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --preview-wrap-sign is independent of --wrap-sign
|
||||||
|
opts = optsFor("--wrap-sign", "| ", "--preview-wrap-sign", ">> ")
|
||||||
|
if opts.WrapSign == nil || *opts.WrapSign != "| " {
|
||||||
|
t.Errorf("expected WrapSign '| ', got %v", opts.WrapSign)
|
||||||
|
}
|
||||||
|
if opts.PreviewWrapSign == nil || *opts.PreviewWrapSign != ">> " {
|
||||||
|
t.Errorf("expected PreviewWrapSign '>> ', got %v", opts.PreviewWrapSign)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --preview-wrap-sign without --wrap-sign
|
||||||
|
opts = optsFor("--preview-wrap-sign", "→ ")
|
||||||
|
if opts.WrapSign != nil {
|
||||||
|
t.Errorf("expected nil WrapSign, got %v", *opts.WrapSign)
|
||||||
|
}
|
||||||
|
if opts.PreviewWrapSign == nil || *opts.PreviewWrapSign != "→ " {
|
||||||
|
t.Errorf("expected PreviewWrapSign '→ ', got %v", opts.PreviewWrapSign)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last --preview-wrap-sign wins
|
||||||
|
opts = optsFor("--preview-wrap-sign", "A ", "--preview-wrap-sign", "B ")
|
||||||
|
if opts.PreviewWrapSign == nil || *opts.PreviewWrapSign != "B " {
|
||||||
|
t.Errorf("expected PreviewWrapSign 'B ', got %v", opts.PreviewWrapSign)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty string is allowed
|
||||||
|
opts = optsFor("--preview-wrap-sign", "")
|
||||||
|
if opts.PreviewWrapSign == nil || *opts.PreviewWrapSign != "" {
|
||||||
|
t.Errorf("expected empty PreviewWrapSign, got %v", opts.PreviewWrapSign)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAdditiveExpect(t *testing.T) {
|
func TestAdditiveExpect(t *testing.T) {
|
||||||
@@ -462,7 +527,7 @@ func TestValidateSign(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
err := validateSign(testCase.inputSign, "")
|
err := validateSign(testCase.inputSign, "", 2)
|
||||||
if testCase.isValid && err != nil {
|
if testCase.isValid && err != nil {
|
||||||
t.Errorf("Input sign `%s` caused error", testCase.inputSign)
|
t.Errorf("Input sign `%s` caused error", testCase.inputSign)
|
||||||
}
|
}
|
||||||
|
|||||||
+79
-21
@@ -64,6 +64,9 @@ type Pattern struct {
|
|||||||
procFun map[termType]algo.Algo
|
procFun map[termType]algo.Algo
|
||||||
cache *ChunkCache
|
cache *ChunkCache
|
||||||
denylist map[int32]struct{}
|
denylist map[int32]struct{}
|
||||||
|
startIndex int32
|
||||||
|
directAlgo algo.Algo
|
||||||
|
directTerm *term
|
||||||
}
|
}
|
||||||
|
|
||||||
var _splitRegex *regexp.Regexp
|
var _splitRegex *regexp.Regexp
|
||||||
@@ -74,7 +77,7 @@ func init() {
|
|||||||
|
|
||||||
// BuildPattern builds Pattern object from the given arguments
|
// BuildPattern builds Pattern object from the given arguments
|
||||||
func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
|
func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
|
||||||
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, revision revision, runes []rune, denylist map[int32]struct{}) *Pattern {
|
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, revision revision, runes []rune, denylist map[int32]struct{}, startIndex int32) *Pattern {
|
||||||
|
|
||||||
var asString string
|
var asString string
|
||||||
if extended {
|
if extended {
|
||||||
@@ -146,9 +149,11 @@ func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy boo
|
|||||||
delimiter: delimiter,
|
delimiter: delimiter,
|
||||||
cache: cache,
|
cache: cache,
|
||||||
denylist: denylist,
|
denylist: denylist,
|
||||||
|
startIndex: startIndex,
|
||||||
procFun: make(map[termType]algo.Algo)}
|
procFun: make(map[termType]algo.Algo)}
|
||||||
|
|
||||||
ptr.cacheKey = ptr.buildCacheKey()
|
ptr.cacheKey = ptr.buildCacheKey()
|
||||||
|
ptr.directAlgo, ptr.directTerm = ptr.buildDirectAlgo(fuzzyAlgo)
|
||||||
ptr.procFun[termFuzzy] = fuzzyAlgo
|
ptr.procFun[termFuzzy] = fuzzyAlgo
|
||||||
ptr.procFun[termEqual] = algo.EqualMatch
|
ptr.procFun[termEqual] = algo.EqualMatch
|
||||||
ptr.procFun[termExact] = algo.ExactMatchNaive
|
ptr.procFun[termExact] = algo.ExactMatchNaive
|
||||||
@@ -272,6 +277,22 @@ func (p *Pattern) buildCacheKey() string {
|
|||||||
return strings.Join(cacheableTerms, "\t")
|
return strings.Join(cacheableTerms, "\t")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildDirectAlgo returns the algo function and term for the direct fast path
|
||||||
|
// in matchChunk. Returns (nil, nil) if the pattern is not suitable.
|
||||||
|
// Requirements: extended mode, single term set with single non-inverse fuzzy term, no nth.
|
||||||
|
func (p *Pattern) buildDirectAlgo(fuzzyAlgo algo.Algo) (algo.Algo, *term) {
|
||||||
|
if !p.extended || len(p.nth) > 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if len(p.termSets) == 1 && len(p.termSets[0]) == 1 {
|
||||||
|
t := &p.termSets[0][0]
|
||||||
|
if !t.inv && t.typ == termFuzzy {
|
||||||
|
return fuzzyAlgo, t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
// CacheKey is used to build string to be used as the key of result cache
|
// CacheKey is used to build string to be used as the key of result cache
|
||||||
func (p *Pattern) CacheKey() string {
|
func (p *Pattern) CacheKey() string {
|
||||||
return p.cacheKey
|
return p.cacheKey
|
||||||
@@ -301,18 +322,56 @@ func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result {
|
|||||||
func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result {
|
func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result {
|
||||||
matches := []Result{}
|
matches := []Result{}
|
||||||
|
|
||||||
if len(p.denylist) == 0 {
|
// Skip header items in chunks that contain them
|
||||||
// Huge code duplication for minimizing unnecessary map lookups
|
startIdx := 0
|
||||||
|
if p.startIndex > 0 && chunk.count > 0 && chunk.items[0].Index() < p.startIndex {
|
||||||
|
startIdx = int(p.startIndex - chunk.items[0].Index())
|
||||||
|
if startIdx >= chunk.count {
|
||||||
|
return matches
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fast path: single fuzzy term, no nth, no denylist.
|
||||||
|
// Calls the algo function directly, bypassing MatchItem/extendedMatch/iter
|
||||||
|
// and avoiding per-match []Offset heap allocation.
|
||||||
|
if p.directAlgo != nil && len(p.denylist) == 0 {
|
||||||
|
t := p.directTerm
|
||||||
if space == nil {
|
if space == nil {
|
||||||
for idx := 0; idx < chunk.count; idx++ {
|
for idx := startIdx; idx < chunk.count; idx++ {
|
||||||
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil {
|
res, _ := p.directAlgo(t.caseSensitive, t.normalize, p.forward,
|
||||||
matches = append(matches, *match)
|
&chunk.items[idx].text, t.text, p.withPos, slab)
|
||||||
|
if res.Start >= 0 {
|
||||||
|
matches = append(matches, buildResultFromBounds(
|
||||||
|
&chunk.items[idx], res.Score,
|
||||||
|
int(res.Start), int(res.End), int(res.End), true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for _, result := range space {
|
for _, result := range space {
|
||||||
if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match != nil {
|
res, _ := p.directAlgo(t.caseSensitive, t.normalize, p.forward,
|
||||||
matches = append(matches, *match)
|
&result.item.text, t.text, p.withPos, slab)
|
||||||
|
if res.Start >= 0 {
|
||||||
|
matches = append(matches, buildResultFromBounds(
|
||||||
|
result.item, res.Score,
|
||||||
|
int(res.Start), int(res.End), int(res.End), true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matches
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(p.denylist) == 0 {
|
||||||
|
// Huge code duplication for minimizing unnecessary map lookups
|
||||||
|
if space == nil {
|
||||||
|
for idx := startIdx; idx < chunk.count; idx++ {
|
||||||
|
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match.item != nil {
|
||||||
|
matches = append(matches, match)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, result := range space {
|
||||||
|
if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match.item != nil {
|
||||||
|
matches = append(matches, match)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -320,12 +379,12 @@ func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Re
|
|||||||
}
|
}
|
||||||
|
|
||||||
if space == nil {
|
if space == nil {
|
||||||
for idx := 0; idx < chunk.count; idx++ {
|
for idx := startIdx; idx < chunk.count; idx++ {
|
||||||
if _, prs := p.denylist[chunk.items[idx].Index()]; prs {
|
if _, prs := p.denylist[chunk.items[idx].Index()]; prs {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil {
|
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match.item != nil {
|
||||||
matches = append(matches, *match)
|
matches = append(matches, match)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -333,30 +392,29 @@ func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Re
|
|||||||
if _, prs := p.denylist[result.item.Index()]; prs {
|
if _, prs := p.denylist[result.item.Index()]; prs {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match != nil {
|
if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match.item != nil {
|
||||||
matches = append(matches, *match)
|
matches = append(matches, match)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return matches
|
return matches
|
||||||
}
|
}
|
||||||
|
|
||||||
// MatchItem returns true if the Item is a match
|
// MatchItem returns the match result if the Item is a match.
|
||||||
func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (*Result, []Offset, *[]int) {
|
// A zero-value Result (with item == nil) indicates no match.
|
||||||
|
func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (Result, []Offset, *[]int) {
|
||||||
if p.extended {
|
if p.extended {
|
||||||
if offsets, bonus, pos := p.extendedMatch(item, withPos, slab); len(offsets) == len(p.termSets) {
|
if offsets, bonus, pos := p.extendedMatch(item, withPos, slab); len(offsets) == len(p.termSets) {
|
||||||
result := buildResult(item, offsets, bonus)
|
return buildResult(item, offsets, bonus), offsets, pos
|
||||||
return &result, offsets, pos
|
|
||||||
}
|
}
|
||||||
return nil, nil, nil
|
return Result{}, nil, nil
|
||||||
}
|
}
|
||||||
offset, bonus, pos := p.basicMatch(item, withPos, slab)
|
offset, bonus, pos := p.basicMatch(item, withPos, slab)
|
||||||
if sidx := offset[0]; sidx >= 0 {
|
if sidx := offset[0]; sidx >= 0 {
|
||||||
offsets := []Offset{offset}
|
offsets := []Offset{offset}
|
||||||
result := buildResult(item, offsets, bonus)
|
return buildResult(item, offsets, bonus), offsets, pos
|
||||||
return &result, offsets, pos
|
|
||||||
}
|
}
|
||||||
return nil, nil, nil
|
return Result{}, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, *[]int) {
|
func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, *[]int) {
|
||||||
|
|||||||
+1
-1
@@ -68,7 +68,7 @@ func buildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
|
|||||||
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
|
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
|
||||||
return BuildPattern(NewChunkCache(), make(map[string]*Pattern),
|
return BuildPattern(NewChunkCache(), make(map[string]*Pattern),
|
||||||
fuzzy, fuzzyAlgo, extended, caseMode, normalize, forward,
|
fuzzy, fuzzyAlgo, extended, caseMode, normalize, forward,
|
||||||
withPos, cacheable, nth, delimiter, revision{}, runes, nil)
|
withPos, cacheable, nth, delimiter, revision{}, runes, nil, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExact(t *testing.T) {
|
func TestExact(t *testing.T) {
|
||||||
|
|||||||
+13
-12
@@ -7,6 +7,7 @@ import (
|
|||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -177,8 +178,8 @@ func (r *Reader) feed(src io.Reader) {
|
|||||||
var err error
|
var err error
|
||||||
for {
|
for {
|
||||||
n := 0
|
n := 0
|
||||||
scope := slab[:util.Min(len(slab), readerBufferSize)]
|
scope := slab[:min(len(slab), readerBufferSize)]
|
||||||
for i := 0; i < 100; i++ {
|
for range 100 {
|
||||||
n, err = src.Read(scope)
|
n, err = src.Read(scope)
|
||||||
if n > 0 || err != nil {
|
if n > 0 || err != nil {
|
||||||
break
|
break
|
||||||
@@ -302,21 +303,21 @@ func (r *Reader) readFiles(roots []string, opts walkerOpts, ignores []string) bo
|
|||||||
}
|
}
|
||||||
path = trimPath(path)
|
path = trimPath(path)
|
||||||
if path != "." {
|
if path != "." {
|
||||||
isDir := de.IsDir()
|
isDirSymlink := isSymlinkToDir(path, de)
|
||||||
if isDir || opts.follow && isSymlinkToDir(path, de) {
|
if isDirSymlink && !opts.follow {
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
isDir := de.IsDir() || isDirSymlink
|
||||||
|
if isDir {
|
||||||
base := filepath.Base(path)
|
base := filepath.Base(path)
|
||||||
if !opts.hidden && base[0] == '.' && base != ".." {
|
if !opts.hidden && base[0] == '.' && base != ".." {
|
||||||
return filepath.SkipDir
|
return filepath.SkipDir
|
||||||
}
|
}
|
||||||
for _, ignore := range ignoresBase {
|
if slices.Contains(ignoresBase, base) {
|
||||||
if ignore == base {
|
return filepath.SkipDir
|
||||||
return filepath.SkipDir
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for _, ignore := range ignoresFull {
|
if slices.Contains(ignoresFull, path) {
|
||||||
if ignore == path {
|
return filepath.SkipDir
|
||||||
return filepath.SkipDir
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for _, ignore := range ignoresSuffix {
|
for _, ignore := range ignoresSuffix {
|
||||||
if strings.HasSuffix(path, ignore) {
|
if strings.HasSuffix(path, ignore) {
|
||||||
|
|||||||
+102
-10
@@ -33,8 +33,6 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
|
|||||||
sort.Sort(ByOrder(offsets))
|
sort.Sort(ByOrder(offsets))
|
||||||
}
|
}
|
||||||
|
|
||||||
result := Result{item: item}
|
|
||||||
numChars := item.text.Length()
|
|
||||||
minBegin := math.MaxUint16
|
minBegin := math.MaxUint16
|
||||||
minEnd := math.MaxUint16
|
minEnd := math.MaxUint16
|
||||||
maxEnd := 0
|
maxEnd := 0
|
||||||
@@ -42,13 +40,21 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
|
|||||||
for _, offset := range offsets {
|
for _, offset := range offsets {
|
||||||
b, e := int(offset[0]), int(offset[1])
|
b, e := int(offset[0]), int(offset[1])
|
||||||
if b < e {
|
if b < e {
|
||||||
minBegin = util.Min(b, minBegin)
|
minBegin = min(b, minBegin)
|
||||||
minEnd = util.Min(e, minEnd)
|
minEnd = min(e, minEnd)
|
||||||
maxEnd = util.Max(e, maxEnd)
|
maxEnd = max(e, maxEnd)
|
||||||
validOffsetFound = true
|
validOffsetFound = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return buildResultFromBounds(item, score, minBegin, minEnd, maxEnd, validOffsetFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildResultFromBounds builds a Result from pre-computed offset bounds.
|
||||||
|
func buildResultFromBounds(item *Item, score int, minBegin, minEnd, maxEnd int, validOffsetFound bool) Result {
|
||||||
|
result := Result{item: item}
|
||||||
|
numChars := item.text.Length()
|
||||||
|
|
||||||
for idx, criterion := range sortCriteria {
|
for idx, criterion := range sortCriteria {
|
||||||
val := uint16(math.MaxUint16)
|
val := uint16(math.MaxUint16)
|
||||||
switch criterion {
|
switch criterion {
|
||||||
@@ -75,7 +81,6 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
|
|||||||
val = item.TrimLength()
|
val = item.TrimLength()
|
||||||
case byPathname:
|
case byPathname:
|
||||||
if validOffsetFound {
|
if validOffsetFound {
|
||||||
// lastDelim := strings.LastIndexByte(item.text.ToString(), '/')
|
|
||||||
lastDelim := -1
|
lastDelim := -1
|
||||||
s := item.text.ToString()
|
s := item.text.ToString()
|
||||||
for i := len(s) - 1; i >= 0; i-- {
|
for i := len(s) - 1; i >= 0; i-- {
|
||||||
@@ -91,7 +96,7 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
|
|||||||
case byBegin, byEnd:
|
case byBegin, byEnd:
|
||||||
if validOffsetFound {
|
if validOffsetFound {
|
||||||
whitePrefixLen := 0
|
whitePrefixLen := 0
|
||||||
for idx := 0; idx < numChars; idx++ {
|
for idx := range numChars {
|
||||||
r := item.text.Get(idx)
|
r := item.text.Get(idx)
|
||||||
whitePrefixLen = idx
|
whitePrefixLen = idx
|
||||||
if idx == minBegin || !unicode.IsSpace(r) {
|
if idx == minBegin || !unicode.IsSpace(r) {
|
||||||
@@ -123,7 +128,7 @@ func minRank() Result {
|
|||||||
return Result{item: &minItem, points: [4]uint16{math.MaxUint16, 0, 0, 0}}
|
return Result{item: &minItem, points: [4]uint16{math.MaxUint16, 0, 0, 0}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, theme *tui.ColorTheme, colBase tui.ColorPair, colMatch tui.ColorPair, attrNth tui.Attr) []colorOffset {
|
func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, theme *tui.ColorTheme, colBase tui.ColorPair, colMatch tui.ColorPair, attrNth tui.Attr, hidden bool) []colorOffset {
|
||||||
itemColors := result.item.Colors()
|
itemColors := result.item.Colors()
|
||||||
|
|
||||||
// No ANSI codes
|
// No ANSI codes
|
||||||
@@ -194,6 +199,10 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
|
|||||||
if !theme.Colored {
|
if !theme.Colored {
|
||||||
return tui.NewColorPair(-1, -1, ansi.color.attr).MergeAttr(base)
|
return tui.NewColorPair(-1, -1, ansi.color.attr).MergeAttr(base)
|
||||||
}
|
}
|
||||||
|
// fd --color always | fzf --ansi --delimiter / --nth -1 --color fg:dim:strip,nth:regular
|
||||||
|
if base.ShouldStripColors() {
|
||||||
|
return base
|
||||||
|
}
|
||||||
fg := ansi.color.fg
|
fg := ansi.color.fg
|
||||||
bg := ansi.color.bg
|
bg := ansi.color.bg
|
||||||
if fg == -1 {
|
if fg == -1 {
|
||||||
@@ -202,7 +211,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
|
|||||||
if bg == -1 {
|
if bg == -1 {
|
||||||
bg = colBase.Bg()
|
bg = colBase.Bg()
|
||||||
}
|
}
|
||||||
return tui.NewColorPair(fg, bg, ansi.color.attr).MergeAttr(base)
|
return tui.NewColorPair(fg, bg, ansi.color.attr).WithUl(ansi.color.ul).MergeAttr(base)
|
||||||
}
|
}
|
||||||
var colors []colorOffset
|
var colors []colorOffset
|
||||||
add := func(idx int) {
|
add := func(idx int) {
|
||||||
@@ -251,6 +260,9 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
|
|||||||
if curr.nth {
|
if curr.nth {
|
||||||
base = base.WithAttr(attrNth)
|
base = base.WithAttr(attrNth)
|
||||||
}
|
}
|
||||||
|
if hidden {
|
||||||
|
base = base.WithFg(theme.Nomatch)
|
||||||
|
}
|
||||||
color := ansiToColorPair(ansi, base)
|
color := ansiToColorPair(ansi, base)
|
||||||
colors = append(colors, colorOffset{
|
colors = append(colors, colorOffset{
|
||||||
offset: [2]int32{int32(start), int32(idx)},
|
offset: [2]int32{int32(start), int32(idx)},
|
||||||
@@ -258,9 +270,13 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
|
|||||||
match: false,
|
match: false,
|
||||||
url: ansi.color.url})
|
url: ansi.color.url})
|
||||||
} else {
|
} else {
|
||||||
|
color := colBase.WithAttr(attrNth)
|
||||||
|
if hidden {
|
||||||
|
color = color.WithFg(theme.Nomatch)
|
||||||
|
}
|
||||||
colors = append(colors, colorOffset{
|
colors = append(colors, colorOffset{
|
||||||
offset: [2]int32{int32(start), int32(idx)},
|
offset: [2]int32{int32(start), int32(idx)},
|
||||||
color: colBase.WithAttr(attrNth),
|
color: color,
|
||||||
match: false,
|
match: false,
|
||||||
url: nil})
|
url: nil})
|
||||||
}
|
}
|
||||||
@@ -323,3 +339,79 @@ func (a ByRelevanceTac) Swap(i, j int) {
|
|||||||
func (a ByRelevanceTac) Less(i, j int) bool {
|
func (a ByRelevanceTac) Less(i, j int) bool {
|
||||||
return compareRanks(a[i], a[j], true)
|
return compareRanks(a[i], a[j], true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// radixSortResults sorts Results by their points key using LSD radix sort.
|
||||||
|
// O(n) time complexity vs O(n log n) for comparison sort.
|
||||||
|
// The sort is stable, so equal-key items maintain original (item-index) order.
|
||||||
|
// For tac mode, runs of equal keys are reversed after sorting.
|
||||||
|
func radixSortResults(a []Result, tac bool, scratch []Result) []Result {
|
||||||
|
n := len(a)
|
||||||
|
if n < 128 {
|
||||||
|
if tac {
|
||||||
|
sort.Sort(ByRelevanceTac(a))
|
||||||
|
} else {
|
||||||
|
sort.Sort(ByRelevance(a))
|
||||||
|
}
|
||||||
|
return scratch[:0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if cap(scratch) < n {
|
||||||
|
scratch = make([]Result, n)
|
||||||
|
}
|
||||||
|
buf := scratch[:n]
|
||||||
|
src, dst := a, buf
|
||||||
|
scattered := 0
|
||||||
|
|
||||||
|
for pass := range 8 {
|
||||||
|
shift := uint(pass) * 8
|
||||||
|
|
||||||
|
var count [256]int
|
||||||
|
for i := range src {
|
||||||
|
count[byte(sortKey(&src[i])>>shift)]++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if all items have the same byte value at this position
|
||||||
|
if count[byte(sortKey(&src[0])>>shift)] == n {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var offset [256]int
|
||||||
|
for i := 1; i < 256; i++ {
|
||||||
|
offset[i] = offset[i-1] + count[i-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range src {
|
||||||
|
b := byte(sortKey(&src[i]) >> shift)
|
||||||
|
dst[offset[b]] = src[i]
|
||||||
|
offset[b]++
|
||||||
|
}
|
||||||
|
|
||||||
|
src, dst = dst, src
|
||||||
|
scattered++
|
||||||
|
}
|
||||||
|
|
||||||
|
// If odd number of scatters, data is in buf, copy back to a
|
||||||
|
if scattered%2 == 1 {
|
||||||
|
copy(a, src)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle tac: reverse runs of equal keys so equal-key items
|
||||||
|
// are in reverse item-index order
|
||||||
|
if tac {
|
||||||
|
i := 0
|
||||||
|
for i < n {
|
||||||
|
ki := sortKey(&a[i])
|
||||||
|
j := i + 1
|
||||||
|
for j < n && sortKey(&a[j]) == ki {
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
if j-i > 1 {
|
||||||
|
for l, r := i, j-1; l < r; l, r = l+1, r-1 {
|
||||||
|
a[l], a[r] = a[r], a[l]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i = j
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return scratch
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !386 && !amd64
|
//go:build !386 && !amd64 && !arm64
|
||||||
|
|
||||||
package fzf
|
package fzf
|
||||||
|
|
||||||
@@ -14,3 +14,7 @@ func compareRanks(irank Result, jrank Result, tac bool) bool {
|
|||||||
}
|
}
|
||||||
return (irank.item.Index() <= jrank.item.Index()) != tac
|
return (irank.item.Index() <= jrank.item.Index()) != tac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sortKey(r *Result) uint64 {
|
||||||
|
return uint64(r.points[0]) | uint64(r.points[1])<<16 | uint64(r.points[2])<<32 | uint64(r.points[3])<<48
|
||||||
|
}
|
||||||
|
|||||||
+64
-6
@@ -2,6 +2,7 @@ package fzf
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
|
"math/rand"
|
||||||
"sort"
|
"sort"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -124,14 +125,14 @@ func TestColorOffset(t *testing.T) {
|
|||||||
item := Result{
|
item := Result{
|
||||||
item: &Item{
|
item: &Item{
|
||||||
colors: &[]ansiOffset{
|
colors: &[]ansiOffset{
|
||||||
{[2]int32{0, 20}, ansiState{1, 5, 0, -1, nil}},
|
{[2]int32{0, 20}, ansiState{1, 5, -1, 0, -1, nil}},
|
||||||
{[2]int32{22, 27}, ansiState{2, 6, tui.Bold, -1, nil}},
|
{[2]int32{22, 27}, ansiState{2, 6, -1, tui.Bold, -1, nil}},
|
||||||
{[2]int32{30, 32}, ansiState{3, 7, 0, -1, nil}},
|
{[2]int32{30, 32}, ansiState{3, 7, -1, 0, -1, nil}},
|
||||||
{[2]int32{33, 40}, ansiState{4, 8, tui.Bold, -1, nil}}}}}
|
{[2]int32{33, 40}, ansiState{4, 8, -1, tui.Bold, -1, nil}}}}}
|
||||||
|
|
||||||
colBase := tui.NewColorPair(89, 189, tui.AttrUndefined)
|
colBase := tui.NewColorPair(89, 189, tui.AttrUndefined)
|
||||||
colMatch := tui.NewColorPair(99, 199, tui.AttrUndefined)
|
colMatch := tui.NewColorPair(99, 199, tui.AttrUndefined)
|
||||||
colors := item.colorOffsets(offsets, nil, tui.Dark256, colBase, colMatch, tui.AttrUndefined)
|
colors := item.colorOffsets(offsets, nil, tui.Dark256, colBase, colMatch, tui.AttrUndefined, false)
|
||||||
assert := func(idx int, b int32, e int32, c tui.ColorPair) {
|
assert := func(idx int, b int32, e int32, c tui.ColorPair) {
|
||||||
o := colors[idx]
|
o := colors[idx]
|
||||||
if o.offset[0] != b || o.offset[1] != e || o.color != c {
|
if o.offset[0] != b || o.offset[1] != e || o.color != c {
|
||||||
@@ -158,7 +159,7 @@ func TestColorOffset(t *testing.T) {
|
|||||||
|
|
||||||
nthOffsets := []Offset{{37, 39}, {42, 45}}
|
nthOffsets := []Offset{{37, 39}, {42, 45}}
|
||||||
for _, attr := range []tui.Attr{tui.AttrRegular, tui.StrikeThrough} {
|
for _, attr := range []tui.Attr{tui.AttrRegular, tui.StrikeThrough} {
|
||||||
colors = item.colorOffsets(offsets, nthOffsets, tui.Dark256, colRegular, colUnderline, attr)
|
colors = item.colorOffsets(offsets, nthOffsets, tui.Dark256, colRegular, colUnderline, attr, false)
|
||||||
|
|
||||||
// [{[0 5] {1 5 0}} {[5 15] {1 5 8}} {[15 20] {1 5 0}}
|
// [{[0 5] {1 5 0}} {[5 15] {1 5 8}} {[15 20] {1 5 0}}
|
||||||
// {[22 25] {2 6 1}} {[25 27] {2 6 9}} {[27 30] {-1 -1 8}}
|
// {[22 25] {2 6 1}} {[25 27] {2 6 9}} {[27 30] {-1 -1 8}}
|
||||||
@@ -182,3 +183,60 @@ func TestColorOffset(t *testing.T) {
|
|||||||
assert(11, 39, 40, tui.NewColorPair(4, 8, tui.Bold))
|
assert(11, 39, 40, tui.NewColorPair(4, 8, tui.Bold))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRadixSortResults(t *testing.T) {
|
||||||
|
sortCriteria = []criterion{byScore, byLength}
|
||||||
|
|
||||||
|
rng := rand.New(rand.NewSource(42))
|
||||||
|
|
||||||
|
for _, n := range []int{128, 256, 500, 1000} {
|
||||||
|
for _, tac := range []bool{false, true} {
|
||||||
|
// Build items with random points and indices
|
||||||
|
items := make([]*Item, n)
|
||||||
|
for i := range items {
|
||||||
|
items[i] = &Item{text: util.Chars{Index: int32(i)}}
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]Result, n)
|
||||||
|
for i := range results {
|
||||||
|
results[i] = Result{
|
||||||
|
item: items[i],
|
||||||
|
points: [4]uint16{
|
||||||
|
uint16(rng.Intn(256)),
|
||||||
|
uint16(rng.Intn(256)),
|
||||||
|
uint16(rng.Intn(256)),
|
||||||
|
uint16(rng.Intn(256)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make some duplicates to test stability
|
||||||
|
for i := 0; i < n/4; i++ {
|
||||||
|
j := rng.Intn(n)
|
||||||
|
k := rng.Intn(n)
|
||||||
|
results[j].points = results[k].points
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy for reference sort
|
||||||
|
expected := make([]Result, n)
|
||||||
|
copy(expected, results)
|
||||||
|
if tac {
|
||||||
|
sort.Sort(ByRelevanceTac(expected))
|
||||||
|
} else {
|
||||||
|
sort.Sort(ByRelevance(expected))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Radix sort
|
||||||
|
var scratch []Result
|
||||||
|
scratch = radixSortResults(results, tac, scratch)
|
||||||
|
|
||||||
|
for i := range results {
|
||||||
|
if results[i] != expected[i] {
|
||||||
|
t.Errorf("n=%d tac=%v: mismatch at index %d: got item %d, want item %d",
|
||||||
|
n, tac, i, results[i].item.Index(), expected[i].item.Index())
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+5
-1
@@ -1,4 +1,4 @@
|
|||||||
//go:build 386 || amd64
|
//go:build 386 || amd64 || arm64
|
||||||
|
|
||||||
package fzf
|
package fzf
|
||||||
|
|
||||||
@@ -14,3 +14,7 @@ func compareRanks(irank Result, jrank Result, tac bool) bool {
|
|||||||
}
|
}
|
||||||
return (irank.item.Index() <= jrank.item.Index()) != tac
|
return (irank.item.Index() <= jrank.item.Index()) != tac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sortKey(r *Result) uint64 {
|
||||||
|
return *(*uint64)(unsafe.Pointer(&r.points[0]))
|
||||||
|
}
|
||||||
|
|||||||
+59
-28
@@ -46,15 +46,20 @@ type httpServer struct {
|
|||||||
type listenAddress struct {
|
type listenAddress struct {
|
||||||
host string
|
host string
|
||||||
port int
|
port int
|
||||||
|
sock string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (addr listenAddress) IsLocal() bool {
|
func (addr listenAddress) IsLocal() bool {
|
||||||
return addr.host == "localhost" || addr.host == "127.0.0.1"
|
return addr.host == "localhost" || addr.host == "127.0.0.1" || len(addr.sock) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultListenAddr = listenAddress{"localhost", 0}
|
var defaultListenAddr = listenAddress{"localhost", 0, ""}
|
||||||
|
|
||||||
func parseListenAddress(address string) (listenAddress, error) {
|
func parseListenAddress(address string) (listenAddress, error) {
|
||||||
|
if strings.HasSuffix(address, ".sock") {
|
||||||
|
return listenAddress{"", 0, address}, nil
|
||||||
|
}
|
||||||
|
|
||||||
parts := strings.SplitN(address, ":", 3)
|
parts := strings.SplitN(address, ":", 3)
|
||||||
if len(parts) == 1 {
|
if len(parts) == 1 {
|
||||||
parts = []string{"localhost", parts[0]}
|
parts = []string{"localhost", parts[0]}
|
||||||
@@ -70,7 +75,7 @@ func parseListenAddress(address string) (listenAddress, error) {
|
|||||||
if len(parts[0]) == 0 {
|
if len(parts[0]) == 0 {
|
||||||
parts[0] = "localhost"
|
parts[0] = "localhost"
|
||||||
}
|
}
|
||||||
return listenAddress{parts[0], port}, nil
|
return listenAddress{parts[0], port, ""}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func startHttpServer(address listenAddress, actionChannel chan []*action, getHandler func(getParams) string) (net.Listener, int, error) {
|
func startHttpServer(address listenAddress, actionChannel chan []*action, getHandler func(getParams) string) (net.Listener, int, error) {
|
||||||
@@ -80,21 +85,40 @@ func startHttpServer(address listenAddress, actionChannel chan []*action, getHan
|
|||||||
if !address.IsLocal() && len(apiKey) == 0 {
|
if !address.IsLocal() && len(apiKey) == 0 {
|
||||||
return nil, port, errors.New("FZF_API_KEY is required to allow remote access")
|
return nil, port, errors.New("FZF_API_KEY is required to allow remote access")
|
||||||
}
|
}
|
||||||
addrStr := fmt.Sprintf("%s:%d", host, port)
|
|
||||||
listener, err := net.Listen("tcp", addrStr)
|
var listener net.Listener
|
||||||
if err != nil {
|
var err error
|
||||||
return nil, port, fmt.Errorf("failed to listen on %s", addrStr)
|
if len(address.sock) > 0 {
|
||||||
}
|
if _, err := os.Stat(address.sock); err == nil {
|
||||||
if port == 0 {
|
// Check if the socket is already in use
|
||||||
addr := listener.Addr().String()
|
if conn, err := net.Dial("unix", address.sock); err == nil {
|
||||||
parts := strings.Split(addr, ":")
|
conn.Close()
|
||||||
if len(parts) < 2 {
|
return nil, 0, fmt.Errorf("socket already in use: %s", address.sock)
|
||||||
return nil, port, fmt.Errorf("cannot extract port: %s", addr)
|
}
|
||||||
|
os.Remove(address.sock)
|
||||||
}
|
}
|
||||||
var err error
|
listener, err = net.Listen("unix", address.sock)
|
||||||
port, err = strconv.Atoi(parts[len(parts)-1])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, port, err
|
return nil, 0, fmt.Errorf("failed to listen on %s", address.sock)
|
||||||
|
}
|
||||||
|
os.Chmod(address.sock, 0600)
|
||||||
|
} else {
|
||||||
|
addrStr := fmt.Sprintf("%s:%d", host, port)
|
||||||
|
listener, err = net.Listen("tcp", addrStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, port, fmt.Errorf("failed to listen on %s", addrStr)
|
||||||
|
}
|
||||||
|
if port == 0 {
|
||||||
|
addr := listener.Addr().String()
|
||||||
|
parts := strings.Split(addr, ":")
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return nil, port, fmt.Errorf("cannot extract port: %s", addr)
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
port, err = strconv.Atoi(parts[len(parts)-1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, port, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,23 +183,22 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
|
|||||||
})
|
})
|
||||||
|
|
||||||
section := 0
|
section := 0
|
||||||
|
var getMatch []string
|
||||||
|
Loop:
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
text := scanner.Text()
|
text := scanner.Text()
|
||||||
switch section {
|
switch section {
|
||||||
case 0:
|
case 0: // Request line
|
||||||
getMatch := getRegex.FindStringSubmatch(text)
|
getMatch = getRegex.FindStringSubmatch(text)
|
||||||
if len(getMatch) > 0 {
|
if len(getMatch) == 0 && !strings.HasPrefix(text, "POST / HTTP") {
|
||||||
response := server.getHandler(parseGetParams(getMatch[1]))
|
|
||||||
if len(response) > 0 {
|
|
||||||
return good(response)
|
|
||||||
}
|
|
||||||
return answer(httpUnavailable+jsonContentType, `{"error":"timeout"}`)
|
|
||||||
} else if !strings.HasPrefix(text, "POST / HTTP") {
|
|
||||||
return bad("invalid request method")
|
return bad("invalid request method")
|
||||||
}
|
}
|
||||||
section++
|
section++
|
||||||
case 1:
|
case 1: // Request headers
|
||||||
if text == crlf {
|
if text == crlf { // End of headers
|
||||||
|
if len(getMatch) > 0 {
|
||||||
|
break Loop
|
||||||
|
}
|
||||||
if contentLength == 0 {
|
if contentLength == 0 {
|
||||||
return bad("content-length header missing")
|
return bad("content-length header missing")
|
||||||
}
|
}
|
||||||
@@ -195,7 +218,7 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
|
|||||||
apiKey = strings.TrimSpace(pair[1])
|
apiKey = strings.TrimSpace(pair[1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 2:
|
case 2: // Request body
|
||||||
body += text
|
body += text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,6 +227,14 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
|
|||||||
return unauthorized("invalid api key")
|
return unauthorized("invalid api key")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(getMatch) > 0 {
|
||||||
|
response := server.getHandler(parseGetParams(getMatch[1]))
|
||||||
|
if len(response) > 0 {
|
||||||
|
return good(response)
|
||||||
|
}
|
||||||
|
return answer(httpUnavailable+jsonContentType, `{"error":"timeout"}`)
|
||||||
|
}
|
||||||
|
|
||||||
if len(body) < contentLength {
|
if len(body) < contentLength {
|
||||||
return bad("incomplete request")
|
return bad("incomplete request")
|
||||||
}
|
}
|
||||||
|
|||||||
+1126
-455
File diff suppressed because it is too large
Load Diff
@@ -699,3 +699,72 @@ func readFile(path string) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWordWrapAnsiLine(t *testing.T) {
|
||||||
|
term := &Terminal{}
|
||||||
|
|
||||||
|
// Simple wrapping
|
||||||
|
result := term.wordWrapAnsiLine("hello world", 7, 2)
|
||||||
|
if len(result) != 2 || result[0] != "hello" || result[1] != "world" {
|
||||||
|
t.Errorf("Simple: %q", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// No wrapping needed
|
||||||
|
result = term.wordWrapAnsiLine("hello", 10, 2)
|
||||||
|
if len(result) != 1 || result[0] != "hello" {
|
||||||
|
t.Errorf("No wrap: %q", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ANSI codes preserved across split
|
||||||
|
result = term.wordWrapAnsiLine("\x1b[31mhello \x1b[32mworld", 8, 2)
|
||||||
|
if len(result) != 2 || result[0] != "\x1b[31mhello" || result[1] != "\x1b[32mworld" {
|
||||||
|
t.Errorf("ANSI: %q", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Long word (no space) — no break, let character wrapping handle it
|
||||||
|
result = term.wordWrapAnsiLine("abcdefghij", 5, 2)
|
||||||
|
if len(result) != 1 || result[0] != "abcdefghij" {
|
||||||
|
t.Errorf("Long word: %q", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiple words with continuation wrapSignWidth
|
||||||
|
result = term.wordWrapAnsiLine("aa bb cc dd", 5, 2)
|
||||||
|
// max=5 for first line, max=3 for continuations (5-2)
|
||||||
|
// "aa bb" (5 wide), split at second space -> "aa bb" | "cc" | "dd"
|
||||||
|
if len(result) != 3 || result[0] != "aa bb" || result[1] != "cc" || result[2] != "dd" {
|
||||||
|
t.Errorf("Multiple words: %q", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty string
|
||||||
|
result = term.wordWrapAnsiLine("", 10, 2)
|
||||||
|
if len(result) != 1 || result[0] != "" {
|
||||||
|
t.Errorf("Empty: %q", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OSC 8 hyperlink preserved
|
||||||
|
result = term.wordWrapAnsiLine("\x1b]8;;http://example.com\x1b\\click here\x1b]8;;\x1b\\", 8, 2)
|
||||||
|
if len(result) != 2 {
|
||||||
|
t.Errorf("Hyperlink split count: %d, %q", len(result), result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab handling: tab expands to tabstop-aligned width
|
||||||
|
term.tabstop = 8
|
||||||
|
// "\thi there" — tab at column 0 expands to 8, total "hi" starts at 8
|
||||||
|
// maxWidth=15: "\thi" = 10 wide, "there" = 5 wide, total 16 > 15, wrap at space
|
||||||
|
result = term.wordWrapAnsiLine("\thi there", 15, 2)
|
||||||
|
if len(result) != 2 || result[0] != "\thi" || result[1] != "there" {
|
||||||
|
t.Errorf("Tab: %q", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab as word boundary: "hello"(5) + tab(3→col8) + "world"(5) = 13 total
|
||||||
|
// maxWidth=13: fits without wrapping
|
||||||
|
result = term.wordWrapAnsiLine("hello\tworld", 13, 2)
|
||||||
|
if len(result) != 1 || result[0] != "hello\tworld" {
|
||||||
|
t.Errorf("Tab no wrap: %q", result)
|
||||||
|
}
|
||||||
|
// maxWidth=12: 13 > 12, wraps at tab
|
||||||
|
result = term.wordWrapAnsiLine("hello\tworld", 12, 2)
|
||||||
|
if len(result) != 2 || result[0] != "hello" || result[1] != "world" {
|
||||||
|
t.Errorf("Tab wrap: %q", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+21
-3
@@ -206,8 +206,9 @@ func Tokenize(text string, delimiter Delimiter) []Token {
|
|||||||
if delimiter.regex != nil {
|
if delimiter.regex != nil {
|
||||||
locs := delimiter.regex.FindAllStringIndex(text, -1)
|
locs := delimiter.regex.FindAllStringIndex(text, -1)
|
||||||
begin := 0
|
begin := 0
|
||||||
for _, loc := range locs {
|
tokens = make([]string, len(locs))
|
||||||
tokens = append(tokens, text[begin:loc[1]])
|
for i, loc := range locs {
|
||||||
|
tokens[i] = text[begin:loc[1]]
|
||||||
begin = loc[1]
|
begin = loc[1]
|
||||||
}
|
}
|
||||||
if begin < len(text) {
|
if begin < len(text) {
|
||||||
@@ -233,6 +234,23 @@ func StripLastDelimiter(str string, delimiter Delimiter) string {
|
|||||||
return strings.TrimRightFunc(str, unicode.IsSpace)
|
return strings.TrimRightFunc(str, unicode.IsSpace)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetLastDelimiter(str string, delimiter Delimiter) string {
|
||||||
|
if delimiter.str != nil {
|
||||||
|
if strings.HasSuffix(str, *delimiter.str) {
|
||||||
|
return *delimiter.str
|
||||||
|
}
|
||||||
|
} else if delimiter.regex != nil {
|
||||||
|
locs := delimiter.regex.FindAllStringIndex(str, -1)
|
||||||
|
if len(locs) > 0 {
|
||||||
|
lastLoc := locs[len(locs)-1]
|
||||||
|
if lastLoc[1] == len(str) {
|
||||||
|
return str[lastLoc[0]:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// JoinTokens concatenates the tokens into a single string
|
// JoinTokens concatenates the tokens into a single string
|
||||||
func JoinTokens(tokens []Token) string {
|
func JoinTokens(tokens []Token) string {
|
||||||
var output bytes.Buffer
|
var output bytes.Buffer
|
||||||
@@ -284,7 +302,7 @@ func Transform(tokens []Token, withNth []Range) []Token {
|
|||||||
end += numTokens + 1
|
end += numTokens + 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
minIdx = util.Max(0, begin-1)
|
minIdx = max(0, begin-1)
|
||||||
for idx := begin; idx <= end; idx++ {
|
for idx := begin; idx <= end; idx++ {
|
||||||
if idx >= 1 && idx <= numTokens {
|
if idx >= 1 && idx <= numTokens {
|
||||||
parts = append(parts, tokens[idx-1].text)
|
parts = append(parts, tokens[idx-1].text)
|
||||||
|
|||||||
+11
-28
@@ -2,30 +2,7 @@
|
|||||||
|
|
||||||
package tui
|
package tui
|
||||||
|
|
||||||
type Attr int32
|
|
||||||
|
|
||||||
func HasFullscreenRenderer() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
var DefaultBorderShape = BorderRounded
|
|
||||||
|
|
||||||
func (a Attr) Merge(b Attr) Attr {
|
|
||||||
if b&AttrRegular > 0 {
|
|
||||||
// Only keep bold attribute set by the system
|
|
||||||
return (b &^ AttrRegular) | (a & BoldForce)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (a &^ AttrRegular) | b
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AttrUndefined = Attr(0)
|
|
||||||
AttrRegular = Attr(1 << 8)
|
|
||||||
AttrClear = Attr(1 << 9)
|
|
||||||
BoldForce = Attr(1 << 10)
|
|
||||||
FullBg = Attr(1 << 11)
|
|
||||||
|
|
||||||
Bold = Attr(1)
|
Bold = Attr(1)
|
||||||
Dim = Attr(1 << 1)
|
Dim = Attr(1 << 1)
|
||||||
Italic = Attr(1 << 2)
|
Italic = Attr(1 << 2)
|
||||||
@@ -36,6 +13,12 @@ const (
|
|||||||
StrikeThrough = Attr(1 << 7)
|
StrikeThrough = Attr(1 << 7)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func HasFullscreenRenderer() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var DefaultBorderShape = BorderRounded
|
||||||
|
|
||||||
func (r *FullscreenRenderer) Init() error { return nil }
|
func (r *FullscreenRenderer) Init() error { return nil }
|
||||||
func (r *FullscreenRenderer) DefaultTheme() *ColorTheme { return nil }
|
func (r *FullscreenRenderer) DefaultTheme() *ColorTheme { return nil }
|
||||||
func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}
|
func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}
|
||||||
@@ -51,11 +34,11 @@ func (r *FullscreenRenderer) ShowCursor() {}
|
|||||||
func (r *FullscreenRenderer) Refresh() {}
|
func (r *FullscreenRenderer) Refresh() {}
|
||||||
func (r *FullscreenRenderer) Close() {}
|
func (r *FullscreenRenderer) Close() {}
|
||||||
func (r *FullscreenRenderer) Size() TermSize { return TermSize{} }
|
func (r *FullscreenRenderer) Size() TermSize { return TermSize{} }
|
||||||
|
func (r *FullscreenRenderer) Top() int { return 0 }
|
||||||
func (r *FullscreenRenderer) GetChar() Event { return Event{} }
|
func (r *FullscreenRenderer) MaxX() int { return 0 }
|
||||||
func (r *FullscreenRenderer) Top() int { return 0 }
|
func (r *FullscreenRenderer) MaxY() int { return 0 }
|
||||||
func (r *FullscreenRenderer) MaxX() int { return 0 }
|
func (r *FullscreenRenderer) GetChar(bool) Event { return Event{} }
|
||||||
func (r *FullscreenRenderer) MaxY() int { return 0 }
|
func (r *FullscreenRenderer) CancelGetChar() {}
|
||||||
|
|
||||||
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {}
|
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {}
|
||||||
|
|
||||||
|
|||||||
+107
-72
@@ -13,7 +13,6 @@ import (
|
|||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/junegunn/fzf/src/util"
|
"github.com/junegunn/fzf/src/util"
|
||||||
"github.com/rivo/uniseg"
|
|
||||||
|
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
@@ -26,6 +25,7 @@ const (
|
|||||||
escPollInterval = 5
|
escPollInterval = 5
|
||||||
offsetPollTries = 10
|
offsetPollTries = 10
|
||||||
maxInputBuffer = 1024 * 1024
|
maxInputBuffer = 1024 * 1024
|
||||||
|
maxSelectTries = 100
|
||||||
)
|
)
|
||||||
|
|
||||||
const DefaultTtyDevice string = "/dev/tty"
|
const DefaultTtyDevice string = "/dev/tty"
|
||||||
@@ -49,6 +49,18 @@ const DIM string = "\x1b[2m"
|
|||||||
const CR string = DIM + "␍"
|
const CR string = DIM + "␍"
|
||||||
const LF string = DIM + "␊"
|
const LF string = DIM + "␊"
|
||||||
|
|
||||||
|
type getCharResult int
|
||||||
|
|
||||||
|
const (
|
||||||
|
getCharSuccess getCharResult = iota
|
||||||
|
getCharError
|
||||||
|
getCharCancelled
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r getCharResult) ok() bool {
|
||||||
|
return r == getCharSuccess
|
||||||
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) stderrInternal(str string, allowNLCR bool, resetCode string) {
|
func (r *LightRenderer) stderrInternal(str string, allowNLCR bool, resetCode string) {
|
||||||
bytes := []byte(str)
|
bytes := []byte(str)
|
||||||
runes := []rune{}
|
runes := []rune{}
|
||||||
@@ -104,6 +116,7 @@ type LightRenderer struct {
|
|||||||
clicks [][2]int
|
clicks [][2]int
|
||||||
ttyin *os.File
|
ttyin *os.File
|
||||||
ttyout *os.File
|
ttyout *os.File
|
||||||
|
cancel func()
|
||||||
buffer []byte
|
buffer []byte
|
||||||
origState *term.State
|
origState *term.State
|
||||||
width int
|
width int
|
||||||
@@ -118,9 +131,9 @@ type LightRenderer struct {
|
|||||||
x int
|
x int
|
||||||
maxHeightFunc func(int) int
|
maxHeightFunc func(int) int
|
||||||
showCursor bool
|
showCursor bool
|
||||||
|
mutex sync.Mutex
|
||||||
|
|
||||||
// Windows only
|
// Windows only
|
||||||
mutex sync.Mutex
|
|
||||||
ttyinChannel chan byte
|
ttyinChannel chan byte
|
||||||
inHandle uintptr
|
inHandle uintptr
|
||||||
outHandle uintptr
|
outHandle uintptr
|
||||||
@@ -193,11 +206,6 @@ func (r *LightRenderer) Init() error {
|
|||||||
if r.fullscreen {
|
if r.fullscreen {
|
||||||
r.smcup()
|
r.smcup()
|
||||||
} else {
|
} else {
|
||||||
// We assume that --no-clear is used for repetitive relaunching of fzf.
|
|
||||||
// So we do not clear the lower bottom of the screen.
|
|
||||||
if r.clearOnExit {
|
|
||||||
r.csi("J")
|
|
||||||
}
|
|
||||||
y, x := r.findOffset()
|
y, x := r.findOffset()
|
||||||
r.mouse = r.mouse && y >= 0
|
r.mouse = r.mouse && y >= 0
|
||||||
// When --no-clear is used for repetitive relaunching, there is a small
|
// When --no-clear is used for repetitive relaunching, there is a small
|
||||||
@@ -208,6 +216,11 @@ func (r *LightRenderer) Init() error {
|
|||||||
r.upOneLine = true
|
r.upOneLine = true
|
||||||
r.makeSpace()
|
r.makeSpace()
|
||||||
}
|
}
|
||||||
|
// We assume that --no-clear is used for repetitive relaunching of fzf.
|
||||||
|
// So we do not clear the lower bottom of the screen.
|
||||||
|
if r.clearOnExit {
|
||||||
|
r.csi("J")
|
||||||
|
}
|
||||||
for i := 1; i < r.MaxY(); i++ {
|
for i := 1; i < r.MaxY(); i++ {
|
||||||
r.makeSpace()
|
r.makeSpace()
|
||||||
}
|
}
|
||||||
@@ -262,16 +275,18 @@ func getEnv(name string, defaultValue int) int {
|
|||||||
return atoi(env, defaultValue)
|
return atoi(env, defaultValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) getBytes() ([]byte, error) {
|
func (r *LightRenderer) getBytes(cancellable bool) ([]byte, getCharResult, error) {
|
||||||
bytes, err := r.getBytesInternal(r.buffer, false)
|
return r.getBytesInternal(cancellable, r.buffer, false)
|
||||||
return bytes, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) ([]byte, error) {
|
func (r *LightRenderer) getBytesInternal(cancellable bool, buffer []byte, nonblock bool) ([]byte, getCharResult, error) {
|
||||||
c, ok := r.getch(nonblock)
|
c, result := r.getch(cancellable, nonblock)
|
||||||
if !nonblock && !ok {
|
if result == getCharCancelled {
|
||||||
|
return buffer, getCharCancelled, nil
|
||||||
|
}
|
||||||
|
if !nonblock && !result.ok() {
|
||||||
r.Close()
|
r.Close()
|
||||||
return nil, errors.New("failed to read " + DefaultTtyDevice)
|
return nil, getCharError, errors.New("failed to read " + DefaultTtyDevice)
|
||||||
}
|
}
|
||||||
|
|
||||||
retries := 0
|
retries := 0
|
||||||
@@ -282,8 +297,8 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) ([]byte,
|
|||||||
|
|
||||||
pc := c
|
pc := c
|
||||||
for {
|
for {
|
||||||
c, ok = r.getch(true)
|
c, result = r.getch(false, true)
|
||||||
if !ok {
|
if !result.ok() {
|
||||||
if retries > 0 {
|
if retries > 0 {
|
||||||
retries--
|
retries--
|
||||||
time.Sleep(escPollInterval * time.Millisecond)
|
time.Sleep(escPollInterval * time.Millisecond)
|
||||||
@@ -302,20 +317,24 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) ([]byte,
|
|||||||
// so terminate fzf immediately.
|
// so terminate fzf immediately.
|
||||||
if len(buffer) > maxInputBuffer {
|
if len(buffer) > maxInputBuffer {
|
||||||
r.Close()
|
r.Close()
|
||||||
return nil, fmt.Errorf("input buffer overflow (%d): %v", len(buffer), buffer)
|
return nil, getCharError, fmt.Errorf("input buffer overflow (%d): %v", len(buffer), buffer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return buffer, nil
|
return buffer, getCharSuccess, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) GetChar() Event {
|
func (r *LightRenderer) GetChar(cancellable bool) Event {
|
||||||
var err error
|
var err error
|
||||||
|
var result getCharResult
|
||||||
if len(r.buffer) == 0 {
|
if len(r.buffer) == 0 {
|
||||||
r.buffer, err = r.getBytes()
|
r.buffer, result, err = r.getBytes(cancellable)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Event{Fatal, 0, nil}
|
return Event{Fatal, 0, nil}
|
||||||
}
|
}
|
||||||
|
if result == getCharCancelled {
|
||||||
|
return Event{Invalid, 0, nil}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if len(r.buffer) == 0 {
|
if len(r.buffer) == 0 {
|
||||||
return Event{Fatal, 0, nil}
|
return Event{Fatal, 0, nil}
|
||||||
@@ -351,9 +370,14 @@ func (r *LightRenderer) GetChar() Event {
|
|||||||
ev := r.escSequence(&sz)
|
ev := r.escSequence(&sz)
|
||||||
// Second chance
|
// Second chance
|
||||||
if ev.Type == Invalid {
|
if ev.Type == Invalid {
|
||||||
if r.buffer, err = r.getBytes(); err != nil {
|
r.buffer, result, err = r.getBytes(true)
|
||||||
|
if err != nil {
|
||||||
return Event{Fatal, 0, nil}
|
return Event{Fatal, 0, nil}
|
||||||
}
|
}
|
||||||
|
if result == getCharCancelled {
|
||||||
|
return Event{Invalid, 0, nil}
|
||||||
|
}
|
||||||
|
|
||||||
ev = r.escSequence(&sz)
|
ev = r.escSequence(&sz)
|
||||||
}
|
}
|
||||||
return ev
|
return ev
|
||||||
@@ -371,6 +395,21 @@ func (r *LightRenderer) GetChar() Event {
|
|||||||
return Event{Rune, char, nil}
|
return Event{Rune, char, nil}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *LightRenderer) CancelGetChar() {
|
||||||
|
r.mutex.Lock()
|
||||||
|
if r.cancel != nil {
|
||||||
|
r.cancel()
|
||||||
|
r.cancel = nil
|
||||||
|
}
|
||||||
|
r.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LightRenderer) setCancel(f func()) {
|
||||||
|
r.mutex.Lock()
|
||||||
|
r.cancel = f
|
||||||
|
r.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) escSequence(sz *int) Event {
|
func (r *LightRenderer) escSequence(sz *int) Event {
|
||||||
if len(r.buffer) < 2 {
|
if len(r.buffer) < 2 {
|
||||||
return Event{Esc, 0, nil}
|
return Event{Esc, 0, nil}
|
||||||
@@ -479,6 +518,7 @@ func (r *LightRenderer) escSequence(sz *int) Event {
|
|||||||
return Event{Delete, 0, nil}
|
return Event{Delete, 0, nil}
|
||||||
}
|
}
|
||||||
if len(r.buffer) == 7 && r.buffer[6] == '~' && r.buffer[4] == '1' {
|
if len(r.buffer) == 7 && r.buffer[6] == '~' && r.buffer[4] == '1' {
|
||||||
|
*sz = 7
|
||||||
switch r.buffer[5] {
|
switch r.buffer[5] {
|
||||||
case '0':
|
case '0':
|
||||||
return Event{AltShiftDelete, 0, nil}
|
return Event{AltShiftDelete, 0, nil}
|
||||||
@@ -525,6 +565,7 @@ func (r *LightRenderer) escSequence(sz *int) Event {
|
|||||||
return Event{PageUp, 0, nil}
|
return Event{PageUp, 0, nil}
|
||||||
}
|
}
|
||||||
if len(r.buffer) == 7 && r.buffer[6] == '~' && r.buffer[4] == '1' {
|
if len(r.buffer) == 7 && r.buffer[6] == '~' && r.buffer[4] == '1' {
|
||||||
|
*sz = 7
|
||||||
switch r.buffer[5] {
|
switch r.buffer[5] {
|
||||||
case '0':
|
case '0':
|
||||||
return Event{AltShiftPageUp, 0, nil}
|
return Event{AltShiftPageUp, 0, nil}
|
||||||
@@ -569,6 +610,7 @@ func (r *LightRenderer) escSequence(sz *int) Event {
|
|||||||
return Event{PageDown, 0, nil}
|
return Event{PageDown, 0, nil}
|
||||||
}
|
}
|
||||||
if len(r.buffer) == 7 && r.buffer[6] == '~' && r.buffer[4] == '1' {
|
if len(r.buffer) == 7 && r.buffer[6] == '~' && r.buffer[4] == '1' {
|
||||||
|
*sz = 7
|
||||||
switch r.buffer[5] {
|
switch r.buffer[5] {
|
||||||
case '0':
|
case '0':
|
||||||
return Event{AltShiftPageDown, 0, nil}
|
return Event{AltShiftPageDown, 0, nil}
|
||||||
@@ -1030,8 +1072,8 @@ func (r *LightRenderer) MaxY() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) NewWindow(top int, left int, width int, height int, windowType WindowType, borderStyle BorderStyle, erase bool) Window {
|
func (r *LightRenderer) NewWindow(top int, left int, width int, height int, windowType WindowType, borderStyle BorderStyle, erase bool) Window {
|
||||||
width = util.Max(0, width)
|
width = max(0, width)
|
||||||
height = util.Max(0, height)
|
height = max(0, height)
|
||||||
w := &LightWindow{
|
w := &LightWindow{
|
||||||
renderer: r,
|
renderer: r,
|
||||||
colored: r.theme.Colored,
|
colored: r.theme.Colored,
|
||||||
@@ -1280,7 +1322,18 @@ func attrCodes(attr Attr) []string {
|
|||||||
codes = append(codes, "3")
|
codes = append(codes, "3")
|
||||||
}
|
}
|
||||||
if (attr & Underline) > 0 {
|
if (attr & Underline) > 0 {
|
||||||
codes = append(codes, "4")
|
switch attr.UnderlineStyle() {
|
||||||
|
case UlStyleDouble:
|
||||||
|
codes = append(codes, "4:2")
|
||||||
|
case UlStyleCurly:
|
||||||
|
codes = append(codes, "4:3")
|
||||||
|
case UlStyleDotted:
|
||||||
|
codes = append(codes, "4:4")
|
||||||
|
case UlStyleDashed:
|
||||||
|
codes = append(codes, "4:5")
|
||||||
|
default:
|
||||||
|
codes = append(codes, "4")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (attr & Blink) > 0 {
|
if (attr & Blink) > 0 {
|
||||||
codes = append(codes, "5")
|
codes = append(codes, "5")
|
||||||
@@ -1318,8 +1371,27 @@ func colorCodes(fg Color, bg Color) []string {
|
|||||||
return codes
|
return codes
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *LightWindow) csiColor(fg Color, bg Color, attr Attr) (bool, string) {
|
func ulColorCode(c Color) string {
|
||||||
|
if c == colDefault {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if c.is24() {
|
||||||
|
r := (c >> 16) & 0xff
|
||||||
|
g := (c >> 8) & 0xff
|
||||||
|
b := (c) & 0xff
|
||||||
|
return fmt.Sprintf("58;2;%d;%d;%d", r, g, b)
|
||||||
|
}
|
||||||
|
if c >= 0 && c < 256 {
|
||||||
|
return fmt.Sprintf("58;5;%d", c)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *LightWindow) csiColor(fg Color, bg Color, ul Color, attr Attr) (bool, string) {
|
||||||
codes := append(attrCodes(attr), colorCodes(fg, bg)...)
|
codes := append(attrCodes(attr), colorCodes(fg, bg)...)
|
||||||
|
if ulCode := ulColorCode(ul); ulCode != "" {
|
||||||
|
codes = append(codes, ulCode)
|
||||||
|
}
|
||||||
code := w.csi(";" + strings.Join(codes, ";") + "m")
|
code := w.csi(";" + strings.Join(codes, ";") + "m")
|
||||||
return len(codes) > 0, code
|
return len(codes) > 0, code
|
||||||
}
|
}
|
||||||
@@ -1333,65 +1405,28 @@ func cleanse(str string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *LightWindow) CPrint(pair ColorPair, text string) {
|
func (w *LightWindow) CPrint(pair ColorPair, text string) {
|
||||||
_, code := w.csiColor(pair.Fg(), pair.Bg(), pair.Attr())
|
_, code := w.csiColor(pair.Fg(), pair.Bg(), pair.Ul(), pair.Attr())
|
||||||
w.stderrInternal(cleanse(text), false, code)
|
w.stderrInternal(cleanse(text), false, code)
|
||||||
w.csi("0m")
|
w.csi("0m")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) {
|
func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) {
|
||||||
hasColors, code := w.csiColor(fg, bg, attr)
|
hasColors, code := w.csiColor(fg, bg, colDefault, attr)
|
||||||
if hasColors {
|
if hasColors {
|
||||||
defer w.csi("0m")
|
defer w.csi("0m")
|
||||||
}
|
}
|
||||||
w.stderrInternal(cleanse(text), false, code)
|
w.stderrInternal(cleanse(text), false, code)
|
||||||
}
|
}
|
||||||
|
|
||||||
type wrappedLine struct {
|
|
||||||
text string
|
|
||||||
displayWidth int
|
|
||||||
}
|
|
||||||
|
|
||||||
func wrapLine(input string, prefixLength int, initialMax int, tabstop int, wrapSignWidth int) []wrappedLine {
|
|
||||||
lines := []wrappedLine{}
|
|
||||||
width := 0
|
|
||||||
line := ""
|
|
||||||
gr := uniseg.NewGraphemes(input)
|
|
||||||
max := initialMax
|
|
||||||
for gr.Next() {
|
|
||||||
rs := gr.Runes()
|
|
||||||
str := string(rs)
|
|
||||||
var w int
|
|
||||||
if len(rs) == 1 && rs[0] == '\t' {
|
|
||||||
w = tabstop - (prefixLength+width)%tabstop
|
|
||||||
str = repeat(' ', w)
|
|
||||||
} else if rs[0] == '\r' {
|
|
||||||
w++
|
|
||||||
} else {
|
|
||||||
w = uniseg.StringWidth(str)
|
|
||||||
}
|
|
||||||
width += w
|
|
||||||
|
|
||||||
if prefixLength+width <= max {
|
|
||||||
line += str
|
|
||||||
} else {
|
|
||||||
lines = append(lines, wrappedLine{string(line), width - w})
|
|
||||||
line = str
|
|
||||||
prefixLength = 0
|
|
||||||
width = w
|
|
||||||
max = initialMax - wrapSignWidth
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lines = append(lines, wrappedLine{string(line), width})
|
|
||||||
return lines
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *LightWindow) fill(str string, resetCode string) FillReturn {
|
func (w *LightWindow) fill(str string, resetCode string) FillReturn {
|
||||||
allLines := strings.Split(str, "\n")
|
allLines := strings.Split(str, "\n")
|
||||||
for i, line := range allLines {
|
for i, line := range allLines {
|
||||||
lines := wrapLine(line, w.posx, w.width, w.tabstop, w.wrapSignWidth)
|
lines := WrapLine(line, w.posx, w.width, w.tabstop, w.wrapSignWidth)
|
||||||
for j, wl := range lines {
|
for j, wl := range lines {
|
||||||
w.stderrInternal(wl.text, false, resetCode)
|
if w.posx < w.width {
|
||||||
w.posx += wl.displayWidth
|
w.stderrInternal(wl.Text, false, resetCode)
|
||||||
|
w.posx += wl.DisplayWidth
|
||||||
|
}
|
||||||
|
|
||||||
// Wrap line
|
// Wrap line
|
||||||
if j < len(lines)-1 || i < len(allLines)-1 {
|
if j < len(lines)-1 || i < len(allLines)-1 {
|
||||||
@@ -1429,7 +1464,7 @@ func (w *LightWindow) fill(str string, resetCode string) FillReturn {
|
|||||||
|
|
||||||
func (w *LightWindow) setBg() string {
|
func (w *LightWindow) setBg() string {
|
||||||
if w.bg != colDefault {
|
if w.bg != colDefault {
|
||||||
_, code := w.csiColor(colDefault, w.bg, AttrRegular)
|
_, code := w.csiColor(colDefault, w.bg, colDefault, AttrRegular)
|
||||||
return code
|
return code
|
||||||
}
|
}
|
||||||
// Should clear dim attribute after ␍ in the preview window
|
// Should clear dim attribute after ␍ in the preview window
|
||||||
@@ -1451,7 +1486,7 @@ func (w *LightWindow) Fill(text string) FillReturn {
|
|||||||
return w.fill(text, code)
|
return w.fill(text, code)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillReturn {
|
func (w *LightWindow) CFill(fg Color, bg Color, ul Color, attr Attr, text string) FillReturn {
|
||||||
w.Move(w.posy, w.posx)
|
w.Move(w.posy, w.posx)
|
||||||
if fg == colDefault {
|
if fg == colDefault {
|
||||||
fg = w.fg
|
fg = w.fg
|
||||||
@@ -1459,7 +1494,7 @@ func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillRetu
|
|||||||
if bg == colDefault {
|
if bg == colDefault {
|
||||||
bg = w.bg
|
bg = w.bg
|
||||||
}
|
}
|
||||||
if hasColors, resetCode := w.csiColor(fg, bg, attr); hasColors {
|
if hasColors, resetCode := w.csiColor(fg, bg, ul, attr); hasColors {
|
||||||
defer w.csi("0m")
|
defer w.csi("0m")
|
||||||
return w.fill(text, resetCode)
|
return w.fill(text, resetCode)
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-1
@@ -15,10 +15,27 @@ func TestLightRenderer(t *testing.T) {
|
|||||||
|
|
||||||
light_renderer := renderer.(*LightRenderer)
|
light_renderer := renderer.(*LightRenderer)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
light_renderer.mutex.Lock()
|
||||||
|
ready := light_renderer.cancel != nil
|
||||||
|
light_renderer.mutex.Unlock()
|
||||||
|
|
||||||
|
if ready {
|
||||||
|
light_renderer.CancelGetChar()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
event := light_renderer.GetChar(true)
|
||||||
|
if event.Type != Invalid {
|
||||||
|
t.Error("Not cancelled")
|
||||||
|
}
|
||||||
|
|
||||||
assertCharSequence := func(sequence string, name string) {
|
assertCharSequence := func(sequence string, name string) {
|
||||||
bytes := []byte(sequence)
|
bytes := []byte(sequence)
|
||||||
light_renderer.buffer = bytes
|
light_renderer.buffer = bytes
|
||||||
event := light_renderer.GetChar()
|
event := light_renderer.GetChar(true)
|
||||||
if event.KeyName() != name {
|
if event.KeyName() != name {
|
||||||
t.Errorf(
|
t.Errorf(
|
||||||
"sequence: %q | %v | '%s' (%s) != %s",
|
"sequence: %q | %v | '%s' (%s) != %s",
|
||||||
|
|||||||
+56
-9
@@ -98,8 +98,8 @@ func (r *LightRenderer) findOffset() (row int, col int) {
|
|||||||
r.flush()
|
r.flush()
|
||||||
var err error
|
var err error
|
||||||
bytes := []byte{}
|
bytes := []byte{}
|
||||||
for tries := 0; tries < offsetPollTries; tries++ {
|
for tries := range offsetPollTries {
|
||||||
bytes, err = r.getBytesInternal(bytes, tries > 0)
|
bytes, _, err = r.getBytesInternal(false, bytes, tries > 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return -1, -1
|
return -1, -1
|
||||||
}
|
}
|
||||||
@@ -114,15 +114,62 @@ func (r *LightRenderer) findOffset() (row int, col int) {
|
|||||||
return -1, -1
|
return -1, -1
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) getch(nonblock bool) (int, bool) {
|
func (r *LightRenderer) getch(cancellable bool, nonblock bool) (int, getCharResult) {
|
||||||
b := make([]byte, 1)
|
|
||||||
fd := r.fd()
|
fd := r.fd()
|
||||||
util.SetNonblock(r.ttyin, nonblock)
|
getter := func() (int, getCharResult) {
|
||||||
_, err := util.Read(fd, b)
|
b := make([]byte, 1)
|
||||||
if err != nil {
|
util.SetNonblock(r.ttyin, nonblock)
|
||||||
return 0, false
|
_, err := util.Read(fd, b)
|
||||||
|
if err != nil {
|
||||||
|
return 0, getCharError
|
||||||
|
}
|
||||||
|
return int(b[0]), getCharSuccess
|
||||||
}
|
}
|
||||||
return int(b[0]), true
|
if nonblock || !cancellable {
|
||||||
|
return getter()
|
||||||
|
}
|
||||||
|
|
||||||
|
rpipe, wpipe, err := os.Pipe()
|
||||||
|
if err != nil {
|
||||||
|
// Fallback to blocking read without cancellation
|
||||||
|
return getter()
|
||||||
|
}
|
||||||
|
r.setCancel(func() {
|
||||||
|
wpipe.Write([]byte{0})
|
||||||
|
})
|
||||||
|
defer func() {
|
||||||
|
r.setCancel(nil)
|
||||||
|
rpipe.Close()
|
||||||
|
wpipe.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
cancelFd := int(rpipe.Fd())
|
||||||
|
for range maxSelectTries {
|
||||||
|
var rfds unix.FdSet
|
||||||
|
limit := len(rfds.Bits) * unix.NFDBITS
|
||||||
|
if fd >= limit || cancelFd >= limit {
|
||||||
|
return getter()
|
||||||
|
}
|
||||||
|
|
||||||
|
rfds.Set(fd)
|
||||||
|
rfds.Set(cancelFd)
|
||||||
|
_, err := unix.Select(max(fd, cancelFd)+1, &rfds, nil, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
if err == syscall.EINTR {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return 0, getCharError
|
||||||
|
}
|
||||||
|
|
||||||
|
if rfds.IsSet(cancelFd) {
|
||||||
|
return 0, getCharCancelled
|
||||||
|
}
|
||||||
|
|
||||||
|
if rfds.IsSet(fd) {
|
||||||
|
return getter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, getCharError
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) Size() TermSize {
|
func (r *LightRenderer) Size() TermSize {
|
||||||
|
|||||||
+27
-10
@@ -151,16 +151,33 @@ func (r *LightRenderer) findOffset() (row int, col int) {
|
|||||||
return int(bufferInfo.CursorPosition.Y), int(bufferInfo.CursorPosition.X)
|
return int(bufferInfo.CursorPosition.Y), int(bufferInfo.CursorPosition.X)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) getch(nonblock bool) (int, bool) {
|
func (r *LightRenderer) getch(cancellable bool, nonblock bool) (int, getCharResult) {
|
||||||
if nonblock {
|
if !nonblock && !cancellable {
|
||||||
select {
|
|
||||||
case bc := <-r.ttyinChannel:
|
|
||||||
return int(bc), true
|
|
||||||
case <-time.After(timeoutInterval * time.Millisecond):
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
bc := <-r.ttyinChannel
|
bc := <-r.ttyinChannel
|
||||||
return int(bc), true
|
return int(bc), getCharSuccess
|
||||||
|
}
|
||||||
|
|
||||||
|
var timeout <-chan time.Time
|
||||||
|
if nonblock {
|
||||||
|
timeout = time.After(timeoutInterval * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cancel chan struct{}
|
||||||
|
if cancellable {
|
||||||
|
cancel = make(chan struct{})
|
||||||
|
r.setCancel(func() {
|
||||||
|
close(cancel)
|
||||||
|
})
|
||||||
|
defer r.setCancel(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case bc := <-r.ttyinChannel:
|
||||||
|
return int(bc), getCharSuccess
|
||||||
|
case <-cancel:
|
||||||
|
return 0, getCharCancelled
|
||||||
|
case <-timeout:
|
||||||
|
// NOTE: not really an error
|
||||||
|
return 0, getCharError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+94
-77
@@ -5,6 +5,7 @@ package tui
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gdamore/tcell/v2"
|
"github.com/gdamore/tcell/v2"
|
||||||
@@ -36,8 +37,6 @@ func (p ColorPair) style() tcell.Style {
|
|||||||
return style.Foreground(asTcellColor(p.Fg())).Background(asTcellColor(p.Bg()))
|
return style.Foreground(asTcellColor(p.Fg())).Background(asTcellColor(p.Bg()))
|
||||||
}
|
}
|
||||||
|
|
||||||
type Attr int32
|
|
||||||
|
|
||||||
type TcellWindow struct {
|
type TcellWindow struct {
|
||||||
color bool
|
color bool
|
||||||
windowType WindowType
|
windowType WindowType
|
||||||
@@ -55,6 +54,7 @@ type TcellWindow struct {
|
|||||||
showCursor bool
|
showCursor bool
|
||||||
wrapSign string
|
wrapSign string
|
||||||
wrapSignWidth int
|
wrapSignWidth int
|
||||||
|
tabstop int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *TcellWindow) Top() int {
|
func (w *TcellWindow) Top() int {
|
||||||
@@ -98,14 +98,6 @@ const (
|
|||||||
Italic = Attr(tcell.AttrItalic)
|
Italic = Attr(tcell.AttrItalic)
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
AttrUndefined = Attr(0)
|
|
||||||
AttrRegular = Attr(1 << 7)
|
|
||||||
AttrClear = Attr(1 << 8)
|
|
||||||
BoldForce = Attr(1 << 10)
|
|
||||||
FullBg = Attr(1 << 11)
|
|
||||||
)
|
|
||||||
|
|
||||||
func (r *FullscreenRenderer) Bell() {
|
func (r *FullscreenRenderer) Bell() {
|
||||||
_screen.Beep()
|
_screen.Beep()
|
||||||
}
|
}
|
||||||
@@ -159,15 +151,6 @@ func (c Color) Style() tcell.Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a Attr) Merge(b Attr) Attr {
|
|
||||||
if b&AttrRegular > 0 {
|
|
||||||
// Only keep bold attribute set by the system
|
|
||||||
return (b &^ AttrRegular) | (a & BoldForce)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (a &^ AttrRegular) | b
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle the following as private members of FullscreenRenderer instance
|
// handle the following as private members of FullscreenRenderer instance
|
||||||
// they are declared here to prevent introducing tcell library in non-windows builds
|
// they are declared here to prevent introducing tcell library in non-windows builds
|
||||||
var (
|
var (
|
||||||
@@ -265,7 +248,7 @@ func (r *FullscreenRenderer) Size() TermSize {
|
|||||||
return TermSize{lines, cols, 0, 0}
|
return TermSize{lines, cols, 0, 0}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *FullscreenRenderer) GetChar() Event {
|
func (r *FullscreenRenderer) GetChar(cancellable bool) Event {
|
||||||
ev := _screen.PollEvent()
|
ev := _screen.PollEvent()
|
||||||
switch ev := ev.(type) {
|
switch ev := ev.(type) {
|
||||||
case *tcell.EventPaste:
|
case *tcell.EventPaste:
|
||||||
@@ -722,15 +705,19 @@ func (r *FullscreenRenderer) GetChar() Event {
|
|||||||
return Event{Invalid, 0, nil}
|
return Event{Invalid, 0, nil}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *FullscreenRenderer) CancelGetChar() {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
func (r *FullscreenRenderer) Pause(clear bool) {
|
func (r *FullscreenRenderer) Pause(clear bool) {
|
||||||
if clear {
|
if clear {
|
||||||
r.Close()
|
_screen.Suspend()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *FullscreenRenderer) Resume(clear bool, sigcont bool) {
|
func (r *FullscreenRenderer) Resume(clear bool, sigcont bool) {
|
||||||
if clear {
|
if clear {
|
||||||
r.initScreen()
|
_screen.Resume()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -748,8 +735,8 @@ func (r *FullscreenRenderer) RefreshWindows(windows []Window) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, windowType WindowType, borderStyle BorderStyle, erase bool) Window {
|
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, windowType WindowType, borderStyle BorderStyle, erase bool) Window {
|
||||||
width = util.Max(0, width)
|
width = max(0, width)
|
||||||
height = util.Max(0, height)
|
height = max(0, height)
|
||||||
normal := ColBorder
|
normal := ColBorder
|
||||||
switch windowType {
|
switch windowType {
|
||||||
case WindowList:
|
case WindowList:
|
||||||
@@ -772,7 +759,8 @@ func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int,
|
|||||||
height: height,
|
height: height,
|
||||||
normal: normal,
|
normal: normal,
|
||||||
borderStyle: borderStyle,
|
borderStyle: borderStyle,
|
||||||
showCursor: r.showCursor}
|
showCursor: r.showCursor,
|
||||||
|
tabstop: r.tabstop}
|
||||||
w.Erase()
|
w.Erase()
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
@@ -840,6 +828,21 @@ func (w *TcellWindow) withUrl(style tcell.Style) tcell.Style {
|
|||||||
return style
|
return style
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func underlineStyleFromAttr(a Attr) tcell.UnderlineStyle {
|
||||||
|
switch a.UnderlineStyle() {
|
||||||
|
case UlStyleDouble:
|
||||||
|
return tcell.UnderlineStyleDouble
|
||||||
|
case UlStyleCurly:
|
||||||
|
return tcell.UnderlineStyleCurly
|
||||||
|
case UlStyleDotted:
|
||||||
|
return tcell.UnderlineStyleDotted
|
||||||
|
case UlStyleDashed:
|
||||||
|
return tcell.UnderlineStyleDashed
|
||||||
|
default:
|
||||||
|
return tcell.UnderlineStyleSolid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (w *TcellWindow) printString(text string, pair ColorPair) {
|
func (w *TcellWindow) printString(text string, pair ColorPair) {
|
||||||
lx := 0
|
lx := 0
|
||||||
a := pair.Attr()
|
a := pair.Attr()
|
||||||
@@ -848,11 +851,18 @@ func (w *TcellWindow) printString(text string, pair ColorPair) {
|
|||||||
if a&AttrClear == 0 {
|
if a&AttrClear == 0 {
|
||||||
style = style.
|
style = style.
|
||||||
Reverse(a&Attr(tcell.AttrReverse) != 0).
|
Reverse(a&Attr(tcell.AttrReverse) != 0).
|
||||||
Underline(a&Attr(tcell.AttrUnderline) != 0).
|
|
||||||
StrikeThrough(a&Attr(tcell.AttrStrikeThrough) != 0).
|
StrikeThrough(a&Attr(tcell.AttrStrikeThrough) != 0).
|
||||||
Italic(a&Attr(tcell.AttrItalic) != 0).
|
Italic(a&Attr(tcell.AttrItalic) != 0).
|
||||||
Blink(a&Attr(tcell.AttrBlink) != 0).
|
Blink(a&Attr(tcell.AttrBlink) != 0).
|
||||||
Dim(a&Attr(tcell.AttrDim) != 0)
|
Dim(a&Attr(tcell.AttrDim) != 0)
|
||||||
|
if a&Attr(tcell.AttrUnderline) != 0 {
|
||||||
|
style = style.Underline(underlineStyleFromAttr(a))
|
||||||
|
if pair.Ul() != colDefault {
|
||||||
|
style = style.Underline(asTcellColor(pair.Ul()))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
style = style.Underline(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
style = w.withUrl(style)
|
style = w.withUrl(style)
|
||||||
|
|
||||||
@@ -887,10 +897,8 @@ func (w *TcellWindow) CPrint(pair ColorPair, text string) {
|
|||||||
w.printString(text, pair)
|
w.printString(text, pair)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
|
func (w *TcellWindow) pairStyle(pair ColorPair) tcell.Style {
|
||||||
lx := 0
|
|
||||||
a := pair.Attr()
|
a := pair.Attr()
|
||||||
|
|
||||||
var style tcell.Style
|
var style tcell.Style
|
||||||
if w.color {
|
if w.color {
|
||||||
style = pair.style()
|
style = pair.style()
|
||||||
@@ -902,64 +910,73 @@ func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
|
|||||||
Bold(a&Attr(tcell.AttrBold) != 0 || a&BoldForce != 0).
|
Bold(a&Attr(tcell.AttrBold) != 0 || a&BoldForce != 0).
|
||||||
Dim(a&Attr(tcell.AttrDim) != 0).
|
Dim(a&Attr(tcell.AttrDim) != 0).
|
||||||
Reverse(a&Attr(tcell.AttrReverse) != 0).
|
Reverse(a&Attr(tcell.AttrReverse) != 0).
|
||||||
Underline(a&Attr(tcell.AttrUnderline) != 0).
|
|
||||||
StrikeThrough(a&Attr(tcell.AttrStrikeThrough) != 0).
|
StrikeThrough(a&Attr(tcell.AttrStrikeThrough) != 0).
|
||||||
Italic(a&Attr(tcell.AttrItalic) != 0)
|
Italic(a&Attr(tcell.AttrItalic) != 0)
|
||||||
style = w.withUrl(style)
|
if a&Attr(tcell.AttrUnderline) != 0 {
|
||||||
|
style = style.Underline(underlineStyleFromAttr(a))
|
||||||
|
if pair.Ul() != colDefault {
|
||||||
|
style = style.Underline(asTcellColor(pair.Ul()))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
style = style.Underline(false)
|
||||||
|
}
|
||||||
|
return w.withUrl(style)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *TcellWindow) renderGraphemes(text string, style tcell.Style) {
|
||||||
gr := uniseg.NewGraphemes(text)
|
gr := uniseg.NewGraphemes(text)
|
||||||
Loop:
|
|
||||||
for gr.Next() {
|
for gr.Next() {
|
||||||
st := style
|
st := style
|
||||||
rs := gr.Runes()
|
rs := gr.Runes()
|
||||||
if len(rs) == 1 {
|
if len(rs) == 1 && rs[0] == '\r' {
|
||||||
r := rs[0]
|
st = style.Dim(true)
|
||||||
switch r {
|
rs[0] = '␍'
|
||||||
case '\r':
|
|
||||||
st = style.Dim(true)
|
|
||||||
rs[0] = '␍'
|
|
||||||
case '\n':
|
|
||||||
w.lastY++
|
|
||||||
w.lastX = 0
|
|
||||||
lx = 0
|
|
||||||
continue Loop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// word wrap:
|
|
||||||
xPos := w.left + w.lastX + lx
|
|
||||||
if xPos >= w.left+w.width {
|
|
||||||
w.lastY++
|
|
||||||
if w.lastY >= w.height {
|
|
||||||
return FillSuspend
|
|
||||||
}
|
|
||||||
w.lastX = 0
|
|
||||||
lx = 0
|
|
||||||
xPos = w.left
|
|
||||||
sign := w.wrapSign
|
|
||||||
if w.wrapSignWidth > w.width {
|
|
||||||
runes, _ := util.Truncate(sign, w.width)
|
|
||||||
sign = string(runes)
|
|
||||||
}
|
|
||||||
wgr := uniseg.NewGraphemes(sign)
|
|
||||||
for wgr.Next() {
|
|
||||||
rs := wgr.Runes()
|
|
||||||
_screen.SetContent(w.left+lx, w.top+w.lastY, rs[0], rs[1:], style.Dim(true))
|
|
||||||
lx += uniseg.StringWidth(string(rs))
|
|
||||||
}
|
|
||||||
xPos = w.left + lx
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
xPos := w.left + w.lastX
|
||||||
yPos := w.top + w.lastY
|
yPos := w.top + w.lastY
|
||||||
if yPos >= (w.top + w.height) {
|
if xPos < (w.left+w.width) && yPos < (w.top+w.height) {
|
||||||
return FillSuspend
|
_screen.SetContent(xPos, yPos, rs[0], rs[1:], st)
|
||||||
}
|
}
|
||||||
|
w.lastX += util.StringWidth(string(rs))
|
||||||
_screen.SetContent(xPos, yPos, rs[0], rs[1:], st)
|
|
||||||
lx += util.StringWidth(string(rs))
|
|
||||||
}
|
}
|
||||||
w.lastX += lx
|
}
|
||||||
if w.lastX == w.width {
|
|
||||||
|
func (w *TcellWindow) renderWrapSign(style tcell.Style) {
|
||||||
|
sign := w.wrapSign
|
||||||
|
if w.wrapSignWidth > w.width {
|
||||||
|
runes, _ := util.Truncate(sign, w.width)
|
||||||
|
sign = string(runes)
|
||||||
|
}
|
||||||
|
gr := uniseg.NewGraphemes(sign)
|
||||||
|
for gr.Next() {
|
||||||
|
rs := gr.Runes()
|
||||||
|
_screen.SetContent(w.left+w.lastX, w.top+w.lastY, rs[0], rs[1:], style.Dim(true))
|
||||||
|
w.lastX += uniseg.StringWidth(string(rs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
|
||||||
|
style := w.pairStyle(pair)
|
||||||
|
|
||||||
|
for i, segment := range strings.Split(text, "\n") {
|
||||||
|
for j, wl := range WrapLine(segment, w.lastX, w.width, w.tabstop, w.wrapSignWidth) {
|
||||||
|
if i > 0 || j > 0 {
|
||||||
|
w.lastY++
|
||||||
|
if w.lastY >= w.height {
|
||||||
|
return FillSuspend
|
||||||
|
}
|
||||||
|
w.lastX = 0
|
||||||
|
if j > 0 {
|
||||||
|
w.renderWrapSign(style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if w.lastX < w.width {
|
||||||
|
w.renderGraphemes(wl.Text, style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if w.lastX >= w.width {
|
||||||
w.lastY++
|
w.lastY++
|
||||||
w.lastX = 0
|
w.lastX = 0
|
||||||
return FillNextLine
|
return FillNextLine
|
||||||
@@ -982,14 +999,14 @@ func (w *TcellWindow) LinkEnd() {
|
|||||||
w.params = nil
|
w.params = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn {
|
func (w *TcellWindow) CFill(fg Color, bg Color, ul Color, a Attr, str string) FillReturn {
|
||||||
if fg == colDefault {
|
if fg == colDefault {
|
||||||
fg = w.normal.Fg()
|
fg = w.normal.Fg()
|
||||||
}
|
}
|
||||||
if bg == colDefault {
|
if bg == colDefault {
|
||||||
bg = w.normal.Bg()
|
bg = w.normal.Bg()
|
||||||
}
|
}
|
||||||
return w.fillString(str, NewColorPair(fg, bg, a))
|
return w.fillString(str, NewColorPair(fg, bg, a).WithUl(ul))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *TcellWindow) DrawBorder() {
|
func (w *TcellWindow) DrawBorder() {
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ func TestGetCharEventKey(t *testing.T) {
|
|||||||
{giveKey{tcell.KeyPause, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // unhandled
|
{giveKey{tcell.KeyPause, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // unhandled
|
||||||
|
|
||||||
}
|
}
|
||||||
r := NewFullscreenRenderer(&ColorTheme{}, false, false)
|
r := NewFullscreenRenderer(&ColorTheme{}, false, false, 8)
|
||||||
r.Init()
|
r.Init()
|
||||||
|
|
||||||
// run and evaluate the tests
|
// run and evaluate the tests
|
||||||
@@ -265,22 +265,22 @@ func TestGetCharEventKey(t *testing.T) {
|
|||||||
t.Logf("giveEvent = %T{key: %v, ch: %q (%[3]v), mod: %#04b}\n", giveEvent, giveEvent.Key(), giveEvent.Rune(), giveEvent.Modifiers())
|
t.Logf("giveEvent = %T{key: %v, ch: %q (%[3]v), mod: %#04b}\n", giveEvent, giveEvent.Key(), giveEvent.Rune(), giveEvent.Modifiers())
|
||||||
|
|
||||||
// process the event in fzf and evaluate the test
|
// process the event in fzf and evaluate the test
|
||||||
gotEvent := r.GetChar()
|
gotEvent := r.GetChar(true)
|
||||||
// skip Resize events, those are sometimes put in the buffer outside of this test
|
// skip Resize events, those are sometimes put in the buffer outside of this test
|
||||||
if initialResizeAsInvalid && gotEvent.Type == Invalid {
|
if initialResizeAsInvalid && gotEvent.Type == Invalid {
|
||||||
t.Logf("Resize as Invalid swallowed")
|
t.Logf("Resize as Invalid swallowed")
|
||||||
initialResizeAsInvalid = false
|
initialResizeAsInvalid = false
|
||||||
gotEvent = r.GetChar()
|
gotEvent = r.GetChar(true)
|
||||||
}
|
}
|
||||||
if gotEvent.Type == Resize {
|
if gotEvent.Type == Resize {
|
||||||
t.Logf("Resize swallowed")
|
t.Logf("Resize swallowed")
|
||||||
gotEvent = r.GetChar()
|
gotEvent = r.GetChar(true)
|
||||||
}
|
}
|
||||||
t.Logf("wantEvent = %T{Type: %v, Char: %q (%[3]v)}\n", test.wantKey, test.wantKey.Type, test.wantKey.Char)
|
t.Logf("wantEvent = %T{Type: %v, Char: %q (%[3]v)}\n", test.wantKey, test.wantKey.Type, test.wantKey.Char)
|
||||||
t.Logf("gotEvent = %T{Type: %v, Char: %q (%[3]v)}\n", gotEvent, gotEvent.Type, gotEvent.Char)
|
t.Logf("gotEvent = %T{Type: %v, Char: %q (%[3]v)}\n", gotEvent, gotEvent.Type, gotEvent.Char)
|
||||||
|
|
||||||
assert(t, "r.GetChar().Type", gotEvent.Type, test.wantKey.Type)
|
assert(t, "r.GetChar(true).Type", gotEvent.Type, test.wantKey.Type)
|
||||||
assert(t, "r.GetChar().Char", gotEvent.Char, test.wantKey.Char)
|
assert(t, "r.GetChar(true).Char", gotEvent.Char, test.wantKey.Char)
|
||||||
}
|
}
|
||||||
|
|
||||||
r.Close()
|
r.Close()
|
||||||
|
|||||||
+421
-221
@@ -2,12 +2,52 @@ package tui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/junegunn/fzf/src/util"
|
"github.com/junegunn/fzf/src/util"
|
||||||
"github.com/rivo/uniseg"
|
"github.com/rivo/uniseg"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Attr int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
AttrUndefined = Attr(0)
|
||||||
|
AttrRegular = Attr(1 << 8)
|
||||||
|
AttrClear = Attr(1 << 9)
|
||||||
|
BoldForce = Attr(1 << 10)
|
||||||
|
FullBg = Attr(1 << 11)
|
||||||
|
Strip = Attr(1 << 12)
|
||||||
|
|
||||||
|
// Underline style stored in bits 13-15 (3 bits, values 0-4)
|
||||||
|
// Only meaningful when the Underline attribute bit is also set.
|
||||||
|
// 0 = solid (default)
|
||||||
|
UnderlineStyleShift = 13
|
||||||
|
UnderlineStyleMask = Attr(0b111 << UnderlineStyleShift)
|
||||||
|
UlStyleDouble = Attr(0b001 << UnderlineStyleShift)
|
||||||
|
UlStyleCurly = Attr(0b010 << UnderlineStyleShift)
|
||||||
|
UlStyleDotted = Attr(0b011 << UnderlineStyleShift)
|
||||||
|
UlStyleDashed = Attr(0b100 << UnderlineStyleShift)
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a Attr) UnderlineStyle() Attr {
|
||||||
|
return a & UnderlineStyleMask
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Attr) Merge(b Attr) Attr {
|
||||||
|
if b&AttrRegular > 0 {
|
||||||
|
// Only keep bold attribute set by the system
|
||||||
|
return (b &^ AttrRegular) | (a & BoldForce)
|
||||||
|
}
|
||||||
|
|
||||||
|
merged := (a &^ AttrRegular) | b
|
||||||
|
// When b sets Underline, use b's underline style instead of OR'ing
|
||||||
|
if b&Underline > 0 {
|
||||||
|
merged = (merged &^ UnderlineStyleMask) | (b & UnderlineStyleMask)
|
||||||
|
}
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
// Types of user action
|
// Types of user action
|
||||||
//
|
//
|
||||||
//go:generate stringer -type=EventType
|
//go:generate stringer -type=EventType
|
||||||
@@ -275,6 +315,14 @@ func (a ColorAttr) IsColorDefined() bool {
|
|||||||
return a.Color != colUndefined
|
return a.Color != colUndefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a ColorAttr) IsAttrDefined() bool {
|
||||||
|
return a.Attr&^BoldForce != AttrUndefined
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a ColorAttr) IsUndefined() bool {
|
||||||
|
return !a.IsColorDefined() && !a.IsAttrDefined()
|
||||||
|
}
|
||||||
|
|
||||||
func NewColorAttr() ColorAttr {
|
func NewColorAttr() ColorAttr {
|
||||||
return ColorAttr{Color: colUndefined, Attr: AttrUndefined}
|
return ColorAttr{Color: colUndefined, Attr: AttrUndefined}
|
||||||
}
|
}
|
||||||
@@ -303,6 +351,14 @@ const (
|
|||||||
colMagenta
|
colMagenta
|
||||||
colCyan
|
colCyan
|
||||||
colWhite
|
colWhite
|
||||||
|
colGrey
|
||||||
|
colBrightRed
|
||||||
|
colBrightGreen
|
||||||
|
colBrightYellow
|
||||||
|
colBrightBlue
|
||||||
|
colBrightMagenta
|
||||||
|
colBrightCyan
|
||||||
|
colBrightWhite
|
||||||
)
|
)
|
||||||
|
|
||||||
type FillReturn int
|
type FillReturn int
|
||||||
@@ -316,6 +372,7 @@ const (
|
|||||||
type ColorPair struct {
|
type ColorPair struct {
|
||||||
fg Color
|
fg Color
|
||||||
bg Color
|
bg Color
|
||||||
|
ul Color
|
||||||
attr Attr
|
attr Attr
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,11 +384,11 @@ func HexToColor(rrggbb string) Color {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewColorPair(fg Color, bg Color, attr Attr) ColorPair {
|
func NewColorPair(fg Color, bg Color, attr Attr) ColorPair {
|
||||||
return ColorPair{fg, bg, attr}
|
return ColorPair{fg, bg, colDefault, attr}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NoColorPair() ColorPair {
|
func NoColorPair() ColorPair {
|
||||||
return ColorPair{-1, -1, 0}
|
return ColorPair{-1, -1, -1, 0}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p ColorPair) Fg() Color {
|
func (p ColorPair) Fg() Color {
|
||||||
@@ -342,6 +399,16 @@ func (p ColorPair) Bg() Color {
|
|||||||
return p.bg
|
return p.bg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p ColorPair) Ul() Color {
|
||||||
|
return p.ul
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p ColorPair) WithUl(ul Color) ColorPair {
|
||||||
|
dup := p
|
||||||
|
dup.ul = ul
|
||||||
|
return dup
|
||||||
|
}
|
||||||
|
|
||||||
func (p ColorPair) Attr() Attr {
|
func (p ColorPair) Attr() Attr {
|
||||||
return p.attr
|
return p.attr
|
||||||
}
|
}
|
||||||
@@ -350,6 +417,10 @@ func (p ColorPair) IsFullBgMarker() bool {
|
|||||||
return p.attr&FullBg > 0
|
return p.attr&FullBg > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p ColorPair) ShouldStripColors() bool {
|
||||||
|
return p.attr&Strip > 0
|
||||||
|
}
|
||||||
|
|
||||||
func (p ColorPair) HasBg() bool {
|
func (p ColorPair) HasBg() bool {
|
||||||
return p.attr&Reverse == 0 && p.bg != colDefault ||
|
return p.attr&Reverse == 0 && p.bg != colDefault ||
|
||||||
p.attr&Reverse > 0 && p.fg != colDefault
|
p.attr&Reverse > 0 && p.fg != colDefault
|
||||||
@@ -364,6 +435,9 @@ func (p ColorPair) merge(other ColorPair, except Color) ColorPair {
|
|||||||
if other.bg != except {
|
if other.bg != except {
|
||||||
dup.bg = other.bg
|
dup.bg = other.bg
|
||||||
}
|
}
|
||||||
|
if other.ul != except {
|
||||||
|
dup.ul = other.ul
|
||||||
|
}
|
||||||
return dup
|
return dup
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,9 +447,15 @@ func (p ColorPair) WithAttr(attr Attr) ColorPair {
|
|||||||
return dup
|
return dup
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p ColorPair) WithFg(fg ColorAttr) ColorPair {
|
||||||
|
dup := p
|
||||||
|
fgPair := ColorPair{fg.Color, colUndefined, colUndefined, fg.Attr}
|
||||||
|
return dup.Merge(fgPair)
|
||||||
|
}
|
||||||
|
|
||||||
func (p ColorPair) WithBg(bg ColorAttr) ColorPair {
|
func (p ColorPair) WithBg(bg ColorAttr) ColorPair {
|
||||||
dup := p
|
dup := p
|
||||||
bgPair := ColorPair{colUndefined, bg.Color, bg.Attr}
|
bgPair := ColorPair{colUndefined, bg.Color, colUndefined, bg.Attr}
|
||||||
return dup.Merge(bgPair)
|
return dup.Merge(bgPair)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,6 +482,7 @@ type ColorTheme struct {
|
|||||||
ListBg ColorAttr
|
ListBg ColorAttr
|
||||||
AltBg ColorAttr
|
AltBg ColorAttr
|
||||||
Nth ColorAttr
|
Nth ColorAttr
|
||||||
|
Nomatch ColorAttr
|
||||||
SelectedFg ColorAttr
|
SelectedFg ColorAttr
|
||||||
SelectedBg ColorAttr
|
SelectedBg ColorAttr
|
||||||
SelectedMatch ColorAttr
|
SelectedMatch ColorAttr
|
||||||
@@ -409,6 +490,7 @@ type ColorTheme struct {
|
|||||||
PreviewBg ColorAttr
|
PreviewBg ColorAttr
|
||||||
DarkBg ColorAttr
|
DarkBg ColorAttr
|
||||||
Gutter ColorAttr
|
Gutter ColorAttr
|
||||||
|
AltGutter ColorAttr
|
||||||
Prompt ColorAttr
|
Prompt ColorAttr
|
||||||
InputBg ColorAttr
|
InputBg ColorAttr
|
||||||
InputBorder ColorAttr
|
InputBorder ColorAttr
|
||||||
@@ -701,7 +783,8 @@ type Renderer interface {
|
|||||||
HideCursor()
|
HideCursor()
|
||||||
ShowCursor()
|
ShowCursor()
|
||||||
|
|
||||||
GetChar() Event
|
GetChar(cancellable bool) Event
|
||||||
|
CancelGetChar()
|
||||||
|
|
||||||
Top() int
|
Top() int
|
||||||
MaxX() int
|
MaxX() int
|
||||||
@@ -734,7 +817,7 @@ type Window interface {
|
|||||||
Print(text string)
|
Print(text string)
|
||||||
CPrint(color ColorPair, text string)
|
CPrint(color ColorPair, text string)
|
||||||
Fill(text string) FillReturn
|
Fill(text string) FillReturn
|
||||||
CFill(fg Color, bg Color, attr Attr, text string) FillReturn
|
CFill(fg Color, bg Color, ul Color, attr Attr, text string) FillReturn
|
||||||
LinkBegin(uri string, params string)
|
LinkBegin(uri string, params string)
|
||||||
LinkEnd()
|
LinkEnd()
|
||||||
Erase()
|
Erase()
|
||||||
@@ -747,16 +830,18 @@ type FullscreenRenderer struct {
|
|||||||
theme *ColorTheme
|
theme *ColorTheme
|
||||||
mouse bool
|
mouse bool
|
||||||
forceBlack bool
|
forceBlack bool
|
||||||
|
tabstop int
|
||||||
prevDownTime time.Time
|
prevDownTime time.Time
|
||||||
clicks [][2]int
|
clicks [][2]int
|
||||||
showCursor bool
|
showCursor bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Renderer {
|
func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int) Renderer {
|
||||||
r := &FullscreenRenderer{
|
r := &FullscreenRenderer{
|
||||||
theme: theme,
|
theme: theme,
|
||||||
mouse: mouse,
|
mouse: mouse,
|
||||||
forceBlack: forceBlack,
|
forceBlack: forceBlack,
|
||||||
|
tabstop: tabstop,
|
||||||
prevDownTime: time.Unix(0, 0),
|
prevDownTime: time.Unix(0, 0),
|
||||||
clicks: [][2]int{},
|
clicks: [][2]int{},
|
||||||
showCursor: true}
|
showCursor: true}
|
||||||
@@ -764,9 +849,11 @@ func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Rende
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
Default16 *ColorTheme
|
NoColorTheme *ColorTheme
|
||||||
Dark256 *ColorTheme
|
EmptyTheme *ColorTheme
|
||||||
Light256 *ColorTheme
|
Default16 *ColorTheme
|
||||||
|
Dark256 *ColorTheme
|
||||||
|
Light256 *ColorTheme
|
||||||
|
|
||||||
ColPrompt ColorPair
|
ColPrompt ColorPair
|
||||||
ColNormal ColorPair
|
ColNormal ColorPair
|
||||||
@@ -776,6 +863,9 @@ var (
|
|||||||
ColMatch ColorPair
|
ColMatch ColorPair
|
||||||
ColCursor ColorPair
|
ColCursor ColorPair
|
||||||
ColCursorEmpty ColorPair
|
ColCursorEmpty ColorPair
|
||||||
|
ColCursorEmptyChar ColorPair
|
||||||
|
ColAltCursorEmpty ColorPair
|
||||||
|
ColAltCursorEmptyChar ColorPair
|
||||||
ColMarker ColorPair
|
ColMarker ColorPair
|
||||||
ColSelected ColorPair
|
ColSelected ColorPair
|
||||||
ColSelectedMatch ColorPair
|
ColSelectedMatch ColorPair
|
||||||
@@ -809,167 +899,173 @@ var (
|
|||||||
ColInputLabel ColorPair
|
ColInputLabel ColorPair
|
||||||
)
|
)
|
||||||
|
|
||||||
func EmptyTheme() *ColorTheme {
|
|
||||||
return &ColorTheme{
|
|
||||||
Colored: true,
|
|
||||||
Input: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
Fg: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
Bg: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
ListFg: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
ListBg: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
AltBg: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
DarkBg: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
Prompt: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
Match: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
Current: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
CurrentMatch: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
Spinner: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
Info: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
Cursor: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
Marker: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
Header: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
Footer: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
Border: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
BorderLabel: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
ListLabel: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
ListBorder: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
Ghost: ColorAttr{colUndefined, Dim},
|
|
||||||
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
Separator: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
InputBg: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
InputBorder: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
InputLabel: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
HeaderBg: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
HeaderBorder: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
HeaderLabel: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
FooterBg: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
FooterBorder: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
FooterLabel: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
GapLine: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
Nth: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NoColorTheme() *ColorTheme {
|
|
||||||
return &ColorTheme{
|
|
||||||
Colored: false,
|
|
||||||
Input: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
ListFg: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
ListBg: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
AltBg: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
SelectedFg: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
SelectedBg: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
SelectedMatch: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
DarkBg: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
Prompt: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
Match: ColorAttr{colDefault, Underline},
|
|
||||||
Current: ColorAttr{colDefault, Reverse},
|
|
||||||
CurrentMatch: ColorAttr{colDefault, Reverse | Underline},
|
|
||||||
Spinner: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
Info: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
Cursor: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
Marker: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
Header: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
Border: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
BorderLabel: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
Ghost: ColorAttr{colDefault, Dim},
|
|
||||||
Disabled: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
PreviewFg: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
PreviewBg: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
Gutter: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
PreviewBorder: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
PreviewScrollbar: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
PreviewLabel: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
ListLabel: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
ListBorder: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
Separator: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
Scrollbar: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
InputBg: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
InputBorder: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
InputLabel: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
HeaderBg: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
HeaderBorder: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
HeaderLabel: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
FooterBg: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
FooterBorder: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
FooterLabel: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
GapLine: ColorAttr{colDefault, AttrUndefined},
|
|
||||||
Nth: ColorAttr{colUndefined, AttrUndefined},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
defaultColor := ColorAttr{colDefault, AttrUndefined}
|
||||||
|
undefined := ColorAttr{colUndefined, AttrUndefined}
|
||||||
|
|
||||||
|
NoColorTheme = &ColorTheme{
|
||||||
|
Colored: false,
|
||||||
|
Input: defaultColor,
|
||||||
|
Fg: defaultColor,
|
||||||
|
Bg: defaultColor,
|
||||||
|
ListFg: defaultColor,
|
||||||
|
ListBg: defaultColor,
|
||||||
|
AltBg: undefined,
|
||||||
|
SelectedFg: defaultColor,
|
||||||
|
SelectedBg: defaultColor,
|
||||||
|
SelectedMatch: defaultColor,
|
||||||
|
DarkBg: defaultColor,
|
||||||
|
Prompt: defaultColor,
|
||||||
|
Match: defaultColor,
|
||||||
|
Current: undefined,
|
||||||
|
CurrentMatch: undefined,
|
||||||
|
Spinner: defaultColor,
|
||||||
|
Info: defaultColor,
|
||||||
|
Cursor: defaultColor,
|
||||||
|
Marker: defaultColor,
|
||||||
|
Header: defaultColor,
|
||||||
|
Border: undefined,
|
||||||
|
BorderLabel: defaultColor,
|
||||||
|
Ghost: undefined,
|
||||||
|
Disabled: defaultColor,
|
||||||
|
PreviewFg: defaultColor,
|
||||||
|
PreviewBg: defaultColor,
|
||||||
|
Gutter: undefined,
|
||||||
|
AltGutter: undefined,
|
||||||
|
PreviewBorder: defaultColor,
|
||||||
|
PreviewScrollbar: defaultColor,
|
||||||
|
PreviewLabel: defaultColor,
|
||||||
|
ListLabel: defaultColor,
|
||||||
|
ListBorder: defaultColor,
|
||||||
|
Separator: defaultColor,
|
||||||
|
Scrollbar: defaultColor,
|
||||||
|
InputBg: defaultColor,
|
||||||
|
InputBorder: defaultColor,
|
||||||
|
InputLabel: defaultColor,
|
||||||
|
HeaderBg: defaultColor,
|
||||||
|
HeaderBorder: defaultColor,
|
||||||
|
HeaderLabel: defaultColor,
|
||||||
|
FooterBg: defaultColor,
|
||||||
|
FooterBorder: defaultColor,
|
||||||
|
FooterLabel: defaultColor,
|
||||||
|
GapLine: defaultColor,
|
||||||
|
Nth: undefined,
|
||||||
|
Nomatch: undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
EmptyTheme = &ColorTheme{
|
||||||
|
Colored: true,
|
||||||
|
Input: undefined,
|
||||||
|
Fg: undefined,
|
||||||
|
Bg: undefined,
|
||||||
|
ListFg: undefined,
|
||||||
|
ListBg: undefined,
|
||||||
|
AltBg: undefined,
|
||||||
|
SelectedFg: undefined,
|
||||||
|
SelectedBg: undefined,
|
||||||
|
SelectedMatch: undefined,
|
||||||
|
DarkBg: undefined,
|
||||||
|
Prompt: undefined,
|
||||||
|
Match: undefined,
|
||||||
|
Current: undefined,
|
||||||
|
CurrentMatch: undefined,
|
||||||
|
Spinner: undefined,
|
||||||
|
Info: undefined,
|
||||||
|
Cursor: undefined,
|
||||||
|
Marker: undefined,
|
||||||
|
Header: undefined,
|
||||||
|
Footer: undefined,
|
||||||
|
Border: undefined,
|
||||||
|
BorderLabel: undefined,
|
||||||
|
ListLabel: undefined,
|
||||||
|
ListBorder: undefined,
|
||||||
|
Ghost: undefined,
|
||||||
|
Disabled: undefined,
|
||||||
|
PreviewFg: undefined,
|
||||||
|
PreviewBg: undefined,
|
||||||
|
Gutter: undefined,
|
||||||
|
AltGutter: undefined,
|
||||||
|
PreviewBorder: undefined,
|
||||||
|
PreviewScrollbar: undefined,
|
||||||
|
PreviewLabel: undefined,
|
||||||
|
Separator: undefined,
|
||||||
|
Scrollbar: undefined,
|
||||||
|
InputBg: undefined,
|
||||||
|
InputBorder: undefined,
|
||||||
|
InputLabel: undefined,
|
||||||
|
HeaderBg: undefined,
|
||||||
|
HeaderBorder: undefined,
|
||||||
|
HeaderLabel: undefined,
|
||||||
|
FooterBg: undefined,
|
||||||
|
FooterBorder: undefined,
|
||||||
|
FooterLabel: undefined,
|
||||||
|
GapLine: undefined,
|
||||||
|
Nth: undefined,
|
||||||
|
Nomatch: undefined,
|
||||||
|
}
|
||||||
|
|
||||||
Default16 = &ColorTheme{
|
Default16 = &ColorTheme{
|
||||||
Colored: true,
|
Colored: true,
|
||||||
Input: ColorAttr{colDefault, AttrUndefined},
|
Input: defaultColor,
|
||||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
Fg: defaultColor,
|
||||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
Bg: defaultColor,
|
||||||
ListFg: ColorAttr{colUndefined, AttrUndefined},
|
ListFg: undefined,
|
||||||
ListBg: ColorAttr{colUndefined, AttrUndefined},
|
ListBg: undefined,
|
||||||
AltBg: ColorAttr{colUndefined, AttrUndefined},
|
AltBg: undefined,
|
||||||
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
|
SelectedFg: undefined,
|
||||||
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
|
SelectedBg: undefined,
|
||||||
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
|
SelectedMatch: undefined,
|
||||||
DarkBg: ColorAttr{colBlack, AttrUndefined},
|
DarkBg: ColorAttr{colGrey, AttrUndefined},
|
||||||
Prompt: ColorAttr{colBlue, AttrUndefined},
|
Prompt: ColorAttr{colBlue, AttrUndefined},
|
||||||
Match: ColorAttr{colGreen, AttrUndefined},
|
Match: ColorAttr{colGreen, AttrUndefined},
|
||||||
Current: ColorAttr{colYellow, AttrUndefined},
|
Current: ColorAttr{colBrightWhite, AttrUndefined},
|
||||||
CurrentMatch: ColorAttr{colGreen, AttrUndefined},
|
CurrentMatch: ColorAttr{colBrightGreen, AttrUndefined},
|
||||||
Spinner: ColorAttr{colGreen, AttrUndefined},
|
Spinner: ColorAttr{colGreen, AttrUndefined},
|
||||||
Info: ColorAttr{colWhite, AttrUndefined},
|
Info: ColorAttr{colYellow, AttrUndefined},
|
||||||
Cursor: ColorAttr{colRed, AttrUndefined},
|
Cursor: ColorAttr{colRed, AttrUndefined},
|
||||||
Marker: ColorAttr{colMagenta, AttrUndefined},
|
Marker: ColorAttr{colMagenta, AttrUndefined},
|
||||||
Header: ColorAttr{colCyan, AttrUndefined},
|
Header: ColorAttr{colCyan, AttrUndefined},
|
||||||
Footer: ColorAttr{colCyan, AttrUndefined},
|
Footer: ColorAttr{colCyan, AttrUndefined},
|
||||||
Border: ColorAttr{colBlack, AttrUndefined},
|
Border: undefined,
|
||||||
BorderLabel: ColorAttr{colWhite, AttrUndefined},
|
BorderLabel: defaultColor,
|
||||||
Ghost: ColorAttr{colUndefined, Dim},
|
Ghost: undefined,
|
||||||
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
Disabled: undefined,
|
||||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
PreviewFg: undefined,
|
||||||
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
PreviewBg: undefined,
|
||||||
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
Gutter: undefined,
|
||||||
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
|
AltGutter: undefined,
|
||||||
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
|
PreviewBorder: undefined,
|
||||||
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
PreviewScrollbar: undefined,
|
||||||
ListLabel: ColorAttr{colUndefined, AttrUndefined},
|
PreviewLabel: undefined,
|
||||||
ListBorder: ColorAttr{colUndefined, AttrUndefined},
|
ListLabel: undefined,
|
||||||
Separator: ColorAttr{colUndefined, AttrUndefined},
|
ListBorder: undefined,
|
||||||
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
|
Separator: undefined,
|
||||||
InputBg: ColorAttr{colUndefined, AttrUndefined},
|
Scrollbar: undefined,
|
||||||
InputBorder: ColorAttr{colUndefined, AttrUndefined},
|
InputBg: undefined,
|
||||||
InputLabel: ColorAttr{colUndefined, AttrUndefined},
|
InputBorder: undefined,
|
||||||
HeaderBg: ColorAttr{colUndefined, AttrUndefined},
|
InputLabel: undefined,
|
||||||
HeaderBorder: ColorAttr{colUndefined, AttrUndefined},
|
HeaderBg: undefined,
|
||||||
HeaderLabel: ColorAttr{colUndefined, AttrUndefined},
|
HeaderBorder: undefined,
|
||||||
FooterBg: ColorAttr{colUndefined, AttrUndefined},
|
HeaderLabel: undefined,
|
||||||
FooterBorder: ColorAttr{colUndefined, AttrUndefined},
|
FooterBg: undefined,
|
||||||
FooterLabel: ColorAttr{colUndefined, AttrUndefined},
|
FooterBorder: undefined,
|
||||||
GapLine: ColorAttr{colUndefined, AttrUndefined},
|
FooterLabel: undefined,
|
||||||
Nth: ColorAttr{colUndefined, AttrUndefined},
|
GapLine: undefined,
|
||||||
|
Nth: undefined,
|
||||||
|
Nomatch: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
Dark256 = &ColorTheme{
|
Dark256 = &ColorTheme{
|
||||||
Colored: true,
|
Colored: true,
|
||||||
Input: ColorAttr{colDefault, AttrUndefined},
|
Input: defaultColor,
|
||||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
Fg: defaultColor,
|
||||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
Bg: defaultColor,
|
||||||
ListFg: ColorAttr{colUndefined, AttrUndefined},
|
ListFg: undefined,
|
||||||
ListBg: ColorAttr{colUndefined, AttrUndefined},
|
ListBg: undefined,
|
||||||
AltBg: ColorAttr{colUndefined, AttrUndefined},
|
AltBg: undefined,
|
||||||
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
|
SelectedFg: undefined,
|
||||||
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
|
SelectedBg: undefined,
|
||||||
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
|
SelectedMatch: undefined,
|
||||||
DarkBg: ColorAttr{236, AttrUndefined},
|
DarkBg: ColorAttr{236, AttrUndefined},
|
||||||
Prompt: ColorAttr{110, AttrUndefined},
|
Prompt: ColorAttr{110, AttrUndefined},
|
||||||
Match: ColorAttr{108, AttrUndefined},
|
Match: ColorAttr{108, AttrUndefined},
|
||||||
@@ -983,41 +1079,44 @@ func init() {
|
|||||||
Footer: ColorAttr{109, AttrUndefined},
|
Footer: ColorAttr{109, AttrUndefined},
|
||||||
Border: ColorAttr{59, AttrUndefined},
|
Border: ColorAttr{59, AttrUndefined},
|
||||||
BorderLabel: ColorAttr{145, AttrUndefined},
|
BorderLabel: ColorAttr{145, AttrUndefined},
|
||||||
Ghost: ColorAttr{colUndefined, Dim},
|
Ghost: undefined,
|
||||||
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
Disabled: undefined,
|
||||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
PreviewFg: undefined,
|
||||||
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
PreviewBg: undefined,
|
||||||
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
Gutter: undefined,
|
||||||
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
|
AltGutter: undefined,
|
||||||
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
|
PreviewBorder: undefined,
|
||||||
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
PreviewScrollbar: undefined,
|
||||||
ListLabel: ColorAttr{colUndefined, AttrUndefined},
|
PreviewLabel: undefined,
|
||||||
ListBorder: ColorAttr{colUndefined, AttrUndefined},
|
ListLabel: undefined,
|
||||||
Separator: ColorAttr{colUndefined, AttrUndefined},
|
ListBorder: undefined,
|
||||||
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
|
Separator: undefined,
|
||||||
InputBg: ColorAttr{colUndefined, AttrUndefined},
|
Scrollbar: undefined,
|
||||||
InputBorder: ColorAttr{colUndefined, AttrUndefined},
|
InputBg: undefined,
|
||||||
InputLabel: ColorAttr{colUndefined, AttrUndefined},
|
InputBorder: undefined,
|
||||||
HeaderBg: ColorAttr{colUndefined, AttrUndefined},
|
InputLabel: undefined,
|
||||||
HeaderBorder: ColorAttr{colUndefined, AttrUndefined},
|
HeaderBg: undefined,
|
||||||
HeaderLabel: ColorAttr{colUndefined, AttrUndefined},
|
HeaderBorder: undefined,
|
||||||
FooterBg: ColorAttr{colUndefined, AttrUndefined},
|
HeaderLabel: undefined,
|
||||||
FooterBorder: ColorAttr{colUndefined, AttrUndefined},
|
FooterBg: undefined,
|
||||||
FooterLabel: ColorAttr{colUndefined, AttrUndefined},
|
FooterBorder: undefined,
|
||||||
GapLine: ColorAttr{colUndefined, AttrUndefined},
|
FooterLabel: undefined,
|
||||||
Nth: ColorAttr{colUndefined, AttrUndefined},
|
GapLine: undefined,
|
||||||
|
Nth: undefined,
|
||||||
|
Nomatch: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
Light256 = &ColorTheme{
|
Light256 = &ColorTheme{
|
||||||
Colored: true,
|
Colored: true,
|
||||||
Input: ColorAttr{colDefault, AttrUndefined},
|
Input: defaultColor,
|
||||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
Fg: defaultColor,
|
||||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
Bg: defaultColor,
|
||||||
ListFg: ColorAttr{colUndefined, AttrUndefined},
|
ListFg: undefined,
|
||||||
ListBg: ColorAttr{colUndefined, AttrUndefined},
|
ListBg: undefined,
|
||||||
AltBg: ColorAttr{colUndefined, AttrUndefined},
|
AltBg: undefined,
|
||||||
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
|
SelectedFg: undefined,
|
||||||
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
|
SelectedBg: undefined,
|
||||||
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
|
SelectedMatch: undefined,
|
||||||
DarkBg: ColorAttr{251, AttrUndefined},
|
DarkBg: ColorAttr{251, AttrUndefined},
|
||||||
Prompt: ColorAttr{25, AttrUndefined},
|
Prompt: ColorAttr{25, AttrUndefined},
|
||||||
Match: ColorAttr{66, AttrUndefined},
|
Match: ColorAttr{66, AttrUndefined},
|
||||||
@@ -1031,37 +1130,55 @@ func init() {
|
|||||||
Footer: ColorAttr{31, AttrUndefined},
|
Footer: ColorAttr{31, AttrUndefined},
|
||||||
Border: ColorAttr{145, AttrUndefined},
|
Border: ColorAttr{145, AttrUndefined},
|
||||||
BorderLabel: ColorAttr{59, AttrUndefined},
|
BorderLabel: ColorAttr{59, AttrUndefined},
|
||||||
Ghost: ColorAttr{colUndefined, Dim},
|
Ghost: undefined,
|
||||||
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
Disabled: undefined,
|
||||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
PreviewFg: undefined,
|
||||||
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
PreviewBg: undefined,
|
||||||
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
Gutter: undefined,
|
||||||
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
|
AltGutter: undefined,
|
||||||
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
|
PreviewBorder: undefined,
|
||||||
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
PreviewScrollbar: undefined,
|
||||||
ListLabel: ColorAttr{colUndefined, AttrUndefined},
|
PreviewLabel: undefined,
|
||||||
ListBorder: ColorAttr{colUndefined, AttrUndefined},
|
ListLabel: undefined,
|
||||||
Separator: ColorAttr{colUndefined, AttrUndefined},
|
ListBorder: undefined,
|
||||||
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
|
Separator: undefined,
|
||||||
InputBg: ColorAttr{colUndefined, AttrUndefined},
|
Scrollbar: undefined,
|
||||||
InputBorder: ColorAttr{colUndefined, AttrUndefined},
|
InputBg: undefined,
|
||||||
InputLabel: ColorAttr{colUndefined, AttrUndefined},
|
InputBorder: undefined,
|
||||||
HeaderBg: ColorAttr{colUndefined, AttrUndefined},
|
InputLabel: undefined,
|
||||||
HeaderBorder: ColorAttr{colUndefined, AttrUndefined},
|
HeaderBg: undefined,
|
||||||
HeaderLabel: ColorAttr{colUndefined, AttrUndefined},
|
HeaderBorder: undefined,
|
||||||
FooterBg: ColorAttr{colUndefined, AttrUndefined},
|
HeaderLabel: undefined,
|
||||||
FooterBorder: ColorAttr{colUndefined, AttrUndefined},
|
FooterBg: undefined,
|
||||||
FooterLabel: ColorAttr{colUndefined, AttrUndefined},
|
FooterBorder: undefined,
|
||||||
GapLine: ColorAttr{colUndefined, AttrUndefined},
|
FooterLabel: undefined,
|
||||||
Nth: ColorAttr{colUndefined, AttrUndefined},
|
GapLine: undefined,
|
||||||
|
Nth: undefined,
|
||||||
|
Nomatch: undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool, hasInputWindow bool, hasHeaderWindow 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}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if boldify {
|
||||||
|
boldify := func(c ColorAttr) ColorAttr {
|
||||||
|
dup := c
|
||||||
|
if (c.Attr & AttrRegular) == 0 {
|
||||||
|
dup.Attr |= BoldForce
|
||||||
|
}
|
||||||
|
return dup
|
||||||
|
}
|
||||||
|
theme.Current = boldify(theme.Current)
|
||||||
|
theme.CurrentMatch = boldify(theme.CurrentMatch)
|
||||||
|
theme.Prompt = boldify(theme.Prompt)
|
||||||
|
theme.Input = boldify(theme.Input)
|
||||||
|
theme.Cursor = boldify(theme.Cursor)
|
||||||
|
theme.Spinner = boldify(theme.Spinner)
|
||||||
|
}
|
||||||
|
|
||||||
o := func(a ColorAttr, b ColorAttr) ColorAttr {
|
o := func(a ColorAttr, b ColorAttr) ColorAttr {
|
||||||
c := a
|
c := a
|
||||||
if b.Color != colUndefined {
|
if b.Color != colUndefined {
|
||||||
@@ -1077,18 +1194,36 @@ func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool, hasInp
|
|||||||
theme.Bg = o(baseTheme.Bg, theme.Bg)
|
theme.Bg = o(baseTheme.Bg, theme.Bg)
|
||||||
theme.DarkBg = o(baseTheme.DarkBg, theme.DarkBg)
|
theme.DarkBg = o(baseTheme.DarkBg, theme.DarkBg)
|
||||||
theme.Prompt = o(baseTheme.Prompt, theme.Prompt)
|
theme.Prompt = o(baseTheme.Prompt, theme.Prompt)
|
||||||
theme.Match = o(baseTheme.Match, theme.Match)
|
match := theme.Match
|
||||||
|
if !baseTheme.Colored && match.IsUndefined() {
|
||||||
|
match.Attr = Underline
|
||||||
|
}
|
||||||
|
theme.Match = o(baseTheme.Match, match)
|
||||||
// Inherit from 'fg', so that we don't have to write 'current-fg:dim'
|
// Inherit from 'fg', so that we don't have to write 'current-fg:dim'
|
||||||
// e.g. fzf --delimiter / --nth -1 --color fg:dim,nth:regular
|
// e.g. fzf --delimiter / --nth -1 --color fg:dim,nth:regular
|
||||||
theme.Current = theme.Fg.Merge(o(baseTheme.Current, theme.Current))
|
current := theme.Current
|
||||||
theme.CurrentMatch = o(baseTheme.CurrentMatch, theme.CurrentMatch)
|
if !baseTheme.Colored && current.IsUndefined() {
|
||||||
|
current.Attr |= Reverse
|
||||||
|
}
|
||||||
|
theme.Current = theme.Fg.Merge(o(baseTheme.Current, current))
|
||||||
|
currentMatch := theme.CurrentMatch
|
||||||
|
if !baseTheme.Colored && currentMatch.IsUndefined() {
|
||||||
|
currentMatch.Attr |= Reverse | Underline
|
||||||
|
}
|
||||||
|
theme.CurrentMatch = o(baseTheme.CurrentMatch, currentMatch)
|
||||||
theme.Spinner = o(baseTheme.Spinner, theme.Spinner)
|
theme.Spinner = o(baseTheme.Spinner, theme.Spinner)
|
||||||
theme.Info = o(baseTheme.Info, theme.Info)
|
theme.Info = o(baseTheme.Info, theme.Info)
|
||||||
theme.Cursor = o(baseTheme.Cursor, theme.Cursor)
|
theme.Cursor = o(baseTheme.Cursor, theme.Cursor)
|
||||||
theme.Marker = o(baseTheme.Marker, theme.Marker)
|
theme.Marker = o(baseTheme.Marker, theme.Marker)
|
||||||
theme.Header = o(baseTheme.Header, theme.Header)
|
theme.Header = o(baseTheme.Header, theme.Header)
|
||||||
theme.Footer = o(baseTheme.Footer, theme.Footer)
|
theme.Footer = o(baseTheme.Footer, theme.Footer)
|
||||||
theme.Border = o(baseTheme.Border, theme.Border)
|
|
||||||
|
// If border color is undefined, set it to default color with dim attribute.
|
||||||
|
border := theme.Border
|
||||||
|
if baseTheme.Border.IsUndefined() && border.IsUndefined() {
|
||||||
|
border.Attr = Dim
|
||||||
|
}
|
||||||
|
theme.Border = o(baseTheme.Border, border)
|
||||||
theme.BorderLabel = o(baseTheme.BorderLabel, theme.BorderLabel)
|
theme.BorderLabel = o(baseTheme.BorderLabel, theme.BorderLabel)
|
||||||
|
|
||||||
undefined := NewColorAttr()
|
undefined := NewColorAttr()
|
||||||
@@ -1101,9 +1236,24 @@ func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool, hasInp
|
|||||||
theme.SelectedFg = o(theme.ListFg, theme.SelectedFg)
|
theme.SelectedFg = o(theme.ListFg, theme.SelectedFg)
|
||||||
theme.SelectedBg = o(theme.ListBg, theme.SelectedBg)
|
theme.SelectedBg = o(theme.ListBg, theme.SelectedBg)
|
||||||
theme.SelectedMatch = o(theme.Match, theme.SelectedMatch)
|
theme.SelectedMatch = o(theme.Match, theme.SelectedMatch)
|
||||||
theme.Ghost = o(theme.Input, theme.Ghost)
|
|
||||||
|
ghost := theme.Ghost
|
||||||
|
if ghost.IsUndefined() {
|
||||||
|
ghost.Attr = Dim
|
||||||
|
} else if ghost.IsColorDefined() && !ghost.IsAttrDefined() {
|
||||||
|
// Don't want to inherit 'bold' from 'input'
|
||||||
|
ghost.Attr = AttrRegular
|
||||||
|
}
|
||||||
|
theme.Ghost = o(theme.Input, ghost)
|
||||||
theme.Disabled = o(theme.Input, theme.Disabled)
|
theme.Disabled = o(theme.Input, theme.Disabled)
|
||||||
theme.Gutter = o(theme.DarkBg, theme.Gutter)
|
|
||||||
|
// Use dim gutter on non-colored themes if undefined
|
||||||
|
gutter := theme.Gutter
|
||||||
|
if !baseTheme.Colored && gutter.IsUndefined() {
|
||||||
|
gutter.Attr = Dim
|
||||||
|
}
|
||||||
|
theme.Gutter = o(theme.DarkBg, gutter)
|
||||||
|
theme.AltGutter = o(theme.Gutter, theme.AltGutter)
|
||||||
theme.PreviewFg = o(theme.Fg, theme.PreviewFg)
|
theme.PreviewFg = o(theme.Fg, theme.PreviewFg)
|
||||||
theme.PreviewBg = o(theme.Bg, theme.PreviewBg)
|
theme.PreviewBg = o(theme.Bg, theme.PreviewBg)
|
||||||
theme.PreviewLabel = o(theme.BorderLabel, theme.PreviewLabel)
|
theme.PreviewLabel = o(theme.BorderLabel, theme.PreviewLabel)
|
||||||
@@ -1145,6 +1295,10 @@ func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool, hasInp
|
|||||||
theme.FooterBorder = o(theme.Border, theme.FooterBorder)
|
theme.FooterBorder = o(theme.Border, theme.FooterBorder)
|
||||||
theme.FooterLabel = o(theme.BorderLabel, theme.FooterLabel)
|
theme.FooterLabel = o(theme.BorderLabel, theme.FooterLabel)
|
||||||
|
|
||||||
|
if theme.Nomatch.IsUndefined() {
|
||||||
|
theme.Nomatch.Attr = Dim
|
||||||
|
}
|
||||||
|
|
||||||
initPalette(theme)
|
initPalette(theme)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1153,7 +1307,7 @@ func initPalette(theme *ColorTheme) {
|
|||||||
if fg.Color == colDefault && (fg.Attr&Reverse) > 0 {
|
if fg.Color == colDefault && (fg.Attr&Reverse) > 0 {
|
||||||
bg.Color = colDefault
|
bg.Color = colDefault
|
||||||
}
|
}
|
||||||
return ColorPair{fg.Color, bg.Color, fg.Attr}
|
return ColorPair{fg.Color, bg.Color, colDefault, fg.Attr}
|
||||||
}
|
}
|
||||||
blank := theme.ListFg
|
blank := theme.ListFg
|
||||||
blank.Attr = AttrRegular
|
blank.Attr = AttrRegular
|
||||||
@@ -1168,10 +1322,13 @@ func initPalette(theme *ColorTheme) {
|
|||||||
ColSelectedMatch = pair(theme.SelectedMatch, theme.SelectedBg)
|
ColSelectedMatch = pair(theme.SelectedMatch, theme.SelectedBg)
|
||||||
ColCursor = pair(theme.Cursor, theme.Gutter)
|
ColCursor = pair(theme.Cursor, theme.Gutter)
|
||||||
ColCursorEmpty = pair(blank, theme.Gutter)
|
ColCursorEmpty = pair(blank, theme.Gutter)
|
||||||
|
ColCursorEmptyChar = pair(theme.Gutter, theme.ListBg)
|
||||||
|
ColAltCursorEmpty = pair(blank, theme.AltGutter)
|
||||||
|
ColAltCursorEmptyChar = pair(theme.AltGutter, theme.ListBg)
|
||||||
if theme.SelectedBg.Color != theme.ListBg.Color {
|
if theme.SelectedBg.Color != theme.ListBg.Color {
|
||||||
ColMarker = pair(theme.Marker, theme.SelectedBg)
|
ColMarker = pair(theme.Marker, theme.SelectedBg)
|
||||||
} else {
|
} else {
|
||||||
ColMarker = pair(theme.Marker, theme.Gutter)
|
ColMarker = pair(theme.Marker, theme.ListBg)
|
||||||
}
|
}
|
||||||
ColCurrent = pair(theme.Current, theme.DarkBg)
|
ColCurrent = pair(theme.Current, theme.DarkBg)
|
||||||
ColCurrentMatch = pair(theme.CurrentMatch, theme.DarkBg)
|
ColCurrentMatch = pair(theme.CurrentMatch, theme.DarkBg)
|
||||||
@@ -1206,3 +1363,46 @@ func initPalette(theme *ColorTheme) {
|
|||||||
func runeWidth(r rune) int {
|
func runeWidth(r rune) int {
|
||||||
return uniseg.StringWidth(string(r))
|
return uniseg.StringWidth(string(r))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WrappedLine represents a single visual line after character-level wrapping.
|
||||||
|
type WrappedLine struct {
|
||||||
|
Text string
|
||||||
|
DisplayWidth int
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapLine splits a single line (no embedded \n) into visual lines
|
||||||
|
// that fit within initialMax columns. Character-level wrapping only.
|
||||||
|
func WrapLine(input string, prefixLength int, initialMax int, tabstop int, wrapSignWidth int) []WrappedLine {
|
||||||
|
lines := []WrappedLine{}
|
||||||
|
width := 0
|
||||||
|
line := ""
|
||||||
|
gr := uniseg.NewGraphemes(input)
|
||||||
|
maxWidth := initialMax
|
||||||
|
contMax := max(1, initialMax-wrapSignWidth)
|
||||||
|
for gr.Next() {
|
||||||
|
rs := gr.Runes()
|
||||||
|
str := string(rs)
|
||||||
|
var w int
|
||||||
|
if len(rs) == 1 && rs[0] == '\t' {
|
||||||
|
w = tabstop - (prefixLength+width)%tabstop
|
||||||
|
str = strings.Repeat(" ", w)
|
||||||
|
} else if rs[0] == '\r' {
|
||||||
|
w++
|
||||||
|
} else {
|
||||||
|
w = uniseg.StringWidth(str)
|
||||||
|
}
|
||||||
|
width += w
|
||||||
|
|
||||||
|
if prefixLength+width <= maxWidth {
|
||||||
|
line += str
|
||||||
|
} else {
|
||||||
|
lines = append(lines, WrappedLine{string(line), width - w})
|
||||||
|
line = str
|
||||||
|
prefixLength = 0
|
||||||
|
width = w
|
||||||
|
maxWidth = contMax
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines = append(lines, WrappedLine{string(line), width})
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,46 @@ package tui
|
|||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
|
func TestWrapLine(t *testing.T) {
|
||||||
|
// Basic wrapping
|
||||||
|
lines := WrapLine("hello world", 0, 7, 8, 2)
|
||||||
|
if len(lines) != 2 || lines[0].Text != "hello w" || lines[1].Text != "orld" {
|
||||||
|
t.Errorf("Basic wrap: %v", lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exact fit — no wrapping needed
|
||||||
|
lines = WrapLine("hello", 0, 5, 8, 2)
|
||||||
|
if len(lines) != 1 || lines[0].Text != "hello" || lines[0].DisplayWidth != 5 {
|
||||||
|
t.Errorf("Exact fit: %v", lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
// With prefix length
|
||||||
|
lines = WrapLine("hello", 3, 5, 8, 2)
|
||||||
|
if len(lines) != 2 || lines[0].Text != "he" || lines[1].Text != "llo" {
|
||||||
|
t.Errorf("Prefix length: %v", lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty string
|
||||||
|
lines = WrapLine("", 0, 10, 8, 2)
|
||||||
|
if len(lines) != 1 || lines[0].Text != "" || lines[0].DisplayWidth != 0 {
|
||||||
|
t.Errorf("Empty string: %v", lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continuation lines account for wrapSignWidth
|
||||||
|
lines = WrapLine("abcdefghij", 0, 5, 8, 2)
|
||||||
|
// First line: "abcde" (5 chars fit in width 5)
|
||||||
|
// Continuation max: 5-2=3, so "fgh" then "ij"
|
||||||
|
if len(lines) != 3 || lines[0].Text != "abcde" || lines[1].Text != "fgh" || lines[2].Text != "ij" {
|
||||||
|
t.Errorf("Continuation: %v", lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab expansion
|
||||||
|
lines = WrapLine("\there", 0, 10, 4, 2)
|
||||||
|
if len(lines) != 1 || lines[0].DisplayWidth != 8 {
|
||||||
|
t.Errorf("Tab: %v", lines)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestHexToColor(t *testing.T) {
|
func TestHexToColor(t *testing.T) {
|
||||||
assert := func(expr string, r, g, b int) {
|
assert := func(expr string, r, g, b int) {
|
||||||
color := HexToColor(expr)
|
color := HexToColor(expr)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
func TestAtExit(t *testing.T) {
|
func TestAtExit(t *testing.T) {
|
||||||
want := []int{3, 2, 1, 0}
|
want := []int{3, 2, 1, 0}
|
||||||
var called []int
|
var called []int
|
||||||
for i := 0; i < 4; i++ {
|
for i := range 4 {
|
||||||
n := i
|
n := i
|
||||||
AtExit(func() { called = append(called, n) })
|
AtExit(func() { called = append(called, n) })
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-4
@@ -52,7 +52,7 @@ func ToChars(bytes []byte) Chars {
|
|||||||
}
|
}
|
||||||
|
|
||||||
runes := make([]rune, bytesUntil, len(bytes))
|
runes := make([]rune, bytesUntil, len(bytes))
|
||||||
for i := 0; i < bytesUntil; i++ {
|
for i := range bytesUntil {
|
||||||
runes[i] = rune(bytes[i])
|
runes[i] = rune(bytes[i])
|
||||||
}
|
}
|
||||||
for i := bytesUntil; i < len(bytes); {
|
for i := bytesUntil; i < len(bytes); {
|
||||||
@@ -187,7 +187,7 @@ func (chars *Chars) TrailingWhitespaces() int {
|
|||||||
func (chars *Chars) TrimTrailingWhitespaces(maxIndex int) {
|
func (chars *Chars) TrimTrailingWhitespaces(maxIndex int) {
|
||||||
whitespaces := chars.TrailingWhitespaces()
|
whitespaces := chars.TrailingWhitespaces()
|
||||||
end := len(chars.slice) - whitespaces
|
end := len(chars.slice) - whitespaces
|
||||||
chars.slice = chars.slice[0:Max(end, maxIndex)]
|
chars.slice = chars.slice[0:max(end, maxIndex)]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (chars *Chars) TrimSuffix(runes []rune) {
|
func (chars *Chars) TrimSuffix(runes []rune) {
|
||||||
@@ -249,7 +249,7 @@ func (chars *Chars) Prepend(prefix string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWidth int, tabstop int) ([][]rune, bool) {
|
func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWidth int, tabstop int, wrapWord bool) ([][]rune, bool) {
|
||||||
text := make([]rune, chars.Length())
|
text := make([]rune, chars.Length())
|
||||||
copy(text, chars.ToRunes())
|
copy(text, chars.ToRunes())
|
||||||
|
|
||||||
@@ -259,7 +259,7 @@ func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWi
|
|||||||
lines = append(lines, text)
|
lines = append(lines, text)
|
||||||
} else {
|
} else {
|
||||||
from := 0
|
from := 0
|
||||||
for off := 0; off < len(text); off++ {
|
for off := range text {
|
||||||
if text[off] == '\n' {
|
if text[off] == '\n' {
|
||||||
lines = append(lines, text[from:off+1]) // Include '\n'
|
lines = append(lines, text[from:off+1]) // Include '\n'
|
||||||
from = off + 1
|
from = off + 1
|
||||||
@@ -307,6 +307,19 @@ func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWi
|
|||||||
if overflowIdx == 0 {
|
if overflowIdx == 0 {
|
||||||
overflowIdx = 1
|
overflowIdx = 1
|
||||||
}
|
}
|
||||||
|
if wrapWord {
|
||||||
|
// Find last space/tab at or before overflowIdx
|
||||||
|
breakIdx := -1
|
||||||
|
for k := overflowIdx; k > 0; k-- {
|
||||||
|
if line[k-1] == ' ' || line[k-1] == '\t' {
|
||||||
|
breakIdx = k
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if breakIdx > 0 {
|
||||||
|
overflowIdx = breakIdx
|
||||||
|
}
|
||||||
|
}
|
||||||
if len(wrapped) >= maxLines {
|
if len(wrapped) >= maxLines {
|
||||||
return wrapped, true
|
return wrapped, true
|
||||||
}
|
}
|
||||||
|
|||||||
+49
-1
@@ -51,7 +51,7 @@ func TestTrimLength(t *testing.T) {
|
|||||||
func TestCharsLines(t *testing.T) {
|
func TestCharsLines(t *testing.T) {
|
||||||
chars := ToChars([]byte("abcdef\n가나다\n\tdef"))
|
chars := ToChars([]byte("abcdef\n가나다\n\tdef"))
|
||||||
check := func(multiLine bool, maxLines int, wrapCols int, wrapSignWidth int, tabstop int, expectedNumLines int, expectedOverflow bool) {
|
check := func(multiLine bool, maxLines int, wrapCols int, wrapSignWidth int, tabstop int, expectedNumLines int, expectedOverflow bool) {
|
||||||
lines, overflow := chars.Lines(multiLine, maxLines, wrapCols, wrapSignWidth, tabstop)
|
lines, overflow := chars.Lines(multiLine, maxLines, wrapCols, wrapSignWidth, tabstop, false)
|
||||||
fmt.Println(lines, overflow)
|
fmt.Println(lines, overflow)
|
||||||
if len(lines) != expectedNumLines || overflow != expectedOverflow {
|
if len(lines) != expectedNumLines || overflow != expectedOverflow {
|
||||||
t.Errorf("Invalid result: %d %v (expected %d %v)", len(lines), overflow, expectedNumLines, expectedOverflow)
|
t.Errorf("Invalid result: %d %v (expected %d %v)", len(lines), overflow, expectedNumLines, expectedOverflow)
|
||||||
@@ -81,3 +81,51 @@ func TestCharsLines(t *testing.T) {
|
|||||||
// With wrap sign (3 + 2) and no multi-line
|
// With wrap sign (3 + 2) and no multi-line
|
||||||
check(false, 100, 3, 2, 1, 13, false)
|
check(false, 100, 3, 2, 1, 13, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCharsLinesWrapWord(t *testing.T) {
|
||||||
|
// "hello world foo bar" with width 12 should break at word boundaries
|
||||||
|
chars := ToChars([]byte("hello world foo bar"))
|
||||||
|
lines, overflow := chars.Lines(false, 100, 12, 0, 8, true)
|
||||||
|
// "hello world " (12) | "foo bar" (7)
|
||||||
|
if len(lines) != 2 || overflow {
|
||||||
|
t.Errorf("Expected 2 lines, got %d (overflow: %v): %v", len(lines), overflow, lines)
|
||||||
|
}
|
||||||
|
if string(lines[0]) != "hello world " {
|
||||||
|
t.Errorf("Expected first line 'hello world ', got %q", string(lines[0]))
|
||||||
|
}
|
||||||
|
if string(lines[1]) != "foo bar" {
|
||||||
|
t.Errorf("Expected second line 'foo bar', got %q", string(lines[1]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// No word boundary: a single long word falls back to character wrap
|
||||||
|
chars2 := ToChars([]byte("abcdefghijklmnop"))
|
||||||
|
lines2, _ := chars2.Lines(false, 100, 10, 0, 8, true)
|
||||||
|
if len(lines2) != 2 {
|
||||||
|
t.Errorf("Expected 2 lines for long word, got %d: %v", len(lines2), lines2)
|
||||||
|
}
|
||||||
|
if string(lines2[0]) != "abcdefghij" {
|
||||||
|
t.Errorf("Expected first line 'abcdefghij', got %q", string(lines2[0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab as word boundary
|
||||||
|
chars3 := ToChars([]byte("hello\tworld"))
|
||||||
|
lines3, _ := chars3.Lines(false, 100, 7, 0, 8, true)
|
||||||
|
// "hello\t" should break at tab (width of tab at pos 5 with tabstop 8 = 3, total width = 8 > 7)
|
||||||
|
// Actually RunesWidth: 'h'=1,'e'=1,'l'=1,'l'=1,'o'=1,'\t'=3 = 8 > 7, overflowIdx=5
|
||||||
|
// Then word-wrap scans back and finds no space/tab before idx 5 (tab IS at idx 5 but we check line[k-1])
|
||||||
|
// Wait - let me think: overflowIdx=5, we check k=5 -> line[4]='o', k=4 -> line[3]='l'... no space/tab found
|
||||||
|
// Falls back to character wrap: "hello" | "\tworld"
|
||||||
|
if len(lines3) < 2 {
|
||||||
|
t.Errorf("Expected at least 2 lines for tab test, got %d: %v", len(lines3), lines3)
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrapWord=false still character-wraps
|
||||||
|
chars4 := ToChars([]byte("hello world"))
|
||||||
|
lines4, _ := chars4.Lines(false, 100, 8, 0, 8, false)
|
||||||
|
if len(lines4) != 2 {
|
||||||
|
t.Errorf("Expected 2 lines with wrapWord=false, got %d: %v", len(lines4), lines4)
|
||||||
|
}
|
||||||
|
if string(lines4[0]) != "hello wo" {
|
||||||
|
t.Errorf("Expected first line 'hello wo', got %q", string(lines4[0]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+10
-63
@@ -1,11 +1,11 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cmp"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/mattn/go-isatty"
|
"github.com/mattn/go-isatty"
|
||||||
"github.com/rivo/uniseg"
|
"github.com/rivo/uniseg"
|
||||||
@@ -18,8 +18,13 @@ func StringWidth(s string) int {
|
|||||||
|
|
||||||
// RunesWidth returns runes width
|
// RunesWidth returns runes width
|
||||||
func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int) {
|
func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int) {
|
||||||
|
return StringsWidth(string(runes), prefixWidth, tabstop, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringsWidth returns the width of the string
|
||||||
|
func StringsWidth(str string, prefixWidth int, tabstop int, limit int) (int, int) {
|
||||||
width := 0
|
width := 0
|
||||||
gr := uniseg.NewGraphemes(string(runes))
|
gr := uniseg.NewGraphemes(str)
|
||||||
idx := 0
|
idx := 0
|
||||||
for gr.Next() {
|
for gr.Next() {
|
||||||
rs := gr.Runes()
|
rs := gr.Runes()
|
||||||
@@ -55,54 +60,8 @@ func Truncate(input string, limit int) ([]rune, int) {
|
|||||||
return runes, width
|
return runes, width
|
||||||
}
|
}
|
||||||
|
|
||||||
// Max returns the largest integer
|
func Constrain[T cmp.Ordered](val, minimum, maximum T) T {
|
||||||
func Max(first int, second int) int {
|
return max(min(val, maximum), minimum)
|
||||||
if first >= second {
|
|
||||||
return first
|
|
||||||
}
|
|
||||||
return second
|
|
||||||
}
|
|
||||||
|
|
||||||
// Max16 returns the largest integer
|
|
||||||
func Max16(first int16, second int16) int16 {
|
|
||||||
if first >= second {
|
|
||||||
return first
|
|
||||||
}
|
|
||||||
return second
|
|
||||||
}
|
|
||||||
|
|
||||||
// Max32 returns the largest 32-bit integer
|
|
||||||
func Max32(first int32, second int32) int32 {
|
|
||||||
if first > second {
|
|
||||||
return first
|
|
||||||
}
|
|
||||||
return second
|
|
||||||
}
|
|
||||||
|
|
||||||
// Min returns the smallest integer
|
|
||||||
func Min(first int, second int) int {
|
|
||||||
if first <= second {
|
|
||||||
return first
|
|
||||||
}
|
|
||||||
return second
|
|
||||||
}
|
|
||||||
|
|
||||||
// Min32 returns the smallest 32-bit integer
|
|
||||||
func Min32(first int32, second int32) int32 {
|
|
||||||
if first <= second {
|
|
||||||
return first
|
|
||||||
}
|
|
||||||
return second
|
|
||||||
}
|
|
||||||
|
|
||||||
// Constrain32 limits the given 32-bit integer with the upper and lower bounds
|
|
||||||
func Constrain32(val int32, min int32, max int32) int32 {
|
|
||||||
return Max32(Min32(val, max), min)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Constrain limits the given integer with the upper and lower bounds
|
|
||||||
func Constrain(val int, min int, max int) int {
|
|
||||||
return Max(Min(val, max), min)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func AsUint16(val int) uint16 {
|
func AsUint16(val int) uint16 {
|
||||||
@@ -114,18 +73,6 @@ func AsUint16(val int) uint16 {
|
|||||||
return uint16(val)
|
return uint16(val)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DurWithin limits the given time.Duration with the upper and lower bounds
|
|
||||||
func DurWithin(
|
|
||||||
val time.Duration, min time.Duration, max time.Duration) time.Duration {
|
|
||||||
if val < min {
|
|
||||||
return min
|
|
||||||
}
|
|
||||||
if val > max {
|
|
||||||
return max
|
|
||||||
}
|
|
||||||
return val
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsTty returns true if the file is a terminal
|
// IsTty returns true if the file is a terminal
|
||||||
func IsTty(file *os.File) bool {
|
func IsTty(file *os.File) bool {
|
||||||
fd := file.Fd()
|
fd := file.Fd()
|
||||||
@@ -197,7 +144,7 @@ func CompareVersions(v1, v2 string) int {
|
|||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < Max(len(parts1), len(parts2)); i++ {
|
for i := 0; i < max(len(parts1), len(parts2)); i++ {
|
||||||
var p1, p2 int
|
var p1, p2 int
|
||||||
if i < len(parts1) {
|
if i < len(parts1) {
|
||||||
p1 = atoi(parts1[i])
|
p1 = atoi(parts1[i])
|
||||||
|
|||||||
@@ -4,72 +4,8 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMax(t *testing.T) {
|
|
||||||
if Max(10, 1) != 10 {
|
|
||||||
t.Error("Expected", 10)
|
|
||||||
}
|
|
||||||
if Max(-2, 5) != 5 {
|
|
||||||
t.Error("Expected", 5)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMax16(t *testing.T) {
|
|
||||||
if Max16(10, 1) != 10 {
|
|
||||||
t.Error("Expected", 10)
|
|
||||||
}
|
|
||||||
if Max16(-2, 5) != 5 {
|
|
||||||
t.Error("Expected", 5)
|
|
||||||
}
|
|
||||||
if Max16(math.MaxInt16, 0) != math.MaxInt16 {
|
|
||||||
t.Error("Expected", math.MaxInt16)
|
|
||||||
}
|
|
||||||
if Max16(0, math.MinInt16) != 0 {
|
|
||||||
t.Error("Expected", 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMax32(t *testing.T) {
|
|
||||||
if Max32(10, 1) != 10 {
|
|
||||||
t.Error("Expected", 10)
|
|
||||||
}
|
|
||||||
if Max32(-2, 5) != 5 {
|
|
||||||
t.Error("Expected", 5)
|
|
||||||
}
|
|
||||||
if Max32(math.MaxInt32, 0) != math.MaxInt32 {
|
|
||||||
t.Error("Expected", math.MaxInt32)
|
|
||||||
}
|
|
||||||
if Max32(0, math.MinInt32) != 0 {
|
|
||||||
t.Error("Expected", 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMin(t *testing.T) {
|
|
||||||
if Min(10, 1) != 1 {
|
|
||||||
t.Error("Expected", 1)
|
|
||||||
}
|
|
||||||
if Min(-2, 5) != -2 {
|
|
||||||
t.Error("Expected", -2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMin32(t *testing.T) {
|
|
||||||
if Min32(10, 1) != 1 {
|
|
||||||
t.Error("Expected", 1)
|
|
||||||
}
|
|
||||||
if Min32(-2, 5) != -2 {
|
|
||||||
t.Error("Expected", -2)
|
|
||||||
}
|
|
||||||
if Min32(math.MaxInt32, 0) != 0 {
|
|
||||||
t.Error("Expected", 0)
|
|
||||||
}
|
|
||||||
if Min32(0, math.MinInt32) != math.MinInt32 {
|
|
||||||
t.Error("Expected", math.MinInt32)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConstrain(t *testing.T) {
|
func TestConstrain(t *testing.T) {
|
||||||
if Constrain(-3, -1, 3) != -1 {
|
if Constrain(-3, -1, 3) != -1 {
|
||||||
t.Error("Expected", -1)
|
t.Error("Expected", -1)
|
||||||
@@ -83,22 +19,6 @@ func TestConstrain(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConstrain32(t *testing.T) {
|
|
||||||
if Constrain32(-3, -1, 3) != -1 {
|
|
||||||
t.Error("Expected", -1)
|
|
||||||
}
|
|
||||||
if Constrain32(2, -1, 3) != 2 {
|
|
||||||
t.Error("Expected", 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
if Constrain32(5, -1, 3) != 3 {
|
|
||||||
t.Error("Expected", 3)
|
|
||||||
}
|
|
||||||
if Constrain32(0, math.MinInt32, math.MaxInt32) != 0 {
|
|
||||||
t.Error("Expected", 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAsUint16(t *testing.T) {
|
func TestAsUint16(t *testing.T) {
|
||||||
if AsUint16(5) != 5 {
|
if AsUint16(5) != 5 {
|
||||||
t.Error("Expected", 5)
|
t.Error("Expected", 5)
|
||||||
@@ -120,18 +40,6 @@ func TestAsUint16(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDurWithIn(t *testing.T) {
|
|
||||||
if DurWithin(time.Duration(5), time.Duration(1), time.Duration(8)) != time.Duration(5) {
|
|
||||||
t.Error("Expected", time.Duration(0))
|
|
||||||
}
|
|
||||||
if DurWithin(time.Duration(0)*time.Second, time.Second, time.Duration(3)*time.Second) != time.Second {
|
|
||||||
t.Error("Expected", time.Second)
|
|
||||||
}
|
|
||||||
if DurWithin(time.Duration(10)*time.Second, time.Duration(0), time.Second) != time.Second {
|
|
||||||
t.Error("Expected", time.Second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOnce(t *testing.T) {
|
func TestOnce(t *testing.T) {
|
||||||
o := Once(false)
|
o := Once(false)
|
||||||
if o() {
|
if o() {
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Unset fzf variables
|
||||||
|
set -e FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS FZF_DEFAULT_OPTS_FILE FZF_TMUX FZF_TMUX_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
|
||||||
|
# Unset completion-specific variables
|
||||||
|
set -e FZF_COMPLETION_TRIGGER FZF_COMPLETION_OPTS
|
||||||
|
|
||||||
|
set -gx FZF_DEFAULT_OPTS "--no-scrollbar --pointer '>' --marker '>'"
|
||||||
|
set -gx FZF_COMPLETION_TRIGGER '++'
|
||||||
|
set -gx fish_history fzf_test
|
||||||
|
|
||||||
|
# Add fzf to PATH
|
||||||
|
fish_add_path <%= BASE %>/bin
|
||||||
|
|
||||||
|
# Source key bindings and completion
|
||||||
|
source "<%= BASE %>/shell/key-bindings.fish"
|
||||||
|
source "<%= BASE %>/shell/completion.fish"
|
||||||
+13
-2
@@ -11,6 +11,7 @@ require 'net/http'
|
|||||||
require 'json'
|
require 'json'
|
||||||
|
|
||||||
TEMPLATE = File.read(File.expand_path('common.sh', __dir__))
|
TEMPLATE = File.read(File.expand_path('common.sh', __dir__))
|
||||||
|
FISH_TEMPLATE = File.read(File.expand_path('common.fish', __dir__))
|
||||||
UNSETS = %w[
|
UNSETS = %w[
|
||||||
FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS
|
FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS
|
||||||
FZF_TMUX FZF_TMUX_OPTS
|
FZF_TMUX FZF_TMUX_OPTS
|
||||||
@@ -24,7 +25,7 @@ DEFAULT_TIMEOUT = 10
|
|||||||
FILE = File.expand_path(__FILE__)
|
FILE = File.expand_path(__FILE__)
|
||||||
BASE = File.expand_path('../..', __dir__)
|
BASE = File.expand_path('../..', __dir__)
|
||||||
Dir.chdir(BASE)
|
Dir.chdir(BASE)
|
||||||
FZF = "FZF_DEFAULT_OPTS=\"--no-scrollbar --pointer \\> --marker \\>\" FZF_DEFAULT_COMMAND= #{BASE}/bin/fzf".freeze
|
FZF = %(FZF_DEFAULT_OPTS="--no-scrollbar --gutter ' ' --pointer '>' --marker '>'" FZF_DEFAULT_COMMAND= #{BASE}/bin/fzf).freeze
|
||||||
|
|
||||||
def wait(timeout = DEFAULT_TIMEOUT)
|
def wait(timeout = DEFAULT_TIMEOUT)
|
||||||
since = Time.now
|
since = Time.now
|
||||||
@@ -66,7 +67,16 @@ class Shell
|
|||||||
end
|
end
|
||||||
|
|
||||||
def fish
|
def fish
|
||||||
"unset #{UNSETS.join(' ')}; rm -f ~/.local/share/fish/fzf_test_history; FZF_DEFAULT_OPTS=\"--no-scrollbar --pointer '>' --marker '>'\" fish_history=fzf_test fish"
|
@fish ||=
|
||||||
|
begin
|
||||||
|
confdir = '/tmp/fzf-fish'
|
||||||
|
FileUtils.rm_rf(confdir)
|
||||||
|
FileUtils.mkdir_p("#{confdir}/fish/conf.d")
|
||||||
|
File.open("#{confdir}/fish/conf.d/fzf.fish", 'w') do |f|
|
||||||
|
f.puts ERB.new(FISH_TEMPLATE).result(binding)
|
||||||
|
end
|
||||||
|
"rm -f ~/.local/share/fish/fzf_test_history; XDG_CONFIG_HOME=#{confdir} fish"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -153,6 +163,7 @@ class Tmux
|
|||||||
self.until(true) do |lines|
|
self.until(true) do |lines|
|
||||||
message = "Prepare[#{tries}]"
|
message = "Prepare[#{tries}]"
|
||||||
send_keys ' ', 'C-u', :Enter, message, :Left, :Right
|
send_keys ' ', 'C-u', :Enter, message, :Left, :Right
|
||||||
|
sleep(0.15)
|
||||||
lines[-1] == message
|
lines[-1] == message
|
||||||
end
|
end
|
||||||
rescue Minitest::Assertion
|
rescue Minitest::Assertion
|
||||||
|
|||||||
+164
-5
@@ -1190,6 +1190,61 @@ class TestCore < TestInteractive
|
|||||||
tmux.until { |lines| assert lines.any_include?('9999␊10000') }
|
tmux.until { |lines| assert lines.any_include?('9999␊10000') }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_freeze_left_keep_right
|
||||||
|
tmux.send_keys %(seq 10000 | #{FZF} --read0 --delimiter "\n" --freeze-left 3 --keep-right --ellipsis XX --no-multi-line --bind space:toggle-multi-line), :Enter
|
||||||
|
tmux.until { |lines| assert_match(/^> 1␊2␊3XX.*10000␊$/, lines[-3]) }
|
||||||
|
tmux.send_keys '5'
|
||||||
|
tmux.until { |lines| assert_match(/^> 1␊2␊3␊4␊5␊.*XX$/, lines[-3]) }
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until { |lines| assert lines.any_include?('> 1') }
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until { |lines| assert lines.any_include?('1␊2␊3␊4␊5␊') }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_freeze_left_and_right
|
||||||
|
tmux.send_keys %(seq 10000 | tr "\n" ' ' | #{FZF} --freeze-left 3 --freeze-right 3 --ellipsis XX), :Enter
|
||||||
|
tmux.until { |lines| assert_match(/XX9998 9999 10000$/, lines[-3]) }
|
||||||
|
tmux.send_keys "'1000"
|
||||||
|
tmux.until { |lines| assert_match(/^> 1 2 3XX.*XX9998 9999 10000$/, lines[-3]) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_freeze_left_and_right_delimiter
|
||||||
|
tmux.send_keys %(seq 10000 | tr "\n" ' ' | sed 's/ / , /g' | #{FZF} --freeze-left 3 --freeze-right 3 --ellipsis XX --delimiter ' , '), :Enter
|
||||||
|
tmux.until { |lines| assert_match(/XX, 9999 , 10000 ,$/, lines[-3]) }
|
||||||
|
tmux.send_keys "'1000"
|
||||||
|
tmux.until { |lines| assert_match(/^> 1 , 2 , 3 ,XX.*XX, 9999 , 10000 ,$/, lines[-3]) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_freeze_right_exceed_range
|
||||||
|
tmux.send_keys %(seq 10000 | tr "\n" ' ' | #{FZF} --freeze-right 100000 --ellipsis XX), :Enter
|
||||||
|
['', "'1000"].each do |query|
|
||||||
|
tmux.send_keys query
|
||||||
|
tmux.until { |lines| assert lines.any_include?("> #{query}".strip) }
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_match(/ 9998 9999 10000$/, lines[-3])
|
||||||
|
assert_equal(1, lines[-3].scan('XX').size)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_freeze_right_exceed_range_with_freeze_left
|
||||||
|
tmux.send_keys %(seq 10000 | tr "\n" ' ' | #{FZF} --freeze-left 3 --freeze-right 100000 --ellipsis XX), :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_match(/^> 1 2 3XX.*9998 9999 10000$/, lines[-3])
|
||||||
|
assert_equal(1, lines[-3].scan('XX').size)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_freeze_right_with_ellipsis_and_scrolling
|
||||||
|
tmux.send_keys "{ seq 6; ruby -e 'print \"g\"*1000, \"\\n\"'; seq 8 100; } | #{FZF} --ellipsis='777' --freeze-right 1 --scroll-off 0 --bind a:offset-up", :Enter
|
||||||
|
tmux.until { |lines| assert_equal ' 100/100', lines[-2] }
|
||||||
|
tmux.send_keys(*Array.new(6) { :a })
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_match(/> 777g+$/, lines[-3])
|
||||||
|
assert_equal(1, lines.count { |l| l.end_with?('g') })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def test_backward_eof
|
def test_backward_eof
|
||||||
tmux.send_keys "echo foo | #{FZF} --bind 'backward-eof:reload(seq 100)'", :Enter
|
tmux.send_keys "echo foo | #{FZF} --bind 'backward-eof:reload(seq 100)'", :Enter
|
||||||
tmux.until { |lines| lines.item_count == 1 && lines.match_count == 1 }
|
tmux.until { |lines| lines.item_count == 1 && lines.match_count == 1 }
|
||||||
@@ -1533,14 +1588,16 @@ class TestCore < TestInteractive
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_track_action
|
def test_track_action
|
||||||
tmux.send_keys "seq 1000 | #{FZF} --query 555 --bind t:track", :Enter
|
tmux.send_keys "seq 1000 | #{FZF} --pointer x --query 555 --bind t:track,T:up+track", :Enter
|
||||||
tmux.until do |lines|
|
tmux.until do |lines|
|
||||||
assert_equal 1, lines.match_count
|
assert_equal 1, lines.match_count
|
||||||
|
assert_includes lines, 'x 555'
|
||||||
assert_includes lines, '> 555'
|
assert_includes lines, '> 555'
|
||||||
end
|
end
|
||||||
tmux.send_keys :BSpace
|
tmux.send_keys :BSpace
|
||||||
tmux.until do |lines|
|
tmux.until do |lines|
|
||||||
assert_equal 28, lines.match_count
|
assert_equal 28, lines.match_count
|
||||||
|
assert_includes lines, 'x 55'
|
||||||
assert_includes lines, '> 55'
|
assert_includes lines, '> 55'
|
||||||
end
|
end
|
||||||
tmux.send_keys :t
|
tmux.send_keys :t
|
||||||
@@ -1550,7 +1607,8 @@ class TestCore < TestInteractive
|
|||||||
tmux.send_keys :BSpace
|
tmux.send_keys :BSpace
|
||||||
tmux.until do |lines|
|
tmux.until do |lines|
|
||||||
assert_equal 271, lines.match_count
|
assert_equal 271, lines.match_count
|
||||||
assert_includes lines, '> 55'
|
assert_includes lines, 'x 55'
|
||||||
|
assert_includes lines, '> 5'
|
||||||
end
|
end
|
||||||
|
|
||||||
# Automatically disabled when the tracking item is no longer visible
|
# Automatically disabled when the tracking item is no longer visible
|
||||||
@@ -1562,16 +1620,33 @@ class TestCore < TestInteractive
|
|||||||
tmux.send_keys :BSpace
|
tmux.send_keys :BSpace
|
||||||
tmux.until do |lines|
|
tmux.until do |lines|
|
||||||
assert_equal 271, lines.match_count
|
assert_equal 271, lines.match_count
|
||||||
|
assert_includes lines, 'x 52'
|
||||||
assert_includes lines, '> 5'
|
assert_includes lines, '> 5'
|
||||||
end
|
end
|
||||||
tmux.send_keys :t
|
tmux.send_keys :t
|
||||||
tmux.until do |lines|
|
tmux.until do |lines|
|
||||||
assert_includes lines[-2], '+t'
|
assert_includes lines[-2], '+t'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Automatically disabled when the focus has moved
|
||||||
tmux.send_keys :Up
|
tmux.send_keys :Up
|
||||||
tmux.until do |lines|
|
tmux.until do |lines|
|
||||||
|
assert_includes lines, 'x 53'
|
||||||
refute_includes lines[-2], '+t'
|
refute_includes lines[-2], '+t'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Should work even when combined with a focus moving actions
|
||||||
|
tmux.send_keys 'T'
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_includes lines, 'x 54'
|
||||||
|
assert_includes lines[-2], '+t'
|
||||||
|
end
|
||||||
|
|
||||||
|
tmux.send_keys 'T'
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_includes lines, 'x 55'
|
||||||
|
assert_includes lines[-2], '+t'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_one_and_zero
|
def test_one_and_zero
|
||||||
@@ -1680,8 +1755,9 @@ class TestCore < TestInteractive
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
tmux.send_keys %(seq 100 | #{FZF} --multi --reverse --preview-window 0 --preview 'env | grep ^FZF_ | sort > #{tempname}' --no-input --bind enter:show-input+refresh-preview,space:disable-search+refresh-preview), :Enter
|
tmux.send_keys %({ echo foo; seq 100; } | #{FZF} --header-lines 1 --multi --reverse --preview-window 0 --preview 'env | grep ^FZF_ | sort > #{tempname}' --no-input --bind enter:show-input+refresh-preview,space:disable-search+refresh-preview), :Enter
|
||||||
expected = {
|
expected = {
|
||||||
|
FZF_DIRECTION: 'down',
|
||||||
FZF_TOTAL_COUNT: '100',
|
FZF_TOTAL_COUNT: '100',
|
||||||
FZF_MATCH_COUNT: '100',
|
FZF_MATCH_COUNT: '100',
|
||||||
FZF_SELECT_COUNT: '0',
|
FZF_SELECT_COUNT: '0',
|
||||||
@@ -2004,7 +2080,7 @@ class TestCore < TestInteractive
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
elapsed = Time.now - time
|
elapsed = Time.now - time
|
||||||
assert elapsed < 2
|
assert_operator elapsed, :<, 2
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_bg_cancel
|
def test_bg_cancel
|
||||||
@@ -2017,7 +2093,7 @@ class TestCore < TestInteractive
|
|||||||
tmux.until { assert_equal 2, it.match_count }
|
tmux.until { assert_equal 2, it.match_count }
|
||||||
tmux.send_keys :Space
|
tmux.send_keys :Space
|
||||||
tmux.until { |lines| assert lines.any_include?('[0]') }
|
tmux.until { |lines| assert lines.any_include?('[0]') }
|
||||||
sleep 2
|
sleep(2)
|
||||||
tmux.until do |lines|
|
tmux.until do |lines|
|
||||||
assert lines.any_include?('[0]')
|
assert lines.any_include?('[0]')
|
||||||
refute lines.any_include?('[1]')
|
refute lines.any_include?('[1]')
|
||||||
@@ -2099,4 +2175,87 @@ class TestCore < TestInteractive
|
|||||||
assert_equal 1, it.match_count
|
assert_equal 1, it.match_count
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_change_header_lines
|
||||||
|
tmux.send_keys %(seq 10 | #{FZF} --header-lines 3 --bind 'space:change-header-lines(5),enter:transform-header-lines(echo 1)'), :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 7, lines.item_count
|
||||||
|
assert lines.any_include?('> 4')
|
||||||
|
end
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 5, lines.item_count
|
||||||
|
assert lines.any_include?('> 6')
|
||||||
|
end
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 9, lines.item_count
|
||||||
|
assert lines.any_include?('> 6')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_change_header_lines_to_zero
|
||||||
|
tmux.send_keys %(seq 5 | #{FZF} --header-lines 3 --bind 'space:bg-transform-header-lines(echo 0)'), :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 2, lines.item_count
|
||||||
|
assert lines.any_include?('> 4')
|
||||||
|
end
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 5, lines.item_count
|
||||||
|
# All items are now in the list, cursor stays on item 4
|
||||||
|
assert lines.any_include?('> 4')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_change_header_lines_deselect
|
||||||
|
# Selected items that become part of the header should be deselected
|
||||||
|
tmux.send_keys %(seq 10 | #{FZF} --multi --header-lines 0 --bind 'space:change-header-lines(3),enter:change-header-lines(1)'), :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 10, lines.item_count
|
||||||
|
assert lines.any_include?('> 1')
|
||||||
|
end
|
||||||
|
# Select items 1, 2, 3 (these will become header lines)
|
||||||
|
tmux.send_keys :BTab, :BTab, :BTab
|
||||||
|
tmux.until { |lines| assert_equal 3, lines.select_count }
|
||||||
|
# Also select item 4 (this should remain selected)
|
||||||
|
tmux.send_keys :BTab
|
||||||
|
tmux.until { |lines| assert_equal 4, lines.select_count }
|
||||||
|
# Change header-lines to 3: items 1, 2, 3 become headers and should be deselected
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 7, lines.item_count
|
||||||
|
assert_equal 1, lines.select_count
|
||||||
|
assert lines.any_include?('> 5')
|
||||||
|
end
|
||||||
|
# Change header-lines to 1
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 9, lines.item_count
|
||||||
|
assert_equal 1, lines.select_count
|
||||||
|
assert lines.any_include?('> 5')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_change_header_lines_reverse
|
||||||
|
tmux.send_keys %(seq 10 | #{FZF} --header-lines 2 --reverse --bind 'space:change-header-lines(4)'), :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 8, lines.item_count
|
||||||
|
assert lines.any_include?('> 3')
|
||||||
|
end
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 6, lines.item_count
|
||||||
|
assert lines.any_include?('> 5')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_zero_width_characters
|
||||||
|
tmux.send_keys %(for i in {1..1000}; do string+="a̱$i"; printf '\\e[43m%s\\e[0m\\n' "$string"; done | #{FZF} --ansi --query a500 --ellipsis XX), :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 981, lines.match_count
|
||||||
|
assert_match(/^> XX.*a̱500/, lines[-3])
|
||||||
|
assert(lines.reverse.drop(5).all? { it.match?(/^ XX.*a̱500.*XX/) })
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+1
-1
@@ -403,7 +403,7 @@ class TestExec < TestInteractive
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_become
|
def test_become
|
||||||
tmux.send_keys "seq 100 | #{FZF} --bind 'enter:become:seq {} | #{FZF}'", :Enter
|
tmux.send_keys "seq 100 | fzf --bind 'enter:become:seq {} | fzf'", :Enter
|
||||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||||
tmux.send_keys 999
|
tmux.send_keys 999
|
||||||
tmux.until { |lines| assert_equal 0, lines.match_count }
|
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||||
|
|||||||
@@ -312,4 +312,41 @@ class TestFilter < TestBase
|
|||||||
assert_equal expected, result
|
assert_equal expected, result
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_accept_nth
|
||||||
|
# Single field selection
|
||||||
|
assert_equal 'three', `echo 'one two three' | #{FZF} -d' ' --with-nth 1 --accept-nth -1 -f one`.chomp
|
||||||
|
|
||||||
|
# Multiple field selection
|
||||||
|
writelines(['ID001:John:Developer', 'ID002:Jane:Manager', 'ID003:Bob:Designer'])
|
||||||
|
assert_equal 'ID001', `#{FZF} -d: --with-nth 2 --accept-nth 1 -f John < #{tempname}`.chomp
|
||||||
|
assert_equal 'ID002:Manager', `#{FZF} -d: --with-nth 2 --accept-nth 1,3 -f Jane < #{tempname}`.chomp
|
||||||
|
|
||||||
|
# Test with different delimiters
|
||||||
|
writelines(['emp001 Alice Engineering', 'emp002 Bob Marketing'])
|
||||||
|
assert_equal 'emp001', `#{FZF} -d' ' --with-nth 2 --accept-nth 1 -f Alice < #{tempname}`.chomp
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_header_lines_filter
|
||||||
|
assert_equal %w[4 5 6 7 8 9 10],
|
||||||
|
`seq 10 | #{FZF} --header-lines 3 -f ""`.lines(chomp: true)
|
||||||
|
assert_equal %w[5],
|
||||||
|
`seq 10 | #{FZF} --header-lines 3 -f 5`.lines(chomp: true)
|
||||||
|
# Header items should not be matched
|
||||||
|
assert_empty `seq 10 | #{FZF} --header-lines 3 -f "^1$"`.lines(chomp: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_header_lines_filter_with_nth
|
||||||
|
writelines(%w[a:1 b:2 c:3 d:4 e:5])
|
||||||
|
assert_equal %w[c:3 d:4 e:5],
|
||||||
|
`#{FZF} --header-lines 2 -d: --with-nth 2 -f "" < #{tempname}`.lines(chomp: true)
|
||||||
|
assert_equal %w[d:4],
|
||||||
|
`#{FZF} --header-lines 2 -d: --with-nth 2 -f 4 < #{tempname}`.lines(chomp: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_header_lines_all_headers
|
||||||
|
# When all lines are header lines, no results
|
||||||
|
assert_empty `seq 3 | #{FZF} --header-lines 10 -f ""`.chomp
|
||||||
|
assert_equal 1, $CHILD_STATUS.exitstatus
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+84
-16
@@ -178,8 +178,8 @@ class TestLayout < TestInteractive
|
|||||||
tmux.send_keys 'seq 3 | fzf --height ~100% --info=inline --border rounded', :Enter
|
tmux.send_keys 'seq 3 | fzf --height ~100% --info=inline --border rounded', :Enter
|
||||||
expected = <<~OUTPUT
|
expected = <<~OUTPUT
|
||||||
╭──────────
|
╭──────────
|
||||||
│ 3
|
│ ▌ 3
|
||||||
│ 2
|
│ ▌ 2
|
||||||
│ > 1
|
│ > 1
|
||||||
│ > < 3/3
|
│ > < 3/3
|
||||||
╰──────────
|
╰──────────
|
||||||
@@ -197,8 +197,8 @@ class TestLayout < TestInteractive
|
|||||||
│ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
│ ╰────────
|
│ ╰────────
|
||||||
│ 3
|
│ ▌ 3
|
||||||
│ 2
|
│ ▌ 2
|
||||||
│ > 1
|
│ > 1
|
||||||
│ > < 3/3
|
│ > < 3/3
|
||||||
╰──────────
|
╰──────────
|
||||||
@@ -247,7 +247,7 @@ class TestLayout < TestInteractive
|
|||||||
tmux.send_keys 'seq 100 | fzf --height ~5 --info=inline --border rounded', :Enter
|
tmux.send_keys 'seq 100 | fzf --height ~5 --info=inline --border rounded', :Enter
|
||||||
expected = <<~OUTPUT
|
expected = <<~OUTPUT
|
||||||
╭──────────────
|
╭──────────────
|
||||||
│ 2
|
│ ▌ 2
|
||||||
│ > 1
|
│ > 1
|
||||||
│ > < 100/100
|
│ > < 100/100
|
||||||
╰──────────────
|
╰──────────────
|
||||||
@@ -275,12 +275,12 @@ class TestLayout < TestInteractive
|
|||||||
def test_fzf_multi_line
|
def test_fzf_multi_line
|
||||||
tmux.send_keys %[(echo -en '0\\0'; echo -en '1\\n2\\0'; seq 1000) | fzf --read0 --multi --bind load:select-all --border rounded], :Enter
|
tmux.send_keys %[(echo -en '0\\0'; echo -en '1\\n2\\0'; seq 1000) | fzf --read0 --multi --bind load:select-all --border rounded], :Enter
|
||||||
block = <<~BLOCK
|
block = <<~BLOCK
|
||||||
│ ┃998
|
│ ▌┃998
|
||||||
│ ┃999
|
│ ▌┃999
|
||||||
│ ┃1000
|
│ ▌┃1000
|
||||||
│ ╹
|
│ ▌╹
|
||||||
│ ╻1
|
│ ▌╻1
|
||||||
│ ╹2
|
│ ▌╹2
|
||||||
│ >>0
|
│ >>0
|
||||||
│ 3/3 (3)
|
│ 3/3 (3)
|
||||||
│ >
|
│ >
|
||||||
@@ -312,11 +312,11 @@ class TestLayout < TestInteractive
|
|||||||
│ >
|
│ >
|
||||||
│ 3/3 (3)
|
│ 3/3 (3)
|
||||||
│ >>0
|
│ >>0
|
||||||
│ ╻1
|
│ ▌╻1
|
||||||
│ ╹2
|
│ ▌╹2
|
||||||
│ ╻1
|
│ ▌╻1
|
||||||
│ ┃2
|
│ ▌┃2
|
||||||
│ ┃3
|
│ ▌┃3
|
||||||
BLOCK
|
BLOCK
|
||||||
tmux.until { assert_block(block, it) }
|
tmux.until { assert_block(block, it) }
|
||||||
end
|
end
|
||||||
@@ -1156,6 +1156,74 @@ class TestLayout < TestInteractive
|
|||||||
tmux.until { assert_block(block, it) }
|
tmux.until { assert_block(block, it) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_gutter_default
|
||||||
|
tmux.send_keys %(seq 10 | fzf), :Enter
|
||||||
|
block = <<~BLOCK
|
||||||
|
▌ 3
|
||||||
|
▌ 2
|
||||||
|
> 1
|
||||||
|
10/10
|
||||||
|
>
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_gutter_default_no_unicode
|
||||||
|
tmux.send_keys %(seq 10 | fzf --no-unicode), :Enter
|
||||||
|
block = <<~BLOCK
|
||||||
|
3
|
||||||
|
2
|
||||||
|
> 1
|
||||||
|
10/10
|
||||||
|
>
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_gutter_custom
|
||||||
|
tmux.send_keys %(seq 10 | fzf --gutter x), :Enter
|
||||||
|
block = <<~BLOCK
|
||||||
|
x 3
|
||||||
|
x 2
|
||||||
|
> 1
|
||||||
|
10/10
|
||||||
|
>
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
# https://github.com/junegunn/fzf/issues/4537
|
||||||
|
def test_no_scrollbar_preview_toggle
|
||||||
|
x = 'x' * 300
|
||||||
|
y = 'y' * 300
|
||||||
|
tmux.send_keys %(yes #{x} | head -1000 | fzf --bind 'tab:toggle-preview' --border --no-scrollbar --preview 'echo #{y}' --preview-window 'border-left'), :Enter
|
||||||
|
|
||||||
|
# │ ▌ xxxxxxxx·· │ yyyyyyyy│
|
||||||
|
tmux.until do |lines|
|
||||||
|
lines.any? { it.match?(/x·· │ y+│$/) }
|
||||||
|
end
|
||||||
|
tmux.send_keys :Tab
|
||||||
|
|
||||||
|
# │ ▌ xxxxxxxx·· │
|
||||||
|
tmux.until do |lines|
|
||||||
|
lines.none? { it.match?(/x··y│$/) }
|
||||||
|
end
|
||||||
|
|
||||||
|
tmux.send_keys :Tab
|
||||||
|
tmux.until do |lines|
|
||||||
|
lines.any? { it.match?(/x·· │ y+│$/) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_header_and_footer_should_not_be_wider_than_list
|
||||||
|
tmux.send_keys %(WIDE=$(printf 'x%.0s' {1..1000}); (echo $WIDE; echo $WIDE) | fzf --header-lines 1 --style full --header-border bottom --header-lines-border top --ellipsis XX --header "$WIDE" --footer "$WIDE" --no-footer-border), :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
matches = lines.filter_map { |line| line[/x+XX/] }
|
||||||
|
assert_equal 4, matches.length
|
||||||
|
assert_equal 1, matches.uniq.length
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def test_combinations
|
def test_combinations
|
||||||
skip unless ENV['LONGTEST']
|
skip unless ENV['LONGTEST']
|
||||||
|
|
||||||
|
|||||||
+38
-1
@@ -383,6 +383,16 @@ class TestPreview < TestInteractive
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_preview_follow_wrap
|
||||||
|
tmux.send_keys "seq 1 | #{FZF} --preview 'seq 1000' --preview-window right,2,follow,wrap", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
|
tmux.until do |lines|
|
||||||
|
idx = lines.rindex { it.include?('│ 10 │') }
|
||||||
|
assert_includes lines[idx + 1], '│ ↳ │'
|
||||||
|
assert_includes lines[idx + 2], '│ ↳ │'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def test_close
|
def test_close
|
||||||
tmux.send_keys "seq 100 | #{FZF} --preview 'echo foo' --bind ctrl-c:close", :Enter
|
tmux.send_keys "seq 100 | #{FZF} --preview 'echo foo' --bind ctrl-c:close", :Enter
|
||||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||||
@@ -541,7 +551,7 @@ class TestPreview < TestInteractive
|
|||||||
tmux.send_keys "seq 10 | #{FZF} --preview-border rounded --preview-window '~5,2,+0,<100000(~0,+100,wrap,noinfo)' --preview 'seq 1000'", :Enter
|
tmux.send_keys "seq 10 | #{FZF} --preview-border rounded --preview-window '~5,2,+0,<100000(~0,+100,wrap,noinfo)' --preview 'seq 1000'", :Enter
|
||||||
tmux.until { |lines| assert_equal 10, lines.match_count }
|
tmux.until { |lines| assert_equal 10, lines.match_count }
|
||||||
tmux.until do |lines|
|
tmux.until do |lines|
|
||||||
assert_equal ['╭────╮', '│ 10 │', '│ ↳ 0│', '│ 10 │', '│ ↳ 1│'], lines.take(5).map(&:strip)
|
assert_equal ['╭────╮', '│ 10 │', '│ ↳ │', '│ 10 │', '│ ↳ │'], lines.take(5).map(&:strip)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -572,4 +582,31 @@ class TestPreview < TestInteractive
|
|||||||
assert_equal 1, lines.match_count
|
assert_equal 1, lines.match_count
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_preview_wrap_sign_between_ansi_fragments
|
||||||
|
tmux.send_keys %(seq 1 | #{FZF} --preview 'echo -e "\\x1b[33m1234567890 \\x1b[mhello"; echo -e "\\x1b[33m1234567890 \\x1b[mhello"' --preview-window 10,wrap-word), :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 1, lines.match_count
|
||||||
|
assert_equal(2, lines.count { |line| line.include?('│ 1234567890 │') })
|
||||||
|
assert_equal(2, lines.count { |line| line.include?('│ ↳ hello │') })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_preview_wrap_sign_between_ansi_fragments_overflow
|
||||||
|
tmux.send_keys %(seq 1 | #{FZF} --preview 'echo -e "\\x1b[33m1234567890 \\x1b[mhello"; echo -e "\\x1b[33m1234567890 \\x1b[mhello"' --preview-window 2,wrap-word), :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 1, lines.match_count
|
||||||
|
assert_equal(2, lines.count { |line| line.include?('│ 12 │') })
|
||||||
|
assert_equal(0, lines.count { |line| line.include?('│ h') })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_preview_wrap_sign_between_ansi_fragments_overflow2
|
||||||
|
tmux.send_keys %(seq 1 | #{FZF} --preview 'echo -e "\\x1b[33m1234567890 \\x1b[mhello"; echo -e "\\x1b[33m1234567890 \\x1b[mhello"' --preview-window 1,wrap-word), :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 1, lines.match_count
|
||||||
|
assert_equal(2, lines.count { |line| line.include?('│ 1 │') })
|
||||||
|
assert_equal(0, lines.count { |line| line.include?('│ h') })
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_relative 'lib/common'
|
||||||
|
|
||||||
|
# Testing raw mode
|
||||||
|
class TestRaw < TestInteractive
|
||||||
|
def test_raw_mode
|
||||||
|
tmux.send_keys %(seq 1000 | #{FZF} --raw --bind ctrl-x:toggle-raw,a:enable-raw,b:disable-raw --gutter '▌' --multi --bind 'space:transform-prompt:echo "[[$FZF_RAW]] "'), :Enter
|
||||||
|
tmux.until { assert_equal 1000, it.match_count }
|
||||||
|
|
||||||
|
tmux.send_keys 1
|
||||||
|
tmux.until { assert_equal 272, it.match_count }
|
||||||
|
|
||||||
|
tmux.send_keys :Up
|
||||||
|
tmux.until { assert_includes it, '> 2' }
|
||||||
|
|
||||||
|
tmux.send_keys 'C-p'
|
||||||
|
tmux.until do
|
||||||
|
assert_includes it, '> 10'
|
||||||
|
assert_includes it, '▖ 9'
|
||||||
|
end
|
||||||
|
|
||||||
|
tmux.send_keys 'C-x'
|
||||||
|
tmux.until do
|
||||||
|
assert_includes it, '> 10'
|
||||||
|
assert_includes it, '▌ 1'
|
||||||
|
end
|
||||||
|
|
||||||
|
tmux.send_keys :Up, 'C-x'
|
||||||
|
tmux.until do
|
||||||
|
assert_includes it, '> 11'
|
||||||
|
assert_includes it, '▖ 10'
|
||||||
|
end
|
||||||
|
|
||||||
|
tmux.send_keys 1
|
||||||
|
tmux.until { assert_equal 28, it.match_count }
|
||||||
|
|
||||||
|
tmux.send_keys 'C-p'
|
||||||
|
tmux.until do
|
||||||
|
assert_includes it, '> 101'
|
||||||
|
assert_includes it, '▖ 100'
|
||||||
|
end
|
||||||
|
|
||||||
|
tmux.send_keys 'C-n'
|
||||||
|
tmux.until do
|
||||||
|
assert_includes it, '> 11'
|
||||||
|
assert_includes it, '▖ 10'
|
||||||
|
end
|
||||||
|
|
||||||
|
tmux.send_keys :Tab, :Tab, :Tab
|
||||||
|
tmux.until { assert_equal 3, it.select_count }
|
||||||
|
|
||||||
|
tmux.send_keys 'C-x'
|
||||||
|
tmux.until do
|
||||||
|
assert_equal 1, it.select_count
|
||||||
|
assert_includes it, '▌ 110'
|
||||||
|
assert_includes it, '>>11'
|
||||||
|
end
|
||||||
|
|
||||||
|
tmux.send_keys 'a'
|
||||||
|
tmux.until do
|
||||||
|
assert_equal 1, it.select_count
|
||||||
|
assert_includes it, '>>11'
|
||||||
|
assert_includes it, '▖ 10'
|
||||||
|
end
|
||||||
|
|
||||||
|
tmux.send_keys :Down, :Space
|
||||||
|
tmux.until { assert_includes it, '[[0]] 11' }
|
||||||
|
|
||||||
|
tmux.send_keys :Up, :Space
|
||||||
|
tmux.until { assert_includes it, '[[1]] 11' }
|
||||||
|
|
||||||
|
tmux.send_keys 'b'
|
||||||
|
tmux.until do
|
||||||
|
assert_equal 1, it.select_count
|
||||||
|
assert_includes it, '▌ 110'
|
||||||
|
assert_includes it, '>>11'
|
||||||
|
end
|
||||||
|
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until { assert_includes it, '[[]] 11' }
|
||||||
|
|
||||||
|
tmux.send_keys 'C-u', '5'
|
||||||
|
tmux.until { assert_includes it, '> 5' }
|
||||||
|
|
||||||
|
tmux.send_keys 'C-x', 'C-p', 'C-p'
|
||||||
|
tmux.until do
|
||||||
|
assert_includes it, '> 25'
|
||||||
|
assert_includes it, '▖ 24'
|
||||||
|
end
|
||||||
|
|
||||||
|
tmux.send_keys 'C-x'
|
||||||
|
tmux.until do
|
||||||
|
assert_includes it, '> 25'
|
||||||
|
assert_includes it, '▌ 15'
|
||||||
|
end
|
||||||
|
|
||||||
|
# 35 is the closest match in raw mode
|
||||||
|
tmux.send_keys 'C-x', :Up, :Up, :Up, :Up, :Up, :Up, 'C-x'
|
||||||
|
tmux.until do
|
||||||
|
assert_includes it, '> 35'
|
||||||
|
assert_includes it, '▌ 25'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_raw_best
|
||||||
|
tmux.send_keys %(seq 1000 | #{FZF} --raw --bind space:best), :Enter
|
||||||
|
tmux.send_keys 999
|
||||||
|
tmux.until { assert_includes it, '> 1' }
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until { assert_includes it, '> 999' }
|
||||||
|
end
|
||||||
|
end
|
||||||
+13
-3
@@ -31,22 +31,32 @@ class TestServer < TestInteractive
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_listen_with_api_key
|
def test_listen_with_api_key
|
||||||
post_uri = URI('http://localhost:6266')
|
uri = URI('http://localhost:6266')
|
||||||
tmux.send_keys 'seq 10 | FZF_API_KEY=123abc fzf --listen 6266', :Enter
|
tmux.send_keys 'seq 10 | FZF_API_KEY=123abc fzf --listen 6266', :Enter
|
||||||
tmux.until { |lines| assert_equal 10, lines.match_count }
|
tmux.until { |lines| assert_equal 10, lines.match_count }
|
||||||
# Incorrect API Key
|
# Incorrect API Key
|
||||||
[nil, { 'x-api-key' => '' }, { 'x-api-key' => '124abc' }].each do |headers|
|
[nil, { 'x-api-key' => '' }, { 'x-api-key' => '124abc' }].each do |headers|
|
||||||
res = Net::HTTP.post(post_uri, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ', headers)
|
res = Net::HTTP.post(uri, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ', headers)
|
||||||
|
assert_equal '401', res.code
|
||||||
|
assert_equal 'Unauthorized', res.message
|
||||||
|
assert_equal "invalid api key\n", res.body
|
||||||
|
|
||||||
|
res = Net::HTTP.get_response(uri, headers)
|
||||||
assert_equal '401', res.code
|
assert_equal '401', res.code
|
||||||
assert_equal 'Unauthorized', res.message
|
assert_equal 'Unauthorized', res.message
|
||||||
assert_equal "invalid api key\n", res.body
|
assert_equal "invalid api key\n", res.body
|
||||||
end
|
end
|
||||||
|
|
||||||
# Valid API Key
|
# Valid API Key
|
||||||
[{ 'x-api-key' => '123abc' }, { 'X-API-Key' => '123abc' }].each do |headers|
|
[{ 'x-api-key' => '123abc' }, { 'X-API-Key' => '123abc' }].each do |headers|
|
||||||
res = Net::HTTP.post(post_uri, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ', headers)
|
res = Net::HTTP.post(uri, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ', headers)
|
||||||
assert_equal '200', res.code
|
assert_equal '200', res.code
|
||||||
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] }
|
||||||
|
|
||||||
|
res = Net::HTTP.get_response(uri, headers)
|
||||||
|
assert_equal '200', res.code
|
||||||
|
assert_equal 'yo', JSON.parse(res.body, symbolize_names: true)[:query]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+614
-76
@@ -27,6 +27,10 @@ module TestShell
|
|||||||
tmux.prepare
|
tmux.prepare
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def trigger
|
||||||
|
'**'
|
||||||
|
end
|
||||||
|
|
||||||
def test_ctrl_t
|
def test_ctrl_t
|
||||||
set_var('FZF_CTRL_T_COMMAND', 'seq 100')
|
set_var('FZF_CTRL_T_COMMAND', 'seq 100')
|
||||||
|
|
||||||
@@ -134,7 +138,7 @@ module TestShell
|
|||||||
tmux.send_keys 'C-r'
|
tmux.send_keys 'C-r'
|
||||||
tmux.until { |lines| assert_equal '>', lines[-1] }
|
tmux.until { |lines| assert_equal '>', lines[-1] }
|
||||||
tmux.send_keys 'foo bar'
|
tmux.send_keys 'foo bar'
|
||||||
tmux.until { |lines| assert_includes lines[-4], '"foo' } unless shell == :zsh
|
tmux.until { |lines| assert_includes lines[-4], '"foo' } if shell == :bash
|
||||||
tmux.until { |lines| assert lines[-3]&.match?(/bar"␊?/) }
|
tmux.until { |lines| assert lines[-3]&.match?(/bar"␊?/) }
|
||||||
tmux.send_keys :Enter
|
tmux.send_keys :Enter
|
||||||
tmux.until { |lines| assert lines[-1]&.match?(/bar"␊?/) }
|
tmux.until { |lines| assert lines[-1]&.match?(/bar"␊?/) }
|
||||||
@@ -165,7 +169,11 @@ module CompletionTest
|
|||||||
FileUtils.touch(File.expand_path(f))
|
FileUtils.touch(File.expand_path(f))
|
||||||
end
|
end
|
||||||
tmux.prepare
|
tmux.prepare
|
||||||
tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab
|
if shell == :fish
|
||||||
|
tmux.send_keys 'cat /tmp/fzf-test/10', 'C-t'
|
||||||
|
else
|
||||||
|
tmux.send_keys "cat /tmp/fzf-test/10#{trigger}", :Tab
|
||||||
|
end
|
||||||
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||||
tmux.send_keys ' !d'
|
tmux.send_keys ' !d'
|
||||||
tmux.until { |lines| assert_equal 2, lines.match_count }
|
tmux.until { |lines| assert_equal 2, lines.match_count }
|
||||||
@@ -179,7 +187,11 @@ module CompletionTest
|
|||||||
# ~USERNAME**<TAB>
|
# ~USERNAME**<TAB>
|
||||||
user = `whoami`.chomp
|
user = `whoami`.chomp
|
||||||
tmux.send_keys 'C-u'
|
tmux.send_keys 'C-u'
|
||||||
tmux.send_keys "cat ~#{user}**", :Tab
|
if shell == :fish
|
||||||
|
tmux.send_keys "cat ~#{user}", 'C-t'
|
||||||
|
else
|
||||||
|
tmux.send_keys "cat ~#{user}#{trigger}", :Tab
|
||||||
|
end
|
||||||
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||||
tmux.send_keys "/#{user}"
|
tmux.send_keys "/#{user}"
|
||||||
tmux.until { |lines| assert(lines.any? { |l| l.end_with?("/#{user}") }) }
|
tmux.until { |lines| assert(lines.any? { |l| l.end_with?("/#{user}") }) }
|
||||||
@@ -190,14 +202,29 @@ module CompletionTest
|
|||||||
|
|
||||||
# ~INVALID_USERNAME**<TAB>
|
# ~INVALID_USERNAME**<TAB>
|
||||||
tmux.send_keys 'C-u'
|
tmux.send_keys 'C-u'
|
||||||
tmux.send_keys 'cat ~such**', :Tab
|
if shell == :fish
|
||||||
|
tmux.send_keys 'cat ~such', 'C-t'
|
||||||
|
else
|
||||||
|
tmux.send_keys "cat ~such#{trigger}", :Tab
|
||||||
|
end
|
||||||
tmux.until(true) { |lines| assert lines.any_include?('no~such~user') }
|
tmux.until(true) { |lines| assert lines.any_include?('no~such~user') }
|
||||||
tmux.send_keys :Enter
|
tmux.send_keys :Enter
|
||||||
tmux.until(true) { |lines| assert_equal 'cat no~such~user', lines[-1] }
|
tmux.until(true) do |lines|
|
||||||
|
if shell == :fish
|
||||||
|
# Fish's string escape quotes filenames with ~ to prevent tilde expansion
|
||||||
|
assert_equal 'cat no\\~such\\~user', lines[-1]
|
||||||
|
else
|
||||||
|
assert_equal 'cat no~such~user', lines[-1]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# /tmp/fzf\ test**<TAB>
|
# /tmp/fzf\ test**<TAB>
|
||||||
tmux.send_keys 'C-u'
|
tmux.send_keys 'C-u'
|
||||||
tmux.send_keys 'cat /tmp/fzf\ test/**', :Tab
|
if shell == :fish
|
||||||
|
tmux.send_keys 'cat /tmp/fzf\\ test/', 'C-t'
|
||||||
|
else
|
||||||
|
tmux.send_keys "cat /tmp/fzf\\ test/#{trigger}", :Tab
|
||||||
|
end
|
||||||
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||||
tmux.send_keys 'foobar$'
|
tmux.send_keys 'foobar$'
|
||||||
tmux.until do |lines|
|
tmux.until do |lines|
|
||||||
@@ -210,7 +237,11 @@ module CompletionTest
|
|||||||
# Should include hidden files
|
# Should include hidden files
|
||||||
(1..100).each { |i| FileUtils.touch("/tmp/fzf-test/.hidden-#{i}") }
|
(1..100).each { |i| FileUtils.touch("/tmp/fzf-test/.hidden-#{i}") }
|
||||||
tmux.send_keys 'C-u'
|
tmux.send_keys 'C-u'
|
||||||
tmux.send_keys 'cat /tmp/fzf-test/hidden**', :Tab
|
if shell == :fish
|
||||||
|
tmux.send_keys 'cat /tmp/fzf-test/hidden', 'C-t'
|
||||||
|
else
|
||||||
|
tmux.send_keys "cat /tmp/fzf-test/hidden#{trigger}", :Tab
|
||||||
|
end
|
||||||
tmux.until(true) do |lines|
|
tmux.until(true) do |lines|
|
||||||
assert_equal 100, lines.match_count
|
assert_equal 100, lines.match_count
|
||||||
assert lines.any_include?('/tmp/fzf-test/.hidden-')
|
assert lines.any_include?('/tmp/fzf-test/.hidden-')
|
||||||
@@ -223,70 +254,89 @@ module CompletionTest
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_file_completion_root
|
def test_file_completion_root
|
||||||
tmux.send_keys 'ls /**', :Tab
|
if shell == :fish
|
||||||
|
tmux.send_keys 'ls /', 'C-t'
|
||||||
|
else
|
||||||
|
tmux.send_keys "ls /#{trigger}", :Tab
|
||||||
|
end
|
||||||
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||||
tmux.send_keys :Enter
|
tmux.send_keys :Enter
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_dir_completion
|
def test_dir_completion
|
||||||
|
FileUtils.mkdir_p('/tmp/fzf-test-dir')
|
||||||
(1..100).each do |idx|
|
(1..100).each do |idx|
|
||||||
FileUtils.mkdir_p("/tmp/fzf-test/d#{idx}")
|
FileUtils.mkdir_p("/tmp/fzf-test-dir/d#{idx}")
|
||||||
end
|
end
|
||||||
FileUtils.touch('/tmp/fzf-test/d55/xxx')
|
FileUtils.touch('/tmp/fzf-test-dir/d55/xxx')
|
||||||
tmux.prepare
|
tmux.prepare
|
||||||
tmux.send_keys 'cd /tmp/fzf-test/**', :Tab
|
if shell == :fish
|
||||||
|
tmux.send_keys 'cd /tmp/fzf-test-dir/', 'C-t'
|
||||||
|
else
|
||||||
|
tmux.send_keys "cd /tmp/fzf-test-dir/#{trigger}", :Tab
|
||||||
|
end
|
||||||
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||||
tmux.send_keys :Tab, :Tab # Tab does not work here
|
# Tab selects items in C-t's --multi mode, so skip for fish
|
||||||
tmux.send_keys 55
|
tmux.send_keys :Tab, :Tab unless shell == :fish # Tab does not work here
|
||||||
|
tmux.send_keys '55/$'
|
||||||
tmux.until do |lines|
|
tmux.until do |lines|
|
||||||
assert_equal 1, lines.match_count
|
assert_equal 1, lines.match_count
|
||||||
assert_includes lines, '> 55'
|
assert_includes lines, '> 55/$'
|
||||||
assert_includes lines, '> /tmp/fzf-test/d55/'
|
assert_includes lines, '> /tmp/fzf-test-dir/d55/'
|
||||||
end
|
end
|
||||||
tmux.send_keys :Enter
|
tmux.send_keys :Enter
|
||||||
tmux.until(true) { |lines| assert_equal 'cd /tmp/fzf-test/d55/', lines[-1] }
|
tmux.until(true) { |lines| assert_equal 'cd /tmp/fzf-test-dir/d55/', lines[-1] }
|
||||||
|
# C-t appends a trailing space after the result
|
||||||
|
tmux.send_keys :BSpace if shell == :fish
|
||||||
tmux.send_keys :xx
|
tmux.send_keys :xx
|
||||||
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test/d55/xx', lines[-1] }
|
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test-dir/d55/xx', lines[-1] }
|
||||||
|
|
||||||
# Should not match regular files (bash-only)
|
# Should not match regular files (bash-only)
|
||||||
if instance_of?(TestBash)
|
if instance_of?(TestBash)
|
||||||
tmux.send_keys :Tab
|
tmux.send_keys :Tab
|
||||||
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test/d55/xx', lines[-1] }
|
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test-dir/d55/xx', lines[-1] }
|
||||||
end
|
end
|
||||||
|
|
||||||
# Fail back to plusdirs
|
# Fail back to plusdirs
|
||||||
tmux.send_keys :BSpace, :BSpace, :BSpace
|
tmux.send_keys :BSpace, :BSpace, :BSpace
|
||||||
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test/d55', lines[-1] }
|
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test-dir/d55', lines[-1] }
|
||||||
tmux.send_keys :Tab
|
tmux.send_keys :Tab
|
||||||
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test/d55/', lines[-1] }
|
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test-dir/d55/', lines[-1] }
|
||||||
|
ensure
|
||||||
|
FileUtils.rm_rf('/tmp/fzf-test-dir')
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_process_completion
|
def test_process_completion
|
||||||
tmux.send_keys 'sleep 12345 &', :Enter
|
skip('fish background job format differs') if shell == :fish
|
||||||
lines = tmux.until { |lines| assert lines[-1]&.start_with?('[1] ') }
|
|
||||||
pid = lines[-1]&.split&.last
|
begin
|
||||||
tmux.prepare
|
tmux.send_keys 'sleep 12345 &', :Enter
|
||||||
tmux.send_keys 'C-L'
|
lines = tmux.until { |lines| assert lines[-1]&.start_with?('[1] ') }
|
||||||
tmux.send_keys 'kill **', :Tab
|
pid = lines[-1]&.split&.last
|
||||||
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
tmux.prepare
|
||||||
tmux.send_keys 'sleep12345'
|
tmux.send_keys 'C-L'
|
||||||
tmux.until { |lines| assert lines.any_include?('sleep 12345') }
|
tmux.send_keys "kill #{trigger}", :Tab
|
||||||
tmux.send_keys :Enter
|
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||||
tmux.until(true) { |lines| assert_equal "kill #{pid}", lines[-1] }
|
tmux.send_keys 'sleep12345'
|
||||||
ensure
|
tmux.until { |lines| assert lines.any_include?('sleep 12345') }
|
||||||
if pid
|
tmux.send_keys :Enter
|
||||||
begin
|
tmux.until(true) { |lines| assert_equal "kill #{pid}", lines[-1] }
|
||||||
Process.kill('KILL', pid.to_i)
|
ensure
|
||||||
rescue StandardError
|
if pid
|
||||||
nil
|
begin
|
||||||
|
Process.kill('KILL', pid.to_i)
|
||||||
|
rescue StandardError
|
||||||
|
nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_custom_completion
|
def test_custom_completion
|
||||||
|
skip('fish does not use _fzf_compgen_path; path completion is via ctrl-t') if shell == :fish
|
||||||
tmux.send_keys '_fzf_compgen_path() { echo "$1"; seq 10; }', :Enter
|
tmux.send_keys '_fzf_compgen_path() { echo "$1"; seq 10; }', :Enter
|
||||||
tmux.prepare
|
tmux.prepare
|
||||||
tmux.send_keys 'ls /tmp/**', :Tab
|
tmux.send_keys "ls /tmp/#{trigger}", :Tab
|
||||||
tmux.until { |lines| assert_equal 11, lines.match_count }
|
tmux.until { |lines| assert_equal 11, lines.match_count }
|
||||||
tmux.send_keys :Tab, :Tab, :Tab
|
tmux.send_keys :Tab, :Tab, :Tab
|
||||||
tmux.until { |lines| assert_equal 3, lines.select_count }
|
tmux.until { |lines| assert_equal 3, lines.select_count }
|
||||||
@@ -295,11 +345,12 @@ module CompletionTest
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_unset_completion
|
def test_unset_completion
|
||||||
|
skip('fish has native completion for set and unset variables') if shell == :fish
|
||||||
tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
|
tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
|
||||||
tmux.prepare
|
tmux.prepare
|
||||||
|
|
||||||
# Using tmux
|
# Using tmux
|
||||||
tmux.send_keys 'unset FZFFOOBR**', :Tab
|
tmux.send_keys "unset FZFFOOBR#{trigger}", :Tab
|
||||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
tmux.send_keys :Enter
|
tmux.send_keys :Enter
|
||||||
tmux.until { |lines| assert_equal 'unset FZFFOOBAR', lines[-1] }
|
tmux.until { |lines| assert_equal 'unset FZFFOOBAR', lines[-1] }
|
||||||
@@ -308,34 +359,57 @@ module CompletionTest
|
|||||||
# FZF_TMUX=1
|
# FZF_TMUX=1
|
||||||
new_shell
|
new_shell
|
||||||
tmux.focus
|
tmux.focus
|
||||||
tmux.send_keys 'unset FZFFOOBR**', :Tab
|
tmux.send_keys "unset FZFFOOBR#{trigger}", :Tab
|
||||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
tmux.send_keys :Enter
|
tmux.send_keys :Enter
|
||||||
tmux.until { |lines| assert_equal 'unset FZFFOOBAR', lines[-1] }
|
tmux.until { |lines| assert_equal 'unset FZFFOOBAR', lines[-1] }
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_completion_in_command_sequence
|
def test_completion_in_command_sequence
|
||||||
tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
|
if shell == :fish
|
||||||
tmux.prepare
|
FileUtils.mkdir_p('/tmp/fzf-test-seq')
|
||||||
|
FileUtils.touch('/tmp/fzf-test-seq/fzffoobar')
|
||||||
triggers = ['**', '~~', '++', 'ff', '/']
|
tmux.prepare
|
||||||
triggers.push('&', '[', ';', '`') if instance_of?(TestZsh)
|
# Fish uses Shift-Tab for fzf completion (no trigger system)
|
||||||
|
command = 'echo foo; QUX=THUD ls /tmp/fzf-test-seq/fzffoobr'
|
||||||
triggers.each do |trigger|
|
expected = 'echo foo; QUX=THUD ls /tmp/fzf-test-seq/fzffoobar'
|
||||||
set_var('FZF_COMPLETION_TRIGGER', trigger)
|
tmux.send_keys command, :BTab
|
||||||
command = "echo foo; QUX=THUD unset FZFFOOBR#{trigger}"
|
|
||||||
tmux.send_keys command.sub(/(;|`)$/, '\\\\\1'), :Tab
|
|
||||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
tmux.send_keys :Enter
|
tmux.send_keys :Enter
|
||||||
tmux.until { |lines| assert_equal 'echo foo; QUX=THUD unset FZFFOOBAR', lines[-1] }
|
tmux.until { |lines| assert_equal expected, lines[-1] }
|
||||||
|
else
|
||||||
|
tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
|
||||||
|
tmux.prepare
|
||||||
|
|
||||||
|
triggers = ['**', '~~', '++', 'ff', '/']
|
||||||
|
triggers.push('&', '[', ';', '`') if instance_of?(TestZsh)
|
||||||
|
|
||||||
|
triggers.each do |trigger|
|
||||||
|
set_var('FZF_COMPLETION_TRIGGER', trigger)
|
||||||
|
command = "echo foo; QUX=THUD unset FZFFOOBR#{trigger}"
|
||||||
|
expected = 'echo foo; QUX=THUD unset FZFFOOBAR'
|
||||||
|
tmux.send_keys command.sub(/(;|`)$/, '\\\\\1'), :Tab
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until { |lines| assert_equal expected, lines[-1] }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
ensure
|
||||||
|
FileUtils.rm_rf('/tmp/fzf-test-seq') if shell == :fish
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_file_completion_unicode
|
def test_file_completion_unicode
|
||||||
FileUtils.mkdir_p('/tmp/fzf-test')
|
FileUtils.mkdir_p('/tmp/fzf-test')
|
||||||
tmux.paste "cd /tmp/fzf-test; echo test3 > $'fzf-unicode \\355\\205\\214\\354\\212\\244\\355\\212\\2701'; echo test4 > $'fzf-unicode \\355\\205\\214\\354\\212\\244\\355\\212\\2702'"
|
# Shell-agnostic file creation
|
||||||
|
File.write('/tmp/fzf-test/fzf-unicode 테스트1', "test3\n")
|
||||||
|
File.write('/tmp/fzf-test/fzf-unicode 테스트2', "test4\n")
|
||||||
|
tmux.send_keys 'cd /tmp/fzf-test', :Enter
|
||||||
tmux.prepare
|
tmux.prepare
|
||||||
tmux.send_keys 'cat fzf-unicode**', :Tab
|
if shell == :fish
|
||||||
|
tmux.send_keys 'cat fzf-unicode', 'C-t'
|
||||||
|
else
|
||||||
|
tmux.send_keys "cat fzf-unicode#{trigger}", :Tab
|
||||||
|
end
|
||||||
tmux.until { |lines| assert_equal 2, lines.match_count }
|
tmux.until { |lines| assert_equal 2, lines.match_count }
|
||||||
|
|
||||||
tmux.send_keys '1'
|
tmux.send_keys '1'
|
||||||
@@ -358,36 +432,41 @@ module CompletionTest
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_custom_completion_api
|
def test_custom_completion_api
|
||||||
tmux.send_keys 'eval "_fzf$(declare -f _comprun)"', :Enter
|
skip('bash-specific _comprun/declare syntax') if shell == :fish
|
||||||
%w[f g].each do |command|
|
|
||||||
tmux.prepare
|
begin
|
||||||
tmux.send_keys "#{command} b**", :Tab
|
tmux.send_keys 'eval "_fzf$(declare -f _comprun)"', :Enter
|
||||||
tmux.until do |lines|
|
%w[f g].each do |command|
|
||||||
assert_equal 2, lines.item_count
|
tmux.prepare
|
||||||
assert_equal 1, lines.match_count
|
tmux.send_keys "#{command} b#{trigger}", :Tab
|
||||||
assert lines.any_include?("prompt-#{command}")
|
tmux.until do |lines|
|
||||||
assert lines.any_include?("preview-#{command}-bar")
|
assert_equal 2, lines.item_count
|
||||||
|
assert_equal 1, lines.match_count
|
||||||
|
assert lines.any_include?("prompt-#{command}")
|
||||||
|
assert lines.any_include?("preview-#{command}-bar")
|
||||||
|
end
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until { |lines| assert_equal "#{command} #{command}barbar", lines[-1] }
|
||||||
|
tmux.send_keys 'C-u'
|
||||||
end
|
end
|
||||||
tmux.send_keys :Enter
|
ensure
|
||||||
tmux.until { |lines| assert_equal "#{command} #{command}barbar", lines[-1] }
|
tmux.prepare
|
||||||
tmux.send_keys 'C-u'
|
tmux.send_keys 'unset -f _fzf_comprun', :Enter
|
||||||
end
|
end
|
||||||
ensure
|
|
||||||
tmux.prepare
|
|
||||||
tmux.send_keys 'unset -f _fzf_comprun', :Enter
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_ssh_completion
|
def test_ssh_completion
|
||||||
|
skip('fish uses native ssh completion') if shell == :fish
|
||||||
(1..5).each { |i| FileUtils.touch("/tmp/fzf-test-ssh-#{i}") }
|
(1..5).each { |i| FileUtils.touch("/tmp/fzf-test-ssh-#{i}") }
|
||||||
|
|
||||||
tmux.send_keys 'ssh jg@localhost**', :Tab
|
tmux.send_keys "ssh jg@localhost#{trigger}", :Tab
|
||||||
tmux.until do |lines|
|
tmux.until do |lines|
|
||||||
assert_operator lines.match_count, :>=, 1
|
assert_operator lines.match_count, :>=, 1
|
||||||
end
|
end
|
||||||
|
|
||||||
tmux.send_keys :Enter
|
tmux.send_keys :Enter
|
||||||
tmux.until { |lines| assert lines.any_include?('ssh jg@localhost') }
|
tmux.until { |lines| assert lines.any_include?('ssh jg@localhost') }
|
||||||
tmux.send_keys ' -i /tmp/fzf-test-ssh**', :Tab
|
tmux.send_keys " -i /tmp/fzf-test-ssh#{trigger}", :Tab
|
||||||
tmux.until do |lines|
|
tmux.until do |lines|
|
||||||
assert_operator lines.match_count, :>=, 5
|
assert_operator lines.match_count, :>=, 5
|
||||||
assert_equal 0, lines.select_count
|
assert_equal 0, lines.select_count
|
||||||
@@ -399,11 +478,344 @@ module CompletionTest
|
|||||||
tmux.send_keys :Enter
|
tmux.send_keys :Enter
|
||||||
tmux.until { |lines| assert lines.any_include?('ssh jg@localhost -i /tmp/fzf-test-ssh-') }
|
tmux.until { |lines| assert lines.any_include?('ssh jg@localhost -i /tmp/fzf-test-ssh-') }
|
||||||
|
|
||||||
tmux.send_keys 'localhost**', :Tab
|
tmux.send_keys "localhost#{trigger}", :Tab
|
||||||
tmux.until do |lines|
|
tmux.until do |lines|
|
||||||
assert_operator lines.match_count, :>=, 1
|
assert_operator lines.match_count, :>=, 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_option_equals_long_option
|
||||||
|
FileUtils.mkdir_p('/tmp/fzf-test-opt-eq-long')
|
||||||
|
FileUtils.touch('/tmp/fzf-test-opt-eq-long/SECURITY.md')
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys 'cd /tmp/fzf-test-opt-eq-long', :Enter
|
||||||
|
tmux.prepare
|
||||||
|
if shell == :fish
|
||||||
|
tmux.send_keys 'some-command --opt=SECURI', 'C-t'
|
||||||
|
else
|
||||||
|
tmux.send_keys "some-command --opt=SECURI#{trigger}", :Tab
|
||||||
|
end
|
||||||
|
|
||||||
|
case shell
|
||||||
|
when :bash
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 1, lines.match_count
|
||||||
|
assert_includes lines, '> SECURI'
|
||||||
|
end
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until(true) { |lines| assert_equal 'some-command --opt=SECURITY.md', lines[-1] }
|
||||||
|
when :fish
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 1, lines.match_count
|
||||||
|
assert lines.any_include?('SECURITY.md')
|
||||||
|
end
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until(true) { |lines| assert_equal 'some-command --opt=SECURITY.md', lines[-1] }
|
||||||
|
when :zsh
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 0, lines.match_count
|
||||||
|
assert_includes lines, '> --opt=SECURI'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
FileUtils.rm_rf('/tmp/fzf-test-opt-eq-long')
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_option_equals_long_option_after_double_dash
|
||||||
|
FileUtils.mkdir_p('/tmp/fzf-test-opt-eq-long-ddash')
|
||||||
|
FileUtils.touch('/tmp/fzf-test-opt-eq-long-ddash/SECURITY.md')
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys 'cd /tmp/fzf-test-opt-eq-long-ddash', :Enter
|
||||||
|
tmux.prepare
|
||||||
|
if shell == :fish
|
||||||
|
tmux.send_keys 'some-command -- --opt=SECURI', 'C-t'
|
||||||
|
else
|
||||||
|
tmux.send_keys "some-command -- --opt=SECURI#{trigger}", :Tab
|
||||||
|
end
|
||||||
|
|
||||||
|
case shell
|
||||||
|
when :bash
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 1, lines.match_count
|
||||||
|
assert_includes lines, '> SECURI'
|
||||||
|
end
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until(true) { |lines| assert_equal 'some-command -- --opt=SECURITY.md', lines[-1] }
|
||||||
|
when :fish, :zsh
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 0, lines.match_count
|
||||||
|
assert_includes lines, '> --opt=SECURI'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
FileUtils.rm_rf('/tmp/fzf-test-opt-eq-long-ddash')
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_option_equals_short_option
|
||||||
|
FileUtils.mkdir_p('/tmp/fzf-test-opt-eq-short')
|
||||||
|
FileUtils.touch('/tmp/fzf-test-opt-eq-short/SECURITY.md')
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys 'cd /tmp/fzf-test-opt-eq-short', :Enter
|
||||||
|
tmux.prepare
|
||||||
|
if shell == :fish
|
||||||
|
tmux.send_keys 'some-command -o=SECURI', 'C-t'
|
||||||
|
else
|
||||||
|
tmux.send_keys "some-command -o=SECURI#{trigger}", :Tab
|
||||||
|
end
|
||||||
|
|
||||||
|
case shell
|
||||||
|
when :bash, :fish
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 1, lines.match_count
|
||||||
|
assert lines.any_include?('> SECURITY.md')
|
||||||
|
end
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until(true) { |lines| assert_equal 'some-command -o=SECURITY.md', lines[-1] }
|
||||||
|
when :zsh
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 0, lines.match_count
|
||||||
|
assert_includes lines, '> -o=SECURI'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
FileUtils.rm_rf('/tmp/fzf-test-opt-eq-short')
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_option_equals_short_option_after_double_dash
|
||||||
|
FileUtils.mkdir_p('/tmp/fzf-test-opt-eq-short-ddash')
|
||||||
|
FileUtils.touch('/tmp/fzf-test-opt-eq-short-ddash/SECURITY.md')
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys 'cd /tmp/fzf-test-opt-eq-short-ddash', :Enter
|
||||||
|
tmux.prepare
|
||||||
|
if shell == :fish
|
||||||
|
tmux.send_keys 'some-command -- -o=SECURI', 'C-t'
|
||||||
|
else
|
||||||
|
tmux.send_keys "some-command -- -o=SECURI#{trigger}", :Tab
|
||||||
|
end
|
||||||
|
|
||||||
|
case shell
|
||||||
|
when :bash
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 1, lines.match_count
|
||||||
|
assert_includes lines, '> SECURITY.md'
|
||||||
|
end
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until(true) { |lines| assert_equal 'some-command -- -o=SECURITY.md', lines[-1] }
|
||||||
|
when :fish, :zsh
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 0, lines.match_count
|
||||||
|
assert_includes lines, '> -o=SECURI'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
FileUtils.rm_rf('/tmp/fzf-test-opt-eq-short-ddash')
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_option_no_equals_long_option
|
||||||
|
FileUtils.mkdir_p('/tmp/fzf-test-opt-no-eq-long')
|
||||||
|
FileUtils.touch('/tmp/fzf-test-opt-no-eq-long/SECURITY.md')
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys 'cd /tmp/fzf-test-opt-no-eq-long', :Enter
|
||||||
|
tmux.prepare
|
||||||
|
if shell == :fish
|
||||||
|
tmux.send_keys 'some-command --optSECURI', 'C-t'
|
||||||
|
else
|
||||||
|
tmux.send_keys "some-command --optSECURI#{trigger}", :Tab
|
||||||
|
end
|
||||||
|
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 0, lines.match_count
|
||||||
|
assert_includes lines, '> --optSECURI'
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
FileUtils.rm_rf('/tmp/fzf-test-opt-no-eq-long')
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_option_no_equals_long_option_after_double_dash
|
||||||
|
FileUtils.mkdir_p('/tmp/fzf-test-opt-no-eq-long-ddash')
|
||||||
|
FileUtils.touch('/tmp/fzf-test-opt-no-eq-long-ddash/SECURITY.md')
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys 'cd /tmp/fzf-test-opt-no-eq-long-ddash', :Enter
|
||||||
|
tmux.prepare
|
||||||
|
if shell == :fish
|
||||||
|
tmux.send_keys 'some-command -- --optSECURI', 'C-t'
|
||||||
|
else
|
||||||
|
tmux.send_keys "some-command -- --optSECURI#{trigger}", :Tab
|
||||||
|
end
|
||||||
|
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 0, lines.match_count
|
||||||
|
assert_includes lines, '> --optSECURI'
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
FileUtils.rm_rf('/tmp/fzf-test-opt-no-eq-long-ddash')
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_option_no_equals_short_option
|
||||||
|
FileUtils.mkdir_p('/tmp/fzf-test-opt-no-eq-short')
|
||||||
|
FileUtils.touch('/tmp/fzf-test-opt-no-eq-short/SECURITY.md')
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys 'cd /tmp/fzf-test-opt-no-eq-short', :Enter
|
||||||
|
tmux.prepare
|
||||||
|
if shell == :fish
|
||||||
|
tmux.send_keys 'some-command -oSECURI', 'C-t'
|
||||||
|
else
|
||||||
|
tmux.send_keys "some-command -oSECURI#{trigger}", :Tab
|
||||||
|
end
|
||||||
|
|
||||||
|
case shell
|
||||||
|
when :bash, :zsh
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 0, lines.match_count
|
||||||
|
assert_includes lines, '> -oSECURI'
|
||||||
|
end
|
||||||
|
when :fish
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 1, lines.match_count
|
||||||
|
assert lines.any_include?('> SECURITY.md')
|
||||||
|
end
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until(true) { |lines| assert_equal 'some-command -oSECURITY.md', lines[-1] }
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
FileUtils.rm_rf('/tmp/fzf-test-opt-no-eq-short')
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_option_no_equals_short_option_after_double_dash
|
||||||
|
FileUtils.mkdir_p('/tmp/fzf-test-opt-no-eq-short-ddash')
|
||||||
|
FileUtils.touch('/tmp/fzf-test-opt-no-eq-short-ddash/SECURITY.md')
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys 'cd /tmp/fzf-test-opt-no-eq-short-ddash', :Enter
|
||||||
|
tmux.prepare
|
||||||
|
if shell == :fish
|
||||||
|
tmux.send_keys 'some-command -- -oSECURI', 'C-t'
|
||||||
|
else
|
||||||
|
tmux.send_keys "some-command -- -oSECURI#{trigger}", :Tab
|
||||||
|
end
|
||||||
|
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 0, lines.match_count
|
||||||
|
assert_includes lines, '> -oSECURI'
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
FileUtils.rm_rf('/tmp/fzf-test-opt-no-eq-short-ddash')
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_filename_with_newline
|
||||||
|
FileUtils.mkdir_p('/tmp/fzf-test-newline')
|
||||||
|
FileUtils.touch("/tmp/fzf-test-newline/xyz\nwith\nnewlines")
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys 'cd /tmp/fzf-test-newline', :Enter
|
||||||
|
tmux.prepare
|
||||||
|
if shell == :fish
|
||||||
|
tmux.send_keys 'cat xyz', 'C-t'
|
||||||
|
else
|
||||||
|
tmux.send_keys "cat xyz#{trigger}", :Tab
|
||||||
|
end
|
||||||
|
|
||||||
|
case shell
|
||||||
|
when :fish
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 1, lines.match_count
|
||||||
|
assert_includes lines, '> xyz'
|
||||||
|
end
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
# fish escapes newlines in filenames
|
||||||
|
tmux.until(true) { |lines| assert_equal 'cat xyz\\nwith\\nnewlines', lines[-1] }
|
||||||
|
when :bash, :zsh
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 1, lines.match_count
|
||||||
|
assert_includes lines, '> xyz'
|
||||||
|
end
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
# bash and zsh replace newlines with spaces in filenames
|
||||||
|
tmux.until(true) { |lines| assert_equal 'cat xyz with newlines', lines[-1] }
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
FileUtils.rm_rf('/tmp/fzf-test-newline')
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_path_with_special_chars
|
||||||
|
FileUtils.mkdir_p('/tmp/fzf-test-[special]')
|
||||||
|
FileUtils.touch('/tmp/fzf-test-[special]/xyz123')
|
||||||
|
tmux.prepare
|
||||||
|
if shell == :fish
|
||||||
|
tmux.send_keys 'ls /tmp/fzf-test-\[special\]/xyz', 'C-t'
|
||||||
|
else
|
||||||
|
tmux.send_keys "ls /tmp/fzf-test-\\[special\\]/xyz#{trigger}", :Tab
|
||||||
|
end
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until(true) { |lines| assert_equal 'ls /tmp/fzf-test-\\[special\\]/xyz123', lines[-1] }
|
||||||
|
ensure
|
||||||
|
FileUtils.rm_rf('/tmp/fzf-test-[special]')
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_query_with_dollar_anchor
|
||||||
|
FileUtils.mkdir_p('/tmp/fzf-test-dollar-anchor')
|
||||||
|
FileUtils.touch('/tmp/fzf-test-dollar-anchor/file.txt')
|
||||||
|
FileUtils.touch('/tmp/fzf-test-dollar-anchor/filetxt.md')
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys 'cd /tmp/fzf-test-dollar-anchor', :Enter
|
||||||
|
tmux.prepare
|
||||||
|
if shell == :fish
|
||||||
|
tmux.send_keys 'ls txt$', 'C-t'
|
||||||
|
else
|
||||||
|
tmux.send_keys "ls txt$#{trigger}", :Tab
|
||||||
|
end
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 1, lines.match_count
|
||||||
|
assert_includes lines, '> txt$'
|
||||||
|
end
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until(true) { |lines| assert_equal 'ls file.txt', lines[-1] }
|
||||||
|
ensure
|
||||||
|
FileUtils.rm_rf('/tmp/fzf-test-dollar-anchor')
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_single_flag_completion
|
||||||
|
FileUtils.mkdir_p('/tmp/fzf-test-single-flag')
|
||||||
|
FileUtils.touch('/tmp/fzf-test-single-flag/-testfile.txt')
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys 'cd /tmp/fzf-test-single-flag', :Enter
|
||||||
|
tmux.prepare
|
||||||
|
if shell == :fish
|
||||||
|
tmux.send_keys 'ls -', 'C-t'
|
||||||
|
else
|
||||||
|
tmux.send_keys "ls -#{trigger}", :Tab
|
||||||
|
end
|
||||||
|
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 1, lines.match_count
|
||||||
|
assert_includes lines, '> -'
|
||||||
|
end
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until(true) { |lines| assert_equal 'ls -testfile.txt', lines[-1] }
|
||||||
|
ensure
|
||||||
|
FileUtils.rm_rf('/tmp/fzf-test-single-flag')
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_double_flag_completion
|
||||||
|
FileUtils.mkdir_p('/tmp/fzf-test-double-flag')
|
||||||
|
FileUtils.touch('/tmp/fzf-test-double-flag/--testfile.txt')
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys 'cd /tmp/fzf-test-double-flag', :Enter
|
||||||
|
tmux.prepare
|
||||||
|
if shell == :fish
|
||||||
|
tmux.send_keys 'ls --', 'C-t'
|
||||||
|
else
|
||||||
|
tmux.send_keys "ls --#{trigger}", :Tab
|
||||||
|
end
|
||||||
|
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 1, lines.match_count
|
||||||
|
assert_includes lines, '> --'
|
||||||
|
end
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until(true) { |lines| assert_equal 'ls --testfile.txt', lines[-1] }
|
||||||
|
ensure
|
||||||
|
FileUtils.rm_rf('/tmp/fzf-test-double-flag')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class TestBash < TestBase
|
class TestBash < TestBase
|
||||||
@@ -424,7 +836,7 @@ class TestBash < TestBase
|
|||||||
tmux.paste 'touch /tmp/foo; _fzf_completion_loader=1'
|
tmux.paste 'touch /tmp/foo; _fzf_completion_loader=1'
|
||||||
tmux.paste '_completion_loader() { complete -o default fake; }'
|
tmux.paste '_completion_loader() { complete -o default fake; }'
|
||||||
tmux.paste 'complete -F _fzf_path_completion -o default -o bashdefault fake'
|
tmux.paste 'complete -F _fzf_path_completion -o default -o bashdefault fake'
|
||||||
tmux.send_keys 'fake /tmp/foo**', :Tab
|
tmux.send_keys "fake /tmp/foo#{trigger}", :Tab
|
||||||
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||||
tmux.send_keys 'C-c'
|
tmux.send_keys 'C-c'
|
||||||
|
|
||||||
@@ -433,7 +845,7 @@ class TestBash < TestBase
|
|||||||
tmux.send_keys :Tab, 'C-u'
|
tmux.send_keys :Tab, 'C-u'
|
||||||
|
|
||||||
tmux.prepare
|
tmux.prepare
|
||||||
tmux.send_keys 'fake /tmp/foo**', :Tab
|
tmux.send_keys "fake /tmp/foo#{trigger}", :Tab
|
||||||
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -455,24 +867,142 @@ class TestZsh < TestBase
|
|||||||
tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
|
tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
|
||||||
['unset', '\unset', "'unset'"].each do |command|
|
['unset', '\unset', "'unset'"].each do |command|
|
||||||
tmux.prepare
|
tmux.prepare
|
||||||
tmux.send_keys "#{command} FZFFOOBR**", :Tab
|
tmux.send_keys "#{command} FZFFOOBR#{trigger}", :Tab
|
||||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
tmux.send_keys :Enter
|
tmux.send_keys :Enter
|
||||||
tmux.until { |lines| assert_equal "#{command} FZFFOOBAR", lines[-1] }
|
tmux.until { |lines| assert_equal "#{command} FZFFOOBAR", lines[-1] }
|
||||||
tmux.send_keys 'C-c'
|
tmux.send_keys 'C-c'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Helper function to run test with Perl and again with Awk
|
||||||
|
def self.test_perl_and_awk(name, &block)
|
||||||
|
define_method(:"test_#{name}") do
|
||||||
|
instance_eval(&block)
|
||||||
|
end
|
||||||
|
|
||||||
|
define_method(:"test_#{name}_awk") do
|
||||||
|
tmux.send_keys "unset 'commands[perl]'", :Enter
|
||||||
|
tmux.prepare
|
||||||
|
# Verify perl is actually unset (0 = not found)
|
||||||
|
tmux.send_keys 'echo ${+commands[perl]}', :Enter
|
||||||
|
tmux.until { |lines| assert_equal '0', lines[-1] }
|
||||||
|
tmux.prepare
|
||||||
|
instance_eval(&block)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def prepare_ctrl_r_test
|
||||||
|
tmux.send_keys ':', :Enter
|
||||||
|
tmux.send_keys 'echo match-collision', :Enter
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys 'echo "line 1', :Enter, '2 line 2"', :Enter
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys 'echo "foo', :Enter, 'bar"', :Enter
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys 'echo "bar', :Enter, 'foo"', :Enter
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys 'echo "trailing_space "', :Enter
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys 'cat <<EOF | wc -c', :Enter, 'qux thud', :Enter, 'EOF', :Enter
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys 'C-l', 'C-r'
|
||||||
|
end
|
||||||
|
|
||||||
|
test_perl_and_awk 'ctrl_r_accept_or_print_query' do
|
||||||
|
set_var('FZF_CTRL_R_OPTS', '--bind enter:accept-or-print-query')
|
||||||
|
prepare_ctrl_r_test
|
||||||
|
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||||
|
tmux.send_keys '1 foobar'
|
||||||
|
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until { |lines| assert_equal '1 foobar', lines[-1] }
|
||||||
|
end
|
||||||
|
|
||||||
|
test_perl_and_awk 'ctrl_r_multiline_index_collision' do
|
||||||
|
# Leading number in multi-line history content is not confused with index
|
||||||
|
prepare_ctrl_r_test
|
||||||
|
tmux.send_keys "'line 1"
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal ['echo "line 1', '2 line 2"'], lines[-2..]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test_perl_and_awk 'ctrl_r_multi_selection' do
|
||||||
|
prepare_ctrl_r_test
|
||||||
|
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||||
|
tmux.send_keys :BTab, :BTab, :BTab
|
||||||
|
tmux.until { |lines| assert_includes lines[-2], '(3)' }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal ['cat <<EOF | wc -c', 'qux thud', 'EOF', 'echo "trailing_space "', 'echo "bar', 'foo"'], lines[-6..]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test_perl_and_awk 'ctrl_r_no_multi_selection' do
|
||||||
|
set_var('FZF_CTRL_R_OPTS', '--no-multi')
|
||||||
|
prepare_ctrl_r_test
|
||||||
|
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||||
|
tmux.send_keys :BTab, :BTab, :BTab
|
||||||
|
tmux.until { |lines| refute_includes lines[-2], '(3)' }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal ['cat <<EOF | wc -c', 'qux thud', 'EOF'], lines[-3..]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# NOTE: 'Perl/$history' won't see foreign cmds immediately, unlike 'awk/fc'.
|
||||||
|
# Perl passes only because another cmd runs between mocking and triggering C-r
|
||||||
|
# https://github.com/junegunn/fzf/issues/4061
|
||||||
|
# https://zsh.org/mla/users/2024/msg00692.html
|
||||||
|
test_perl_and_awk 'ctrl_r_foreign_commands' do
|
||||||
|
histfile = "#{tempname}-foreign-hist"
|
||||||
|
tmux.send_keys "HISTFILE=#{histfile}", :Enter
|
||||||
|
tmux.prepare
|
||||||
|
# SHARE_HISTORY picks up foreign commands; marked with * in fc
|
||||||
|
tmux.send_keys 'setopt SHARE_HISTORY', :Enter
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys 'fzf_cmd_local', :Enter
|
||||||
|
tmux.prepare
|
||||||
|
# Mock foreign command (for testing only; don't edit your HISTFILE this way)
|
||||||
|
tmux.send_keys "echo ': 0:0;fzf_cmd_foreign' >> $HISTFILE", :Enter
|
||||||
|
tmux.prepare
|
||||||
|
# Verify fc shows foreign command with asterisk
|
||||||
|
tmux.send_keys 'fc -rl -1', :Enter
|
||||||
|
tmux.until { |lines| assert(lines.any? { |l| l.match?(/^\s*\d+\* fzf_cmd_foreign/) }) }
|
||||||
|
tmux.prepare
|
||||||
|
# Test ctrl-r correctly extracts the foreign command
|
||||||
|
tmux.send_keys 'C-r'
|
||||||
|
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||||
|
tmux.send_keys '^fzf_cmd_'
|
||||||
|
tmux.until { |lines| assert_equal 2, lines.match_count }
|
||||||
|
tmux.send_keys :BTab, :BTab
|
||||||
|
tmux.until { |lines| assert_includes lines[-2], '(2)' }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal %w[fzf_cmd_foreign fzf_cmd_local], lines[-2..]
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
FileUtils.rm_f(histfile)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class TestFish < TestBase
|
class TestFish < TestBase
|
||||||
include TestShell
|
include TestShell
|
||||||
|
include CompletionTest
|
||||||
|
|
||||||
def shell
|
def shell
|
||||||
:fish
|
:fish
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def trigger
|
||||||
|
'++'
|
||||||
|
end
|
||||||
|
|
||||||
def new_shell
|
def new_shell
|
||||||
tmux.send_keys 'env FZF_TMUX=1 FZF_DEFAULT_OPTS=--no-scrollbar fish', :Enter
|
tmux.send_keys 'env FZF_TMUX=1 XDG_CONFIG_HOME=/tmp/fzf-fish fish', :Enter
|
||||||
tmux.send_keys 'function fish_prompt; end; clear', :Enter
|
tmux.send_keys 'function fish_prompt; end; clear', :Enter
|
||||||
tmux.until { |lines| assert_empty lines }
|
tmux.until { |lines| assert_empty lines }
|
||||||
end
|
end
|
||||||
@@ -490,15 +1020,23 @@ class TestFish < TestBase
|
|||||||
tmux.send_keys 'echo "bar', :Enter, 'foo"', :Enter
|
tmux.send_keys 'echo "bar', :Enter, 'foo"', :Enter
|
||||||
tmux.prepare
|
tmux.prepare
|
||||||
tmux.send_keys 'C-l', 'C-r'
|
tmux.send_keys 'C-l', 'C-r'
|
||||||
|
offset = -6
|
||||||
block = <<~BLOCK
|
block = <<~BLOCK
|
||||||
echo "foo
|
echo "foo
|
||||||
bar"
|
bar"
|
||||||
echo "bar
|
echo "bar
|
||||||
foo"
|
foo"
|
||||||
BLOCK
|
BLOCK
|
||||||
|
if shell == :fish
|
||||||
|
offset = -4
|
||||||
|
block = <<~FISH
|
||||||
|
echo "foo␊bar"
|
||||||
|
echo "bar␊foo"
|
||||||
|
FISH
|
||||||
|
end
|
||||||
tmux.until do |lines|
|
tmux.until do |lines|
|
||||||
block.lines.each_with_index do |line, idx|
|
block.lines.each_with_index do |line, idx|
|
||||||
assert_includes lines[-6 + idx], line.chomp
|
assert_includes lines[idx + offset], line.chomp
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
tmux.send_keys :BTab, :BTab
|
tmux.send_keys :BTab, :BTab
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ for opt in "$@"; do
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||||
|
fzf_base=$(pwd)
|
||||||
|
|
||||||
ask() {
|
ask() {
|
||||||
while true; do
|
while true; do
|
||||||
read -p "$1 ([y]/n) " -r
|
read -p "$1 ([y]/n) " -r
|
||||||
@@ -64,19 +67,19 @@ remove_line() {
|
|||||||
line_no=1
|
line_no=1
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
line_no=$(( $(sed 's/:.*//' <<< "$line") + line_no - 1 ))
|
line_no=$(($(sed 's/:.*//' <<< "$line") + line_no - 1))
|
||||||
content=$(sed 's/^[0-9]*://' <<< "$line")
|
content=$(sed 's/^[0-9]*://' <<< "$line")
|
||||||
match=1
|
match=1
|
||||||
echo " - Line #$line_no: $content"
|
echo " - Line #$line_no: $content"
|
||||||
[ "$content" = "$1" ] || ask " - Remove?"
|
[ "$content" = "$1" ] || ask " - Remove?"
|
||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
temp=$(mktemp)
|
temp=$(mktemp)
|
||||||
awk -v n=$line_no 'NR == n {next} {print}' "$src" > "$temp" &&
|
awk -v n=$line_no 'NR == n {next} {print}' "$src" > "$temp" &&
|
||||||
cat "$temp" > "$src" && rm -f "$temp" || break
|
cat "$temp" > "$src" && rm -f "$temp" || break
|
||||||
echo " - Removed"
|
echo " - Removed"
|
||||||
else
|
else
|
||||||
echo " - Skipped"
|
echo " - Skipped"
|
||||||
line_no=$(( line_no + 1 ))
|
line_no=$((line_no + 1))
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
[ $match -eq 0 ] && echo " - Nothing found"
|
[ $match -eq 0 ] && echo " - Nothing found"
|
||||||
@@ -94,12 +97,15 @@ done
|
|||||||
bind_file="${fish_dir}/functions/fish_user_key_bindings.fish"
|
bind_file="${fish_dir}/functions/fish_user_key_bindings.fish"
|
||||||
if [ -f "$bind_file" ]; then
|
if [ -f "$bind_file" ]; then
|
||||||
remove_line "$bind_file" "fzf_key_bindings"
|
remove_line "$bind_file" "fzf_key_bindings"
|
||||||
|
remove_line "$bind_file" "fzf_completion_setup"
|
||||||
remove_line "$bind_file" "fzf --fish | source"
|
remove_line "$bind_file" "fzf --fish | source"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -d "${fish_dir}/functions" ]; then
|
if [ -d "${fish_dir}/functions" ]; then
|
||||||
remove "${fish_dir}/functions/fzf.fish"
|
remove "${fish_dir}/functions/fzf.fish"
|
||||||
remove "${fish_dir}/functions/fzf_key_bindings.fish"
|
remove "${fish_dir}/functions/fzf_key_bindings.fish"
|
||||||
|
remove_line "$bind_file" "source \"${fzf_base}/shell/completion.fish\""
|
||||||
|
remove_line "$bind_file" "source \"${fzf_base}/shell/key-bindings.fish\""
|
||||||
|
|
||||||
if [ -z "$(ls -A "${fish_dir}/functions")" ]; then
|
if [ -z "$(ls -A "${fish_dir}/functions")" ]; then
|
||||||
rmdir "${fish_dir}/functions"
|
rmdir "${fish_dir}/functions"
|
||||||
@@ -109,6 +115,6 @@ if [ -d "${fish_dir}/functions" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
config_dir=$(dirname "$prefix_expand")
|
config_dir=$(dirname "$prefix_expand")
|
||||||
if [[ "$xdg" = 1 ]] && [[ "$config_dir" = */fzf ]] && [[ -d "$config_dir" ]]; then
|
if [[ $xdg == 1 ]] && [[ $config_dir == */fzf ]] && [[ -d $config_dir ]]; then
|
||||||
rmdir "$config_dir"
|
rmdir "$config_dir"
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user