Compare commits

..

11 Commits

Author SHA1 Message Date
Junegunn Choi f5fbfd848e Let bw theme inherit overridden colors
- Mark derived color slots undefined so fg/bg/list-bg propagate
- Add missing Footer slot (was rendering black)
2026-06-07 18:12:19 +09:00
Junegunn Choi dea72834ed Keep base fg/bg when resolving colors in bw theme
Trailing prompt space and other base-colored segments no longer reset to terminal default
2026-06-07 18:12:19 +09:00
LangLangBart abee152255 test: ALT-C regression tests
discussed in https://github.com/junegunn/fzf/issues/4816
2026-06-05 16:24:27 +09:00
LangLangBart bf114bcc21 test: install nushell in Dockerfile
ref: https://docs.docker.com/reference/dockerfile/#here-documents
2026-06-05 16:24:27 +09:00
Junegunn Choi 838ac7554b make lint 2026-06-02 20:23:23 +09:00
Junegunn Choi ae78a5c56d Allow bare put action in transform output
transform/bg-transform now permit bare `put`, inserting the key that
triggered the action (`a:transform:echo put` puts `a`).
2026-06-02 20:21:04 +09:00
Yi-Yo Chiang 7d647c70c2 [shell][zsh] Don't resolve symlinks in ALT-c (#4816)
This way ALT-c behaves more aligned with `cd`.

Imagine a setup like:
```
/foo -> foo_real
/foo_real/bar
```

Right now if we first `cd foo` (a symlink to `foo_real`), and
then use ALT-c to goto `bar`, then we would end up executing
`cd /foo_real/bar` instead of `cd /foo/bar`. `$PWD = /foo_real/bar`.

For comparison, if we first `cd foo` and then `cd bar`, we end up with
`$PWD = /foo/bar`.

This commit changes the internal logic of `fzf-cd-widget` to first run
`cd <result of FZF_ALT_C_COMMAND>` in a subshell to simulate the
behavior of `cd`, and then insert the target PWD into the shell history.
This way we get behavior consistent with the builtin `cd` command, while
also recording reusable shell history.
2026-05-31 12:38:12 +09:00
Junegunn Choi 6bd17f8f9a Revert "[shell][zsh] Don't resolve symlinks in ALT-c (#4816) (#4817)"
This reverts commit 249a6df4a4.
2026-05-31 12:37:05 +09:00
Junegunn Choi 249a6df4a4 [shell][zsh] Don't resolve symlinks in ALT-c (#4816) (#4817)
This way ALT-c behaves more aligned with `cd`.

Imagine a setup like:
```
/foo -> foo_real
/foo_real/bar
```

Right now if we first `cd foo` (a symlink to `foo_real`), and
then use ALT-c to goto `bar`, then we would end up executing
`cd /foo_real/bar` instead of `cd /foo/bar`. `$PWD = /foo_real/bar`.

For comparison, if we first `cd foo` and then `cd bar`, we end up with
`$PWD = /foo/bar`.

This commit changes the internal logic of `fzf-cd-widget` to first run
`cd <result of FZF_ALT_C_COMMAND>` in a subshell to simulate the
behavior of `cd`, and then insert the target PWD into the shell history.
This way we get behavior consistent with the builtin `cd` command, while
also recording reusable shell history.

Co-authored-by: Yi-Yo Chiang <5255547+silverneko@users.noreply.github.com>

Close #4816
2026-05-31 12:34:58 +09:00
Junegunn Choi a50619388d [install] Fix empty-shell detection in install script
${#shells} is the string length, not the shell count.

Thanks to @matheus-pacifico for the report.

Close #4813
2026-05-28 23:02:34 +09:00
Copilot 5ef8dea36e Prevent nushell source contamination in install shell loop (#4812) 2026-05-28 10:26:43 +09:00
14 changed files with 147 additions and 57 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
go-version: "1.23"
- name: Setup Ruby
uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1
uses: ruby/setup-ruby@97ecb7b512899eb71ab1bf2310a624c6f1589ac6 # v1
with:
ruby-version: 3.4.6
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
go-version: "1.23"
- name: Setup Ruby
uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1
uses: ruby/setup-ruby@97ecb7b512899eb71ab1bf2310a624c6f1589ac6 # v1
with:
ruby-version: 3.0.0
+11 -1
View File
@@ -1,5 +1,15 @@
FROM rubylang/ruby:3.4.1-noble
RUN apt-get update -y && apt install -y git make golang zsh fish tmux
RUN apt-get update && apt-get install -y git make golang zsh fish tmux
# https://www.nushell.sh/book/installation.html
RUN <<EOF
set -ex
apt-get install -y wget gnupg
wget -qO- https://apt.fury.io/nushell/gpg.key | gpg --dearmor -o /etc/apt/keyrings/fury-nushell.gpg
echo "deb [signed-by=/etc/apt/keyrings/fury-nushell.gpg] https://apt.fury.io/nushell/ /" | tee /etc/apt/sources.list.d/fury-nushell.list
apt-get update
apt-get install -y nushell
EOF
RUN gem install --no-document -v 5.22.3 minitest
RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc
RUN echo '. ~/.bashrc' >> ~/.bash_profile
+4 -4
View File
@@ -227,13 +227,13 @@ fi
for s in $shells; do
bin=$s
[[ "$s" = nushell ]] && bin=nu
[[ $s == nushell ]] && bin=nu
if ! command -v "$bin" > /dev/null; then
shells=${shells/$s/}
fi
done
if [[ ${#shells} -lt 3 ]]; then
if [[ -z ${shells// /} ]]; then
echo "No shell configuration to be updated."
exit 0
fi
@@ -252,10 +252,10 @@ fi
echo
for shell in $shells; do
[[ $shell == nushell ]] && continue
fzf_completion="source \"$fzf_base/shell/completion.${shell}\""
fzf_key_bindings="source \"$fzf_base/shell/key-bindings.${shell}\""
[[ $shell == fish ]] && continue
[[ $shell == nushell ]] && continue
src=${prefix_expand}.${shell}
echo -n "Generate $src ... "
@@ -442,7 +442,7 @@ if [[ $shells =~ fish ]]; then
fi
fi
if [[ "$shells" =~ nushell ]]; then
if [[ $shells =~ nushell ]]; then
if [[ $key_bindings -eq 1 || $auto_completion -eq 1 ]]; then
echo "Setting up Nushell integration ..."
nushell_autoload_dir=$(nu -c '$nu.user-autoload-dirs | first')
+7 -1
View File
@@ -110,8 +110,14 @@ fzf-cd-widget() {
zle redisplay
return 0
fi
# Use subshell expansion to get the absolute PWD of the target dir.
# This allows the recorded shell history to be reused even from a different
# working directory.
# If failed, fallback to the unexpanded path to surface the error to the user.
# NOTE: Don't use the `:a` modifier as it resolves symlinks like `pwd -P`.
dir=$(builtin cd >/dev/null -- "${dir}" && echo "${PWD}" || echo "${dir}")
zle push-line # Clear buffer. Auto-restored on next prompt.
BUFFER="builtin cd -- ${(q)dir:a}"
BUFFER="builtin cd -- ${(q)dir}"
zle accept-line
local ret=$?
unset dir # ensure this doesn't end up appearing in prompt expansion
+3 -5
View File
@@ -10,7 +10,6 @@ import (
"strconv"
"strings"
"time"
"unicode"
"github.com/junegunn/fzf/src/algo"
"github.com/junegunn/fzf/src/tui"
@@ -1734,10 +1733,10 @@ Loop:
return masked
}
func parseSingleActionList(str string) ([]*action, error) {
func parseSingleActionList(str string, putAllowed bool) ([]*action, error) {
// We prepend a colon to satisfy argActionRegexp and remove it later
masked := maskActionContents(":" + str)[1:]
return parseActionList(masked, str, []*action{}, false)
return parseActionList(masked, str, []*action{}, putAllowed)
}
func parseActionList(masked string, original string, prevActions []*action, putAllowed bool) ([]*action, error) {
@@ -2043,8 +2042,7 @@ func parseKeymap(keymap map[tui.Event][]*action, str string) error {
}
key = firstKey(keys)
}
putAllowed := key.Type == tui.Rune && unicode.IsGraphic(key.Char)
keymap[key], err = parseActionList(pair[1], origPairStr[len(pair[0])+1:], keymap[key], putAllowed)
keymap[key], err = parseActionList(pair[1], origPairStr[len(pair[0])+1:], keymap[key], key.Printable())
if err != nil {
return err
}
+2 -2
View File
@@ -572,7 +572,7 @@ func TestValidateSign(t *testing.T) {
}
func TestParseSingleActionList(t *testing.T) {
actions, _ := parseSingleActionList("Execute@foo+bar,baz@+up+up+reload:down+down")
actions, _ := parseSingleActionList("Execute@foo+bar,baz@+up+up+reload:down+down", false)
if len(actions) != 4 {
t.Errorf("Invalid number of actions parsed:%d", len(actions))
}
@@ -588,7 +588,7 @@ func TestParseSingleActionList(t *testing.T) {
}
func TestParseSingleActionListError(t *testing.T) {
_, err := parseSingleActionList("change-query(foobar)baz")
_, err := parseSingleActionList("change-query(foobar)baz", false)
if err == nil {
t.Errorf("Failed to detect error")
}
+4 -1
View File
@@ -198,7 +198,10 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
start := 0
ansiToColorPair := func(ansi ansiOffset, base tui.ColorPair) tui.ColorPair {
if !theme.Colored {
return tui.NewColorPair(-1, -1, ansi.color.attr).MergeAttr(base)
// Ignore ANSI colors but keep the attributes. Retain the base
// colors (e.g. an overridden input-bg or list-bg) instead of
// resetting to the terminal default.
return tui.NewColorPair(base.Fg(), base.Bg(), ansi.color.attr).MergeAttr(base)
}
// fd --color always | fzf --ansi --delimiter / --nth -1 --color fg:dim:strip,nth:regular
if base.ShouldStripColors() {
+1 -1
View File
@@ -240,7 +240,7 @@ Loop:
}
body = body[:contentLength]
actions, err := parseSingleActionList(strings.Trim(string(body), "\r\n"))
actions, err := parseSingleActionList(strings.Trim(string(body), "\r\n"), false)
if err != nil {
return bad(err.Error())
}
+2 -1
View File
@@ -6973,7 +6973,8 @@ func (t *Terminal) Loop() error {
})
case actTransform, actBgTransform:
capture(false, func(body string) {
if actions, err := parseSingleActionList(strings.Trim(body, "\r\n")); err == nil {
// Allow 'put' if the triggering key is a printable character
if actions, err := parseSingleActionList(strings.Trim(body, "\r\n"), event.Printable()); err == nil {
// NOTE: We're not properly passing the return value here
doActions(actions)
}
+50 -38
View File
@@ -4,6 +4,7 @@ import (
"strconv"
"strings"
"time"
"unicode"
"github.com/junegunn/fzf/src/util"
"github.com/rivo/uniseg"
@@ -252,6 +253,12 @@ func (e Event) Comparable() Event {
return Event{e.Type, e.Char, nil}
}
// Printable returns true if the event is a printable character that can be
// inserted into the query (e.g. via the 'put' action).
func (e Event) Printable() bool {
return e.Type == Rune && unicode.IsGraphic(e.Char)
}
func (e Event) KeyName() string {
if me := e.MouseEvent; me != nil {
return me.Name()
@@ -995,51 +1002,56 @@ func init() {
undefined := ColorAttr{colUndefined, AttrUndefined}
NoColorTheme = &ColorTheme{
Colored: false,
Input: defaultColor,
Fg: defaultColor,
Bg: defaultColor,
ListFg: defaultColor,
ListBg: defaultColor,
Colored: false,
// Root colors. Everything else is left undefined so that overriding a
// root (e.g. --color bw,bg:blue) propagates to the derived colors,
// just like in the colored base themes.
Input: defaultColor,
Fg: defaultColor,
Bg: defaultColor,
DarkBg: defaultColor,
Prompt: defaultColor,
Match: defaultColor,
Spinner: defaultColor,
Info: defaultColor,
Pointer: defaultColor,
Marker: defaultColor,
Header: defaultColor,
Footer: defaultColor,
BorderLabel: defaultColor,
// Derived colors. Left undefined so they inherit from a root.
ListFg: undefined,
ListBg: undefined,
AltBg: undefined,
SelectedFg: defaultColor,
SelectedBg: defaultColor,
SelectedMatch: defaultColor,
DarkBg: defaultColor,
Prompt: defaultColor,
Match: defaultColor,
SelectedFg: undefined,
SelectedBg: undefined,
SelectedMatch: undefined,
Current: undefined,
CurrentMatch: undefined,
Spinner: defaultColor,
Info: defaultColor,
Pointer: defaultColor,
Marker: defaultColor,
Header: defaultColor,
Border: undefined,
BorderLabel: defaultColor,
Ghost: undefined,
Disabled: defaultColor,
PreviewFg: defaultColor,
PreviewBg: defaultColor,
Disabled: undefined,
PreviewFg: undefined,
PreviewBg: undefined,
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,
PreviewBorder: undefined,
PreviewScrollbar: undefined,
PreviewLabel: undefined,
ListLabel: undefined,
ListBorder: 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,
}
+2 -1
View File
@@ -136,6 +136,7 @@ class Tmux
rescue Minitest::Assertion
retries += 1
raise if retries > 5
retry
end
send_keys 'clear', :Enter
@@ -295,7 +296,7 @@ class Tmux
if @shell == :nushell
message = "Prepare[#{tries}]"
send_keys 'C-u', 'C-l'
sleep 0.2
sleep(0.2)
send_keys ' ', 'C-u', :Enter, message
self.until { |lines| lines[-1] == message }
else
+18
View File
@@ -971,6 +971,24 @@ class TestCore < TestInteractive
tmux.until { |lines| assert_includes lines[1], ' aabravo/aabravo' }
end
def test_transform_put
tmux.send_keys %(seq 1000 | #{FZF} --bind 'a:transform:echo put'), :Enter
tmux.until { |lines| assert_equal 1000, lines.match_count }
tmux.send_keys :a
tmux.until { |lines| assert_equal '> a', lines.last }
tmux.send_keys :b
tmux.until { |lines| assert_equal '> ab', lines.last }
end
# The async callback runs in a later iteration, but 'put' must still insert
# the key that triggered the bg-transform (snapshot of the scheduling event).
def test_bg_transform_put
tmux.send_keys %(seq 1000 | #{FZF} --bind 'a:bg-transform:sleep 0.5; echo put'), :Enter
tmux.until { |lines| assert_equal 1000, lines.match_count }
tmux.send_keys 'ab'
tmux.until { |lines| assert_equal '> ba', lines.last }
end
def test_accept_non_empty
tmux.send_keys %(seq 1000 | #{fzf('--print-query --bind enter:accept-non-empty')}), :Enter
tmux.until { |lines| assert_equal 1000, lines.match_count }
+41
View File
@@ -100,6 +100,47 @@ module TestShell
tmux.until { |lines| assert_equal '/tmp', lines[-1] }
end
def test_alt_c_symlink
base = '/tmp/fzf-test-alt-c-symlink'
FileUtils.rm_rf(base)
FileUtils.mkdir_p("#{base}/real/subdir")
FileUtils.ln_s("#{base}/real", "#{base}/link")
tmux.prepare
tmux.send_keys "cd #{base}/link", :Enter
tmux.prepare
tmux.send_keys :Escape, :c
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys 'subdir'
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
tmux.prepare
tmux.send_keys :pwd, :Enter
tmux.until { |lines| assert_equal "#{base}/link/subdir", lines[-1] }
ensure
FileUtils.rm_rf(base)
end
def test_alt_c_absolute_cmd
base = '/tmp/fzf-test-alt-c-absolute'
FileUtils.rm_rf(base)
FileUtils.mkdir_p(base)
set_var('FZF_ALT_C_COMMAND', "echo #{base}")
tmux.prepare
tmux.send_keys 'cd /tmp', :Enter
tmux.prepare
tmux.send_keys :Escape, :c
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
tmux.prepare
tmux.send_keys :pwd, :Enter
tmux.until { |lines| assert_equal base, lines[-1] }
ensure
FileUtils.rm_rf(base)
end
def test_ctrl_r
tmux.prepare
tmux.send_keys 'echo 1st', :Enter