use super::state::{Divergence, MainState, OperationState, WorktreeState};
#[derive(Debug, Clone, Copy, Default)]
pub struct PositionMask {
widths: [usize; 7],
}
impl PositionMask {
pub(crate) const STAGED: usize = 0; pub(crate) const MODIFIED: usize = 1; pub(crate) const UNTRACKED: usize = 2; pub(crate) const WORKTREE_STATE: usize = 3; pub(crate) const MAIN_STATE: usize = 4; pub(crate) const UPSTREAM_DIVERGENCE: usize = 5; pub(crate) const USER_MARKER: usize = 6;
pub const FULL: Self = Self {
widths: [
1, 1, 1, 1, 1, 1, 2, ],
};
pub(crate) fn width(&self, pos: usize) -> usize {
self.widths[pos]
}
}
#[derive(Debug, Clone, Copy, Default, serde::Serialize)]
pub struct WorkingTreeStatus {
pub staged: bool,
pub modified: bool,
pub untracked: bool,
pub renamed: bool,
pub deleted: bool,
}
impl WorkingTreeStatus {
pub fn new(
staged: bool,
modified: bool,
untracked: bool,
renamed: bool,
deleted: bool,
) -> Self {
Self {
staged,
modified,
untracked,
renamed,
deleted,
}
}
pub fn is_dirty(&self) -> bool {
self.staged || self.modified || self.untracked || self.renamed || self.deleted
}
pub fn to_symbols(self) -> String {
let mut s = String::with_capacity(5);
if self.staged {
s.push('+');
}
if self.modified {
s.push('!');
}
if self.untracked {
s.push('?');
}
if self.renamed {
s.push('»');
}
if self.deleted {
s.push('✘');
}
s
}
}
#[derive(Debug, Clone, Default)]
pub struct StatusSymbols {
pub(crate) main_state: MainState,
pub(crate) operation_state: OperationState,
pub(crate) worktree_state: WorktreeState,
pub(crate) upstream_divergence: Divergence,
pub(crate) working_tree: WorkingTreeStatus,
pub(crate) user_marker: Option<String>,
}
impl StatusSymbols {
pub fn render_with_mask(&self, mask: &PositionMask) -> String {
use worktrunk::styling::StyledLine;
let mut result = String::with_capacity(64);
if self.is_empty() {
return result;
}
for (pos, styled_content, has_data) in self.styled_symbols() {
let allocated_width = mask.width(pos);
if has_data {
let mut segment = StyledLine::new();
segment.push_raw(styled_content);
segment.pad_to(allocated_width);
result.push_str(&segment.render());
} else {
for _ in 0..allocated_width {
result.push(' ');
}
}
}
result
}
pub fn is_empty(&self) -> bool {
self.main_state == MainState::None
&& self.operation_state == OperationState::None
&& self.worktree_state == WorktreeState::None
&& self.upstream_divergence == Divergence::None
&& !self.working_tree.is_dirty()
&& self.user_marker.is_none()
}
pub fn format_compact(&self) -> String {
self.styled_symbols()
.into_iter()
.filter_map(|(_, styled, has_data)| has_data.then_some(styled))
.collect()
}
pub(crate) fn styled_symbols(&self) -> [(usize, String, bool); 7] {
use color_print::cformat;
let style_working = |has: bool, sym: char| -> (String, bool) {
if has {
(cformat!("<cyan>{sym}</>"), true)
} else {
(String::new(), false)
}
};
let (staged_str, has_staged) = style_working(self.working_tree.staged, '+');
let (modified_str, has_modified) = style_working(self.working_tree.modified, '!');
let (untracked_str, has_untracked) = style_working(self.working_tree.untracked, '?');
let (main_state_str, has_main_state) = self
.main_state
.styled()
.map_or((String::new(), false), |s| (s, true));
let (upstream_divergence_str, has_upstream_divergence) = self
.upstream_divergence
.styled()
.map_or((String::new(), false), |s| (s, true));
let (worktree_str, has_worktree) = if self.operation_state != OperationState::None {
(self.operation_state.styled().unwrap_or_default(), true)
} else {
match self.worktree_state {
WorktreeState::None => (String::new(), false),
WorktreeState::Branch => (cformat!("<dim>{}</>", self.worktree_state), true),
WorktreeState::BranchWorktreeMismatch => {
(cformat!("<red>{}</>", self.worktree_state), true)
}
_ => (cformat!("<yellow>{}</>", self.worktree_state), true),
}
};
let user_marker_str = self.user_marker.as_deref().unwrap_or("").to_string();
[
(PositionMask::STAGED, staged_str, has_staged),
(PositionMask::MODIFIED, modified_str, has_modified),
(PositionMask::UNTRACKED, untracked_str, has_untracked),
(PositionMask::WORKTREE_STATE, worktree_str, has_worktree),
(PositionMask::MAIN_STATE, main_state_str, has_main_state),
(
PositionMask::UPSTREAM_DIVERGENCE,
upstream_divergence_str,
has_upstream_divergence,
),
(
PositionMask::USER_MARKER,
user_marker_str,
self.user_marker.is_some(),
),
]
}
}
#[cfg(test)]
mod tests {
use insta::assert_snapshot;
use super::*;
#[test]
fn test_working_tree_status_is_dirty() {
assert!(!WorkingTreeStatus::default().is_dirty());
assert!(WorkingTreeStatus::new(true, false, false, false, false).is_dirty());
assert!(WorkingTreeStatus::new(false, true, false, false, false).is_dirty());
assert!(WorkingTreeStatus::new(false, false, true, false, false).is_dirty());
assert!(WorkingTreeStatus::new(false, false, false, true, false).is_dirty());
assert!(WorkingTreeStatus::new(false, false, false, false, true).is_dirty());
assert!(WorkingTreeStatus::new(true, true, true, true, true).is_dirty());
}
#[test]
fn test_working_tree_status_to_symbols() {
assert_eq!(WorkingTreeStatus::default().to_symbols(), "");
assert_eq!(
WorkingTreeStatus::new(true, false, false, false, false).to_symbols(),
"+"
);
assert_eq!(
WorkingTreeStatus::new(false, true, false, false, false).to_symbols(),
"!"
);
assert_eq!(
WorkingTreeStatus::new(false, false, true, false, false).to_symbols(),
"?"
);
assert_eq!(
WorkingTreeStatus::new(false, false, false, true, false).to_symbols(),
"»"
);
assert_eq!(
WorkingTreeStatus::new(false, false, false, false, true).to_symbols(),
"✘"
);
assert_eq!(
WorkingTreeStatus::new(true, true, false, false, false).to_symbols(),
"+!"
);
assert_eq!(
WorkingTreeStatus::new(true, true, true, false, false).to_symbols(),
"+!?"
);
assert_eq!(
WorkingTreeStatus::new(true, true, true, true, true).to_symbols(),
"+!?»✘"
);
}
#[test]
fn test_status_symbols_is_empty() {
let symbols = StatusSymbols::default();
assert!(symbols.is_empty());
let symbols = StatusSymbols {
main_state: MainState::Ahead,
..Default::default()
};
assert!(!symbols.is_empty());
let symbols = StatusSymbols {
operation_state: OperationState::Rebase,
..Default::default()
};
assert!(!symbols.is_empty());
let symbols = StatusSymbols {
worktree_state: WorktreeState::Locked,
..Default::default()
};
assert!(!symbols.is_empty());
let symbols = StatusSymbols {
upstream_divergence: Divergence::Ahead,
..Default::default()
};
assert!(!symbols.is_empty());
let symbols = StatusSymbols {
working_tree: WorkingTreeStatus::new(true, false, false, false, false),
..Default::default()
};
assert!(!symbols.is_empty());
let symbols = StatusSymbols {
user_marker: Some("🔥".to_string()),
..Default::default()
};
assert!(!symbols.is_empty());
}
#[test]
fn test_status_symbols_format_compact() {
let symbols = StatusSymbols::default();
assert_eq!(symbols.format_compact(), "");
let symbols = StatusSymbols {
main_state: MainState::Ahead,
..Default::default()
};
assert_snapshot!(symbols.format_compact(), @"[2m↑[22m");
let symbols = StatusSymbols {
working_tree: WorkingTreeStatus::new(true, true, false, false, false),
main_state: MainState::Ahead,
..Default::default()
};
assert_snapshot!(symbols.format_compact(), @"[36m+[39m[36m![39m[2m↑[22m");
}
#[test]
fn test_status_symbols_render_with_mask() {
let symbols = StatusSymbols {
main_state: MainState::Ahead,
..Default::default()
};
let rendered = symbols.render_with_mask(&PositionMask::FULL);
assert_snapshot!(rendered, @" [2m↑[22m");
}
#[test]
fn test_position_mask_width() {
let mask = PositionMask::FULL;
assert_eq!(mask.width(PositionMask::STAGED), 1);
assert_eq!(mask.width(PositionMask::MODIFIED), 1);
assert_eq!(mask.width(PositionMask::UNTRACKED), 1);
assert_eq!(mask.width(PositionMask::WORKTREE_STATE), 1);
assert_eq!(mask.width(PositionMask::MAIN_STATE), 1);
assert_eq!(mask.width(PositionMask::UPSTREAM_DIVERGENCE), 1);
assert_eq!(mask.width(PositionMask::USER_MARKER), 2);
}
}