Here is the script I use to define a custom ZSH prompt
import childProcess from 'child_process';
const BRAND = '🧙';
/**
* SETUP
* Add the following to ~/.zshrc file
* setopt PROMPT_SUBST
* PROMPT='$(node {SCRIPT_PATH} $?)'
*
* LEGEND
* The color of time indicates the status of the last command
* '+' before the git branch indicates that you are ahead of origin
* '-' before the git branch indicates that you are behind origin
* '±' before the git branch indicates that you are ahead AND behind origin
* '?' after the git branch indicates that there are unstaged changes
* '+' after the git branch indicates that there are only staged changes
* The color of the git branch is inferred from the symbols and does not add any additional context
*/
async function exec(cmd) {
return await new Promise((resolve) => {
childProcess.exec(cmd, (error, stdout) => {
resolve({
error: !!error,
output: stdout.replace(/\n$/, ''),
});
});
});
}
async function getGitState() {
const { error: gitError } = await exec('git rev-parse --git-dir');
if (gitError) return null;
const { output: branch } = await exec('git rev-parse --abbrev-ref HEAD');
const { output: aheadLog } = await exec(`git log origin/${branch}..HEAD`)
const { output: behindLog } = await exec(`git log HEAD..origin/${branch}`)
const { output: status } = await exec('git status --porcelain');
return {
branch,
ahead: !!aheadLog.match(/^commit /m),
behind: !!behindLog.match(/^commit /m),
untracked: !!status.match(/^\?\? /m),
unstaged: !!status.match(/^([ MARC][MD]) /m),
staged: !!status.match(/^(D[ M]|[MARC][ MD]) /m),
unmerged: !!status.match(/^(A[AU]|D[DU]|U[ADU]) /m),
};
}
const color = (color, str) => `%F{${color}}${str}%f`;
const green = (str) => color('green', str);
const red = (str) => color('red', str);
const yellow = (str) => color('yellow', str);
const cyan = (str) => color('cyan', str);
const magenta = (str) => color('magenta', str);
const cmdStatus = parseInt(process.argv[2] || '0');
const prompt = [];
const time = new Date().toLocaleTimeString();
const gitState = await getGitState();
prompt.push('\n');
if (BRAND) {
prompt.push(`${BRAND} `);
}
prompt.push(cmdStatus === 0 ? green(time) : red(time));
prompt.push(' ∙ ');
prompt.push(cyan(process.cwd()));
if (gitState) {
const gitSegment = [];
gitSegment.push('(');
if (gitState.ahead && gitState.behind) {
gitSegment.push('±');
} else if (gitState.ahead) {
gitSegment.push('+');
} else if (gitState.behind) {
gitSegment.push('-');
}
if (gitState.behind || gitState.untracked || gitState.unstaged || gitState. unmerged) {
gitSegment.push(red(gitState.branch));
} else if (gitState.ahead || gitState.staged) {
gitSegment.push(yellow(gitState.branch));
} else {
gitSegment.push(green(gitState.branch));
}
if (gitState.untracked || gitState.unstaged || gitState. unmerged) {
gitSegment.push('?');
} else if (gitState.staged) {
gitSegment.push('+');
}
gitSegment.push(')');
prompt.push(' ∙ ');
prompt.push(gitSegment.join(''));
}
prompt.push('\n');
prompt.push(magenta('» '));
console.log(prompt.join(''));