Skip to content

fix(zsh): integrate zsh-autocomplete menu navigation with fzf #205

Description

@ricardo-rod

Summary

Gentleman.Dots loads zsh-autocomplete, Oh My Zsh, Carapace, and fzf --zsh in the same startup path. These integrations compete for completion widgets and key bindings, so reliable menu navigation and fzf-triggered completion cannot currently coexist without a custom override.

The intended behavior is:

  • zsh-autocomplete keeps showing candidates automatically while typing.
  • Tab enters the menu and moves forward through candidates.
  • Shift+Tab moves backward through candidates.
  • **<Tab> still opens the fzf completion picker.

Current behavior

The checked-in GentlemanZsh/.zshrc loads zsh-autocomplete before Oh My Zsh and later runs eval "$(fzf --zsh)". At runtime:

  • zsh-autocomplete owns completion styles and menu widgets.
  • fzf binds Ctrl-I (Tab) to fzf-completion and captures the previous widget as its fallback.
  • A direct post-fzf bindkey '^I' menu-select restores menu navigation but makes the fzf ** trigger unreachable.
  • Setting fzf's fallback directly to either menu-select or menu-complete does not preserve repeated forward/backward navigation.

Reproduction

  1. Start Zsh with the current Gentleman.Dots stack: zsh-autocomplete, Oh My Zsh, Carapace, and fzf --zsh.

  2. Create several similarly named directories, for example:

    abc/  abcd/  abcn/  abcp/  abcz/
    
  3. Type cd abc and wait for the automatic candidate list.

  4. Press Tab repeatedly, then Shift+Tab.

  5. Type cd ** and press Tab.

Verified fix

A trigger-aware ZLE widget can delegate only explicit fzf-triggered completion to fzf and use the zsh-autocomplete menu for normal Tab completion:

gentleman-fzf-or-menu() {
  local trigger=${FZF_COMPLETION_TRIGGER-'**'}
  if [[ -n $trigger && $LBUFFER == *"$trigger" ]]; then
    zle fzf-completion
  else
    zle menu-select
  fi
}

zle -N gentleman-fzf-or-menu
bindkey '^I' gentleman-fzf-or-menu
bindkey -M menuselect '^I' menu-complete

if [[ -n "${terminfo[kcbt]}" ]]; then
  bindkey "${terminfo[kcbt]}" menu-select
  bindkey -M menuselect "${terminfo[kcbt]}" reverse-menu-complete
fi

This must be installed after eval "$(fzf --zsh)" so it becomes the final Tab dispatcher while retaining the fzf-completion widget.

Verification evidence

The bridge was exercised in isolated real terminal sessions using tmux capture panes, not only by inspecting bindings.

Fedora container

  • Fedora 43 container
  • Zsh 5.9
  • zsh-autocomplete loaded
  • Oh My Zsh, Carapace, and fzf loaded

Observed:

  1. Before Tab, all five candidates appeared automatically.
  2. Tab selected abc/.
  3. Further Tab presses moved to abcd/ and abcn/.
  4. Shift+Tab moved backward to abcd/.
  5. cd **<Tab> opened fzf with all five candidates.

Local regression check

The same bridge was tested with Zsh 5.9.2:

  • zsh -n ~/.zshrc passed.
  • Automatic candidate display passed.
  • Tab forward navigation passed.
  • Shift+Tab reverse navigation passed.
  • **<Tab> fzf activation passed.
  • No interactive startup warnings occurred during the tmux tests.

Terminal portability

Shift+Tab bindings are guarded by a non-empty terminfo[kcbt] check. With TERM=dumb, kcbt was absent and Zsh still started without empty-argument bindkey errors. This verifies the fallback path locally; actual macOS execution remains to be tested.

Acceptance criteria

  • Automatic candidates from zsh-autocomplete remain visible while typing.
  • Normal Tab enters the completion menu and advances through candidates.
  • Shift+Tab moves backward when the terminal provides terminfo[kcbt].
  • Startup succeeds without Shift+Tab bindings when terminfo[kcbt] is unavailable.
  • **<Tab> opens fzf completion.
  • The behavior is covered by a focused regression test or reproducible runtime harness.
  • Linux verification passes.
  • macOS is either verified or explicitly documented as pending.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions