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, PartialEq, Eq, 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: Option<MainState>,
pub(crate) operation_state: Option<OperationState>,
pub(crate) worktree_state: Option<WorktreeState>,
pub(crate) upstream_divergence: Option<Divergence>,
pub(crate) working_tree: Option<WorkingTreeStatus>,
pub(crate) user_marker: Option<Option<String>>,
}
impl StatusSymbols {
pub fn render_with_mask(&self, mask: &PositionMask, placeholder: &str) -> String {
use anstyle::Style;
use worktrunk::styling::StyledLine;
let mut result = String::with_capacity(64);
for (pos, slot) in self.styled_symbols() {
let allocated_width = mask.width(pos);
match slot {
SlotState::Visible(content) => {
let mut segment = StyledLine::new();
segment.push_raw(content);
segment.pad_to(allocated_width);
result.push_str(&segment.render());
}
SlotState::Loading => {
let mut segment = StyledLine::new();
segment.push_styled(placeholder.to_string(), Style::new().dimmed());
segment.pad_to(allocated_width);
result.push_str(&segment.render());
}
SlotState::Empty => {
for _ in 0..allocated_width {
result.push(' ');
}
}
}
}
result
}
pub fn format_compact(&self) -> String {
self.styled_symbols()
.into_iter()
.filter_map(|(_, slot)| match slot {
SlotState::Visible(s) => Some(s),
SlotState::Loading | SlotState::Empty => None,
})
.collect()
}
pub(crate) fn styled_symbols(&self) -> [(usize, SlotState); 7] {
use color_print::cformat;
let (staged, modified, untracked) = match self.working_tree {
Some(wt) => {
let flag = |has: bool, sym: char| -> SlotState {
if has {
SlotState::Visible(cformat!("<cyan>{sym}</>"))
} else {
SlotState::Empty
}
};
(
flag(wt.staged, '+'),
flag(wt.modified, '!'),
flag(wt.untracked, '?'),
)
}
None => (SlotState::Loading, SlotState::Empty, SlotState::Empty),
};
let main_state_slot = match self.main_state {
Some(ms) => match ms.styled() {
Some(s) => SlotState::Visible(s),
None => SlotState::Empty,
},
None => SlotState::Loading,
};
let upstream_slot = match self.upstream_divergence {
Some(d) => match d.styled() {
Some(s) => SlotState::Visible(s),
None => SlotState::Empty,
},
None => SlotState::Loading,
};
let worktree_slot = match self.operation_state {
None => SlotState::Loading,
Some(op) if op != OperationState::None => {
SlotState::Visible(op.styled().unwrap_or_default())
}
Some(_) => match self.worktree_state {
None | Some(WorktreeState::None) => SlotState::Empty,
Some(WorktreeState::Branch) => {
SlotState::Visible(cformat!("<dim>{}</>", WorktreeState::Branch))
}
Some(WorktreeState::BranchWorktreeMismatch) => SlotState::Visible(cformat!(
"<red>{}</>",
WorktreeState::BranchWorktreeMismatch
)),
Some(other) => SlotState::Visible(cformat!("<yellow>{}</>", other)),
},
};
let user_marker_slot = match &self.user_marker {
None => SlotState::Loading,
Some(None) => SlotState::Empty,
Some(Some(s)) => SlotState::Visible(s.clone()),
};
[
(PositionMask::STAGED, staged),
(PositionMask::MODIFIED, modified),
(PositionMask::UNTRACKED, untracked),
(PositionMask::WORKTREE_STATE, worktree_slot),
(PositionMask::MAIN_STATE, main_state_slot),
(PositionMask::UPSTREAM_DIVERGENCE, upstream_slot),
(PositionMask::USER_MARKER, user_marker_slot),
]
}
}
#[derive(Debug, Clone)]
pub(crate) enum SlotState {
Loading,
Empty,
Visible(String),
}
#[cfg(test)]
mod tests {
use insta::assert_snapshot;
use super::*;
fn is_empty(s: &StatusSymbols) -> bool {
let main_empty = s.main_state.is_none_or(|s| s == MainState::None);
let op_empty = s.operation_state.is_none_or(|s| s == OperationState::None);
let wt_state_empty = s.worktree_state.is_none_or(|s| s == WorktreeState::None);
let upstream_empty = s.upstream_divergence.is_none_or(|s| s == Divergence::None);
let working_tree_empty = s.working_tree.is_none_or(|wt| !wt.is_dirty());
let user_marker_empty = s.user_marker.as_ref().is_none_or(|m| m.is_none());
main_empty
&& op_empty
&& wt_state_empty
&& upstream_empty
&& working_tree_empty
&& user_marker_empty
}
#[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!(is_empty(&symbols));
let symbols = StatusSymbols {
main_state: Some(MainState::Ahead),
..Default::default()
};
assert!(!is_empty(&symbols));
let symbols = StatusSymbols {
operation_state: Some(OperationState::Rebase),
..Default::default()
};
assert!(!is_empty(&symbols));
let symbols = StatusSymbols {
worktree_state: Some(WorktreeState::Locked),
..Default::default()
};
assert!(!is_empty(&symbols));
let symbols = StatusSymbols {
upstream_divergence: Some(Divergence::Ahead),
..Default::default()
};
assert!(!is_empty(&symbols));
let symbols = StatusSymbols {
working_tree: Some(WorkingTreeStatus::new(true, false, false, false, false)),
..Default::default()
};
assert!(!is_empty(&symbols));
let symbols = StatusSymbols {
user_marker: Some(Some("🔥".to_string())),
..Default::default()
};
assert!(!is_empty(&symbols));
let symbols = StatusSymbols {
main_state: Some(MainState::None),
operation_state: Some(OperationState::None),
worktree_state: Some(WorktreeState::None),
upstream_divergence: Some(Divergence::None),
working_tree: Some(WorkingTreeStatus::default()),
user_marker: Some(None),
};
assert!(is_empty(&symbols));
}
#[test]
fn test_status_symbols_format_compact() {
let symbols = StatusSymbols::default();
assert_eq!(symbols.format_compact(), "");
let symbols = StatusSymbols {
main_state: Some(MainState::Ahead),
..Default::default()
};
assert_snapshot!(symbols.format_compact(), @"[2m↑[22m");
let symbols = StatusSymbols {
working_tree: Some(WorkingTreeStatus::new(true, true, false, false, false)),
main_state: Some(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: Some(MainState::Ahead),
..Default::default()
};
let rendered = symbols.render_with_mask(&PositionMask::FULL, "·");
assert_snapshot!(rendered, @"[2m·[0m [2m·[0m[2m↑[22m[2m·[0m[2m·[0m");
}
#[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);
}
}