par_term/status_bar/
git_poller.rs1use parking_lot::Mutex;
8use std::process::Command;
9use std::sync::Arc;
10use std::sync::atomic::{AtomicBool, Ordering};
11use std::time::{Duration, Instant};
12
13#[derive(Debug, Clone, Default)]
15pub struct GitStatus {
16 pub branch: Option<String>,
18 pub ahead: u32,
20 pub behind: u32,
22 pub dirty: bool,
24}
25
26pub(super) struct GitBranchPoller {
28 pub(super) status: Arc<Mutex<GitStatus>>,
30 cwd: Arc<Mutex<Option<String>>>,
32 running: Arc<AtomicBool>,
34 thread: Mutex<Option<std::thread::JoinHandle<()>>>,
36}
37
38impl GitBranchPoller {
39 pub(super) fn new() -> Self {
40 Self {
41 status: Arc::new(Mutex::new(GitStatus::default())),
42 cwd: Arc::new(Mutex::new(None)),
43 running: Arc::new(AtomicBool::new(false)),
44 thread: Mutex::new(None),
45 }
46 }
47
48 pub(super) fn start(&self, poll_interval_secs: f32) {
50 if self
51 .running
52 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
53 .is_err()
54 {
55 return;
56 }
57
58 let status = Arc::clone(&self.status);
59 let cwd = Arc::clone(&self.cwd);
60 let running = Arc::clone(&self.running);
61 let interval = Duration::from_secs_f32(poll_interval_secs.max(1.0));
62
63 let handle = std::thread::Builder::new()
64 .name("status-bar-git".into())
65 .spawn(move || {
66 while running.load(Ordering::SeqCst) {
67 let dir = cwd.lock().clone();
68 let result = dir.map(|d| poll_git_status(&d)).unwrap_or_default();
69 *status.lock() = result;
70 let deadline = Instant::now() + interval;
72 while Instant::now() < deadline && running.load(Ordering::Relaxed) {
73 std::thread::sleep(Duration::from_millis(50));
74 }
75 }
76 });
77
78 match handle {
79 Ok(h) => *self.thread.lock() = Some(h),
80 Err(e) => {
81 self.running.store(false, Ordering::SeqCst);
85 crate::debug_error!(
86 "SESSION_LOGGER",
87 "failed to spawn git branch poller thread: {:?}",
88 e
89 );
90 }
91 }
92 }
93
94 pub(super) fn signal_stop(&self) {
96 self.running.store(false, Ordering::SeqCst);
97 }
98
99 pub(super) fn stop(&self) {
101 self.signal_stop();
102 if let Some(handle) = self.thread.lock().take() {
103 let _ = handle.join();
104 }
105 }
106
107 pub(super) fn set_cwd(&self, new_cwd: Option<&str>) {
109 *self.cwd.lock() = new_cwd.map(String::from);
110 }
111
112 pub(super) fn status(&self) -> GitStatus {
114 self.status.lock().clone()
115 }
116
117 pub(super) fn is_running(&self) -> bool {
118 self.running.load(Ordering::SeqCst)
119 }
120}
121
122impl Drop for GitBranchPoller {
123 fn drop(&mut self) {
124 self.stop();
125 }
126}
127
128pub(super) fn poll_git_status(dir: &str) -> GitStatus {
130 let branch = Command::new("git")
132 .args(["rev-parse", "--abbrev-ref", "HEAD"])
133 .current_dir(dir)
134 .output()
135 .ok()
136 .and_then(|out| {
137 if out.status.success() {
138 let b = String::from_utf8_lossy(&out.stdout).trim().to_string();
139 if b.is_empty() { None } else { Some(b) }
140 } else {
141 None
142 }
143 });
144
145 if branch.is_none() {
146 return GitStatus::default();
147 }
148
149 let (ahead, behind) = Command::new("git")
151 .args(["rev-list", "--left-right", "--count", "HEAD...@{upstream}"])
152 .current_dir(dir)
153 .output()
154 .ok()
155 .and_then(|out| {
156 if out.status.success() {
157 let text = String::from_utf8_lossy(&out.stdout);
158 let parts: Vec<&str> = text.trim().split('\t').collect();
159 if parts.len() == 2 {
160 let a = parts[0].parse::<u32>().unwrap_or(0);
161 let b = parts[1].parse::<u32>().unwrap_or(0);
162 Some((a, b))
163 } else {
164 None
165 }
166 } else {
167 None
169 }
170 })
171 .unwrap_or((0, 0));
172
173 let dirty = Command::new("git")
175 .args(["status", "--porcelain", "-uno"])
176 .current_dir(dir)
177 .output()
178 .ok()
179 .is_some_and(|out| out.status.success() && !out.stdout.is_empty());
180
181 GitStatus {
182 branch,
183 ahead,
184 behind,
185 dirty,
186 }
187}