1
0
Fork 0
mirror of https://github.com/nix-community/home-manager.git synced 2025-11-08 11:36:05 +01:00
home-manager/modules/programs/fish.nix
2025-11-06 10:27:58 -06:00

823 lines
24 KiB
Nix

{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
isAttrs
literalExpression
mkIf
mkOption
mkEnableOption
optional
types
;
cfg = config.programs.fish;
pluginModule = types.submodule {
options = {
src = mkOption {
type = types.path;
description = ''
Path to the plugin folder.
Relevant pieces will be added to the fish function path and
the completion path. The {file}`init.fish` and
{file}`key_binding.fish` files are sourced if
they exist.
'';
};
name = mkOption {
type = types.str;
description = ''
The name of the plugin.
'';
};
};
};
functionModule = types.submodule {
options = {
body = mkOption {
type = types.lines;
description = ''
The function body.
'';
};
argumentNames = mkOption {
type = with types; nullOr (either str (listOf str));
default = null;
description = ''
Assigns the value of successive command line arguments to the names
given.
'';
};
description = mkOption {
type = with types; nullOr str;
default = null;
description = ''
A description of what the function does, suitable as a completion
description.
'';
};
wraps = mkOption {
type = with types; nullOr str;
default = null;
description = ''
Causes the function to inherit completions from the given wrapped
command.
'';
};
onEvent = mkOption {
type = with types; nullOr (either str (listOf str));
default = null;
description = ''
Tells fish to run this function when the specified named event is
emitted. Fish internally generates named events e.g. when showing the
prompt.
'';
};
onVariable = mkOption {
type = with types; nullOr str;
default = null;
description = ''
Tells fish to run this function when the specified variable changes
value.
'';
};
onJobExit = mkOption {
type = with types; nullOr (either str int);
default = null;
description = ''
Tells fish to run this function when the job with the specified group
ID exits. Instead of a PID, the stringer `caller` can
be specified. This is only legal when in a command substitution, and
will result in the handler being triggered by the exit of the job
which created this command substitution.
'';
};
onProcessExit = mkOption {
type = with types; nullOr (either str int);
default = null;
example = "$fish_pid";
description = ''
Tells fish to run this function when the fish child process with the
specified process ID exits. Instead of a PID, for backwards
compatibility, `%self` can be specified as an alias
for `$fish_pid`, and the function will be run when
the current fish instance exits.
'';
};
onSignal = mkOption {
type = with types; nullOr (either str int);
default = null;
example = [
"SIGHUP"
"HUP"
1
];
description = ''
Tells fish to run this function when the specified signal is
delivered. The signal can be a signal number or signal name.
'';
};
noScopeShadowing = mkOption {
type = types.bool;
default = false;
description = ''
Allows the function to access the variables of calling functions.
'';
};
inheritVariable = mkOption {
type = with types; nullOr str;
default = null;
description = ''
Snapshots the value of the specified variable and defines a local
variable with that same name and value when the function is defined.
'';
};
};
};
completionModule = types.submodule {
options = {
body = mkOption {
type = types.lines;
description = ''
The completion file's body.
'';
};
};
};
abbrModule = types.submodule {
options = {
expansion = mkOption {
type = with types; nullOr str;
default = null;
description = ''
The command expanded by an abbreviation.
'';
};
position = mkOption {
type = with types; nullOr str;
default = null;
example = "anywhere";
description = ''
If the position is "command", the abbreviation expands only if
the position is a command. If it is "anywhere", the abbreviation
expands anywhere.
'';
};
regex = mkOption {
type = with types; nullOr str;
default = null;
description = ''
The regular expression pattern matched instead of the literal name.
'';
};
command = mkOption {
type = with types; nullOr (either str (listOf str));
default = null;
description = ''
Specifies the command(s) for which the abbreviation should expand. If
set, the abbreviation will only expand when used as an argument to
the given command(s).
'';
};
setCursor = mkOption {
type = with types; (either bool str);
default = false;
description = ''
The marker indicates the position of the cursor when the abbreviation
is expanded. When setCursor is true, the marker is set with a default
value of "%".
'';
};
function = mkOption {
type = with types; nullOr str;
default = null;
description = ''
The fish function expanded instead of a literal string.
'';
};
};
};
bindModule = types.submodule (
{ config, ... }:
{
options = {
enable = mkEnableOption "enable the bind. Set false if you want to ignore the bind" // {
default = true;
};
mode = mkOption {
description = "Specify the bind mode that the bind is used in";
type =
with types;
nullOr (enum [
"default"
"insert"
"paste"
]);
default = null;
};
command = mkOption {
description = "command that will be execute";
type =
let
origin =
with types;
nullOr (oneOf [
str
(listOf str)
]);
in
origin
// {
description = "string or list of string (optional when erase is set to true)";
check = x: if !config.erase && isNull x then false else origin.check x;
};
default = null;
};
setsMode = mkOption {
description = "Change current mode after bind is executed";
type =
with types;
nullOr (enum [
"default"
"insert"
"paste"
]);
default = null;
};
erase = mkEnableOption "remove bind";
silent = mkEnableOption "Operate silently";
repaint = mkEnableOption "redraw prompt after command";
operate = mkOption {
description = "Operate on preset bindings or user bindings";
type =
with types;
nullOr (enum [
"preset"
"user"
]);
default = null;
};
};
}
);
abbrsStr = lib.concatStringsSep "\n" (
lib.mapAttrsToList (
name: def:
let
mods =
lib.cli.toGNUCommandLineShell
{
mkOption =
k: v:
if v == null then
[ ]
else if k == "set-cursor" then
[ "--${k}=${lib.generators.mkValueStringDefault { } v}" ]
else
[
"--${k}"
(lib.generators.mkValueStringDefault { } v)
];
}
{
inherit (def)
position
regex
command
function
;
set-cursor = def.setCursor;
};
modifiers = if isAttrs def then mods else "";
expansion = if isAttrs def then def.expansion else def;
in
"abbr --add ${modifiers} -- ${name}"
+ lib.optionalString (expansion != null) " ${lib.escapeShellArg expansion}"
) cfg.shellAbbrs
);
aliasesStr = lib.concatStringsSep "\n" (
lib.mapAttrsToList (k: v: "alias ${k} ${lib.escapeShellArg v}") cfg.shellAliases
);
filteredBinds = lib.filterAttrs (_: { enable, ... }: enable) cfg.binds;
bindsStr = lib.concatStringsSep "\n" (
lib.flatten (
lib.mapAttrsToList (
k:
{
silent,
erase,
repaint,
operate,
mode,
setsMode,
command,
...
}:
let
opts =
lib.optionals silent [ "-s" ]
++ lib.optionals (!isNull operate) [ "--${operate}" ]
++ lib.optionals (!isNull mode) [
"--mode"
mode
]
++ lib.optionals (!isNull setsMode) [
"--sets-mode"
setsMode
];
cmdNormal = lib.concatStringsSep " " (
[ "bind" ]
++ opts
++ [ k ]
++ map lib.escapeShellArg (lib.flatten [ command ])
++ lib.optional repaint "repaint"
);
cmdErase = lib.concatStringsSep " " (
[
"bind"
"-e"
]
++ opts
++ [ k ]
);
in
lib.optionals erase [ cmdErase ] ++ lib.optionals (!isNull command) [ cmdNormal ]
) filteredBinds
)
);
fishIndent =
name: text:
pkgs.runCommand name {
nativeBuildInputs = [ cfg.package ];
inherit text;
passAsFile = [ "text" ];
} "env HOME=$(mktemp -d) fish_indent < $textPath > $out";
translatedSessionVariables = pkgs.runCommandLocal "hm-session-vars.fish" { } ''
(echo "function setup_hm_session_vars;"
${pkgs.buildPackages.babelfish}/bin/babelfish \
<${config.home.sessionVariablesPackage}/etc/profile.d/hm-session-vars.sh
echo "end"
echo "setup_hm_session_vars") > $out
'';
in
{
imports = [
(lib.mkRemovedOptionModule [ "programs" "fish" "promptInit" ] ''
Prompt is now configured through the
programs.fish.interactiveShellInit
option. Please change to use that instead.
'')
];
options = {
programs.fish = {
enable = lib.mkEnableOption "fish, the friendly interactive shell";
package = lib.mkPackageOption pkgs "fish" { };
generateCompletions =
lib.mkEnableOption "the automatic generation of completions based upon installed man pages"
// {
default = true;
};
shellAliases = mkOption {
type = with types; attrsOf str;
default = { };
example = literalExpression ''
{
g = "git";
"..." = "cd ../..";
}
'';
description = ''
An attribute set that maps aliases (the top level attribute names
in this option) to command strings or directly to build outputs.
'';
};
shellAbbrs = mkOption {
type = with types; attrsOf (either str abbrModule);
default = { };
example = literalExpression ''
{
l = "less";
gco = "git checkout";
"-C" = {
position = "anywhere";
expansion = "--color";
};
}
'';
description = ''
An attribute set that maps aliases (the top level attribute names
in this option) to abbreviations. Abbreviations are expanded with
the longer phrase after they are entered.
'';
};
preferAbbrs = mkOption {
type = types.bool;
default = false;
example = true;
description = ''
If enabled, abbreviations will be preferred over aliases when
other modules define aliases for fish.
'';
};
binds = mkOption {
type = types.attrsOf bindModule;
default = { };
description = "Manage key bindings";
example =
lib.literalExpression # nix
''
{
"alt-shift-b".command = "fish_commandline_append bat";
"alt-s".erase = true;
"alt-s".operate = "preset";
}
'';
};
shellInit = mkOption {
type = types.lines;
default = "";
description = ''
Shell script code called during fish shell
initialisation.
'';
};
loginShellInit = mkOption {
type = types.lines;
default = "";
description = ''
Shell script code called during fish login shell
initialisation.
'';
};
interactiveShellInit = mkOption {
type = types.lines;
default = "";
description = ''
Shell script code called during interactive fish shell
initialisation.
'';
};
shellInitLast = mkOption {
type = types.lines;
default = "";
description = ''
Shell script code called during interactive fish shell
initialisation, this will be the last thing executed in fish startup.
'';
};
};
programs.fish.plugins = mkOption {
type = types.listOf pluginModule;
default = [ ];
example = literalExpression ''
[
{
name = "z";
src = pkgs.fetchFromGitHub {
owner = "jethrokuan";
repo = "z";
rev = "ddeb28a7b6a1f0ec6dae40c636e5ca4908ad160a";
sha256 = "0c5i7sdrsp0q3vbziqzdyqn4fmp235ax4mn4zslrswvn8g3fvdyh";
};
}
# oh-my-fish plugins are stored in their own repositories, which
# makes them simple to import into home-manager.
{
name = "fasd";
src = pkgs.fetchFromGitHub {
owner = "oh-my-fish";
repo = "plugin-fasd";
rev = "38a5b6b6011106092009549e52249c6d6f501fba";
sha256 = "06v37hqy5yrv5a6ssd1p3cjd9y3hnp19d3ab7dag56fs1qmgyhbs";
};
}
]
'';
description = ''
The plugins to source in
{file}`conf.d/99plugins.fish`.
'';
};
programs.fish.functions = mkOption {
type = with types; attrsOf (either lines functionModule);
default = { };
example = literalExpression ''
{
__fish_command_not_found_handler = {
body = "__fish_default_command_not_found_handler $argv[1]";
onEvent = "fish_command_not_found";
};
gitignore = "curl -sL https://www.gitignore.io/api/$argv";
}
'';
description = ''
Basic functions to add to fish. For more information see
<https://fishshell.com/docs/current/cmds/function.html>.
'';
};
programs.fish.completions = mkOption {
type = with types; attrsOf (either lines completionModule);
default = { };
example = literalExpression ''
{
my-prog = '''
complete -c myprog -s o -l output
''';
my-app = {
body = '''
complete -c myapp -s -v
''';
};
}
'';
description = ''
Custom fish completions. For more information see
<https://fishshell.com/docs/current/completions.html>.
'';
};
};
config = mkIf cfg.enable (
lib.mkMerge [
{ home.packages = [ cfg.package ]; }
(mkIf cfg.generateCompletions (
let
generateCompletions =
let
getName =
attrs: attrs.name or "${attrs.pname or "«pname-missing»"}-${attrs.version or "«version-missing»"}";
in
package:
pkgs.runCommand "${getName package}-fish-completions"
{
srcs = [
package
]
++ lib.filter (p: p != null) (
builtins.map (outName: package.${outName} or null) config.home.extraOutputsToInstall
);
nativeBuildInputs = [ pkgs.python3 ];
buildInputs = [ cfg.package ];
preferLocalBuild = true;
}
''
mkdir -p $out
for src in $srcs; do
if [ -d $src/share/man ]; then
find -L $src/share/man -type f \
-exec python ${cfg.package}/share/fish/tools/create_manpage_completions.py --directory $out {} + \
> /dev/null
fi
done
'';
allCompletions =
let
cmp = (a: b: (a.meta.priority or 0) > (b.meta.priority or 0));
in
map generateCompletions (lib.sort cmp config.home.packages);
in
{
# Support completion for `man` by building a cache for `apropos`.
programs.man.generateCaches = lib.mkDefault true;
xdg.dataFile."fish/home-manager_generated_completions".source =
let
# Paths later in the list will overwrite those already linked
destructiveSymlinkJoin =
args_@{
name,
preferLocalBuild ? true,
allowSubstitutes ? false,
postBuild ? "",
...
}:
let
args =
removeAttrs args_ [
"name"
"postBuild"
]
// {
# pass the defaults
inherit preferLocalBuild allowSubstitutes;
};
in
pkgs.runCommand name args ''
mkdir -p $out
for i in $paths; do
if [ -z "$(find $i -prune -empty)" ]; then
cp -srf $i/* $out
fi
done
${postBuild}
'';
in
destructiveSymlinkJoin {
name = "${config.home.username}-fish-completions";
paths = allCompletions;
};
# For packages with no Fish completions, generateCompletions will build an empty directory,
# which means they will not be in our runtime closure. Force a dependency so these do not get
# constantly rebuilt.
home.extraDependencies = allCompletions;
programs.fish.interactiveShellInit = ''
# add completions generated by Home Manager to $fish_complete_path
begin
set -l joined (string join " " $fish_complete_path)
set -l prev_joined (string replace --regex "[^\s]*generated_completions.*" "" $joined)
set -l post_joined (string replace $prev_joined "" $joined)
set -l prev (string split " " (string trim $prev_joined))
set -l post (string split " " (string trim $post_joined))
set fish_complete_path $prev "${config.xdg.dataHome}/fish/home-manager_generated_completions" $post
end
'';
}
))
(mkIf (filteredBinds != { }) {
programs.fish.functions.fish_user_key_bindings = bindsStr;
})
{
xdg.configFile."fish/config.fish".source = fishIndent "config.fish" ''
# ~/.config/fish/config.fish: DO NOT EDIT -- this file has been generated
# automatically by home-manager.
# Only execute this file once per shell.
set -q __fish_home_manager_config_sourced; and exit
set -g __fish_home_manager_config_sourced 1
source ${translatedSessionVariables}
${cfg.shellInit}
status is-login; and begin
# Login shell initialisation
${cfg.loginShellInit}
end
status is-interactive; and begin
# Abbreviations
${abbrsStr}
# Aliases
${aliasesStr}
# Interactive shell initialisation
${cfg.interactiveShellInit}
end
${cfg.shellInitLast}
'';
}
{
xdg.configFile = lib.mapAttrs' (name: def: {
name = "fish/functions/${name}.fish";
value = {
source =
let
modifierStr = n: v: optional (v != null) ''--${n}="${toString v}"'';
modifierStrs = n: v: optional (v != null) "--${n}=${toString v}";
modifierBool = n: v: optional (v != null && v) "--${n}";
mods =
with def;
modifierStr "description" description
++ modifierStr "wraps" wraps
++ lib.concatMap (modifierStr "on-event") (lib.toList onEvent)
++ modifierStr "on-variable" onVariable
++ modifierStr "on-job-exit" onJobExit
++ modifierStr "on-process-exit" onProcessExit
++ modifierStr "on-signal" onSignal
++ modifierBool "no-scope-shadowing" noScopeShadowing
++ modifierStr "inherit-variable" inheritVariable
++ modifierStrs "argument-names" argumentNames;
modifiers = if isAttrs def then " ${toString mods}" else "";
body = if isAttrs def then def.body else def;
in
fishIndent "${name}.fish" ''
function ${name}${modifiers}
${lib.strings.removeSuffix "\n" body}
end
'';
};
}) cfg.functions;
}
{
xdg.configFile = lib.mapAttrs' (name: def: {
name = "fish/completions/${name}.fish";
value = {
source =
let
body = if isAttrs def then def.body else def;
in
fishIndent "${name}.fish" ''
${lib.strings.removeSuffix "\n" body}
'';
};
}) cfg.completions;
}
# Each plugin gets a corresponding conf.d/plugin-NAME.fish file to load
# in the paths and any initialization scripts.
(mkIf (lib.length cfg.plugins > 0) {
xdg.configFile = lib.mkMerge (
map (plugin: {
"fish/conf.d/plugin-${plugin.name}.fish".source = fishIndent "${plugin.name}.fish" ''
# Plugin ${plugin.name}
set -l plugin_dir ${plugin.src}
# Set paths to import plugin components
if test -d $plugin_dir/functions
set fish_function_path $fish_function_path[1] $plugin_dir/functions $fish_function_path[2..-1]
end
if test -d $plugin_dir/completions
set fish_complete_path $fish_complete_path[1] $plugin_dir/completions $fish_complete_path[2..-1]
end
# Source initialization code if it exists.
if test -d $plugin_dir/conf.d
for f in $plugin_dir/conf.d/*.fish
source $f
end
end
if test -f $plugin_dir/key_bindings.fish
source $plugin_dir/key_bindings.fish
end
if test -f $plugin_dir/init.fish
source $plugin_dir/init.fish
end
'';
}) cfg.plugins
);
})
]
);
}