diff options
author | Jesse Luehrs <doy@tozt.net> | 2018-02-20 01:48:17 -0500 |
---|---|---|
committer | Jesse Luehrs <doy@tozt.net> | 2018-02-20 03:30:54 -0500 |
commit | 83f89188161b38046b39124ffb45bbbd6fcb572f (patch) | |
tree | 05a68afe3ad0394b3383adec01e98225b0ba92d4 /src | |
parent | 0820849c2b6793f035bfcf7d5cd22dfbe9b45c25 (diff) | |
download | fancy-prompt-83f89188161b38046b39124ffb45bbbd6fcb572f.tar.gz fancy-prompt-83f89188161b38046b39124ffb45bbbd6fcb572f.zip |
add vcs support
Diffstat (limited to 'src')
-rw-r--r-- | src/main.rs | 3 | ||||
-rw-r--r-- | src/prompt.rs | 115 | ||||
-rw-r--r-- | src/system_info.rs | 5 | ||||
-rw-r--r-- | src/vcs/git.rs | 176 | ||||
-rw-r--r-- | src/vcs/mod.rs | 35 |
5 files changed, 328 insertions, 6 deletions
diff --git a/src/main.rs b/src/main.rs index 471a3a2..85b4627 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ extern crate chrono; extern crate clap; +extern crate git2; extern crate hostname; extern crate regex; extern crate term; @@ -11,6 +12,7 @@ mod colors; mod power; mod prompt; mod system_info; +mod vcs; fn collect_data() -> prompt::PromptData { let matches = clap::App::new("fancy-prompt") @@ -45,6 +47,7 @@ fn collect_data() -> prompt::PromptData { is_root: system_info::is_root(), time: system_info::time(), power_info: system_info::power_info(), + vcs_info: system_info::vcs_info(), } } diff --git a/src/prompt.rs b/src/prompt.rs index 2679bc9..12a7ee5 100644 --- a/src/prompt.rs +++ b/src/prompt.rs @@ -2,15 +2,17 @@ use chrono; use regex; use std; +use std::fmt::Write; + use colors; use power; +use vcs; pub struct Prompt { colors: colors::Colors, data: PromptData, } -#[derive(Debug)] pub struct PromptData { pub shell: colors::ShellType, pub error_code: u8, @@ -22,6 +24,7 @@ pub struct PromptData { pub is_root: bool, pub time: chrono::DateTime<chrono::Local>, pub power_info: power::PowerInfo, + pub vcs_info: Option<Box<vcs::VcsInfo>>, } impl Prompt { @@ -36,13 +39,22 @@ impl Prompt { let user = self.data.user.clone().unwrap_or(String::from("???")); let host = self.data.hostname.clone().unwrap_or(String::from("???")); + let max_vcs_len = 20; // "g*+?:mybr...nch:+1-1" + let (vcs, vcs_err) = self.format_vcs(); + let vcs = vcs.map(|vcs| { + compress_vcs(&vcs, max_vcs_len) + }); + let battery_len = 10; let cols = self.data.terminal_cols.unwrap_or(80); - // " (~/a/...cde) ---- {--<=======} doy@lance [19:40:50] " + // " (~/a/...cde|g*+?:mybr:+1-1) -- {--<=======} doy@lance [19:40:50] " let max_path_len = cols - 1 // " " - - 2 // "()" + - vcs + .as_ref() + .map(|vcs| vcs.len() + 1) // "|g*+?:mybr:+1-1" + .unwrap_or(0) - 2 // "()" - 1 // " " - 1 // "-" - 1 // " " @@ -65,9 +77,10 @@ impl Prompt { &self.data.home, max_path_len ); + let path_err = false; // XXX self.colors.pad(1); - self.display_path(&path); + self.display_path(&path, path_err, &vcs, vcs_err); self.colors.pad(1); self.display_border(max_path_len - path.len() + 1); @@ -91,9 +104,19 @@ impl Prompt { self.colors.pad(1); } - fn display_path(&self, path: &str) { + fn display_path( + &self, + path: &str, + path_err: bool, + vcs: &Option<String>, + vcs_err: bool + ) { self.colors.print_host(&self.data.hostname, "("); - self.colors.print("default", path); + self.colors.print(if path_err { "error" } else { "default" }, path); + if let &Some(ref vcs) = vcs { + self.colors.print_host(&self.data.hostname, "|"); + self.colors.print(if vcs_err { "error" } else { "default" }, &vcs); + } self.colors.print_host(&self.data.hostname, ")"); } @@ -159,6 +182,64 @@ impl Prompt { let prompt = if self.data.is_root { "#" } else { "$" }; self.colors.print_user(&self.data.user, prompt); } + + fn format_vcs(&self) -> (Option<String>, bool) { + (self.data.vcs_info.as_ref().map(|vcs_info| { + let mut vcs = String::new(); + + write!(vcs, "{}", vcs_id(vcs_info.vcs())).unwrap(); + + if vcs_info.has_modified_files() { + write!(vcs, "*").unwrap(); + } + if vcs_info.has_staged_files() { + write!(vcs, "+").unwrap(); + } + if vcs_info.has_new_files() { + write!(vcs, "?").unwrap(); + } + if !vcs_info.has_commits() { + write!(vcs, "!").unwrap(); + } + + let branch = vcs_info.branch().map(|branch| { + if branch == "master" { + String::new() + } + else { + branch + } + }).unwrap_or(String::from("???")); + if branch != "" { + write!(vcs, ":").unwrap(); + } + write!(vcs, "{}", branch).unwrap(); + + if let Some((local, remote)) = vcs_info.remote_branch_diff() { + if local > 0 || remote > 0 { + write!(vcs, ":").unwrap(); + } + if local > 0 { + write!(vcs, "+{}", local).unwrap(); + } + if remote > 0 { + write!(vcs, "-{}", remote).unwrap(); + } + } + else { + write!(vcs, ":-").unwrap(); + } + + match vcs_info.active_operation() { + vcs::ActiveOperation::None => {}, + op => { + write!(vcs, "({})", active_operation_id(op)).unwrap(); + } + } + + vcs + }), false) // XXX + } } fn battery_discharge_color(usage: f64, charging: bool) -> &'static str { @@ -226,3 +307,25 @@ fn compress_path<T, U>( String::from("???") } } + +fn compress_vcs(vcs: &str, _len: usize) -> String { + // XXX + String::from(vcs) +} + +fn vcs_id(vcs: vcs::VcsType) -> String { + match vcs { + vcs::VcsType::Git => String::from("g"), + } +} + +fn active_operation_id(op: vcs::ActiveOperation) -> String { + match op { + vcs::ActiveOperation::None => String::new(), + vcs::ActiveOperation::Merge => String::from("m"), + vcs::ActiveOperation::Revert => String::from("v"), + vcs::ActiveOperation::CherryPick => String::from("c"), + vcs::ActiveOperation::Bisect => String::from("b"), + vcs::ActiveOperation::Rebase => String::from("r"), + } +} diff --git a/src/system_info.rs b/src/system_info.rs index 2079ea7..47ed0f9 100644 --- a/src/system_info.rs +++ b/src/system_info.rs @@ -5,6 +5,7 @@ use std; use users; use power; +use vcs; pub fn hostname() -> Option<String> { hostname::get_hostname() @@ -44,3 +45,7 @@ pub fn time() -> chrono::DateTime<chrono::Local> { pub fn power_info() -> power::PowerInfo { power::PowerInfo::new() } + +pub fn vcs_info() -> Option<Box<vcs::VcsInfo>> { + vcs::detect() +} diff --git a/src/vcs/git.rs b/src/vcs/git.rs new file mode 100644 index 0000000..749d498 --- /dev/null +++ b/src/vcs/git.rs @@ -0,0 +1,176 @@ +use git2; +use std; + +use std::fmt::Write; + +#[derive(Debug)] +pub struct GitInfo { + modified_files: bool, + staged_files: bool, + new_files: bool, + commits: bool, + active_operation: super::ActiveOperation, + branch: Option<String>, + remote_branch_diff: Option<(usize, usize)>, +} + +impl GitInfo { + pub fn new(git: git2::Repository) -> GitInfo { + let mut modified_statuses = git2::Status::empty(); + modified_statuses.insert(git2::STATUS_WT_DELETED); + modified_statuses.insert(git2::STATUS_WT_MODIFIED); + modified_statuses.insert(git2::STATUS_WT_RENAMED); + modified_statuses.insert(git2::STATUS_WT_TYPECHANGE); + let mut staged_statuses = git2::Status::empty(); + staged_statuses.insert(git2::STATUS_INDEX_DELETED); + staged_statuses.insert(git2::STATUS_INDEX_MODIFIED); + staged_statuses.insert(git2::STATUS_INDEX_NEW); + staged_statuses.insert(git2::STATUS_INDEX_RENAMED); + staged_statuses.insert(git2::STATUS_INDEX_TYPECHANGE); + let mut new_statuses = git2::Status::empty(); + new_statuses.insert(git2::STATUS_WT_NEW); + + let mut status_options = git2::StatusOptions::new(); + status_options.include_untracked(true); + if true { // XXX + status_options.update_index(true); + } + else { + status_options.update_index(false); + status_options.no_refresh(true); + } + let status = git.statuses(Some(&mut status_options)); + let mut modified_files = false; + let mut staged_files = false; + let mut new_files = false; + for status in status.iter() { + for file in status.iter() { + if file.status().intersects(modified_statuses) { + modified_files = true; + } + if file.status().intersects(staged_statuses) { + staged_files = true; + } + if file.status().intersects(new_statuses) { + new_files = true; + } + } + } + + let head = git.head(); + let commits = head.is_ok(); + let branch = head.ok() + .and_then(|head| { + if head.is_branch() { + head.shorthand().map(|s| s.to_string()) + } + else { + head.resolve().ok() + .and_then(|head| head.target()) + .map(|oid| { + let mut sha = String::new(); + for b in oid.as_bytes().iter() { + write!(sha, "{:02x}", b).unwrap(); + } + sha.truncate(7); + sha + }) + } + }); + + let active_operation = match git.state() { + git2::RepositoryState::Merge + => super::ActiveOperation::Merge, + git2::RepositoryState::Revert + | git2::RepositoryState::RevertSequence + => super::ActiveOperation::Revert, + git2::RepositoryState::CherryPick + | git2::RepositoryState::CherryPickSequence + => super::ActiveOperation::CherryPick, + git2::RepositoryState::Bisect + => super::ActiveOperation::Bisect, + git2::RepositoryState::Rebase + | git2::RepositoryState::RebaseInteractive + | git2::RepositoryState::RebaseMerge + => super::ActiveOperation::Rebase, + _ => super::ActiveOperation::None, + }; + + let remote_branch_diff = git.head().ok() + .and_then(|head| if head.is_branch() { Some(head) } else { None }) + .and_then(|head| { + head.resolve().ok() + }) + .map(|head| { + (head.target(), head.shorthand().map(|s| s.to_string())) + }) + .and_then(|(head_id, name)| { + head_id.and_then(|head_id| { + name.and_then(|name| { + git.refname_to_id( + &(String::from("refs/remotes/origin/") + &name) + ).ok().and_then(|remote_id| { + git.graph_ahead_behind(head_id, remote_id).ok() + }) + }) + }) + }); + + GitInfo { + modified_files: modified_files, + staged_files: staged_files, + new_files: new_files, + commits: commits, + active_operation: active_operation, + branch: branch, + remote_branch_diff: remote_branch_diff, + } + } +} + +impl super::VcsInfo for GitInfo { + fn vcs(&self) -> super::VcsType { + super::VcsType::Git + } + + fn has_modified_files(&self) -> bool { + self.modified_files + } + + fn has_staged_files(&self) -> bool { + self.staged_files + } + + fn has_new_files(&self) -> bool { + self.new_files + } + + fn has_commits(&self) -> bool { + self.commits + } + + fn active_operation(&self) -> super::ActiveOperation { + self.active_operation + } + + fn branch(&self) -> Option<String> { + self.branch.clone() + } + + fn remote_branch_diff(&self) -> Option<(usize, usize)> { + self.remote_branch_diff + } +} + +pub fn detect() -> Option<Box<super::VcsInfo>> { + let git = std::env::current_dir().ok().and_then(|pwd| { + git2::Repository::discover(pwd).ok() + }); + + if let Some(git) = git { + Some(Box::new(GitInfo::new(git))) + } + else { + None + } +} diff --git a/src/vcs/mod.rs b/src/vcs/mod.rs new file mode 100644 index 0000000..02c2eb7 --- /dev/null +++ b/src/vcs/mod.rs @@ -0,0 +1,35 @@ +mod git; + +pub enum VcsType { + Git, +} + +#[derive(Debug,Copy,Clone)] +pub enum ActiveOperation { + None, + Merge, + Revert, + CherryPick, + Bisect, + Rebase, +} + +pub trait VcsInfo { + fn vcs(&self) -> VcsType; + fn has_modified_files(&self) -> bool; + fn has_staged_files(&self) -> bool; + fn has_new_files(&self) -> bool; + fn has_commits(&self) -> bool; + fn active_operation(&self) -> ActiveOperation; + fn branch(&self) -> Option<String>; + fn remote_branch_diff(&self) -> Option<(usize, usize)>; +} + +pub fn detect() -> Option<Box<VcsInfo>> { + if let Some(git) = git::detect() { + Some(git) + } + else { + None + } +} |