diff --git a/plugins/pj/README.md b/plugins/pj/README.md index 27e5638ec..4161a631b 100644 --- a/plugins/pj/README.md +++ b/plugins/pj/README.md @@ -1,11 +1,6 @@ -# pj +# pj plugin -The `pj` plugin (short for `Project Jump`) allows you to define several -folders where you store your projects, so that you can jump there directly -by just using the name of the project directory. - -Original idea and code by Jan De Poorter ([@DefV](https://github.com/DefV)) -Source: https://gist.github.com/pjaspers/368394#gistcomment-1016 +The `pj` plugin (short for `Project Jump`) allows you to define a list of directories where your projects are located. You can quickly jump to a project directory using `pj project-name` or open it in your preferred editor with `pj open project-name`. ## Usage @@ -15,31 +10,108 @@ Source: https://gist.github.com/pjaspers/368394#gistcomment-1016 plugins=(... pj) ``` -2. Set `$PROJECT_PATHS` in your ~/.zshrc: +2. Add project to the registry: ```zsh - PROJECT_PATHS=(~/src ~/work ~/"dir with spaces") + $ pj add ``` -You can now use one of the following commands: +> This will add the current directory to the registry, using the directory name as the project name. -##### `pj my-project`: +3. Jump to project directory: -`cd` to the directory named "my-project" found in one of the `$PROJECT_PATHS` -directories. If there are several directories named the same, the first one -to appear in `$PROJECT_PATHS` has preference. + ```zsh + $ pj project-name + ``` +> `pj` has auto-complete support for project names. + +4. Open the project in your defined `$EDITOR`: + + ```zsh + $ pjo project-name + ``` +> Opens the project in your default $EDITOR. You can override the editor using `-e`. + +## Commands + +#### `pj ` + +`cd` to the project directory with the given name. Note: you can use auto-complete for project names. For example: ```zsh -PROJECT_PATHS=(~/code ~/work) -$ ls ~/code # ~/code/blog ~/code/react -$ ls ~/work # ~/work/blog ~/work/project -$ pj blog # <-- will cd to ~/code/blog +$ pj my-project ``` -##### `pjo my-project` +#### `pj add [path] [name]` -Open the project directory with your defined `$EDITOR`. This follows the same -directory rules as the `pj` command above. +Add a project to the registry. +Note: `pja` is an alias of `pj add`. +For example: +```zsh +$ pja +$ # Add the current directory with the name "my-project" +$ pja . my-project +$ # Add the specified directory to the registry with the name "my-project" +$ pja /path/to/project my-project +``` + +##### `pj open ` + +Open the project with your defined `$EDITOR` or specify an editor with the `-e` flag. Note: `pjo` is an alias of `pj open`. + +For example: +```zsh +$ pjo my-project +$ # open the project path named "my-project" with VSCode +$ pjo -e code my-project +$ # open multiple projects +$ pjo my-project another-project +``` + +##### `pj ls [pattern]` + +List all the projects in the registry. +Note: `pjl` is an alias of `pj ls`. + +For example: +```zsh +$ pj ls +$ # list all the projects in the registry that match the pattern 'web-*' +$ pjl 'web-*' +``` + +##### `pj rm ` + +Remove a project from the registry. + +For example: +```zsh +$ pj rm my-project +$ # remove multiple projects from the registry +$ pj rm my-project another-project +``` + +#### `pj mv ` + +Rename a project in the registry. + +For example: +```zsh +$ pj mv old-name new-name +``` + +## Aliases +| Alias | Command | +|-------|---------| +| `pja` | `pj add` | +| `pjo` | `pj open` | +| `pjl` | `pj ls` | + +## Contributors +Code by [@ibrahimcetin](https://github.com/ibrahimcetin) + +Original idea and code by Jan De Poorter ([@DefV](https://github.com/DefV)) +Source: https://gist.github.com/pjaspers/368394#gistcomment-1016 \ No newline at end of file diff --git a/plugins/pj/pj.plugin.zsh b/plugins/pj/pj.plugin.zsh old mode 100644 new mode 100755 index 431576f4b..afb233d7c --- a/plugins/pj/pj.plugin.zsh +++ b/plugins/pj/pj.plugin.zsh @@ -1,34 +1,361 @@ -alias pjo="pj open" +PJ_DB=~/.pj_projects # Project database file +touch "$PJ_DB" -function pj() { - local cmd="cd" - local project="$1" +_pj_help() { + cat <&2 + echo "Usage: pj [PROJECT_NAME]" >&2 + return 1 fi - for basedir ($PROJECT_PATHS); do - if [[ -d "$basedir/$project" ]]; then - $cmd "$basedir/$project" - return + # Validate input + if [[ -z "$project_name" ]]; then + echo "Error: Missing project name" >&2 + echo "Usage: pj [PROJECT_NAME]" >&2 + return 1 + fi + + # Find project in database + if [[ -f "$PJ_DB" ]]; then + target_path=$(awk -F: -v project="$project_name" ' + $1 == project {print $2; exit} + ' "$PJ_DB") + fi + + # Handle found project + if [[ -n "$target_path" ]]; then + if [[ -d "$target_path" ]]; then + cd "$target_path" || { + echo "Error: Failed to access $target_path" >&2 + return 1 + } + return 0 + else + echo "Error: Path not exists for project '$project_name'" >&2 + echo "Path: $target_path" >&2 + return 1 + fi + fi + + # Project not found + echo "Error: Project not found - $project_name" >&2 + return 1 +} + +_pj_add() { + local path_input=${1:-.} + local name=${2:-} + local resolved_path="${path_input:a}" + local default_name="${resolved_path:t}" + + # Check for extra arguments + if [[ $# -gt 2 ]]; then + echo "Error: Too many arguments" >&2 + echo "Usage: pj add [PATH] [NAME]" >&2 + return 1 + fi + + # Check if the name is a command name + case "$name" in + add|rm|ls|open|mv|help) + echo "Error: Invalid project name '$name'. It conflicts with a command name." >&2 + return 1 + ;; + esac + + # Validate directory exists + if [[ ! -d "$resolved_path" ]]; then + echo "Error: Invalid directory path '$path_input'" >&2 + echo "Resolved to: $resolved_path" >&2 + return 1 + fi + + # Set default name if not provided + if [[ -z "$name" ]]; then + name="$default_name" + fi + + # Validate name format + if [[ "$name" =~ [^a-zA-Z0-9_-] ]]; then + echo "Error: Invalid name '$name'" >&2 + echo "Allowed characters: A-Z, 0-9, -, _" >&2 + return 1 + fi + + # Check for existing project name + if [[ -f "$PJ_DB" ]] && grep -q "^${name}:" "$PJ_DB"; then + local existing_path=$(awk -F: -v n="$name" '$1 == n {print $2}' "$PJ_DB") + echo "Error: Project name '$name' already exists" >&2 + echo "Existing path: $existing_path" >&2 + return 1 + fi + + # Add new entry + echo "${name}:${resolved_path}" >> "$PJ_DB" + echo "Added project: ${name} -> ${resolved_path}" +} + +_pj_ls() { + local pattern="${1:-*}" # Default to all projects + local -a projects=() + local name path + + # Read database file + while IFS=: read -r name path; do + # Case-insensitive glob matching + if [[ "${name:l}" == *${~pattern:l}* || \ + "${path:l}" == *${~pattern:l}* ]]; then + projects+=("${name} -> ${path}") + fi + done < "$PJ_DB" 2>/dev/null + + # Process projects using Zsh built-ins + if (( ${#projects} > 0 )); then + # Remove duplicates and sort + projects=(${(u)projects}) # Remove duplicates + projects=(${(o)projects}) # Sort alphabetically + + # Print results + print -l "${projects[@]}" + fi +} + +_pj_open() { + local editor="$EDITOR" + local project_names=() + local target_path + local errors=0 + + # Parse options + while [[ "$1" =~ ^- ]]; do + case "$1" in + -e|--editor) + shift + editor="$1" + ;; + *) + echo "Error: Invalid option $1" >&2 + echo "Usage: pj open [-e editor] (PROJECT_NAME ...)" >&2 + return 1 + ;; + esac + shift + done + + # Collect project names + project_names=("$@") + + # Validate input + if [[ ${#project_names[@]} -eq 0 ]]; then + echo "Error: Missing project name(s)" >&2 + echo "Usage: pj open [-e editor] [PROJECT_NAME ...]" >&2 + return 1 + fi + + # Validate editor + if [[ -z "$editor" ]]; then + echo "Error: No editor specified" >&2 + echo "Set \$EDITOR or use -e option" >&2 + return 1 + fi + + for project_name in "${project_names[@]}"; do + # Find project in database + target_path=$(awk -F: -v project="$project_name" '$1 == project {print $2; exit}' "$PJ_DB") + + # Handle project path + if [[ -n "$target_path" ]]; then + if [[ -d "$target_path" ]]; then + ${=editor} "$target_path" + else + echo "Error: Invalid path for project '$project_name'" >&2 + echo "Path: $target_path" >&2 + errors=$((errors + 1)) + fi + else + echo "Error: Project not found - $project_name" >&2 + errors=$((errors + 1)) fi done - echo "No such project '${project}'." + return $errors } -_pj () { - local -a projects - for basedir ($PROJECT_PATHS); do - projects+=(${basedir}/*(/N)) +_pj_rm() { + local project_names=("$@") + local errors=0 + + # Validate input + if [[ ${#project_names[@]} -eq 0 ]]; then + echo "Error: Missing project name(s)" >&2 + echo "Usage: pj rm [PROJECT_NAME ...]" >&2 + return 1 + fi + + for project_name in "${project_names[@]}"; do + # Verify project exists + if ! grep -q "^${project_name}:" "$PJ_DB"; then + echo "Error: Project not found - $project_name" >&2 + errors=$((errors + 1)) + continue + fi + + # Remove entry from database + local project_path=$(awk -F: -v project="$project_name" '$1 == project {print $2; exit}' "$PJ_DB") + sed -i.bak "/^${project_name}:/d" "$PJ_DB" && rm -f "$PJ_DB.bak" + echo "Removed project: $project_name -> $project_path" done - compadd ${projects:t} + return $errors } +_pj_mv() { + local project_name="$1" + local new_name="$2" + + # Validate input + if [[ -z "$project_name" || -z "$new_name" ]]; then + echo "Error: Missing project name or new name" >&2 + echo "Usage: pj mv [PROJECT_NAME] [NEW_NAME]" >&2 + return 1 + fi + + # Verify project exists + if ! grep -q "^${project_name}:" "$PJ_DB"; then + echo "Error: Project not found - $project_name" >&2 + return 1 + fi + + # Verify new name is not taken + if grep -q "^${new_name}:" "$PJ_DB"; then + echo "Error: New project name already exists - $new_name" >&2 + return 1 + fi + + # Move project + sed -i.bak "s/^${project_name}:/${new_name}:/g" "$PJ_DB" && rm -f "$PJ_DB.bak" + echo "Moved project: $project_name -> $new_name" +} + +# Main function +function pj() { + if [ $# -eq 0 ]; then + _pj_help + return 1 + fi + + # Command dispatch + case "$1" in + "add") + shift # Remove 'add' from arguments + _pj_add "$@" + ;; + "rm") + shift + _pj_rm "$@" + ;; + "ls") + shift + _pj_ls "$@" + ;; + "open") + shift + _pj_open "$@" + ;; + "mv") + shift + _pj_mv "$@" + ;; + "help"|"--help"|"-h"|"") + _pj_help + return 1 + ;; + *) + _pj_jump "$@" + ;; + esac +} + +# Main completion entry point +_pj() { + local context state state_descr line + typeset -A opt_args + + # Parse command line state + _arguments -C \ + '1: :->command_or_project' \ + '*: :->args' + + case $state in + (command_or_project) + _pj_show_projects + ;; + (args) + _pj_handle_subcommand_args + ;; + esac +} + +# Helper: Handle arguments after subcommand +_pj_handle_subcommand_args() { + case $line[1] in + (add) + _files -/ + ;; + + (open|rm|ls|mv) + _pj_show_projects + ;; + esac +} + +# Helper: Show just project names +_pj_show_projects() { + local projects=(${(f)"$(cut -d: -f1 "$PJ_DB" 2>/dev/null)"}) + _describe 'projects' projects +} + +# Register completion compdef _pj pj + +# Editor aliases +alias pja="pj add" +alias pjo="pj open" +alias pjl="pj ls"