Skip to content

Language-agnostic project detection plugin for Zsh with customizable `on_project` and `off_project` hooks.

Notifications You must be signed in to change notification settings

aemonge/zsh-hookie-projects

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

6 Commits
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸͺ zsh-hookie-projects

Language-agnostic project detection plugin for Zsh with customizable on_project and off_project hooks.

Features

  • πŸ” Smart Detection: Automatically detects project directories by common markers (.git, pyproject.toml, package.json, etc.)
  • ⚑ Performance: Efficient implementation with minimal overhead (~1-5ms per directory change)
  • 🎯 Language Agnostic: Works with any project type - Python, Node.js, Rust, Go, Java, PHP, Ruby, etc.
  • πŸͺ Customizable Hooks: Define custom behavior when entering/leaving projects
  • πŸ“ Project Root Detection: Walks up directory tree to find project boundaries
  • 🚫 Path Filtering: Blacklist/whitelist support to avoid unwanted triggers
  • 🎨 PowerLevel10k Integration: Built-in hookie_dir segment with intelligent path shortening
  • πŸ“‚ Smart Directory Display: Project-aware path shortening with color coding
  • ⚑ Smart cd Command: Empty cd goes to project root instead of home
  • πŸ”‡ Message Control: Configurable notifications for project enter/leave events
  • 🌍 Comprehensive Coverage: Supports 100+ project markers across all major languages and tools

Installation

Using a Plugin Manager

Oh My Zsh:

git clone https://github.com/yourusername/zsh-hookie-projects ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-hookie-projects
# Add 'zsh-hookie-projects' to plugins array in ~/.zshrc

Zinit:

zinit load "yourusername/zsh-hookie-projects"

Manual:

git clone https://github.com/yourusername/zsh-hookie-projects ~/.zsh/zsh-hookie-projects
echo "source ~/.zsh/zsh-hookie-projects/zsh-hookie-projects.plugin.zsh" >> ~/.zshrc

Usage

The plugin works automatically once installed. Navigate between directories and watch the hooks trigger:

❯ cd ~/projects/zsh-hookie-projects
πŸš€ Entered project: zsh-hookie-projects (.git, README.md)

❯ cd functions
~/p/zsh-hookie-projects/functions  main ⇑ 1 !3 βšͺ2

❯ cd
πŸ“ Going to project root: zsh-hookie-projects
~/p/zsh-hookie-projects  main ⇑ 1 !3 βšͺ2

❯ cd ~
πŸ‘‹ Left project: zsh-hookie-projects
~

Environment Variables

The plugin automatically sets these environment variables when entering projects:

  • HOOKIE_CURRENT_PROJECT - Project name (e.g., my-project)
  • HOOKIE_PROJECT_ROOT - Full path to project root
  • HOOKIE_PROJECT_MARKERS_STRING - Comma-separated detected markers
❯ echo $HOOKIE_CURRENT_PROJECT
zsh-hookie-projects

❯ echo $HOOKIE_PROJECT_ROOT
/home/user/projects/zsh-hookie-projects

❯ echo $HOOKIE_PROJECT_MARKERS_STRING
.git, README.md, zsh-hookie-projects.plugin.zsh

PowerLevel10k Integration

Enhanced Directory Segment

Replace the default dir segment with hookie_dir for project-aware path display:

# In your ~/.p10k.zsh, replace 'dir' with 'hookie_dir'
typeset -g POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=(
    # ... other elements ...
    hookie_dir      # Use instead of 'dir'
    vcs
    # ... other elements ...
)

Features:

  • Intelligent Path Shortening: ~/projects/my-app β†’ ~/p/my-app
  • Color-Coded Paths: Project root in blue, subdirectories in cyan
  • Copy-Pasteable: Paths work when copied to clipboard
  • Fallback Support: Standard directory display when not in projects

Project Name Segment

Add a simple project indicator segment:

# Add to your prompt elements
typeset -g POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS+=(hookie_project)

# Simple project segment
function prompt_hookie_project() {
    [[ -n "$HOOKIE_CURRENT_PROJECT" ]] && p10k segment -f 6 -t "$HOOKIE_CURRENT_PROJECT"
}

With Dynamic Icons

function prompt_hookie_project() {
    if [[ -n "$HOOKIE_CURRENT_PROJECT" ]]; then
        local icon="●"
        case "$HOOKIE_PROJECT_MARKERS_STRING" in
            *"pyproject.toml"*) icon="🐍" ;;  # Python
            *"package.json"*)   icon="πŸ“¦" ;;  # Node.js
            *"Cargo.toml"*)     icon="πŸ¦€" ;;  # Rust
            *"go.mod"*)         icon="🐹" ;;  # Go
            *".git"*)           icon="πŸ“" ;;  # Git
        esac
        p10k segment -f 6 -i "$icon" -t "$HOOKIE_CURRENT_PROJECT"
    fi
}

Configure Colors

Add to the bottom of your ~/.p10k.zsh:

##########################[ hookie_dir: current project + directory ]##########################
# Hookie-dir segment colors (customize as needed)
typeset -g HOOKIE_DIR_PROJECT_COLOR=4      # Blue for project paths
typeset -g HOOKIE_DIR_SUBDIR_COLOR=6       # Cyan for subdirectories
typeset -g HOOKIE_DIR_DEFAULT_COLOR=4      # Blue for non-project directories

Smart cd Command

The plugin overrides the cd command to provide project-aware navigation:

❯ cd ~/projects/my-app
πŸš€ Entered project: my-app

❯ cd src/components
~/p/my-app/src/components

❯ cd              # Goes to project root instead of HOME!
πŸ“ Going to project root: my-app
~/p/my-app

❯ cd ~            # Explicit ~ still works
πŸ‘‹ Left project: my-app
~

Disable Smart cd

# Disable smart cd behavior
export HOOKIE_DISABLE_SMART_CD=1

Message Control

Configure which messages are displayed:

# Message control flags (set to 0 to disable, 1 to enable)
export HOOKIE_SHOW_ENTERING=1          # Show "πŸš€ Entered project"
export HOOKIE_SHOW_LEAVING=1           # Show "πŸ‘‹ Left project"
export HOOKIE_SHOW_CD_PROJECT_ROOT=1   # Show "πŸ“ Going to project root"

Silent Mode

# Completely silent operation
export HOOKIE_SHOW_ENTERING=0
export HOOKIE_SHOW_LEAVING=0
export HOOKIE_SHOW_CD_PROJECT_ROOT=0

Quick Toggle Functions

# Add these convenience functions to ~/.zshrc
hookie_messages_on() {
    export HOOKIE_SHOW_ENTERING=1
    export HOOKIE_SHOW_LEAVING=1
    export HOOKIE_SHOW_CD_PROJECT_ROOT=1
    echo "πŸ”” Hookie messages enabled"
}

hookie_messages_off() {
    export HOOKIE_SHOW_ENTERING=0
    export HOOKIE_SHOW_LEAVING=0
    export HOOKIE_SHOW_CD_PROJECT_ROOT=0
    echo "πŸ”‡ Hookie messages disabled"
}

Customization

Custom Project Markers

Add your own project markers in ~/.zshrc:

# Add custom markers
HOOKIE_PROJECT_MARKERS+=(
    'deno.json'        # Deno
    'requirements.txt' # Python legacy
    'yarn.lock'        # Yarn
    'flake.nix'        # Nix
    '.project-root'    # Custom marker
)

Path Filtering

Blacklist Mode (default) - Block specific directories:

# Add to ~/.zshrc to customize blacklist
HOOKIE_BLACKLIST_PATHS+=(
    "$HOME/Downloads"          # Downloads folder
    "$HOME/Desktop"            # Desktop
    "$HOME/.Trash"             # Trash
    "/mnt"                     # Mount points
    "$HOME/node_modules"       # npm modules
)

Whitelist Mode - Only allow specific directories:

# Enable whitelist mode by adding allowed paths
HOOKIE_WHITELIST_PATHS=(
    "$HOME/projects"           # Only allow ~/projects
    "$HOME/work"               # And ~/work
    "$HOME/dev"                # And ~/dev
    "/workspace"               # And /workspace
)

Custom Hooks

Override the default hook functions to add your own behavior:

# Custom on_project hook
on_project_hook() {
    local project_root="$1"
    shift
    local markers=("$@")

    echo "🎯 Working on: ${project_root:t}"

    # Auto-activate Python venv
    if (( ${markers[(Ie)pyproject.toml]} )); then
        [[ -d "$project_root/.venv" ]] && source "$project_root/.venv/bin/activate"
    fi

    # Load project environment
    [[ -f "$project_root/.env" ]] && source "$project_root/.env"

    # Set custom environment variables
    export PROJECT_NAME="${project_root:t}"
    export PROJECT_ROOT="$project_root"
}

# Custom off_project hook
off_project_hook() {
    local project_root="$1"

    echo "✨ Goodbye ${project_root:t}"

    # Cleanup
    [[ -n "$VIRTUAL_ENV" ]] && deactivate 2>/dev/null
    unset PROJECT_NAME PROJECT_ROOT
}

Auto-Activate Python Virtual Environment

Automatically activate Python virtual environments when entering Python projects:

HOOKIE_PROJECT() {

    # Silent mode - no messages
    export HOOKIE_SHOW_ENTERING=1
    export HOOKIE_SHOW_LEAVING=0
    export HOOKIE_SHOW_CD_PROJECT_ROOT=0

    zinit load ~/projects/zsh-hookie-projects

    on_project_hook() {
        local project_root="$1"
        shift
        local markers=("$@")

        # Set standard environment variables (silent)
        export HOOKIE_CURRENT_PROJECT="${project_root:t}"
        export HOOKIE_PROJECT_ROOT="$project_root"
        typeset -g HOOKIE_PROJECT_MARKERS_STRING="${(j:, :)markers}"
        export HOOKIE_PROJECT_MARKERS_STRING

        # Check if it's a Python project
        local python_markers=("pyproject.toml" "requirements.txt" "setup.py" ".venv" "venv" "env" "Pipfile")
        local is_python=0

        for marker in "${python_markers[@]}"; do
            if (( ${markers[(Ie)$marker]} )); then
                is_python=1
                break
            fi
        done

        if (( is_python )) && [[ -f "$project_root/.venv/bin/activate" ]]; then
            # Python project with .venv
            source .venv/bin/activate
            v $(pwd)
        else
            # Any other project
            v $(pwd)
        fi
    }

    off_project_hook() {
        local project_root="$1"

        # Silent cleanup
        unset HOOKIE_CURRENT_PROJECT
        unset HOOKIE_PROJECT_ROOT
        unset HOOKIE_PROJECT_MARKERS_STRING

        HOOKIE_CURRENT_PROJECT_DIR=""
        HOOKIE_CURRENT_PROJECT_MARKERS=()
    }
}
HOOKIE_PROJECT

Simple Python Virtual Environment Example

For a more minimal approach, just focusing on Python projects:

# Pre-hook: Always deactivate before entering any project
on_project__pre_hook() {
    local project_root="$1"
    # Deactivate any existing virtual environment before entering new project
    deactivate 2>/dev/null || true
}

on_project_hook() {
    local project_root="$1"
    shift
    local markers=("$@")

    # Standard setup
    export HOOKIE_CURRENT_PROJECT="${project_root:t}"
    export HOOKIE_PROJECT_ROOT="$project_root"
    typeset -g HOOKIE_PROJECT_MARKERS_STRING="${(j:, :)markers}"
    export HOOKIE_PROJECT_MARKERS_STRING

    echo "πŸš€ Entered project: ${project_root:t}"

    # Simple Python venv activation
    if [[ -f "$project_root/.venv/bin/activate" ]]; then
        echo "🐍 Activating .venv"
        source "$project_root/.venv/bin/activate"
    elif [[ -f "$project_root/venv/bin/activate" ]]; then
        echo "🐍 Activating venv"
        source "$project_root/venv/bin/activate"
    fi
}

# Pre-hook: Always deactivate before leaving any project
off_project__pre_hook() {
    deactivate 2>/dev/null || true
}

off_project_hook() {
    local project_root="$1"
    echo "πŸ‘‹ Left project: ${project_root:t}"

    # Deactivate any active virtual environment
    [[ -n "$VIRTUAL_ENV" ]] && deactivate 2>/dev/null

    # Cleanup
    unset HOOKIE_CURRENT_PROJECT HOOKIE_PROJECT_ROOT HOOKIE_PROJECT_MARKERS_STRING
}

Default Project Markers

The plugin detects projects by looking for these files and directories:

Version Control

  • .git, .gitignore, .gitmodules

Python

  • pyproject.toml, requirements.txt, setup.py, setup.cfg, Pipfile, poetry.lock, .venv, venv, env, .python-version, tox.ini, pytest.ini

JavaScript/Node.js

  • package.json, package-lock.json, yarn.lock, pnpm-lock.yaml, .nvmrc, tsconfig.json, webpack.config.js, vite.config.js, .eslintrc.js, .prettierrc, jest.config.js

Other Languages

  • Rust: Cargo.toml, Cargo.lock
  • Go: go.mod, go.sum, go.work
  • Java: pom.xml, build.gradle, build.gradle.kts
  • PHP: composer.json, composer.lock
  • Ruby: Gemfile, Gemfile.lock, Rakefile
  • Elixir: mix.exs, mix.lock
  • Gleam: gleam.toml
  • Deno: deno.json, deno.jsonc
  • Swift: Package.swift
  • Dart/Flutter: pubspec.yaml

Infrastructure & DevOps

  • Docker: Dockerfile, docker-compose.yml, .dockerignore
  • Infrastructure: terraform, Vagrantfile, ansible
  • CI/CD: .github, .gitlab-ci.yml, Jenkinsfile, .circleci

Configuration & Documentation

  • Config: .env, config.toml, config.yaml, .editorconfig
  • Docs: README.md, README.rst, LICENSE, CHANGELOG.md, docs
  • IDE: .vscode, .idea, .vim, .nvim

Build Tools & Databases

  • Build: Makefile, CMakeLists.txt, gulpfile.js, webpack.config.js
  • Database: schema.sql, migrations, alembic.ini
  • Package Managers: Brewfile, Podfile, flake.nix

And many more! See the full list in the plugin source.

Default Blacklisted Paths

These paths are blacklisted by default (exact matches only) to prevent false positives:

  • System directories: /, /usr, /opt, /var, /etc, /tmp
  • macOS system: /System, /Library
  • User config: ~/.config, ~/.cache, ~/.local
  • Development tools: ~/.oh-my-zsh, ~/.zinit, ~/.npm, ~/.cargo
  • User folders: ~/Desktop, ~/Downloads, ~/Documents

Performance

  • Efficient Detection: ~1-5ms overhead per directory change
  • Smart Caching: Only triggers hooks when project context changes
  • Minimal File I/O: Optimized file existence checks
  • No Background Processes: All operations are synchronous and fast
  • Path Shortening: Intelligent parent directory compression

Advanced Features

Plugin Structure

zsh-hookie-projects/
β”œβ”€β”€ zsh-hookie-projects.plugin.zsh    # Main plugin file
β”œβ”€β”€ functions/                        # Function directory
β”‚   β”œβ”€β”€ _hookie_find_project_root     # Project detection logic
β”‚   β”œβ”€β”€ _hookie_detect_markers        # Marker detection
β”‚   β”œβ”€β”€ _hookie_check_project_change  # State management
β”‚   β”œβ”€β”€ _hookie_is_path_allowed       # Path filtering
β”‚   β”œβ”€β”€ on_project_hook               # Enter project hook
β”‚   β”œβ”€β”€ off_project_hook              # Leave project hook
β”‚   β”œβ”€β”€ prompt_hookie_dir             # PowerLevel10k directory segment
β”‚   └── cd                            # Smart cd command
β”œβ”€β”€ README.md                         # Documentation
└── LICENSE                           # MIT License

Configuration Variables

# Path filtering
HOOKIE_BLACKLIST_PATHS=(...)      # Blocked directories
HOOKIE_WHITELIST_PATHS=(...)      # Allowed directories (optional)

# Project detection
HOOKIE_PROJECT_MARKERS=(...)      # File/directory markers

# Directory display colors
HOOKIE_DIR_PROJECT_COLOR=4        # Project path color
HOOKIE_DIR_SUBDIR_COLOR=6         # Subdirectory color
HOOKIE_DIR_DEFAULT_COLOR=4        # Non-project directory color

# Message control
HOOKIE_SHOW_ENTERING=1            # Show enter messages
HOOKIE_SHOW_LEAVING=1             # Show leave messages
HOOKIE_SHOW_CD_PROJECT_ROOT=1     # Show smart cd messages

# Behavior control
HOOKIE_DISABLE_SMART_CD=0         # Disable smart cd command

Troubleshooting

Plugin Not Loading

Make sure the plugin is properly loaded and functions are available:

# Check if functions are loaded
type prompt_hookie_dir
type _hookie_check_project_change

# Manually reload if needed
autoload -Uz prompt_hookie_dir

Startup Delay

If the directory segment shows empty on first terminal startup, simply press Enter once. This is a minor initialization timing issue with PowerLevel10k.

Export Type Errors

If you see inconsistent type for assignment errors in custom hooks:

# Use this pattern in custom hooks:
typeset -g HOOKIE_PROJECT_MARKERS_STRING="${(j:, :)markers}"
export HOOKIE_PROJECT_MARKERS_STRING

Debug Mode

Enable debug output to troubleshoot issues:

# Add to your hook functions temporarily
echo "DEBUG: Project root: $project_root"
echo "DEBUG: Markers: ${markers[@]}"
echo "DEBUG: PWD: $PWD"

Contributing

Contributions are welcome! Please feel free to:

  • Add support for new project markers
  • Improve performance optimizations
  • Fix bugs or edge cases
  • Enhance documentation
  • Add new PowerLevel10k segment features

License

MIT License - see LICENSE file.

Credits

Inspired by various zsh plugins and the need for a language-agnostic project detection system. Built for developers who work with multiple programming languages and want consistent project-aware shell behavior.

Special thanks to the PowerLevel10k project for providing the excellent prompt framework that makes the hookie_dir segment possible.

About

Language-agnostic project detection plugin for Zsh with customizable `on_project` and `off_project` hooks.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages