GitBucket
4.21.2
Toggle navigation
Snippets
Sign in
Files
Branches
1
Releases
Issues
Pull requests
Labels
Priorities
Milestones
Wiki
Forks
nigel.stanger
/
zsh-rust-git-prompt
Browse code
Documentation
master
1 parent
5abfb7b
commit
d00a9da185d746b79843f4277e05779fa1008f56
Nigel Stanger
authored
on 13 Oct 2022
Patch
Showing
2 changed files
README.md
src/main.rs
Ignore Space
Show notes
View
README.md
0 → 100644
# zsh-rust-git-prompt A bare-bones Git prompt for zsh implemented in Rust for speed (and as a learning exercise). Configuration is rudimentary because I figured “how often am I going to change the appearance of my Git prompt?” ## Features * Automatically detects Git repositories and only displays the prompt when inside one. * Includes the following Git status information: * current branch * commits ahead/behind remote * number of stashes (if any) * number of staged files (if any) * number of changed files (if any) * number of conflicting files (if any) * number of untracked files (if any) * number of ignored files (if any) * Basic format configuration via constants in the code. * Partial ANSI formatting support (currently only the ones I use). ## Dependencies * Rust (compilation). * Your favourite Nerd Font. ## Installation * Install Rust. * Install the relevant Nerd Font and set it as default in your terminal app. * Clone this repo. * `cargo build --release` * `mv target/release/zsh-rust-git-prompt <bindir>` where `<bindir>` is in `PATH`. * Add this to your `.zshrc`: ```zsh RPROMPT='$(git status --porcelain=v2 --branch --show-stash -z 2>/dev/null | zsh-rust-git-prompt)'` ``` (Or `PROMPT` if you prefer, but it’s really designed more for `RPROMPT`.) ## Configuration * Changing the configuration requires a rebuild (`cargo build --release`) and reinstall. * The strings used to generate the various components of the status line can be modified by changing the constants at the top of `main.rs`. * The format of the status line can be modified by changing the format strings at the bottom of `main.rs`. * There is only partial support for ANSI formatting codes at present, provided by the `*_style()` functions.
Ignore Space
Show notes
View
src/main.rs
use anes::{Attribute, Color, ResetAttributes, SetAttribute, SetForegroundColor}; use std::io; use std::process::ExitCode; use regex::Regex; use std::num::NonZeroUsize; use substring::Substring; // Configuration. // What to print before and after the current branch name. const BRANCH_PREFIX: &str = "\u{e725} "; // Nerd Font nf-dev-git_branch const BRANCH_SUFFIX: &str = " "; // What to print before the number of commits ahead and behind remote. const AHEAD_PREFIX: &str = "\u{f55c}"; // Nerd Font nf-mdi-arrow_up const BEHIND_PREFIX: &str = "\u{f544}"; // Nerd Font nf-mdi-arrow_down // What to print between the current branch information and the file // status information. const STATUS_SEPARATOR: &str = " | "; // U+007C VERTICAL LINE // What to print before the number of stashes. const STASHED_PREFIX: &str = "\u{2691}"; // U+2691 BLACK FLAG // What to print before the number of staged files. const STAGED_PREFIX: &str = "\u{ea71}"; // Nerd Font nf-cod-circle_filled // What to print before the number of changed files. const CHANGED_PREFIX: &str = "+"; // U+002B PLUS SIGN // What to print before the number of conflicting files. const CONFLICTED_PREFIX: &str = "\u{f655}"; // Nerd font nf-mdi-close // What to print before the number of untracked files. const UNTRACKED_PREFIX: &str = "\u{f142}"; // Nerd Font nf-fa-ellipsis_v // What to print before the number of ignored files. const IGNORED_PREFIX: &str = "!"; // U+0021 EXCLAMATION MARK // What to print if the branch is clean. const CLEAN_INDICATOR: &str = "\u{f00c}"; // Nerd Font nf-fa-check // Styling. fn reset_style() -> String { return format!("%{{{}%}}", ResetAttributes); } fn branch_style() -> String { return format!("%{{{}{}{}%}}", SetForegroundColor(Color::Black), SetAttribute(Attribute::Bold), SetAttribute(Attribute::Underline)); } fn stashed_style() -> String { return format!("%{{{}%}}", SetForegroundColor(Color::Blue)); } fn staged_style() -> String { return format!("%{{{}%}}", SetForegroundColor(Color::Blue)); } fn changed_style() -> String { return format!("%{{{}%}}", SetForegroundColor(Color::DarkMagenta)); } fn conflicted_style() -> String { return format!("%{{{}%}}", SetForegroundColor(Color::DarkRed)); } fn untracked_style() -> String { return format!("%{{{}%}}", SetForegroundColor(Color::DarkGreen)); } fn ignored_style() -> String { return format!("%{{{}%}}", SetForegroundColor(Color::Gray)); } fn clean_style() -> String { return format!("%{{{}%}}", SetForegroundColor(Color::DarkGreen)); } fn main() -> ExitCode { let mut git_status = String::new(); io::stdin().read_line(&mut git_status).expect("Failed to read input"); if git_status.len() == 0 { print!(""); return ExitCode::SUCCESS; } // Current commit. // example: # branch.oid 825882679bbf52650f8115bfec8591d973d80b49 let hash = match Regex::new(r"# branch\.oid ([^\x00]+)").unwrap().captures(&git_status) { Some(caps) => caps.get(1).unwrap().as_str(), None => "???", }; // Current branch. // example: # branch.head master // example: # branch.head (detached) let branch = match Regex::new(r"# branch\.head ([^\x00]+)").unwrap().captures(&git_status) { Some(caps) => { let name = caps.get(1).unwrap().as_str(); if name == "(detached)" { // use the (shortened) commit hash format!("{BRANCH_PREFIX}:{}{}{}{BRANCH_SUFFIX}", branch_style(), hash.substring(0, 7), reset_style()) } else { format!("{BRANCH_PREFIX}{}{}{}{BRANCH_SUFFIX}", branch_style(), name, reset_style()) } }, None => "???".to_string(), }; // Commits ahead/behind of upstream branch. // example: # branch.ab +2 -5 let (ahead, behind) = match Regex::new(r"# branch\.ab \+(\d+) -(\d+)").unwrap().captures(&git_status) { Some(caps) => ( format!("{AHEAD_PREFIX}{}", caps.get(1).unwrap().as_str()), format!("{BEHIND_PREFIX}{}", caps.get(2).unwrap().as_str()) ), None => ( format!("{AHEAD_PREFIX}0"), format!("{BEHIND_PREFIX}0") ), }; // Number of stashed files. // example: # stash 4 let stashed = match Regex::new(r"# stash (\d+)").unwrap().captures(&git_status) { Some(caps) => format!("{}{STASHED_PREFIX}{}{} ", stashed_style(), caps.get(1).unwrap().as_str(), reset_style()), None => "".to_string(), }; // Number of staged files. // example: 1 A. N... 000000 100644 100644 0000000000000000000000000000000000000000 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 file.staged let staged = match NonZeroUsize::new(Regex::new(r"1 A. ([^\x00]+)").unwrap().find_iter(&git_status).count()) { Some(n) => format!("{}{STAGED_PREFIX}{n}{} ", staged_style(), reset_style()), None => "".to_string(), }; // Number of changed files. // example: 1 .M N... 000000 100644 100644 0000000000000000000000000000000000000000 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 file.changed let changed = match NonZeroUsize::new(Regex::new(r"1 .M ([^\x00]+)").unwrap().find_iter(&git_status).count()) { Some(n) => format!("{}{CHANGED_PREFIX}{n}{} ", changed_style(), reset_style()), None => "".to_string(), }; // Number of conflicted files. // example: u UD N... 000000 100644 100644 0000000000000000000000000000000000000000 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 file.conflicted let conflicted = match NonZeroUsize::new(Regex::new(r"u .. ([^\x00]+)").unwrap().find_iter(&git_status).count()) { Some(n) => format!("{}{CONFLICTED_PREFIX}{n}{} ", conflicted_style(), reset_style()), None => "".to_string(), }; // Number of untracked files. // example: ? file.untracked let untracked = match NonZeroUsize::new(Regex::new(r"\? ([^\x00]+)").unwrap().find_iter(&git_status).count()) { Some(n) => format!("{}{UNTRACKED_PREFIX}{n}{} ", untracked_style(), reset_style()), None => "".to_string(), }; // Number of ignored files. // example: ! file.ignored let ignored = match NonZeroUsize::new(Regex::new(r"! ([^\x00]+)").unwrap().find_iter(&git_status).count()) { Some(n) => format!("{}{IGNORED_PREFIX}{n}{} ", ignored_style(), reset_style()), None => "".to_string(), }; let status_string = format!("{stashed}{ignored}{staged}{changed}{conflicted}{untracked}"); print!("{branch}{ahead}{behind}{STATUS_SEPARATOR}{}", if status_string.len() > 0 { status_string.trim().to_string() } else { format!("{}{CLEAN_INDICATOR}{}", clean_style(), reset_style()) } ); ExitCode::SUCCESS }
use anes::{Attribute, Color, ResetAttributes, SetAttribute, SetForegroundColor}; use std::io; use std::process::ExitCode; use regex::Regex; use std::num::NonZeroUsize; use substring::Substring; const BRANCH_PREFIX: &str = "\u{e725} "; const BRANCH_SUFFIX: &str = " "; const AHEAD_PREFIX: &str = "\u{f55c}"; const BEHIND_PREFIX: &str = "\u{f544}"; const STATUS_SEPARATOR: &str = " | "; const STASHED_PREFIX: &str = "⚑"; const STAGED_PREFIX: &str = "\u{ea71}"; const CHANGED_PREFIX: &str = "+"; const CONFLICTED_PREFIX: &str = "\u{f655}"; const UNTRACKED_PREFIX: &str = "\u{f142}"; const IGNORED_PREFIX: &str = "!"; const CLEAN_INDICATOR: &str = "\u{f00c}"; fn reset_style() -> String { return format!("%{{{}%}}", ResetAttributes); } fn branch_style() -> String { return format!("%{{{}{}{}%}}", SetForegroundColor(Color::Black), SetAttribute(Attribute::Bold), SetAttribute(Attribute::Underline)); } fn stashed_style() -> String { return format!("%{{{}%}}", SetForegroundColor(Color::Blue)); } fn staged_style() -> String { return format!("%{{{}%}}", SetForegroundColor(Color::Blue)); } fn changed_style() -> String { return format!("%{{{}%}}", SetForegroundColor(Color::DarkMagenta)); } fn conflicted_style() -> String { return format!("%{{{}%}}", SetForegroundColor(Color::DarkRed)); } fn untracked_style() -> String { return format!("%{{{}%}}", SetForegroundColor(Color::DarkGreen)); } fn ignored_style() -> String { return format!("%{{{}%}}", SetForegroundColor(Color::Gray)); } fn clean_style() -> String { return format!("%{{{}%}}", SetForegroundColor(Color::DarkGreen)); } fn main() -> ExitCode { let mut git_status = String::new(); io::stdin().read_line(&mut git_status).expect("Failed to read input"); if git_status.len() == 0 { print!(""); return ExitCode::SUCCESS; } // Current commit. // example: # branch.oid 825882679bbf52650f8115bfec8591d973d80b49 let hash = match Regex::new(r"# branch\.oid ([^\x00]+)").unwrap().captures(&git_status) { Some(caps) => caps.get(1).unwrap().as_str(), None => "???", }; // Current branch. // example: # branch.head master // example: # branch.head (detached) let branch = match Regex::new(r"# branch\.head ([^\x00]+)").unwrap().captures(&git_status) { Some(caps) => { let name = caps.get(1).unwrap().as_str(); if name == "(detached)" { // use the (shortened) commit hash format!("{BRANCH_PREFIX}:{}{}{}{BRANCH_SUFFIX}", branch_style(), hash.substring(0, 7), reset_style()) } else { format!("{BRANCH_PREFIX}{}{}{}{BRANCH_SUFFIX}", branch_style(), name, reset_style()) } }, None => "???".to_string(), }; // Commits ahead/behind of upstream branch. // example: # branch.ab +2 -5 let (ahead, behind) = match Regex::new(r"# branch\.ab \+(\d+) -(\d+)").unwrap().captures(&git_status) { Some(caps) => ( format!("{AHEAD_PREFIX}{}", caps.get(1).unwrap().as_str()), format!("{BEHIND_PREFIX}{}", caps.get(2).unwrap().as_str()) ), None => ( format!("{AHEAD_PREFIX}0"), format!("{BEHIND_PREFIX}0") ), }; // Number of stashed files. // example: # stash 4 let stashed = match Regex::new(r"# stash (\d+)").unwrap().captures(&git_status) { Some(caps) => format!("{}{STASHED_PREFIX}{}{} ", stashed_style(), caps.get(1).unwrap().as_str(), reset_style()), None => "".to_string(), }; // Number of staged files. // example: 1 A. N... 000000 100644 100644 0000000000000000000000000000000000000000 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 file.staged let staged = match NonZeroUsize::new(Regex::new(r"1 A. ([^\x00]+)").unwrap().find_iter(&git_status).count()) { Some(n) => format!("{}{STAGED_PREFIX}{n}{} ", staged_style(), reset_style()), None => "".to_string(), }; // Number of changed files. // example: 1 .M N... 000000 100644 100644 0000000000000000000000000000000000000000 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 file.changed let changed = match NonZeroUsize::new(Regex::new(r"1 .M ([^\x00]+)").unwrap().find_iter(&git_status).count()) { Some(n) => format!("{}{CHANGED_PREFIX}{n}{} ", changed_style(), reset_style()), None => "".to_string(), }; // Number of conflicted files. // example: u UD N... 000000 100644 100644 0000000000000000000000000000000000000000 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 file.conflicted let conflicted = match NonZeroUsize::new(Regex::new(r"u .. ([^\x00]+)").unwrap().find_iter(&git_status).count()) { Some(n) => format!("{}{CONFLICTED_PREFIX}{n}{} ", conflicted_style(), reset_style()), None => "".to_string(), }; // Number of untracked files. // example: ? file.untracked let untracked = match NonZeroUsize::new(Regex::new(r"\? ([^\x00]+)").unwrap().find_iter(&git_status).count()) { Some(n) => format!("{}{UNTRACKED_PREFIX}{n}{} ", untracked_style(), reset_style()), None => "".to_string(), }; // Number of ignored files. // example: ! file.ignored let ignored = match NonZeroUsize::new(Regex::new(r"! ([^\x00]+)").unwrap().find_iter(&git_status).count()) { Some(n) => format!("{}{IGNORED_PREFIX}{n}{} ", ignored_style(), reset_style()), None => "".to_string(), }; let status_string = format!("{stashed}{ignored}{staged}{changed}{conflicted}{untracked}"); print!("{branch}{ahead}{behind}{STATUS_SEPARATOR}{}", if status_string.len() > 0 { status_string.trim().to_string() } else { format!("{}{CLEAN_INDICATOR}{}", clean_style(), reset_style()) } ); ExitCode::SUCCESS }
Show line notes below