i’ve been using fish shell over zsh recently, and i haven’t wanted to go back for even a second. the only thing that kind of annoyed me was that the ecosystem isn’t as big, so some scripts and plugins that existed for zsh don’t have fish counterparts (or maybe i just didn’t look hard enough, this is a real possibility)

the main thing i ran into this issue with (and something i took for granted) was automatic activation of python environments when you enter a folder. i was worried it was going to be a gigantic pain to fix, but it was actually extremely simple.

to start, the function declaration looks roughly like this:

~/.config/fish/config.fish
function __auto_py_venv --on-variable PWD --description "auto enter python venvs"
	...
end

--on-variable PWD allows us to have the function run automatically whenever $PWD changes (so whenever we switch directories). pretty cool

now, we can test if an activation file exists in the current folder like this:

test -f "$dir/.venv/bin/activate.fish" # -f just means it's a file

and if it exists, we can source it:

~/.config/fish/config.fish
function __auto_py_venv --on-variable PWD --description "auto enter python venvs"
	if test -f ".venv/bin/activate.fish"
		source ".venv/bin/activate.fish"
	end
end

all done! it was that simple

the next issue i had was deactivating the venv when we leave it. this was as simple as setting a global $__VENV_DIR variable with set -g __VENV_DIR $PWD when successfully activating a venv, and then unsetting it and deactivating the venv if the current directory doesnt match $__VENV_DIR:

~/.config/fish/config.fish
function __auto_py_venv --on-variable PWD --description "auto activate python venv"
    if set -q __VENV_DIR
        and not string match -q "$__VENV_DIR*" $PWD
        deactivate
        set -e __VENV_DIR # -e for erase
    end
	if test -f ".venv/bin/activate.fish"
		source ".venv/bin/activate.fish"
		set -g __VENV_DIR $PWD
		break
	end
end

the final issue i had was cding too deep, especially since i use zoxide. this function can only match .venv/bin/activate.fish, so if i’m in a subdirectory it won’t work.

the solution for this is to cycle upwards and iterate through each directory up to the root.

we can do this by:

  1. declare $dir as the current directory
  2. check for .venv/bin/activate.fish. if it’s there, activate + end the loop
  3. if it’s not there, set $dir to the parent directory
  4. repeat, as long as $dir != /

in my function, this looked like so:

~/.config/fish/config.fish
function __auto_py_venv --on-variable PWD --description "auto activate python venv"
    if set -q __VENV_DIR
        and not string match -q "$__VENV_DIR*" $PWD
        deactivate
        set -e __VENV_DIR
    end
    set -l dir $PWD # -l for local (meaning function only)
    while test $dir != / # if it's the root we don't care
        if test -f "$dir/.venv/bin/activate.fish"
            source "$dir/.venv/bin/activate.fish"
            set -g __VENV_DIR $PWD
            break
        end
        set dir (dirname "$dir") # set $dir to the parent directory
    end
end