diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/main.rs | 183 |
1 files changed, 183 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..0b1ec1b --- /dev/null +++ b/src/main.rs | |||
@@ -0,0 +1,183 @@ | |||
1 | use std::{env, fmt, path::Path}; | ||
2 | |||
3 | use git2::{Oid, Repository, Status}; | ||
4 | use tico::tico; | ||
5 | |||
6 | fn main() { | ||
7 | let args = env::args().collect::<Vec<_>>(); | ||
8 | match args | ||
9 | .iter() | ||
10 | .map(String::as_str) | ||
11 | .collect::<Vec<&str>>() | ||
12 | .as_slice() | ||
13 | { | ||
14 | [_, "cwd", target] => print!("{}", cwd(target)), | ||
15 | [_, "vcs", target] => { | ||
16 | if let Some(status) = vcs(target) { | ||
17 | print!("{}", status) | ||
18 | } | ||
19 | } | ||
20 | _ => (), | ||
21 | } | ||
22 | } | ||
23 | |||
24 | fn cwd(target: &str) -> String { | ||
25 | let home = env::var("HOME").unwrap(); | ||
26 | |||
27 | let home_dir_ext = format!("{}{}", home, "/"); | ||
28 | if target == home.as_str() || target.starts_with(&home_dir_ext) { | ||
29 | let replaced = target.replacen(home.as_str(), "~", 1); | ||
30 | tico(&replaced) | ||
31 | } else { | ||
32 | tico(&target) | ||
33 | } | ||
34 | } | ||
35 | |||
36 | struct VcsStatus { | ||
37 | branch: Branch, | ||
38 | dist: Dist, | ||
39 | status: StatusSummary, | ||
40 | } | ||
41 | |||
42 | impl fmt::Display for VcsStatus { | ||
43 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
44 | write!( | ||
45 | f, | ||
46 | "{}{}{}#[fg=colour7]", | ||
47 | self.branch, self.dist, self.status | ||
48 | ) | ||
49 | } | ||
50 | } | ||
51 | |||
52 | enum Branch { | ||
53 | Id(Oid), | ||
54 | Ref(String), | ||
55 | Unknown, | ||
56 | } | ||
57 | |||
58 | impl fmt::Display for Branch { | ||
59 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
60 | match self { | ||
61 | Branch::Id(id) => write!(f, "#[fg=colour3]{:.7} ", id), | ||
62 | Branch::Ref(s) => write!(f, "#[fg=colour8]{} ", s), | ||
63 | Branch::Unknown => write!(f, ""), | ||
64 | } | ||
65 | } | ||
66 | } | ||
67 | |||
68 | enum Dist { | ||
69 | Ahead, | ||
70 | Behind, | ||
71 | Both, | ||
72 | Neither, | ||
73 | } | ||
74 | |||
75 | impl fmt::Display for Dist { | ||
76 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
77 | write!( | ||
78 | f, | ||
79 | "#[fg=colour8]{}", | ||
80 | match self { | ||
81 | Self::Ahead => "↑ ", | ||
82 | Self::Behind => "↓ ", | ||
83 | Self::Both => "↑↓ ", | ||
84 | Self::Neither => "", | ||
85 | } | ||
86 | ) | ||
87 | } | ||
88 | } | ||
89 | |||
90 | enum StatusSummary { | ||
91 | WtModified, | ||
92 | IdxModified, | ||
93 | Conflict, | ||
94 | Clean, | ||
95 | } | ||
96 | |||
97 | impl fmt::Display for StatusSummary { | ||
98 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
99 | write!( | ||
100 | f, | ||
101 | "{}", | ||
102 | match self { | ||
103 | Self::WtModified => "#[fg=colour1]×", | ||
104 | Self::IdxModified => "#[fg=colour3]±", | ||
105 | Self::Conflict => "#[fg=colour5]!", | ||
106 | Self::Clean => "#[fg=colour2]·", | ||
107 | } | ||
108 | ) | ||
109 | } | ||
110 | } | ||
111 | |||
112 | fn vcs(target: &str) -> Option<VcsStatus> { | ||
113 | let repo = match Path::new(target) | ||
114 | .ancestors() | ||
115 | .map(Repository::open) | ||
116 | .find_map(|r| r.ok()) | ||
117 | { | ||
118 | Some(r) => r, | ||
119 | None => return None, | ||
120 | }; | ||
121 | |||
122 | let dist = match get_ahead_behind(&repo) { | ||
123 | Some((ahead, behind)) if ahead > 0 && behind > 0 => Dist::Both, | ||
124 | Some((ahead, _)) if ahead > 0 => Dist::Ahead, | ||
125 | Some((_, behind)) if behind > 0 => Dist::Behind, | ||
126 | _ => Dist::Neither, | ||
127 | }; | ||
128 | |||
129 | let branch = match repo.head() { | ||
130 | Ok(reference) if reference.is_branch() => { | ||
131 | Branch::Ref(reference.shorthand().unwrap().to_string()) | ||
132 | } | ||
133 | Ok(reference) => Branch::Id(reference.peel_to_commit().unwrap().id()), | ||
134 | _ => Branch::Unknown, | ||
135 | }; | ||
136 | |||
137 | let status = repo_status(&repo); | ||
138 | |||
139 | Some(VcsStatus { | ||
140 | branch, | ||
141 | dist, | ||
142 | status, | ||
143 | }) | ||
144 | } | ||
145 | |||
146 | fn repo_status(repo: &Repository) -> StatusSummary { | ||
147 | for file in repo.statuses(None).unwrap().iter() { | ||
148 | match file.status() { | ||
149 | // STATE: conflicted | ||
150 | Status::CONFLICTED => return StatusSummary::Conflict, | ||
151 | // STATE: unstaged (working tree modified) | ||
152 | Status::WT_NEW | ||
153 | | Status::WT_MODIFIED | ||
154 | | Status::WT_DELETED | ||
155 | | Status::WT_TYPECHANGE | ||
156 | | Status::WT_RENAMED => return StatusSummary::WtModified, | ||
157 | // STATE: staged (changes added to index) | ||
158 | Status::INDEX_NEW | ||
159 | | Status::INDEX_MODIFIED | ||
160 | | Status::INDEX_DELETED | ||
161 | | Status::INDEX_TYPECHANGE | ||
162 | | Status::INDEX_RENAMED => return StatusSummary::IdxModified, | ||
163 | // STATE: committed (changes have been saved in the repo) | ||
164 | _ => return StatusSummary::Clean, | ||
165 | } | ||
166 | } | ||
167 | StatusSummary::Clean | ||
168 | } | ||
169 | |||
170 | fn get_ahead_behind(r: &Repository) -> Option<(usize, usize)> { | ||
171 | let head = (r.head().ok())?; | ||
172 | if !head.is_branch() { | ||
173 | return None; | ||
174 | } | ||
175 | |||
176 | let head_name = head.shorthand()?; | ||
177 | let head_branch = r.find_branch(head_name, git2::BranchType::Local).ok()?; | ||
178 | let upstream = head_branch.upstream().ok()?; | ||
179 | let head_oid = head.target()?; | ||
180 | let upstream_oid = upstream.get().target()?; | ||
181 | |||
182 | r.graph_ahead_behind(head_oid, upstream_oid).ok() | ||
183 | } | ||