use crate::git_tree::{Agent, AgentState, descent_order};
use crate::ui_state::SeenKind;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AttentionKind {
Notify,
Stopped,
Budget,
Conflicted,
Mail,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Attention {
pub notify: bool,
pub stopped: bool,
pub budget: bool,
pub conflicted: bool,
pub mail: bool,
}
impl Attention {
pub fn any(self) -> bool {
self.notify || self.stopped || self.budget || self.conflicted || self.mail
}
pub fn kinds(self) -> Vec<AttentionKind> {
[
(self.notify, AttentionKind::Notify),
(self.stopped, AttentionKind::Stopped),
(self.budget, AttentionKind::Budget),
(self.conflicted, AttentionKind::Conflicted),
(self.mail, AttentionKind::Mail),
]
.into_iter()
.filter_map(|(on, kind)| on.then_some(kind))
.collect()
}
}
pub fn attention(
agent: &Agent,
ws: &str,
seen: &dyn Fn(SeenKind, &str, &str, &str) -> bool,
) -> Attention {
let id = agent.agent_id.as_str();
let unseen = |kind, oid: &str| !seen(kind, ws, id, oid);
Attention {
notify: agent
.notify_oid
.as_deref()
.is_some_and(|o| unseen(SeenKind::Notify, o)),
stopped: agent.state == AgentState::Stopped
&& agent.abandoned_oid.is_none()
&& unseen(SeenKind::Stopped, &agent.tip_oid),
budget: agent
.budget_oid
.as_deref()
.is_some_and(|o| unseen(SeenKind::Budget, o)),
conflicted: agent
.conflicted_oid
.as_deref()
.is_some_and(|o| unseen(SeenKind::Conflicted, o)),
mail: agent.pending_messages > 0 && lock_free(agent),
}
}
fn lock_free(agent: &Agent) -> bool {
matches!(agent.state, AgentState::Quiescent | AgentState::Stopped) && !agent.state_uncertain
}
fn rank(agent: &Agent, ws: &str, seen: &dyn Fn(SeenKind, &str, &str, &str) -> bool) -> u8 {
if attention(agent, ws, seen).any() {
0
} else if matches!(agent.state, AgentState::Live | AgentState::InFlight) {
1
} else {
2
}
}
pub fn sorted_roster(
agents: &[Agent],
ws: &str,
seen: &dyn Fn(SeenKind, &str, &str, &str) -> bool,
) -> Vec<usize> {
let mut order: Vec<usize> = descent_order(agents)
.into_iter()
.map(|row| row.index)
.collect();
order.sort_by_key(|&i| agents.get(i).map(|a| rank(a, ws, seen)));
order
}
pub fn workspace_count(
agents: &[Agent],
ws: &str,
seen: &dyn Fn(SeenKind, &str, &str, &str) -> bool,
) -> usize {
agents
.iter()
.filter(|a| attention(a, ws, seen).any())
.count()
}
pub fn strip_total(
workspaces: &[(&str, &[Agent])],
seen: &dyn Fn(SeenKind, &str, &str, &str) -> bool,
) -> usize {
workspaces
.iter()
.map(|(path, agents)| workspace_count(agents, path, seen))
.sum()
}
#[derive(Clone)]
pub struct RosterKey {
pub ws: String,
pub agent_id: String,
pub attention: bool,
}
impl RosterKey {
fn is_at(&self, focus: (&str, &str)) -> bool {
self.ws == focus.0 && self.agent_id == focus.1
}
}
pub fn roster_order(
workspaces: &[(&str, &[Agent])],
seen: &dyn Fn(SeenKind, &str, &str, &str) -> bool,
) -> Vec<RosterKey> {
let mut wss: Vec<(&str, &[Agent])> = workspaces.to_vec();
wss.sort_by_key(|(path, _)| *path);
let mut out = Vec::new();
for (path, agents) in wss {
out.extend(
sorted_roster(agents, path, seen)
.into_iter()
.filter_map(|i| agents.get(i))
.map(|agent| RosterKey {
ws: path.to_string(),
agent_id: agent.agent_id.clone(),
attention: attention(agent, path, seen).any(),
}),
);
}
out
}
pub fn next_attention(roster: &[RosterKey], focus: Option<(&str, &str)>) -> Option<RosterKey> {
let n = roster.len();
if n == 0 {
return None;
}
let mut start = 0;
if let Some(f) = focus
&& let Some(i) = roster.iter().position(|e| e.is_at(f))
{
start = i + 1;
}
(0..n)
.filter_map(|step| roster.get((start + step) % n))
.find(|e| e.attention)
.cloned()
}
pub fn step(roster: &[RosterKey], focus: Option<(&str, &str)>, delta: isize) -> Option<RosterKey> {
let n = roster.len();
if n == 0 {
return None;
}
let here = focus.and_then(|f| roster.iter().position(|e| e.is_at(f)));
let next = match here {
Some(i) => (i as isize + delta).rem_euclid(n as isize) as usize,
None if delta >= 0 => 0,
None => n - 1,
};
roster.get(next).cloned()
}
#[cfg(test)]
mod tests;