Compare commits

...
3 Commits
Author SHA1 Message Date
Junegunn Choi c7bf2b8eea 0.75.0 2026-11-07 20:58:45 +09:00
Junegunn Choi 69114661a6 Fix data races on preview placeholders and merger.final
- Capture lastAction and prompt in preview request, like query and env.
  Preview goroutine read t.lastAction and t.promptString while the main
  loop wrote them.
- Do not write merger.final on a cached merger. Cache hit already
  requires the same value, and the terminal may be reading it.

Close #4947
2026-11-07 20:37:20 +09:00
Junegunn ChoiandMarshall Porter 96028e2a54 Add MSI installers for Windows and offer them through winget (#4948)
Co-authored-by: Marshall Porter <meoporter@gmail.com>
2026-11-06 10:45:01 +09:00
16 changed files with 230 additions and 65 deletions

No files matched your search

+94
View File
@@ -18,6 +18,8 @@ jobs:
release:
runs-on: macos-latest
environment: release
outputs:
version: ${{ steps.ver.outputs.version }}
steps:
- uses: actions/checkout@v7
with:
@@ -74,3 +76,95 @@ jobs:
MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }}
MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }}
MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }}
# For the msi job
- uses: actions/upload-artifact@v7
with:
name: windows-binaries
path: |
dist/*windows_amd64*/fzf.exe
dist/*windows_arm64*/fzf.exe
if-no-files-found: error
# winget's portable install runs fzf through a symlink that admin SSH
# sessions refuse to follow (#4943). The MSI installs a real file instead.
# WiX only runs on Windows.
msi:
needs: release
runs-on: windows-latest
env:
# MSI versions cannot have a suffix such as -rc1
BUILD_MSI: ${{ !contains(needs.release.outputs.version, '-') }}
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v8
if: env.BUILD_MSI == 'true'
with:
name: windows-binaries
path: dist
- uses: actions/setup-dotnet@v6
if: env.BUILD_MSI == 'true'
with:
dotnet-version: '8.0.x'
# WiX 6 and later fall under the Open Source Maintenance Fee
- name: Install WiX
if: env.BUILD_MSI == 'true'
run: dotnet tool install --global wix --version 5.0.2
- name: Build Windows MSI installers
if: env.BUILD_MSI == 'true'
shell: bash
run: |
set -e
V='${{ needs.release.outputs.version }}'
for pair in "amd64:x64" "arm64:arm64"; do
goarch="${pair%%:*}"
wixarch="${pair##*:}"
bindir=$(dirname "$(find dist -type f -iname fzf.exe -path "*windows_${goarch}*" | head -n1)")
wix build wix/main.wxs -arch "$wixarch" -d Version="$V" -d BinDir="$bindir" -o "dist/fzf-${V}-windows_${goarch}.msi"
done
ls -la dist/*.msi
- uses: actions/upload-artifact@v7
if: env.BUILD_MSI == 'true'
with:
name: windows-msi
path: dist/*.msi
- name: Upload MSI installers and publish release
if: github.event_name == 'push'
shell: bash
run: |
set -e
V='${{ needs.release.outputs.version }}'
# A re-run would replace MSIs that a winget manifest may already
# reference, and submit to winget again
if [[ $(gh release view "v$V" --json isDraft --jq .isDraft) != true ]]; then
echo "::error::v$V is already published"
exit 1
fi
if [[ $BUILD_MSI == true ]]; then
sums="fzf_${V}_checksums.txt"
# Drop MSI lines from a previous run of this job
gh release download "v$V" -p "$sums" -O - | grep -v '\.msi$' > "dist/$sums"
# sha256sum on Windows may print '<hash> *<name>'
(cd dist && sha256sum fzf-*.msi) | sed 's/ \*/ /' >> "dist/$sums"
gh release upload "v$V" dist/*.msi "dist/$sums" --clobber
fi
gh release edit "v$V" --draft=false
env:
GITHUB_TOKEN: ${{ github.token }}
# Called directly because a release published with GITHUB_TOKEN does not
# trigger other workflows
winget:
needs: [release, msi]
if: github.event_name == 'push' && !contains(needs.release.outputs.version, '-')
uses: ./.github/workflows/winget.yml
with:
release-tag: v${{ needs.release.outputs.version }}
secrets:
WINGET_TOKEN: ${{ secrets.WINGET_TOKEN }}
+15 -4
View File
@@ -1,7 +1,13 @@
name: Publish to Winget
on:
release:
types: [released]
workflow_call:
inputs:
release-tag:
type: string
required: true
secrets:
WINGET_TOKEN:
required: true
workflow_dispatch:
inputs:
release-tag:
@@ -9,6 +15,11 @@ on:
required: true
type: string
# winget-releaser passes this token to unpinned actions. Submission uses
# WINGET_TOKEN.
permissions:
contents: read
jobs:
publish:
runs-on: ubuntu-latest
@@ -16,6 +27,6 @@ jobs:
- uses: vedantmgoyal2009/winget-releaser@4ffc7888bffd451b357355dc214d43bb9f23917e # v2
with:
identifier: junegunn.fzf
release-tag: ${{ inputs.release-tag || github.event.release.tag_name }}
installers-regex: '-windows_(armv7|arm64|amd64)\.zip$'
release-tag: ${{ inputs.release-tag }}
installers-regex: '-windows_(arm64|amd64)\.(zip|msi)$'
token: ${{ secrets.WINGET_TOKEN }}
+4
View File
@@ -120,6 +120,10 @@ release:
owner: junegunn
name: fzf
prerelease: auto
# Published by the msi job in release.yml after it uploads the MSIs
draft: true
# A retried release job would otherwise add a second draft for the tag
replace_existing_draft: true
name_template: '{{ .Version }}'
snapshot:
-3
View File
@@ -19,9 +19,6 @@ make install
# Build fzf binaries and archives for all platforms using goreleaser
make build
# Publish GitHub release
make release
```
> [!WARNING]
+13 -6
View File
@@ -9,8 +9,6 @@ CHANGELOG
--bind 'ctrl-a:change-info-command:echo "mode: A | $FZF_INFO"' \
--bind 'ctrl-b:change-info-command:echo "mode: B | $FZF_INFO"'
```
- Fixed `--with-shell` on Windows not detecting cmd, PowerShell, and pwsh, which made fzf quote commands and entries for a POSIX shell (#4936) (@vimkim)
- e.g. With `--with-shell "cmd.exe /d /c"`, `--preview '"C:\Program Files\nu\bin\nu.exe" --version'` failed
- `transform-nth` and `transform-with-nth` (and their `bg-` variants) no longer split the output at `|`
- Only `change-nth` and `change-with-nth` rotate through `|`-separated values
- Fixed the bound command being replaced with the output. e.g. With `transform-nth(echo '1|2')`, the next key press ran `2|1` as a shell command.
@@ -30,12 +28,21 @@ CHANGELOG
# Now you can write:
ls -al | fzf --with-nth .. --bind 'space:change-with-nth:..|1|2'
```
- Fixed `--no-sort` being ignored in filter mode with `--sync`, `--tac`, or `--bench` (#4940) (@NavpreetSSidhu)
- Fixed `--walker=follow` skipping a symlink in one `--walker-root` when its target is an ancestor of another root (#4944) (@mabunemeh)
- Fixed data races in the preview and the search result cache (#4947)
- Fixed O(n^2) action parsing in `--listen`. A single ~1 MB request could block the server for ~30 s (@zachariah-mithani, AploSec)
- Windows
- Fixed `--with-shell` on Windows not detecting cmd, PowerShell, and pwsh, which made fzf quote commands and entries for a POSIX shell (#4936) (@vimkim)
- e.g. With `--with-shell "cmd.exe /d /c"`, `--preview '"C:\Program Files\nu\bin\nu.exe" --version'` failed
- Added `.msi` installers for amd64 and arm64, also offered through winget (#4943) (@meop)
- The portable package runs fzf through a symlink in the WinGet Links folder, which an administrator's SSH session refuses to follow
- The MSI installs `fzf.exe` under Program Files and adds it to the system PATH
- `winget upgrade` keeps an existing portable install. To switch to the MSI, run `winget uninstall junegunn.fzf` and `winget install junegunn.fzf`.
- Vim plugin
- Fixed garbage text on screen in Neovim with `set title` when the fzf command contains ANSI escape sequences (#4941)
- nushell
- The completion integration now requires nushell 0.116.1 or above, where an interactive completer can take the terminal through the `@interactive` attribute (#4934) (@sim590)
- Fixed `--no-sort` being ignored in filter mode with `--sync`, `--tac`, or `--bench` (#4940) (@NavpreetSSidhu)
- Vim plugin: Fixed garbage text on screen in Neovim with `set title` when the fzf command contains ANSI escape sequences (#4941)
- Fixed `--walker=follow` skipping a symlink in one `--walker-root` when its target is an ancestor of another root (#4944) (@mabunemeh)
- Fixed O(n^2) action parsing in `--listen`. A single ~1 MB request could block the server for ~30 s (@zachariah-mithani, AploSec)
0.74.5
------
+1 -41
View File
@@ -144,46 +144,6 @@ tag: prerelease
git tag -s v$(VERSION) -m v$(VERSION)
git push origin v$(VERSION)
release:
# Make sure that the tests pass and the build works
TAGS=tcell make test
make test build clean
ifndef GITHUB_TOKEN
$(error GITHUB_TOKEN is not defined)
endif
# Check if we are on master branch
ifneq ($(shell git symbolic-ref --short HEAD),master)
$(error Not on master branch)
endif
# Check if version numbers are properly updated
grep -q ^$(VERSION_REGEX)$$ CHANGELOG.md
grep -qF '"fzf $(VERSION_TRIM)"' man/man1/fzf.1
grep -qF '"fzf $(VERSION_TRIM)"' man/man1/fzf-tmux.1
grep -qF $(VERSION) install
grep -qF $(VERSION) install.ps1
# Make release note out of CHANGELOG.md
mkdir -p tmp
sed -n '/^$(VERSION_REGEX)$$/,/^[0-9]/p' CHANGELOG.md | tail -r | \
sed '1,/^ *$$/d' | tail -r | sed 1,2d | tee tmp/release-note
# Push to temp branch first so that install scripts always works on master branch
git checkout -B temp master
git push origin temp --follow-tags --force
# Make a GitHub release
goreleaser --clean --release-notes tmp/release-note
# Push to master
git checkout master
git push origin master
# Delete temp branch
git push origin --delete temp
clean:
$(RM) -r dist target
@@ -233,4 +193,4 @@ update:
$(GO) get -u
$(GO) mod tidy
.PHONY: all generate build prerelease tag release test itest bench lint install clean docker docker-test update fmt
.PHONY: all generate build prerelease tag test itest bench lint install clean docker docker-test update fmt
+21 -2
View File
@@ -22,7 +22,7 @@ triggered by a tag push.
3. Verify file consistency, sign the tag, and push the tag.
```sh
make tag VERSION=0.74.5
make tag VERSION=0.75.0
```
`make tag` runs `prerelease` first (checks that the version
@@ -35,13 +35,29 @@ triggered by a tag push.
4. The workflow fires on the tag push and pauses on the `release`
environment gate. Approve it in the Actions tab to release.
- goreleaser creates a draft release
- The `msi` job builds the Windows MSIs, uploads them, and publishes
the release
- The `winget` job submits the release to winget
5. After the GitHub release is published, fast-forward `master`:
5. After the `msi` job publishes the GitHub release, fast-forward `master`:
```sh
git push origin master
```
### If the `msi` job fails
The release stays a draft. Do not push `master` until it is published.
- Fix the problem and re-run the failed jobs, or
- Publish without MSIs and submit to winget by hand:
```sh
gh release edit v0.75.0 --draft=false
gh workflow run winget.yml -f release-tag=v0.75.0
```
## Testing the workflow
To exercise the workflow without firing a real release:
@@ -53,6 +69,9 @@ To exercise the workflow without firing a real release:
3. Approve the `release` environment gate when prompted.
4. Goreleaser runs with `--snapshot --skip=publish`. Signing and
notarization run; only the GitHub release upload is skipped.
5. The `msi` job builds the MSIs and uploads them as the `windows-msi`
artifact of the run. The release upload and the `winget` job are
skipped.
Use this to validate the workflow YAML, version-extraction logic,
the macOS runner setup, and the signing/notarization credentials.
+1 -1
View File
@@ -2,7 +2,7 @@
set -u
version=0.74.5
version=0.75.0
auto_completion=
key_bindings=
update_config=2
+1 -1
View File
@@ -1,4 +1,4 @@
$version="0.74.5"
$version="0.75.0"
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"github.com/junegunn/fzf/src/protector"
)
var version = "0.74"
var version = "0.75"
var revision = "devel"
//go:embed shell/key-bindings.bash
+1 -1
View File
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
..
.TH fzf\-tmux 1 "Oct 2026" "fzf 0.74.5" "fzf\-tmux - open fzf in tmux split pane"
.TH fzf\-tmux 1 "Nov 2026" "fzf 0.75.0" "fzf\-tmux - open fzf in tmux split pane"
.SH NAME
fzf\-tmux - open fzf in tmux split pane
+1 -1
View File
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
..
.TH fzf 1 "Oct 2026" "fzf 0.74.5" "fzf - a command-line fuzzy finder"
.TH fzf 1 "Nov 2026" "fzf 0.75.0" "fzf - a command-line fuzzy finder"
.SH NAME
fzf - a command-line fuzzy finder
+5 -1
View File
@@ -137,13 +137,17 @@ func (m *Matcher) Loop() {
m.scanMutex.Lock()
result = m.scan(request)
m.scanMutex.Unlock()
// Not on a cached merger, which the terminal may be reading. It
// already has the same value.
if result.merger != nil {
result.merger.final = request.final
}
}
if !result.cancelled {
if result.cacheable() {
m.mergerCache[patternString] = result
}
result.merger.final = request.final
m.eventBox.Set(EvtSearchFin, result)
}
}
+20 -3
View File
@@ -843,6 +843,8 @@ type previewRequest struct {
list [3][]*Item // current, select, and all matched items
env []string
query string
lastAction actionType
prompt string
}
type previewResult struct {
@@ -6458,6 +6460,8 @@ func (t *Terminal) Loop() error {
var commandTemplate string
var env []string
var query string
var lastAction actionType
var prompt string
initialOffset := 0
t.previewBox.Wait(func(events *util.Events) {
for req, value := range *events {
@@ -6472,6 +6476,8 @@ func (t *Terminal) Loop() error {
items = request.list
env = request.env
query = request.query
lastAction = request.lastAction
prompt = request.prompt
requested = true
}
}
@@ -6486,7 +6492,18 @@ func (t *Terminal) Loop() error {
version++
// We don't display preview window if no match
if items[0] != nil {
command, tempFiles := t.replacePlaceholder(commandTemplate, false, query, items)
// Not t.replacePlaceholder, which reads fields the main loop writes
command, tempFiles := replacePlaceholder(replacePlaceholderParams{
template: commandTemplate,
stripAnsi: t.ansi,
delimiter: t.delimiter,
printsep: t.printsep,
query: query,
allItems: items,
lastAction: lastAction,
prompt: prompt,
executor: t.executor,
})
cmd := t.executor.ExecCommand(command, true)
cmd.Env = env
@@ -6619,7 +6636,7 @@ func (t *Terminal) Loop() error {
if len(command) > 0 && t.canPreview() {
_, list := t.buildPlusList(command, false)
t.cancelPreview()
t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.evaluateScrollOffset(), list, t.environForPreview(), string(t.input)})
t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.evaluateScrollOffset(), list, t.environForPreview(), string(t.input), t.lastAction, t.promptString})
}
}
@@ -7234,7 +7251,7 @@ func (t *Terminal) Loop() error {
if valid {
t.cancelPreview()
t.previewBox.Set(reqPreviewEnqueue,
previewRequest{t.previewOpts.command, t.evaluateScrollOffset(), list, t.environForPreview(), string(t.input)})
previewRequest{t.previewOpts.command, t.evaluateScrollOffset(), list, t.environForPreview(), string(t.input), t.lastAction, t.promptString})
}
} else {
// Discard the preview content so that it won't accidentally appear
+9
View File
@@ -640,6 +640,15 @@ class TestPreview < TestInteractive
end
end
def test_preview_action_and_prompt
tmux.send_keys %(seq 100 | #{FZF} --preview 'echo [$FZF_ACTION/$FZF_PROMPT/{q}]' --bind 'space:change-prompt(foo> )'), :Enter
tmux.until { |lines| assert lines.any_include?('[start/> /]') }
tmux.send_keys '1'
tmux.until { |lines| assert lines.any_include?('[char/> /1]') }
tmux.send_keys :Space, '2'
tmux.until { |lines| assert lines.any_include?('[char/foo> /12]') }
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|
+43
View File
@@ -0,0 +1,43 @@
<?xml version='1.0' encoding='utf-8'?>
<!--
Per-machine installer for fzf. See the msi job in .github/workflows/release.yml.
wix build wix/main.wxs -arch <x64|arm64> -d Version=<ver> -d BinDir=<dir of fzf.exe>
-->
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<Package
Name="fzf"
Manufacturer="junegunn"
Version="$(var.Version)"
UpgradeCode="C34C8C24-3E05-4D14-BD9A-439C3F6E9942"
Language="1033"
Scope="perMachine">
<SummaryInformation Description="fzf - a command-line fuzzy finder" Manufacturer="junegunn" />
<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed. Setup will now exit." />
<MediaTemplate EmbedCab="yes" />
<StandardDirectory Id="ProgramFiles6432Folder">
<Directory Id="APPLICATIONFOLDER" Name="fzf">
<Directory Id="BinFolder" Name="bin" />
</Directory>
</StandardDirectory>
<Feature Id="Main" Title="fzf" Level="1" AllowAbsent="no">
<ComponentGroupRef Id="MainComponents" />
</Feature>
<ComponentGroup Id="MainComponents" Directory="BinFolder">
<Component Id="FzfBinary">
<File Id="FzfExe" Name="fzf.exe" Source="$(var.BinDir)\fzf.exe" KeyPath="yes" />
</Component>
<Component Id="LicenseFile" Directory="APPLICATIONFOLDER">
<File Id="License" Name="LICENSE" Source="LICENSE" KeyPath="yes" />
</Component>
<Component Id="PathEnv" Guid="8B7C2C6A-F4A0-4E3E-9E37-2E8F3E4E5B6A">
<Environment Id="PATH" Name="PATH" Value="[BinFolder]" Permanent="no" Part="last" Action="set" System="yes" />
</Component>
</ComponentGroup>
</Package>
</Wix>