notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

git_status_ui.rs (5240B)


      1 use crate::git_status::GitStatusCache;
      2 use egui::{Color32, RichText, Ui};
      3 
      4 const MODIFIED_COLOR: Color32 = Color32::from_rgb(200, 170, 50);
      5 const ADDED_COLOR: Color32 = Color32::from_rgb(60, 180, 60);
      6 const DELETED_COLOR: Color32 = Color32::from_rgb(200, 60, 60);
      7 const UNTRACKED_COLOR: Color32 = Color32::from_rgb(128, 128, 128);
      8 
      9 /// Snapshot of git status data extracted from the cache to avoid
     10 /// borrow conflicts when mutating `cache.expanded`.
     11 pub struct StatusSnapshot {
     12     branch: Option<String>,
     13     modified: usize,
     14     added: usize,
     15     deleted: usize,
     16     untracked: usize,
     17     is_clean: bool,
     18     files: Vec<(String, String)>, // (status, path)
     19 }
     20 
     21 impl StatusSnapshot {
     22     pub fn from_cache(cache: &GitStatusCache) -> Option<Result<Self, ()>> {
     23         match cache.current() {
     24             Some(Ok(data)) => Some(Ok(StatusSnapshot {
     25                 branch: data.branch.clone(),
     26                 modified: data.modified_count(),
     27                 added: data.added_count(),
     28                 deleted: data.deleted_count(),
     29                 untracked: data.untracked_count(),
     30                 is_clean: data.is_clean(),
     31                 files: data
     32                     .files
     33                     .iter()
     34                     .map(|f| (f.status.clone(), f.path.clone()))
     35                     .collect(),
     36             })),
     37             Some(Err(_)) => Some(Err(())),
     38             None => None,
     39         }
     40     }
     41 }
     42 
     43 fn count_label(ui: &mut Ui, prefix: &str, count: usize, color: Color32) {
     44     if count > 0 {
     45         ui.label(
     46             RichText::new(format!("{}{}", prefix, count))
     47                 .color(color)
     48                 .monospace()
     49                 .size(11.0),
     50         );
     51     }
     52 }
     53 
     54 fn status_color(status: &str) -> Color32 {
     55     if status.starts_with('?') {
     56         UNTRACKED_COLOR
     57     } else if status.contains('D') {
     58         DELETED_COLOR
     59     } else if status.contains('A') {
     60         ADDED_COLOR
     61     } else {
     62         MODIFIED_COLOR
     63     }
     64 }
     65 
     66 /// Render the left-side git status content (expand arrow, branch, counts).
     67 pub fn git_status_content_ui(
     68     cache: &mut GitStatusCache,
     69     snapshot: &Option<Result<StatusSnapshot, ()>>,
     70     ui: &mut Ui,
     71 ) {
     72     match snapshot {
     73         Some(Ok(snap)) => {
     74             // Show expand arrow only when dirty
     75             if !snap.is_clean {
     76                 let arrow = if cache.expanded {
     77                     "\u{25BC}"
     78                 } else {
     79                     "\u{25B6}"
     80                 };
     81                 if ui
     82                     .add(
     83                         egui::Label::new(RichText::new(arrow).weak().monospace().size(9.0))
     84                             .sense(egui::Sense::click()),
     85                     )
     86                     .clicked()
     87                 {
     88                     cache.expanded = !cache.expanded;
     89                 }
     90             }
     91 
     92             // Branch name
     93             let branch_text = snap.branch.as_deref().unwrap_or("detached");
     94             ui.label(RichText::new(branch_text).weak().monospace().size(11.0));
     95 
     96             if snap.is_clean {
     97                 ui.label(RichText::new("clean").weak().size(11.0));
     98             } else {
     99                 count_label(ui, "~", snap.modified, MODIFIED_COLOR);
    100                 count_label(ui, "+", snap.added, ADDED_COLOR);
    101                 count_label(ui, "-", snap.deleted, DELETED_COLOR);
    102                 count_label(ui, "?", snap.untracked, UNTRACKED_COLOR);
    103             }
    104         }
    105         Some(Err(_)) => {
    106             ui.label(RichText::new("git: not available").weak().size(11.0));
    107         }
    108         None => {
    109             ui.spinner();
    110             ui.label(RichText::new("checking git...").weak().size(11.0));
    111         }
    112     }
    113 }
    114 
    115 /// Render the expanded file list portion of git status.
    116 pub fn git_expanded_files_ui(
    117     cache: &GitStatusCache,
    118     snapshot: &Option<Result<StatusSnapshot, ()>>,
    119     ui: &mut Ui,
    120 ) {
    121     if cache.expanded {
    122         if let Some(Ok(snap)) = snapshot {
    123             if !snap.files.is_empty() {
    124                 ui.add_space(4.0);
    125 
    126                 egui::Frame::new()
    127                     .fill(ui.visuals().extreme_bg_color)
    128                     .inner_margin(egui::Margin::symmetric(8, 4))
    129                     .corner_radius(4.0)
    130                     .show(ui, |ui| {
    131                         egui::ScrollArea::vertical()
    132                             .max_height(150.0)
    133                             .show(ui, |ui| {
    134                                 for (status, path) in &snap.files {
    135                                     ui.horizontal(|ui| {
    136                                         ui.spacing_mut().item_spacing.x = 8.0;
    137                                         let color = status_color(status);
    138                                         ui.label(
    139                                             RichText::new(status)
    140                                                 .monospace()
    141                                                 .size(11.0)
    142                                                 .color(color),
    143                                         );
    144                                         ui.label(RichText::new(path).monospace().size(11.0).weak());
    145                                     });
    146                                 }
    147                             });
    148                     });
    149             }
    150         }
    151     }
    152 }