This commit is contained in:
MasterGordon 2024-08-10 12:39:49 +02:00
parent 7dae789ccc
commit 2d6f4c5c58
12 changed files with 678 additions and 8 deletions

174
Cargo.lock generated
View File

@ -2,6 +2,15 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 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]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.6.0" version = "2.6.0"
@ -18,6 +27,23 @@ dependencies = [
"libc", "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]] [[package]]
name = "form_urlencoded" name = "form_urlencoded"
version = "1.2.1" version = "1.2.1"
@ -52,6 +78,12 @@ dependencies = [
"unicode-normalization", "unicode-normalization",
] ]
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]] [[package]]
name = "jobserver" name = "jobserver"
version = "0.1.32" version = "0.1.32"
@ -113,6 +145,12 @@ version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]] [[package]]
name = "openssl-probe" name = "openssl-probe"
version = "0.1.5" version = "0.1.5"
@ -144,10 +182,134 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
[[package]] [[package]]
name = "rs-prompt" name = "proc-macro2"
version = "0.1.0" version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [ 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]] [[package]]
@ -171,6 +333,12 @@ version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]] [[package]]
name = "unicode-normalization" name = "unicode-normalization"
version = "0.1.23" version = "0.1.23"

View File

@ -1,7 +1,11 @@
[package] [package]
name = "rs-prompt" name = "fast-git-prompt"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
git2 = "0.19.0" git2 = "0.19.0"
serde_json = "1.0.122"
serde = { version = "1.0.205", features = ["derive"] }
regex = "1.10.6"
schemars = "0.8.21"

139
readme.md Normal file
View File

@ -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

59
src/colors.rs Normal file
View File

@ -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<Color>) -> 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),
}
}
}

View File

@ -1,11 +1,143 @@
use std::env;
use std::path::Path;
use std::process::exit;
use git2::Repository; 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<String>,
#[serde(rename = "version-do-not-modify")]
version: Option<String>,
base_color: Option<Color>,
prompt: Vec<PromptPart>,
}
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() { fn main() {
setup();
let mut prompt: Vec<String> = vec![];
let config = read_config();
// let repo = match Repository::open(".") {
let repo = match Repository::open(".") { let repo = match Repository::open(".") {
Ok(repo) => repo, Ok(repo) => repo,
Err(e) => panic!("failed to init: {}", e), Err(_) => {
exit(0);
}
}; };
let head = repo.head().unwrap(); for part in config.prompt {
let name = head.name().unwrap(); match part {
println!("Hello, world! {}", name); 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())
);
} }

6
src/prompt_parts.rs Normal file
View File

@ -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;

View File

@ -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<String>,
}
impl RenderablePromptPart for BranchName {
fn render(self, repo: &Repository) -> Option<String> {
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));
}
}

View File

@ -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<String> {
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(" "));
}
}

View File

@ -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<String> {
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(" "));
}
}

20
src/prompt_parts/icon.rs Normal file
View File

@ -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<Color>,
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);
}
}

View File

@ -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<String, Icon>,
default_icon: Icon,
}
impl RenderablePromptPart for OriginIcon {
fn render(self, repo: &Repository) -> Option<String> {
let host_regex = Regex::new(r"^(.*@)?(?<host>[^:\/]*)").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());
}
}

View File

@ -0,0 +1,5 @@
use git2::Repository;
pub trait RenderablePromptPart {
fn render(self, repo: &Repository) -> Option<String>;
}