irongit

fix: screw it, custom version of autoswitch env

huncholanehuncholaneauthored
parent d79acefcommit c088b6a250bb112aab10998f62c6cddfcfbdd5eeBrowse files

4 files changed, +474 -3

+5-0.zshrc
@@ -1,5 +1,10 @@
11 [ -f "$ZDOTDIR/before_plugins.sh" ] && source "$ZDOTDIR/before_plugins.sh"
22
3+# Load custom plugins
4+for plugin in ~/.config/zsh/custom-plugins/*.zsh; do
5+ source "$plugin"
6+done
7+
38 # Set up antidote
49 source "$ZDOTDIR/.antidote/antidote.zsh"
510 antidote load ${ZDOTDIR:-$HOME}/zsh_plugins.txt
+469-0custom-plugins/autoswitch_virtualenv.zsh
@@ -0,0 +1,469 @@
1+export AUTOSWITCH_VERSION="3.9.0"
2+export AUTOSWITCH_FILE=".venv"
3+export AUTOSWITCH_SILENT=true
4+
5+AUTOSWITCH_RED="\e[31m"
6+AUTOSWITCH_GREEN="\e[32m"
7+AUTOSWITCH_PURPLE="\e[35m"
8+AUTOSWITCH_BOLD="\e[1m"
9+AUTOSWITCH_NORMAL="\e[0m"
10+
11+VIRTUAL_ENV_DIR="${AUTOSWITCH_VIRTUAL_ENV_DIR:-$HOME/.virtualenvs}"
12+
13+function _validated_source() {
14+ local target_path="$1"
15+
16+ if [[ "$target_path" == *'..'* ]]; then
17+ (>&2 printf "AUTOSWITCH WARNING: ")
18+ (>&2 printf "target virtualenv contains invalid characters\n")
19+ (>&2 printf "virtualenv activation cancelled\n")
20+ return
21+ else
22+ source "$target_path"
23+ fi
24+}
25+
26+
27+function _virtual_env_dir() {
28+ local venv_name="$1"
29+ printf "%s/%s" "$VIRTUAL_ENV_DIR" "$venv_name"
30+}
31+
32+
33+function _python_version() {
34+ local PYTHON_BIN="$1"
35+ if [[ -f "$PYTHON_BIN" ]] then
36+ # For some reason python --version writes to stderr
37+ printf "%s" "$($PYTHON_BIN --version 2>&1)"
38+ else
39+ printf "unknown"
40+ fi
41+}
42+
43+
44+function _autoswitch_message() {
45+ if [ -z "$AUTOSWITCH_SILENT" ]; then
46+ (>&2 printf "$@")
47+ fi
48+}
49+
50+
51+function _is_valid_virtualenv() {
52+ [[ -d "$1" ]] && [[ -f "$1/bin/activate" ]]
53+}
54+
55+
56+function _get_venv_type() {
57+ local venv_dir="$1"
58+ local venv_type="${2:-virtualenv}"
59+ if [[ -f "$venv_dir/Pipfile" ]]; then
60+ venv_type="pipenv"
61+ elif [[ -f "$venv_dir/poetry.lock" ]]; then
62+ venv_type="poetry"
63+ elif [[ -f "$venv_dir/uv.lock" ]]; then
64+ venv_type="uv"
65+ elif [[ -f "$venv_dir/requirements.txt" || -f "$venv_dir/setup.py" ]]; then
66+ venv_type="virtualenv"
67+ fi
68+ printf "%s" "$venv_type"
69+}
70+
71+
72+function _get_venv_name() {
73+ local venv_dir="$1"
74+ local venv_type="$2"
75+ local venv_name="$(basename "$venv_dir")"
76+
77+ # clear pipenv from the extra identifiers at the end
78+ if [[ "$venv_type" == "pipenv" ]]; then
79+ venv_name="${venv_name%-*}"
80+ fi
81+
82+ printf "%s" "$venv_name"
83+}
84+
85+
86+function _maybeworkon() {
87+ local venv_dir="$1"
88+ local venv_type="$2"
89+ local venv_name="$(_get_venv_name $venv_dir $venv_type)"
90+
91+ local DEFAULT_MESSAGE_FORMAT="Switching to ${AUTOSWITCH_BOLD}%venv_type${AUTOSWITCH_NORMAL} project: ${AUTOSWITCH_BOLD}${AUTOSWITCH_PURPLE}%venv_name${AUTOSWITCH_NORMAL} ${AUTOSWITCH_GREEN}[🐍%py_version]${AUTOSWITCH_NORMAL}"
92+ if [[ "$LANG" != *".UTF-8" ]]; then
93+ # Remove multibyte characters if the terminal does not support utf-8
94+ DEFAULT_MESSAGE_FORMAT="${DEFAULT_MESSAGE_FORMAT/🐍/}"
95+ fi
96+
97+ # Don't reactivate an already activated virtual environment
98+ if [[ -z "$VIRTUAL_ENV" || "$venv_dir" != "$VIRTUAL_ENV" ]]; then
99+
100+ if [[ ! -d "$venv_dir" ]]; then
101+ printf "Unable to find ${AUTOSWITCH_PURPLE}$venv_name${AUTOSWITCH_NORMAL} virtualenv\n"
102+ printf "If the issue persists run ${AUTOSWITCH_PURPLE}rmvenv && mkvenv${AUTOSWITCH_NORMAL} in this directory\n"
103+ return
104+ fi
105+
106+ local py_version="$(_python_version "$venv_dir/bin/python")"
107+ local message="${AUTOSWITCH_MESSAGE_FORMAT:-"$DEFAULT_MESSAGE_FORMAT"}"
108+ message="${message//\%venv_type/$venv_type}"
109+ message="${message//\%venv_name/$venv_name}"
110+ message="${message//\%py_version/$py_version}"
111+ _autoswitch_message "${message}\n"
112+
113+ # If we are using pipenv and activate its virtual environment - turn down its verbosity
114+ # to prevent users seeing " Pipenv found itself running within a virtual environment" warning
115+ if [[ "$venv_type" == "pipenv" && "$PIPENV_VERBOSITY" != -1 ]]; then
116+ export PIPENV_VERBOSITY=-1
117+ fi
118+
119+ # Much faster to source the activate file directly rather than use the `workon` command
120+ local activate_script="$venv_dir/bin/activate"
121+
122+ _validated_source "$activate_script"
123+ fi
124+}
125+
126+
127+# Gives the path to the nearest target file
128+function _check_path()
129+{
130+ local check_dir="$1"
131+
132+ if [[ -f "${check_dir}/${AUTOSWITCH_FILE}" ]]; then
133+ printf "${check_dir}/${AUTOSWITCH_FILE}"
134+ return
135+ elif _is_valid_virtualenv "${check_dir}/${AUTOSWITCH_FILE}"; then
136+ printf "${check_dir}/${AUTOSWITCH_FILE}"
137+ return
138+ elif [[ -f "${check_dir}/poetry.lock" ]]; then
139+ printf "${check_dir}/poetry.lock"
140+ elif [[ -f "${check_dir}/Pipfile" ]]; then
141+ printf "${check_dir}/Pipfile"
142+ elif [[ -f "${check_dir}/uv.lock" ]]; then
143+ printf "${check_dir}/uv.lock"
144+ else
145+ # Abort search at file system root or HOME directory (latter is a performance optimisation).
146+ if [[ "$check_dir" = "/" || "$check_dir" = "$HOME" ]]; then
147+ return
148+ fi
149+ _check_path "$(dirname "$check_dir")"
150+ fi
151+}
152+
153+
154+function _activate_poetry() {
155+ # check if any environments exist before trying to activate
156+ # if env list is empty, then no environment exists that can be activated
157+ local name="$(poetry env list --full-path | sort -k 2 | tail -n 1 | cut -d' ' -f1)"
158+ if [[ -n "$name" ]]; then
159+ _maybeworkon "$name" "poetry"
160+ return 0
161+ fi
162+ return 1
163+}
164+
165+
166+function _activate_pipenv() {
167+ # unfortunately running pipenv each time we are in a pipenv project directory is slow :(
168+ if venv_path="$(PIPENV_IGNORE_VIRTUALENVS=1 pipenv --venv 2>/dev/null)"; then
169+ _maybeworkon "$venv_path" "pipenv"
170+ return 0
171+ fi
172+ return 1
173+}
174+
175+function _activate_uv() {
176+ if [[ -d ".venv" ]]; then
177+ _maybeworkon ".venv" "uv"
178+ return 0
179+ fi
180+ return 1
181+}
182+
183+
184+# Automatically switch virtualenv when $AUTOSWITCH_FILE file detected
185+function check_venv()
186+{
187+ local file_owner
188+ local file_permissions
189+
190+ # Get the $AUTOSWITCH_FILE, scanning parent directories
191+ local venv_path="$(_check_path "$PWD")"
192+
193+ if [[ -n "$venv_path" ]]; then
194+
195+ /usr/bin/stat --version &> /dev/null
196+ if [[ $? -eq 0 ]]; then # Linux, or GNU stat
197+ file_owner="$(/usr/bin/stat -c %u "$venv_path")"
198+ file_permissions="$(/usr/bin/stat -c %a "$venv_path")"
199+ else # macOS, or FreeBSD stat
200+ file_owner="$(/usr/bin/stat -f %u "$venv_path")"
201+ file_permissions="$(/usr/bin/stat -f %OLp "$venv_path")"
202+ fi
203+
204+ if [[ -f "$venv_path" ]] && [[ "$file_owner" != "$(id -u)" ]]; then
205+ printf "AUTOSWITCH WARNING: Virtualenv will not be activated\n\n"
206+ printf "Reason: Found a $AUTOSWITCH_FILE file but it is not owned by the current user\n"
207+ printf "Change ownership of ${AUTOSWITCH_PURPLE}$venv_path${AUTOSWITCH_NORMAL} to ${PURPLE}'$USER'${NORMAL} to fix this\n"
208+ elif [[ -f "$venv_path" ]] && ! [[ "$file_permissions" =~ ^[64][04][04]$ ]]; then
209+ printf "AUTOSWITCH WARNING: Virtualenv will not be activated\n\n"
210+ printf "Reason: Found a $AUTOSWITCH_FILE file with weak permission settings ($file_permissions).\n"
211+ printf "Run the following command to fix this: ${AUTOSWITCH_PURPLE}\"chmod 600 $venv_path\"${AUTOSWITCH_NORMAL}\n"
212+ else
213+ if [[ "$venv_path" == *"/Pipfile" ]]; then
214+ if type "pipenv" > /dev/null && _activate_pipenv; then
215+ return
216+ fi
217+ elif [[ "$venv_path" == *"/poetry.lock" ]]; then
218+ if type "poetry" > /dev/null && _activate_poetry; then
219+ return
220+ fi
221+ elif [[ "$venv_path" == *"/uv.lock" ]]; then
222+ local uv_venv_path="$(dirname "$venv_path")/.venv"
223+ _maybeworkon "$uv_venv_path" "uv"
224+ return
225+ # standard use case: $venv_path is a file containing a virtualenv name
226+ elif [[ -f "$venv_path" ]]; then
227+ local switch_to="$(<"$venv_path")"
228+ _maybeworkon "$(_virtual_env_dir "$switch_to")" "virtualenv"
229+ return
230+ elif _is_valid_virtualenv "$venv_path"; then
231+ _maybeworkon "$venv_path" "virtualenv"
232+ return
233+ fi
234+ fi
235+ fi
236+
237+ local venv_type="$(_get_venv_type "$PWD" "unknown")"
238+
239+ # If we still haven't got anywhere, fallback to defaults
240+ if [[ "$venv_type" != "unknown" ]]; then
241+ printf "Python ${AUTOSWITCH_PURPLE}$venv_type${AUTOSWITCH_NORMAL} project detected. "
242+ printf "Run ${AUTOSWITCH_PURPLE}mkvenv${AUTOSWITCH_NORMAL} to setup autoswitching\n"
243+ fi
244+ _default_venv
245+}
246+
247+
248+# Switch to the default virtual environment
249+function _default_venv()
250+{
251+ local venv_type="$(_get_venv_type "$OLDPWD")"
252+ if [[ -n "$AUTOSWITCH_DEFAULTENV" ]]; then
253+ _maybeworkon "$(_virtual_env_dir "$AUTOSWITCH_DEFAULTENV")" "$venv_type"
254+ elif [[ -n "$VIRTUAL_ENV" ]]; then
255+ local venv_name="$(_get_venv_name "$VIRTUAL_ENV" "$venv_type")"
256+ _autoswitch_message "Deactivating: ${AUTOSWITCH_BOLD}${AUTOSWITCH_PURPLE}%s${AUTOSWITCH_NORMAL}\n" "$venv_name"
257+ deactivate
258+ fi
259+}
260+
261+
262+# remove project environment for current directory
263+function rmvenv()
264+{
265+ local venv_type="$(_get_venv_type "$PWD" "unknown")"
266+
267+ if [[ "$venv_type" == "pipenv" ]]; then
268+ deactivate
269+ pipenv --rm
270+ elif [[ "$venv_type" == "poetry" ]]; then
271+ deactivate
272+ poetry env remove "$(poetry run which python)"
273+ elif [[ "$venv_type" == "uv" ]]; then
274+ deactivate
275+ rm -rf ".venv"
276+ else
277+ if [[ -f "$AUTOSWITCH_FILE" ]]; then
278+ local venv_name="$(<$AUTOSWITCH_FILE)"
279+
280+ # detect if we need to switch virtualenv first
281+ if [[ -n "$VIRTUAL_ENV" ]]; then
282+ local current_venv="$(basename $VIRTUAL_ENV)"
283+ if [[ "$current_venv" = "$venv_name" ]]; then
284+ _default_venv
285+ fi
286+ fi
287+
288+ printf "Removing ${AUTOSWITCH_PURPLE}%s${AUTOSWITCH_NORMAL}...\n" "$venv_name"
289+ # Using explicit paths to avoid any alias/function interference.
290+ # rm should always be found in this location according to
291+ # https://refspecs.linuxfoundation.org/FHS_3.0/fhs/ch03s04.html
292+ # https://www.freedesktop.org/wiki/Software/systemd/TheCaseForTheUsrMerge/
293+ /bin/rm -rf "$(_virtual_env_dir "$venv_name")"
294+ if [[ -z "$(/bin/ls -A $VIRTUAL_ENV_DIR)" ]]; then
295+ /bin/rm -rf "$VIRTUAL_ENV_DIR"
296+ fi
297+ /bin/rm "$AUTOSWITCH_FILE"
298+ else
299+ printf "No $AUTOSWITCH_FILE file in the current directory!\n"
300+ fi
301+ fi
302+}
303+
304+
305+function _missing_error_message() {
306+ local command="$1"
307+ printf "${AUTOSWITCH_BOLD}${AUTOSWITCH_RED}"
308+ printf "zsh-autoswitch-virtualenv requires '%s' to install this project!\n\n" "$command"
309+ printf "${AUTOSWITCH_NORMAL}"
310+ printf "If this is already installed but you are still seeing this message, \n"
311+ printf "then make sure the ${AUTOSWITCH_BOLD}$command${AUTOSWITCH_NORMAL} command is in your PATH.\n" $command
312+ printf "\n"
313+}
314+
315+function randstr()
316+{
317+ ${AUTOSWITCH_DEFAULT_PYTHON:-python3} -c "from __future__ import print_function; import string, random; print(''.join(random.choice(string.ascii_lowercase) for _ in range(4)))"
318+}
319+
320+
321+# helper function to create a project environment for the current directory
322+function mkvenv()
323+{
324+ local venv_type="$(_get_venv_type "$PWD" "unknown")"
325+ # Copy parameters variable so that we can mutate it
326+ # NOTE: Keep declaration of variable and assignment separate for zsh 5.0 compatibility
327+ local params
328+ params=("${@[@]}")
329+
330+ if [[ "$venv_type" == "pipenv" ]]; then
331+ if ! type "pipenv" > /dev/null; then
332+ _missing_error_message pipenv
333+ return
334+ fi
335+ # TODO: detect if this is already installed
336+ if [[ "$AUTOSWITCH_PIPINSTALL" = "FULL" ]]
337+ then
338+ pipenv install --dev $params
339+ else
340+ pipenv install --dev --editable . $params
341+ fi
342+ _activate_pipenv
343+ return
344+ elif [[ "$venv_type" == "poetry" ]]; then
345+ if ! type "poetry" > /dev/null; then
346+ _missing_error_message poetry
347+ return
348+ fi
349+ # TODO: detect if this is already installed
350+ poetry install $params
351+ _activate_poetry
352+ return
353+ elif [[ "$venv_type" == "uv" ]]; then
354+ if ! type "uv" > /dev/null; then
355+ _missing_error_message uv
356+ return
357+ fi
358+ uv sync $params
359+ _activate_uv
360+ return
361+ else
362+ if ! type "virtualenv" > /dev/null; then
363+ _missing_error_message virtualenv
364+ return
365+ fi
366+
367+ if [[ -f "$AUTOSWITCH_FILE" ]]; then
368+ printf "$AUTOSWITCH_FILE file already exists. If this is a mistake use the rmvenv command\n"
369+ else
370+ local venv_name="$(basename $PWD)-$(randstr)"
371+
372+ printf "Creating ${AUTOSWITCH_PURPLE}%s${NONE} virtualenv\n" "$venv_name"
373+
374+
375+ if [[ -n "$AUTOSWITCH_DEFAULT_PYTHON" && ${params[(I)--python*]} -eq 0 ]]; then
376+ printf "${AUTOSWITCH_PURPLE}"
377+ printf 'Using $AUTOSWITCH_DEFAULT_PYTHON='
378+ printf "$AUTOSWITCH_DEFAULT_PYTHON"
379+ printf "${NONE}\n"
380+ params+="--python=$AUTOSWITCH_DEFAULT_PYTHON"
381+ fi
382+
383+ /bin/mkdir -p "$VIRTUAL_ENV_DIR"
384+
385+ if [[ ${params[(I)--verbose]} -eq 0 ]]; then
386+ virtualenv $params "$(_virtual_env_dir "$venv_name")"
387+ else
388+ virtualenv $params "$(_virtual_env_dir "$venv_name")" > /dev/null
389+ fi
390+
391+ printf "$venv_name\n" > "$AUTOSWITCH_FILE"
392+ chmod 600 "$AUTOSWITCH_FILE"
393+
394+ _maybeworkon "$(_virtual_env_dir "$venv_name")" "virtualenv"
395+
396+ install_requirements
397+ fi
398+ fi
399+}
400+
401+
402+function install_requirements() {
403+ if [[ -f "$AUTOSWITCH_DEFAULT_REQUIREMENTS" ]]; then
404+ printf "Install default requirements? (${AUTOSWITCH_PURPLE}$AUTOSWITCH_DEFAULT_REQUIREMENTS${AUTOSWITCH_NORMAL}) [y/N]: "
405+ read ans
406+
407+ if [[ "$ans" = "y" || "$ans" == "Y" ]]; then
408+ pip install -r "$AUTOSWITCH_DEFAULT_REQUIREMENTS"
409+ fi
410+ fi
411+
412+ if [[ -f "$PWD/setup.py" ]]; then
413+ printf "Found a ${AUTOSWITCH_PURPLE}setup.py${AUTOSWITCH_NORMAL} file. Install dependencies? [y/N]: "
414+ read ans
415+
416+ if [[ "$ans" = "y" || "$ans" = "Y" ]]; then
417+ if [[ "$AUTOSWITCH_PIPINSTALL" = "FULL" ]]
418+ then
419+ pip install .
420+ else
421+ pip install -e .
422+ fi
423+ fi
424+ fi
425+
426+ setopt localoptions
427+ setopt nullglob
428+ local requirements
429+ for requirements in **/*requirements.txt
430+ do
431+ printf "Found a ${AUTOSWITCH_PURPLE}%s${AUTOSWITCH_NORMAL} file. Install? [y/N]: " "$requirements"
432+ read ans
433+
434+ if [[ "$ans" = "y" || "$ans" = "Y" ]]; then
435+ pip install -r "$requirements"
436+ fi
437+ done
438+}
439+
440+
441+function enable_autoswitch_virtualenv() {
442+ disable_autoswitch_virtualenv
443+ add-zsh-hook chpwd check_venv
444+}
445+
446+
447+function disable_autoswitch_virtualenv() {
448+ add-zsh-hook -D chpwd check_venv
449+}
450+
451+# This function is only used to startup zsh-autoswitch-virtualenv
452+# the first time a terminal is started up
453+# it waits for the terminal to be ready using precmd and then
454+# immediately removes itself from the zsh-hook.
455+# This seems important for "instant prompt" zsh themes like powerlevel10k
456+function _autoswitch_startup() {
457+ local python_bin="${AUTOSWITCH_DEFAULT_PYTHON:-python3}"
458+ if ! type "${python_bin}" > /dev/null; then
459+ printf "WARNING: python binary '${python_bin}' not found on PATH.\n"
460+ printf "zsh-autoswitch-virtualenv plugin will be disabled.\n"
461+ else
462+ enable_autoswitch_virtualenv
463+ check_venv
464+ fi
465+ add-zsh-hook -D precmd _autoswitch_startup
466+}
467+
468+autoload -Uz add-zsh-hook
469+add-zsh-hook precmd _autoswitch_startup
+0-1zsh_plugins.txt
@@ -6,5 +6,4 @@ https://github.com/hlissner/zsh-autopair
66 https://github.com/zsh-users/zsh-history-substring-search
77 https://github.com/ajeetdsouza/zoxide
88 https://github.com/romkatv/powerlevel10k
9-https://github.com/MichaelAquilina/zsh-autoswitch-virtualenv
109 https://github.com/wfxr/forgit
+0-2zsh_plugins.zsh
@@ -16,7 +16,5 @@ source "$HOME/.cache/antidote/https-COLON--SLASH--SLASH-github.com-SLASH-ajeetds
1616 fpath+=( "$HOME/.cache/antidote/https-COLON--SLASH--SLASH-github.com-SLASH-romkatv-SLASH-powerlevel10k" )
1717 source "$HOME/.cache/antidote/https-COLON--SLASH--SLASH-github.com-SLASH-romkatv-SLASH-powerlevel10k/powerlevel10k.zsh-theme"
1818 source "$HOME/.cache/antidote/https-COLON--SLASH--SLASH-github.com-SLASH-romkatv-SLASH-powerlevel10k/powerlevel9k.zsh-theme"
19-fpath+=( "$HOME/.cache/antidote/https-COLON--SLASH--SLASH-github.com-SLASH-MichaelAquilina-SLASH-zsh-autoswitch-virtualenv" )
20-source "$HOME/.cache/antidote/https-COLON--SLASH--SLASH-github.com-SLASH-MichaelAquilina-SLASH-zsh-autoswitch-virtualenv/autoswitch_virtualenv.plugin.zsh"
2119 fpath+=( "$HOME/.cache/antidote/https-COLON--SLASH--SLASH-github.com-SLASH-wfxr-SLASH-forgit" )
2220 source "$HOME/.cache/antidote/https-COLON--SLASH--SLASH-github.com-SLASH-wfxr-SLASH-forgit/forgit.plugin.zsh"