fzf: add per-shell widget overrides

Allow each shell integration to override widget commands and options before fzf is sourced, so users can disable or tune bindings only where needed.
This commit is contained in:
Austin Horstman 2026-07-01 19:14:04 -05:00
parent 2d92f5398d
commit f15a4bf065
6 changed files with 364 additions and 80 deletions

View file

@ -18,38 +18,13 @@ let
cfg = config.programs.fzf;
renderedColors =
colors: lib.concatStringsSep "," (lib.mapAttrsToList (name: value: "${name}:${value}") colors);
shells = [
"bash"
"fish"
"nushell"
"zsh"
];
bashIntegration = ''
if [[ :$SHELLOPTS: =~ :(vi|emacs): ]]; then
eval "$(${getExe cfg.package} --bash)"
fi
'';
zshIntegration = ''
if [[ $options[zle] = on ]]; then
source <(${getExe cfg.package} --zsh)
fi
'';
fishIntegration = ''
${getExe cfg.package} --fish | source
'';
fzfEnvVars = lib.filterAttrs (_n: v: v != [ ] && v != null) {
FZF_ALT_C_COMMAND = cfg.changeDirWidget.command;
FZF_ALT_C_OPTS = cfg.changeDirWidget.options;
FZF_CTRL_R_COMMAND = cfg.historyWidget.command;
FZF_CTRL_R_OPTS = cfg.historyWidget.options;
FZF_CTRL_T_COMMAND = cfg.fileWidget.command;
FZF_CTRL_T_OPTS = cfg.fileWidget.options;
FZF_DEFAULT_COMMAND = cfg.defaultCommand;
FZF_DEFAULT_OPTS =
cfg.defaultOptions ++ lib.optionals (cfg.colors != { }) [ "--color ${renderedColors cfg.colors}" ];
FZF_TMUX = if cfg.tmux.enableShellIntegration then "1" else null;
FZF_TMUX_OPTS = cfg.tmux.shellIntegrationOptions;
};
in
{
meta.maintainers = with lib.maintainers; [ khaneliman ];
@ -95,8 +70,44 @@ in
options.programs.fzf =
let
mkWidgetOption =
mkWidgetShellOverrides =
{
commandExample ? null,
commandDescription ? null,
optionsExample,
...
}:
lib.genAttrs shells (
_shell:
lib.optionalAttrs (commandDescription != null) {
command = mkOption {
type = types.nullOr types.str;
default = null;
example = commandExample;
description = ''
Shell-specific override for this widget command.
If this is `null`, the global command is used.
'';
};
}
// {
options = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
example = optionsExample;
description = ''
Shell-specific override for this widget's command line
options.
If this is `null`, the global options are used.
'';
};
}
);
mkWidgetOption =
args@{
commandExample ? null,
commandDescription ? null,
optionsExample,
@ -117,7 +128,8 @@ in
example = optionsExample;
description = optionsDescription;
};
};
}
// mkWidgetShellOverrides args;
in
{
enable = lib.mkEnableOption "fzf - a command-line fuzzy finder";
@ -234,55 +246,144 @@ in
enableNushellIntegration = lib.hm.shell.mkNushellIntegrationOption { inherit config; };
};
config = mkIf cfg.enable {
warnings =
lib.optional (cfg.historyWidget.command != null && lib.versionOlder cfg.package.version "0.66.0")
''
`programs.fzf.historyWidget.command` defined in ${lib.showFiles options.programs.fzf.historyWidget.command.files} requires fzf 0.66.0 or greater.
config =
let
shellFzfEnvVars =
shell:
lib.filterAttrs (_n: v: v != null) {
FZF_ALT_C_COMMAND = cfg.changeDirWidget.${shell}.command;
FZF_ALT_C_OPTS = cfg.changeDirWidget.${shell}.options;
FZF_CTRL_R_COMMAND = cfg.historyWidget.${shell}.command;
FZF_CTRL_R_OPTS = cfg.historyWidget.${shell}.options;
FZF_CTRL_T_COMMAND = cfg.fileWidget.${shell}.command;
FZF_CTRL_T_OPTS = cfg.fileWidget.${shell}.options;
};
The configured FZF_CTRL_R_COMMAND value will be ignored by older fzf
versions.
'';
fzfEnvVars = lib.filterAttrs (_n: v: v != [ ] && v != null) {
FZF_ALT_C_COMMAND = cfg.changeDirWidget.command;
FZF_ALT_C_OPTS = cfg.changeDirWidget.options;
FZF_CTRL_R_COMMAND = cfg.historyWidget.command;
FZF_CTRL_R_OPTS = cfg.historyWidget.options;
FZF_CTRL_T_COMMAND = cfg.fileWidget.command;
FZF_CTRL_T_OPTS = cfg.fileWidget.options;
FZF_DEFAULT_COMMAND = cfg.defaultCommand;
FZF_DEFAULT_OPTS =
cfg.defaultOptions
++ lib.optionals (cfg.colors != { }) [
"--color ${
lib.concatStringsSep "," (lib.mapAttrsToList (name: value: "${name}:${value}") cfg.colors)
}"
];
FZF_TMUX = if cfg.tmux.enableShellIntegration then "1" else null;
FZF_TMUX_OPTS = cfg.tmux.shellIntegrationOptions;
};
assertions = [
{
assertion =
(cfg.enableBashIntegration || cfg.enableFishIntegration || cfg.enableZshIntegration)
-> lib.versionAtLeast cfg.package.version "0.48.0";
message = "fzf package version must be 0.48.0 or greater for bash, fish, or zsh integration";
}
{
assertion = cfg.enableNushellIntegration -> lib.versionAtLeast cfg.package.version "0.73.0";
message = "fzf package version must be 0.73.0 or greater for nushell integration";
}
];
home.packages = [ cfg.package ];
home.sessionVariables = lib.mapAttrs (_n: toString) fzfEnvVars;
# Load early so history managers can reclaim Ctrl-R.
programs.bash.initExtra = mkIf cfg.enableBashIntegration (mkOrder 200 bashIntegration);
# Still needs to be initialized after oh-my-zsh (order 800), otherwise
# omz will take precedence.
programs.zsh.initContent = mkIf cfg.enableZshIntegration (mkOrder 910 zshIntegration);
programs.fish.interactiveShellInit = mkIf cfg.enableFishIntegration (mkOrder 200 fishIntegration);
# Initialize after other completion integrations, such as carapace.
# fzf preserves the previous external completer and falls back to it
# when its own completer does not apply.
programs.nushell = lib.mkIf cfg.enableNushellIntegration {
environmentVariables = lib.mapAttrs (_n: toString) fzfEnvVars;
extraConfig = mkAfter ''
source ${
pkgs.runCommand "nushell-fzf-integration.nu" { } ''
${getExe cfg.package} --nushell > $out
in
mkIf cfg.enable {
warnings =
lib.optional (cfg.historyWidget.command != null && lib.versionOlder cfg.package.version "0.66.0")
''
}
'';
};
`programs.fzf.historyWidget.command` defined in ${lib.showFiles options.programs.fzf.historyWidget.command.files} requires fzf 0.66.0 or greater.
};
The configured FZF_CTRL_R_COMMAND value will be ignored by older fzf
versions.
'';
assertions = [
{
assertion =
(cfg.enableBashIntegration || cfg.enableFishIntegration || cfg.enableZshIntegration)
-> lib.versionAtLeast cfg.package.version "0.48.0";
message = "fzf package version must be 0.48.0 or greater for bash, fish, or zsh integration";
}
{
assertion = cfg.enableNushellIntegration -> lib.versionAtLeast cfg.package.version "0.73.0";
message = "fzf package version must be 0.73.0 or greater for nushell integration";
}
];
home.packages = [ cfg.package ];
home.sessionVariables = lib.mapAttrs (_n: toString) fzfEnvVars;
# Load early so history managers can reclaim Ctrl-R.
programs.bash.initExtra =
let
vars = shellFzfEnvVars "bash";
in
mkIf cfg.enableBashIntegration (
mkOrder 200 (
''
if [[ :$SHELLOPTS: =~ :(vi|emacs): ]]; then
''
+ lib.optionalString (vars != { }) "${config.lib.shell.exportAll vars}\n"
+ ''
eval "$(${getExe cfg.package} --bash)"
fi
''
)
);
# Still needs to be initialized after oh-my-zsh (order 800), otherwise
# omz will take precedence.
programs.zsh.initContent =
let
vars = shellFzfEnvVars "zsh";
in
mkIf cfg.enableZshIntegration (
mkOrder 910 (
''
if [[ $options[zle] = on ]]; then
''
+ lib.optionalString (vars != { }) "${config.lib.shell.exportAll vars}\n"
+ ''
source <(${getExe cfg.package} --zsh)
fi
''
)
);
programs.fish.interactiveShellInit =
let
vars = shellFzfEnvVars "fish";
exports = lib.concatStringsSep "\n" (
lib.mapAttrsToList (name: value: "set -gx ${name} ${lib.escapeShellArg (toString value)}") vars
);
in
mkIf cfg.enableFishIntegration (
mkOrder 200 (
lib.optionalString (vars != { }) "${exports}\n"
+ ''
${getExe cfg.package} --fish | source
''
)
);
# Initialize after other completion integrations, such as carapace.
# fzf preserves the previous external completer and falls back to it
# when its own completer does not apply.
programs.nushell = lib.mkIf cfg.enableNushellIntegration {
environmentVariables = lib.mapAttrs (_n: toString) fzfEnvVars;
extraConfig =
let
vars = shellFzfEnvVars "nushell";
assignments = lib.concatStringsSep "\n" (
lib.mapAttrsToList (
name: value: "$env.${name} = ${lib.hm.nushell.toNushell { } (toString value)}"
) vars
);
in
mkAfter (
lib.optionalString (vars != { }) "${assignments}\n"
+ ''
source ${
pkgs.runCommand "nushell-fzf-integration.nu" { } ''
${getExe cfg.package} --nushell > $out
''
}
''
);
};
};
}

View file

@ -0,0 +1,45 @@
{ config, ... }:
{
programs.fzf = {
enable = true;
enableBashIntegration = true;
historyWidget = {
command = "global-history";
bash.command = "";
};
fileWidget.bash.command = "$HOME/bin/files";
package = config.lib.test.mkStubPackage {
name = "fzf";
version = "0.73.0";
buildScript = ''
mkdir -p $out/bin
cat > $out/bin/fzf <<'EOF'
#!/bin/sh
echo "Stub fzf"
EOF
chmod +x $out/bin/fzf
'';
};
};
programs.bash.enable = true;
nmt.script = ''
assertFileRegex home-files/.bashrc \
'export FZF_CTRL_R_COMMAND='
assertFileContains home-files/.bashrc \
'export FZF_CTRL_T_COMMAND="$HOME/bin/files"'
overrideLine=$(grep -n 'export FZF_CTRL_R_COMMAND=' "$TESTED/home-files/.bashrc" | head -n1 | cut -d: -f1)
sourceLine=$(grep -n 'fzf.*--bash' "$TESTED/home-files/.bashrc" | head -n1 | cut -d: -f1)
if [[ -z "$overrideLine" || -z "$sourceLine" || "$overrideLine" -ge "$sourceLine" ]]; then
fail "bash fzf widget override must appear before fzf initialization"
fi
'';
}

View file

@ -1,11 +1,15 @@
{
fzf-bash-widget-overrides = ./bash-widget-overrides.nix;
fzf-disabled = ./disabled.nix;
fzf-options = ./all-options.nix;
fzf-bash-integration = ./bash-integration.nix;
fzf-zsh-widget-overrides = ./zsh-widget-overrides.nix;
fzf-zsh-integration = ./zsh-integration.nix;
fzf-fish-widget-overrides = ./fish-widget-overrides.nix;
fzf-fish-integration = ./fish-integration.nix;
fzf-history-command-version-warning = ./history-command-version-warning.nix;
fzf-nushell-integration = ./nushell-integration.nix;
fzf-nushell-history-command = ./nushell-history-command.nix;
fzf-nushell-widget-overrides = ./nushell-widget-overrides.nix;
fzf-renamed-options = ./renamed-options.nix;
}

View file

@ -0,0 +1,40 @@
{ config, ... }:
{
programs.fzf = {
enable = true;
enableFishIntegration = true;
historyWidget = {
command = "global-history";
fish.command = "";
};
package = config.lib.test.mkStubPackage {
name = "fzf";
version = "0.73.0";
buildScript = ''
mkdir -p $out/bin
cat > $out/bin/fzf <<'EOF'
#!/bin/sh
echo "Stub fzf"
EOF
chmod +x $out/bin/fzf
'';
};
};
programs.fish.enable = true;
nmt.script = ''
assertFileRegex home-files/.config/fish/config.fish \
'set -gx FZF_CTRL_R_COMMAND'
overrideLine=$(grep -n 'set -gx FZF_CTRL_R_COMMAND' "$TESTED/home-files/.config/fish/config.fish" | head -n1 | cut -d: -f1)
sourceLine=$(grep -n 'fzf.*--fish.*source' "$TESTED/home-files/.config/fish/config.fish" | head -n1 | cut -d: -f1)
if [[ -z "$overrideLine" || -z "$sourceLine" || "$overrideLine" -ge "$sourceLine" ]]; then
fail "fish fzf widget override must appear before fzf initialization"
fi
'';
}

View file

@ -0,0 +1,49 @@
{ config, pkgs, ... }:
{
programs.fzf = {
enable = true;
enableNushellIntegration = true;
historyWidget = {
command = "global-history";
nushell.command = "";
};
package = config.lib.test.mkStubPackage {
name = "fzf";
version = "0.73.0";
buildScript = ''
mkdir -p $out/bin
cat > $out/bin/fzf <<'EOF'
#!/bin/sh
echo "Stub fzf"
EOF
chmod +x $out/bin/fzf
'';
};
};
programs.nushell.enable = true;
nmt.script =
let
nushellConfigFile =
if pkgs.stdenv.isDarwin && !config.xdg.enable then
"home-files/Library/Application Support/nushell/config.nu"
else
"home-files/.config/nushell/config.nu";
in
''
assertFileExists "${nushellConfigFile}"
assertFileContent "$(normalizeStorePaths "${nushellConfigFile}")" ${builtins.toFile "nushell-fzf-widget-overrides-expected.nu" ''
load-env {
"FZF_CTRL_R_COMMAND": "global-history"
}
$env.FZF_CTRL_R_COMMAND = ""
source /nix/store/00000000000000000000000000000000-nushell-fzf-integration.nu
''}
'';
}

View file

@ -0,0 +1,45 @@
{ config, ... }:
{
programs.fzf = {
enable = true;
enableZshIntegration = true;
historyWidget = {
command = "global-history";
zsh.command = "";
};
fileWidget.zsh.command = "$HOME/bin/files";
package = config.lib.test.mkStubPackage {
name = "fzf";
version = "0.73.0";
buildScript = ''
mkdir -p $out/bin
cat > $out/bin/fzf <<'EOF'
#!/bin/sh
echo "Stub fzf"
EOF
chmod +x $out/bin/fzf
'';
};
};
programs.zsh.enable = true;
nmt.script = ''
assertFileRegex home-files/.zshrc \
'export FZF_CTRL_R_COMMAND='
assertFileContains home-files/.zshrc \
'export FZF_CTRL_T_COMMAND="$HOME/bin/files"'
overrideLine=$(grep -n 'export FZF_CTRL_R_COMMAND=' "$TESTED/home-files/.zshrc" | head -n1 | cut -d: -f1)
sourceLine=$(grep -n 'fzf.*--zsh' "$TESTED/home-files/.zshrc" | head -n1 | cut -d: -f1)
if [[ -z "$overrideLine" || -z "$sourceLine" || "$overrideLine" -ge "$sourceLine" ]]; then
fail "zsh fzf widget override must appear before fzf initialization"
fi
'';
}