use super::Tab;
pub(crate) const ZOOM_BADGE: &str = "\u{f065}";
pub(crate) const BROADCAST_BADGE: &str = "📡";
impl Tab {
pub(crate) fn pane_mode_badge(&self) -> Option<&'static str> {
if self.broadcast_input {
return Some(BROADCAST_BADGE);
}
self.pane_manager
.as_ref()
.is_some_and(|pm| pm.is_zoomed())
.then_some(ZOOM_BADGE)
}
pub(crate) fn pane_mode_badge_tooltip(&self) -> &'static str {
if self.broadcast_input {
"Broadcast input: typing and pastes go to every pane in this tab"
} else {
"A pane is zoomed to fill this tab"
}
}
pub(crate) fn broadcast_receivers(&self) -> Vec<crate::pane::PaneId> {
self.pane_manager
.as_ref()
.map(|pm| {
pm.all_panes()
.iter()
.filter(|p| !p.broadcast_excluded)
.map(|p| p.id)
.collect()
})
.unwrap_or_default()
}
pub(crate) fn broadcast_outline_bounds(&self) -> Vec<crate::pane::PaneBounds> {
if !self.broadcast_input {
return Vec::new();
}
let Some(pm) = self.pane_manager.as_ref() else {
return Vec::new();
};
if !pm.has_multiple_panes() {
return Vec::new();
}
pm.visible_panes()
.iter()
.filter(|p| !p.broadcast_excluded)
.map(|p| p.bounds)
.collect()
}
}
pub(crate) const ATTACHED_BADGE: &str = "🔗";
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum AgentAttention {
Working,
DoneUnseen,
Blocked,
}
impl AgentAttention {
pub(crate) fn from_roster(state: &str, done_unseen: bool) -> Option<Self> {
if done_unseen {
return Some(Self::DoneUnseen);
}
if state.eq_ignore_ascii_case("blocked") {
Some(Self::Blocked)
} else if state.eq_ignore_ascii_case("working") {
Some(Self::Working)
} else {
None
}
}
pub(crate) fn glyph(self) -> &'static str {
match self {
Self::Blocked => "\u{f111}",
Self::DoneUnseen => "\u{f058}",
Self::Working => "\u{f110}",
}
}
pub(crate) fn color(self, text_color: egui::Color32) -> egui::Color32 {
match self {
Self::Blocked => egui::Color32::from_rgb(255, 193, 7),
Self::DoneUnseen => egui::Color32::from_rgb(76, 175, 80),
Self::Working => text_color,
}
}
pub(crate) fn describe(self) -> &'static str {
match self {
Self::Blocked => "an agent is waiting for you",
Self::DoneUnseen => "an agent finished (not yet seen)",
Self::Working => "an agent is working",
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct TabMuxView {
pub(crate) attached: bool,
pub(crate) session: Option<String>,
pub(crate) agents: Vec<(crate::pane::PaneId, AgentAttention)>,
}
impl TabMuxView {
pub(crate) fn agent_badge(&self) -> Option<AgentAttention> {
self.agents.iter().map(|(_, a)| *a).max()
}
pub(crate) fn pane_agent(&self, pane: crate::pane::PaneId) -> Option<AgentAttention> {
self.agents
.iter()
.find(|(id, _)| *id == pane)
.map(|(_, a)| *a)
}
pub(crate) fn attached_tooltip(&self) -> String {
match &self.session {
Some(name) => format!("Attached to par-mux session {name}; survives quit"),
None => "Attached to a par-mux session; survives quit".to_string(),
}
}
}
pub(crate) fn decorate_pane_title(title: String, zoomed: bool, number: Option<usize>) -> String {
let title = match number {
Some(n) => format!("{n}: {title}"),
None => title,
};
if zoomed {
format!("{ZOOM_BADGE} {title}")
} else {
title
}
}
pub(crate) fn decorate_pane_title_with_agent(
title: String,
zoomed: bool,
number: Option<usize>,
agent: Option<AgentAttention>,
) -> String {
let title = decorate_pane_title(title, zoomed, number);
match agent {
Some(agent) => format!("{} {title}", agent.glyph()),
None => title,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_badge_glyph_is_carried_by_the_egui_stack() {
for glyph in [
AgentAttention::Blocked.glyph(),
AgentAttention::DoneUnseen.glyph(),
AgentAttention::Working.glyph(),
ZOOM_BADGE,
] {
for ch in glyph.chars() {
assert!(
crate::settings_ui::nerd_font::nerd_font_covers(ch),
"badge glyph {ch:?} (U+{:04X}) is in no egui-stack font — it will render as tofu",
ch as u32
);
}
}
for (name, emoji) in [("broadcast", BROADCAST_BADGE), ("attached", ATTACHED_BADGE)] {
assert!(
emoji.chars().count() == 1,
"{name} badge must be a single codepoint"
);
}
}
#[test]
fn a_zoomed_pane_title_carries_the_zoom_marker() {
assert_eq!(
decorate_pane_title("vim".into(), true, None),
"\u{f065} vim"
);
assert_eq!(decorate_pane_title("vim".into(), false, None), "vim");
}
#[test]
fn roster_states_map_to_badges_with_done_unseen_winning() {
use AgentAttention as A;
assert_eq!(
AgentAttention::from_roster("blocked", false),
Some(A::Blocked)
);
assert_eq!(
AgentAttention::from_roster("Working", false),
Some(A::Working)
);
assert_eq!(AgentAttention::from_roster("idle", false), None);
assert_eq!(
AgentAttention::from_roster("idle", true),
Some(A::DoneUnseen)
);
}
#[test]
fn a_tab_badge_shows_its_most_urgent_pane() {
use AgentAttention as A;
let view = TabMuxView {
attached: true,
session: None,
agents: vec![(1, A::Working), (2, A::Blocked), (3, A::DoneUnseen)],
};
assert_eq!(view.agent_badge(), Some(A::Blocked));
assert_eq!(view.pane_agent(3), Some(A::DoneUnseen));
assert_eq!(view.pane_agent(9), None);
assert_eq!(TabMuxView::default().agent_badge(), None);
}
#[test]
fn a_pane_title_leads_with_its_agent_badge() {
assert_eq!(
decorate_pane_title_with_agent(
"claude".into(),
false,
None,
Some(AgentAttention::Blocked)
),
"\u{f111} claude"
);
assert_eq!(
decorate_pane_title_with_agent("vim".into(), true, None, None),
"\u{f065} vim"
);
}
#[test]
fn a_numbered_pane_title_leads_with_its_number() {
assert_eq!(decorate_pane_title("vim".into(), false, Some(3)), "3: vim");
assert_eq!(
decorate_pane_title("vim".into(), true, Some(1)),
"\u{f065} 1: vim"
);
}
}