aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorJesse Luehrs <doy@tozt.net>2018-02-20 01:48:17 -0500
committerJesse Luehrs <doy@tozt.net>2018-02-20 03:30:54 -0500
commit83f89188161b38046b39124ffb45bbbd6fcb572f (patch)
tree05a68afe3ad0394b3383adec01e98225b0ba92d4 /src
parent0820849c2b6793f035bfcf7d5cd22dfbe9b45c25 (diff)
downloadfancy-prompt-83f89188161b38046b39124ffb45bbbd6fcb572f.tar.gz
fancy-prompt-83f89188161b38046b39124ffb45bbbd6fcb572f.zip
add vcs support
Diffstat (limited to 'src')
-rw-r--r--src/main.rs3
-rw-r--r--src/prompt.rs115
-rw-r--r--src/system_info.rs5
-rw-r--r--src/vcs/git.rs176
-rw-r--r--src/vcs/mod.rs35
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
+ }
+}