From 2d6f4c5c588f4bf7fd435d06489b0f2946bd0591 Mon Sep 17 00:00:00 2001 From: MasterGordon Date: Sat, 10 Aug 2024 12:39:49 +0200 Subject: [PATCH] init --- Cargo.lock | 174 +++++++++++++++++++++++++++++- Cargo.toml | 6 +- readme.md | 139 ++++++++++++++++++++++++ src/colors.rs | 59 ++++++++++ src/main.rs | 140 +++++++++++++++++++++++- src/prompt_parts.rs | 6 ++ src/prompt_parts/branch_name.rs | 21 ++++ src/prompt_parts/branch_status.rs | 53 +++++++++ src/prompt_parts/branch_sync.rs | 33 ++++++ src/prompt_parts/icon.rs | 20 ++++ src/prompt_parts/origin_icon.rs | 30 ++++++ src/prompt_parts/prompt_part.rs | 5 + 12 files changed, 678 insertions(+), 8 deletions(-) create mode 100644 readme.md create mode 100644 src/colors.rs create mode 100644 src/prompt_parts.rs create mode 100644 src/prompt_parts/branch_name.rs create mode 100644 src/prompt_parts/branch_status.rs create mode 100644 src/prompt_parts/branch_sync.rs create mode 100644 src/prompt_parts/icon.rs create mode 100644 src/prompt_parts/origin_icon.rs create mode 100644 src/prompt_parts/prompt_part.rs diff --git a/Cargo.lock b/Cargo.lock index 22dea58..ba48384 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "bitflags" version = "2.6.0" @@ -18,6 +27,23 @@ dependencies = [ "libc", ] +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + +[[package]] +name = "fast-git-prompt" +version = "0.1.0" +dependencies = [ + "git2", + "regex", + "schemars", + "serde", + "serde_json", +] + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -52,6 +78,12 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + [[package]] name = "jobserver" version = "0.1.32" @@ -113,6 +145,12 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + [[package]] name = "openssl-probe" version = "0.1.5" @@ -144,10 +182,134 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] -name = "rs-prompt" -version = "0.1.0" +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ - "git2", + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "schemars" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn", +] + +[[package]] +name = "serde" +version = "1.0.205" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e33aedb1a7135da52b7c21791455563facbbcc43d0f0f66165b42c21b3dfb150" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.205" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692d6f5ac90220161d6774db30c662202721e64aed9058d2c394f451261420c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "2.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] @@ -171,6 +333,12 @@ version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + [[package]] name = "unicode-normalization" version = "0.1.23" diff --git a/Cargo.toml b/Cargo.toml index e1f7e24..de62156 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,11 @@ [package] -name = "rs-prompt" +name = "fast-git-prompt" version = "0.1.0" edition = "2021" [dependencies] git2 = "0.19.0" +serde_json = "1.0.122" +serde = { version = "1.0.205", features = ["derive"] } +regex = "1.10.6" +schemars = "0.8.21" diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..2308cdb --- /dev/null +++ b/readme.md @@ -0,0 +1,139 @@ +# fast-git-prompt + +A fast git prompt for zsh and bash. + +> This is a work in progress. +> More features will be added in the future. + +## Installation + +```bash +cargo install fast-git-prompt +``` + +Make sure you have `$HOME/.cargo/bin` in your `$PATH`. + +## Usage + +Include `fast-git-prompt` in your `.zshrc` or `.bashrc` file as part of your prompt. + +## Configuration + +Create a file called `config.json` in your `$XDG_CONFIG_HOME/fast-git-prompt` or `$HOME/.config/fast-git-prompt` directory. +The configuration of the prompt is fully modular and customizable. + +### Example + +````json +{ + "version-do-not-modify": "0.1.0", + "schema": "$XDG_CONFIG_HOME/fast-git-prompt/schema.json", + "baseColor": "white", + "prompt": [ + // Your prompt parts go here + ] +} + +### Prompt Parts + +#### Branch Name + +The branch name is the name of the current branch. + +```json +{ + "type": "branchName", + "color": "white" // Optional +} +```` + +#### Origin Icon + +The origin icon is the icon of the current branch's remote. + +```json +{ + "type": "originIcon", + "icons": { + "github.com": { + "icon": "", + "color": "white" + }, + "gitlab.com": { + "icon": "", + "color": "brightRed" + } + }, + "defaultIcon": { + "icon": "", + "color": "orange" + } +} +``` + +#### Branch Status + +The branch status is the status of the current branch. + +```json +{ + "type": "BranchStatus", + "dirty": { + "color": "red", + "icon": "✗" + }, + "clean": { + "color": "green", + "icon": "󰸞" + }, + "deleted": { + "color": "red", + "icon": "" + }, + "changed": { + "color": "yellow", + "icon": "" + }, + "new": { + "color": "yellow", + "icon": "" + } +} +``` + +#### Branch Sync + +The branch sync is the sync status of the current branch. + +```json +{ + "type": "BranchSync", + "ahead": { + "icon": "↑" + }, + "behind": { + "icon": "↓" + } +} +``` + +### Colors + +You can currently only use ansi colors. Which will use by your terminal emulator. + +- black +- red +- green +- yellow +- blue +- magenta +- cyan +- white +- brightBlack +- brightRed +- brightGreen +- brightYellow +- brightBlue +- brightMagenta +- brightCyan +- brightWhite diff --git a/src/colors.rs b/src/colors.rs new file mode 100644 index 0000000..f2b3db7 --- /dev/null +++ b/src/colors.rs @@ -0,0 +1,59 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, Copy)] +#[serde(rename_all = "camelCase")] +pub enum Color { + Black, + Red, + Green, + Yellow, + Blue, + Magenta, + Cyan, + White, + BrightBlack, + BrightRed, + BrightGreen, + BrightYellow, + BrightBlue, + BrightMagenta, + BrightCyan, + BrightWhite, +} + +const ESC: &str = "\x1b"; + +pub trait Ansi { + fn to_ansi_code(&self) -> String; +} + +pub fn color(color: Option) -> String { + if let Some(color) = color { + return color.to_ansi_code(); + } + return "".to_string(); +} + +impl Ansi for Color { + fn to_ansi_code(&self) -> String { + match self { + Color::Black => format!("{}[30m", ESC), + Color::Red => format!("{}[31m", ESC), + Color::Green => format!("{}[32m", ESC), + Color::Yellow => format!("{}[33m", ESC), + Color::Blue => format!("{}[34m", ESC), + Color::Magenta => format!("{}[35m", ESC), + Color::Cyan => format!("{}[36m", ESC), + Color::White => format!("{}[37m", ESC), + Color::BrightBlack => format!("{}[90m", ESC), + Color::BrightRed => format!("{}[91m", ESC), + Color::BrightGreen => format!("{}[92m", ESC), + Color::BrightYellow => format!("{}[93m", ESC), + Color::BrightBlue => format!("{}[94m", ESC), + Color::BrightMagenta => format!("{}[95m", ESC), + Color::BrightCyan => format!("{}[96m", ESC), + Color::BrightWhite => format!("{}[97m", ESC), + } + } +} diff --git a/src/main.rs b/src/main.rs index 3016e5f..d7efa54 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,143 @@ +use std::env; +use std::path::Path; +use std::process::exit; + use git2::Repository; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +mod prompt_parts; +use crate::prompt_parts::branch_name::BranchName; +use crate::prompt_parts::branch_status::BranchStatus; +use crate::prompt_parts::branch_sync::BranchSync; +use crate::prompt_parts::origin_icon::OriginIcon; +use crate::prompt_parts::prompt_part::RenderablePromptPart; +mod colors; +use crate::colors::{color, Color}; + +const VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +#[serde(tag = "type")] +enum PromptPart { + #[serde(alias = "branchName")] + BranchName(BranchName), + #[serde(alias = "originIcon")] + OriginIcon(OriginIcon), + #[serde(alias = "branchStatus")] + BranchStatus(BranchStatus), + #[serde(alias = "branchSync")] + BranchSync(BranchSync), +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +struct Config { + #[serde(rename = "$schema")] + schema: Option, + #[serde(rename = "version-do-not-modify")] + version: Option, + base_color: Option, + prompt: Vec, +} + +fn get_config_dir() -> String { + let config_dir_base = env::var("XDG_CONFIG_HOME").unwrap_or(format!( + "{}/.config", + match env::var("HOME") { + Ok(home) => home, + Err(_) => { + println!("Could not find HOME env variable"); + exit(1); + } + } + )); + format!("{}/fast-git-prompt", config_dir_base) +} + +fn write_schema() { + let schema = schemars::schema_for!(Config); + let mut file = std::fs::File::create(format!("{}/schema.json", get_config_dir())).unwrap(); + serde_json::to_writer_pretty(&mut file, &schema).unwrap(); +} + +fn read_config() -> Config { + let json = std::fs::read_to_string(format!("{}/config.json", get_config_dir())).unwrap(); + serde_json::from_str(&json).unwrap() +} + +fn write_config(config: Config) { + let mut config_file = + std::fs::File::create(format!("{}/config.json", get_config_dir())).unwrap(); + serde_json::to_writer_pretty(&mut config_file, &config).unwrap() +} + +fn setup() { + let config_dir = get_config_dir(); + if !Path::new(&config_dir).exists() { + std::fs::create_dir_all(&config_dir).unwrap(); + write_schema(); + let config = Config { + version: Some(VERSION.to_string()), + schema: Some(format!("{}/schema.json", config_dir)), + base_color: None, + prompt: vec![], + }; + write_config(config); + } else { + let mut config = read_config(); + if config.version != Some(VERSION.to_string()) { + write_schema(); + config.version = Some(VERSION.to_string()); + write_config(config); + } + } +} fn main() { + setup(); + let mut prompt: Vec = vec![]; + let config = read_config(); + + // let repo = match Repository::open(".") { let repo = match Repository::open(".") { Ok(repo) => repo, - Err(e) => panic!("failed to init: {}", e), + Err(_) => { + exit(0); + } }; - let head = repo.head().unwrap(); - let name = head.name().unwrap(); - println!("Hello, world! {}", name); + for part in config.prompt { + match part { + PromptPart::BranchName(branch_name) => { + let branch_name = branch_name.render(&repo); + if branch_name.is_some() { + prompt.push(branch_name.unwrap()); + } + } + PromptPart::OriginIcon(origin_icon) => { + let origin_icon = origin_icon.render(&repo); + if origin_icon.is_some() { + prompt.push(origin_icon.unwrap()); + } + } + PromptPart::BranchStatus(branch_status) => { + let branch_status = branch_status.render(&repo); + if branch_status.is_some() { + prompt.push(branch_status.unwrap()); + } + } + PromptPart::BranchSync(branch_sync) => { + let branch_sync = branch_sync.render(&repo); + if branch_sync.is_some() { + prompt.push(branch_sync.unwrap()); + } + } + } + } + prompt = prompt.drain(..).filter(|s| !s.is_empty()).collect(); + + print!( + "{}{}", + color(config.base_color), + prompt.join(format!("{} ", color(config.base_color)).as_str()) + ); } diff --git a/src/prompt_parts.rs b/src/prompt_parts.rs new file mode 100644 index 0000000..6711e2d --- /dev/null +++ b/src/prompt_parts.rs @@ -0,0 +1,6 @@ +pub mod branch_name; +pub mod branch_status; +pub mod branch_sync; +pub mod icon; +pub mod origin_icon; +pub mod prompt_part; diff --git a/src/prompt_parts/branch_name.rs b/src/prompt_parts/branch_name.rs new file mode 100644 index 0000000..94a53ab --- /dev/null +++ b/src/prompt_parts/branch_name.rs @@ -0,0 +1,21 @@ +use crate::prompt_parts::prompt_part::RenderablePromptPart; +use git2::Repository; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct BranchName { + pub color: Option, +} + +impl RenderablePromptPart for BranchName { + fn render(self, repo: &Repository) -> Option { + let head = match repo.head() { + Ok(r) => Some(r), + Err(_) => None, + }?; + let name = head.name()?; + let last = name.split('/').last()?; + return Some(format!("{}{}", self.color.unwrap_or("".to_string()), last)); + } +} diff --git a/src/prompt_parts/branch_status.rs b/src/prompt_parts/branch_status.rs new file mode 100644 index 0000000..203109e --- /dev/null +++ b/src/prompt_parts/branch_status.rs @@ -0,0 +1,53 @@ +use crate::prompt_parts::icon::{Icon, RenderableIcon}; +use crate::prompt_parts::prompt_part::RenderablePromptPart; +use git2::{Repository, Status}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct BranchStatus { + pub dirty: Icon, + pub clean: Icon, + pub deleted: Icon, + pub changed: Icon, + pub new: Icon, +} + +impl RenderablePromptPart for BranchStatus { + fn render(self, repo: &Repository) -> Option { + let state = repo.state(); + let changes = repo.statuses(None).ok()?; + let has_new = changes + .iter() + .any(|status| status.status() == Status::WT_NEW); + let has_changed = changes.iter().any(|status| { + status.status() == Status::INDEX_MODIFIED + || status.status() == Status::WT_MODIFIED + || status.status() == Status::INDEX_RENAMED + || status.status() == Status::WT_RENAMED + || status.status() == Status::WT_NEW + }); + let has_deleted = changes.iter().any(|status| { + status.status() == Status::INDEX_DELETED || status.status() == Status::WT_DELETED + }); + let is_clean = + state == git2::RepositoryState::Clean && !has_changed && !has_deleted && !has_new; + + let mut parts = vec![]; + if is_clean { + parts.push(self.clean.render()); + } else { + parts.push(self.dirty.render()); + } + if has_changed { + parts.push(self.changed.render()); + } + if has_new { + parts.push(self.new.render()); + } + if has_deleted { + parts.push(self.deleted.render()); + } + return Some(parts.join(" ")); + } +} diff --git a/src/prompt_parts/branch_sync.rs b/src/prompt_parts/branch_sync.rs new file mode 100644 index 0000000..1cb0b06 --- /dev/null +++ b/src/prompt_parts/branch_sync.rs @@ -0,0 +1,33 @@ +use crate::prompt_parts::icon::{Icon, RenderableIcon}; +use crate::prompt_parts::prompt_part::RenderablePromptPart; +use git2::Repository; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct BranchSync { + pub ahead: Icon, + pub behind: Icon, +} + +impl RenderablePromptPart for BranchSync { + fn render(self, repo: &Repository) -> Option { + let head = repo.head().ok()?; + let head_oid = head.target()?; + let remote = repo + .find_branch("origin/main", git2::BranchType::Remote) + .ok()?; + let remote_oid = remote.get().target()?; + let (ahead, behind) = repo.graph_ahead_behind(head_oid, remote_oid).ok()?; + + let mut parts = vec![]; + if ahead > 0 { + parts.push(format!("{} {}", self.ahead.render(), ahead)); + } + if behind > 0 { + parts.push(format!("{} {}", self.behind.render(), behind)); + } + + return Some(parts.join(" ")); + } +} diff --git a/src/prompt_parts/icon.rs b/src/prompt_parts/icon.rs new file mode 100644 index 0000000..76cc822 --- /dev/null +++ b/src/prompt_parts/icon.rs @@ -0,0 +1,20 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::colors::{color, Color}; + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct Icon { + pub color: Option, + pub icon: String, +} + +pub trait RenderableIcon { + fn render(&self) -> String; +} + +impl RenderableIcon for Icon { + fn render(&self) -> String { + return format!("{}{}", color(self.color), self.icon); + } +} diff --git a/src/prompt_parts/origin_icon.rs b/src/prompt_parts/origin_icon.rs new file mode 100644 index 0000000..55952eb --- /dev/null +++ b/src/prompt_parts/origin_icon.rs @@ -0,0 +1,30 @@ +use std::collections::HashMap; + +use crate::prompt_parts::icon::{Icon, RenderableIcon}; +use crate::prompt_parts::prompt_part::RenderablePromptPart; +use git2::Repository; +use regex::Regex; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct OriginIcon { + icons: HashMap, + default_icon: Icon, +} + +impl RenderablePromptPart for OriginIcon { + fn render(self, repo: &Repository) -> Option { + let host_regex = Regex::new(r"^(.*@)?(?[^:\/]*)").ok()?; + let origin = repo.find_remote("origin").ok()?; + let url = origin.url()?; + let captures = host_regex.captures(url)?; + let host = captures.name("host")?.as_str(); + let icon = match self.icons.get(host) { + Some(i) => i, + None => &self.default_icon, + }; + return Some(icon.render()); + } +} diff --git a/src/prompt_parts/prompt_part.rs b/src/prompt_parts/prompt_part.rs new file mode 100644 index 0000000..12ebca8 --- /dev/null +++ b/src/prompt_parts/prompt_part.rs @@ -0,0 +1,5 @@ +use git2::Repository; + +pub trait RenderablePromptPart { + fn render(self, repo: &Repository) -> Option; +}