use egui::{Color32, RichText};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MuxHealth {
#[default]
Connected,
Attaching,
Unresponsive,
Stale,
}
impl MuxHealth {
fn label(self) -> &'static str {
match self {
Self::Connected => "connected",
Self::Attaching => "attaching",
Self::Unresponsive => "not responding",
Self::Stale => "daemon out of date",
}
}
fn color(self) -> Color32 {
match self {
Self::Connected => Color32::from_rgb(76, 175, 80),
Self::Attaching => Color32::from_rgb(33, 150, 243),
Self::Unresponsive | Self::Stale => Color32::from_rgb(255, 193, 7),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct SessionChip {
pub session: Option<String>,
pub health: MuxHealth,
pub hidden_tabs: usize,
pub last_error: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionChipAction {
OpenPicker,
DismissError,
}
impl SessionChip {
pub fn is_visible(&self) -> bool {
self.session.is_some() || self.last_error.is_some()
}
pub fn label(&self) -> String {
let mut text = match &self.session {
Some(name) => format!("\u{f24d} {name}"),
None => "\u{f24d} par-mux".to_string(),
};
if self.hidden_tabs > 0 {
text.push_str(&format!(" · {} hidden", self.hidden_tabs));
}
if self.session.is_some() && self.health != MuxHealth::Connected {
text.push_str(&format!(" · {}", self.health.label()));
}
text
}
pub fn tooltip(&self) -> String {
let mut lines = Vec::new();
match &self.session {
Some(name) => {
lines.push(format!("par-mux session {name} — {}", self.health.label()));
lines.push("Tabs here survive quitting par-term.".to_string());
}
None => lines.push("Not attached to a par-mux session".to_string()),
}
if self.hidden_tabs > 0 {
lines.push(format!(
"{} hidden tab(s): still running; the session picker and reopen closed tab \
show them again",
self.hidden_tabs
));
}
if let Some(error) = &self.last_error {
lines.push(format!("Last error: {error}"));
}
lines.push("Click to open the session picker".to_string());
lines.join("\n")
}
}
pub fn show(ui: &mut egui::Ui, chip: &SessionChip, height: f32) -> Option<SessionChipAction> {
if !chip.is_visible() {
return None;
}
let mut action = None;
let color = if chip.last_error.is_some() {
Color32::from_rgb(244, 67, 54)
} else {
chip.health.color()
};
let button = egui::Button::new(RichText::new(chip.label()).color(color))
.min_size(egui::vec2(0.0, height))
.fill(Color32::TRANSPARENT);
if ui.add(button).on_hover_text(chip.tooltip()).clicked() {
action = Some(SessionChipAction::OpenPicker);
}
if let Some(error) = &chip.last_error {
let short: String = error.chars().take(40).collect();
let ellipsis = if error.chars().count() > 40 {
"…"
} else {
""
};
ui.label(
RichText::new(format!("{short}{ellipsis}"))
.color(color)
.small(),
)
.on_hover_text(error);
if ui
.add(egui::Button::new("×").fill(Color32::TRANSPARENT))
.on_hover_text("Dismiss this error")
.clicked()
{
action = Some(SessionChipAction::DismissError);
}
}
action
}
#[cfg(test)]
mod tests {
use super::*;
fn chip(session: Option<&str>) -> SessionChip {
SessionChip {
session: session.map(str::to_string),
..Default::default()
}
}
#[test]
fn hidden_when_local_and_error_free() {
assert!(!chip(None).is_visible());
}
#[test]
fn shows_the_session_name() {
let c = chip(Some("work"));
assert!(c.is_visible());
assert_eq!(c.label(), "\u{f24d} work");
}
#[test]
fn shows_the_hidden_count_and_health() {
let c = SessionChip {
hidden_tabs: 2,
health: MuxHealth::Unresponsive,
..chip(Some("work"))
};
assert_eq!(c.label(), "\u{f24d} work · 2 hidden · not responding");
}
#[test]
fn a_held_error_keeps_the_chip_after_the_session_is_gone() {
let c = SessionChip {
last_error: Some("par-mux: daemon connection lost".to_string()),
..chip(None)
};
assert!(c.is_visible(), "the error outlives the attach");
assert!(c.tooltip().contains("daemon connection lost"));
}
}