diff --git a/.commitlintrc b/.commitlintrc new file mode 100644 index 0000000..319b0c1 --- /dev/null +++ b/.commitlintrc @@ -0,0 +1,8 @@ +{ + "rules": { + "subject-case": [ + 2, + "never" + ] + } +} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d9680cc --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,53 @@ +name: Release +on: + push: + branches: [master] +permissions: + contents: write +jobs: + release: + name: Release + runs-on: ubuntu-latest + env: + GIT_AUTHOR_NAME: github-actions[bot] + GIT_AUTHOR_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 21 + - name: Release semantic version + run: npx semantic-release + - name: Release major version + run: | + VERSION="$(git tag --points-at ${{ github.sha }})" + + if [ -z $VERSION ]; then + echo "No major version to publish" >> $GITHUB_STEP_SUMMARY + else + MAJOR_VERSION="$(cut -d '.' -f 1 <<< "$VERSION")" + + git config user.name "${GIT_AUTHOR_NAME}" + git config user.email "${GIT_AUTHOR_EMAIL}" + git tag -fa ${MAJOR_VERSION} -m "Map ${MAJOR_VERSION} to ${VERSION}" + git push origin ${MAJOR_VERSION} --force + + echo "Successfully published semantic version \`$VERSION\` and major version \`$MAJOR_VERSION\`" >> $GITHUB_STEP_SUMMARY + fi + notify: + needs: [release] + name: Notify + if: always() + runs-on: ubuntu-latest + steps: + - uses: andrewscwei/telegram-action@v1 + with: + success: ${{ needs.release.result == 'success' }} + cancelled: ${{ needs.release.result == 'cancelled' }} + bot-token: ${{ secrets.TELEGRAM_DEVOPS_BOT_TOKEN }} + chat-id: ${{ secrets.TELEGRAM_DEVOPS_CHAT_ID }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..586f1a1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# OS generated files +Thumbs.db +ehthumbs.db +Desktop.ini +.DS_Store +.AppleDouble +.LSOverride +Icon\r +*~ + +# Keys +*.pem +*.crt +*.key + +# Temporary files +.tmp/ + +# Cache files +.cache/ + +# Secrets +*.secret + +# mu resources +registry +registry-cache diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..992b03b --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,17 @@ +default_stages: [pre-commit] +default_install_hook_types: [pre-commit, pre-push, commit-msg] +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.0.1 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-added-large-files + - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook + rev: v9.16.0 + hooks: + - id: commitlint + name: Lint commit message + stages: [commit-msg] + additional_dependencies: + - "@commitlint/config-conventional" diff --git a/.releaserc b/.releaserc new file mode 100644 index 0000000..ec3b4f8 --- /dev/null +++ b/.releaserc @@ -0,0 +1,35 @@ +{ + "branches": [ + "master" + ], + "plugins": [ + "@semantic-release/commit-analyzer", + "@semantic-release/release-notes-generator", + "@semantic-release/changelog", + [ + "@semantic-release/exec", + { + "prepareCmd": "sed -i 's/VERSION=\"[^\"]*\"/VERSION=\"'${nextRelease.version}'\"/' install.sh && sed -i 's/VERSION=\"[^\"]*\"/VERSION=\"'${nextRelease.version}'\"/' mu.sh && sed -i 's|\\(https://raw.githubusercontent.com/andrewscwei/mu/v\\)[^\"/]*\\(/install.sh\\)|\\1'${nextRelease.version}'\\2|' README.md" + } + ], + [ + "@semantic-release/git", + { + "assets": [ + "install.sh", + "mu.sh", + "CHANGELOG.md", + "README.md" + ], + "message": "chore: Release `v${nextRelease.version}` [skip ci]" + } + ], + [ + "@semantic-release/github", + { + "successComment": false, + "failComment": false + } + ] + ] +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c93c2a5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Andrew Wei + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d6a28cf --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# mu + +> A productivity-focused CLI for switching between and interacting with local projects + +`mu` is a CLI that allows you to switch your working directory to another local directory quickly. It also provides shortcuts for performing common operational tasks if that directory is a Git repo. + +## TL;DR + +First you need to teach `mu` where to look for your projects: + +1. From Terminal, `cd` to the directory of a repo. +2. Run `mu add ` to add the current directory to the `mu` registry, where `` is the key you wish to use to name this project. + +From now on you can just run `mu cd ` to navigate to that project directly from Terminal. Better yet, you can run `mu project ` (or `mu p ` for short) to immediate open it with your default text editor (`mu` scans for Xcode project files and Android Studio projects first then falls back to VSCode/Sublime/Atom/TextMate respectively, depending on which editor is installed in your system). + +## Usage + +Install mu via cURL: + +```sh +$ curl -o- https://raw.githubusercontent.com/andrewscwei/mu/v1.0.0/install.sh | bash +``` + +## Commands + +```sh +Usage: mu or mu -h for more info + +where is one of: + add - Maps the current working directory to a project key (alias: a) + cd - Changes the current working directory to the working directory of a project + clean - Cleans the registry by reconciling invalid entries + edit - Edits the registry file directly in the default text editor (USE WITH CAUTION) + help - Provides access to additional info regarding specific commands (alias: h) + list - Lists all current projects in the registry (aliases: ls, l) + project - Opens a project in intended IDE (alias: p) + remove - Removes a project from the registry (aliases: rm, r) + + GitHub: + gist - Downloads all files from a gist to the working directory +``` + +### `mu add ` +Maps the current working directory to a project key. If you don't specify a project key, the name of the current working directory will be used. + +### `mu cd ` +Changes the working directory to the working directory of a `mu` project. + +### `mu list` +Lists all current projects managed by `mu` + +### `mu project ` +Opens a `mu` project in designated IDE (supports Xcode/Sublime in respective priority). + +### `mu remove ` +Removes a `mu` project from the `mu` registry. If you don't specify a project key or index, the name of the current working directory will be used. + +> Whenever you run a command that expects a project key or index, you can optionally leave the key or index blank. The command infers it from the last used key. You can run `mu cache` to see what the last interacted project is. + +> Whenever you run a command that expects a project key or index, you can use `.` to refer to the current working directory (`pwd`). + +> Most commands have 1-letter short notations. For example, instead of doing `mu project` you can do `mu p`. diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..0626483 --- /dev/null +++ b/install.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash + +{ # This ensures the entire script is downloaded + +# Config. +VERSION="1.0.0" +SOURCE=https://raw.githubusercontent.com/andrewscwei/mu/v$VERSION/mu.sh + +# Colors. +COLOR_PREFIX="\x1b[" +COLOR_RESET=$COLOR_PREFIX"0m" +COLOR_BLACK=$COLOR_PREFIX"0;30m" +COLOR_RED=$COLOR_PREFIX"0;31m" +COLOR_GREEN=$COLOR_PREFIX"0;32m" +COLOR_ORANGE=$COLOR_PREFIX"0;33m" +COLOR_BLUE=$COLOR_PREFIX"0;34m" +COLOR_PURPLE=$COLOR_PREFIX"0;35m" +COLOR_CYAN=$COLOR_PREFIX"0;36m" +COLOR_LIGHT_GRAY=$COLOR_PREFIX"0;37m" + +# Checks if a command is available. +# +# @param $1 Name of the command. +function cmd_exists() { + type "$1" > /dev/null 2>&1 +} + +# Gets the default install path. This can be overridden when calling the +# download script by passing the `MU_DIR` variable. +function install_dir() { + printf %s "${MU_DIR:-"$HOME/.mu"}" +} + +# Installs mu as a script. +function install() { + local dest="$(install_dir)" + + mkdir -p "$dest" + + if [ -f "$dest/mu.sh" ]; then + echo -e "${COLOR_BLUE}mu: mu ${COLOR_ORANGE}is already installed in ${COLOR_CYAN}$dest${COLOR_ORANGE}, updating it instead...${COLOR_RESET}" + else + echo -e "${COLOR_BLUE}mu: ${COLOR_RESET}Downloading ${COLOR_BLUE}mu${COLOR_RESET} to ${COLOR_CYAN}$dest${COLOR_RESET}" + fi + + # Download the script. + curl --compressed -q -s "$SOURCE" -o "$dest/mu.sh" || { + echo >&2 "${COLOR_BLUE}mu: ${COLOR_RED}Failed to download from ${COLOR_CYAN}$SOURCE${COLOR_RESET}" + return 1 + } + + # Make script executable. + chmod a+x "$dest/mu.sh" || { + echo >&2 "${COLOR_BLUE}mu: ${COLOR_RED}Failed to mark ${COLOR_CYAN}$dest/mu.sh${COLOR_RESET} as executable" + return 3 + } +} + +# Main process +function main() { + # Download and install the script. + if cmd_exists curl; then + install + else + echo >&2 "${COLOR_BLUE}mu: ${COLOR_RED}You need ${COLOR_CYAN}curl${COLOR_RED} to install ${COLOR_BLUE}mu${COLOR_RESET}" + exit 1 + fi + + # Edit Bash and ZSH profile files to set up mu. + local dest="$(install_dir)" + local bashprofile="" + local zshprofile="" + local sourcestr="\nalias mu='. ${dest}/mu.sh'\n" + + if [ -f "$HOME/.bashrc" ]; then + bashprofile="$HOME/.bashrc" + elif [ -f "$HOME/.profile" ]; then + bahsprofile="$HOME/.profile" + elif [ -f "$HOME/.bash_profile" ]; then + bashprofile="$HOME/.bash_profile" + fi + + if [ -f "$HOME/.zshrc" ]; then + zshprofile="$HOME/.zshrc" + fi + + if [[ "$bashprofile" == "" ]] && [[ "$zshprofile" == "" ]]; then + echo -e "${COLOR_BLUE}mu: ${COLOR_RESET}Bash profile not found, tried ${COLOR_CYAN}~/.bashrc${COLOR_RESET}, ${COLOR_CYAN}~/.zshrc${COLOR_RESET}, ${COLOR_CYAN}~/.profile${COLOR_RESET} and ${COLOR_CYAN}~/.bash_profile${COLOR_RESET}" + echo -e " Create one of them and run this script again" + echo -e " OR" + echo -e " Append the following lines to the correct file yourself:" + echo -e " ${COLOR_CYAN}${sourcestr}${COLOR_RESET}" + exit 1 + fi + + if [[ "$bashprofile" != "" ]]; then + if ! command grep -qc '/mu.sh' "$bashprofile"; then + echo -e "${COLOR_BLUE}mu: ${COLOR_RESET}Appending ${COLOR_BLUE}mu${COLOR_RESET} source string to ${COLOR_CYAN}$bashprofile${COLOR_RESET}" + command printf "${sourcestr}" >> "$bashprofile" + else + echo -e "${COLOR_BLUE}mu: mu ${COLOR_RESET}source string is already in ${COLOR_CYAN}$bashprofile${COLOR_RESET}" + fi + fi + + if [[ "$zshprofile" != "" ]]; then + if ! command grep -qc '/mu.sh' "$zshprofile"; then + echo -e "${COLOR_BLUE}mu: ${COLOR_RESET}Appending ${COLOR_BLUE}mu${COLOR_RESET} source string to ${COLOR_CYAN}$zshprofile${COLOR_RESET}" + command printf "${sourcestr}" >> "$zshprofile" + else + echo -e "${COLOR_BLUE}mu: mu ${COLOR_RESET}source string is already in ${COLOR_CYAN}$zshprofile${COLOR_RESET}" + fi + fi + + # Source mu + \. "$dest/mu.sh" + + echo -e "${COLOR_BLUE}mu: ${COLOR_GREEN}Installation complete. Close and reopen your terminal to start using ${COLOR_BLUE}mu${COLOR_RESET}" +} + +main + +} # This ensures the entire script is downloaded diff --git a/mu.sh b/mu.sh new file mode 100755 index 0000000..df3f3cb --- /dev/null +++ b/mu.sh @@ -0,0 +1,619 @@ +#!/bin/bash + +# mu CLI +# © Andrew Wei +# +# This software is released under the MIT License: +# http://www.opensource.org/licenses/mit-license.php + +{ # This ensures the entire script is downloaded # + +# Config. +VERSION="1.0.0" + +# Colors. +COLOR_PREFIX="\x1b[" +COLOR_RESET=$COLOR_PREFIX"0m" +COLOR_BLACK=$COLOR_PREFIX"0;30m" +COLOR_RED=$COLOR_PREFIX"0;31m" +COLOR_GREEN=$COLOR_PREFIX"0;32m" +COLOR_ORANGE=$COLOR_PREFIX"0;33m" +COLOR_BLUE=$COLOR_PREFIX"0;34m" +COLOR_PURPLE=$COLOR_PREFIX"0;35m" +COLOR_CYAN=$COLOR_PREFIX"0;36m" +COLOR_LIGHT_GRAY=$COLOR_PREFIX"0;37m" + +# Paths. +PATH_ROOT=$(dirname ${BASH_SOURCE[0]-$0}) +PATH_REPOSITORY=$PATH_ROOT"/registry" +PATH_CACHE=$PATH_ROOT"/registry-cache" + +# Checks if a command is available +# +# @param $1 Name of the command. +function cmd_exists() { + type "$1" > /dev/null 2>&1 +} + +# Serializes the registry into an array of project entries in the form of +# "key":"path" string pair. This operation stores the array of project entries +# into `PROJECT_LIST` and its length into `PROJECT_LENGTH`. +function serialize_repo() { + # Reset global variable. + PROJECT_LIST=() + + if [ -e $PATH_REPOSITORY ]; then + # Read line-by-line. + while read l; do + if [[ $l == *:* ]]; then + PROJECT_LIST=("${PROJECT_LIST[@]}" "$l") + else + continue + fi + done <$PATH_REPOSITORY + fi + + PROJECT_LENGTH=${#PROJECT_LIST[@]} +} + +# Parses a project entry in the form of "key":"path" string pair and stores the +# key and the path into `TMP_PROJECT_ALIAS` and `TMP_PROJECT_PATH` respectively. +# +# @param $1 The "key":"path" string pair. +function decode_project_pair() { + if [[ "$1" == "" ]]; then return; fi + + # Store the key and path globally. Account for 1-base arrays in ZSH. + if [ -n "$ZSH_VERSION" ]; then + local arr=("${(@s/:/)1}") + TMP_PROJECT_ALIAS="${arr[1]}" + TMP_PROJECT_PATH="${arr[2]}" + else + local arr=(${1//\:/ }) + TMP_PROJECT_ALIAS="${arr[0]}" + TMP_PROJECT_PATH="${arr[1]}" + fi +} + +# Looks up the repo by key, index, or cache and stores the matching project pair +# globally. +# +# @param $1 Project key or index +function get_project_pair() { + if [[ "$1" == "" ]]; then + get_cache + get_project_pair_by_alias $PROJECT_CACHE + return + fi + + # . means get the project key from cache. + if [[ "$1" == "." ]]; then + get_project_pair_by_path "$(pwd)" + return + fi + + # Check if getting project pair by key or index. + [[ $1 =~ ^-?[0-9]+$ ]] && use_idx=1 || use_idx=0 + + if (($use_idx == 1)); then + get_project_pair_by_index $1 + else + get_project_pair_by_alias $1 + fi +} + +# Looks up the repo by key and stores the matching project pair globally. +# +# @param $1 Project key +function get_project_pair_by_alias() { + if [[ "$1" != "" ]]; then + serialize_repo + + # Iterate through the list of projects. + for ((i = 1; i <= $PROJECT_LENGTH; i++)); do + local idx=$([ -n "$ZSH_VERSION" ] && echo "$i" || echo "$((i-1))") + decode_project_pair "${PROJECT_LIST[$idx]}" + + if [[ "$TMP_PROJECT_ALIAS" == "$1" ]]; then + return + fi + done + fi + + TMP_PROJECT_ALIAS="" + TMP_PROJECT_PATH="" +} + +# Looks up the repo by index and stores the matching project pair globally. +# +# @param $1 Project index +function get_project_pair_by_index() { + if [[ "$1" != "" ]]; then + serialize_repo + + # Iterate through the list of projects. + for ((i = 1; i <= $PROJECT_LENGTH; i++)); do + local idx=$([ -n "$ZSH_VERSION" ] && echo "$i" || echo "$((i-1))") + decode_project_pair "${PROJECT_LIST[$idx]}" + + if (($i == $1)); then + return + fi + done + fi + + TMP_PROJECT_ALIAS="" + TMP_PROJECT_PATH="" +} + +# Looks up the repo by path and stores the matching project pair globally. +# +# @param $1 Project path +function get_project_pair_by_path() { + if [[ "$1" != "" ]]; then + serialize_repo + + # Iterate through the list of projects. + for ((i = 1; i <= $PROJECT_LENGTH; i++)); do + local idx=$([ -n "$ZSH_VERSION" ] && echo "$i" || echo "$((i-1))") + decode_project_pair "${PROJECT_LIST[$idx]}" + + if [[ "$TMP_PROJECT_PATH" == "$1" ]]; then + return + fi + done + fi + + TMP_PROJECT_ALIAS="" + TMP_PROJECT_PATH="" +} + +# Stores the cached key globally. +function get_cache() { + if [ -e $PATH_CACHE ]; then + PROJECT_CACHE=$(<$PATH_CACHE) + else + PROJECT_CACHE="" + fi +} + +# Writes the last used project key into cache. +# +# @param $1 Project key to be cached +function set_cache() { + if [[ "$1" == "" ]]; then return; fi + + # Iterate through the list of projects. + for ((i = 1; i <= $PROJECT_LENGTH; i++)); do + local idx=$([ -n "$ZSH_VERSION" ] && echo "$i" || echo "$((i-1))") + decode_project_pair "${PROJECT_LIST[$idx]}" + + if [[ "$TMP_PROJECT_ALIAS" == "$1" ]]; then + echo -e $1 >|$PATH_CACHE + return + fi + done + + echo -e "${COLOR_BLUE}mu: ${COLOR_RED}ERR! ${COLOR_RESET}Problem writing cache" +} + +# Opens the provided path in the preferred editor. +# +# @param $1 Path to open. +function open_in_editor() { + if [[ "$1" == "" ]]; then return; fi + + if cmd_exists "code"; then + code "$1" + elif cmd_exists "subl"; then + subl "$1" + elif cmd_exists "atom"; then + atom "$1" + elif cmd_exists "mate"; then + mate "$1" + else + echo -e "${COLOR_BLUE}mu: ${COLOR_RESET}No editors available" + fi +} + +# Shows the current cached project key. +function cmd_cache() { + get_cache + + if [[ "$PROJECT_CACHE" == "" ]]; then + echo -e "${COLOR_BLUE}mu: ${COLOR_RESET}The cache is empty" + else + echo -e "${COLOR_BLUE}mu: ${COLOR_RESET}Current project in cache: ${COLOR_CYAN}$PROJECT_CACHE${COLOR_RESET}" + fi +} + +# Adds to the registry the current directory associated with the specified +# project key. +# +# @param [$1] Key of project. Leave blank or use "." to use the name of the +# current directory. +function cmd_add() { + if [[ "$1" == "-h" ]]; then + echo -e "${COLOR_PURPLE}HELP: ${COLOR_BLUE}mu ${COLOR_CYAN}add ${COLOR_RESET} (alias: ${COLOR_CYAN}a${COLOR_RESET})" + echo + echo -e "Maps the current working directory to . If there already exists a project with the same key, its working directory will be replaced." + return + fi + + serialize_repo + + local key="$1" + local dir="$(pwd)" + local buffer="" + local check=0 + + if [[ "$key" == "" ]] || [[ "$key" == "." ]]; then + key="${PWD##*/}" + fi + + # Iterate through the list of projects. + for ((i = 1; i <= $PROJECT_LENGTH; i++)); do + local idx=$([ -n "$ZSH_VERSION" ] && echo "$i" || echo "$((i-1))") + local pair=${PROJECT_LIST[$idx]} + + decode_project_pair "$pair" + + # If the specified project key already exists... + if [[ "$TMP_PROJECT_ALIAS" == "$key" ]]; then + check=1 + buffer="$buffer$TMP_PROJECT_ALIAS:${dir}\n" + # Else just add the current line to the output buffer. + else + buffer="$buffer$pair\n" + fi + done + + if [[ $check == 0 ]]; then + buffer="$buffer$key:${dir}\n" + echo -e "${COLOR_BLUE}mu: ${COLOR_GREEN}OK ${COLOR_RESET}Mapped ${COLOR_CYAN}$key ${COLOR_RESET}to ${COLOR_CYAN}${dir}${COLOR_RESET}" + else + echo -e "${COLOR_BLUE}mu: ${COLOR_GREEN}OK ${COLOR_RESET}Remapped ${COLOR_CYAN}$key${COLOR_RESET} to ${COLOR_CYAN}${dir}${COLOR_RESET}" + fi + + echo -e $buffer >|$PATH_REPOSITORY + + serialize_repo + set_cache $key +} + +# Navigates to the root path of a project in Terminal. Either specify a string +# representing the project key or a number prefixed by '#' representing the +# index. +# +# @param $1 Project key or index +function cmd_cd() { + if [[ "$1" == "-h" ]]; then + echo -e "${COLOR_PURPLE}HELP: ${COLOR_BLUE}mu ${COLOR_CYAN}cd ${COLOR_RESET}" + echo + echo -e "Changes the current working directory to that of the specified ${COLOR_CYAN}${COLOR_RESET}." + return + fi + + if [[ "$1" == "-r" ]]; then + cd $PATH_ROOT + return + fi + + get_project_pair $1 + + if [[ "$TMP_PROJECT_ALIAS" != "" ]]; then + set_cache $TMP_PROJECT_ALIAS + cd "$TMP_PROJECT_PATH" + + return + fi + + echo -e "${COLOR_BLUE}mu: ${COLOR_RED}ERR! ${COLOR_RESET}Project with reference ${COLOR_CYAN}$1${COLOR_RESET} not found" +} + +# Tidies up the registry file, removing blank lines and fixing bad formatting. +function cmd_clean() { + if [[ "$1" == "-h" ]]; then + echo -e "${COLOR_PURPLE}HELP: ${COLOR_BLUE}mu ${COLOR_CYAN}clean${COLOR_RESET}" + echo + echo -e "Scans the registry and reconsiles invalid project entries." + return + fi + + serialize_repo + + local count=0 + local buffer="" + + # Iterate through the list of projects. + for ((i = 1; i <= $PROJECT_LENGTH; i++)); do + local idx=$([ -n "$ZSH_VERSION" ] && echo "$i" || echo "$((i-1))") + local pair=${PROJECT_LIST[$idx]} + + decode_project_pair "$pair" + + # Store entry in buffer if it is valid. If invalid it will not be recorded, + # thus 'cleaned'. + if [[ "$pair" != "" ]] && [[ "$TMP_PROJECT_ALIAS" != "" ]] && [[ "$TMP_PROJECT_PATH" != "" ]]; then + buffer="$buffer$pair\n" + else + count=$((count + 1)) + fi + done + + echo -e $buffer >|$PATH_REPOSITORY + echo -e "${COLOR_BLUE}mu: ${COLOR_GREEN}OK ${COLOR_RESET}Reconciled $count project(s)" +} + +# Lists all the projects in the registry. +function cmd_list() { + if [[ "$1" == "-h" ]]; then + echo -e "${COLOR_PURPLE}HELP: ${COLOR_BLUE}mu ${COLOR_CYAN}list${COLOR_RESET} (aliases: ${COLOR_CYAN}ls${COLOR_RESET}, ${COLOR_CYAN}l${COLOR_RESET})" + echo + echo -e "Lists all the current projects in the registry." + return + fi + + # Update PROJECT_LIST array. + serialize_repo + + local output="" + + if (($PROJECT_LENGTH == 0)); then + output="${output}${COLOR_BLUE}mu: ${COLOR_RED}ERR! ${COLOR_RESET}There are no projects in the registry." + else + output="${output}${COLOR_BLUE}mu: ${COLOR_RESET}Found ${COLOR_PURPLE}$PROJECT_LENGTH${COLOR_RESET} project(s) in the registry" + output="${output}\n\n" + + for ((i = 1; i <= $PROJECT_LENGTH; i++)); do + local idx=$([ -n "$ZSH_VERSION" ] && echo "$i" || echo "$((i-1))") + local pair=${PROJECT_LIST[$idx]} + + decode_project_pair "$pair" + + output="${output}$i. ${COLOR_CYAN}$TMP_PROJECT_ALIAS${COLOR_RESET}: $TMP_PROJECT_PATH" + + if (($idx != $PROJECT_LENGTH)); then + output="${output}\n" + fi + done + fi + + echo -e $output +} + +# Edits the local registry. +function cmd_edit_registry() { + if [[ "$1" == "-h" ]]; then + echo -e "${COLOR_PURPLE}HELP: ${COLOR_BLUE}mu ${COLOR_CYAN}edit${COLOR_RESET}" + echo + echo -e "Edits the registry file directly in the default text editor ${COLOR_PURPLE}(USE WITH CAUTION)${COLOR_RESET}." + return + fi + + open_in_editor $PATH_REPOSITORY +} + +# Opens a project in Finder. Either specify a string representing the project +# key or a number representing the index. +# +# @param $1 Project key or index +function cmd_open() { + if [[ "$1" == "-h" ]]; then + echo -e "${COLOR_PURPLE}HELP: ${COLOR_BLUE}mu ${COLOR_CYAN}open ${COLOR_RESET} (aliases: ${COLOR_CYAN}o${COLOR_RESET})" + echo + echo -e "Opens a project in Finder specified by ${COLOR_CYAN}${COLOR_RESET} from the registry." + return + fi + if [[ "$1" == "-h" ]]; then + echo -e "${COLOR_PURPLE}HELP: ${COLOR_RESET}No help data available regarding ${COLOR_RED}$1${COLOR_RESET} at this point" + return + fi + + if cmd_exists "open"; then + # If arg is blank, open root directory of mu. + if [[ "$1" == "-r" ]]; then + open $PATH_ROOT + echo -e "${COLOR_BLUE}mu: ${COLOR_GREEN}OK ${COLOR_RESET}Opened root in Finder" + return + fi + + get_project_pair $1 + + if [[ $TMP_PROJECT_ALIAS != "" ]]; then + set_cache $TMP_PROJECT_ALIAS + open "$TMP_PROJECT_PATH" + echo -e "${COLOR_BLUE}mu: ${COLOR_GREEN}OK ${COLOR_RESET}Opened project ${COLOR_CYAN}$TMP_PROJECT_ALIAS${COLOR_RESET} in Finder" + else + echo -e "${COLOR_BLUE}mu: ${COLOR_RED}ERR! ${COLOR_RESET}Project with reference ${COLOR_CYAN}$1${COLOR_RESET} not found" + fi + else + echo -e "${COLOR_BLUE}mu: ${COLOR_RED}ERR! ${COLOR_RESET}This command is only available in macOS" + fi +} + +# Removes a project from the registry. Either specify a string representing the +# project key or a number representing the index. +# +# @param $1 Project key or index. Leave blank or specify "." to use the name of +# the current directory. +function cmd_remove() { + if [[ "$1" == "-h" ]]; then + echo -e "${COLOR_PURPLE}HELP: ${COLOR_BLUE}mu ${COLOR_CYAN}remove ${COLOR_RESET} (aliases: ${COLOR_CYAN}rm${COLOR_RESET}, ${COLOR_CYAN}r${COLOR_RESET})" + echo + echo -e "Removes a project specified by ${COLOR_CYAN}${COLOR_RESET} from the registry." + return + fi + + local key="$1" + + if [[ "$key" == "" ]] || [[ "$key" == "." ]]; then + key="${PWD##*/}" + fi + + [[ $key =~ ^-?[0-9]+$ ]] && use_idx=1 || use_idx=0 + + serialize_repo + + local removed=0 + local buffer="" + + # Iterate through the list of projects. + for ((i = 1; i <= $PROJECT_LENGTH; i++)); do + local idx=$([ -n "$ZSH_VERSION" ] && echo "$i" || echo "$((i-1))") + local pair=${PROJECT_LIST[$idx]} + local skip=0 + + decode_project_pair "$pair" + + # If arg is a project index... + if (($use_idx == 1)) && (($i == $key)); then + skip=1 + removed=1 + + echo -e "${COLOR_BLUE}mu: ${COLOR_GREEN}OK ${COLOR_RESET}Removed project ${COLOR_CYAN}$TMP_PROJECT_ALIAS${COLOR_RESET} at index ${COLOR_PURPLE}$i${COLOR_RESET}" + + # Else if arg is a project key... + elif (($use_idx == 0)) && [ "$TMP_PROJECT_ALIAS" == "$key" ]; then + skip=1 + removed=1 + + echo -e "${COLOR_BLUE}mu: ${COLOR_GREEN}OK ${COLOR_RESET}Removed project ${COLOR_CYAN}$TMP_PROJECT_ALIAS${COLOR_RESET} at index ${COLOR_PURPLE}$i${COLOR_RESET}" + fi + + # If there was no match for this loop... + if (($skip == 0)); then + buffer="$buffer$pair\n" + fi + done + + # If nothing was removed, throw error. + if (($removed == 0)); then + if (($use_idx == 1)); then + echo -e "${COLOR_BLUE}mu: ${COLOR_RED}ERR! ${COLOR_RESET}Index ${COLOR_PURPLE}$key${COLOR_RESET} is out of bounds" + else + echo -e "${COLOR_BLUE}mu: ${COLOR_RED}ERR! ${COLOR_RESET}Project with key ${COLOR_CYAN}$key${COLOR_RESET} not found" + fi + fi + + echo -e $buffer >|$PATH_REPOSITORY +} + +# Opens a project from the registry. Either specify a string representing the +# project key or a number representing the index. +# +# @param $1 Project key or index +function cmd_project() { + if [[ "$1" == "-h" ]]; then + echo -e "${COLOR_PURPLE}HELP: ${COLOR_BLUE}mu ${COLOR_CYAN}project ${COLOR_RESET} (alias: ${COLOR_CYAN}p${COLOR_RESET})" + echo + echo -e "Opens a project specified by ${COLOR_CYAN}${COLOR_RESET} in its intended IDE. The following IDEs will be scanned: Xcode, Android Studio, Sublime, Atom and TextMate. If an IDE cannot be inferred, this command will be ignored." + return + fi + + get_project_pair $1 + + if [[ "$TMP_PROJECT_ALIAS" != "" ]]; then + TARGET_PROJECT_FILE="" + + for file in "$TMP_PROJECT_PATH"/*; do + # If *.xcworkspace file is found, use it immediately. + if [[ "$file" == *"xcworkspace" ]]; then + TARGET_PROJECT_FILE="$file" + break + # If *.xcodeproj is found, store it temporarily until another + # project file with higher priority is found. + elif [[ "$file" == *"xcodeproj" ]]; then + if [[ "$TARGET_PROJECT_FILE" != *"xcworkspace" ]]; then + TARGET_PROJECT_FILE="$file" + fi + # If *.sublime-project is found, store it temporarily until another + # project file with higher priority is found. + elif [[ "$file" == *".gradle" ]]; then + echo -e "${COLOR_BLUE}mu: ${COLOR_GREEN}OK ${COLOR_RESET}Found Android Studio project, opening project ${COLOR_CYAN}$TMP_PROJECT_ALIAS${COLOR_RESET} with ${COLOR_CYAN}Android Studio${COLOR_RESET}" + open -a /Applications/Android\ Studio.app $TMP_PROJECT_PATH + return + elif [[ "$file" == *"sublime-project" ]]; then + if [[ "$TARGET_PROJECT_FILE" != *"xcworkspace" ]] && [[ "$TARGET_PROJECT_FILE" != *"xcodeproj" ]]; then + TARGET_PROJECT_FILE="$file" + fi + fi + done + + if [[ "$TARGET_PROJECT_FILE" != "" ]]; then + set_cache $TMP_PROJECT_ALIAS + + if [[ "$TARGET_PROJECT_FILE" == *"xcworkspace" ]]; then + echo -e "${COLOR_BLUE}mu: ${COLOR_GREEN}OK ${COLOR_RESET}Found Xcode workspace, opening project ${COLOR_CYAN}$TMP_PROJECT_ALIAS${COLOR_RESET} with ${COLOR_CYAN}Xcode${COLOR_RESET}" + elif [[ "$TARGET_PROJECT_FILE" == *"xcodeproj" ]]; then + echo -e "${COLOR_BLUE}mu: ${COLOR_GREEN}OK ${COLOR_RESET}Found Xcode project, opening project ${COLOR_CYAN}$TMP_PROJECT_ALIAS${COLOR_RESET} with ${COLOR_CYAN}Xcode${COLOR_RESET}" + elif [[ "$TARGET_PROJECT_FILE" == *"sublime-project" ]]; then + echo -e "${COLOR_BLUE}mu: ${COLOR_GREEN}OK ${COLOR_RESET}Found Sublime project, opening project ${COLOR_CYAN}$TMP_PROJECT_ALIAS${COLOR_RESET} with ${COLOR_CYAN}Sublime${COLOR_RESET}" + fi + + open "$TARGET_PROJECT_FILE" + else + set_cache $TMP_PROJECT_ALIAS + echo -e "${COLOR_BLUE}mu: ${COLOR_GREEN}OK ${COLOR_RESET}No unique project files found, opening project ${COLOR_CYAN}$TMP_PROJECT_ALIAS${COLOR_RESET} in preferred editor" + open_in_editor "$TMP_PROJECT_PATH" + fi + + return + fi + + echo -e "${COLOR_BLUE}mu: ${COLOR_RED}ERR! ${COLOR_RESET}Project with reference ${COLOR_CYAN}$2${COLOR_RESET} not found" +} + +# Displays the directory. +function cmd_directory() { + echo + echo -e "Usage: ${COLOR_BLUE}mu ${COLOR_CYAN}${COLOR_RESET} or ${COLOR_BLUE}mu ${COLOR_CYAN} -h${COLOR_RESET} for more info" + echo + echo -e "where ${COLOR_CYAN}${COLOR_RESET} is one of:" + echo -e "${COLOR_CYAN} add${COLOR_RESET} - Maps the current working directory to a project key (alias: ${COLOR_CYAN}a${COLOR_RESET})" + echo -e "${COLOR_CYAN} cd${COLOR_RESET} - Changes the current working directory to the working directory of a project" + echo -e "${COLOR_CYAN} clean${COLOR_RESET} - Cleans the registry by reconciling invalid entries" + echo -e "${COLOR_CYAN} edit${COLOR_RESET} - Edits the registry file directly in the default text editor ${COLOR_PURPLE}(USE WITH CAUTION)${COLOR_RESET}" + echo -e "${COLOR_CYAN} help${COLOR_RESET} - Provides access to additional info regarding specific commands (alias: ${COLOR_CYAN}h${COLOR_RESET})" + echo -e "${COLOR_CYAN} list${COLOR_RESET} - Lists all current projects in the registry (aliases: ${COLOR_CYAN}ls${COLOR_RESET}, ${COLOR_CYAN}l${COLOR_RESET})" + echo -e "${COLOR_CYAN} project${COLOR_RESET} - Opens a project in intended IDE (alias: ${COLOR_CYAN}p${COLOR_RESET})" + echo -e "${COLOR_CYAN} remove${COLOR_RESET} - Removes a project from the registry (aliases: ${COLOR_CYAN}rm${COLOR_RESET}, ${COLOR_CYAN}r${COLOR_RESET})" + echo + echo -e " GitHub:" + echo -e "${COLOR_CYAN} gist${COLOR_RESET} - Downloads all files from a gist to the working directory" +} + +# Downloads all files from a Gist to the working directory individually. This +# function requires `jq`. +# +# @param $1 ID of the gist +# +# @see https://stedolan.github.io/jq/ +function cmd_gist() { + if [[ "$1" == "-h" ]]; then + echo -e "${COLOR_PURPLE}HELP: ${COLOR_BLUE}mu ${COLOR_CYAN}gist ${COLOR_RESET}" + echo + echo -e "Downloads all the files from a Gist as specified by ${COLOR_CYAN}${COLOR_RESET} to the working directory." + return + fi + + curl -sS --remote-name-all $(curl -sS https://api.github.com/gists/$1 | jq -r '.files[].raw_url') +} + +# Main process. +if [[ "$1" == "" ]] || [[ "$1" == "help" ]] || [[ "$1" == "h" ]]; then cmd_directory $2 +elif [[ "$1" == "add" ]] || [[ "$1" == "a" ]]; then cmd_add $2 +elif [[ "$1" == "cache" ]]; then cmd_cache $2 +elif [[ "$1" == "cd" ]]; then cmd_cd $2 +elif [[ "$1" == "clean" ]]; then cmd_clean $2 +elif [[ "$1" == "gist" ]]; then cmd_gist $2 +elif [[ "$1" == "list" ]] || [[ "$1" == "ls" ]] || [[ "$1" == "l" ]]; then cmd_list $2 +elif [[ "$1" == "edit" ]] then cmd_edit_registry $2 +elif [[ "$1" == "open" ]] || [[ "$1" == "o" ]]; then cmd_open $2 +elif [[ "$1" == "remove" ]] || [[ "$1" == "rm" ]] || [[ "$1" == "r" ]]; then cmd_remove $2 +elif [[ "$1" == "project" ]] || [[ "$1" == "p" ]]; then cmd_project $2 +elif [[ "$1" == "version" ]] || [[ "$1" == "-v" ]]; then echo -e "v$VERSION" +else echo -e "${COLOR_BLUE}mu: ${COLOR_RESET}Unsupported command:" $1 +fi + +} # This ensures the entire script is downloaded #