use std::collections::HashSet;
use std::path::PathBuf;
use std::time::{Duration, SystemTime};
use pixtuoid_core::source::manager::SourceDeath;
use pixtuoid_core::state::SceneState;
use crate::install::target::{self, Target};
use crate::install::{InstallOutcome, InstallReport, UninstallOutcome, UninstallReport};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConnState {
Connected,
Disconnected,
NoCli,
}
#[derive(Debug, Clone)]
pub struct ConnectionRow {
pub source_id: &'static str,
pub label_prefix: &'static str,
pub display_name: &'static str,
pub state: ConnState,
pub config_path: Option<PathBuf>,
pub target: Option<&'static Target>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct LiveInfo {
pub agents: usize,
pub last_event_age: Option<Duration>,
pub dead: bool,
}
#[derive(Debug, Default)]
pub struct ConnectionUi {
pub open: bool,
pub selected: usize,
pub rows: Vec<ConnectionRow>,
pub confirm: Option<usize>,
pub last_result: Option<String>,
}
#[derive(Debug, Clone)]
pub struct RowFacts {
pub present: bool,
pub config_path: Option<PathBuf>,
}
#[derive(Debug, Clone)]
pub struct RowInput {
pub source_id: &'static str,
pub label_prefix: &'static str,
pub target: Option<&'static Target>,
pub facts: Option<RowFacts>,
pub connected: bool,
}
fn display_name_for(source_id: &'static str) -> &'static str {
match source_id {
"antigravity" => "Antigravity",
other => other,
}
}
pub fn build_rows_from(inputs: Vec<RowInput>) -> Vec<ConnectionRow> {
inputs
.into_iter()
.map(|input| {
let absent_cli = matches!(
(&input.target, &input.facts),
(Some(_), Some(f)) if !f.present
);
let state = if absent_cli {
ConnState::NoCli
} else if input.connected {
ConnState::Connected
} else {
ConnState::Disconnected
};
ConnectionRow {
source_id: input.source_id,
label_prefix: input.label_prefix,
display_name: input
.target
.map_or_else(|| display_name_for(input.source_id), |t| t.display_name),
state,
config_path: input.facts.and_then(|f| f.config_path),
target: input.target,
}
})
.collect()
}
pub fn build_rows(connected: &HashSet<String>) -> Vec<ConnectionRow> {
use pixtuoid_core::source::registry::REGISTRY;
let inputs = REGISTRY
.iter()
.map(|d| {
let target = target::by_source(d.name);
let facts = target.map(|t| RowFacts {
present: target::is_present(t),
config_path: (t.default_config_path)().ok(),
});
RowInput {
source_id: d.name,
label_prefix: d.label_prefix,
target,
facts,
connected: connected.contains(d.name),
}
})
.collect();
build_rows_from(inputs)
}
pub fn live_for(
now: SystemTime,
source_id: &str,
scene: &SceneState,
health: &[SourceDeath],
) -> LiveInfo {
let mut agents = 0usize;
let mut max_evt: Option<SystemTime> = None;
for slot in scene.agents.values() {
if slot.source.as_ref() == source_id {
agents += 1;
max_evt = Some(max_evt.map_or(slot.last_event_at, |m: SystemTime| {
m.max(slot.last_event_at)
}));
}
}
LiveInfo {
agents,
last_event_age: max_evt.map(|t| now.duration_since(t).unwrap_or_default()),
dead: health.iter().any(|d| d.source == source_id),
}
}
pub fn live_view(
now: SystemTime,
rows: &[ConnectionRow],
scene: &SceneState,
health: &[SourceDeath],
) -> Vec<LiveInfo> {
rows.iter()
.map(|r| live_for(now, r.source_id, scene, health))
.collect()
}
pub fn move_selection(rows: &[ConnectionRow], sel: usize, delta: i32) -> usize {
if rows.is_empty() {
return 0;
}
(sel as i32 + delta).clamp(0, rows.len() as i32 - 1) as usize
}
pub fn no_action_hint(row: &ConnectionRow) -> String {
match row.state {
ConnState::NoCli => format!("{} not detected on this machine", row.display_name),
_ => format!("nothing to do for {}", row.display_name),
}
}
pub fn format_connect_result(r: &InstallReport, display_name: &str) -> String {
let mut s = match r.outcome {
InstallOutcome::AlreadyUpToDate | InstallOutcome::Installed => {
format!("\u{2713} {display_name} connected")
}
};
if r.backup.is_some() {
s.push_str(" \u{00b7} backup saved");
}
if r.path_warning {
s.push_str(" \u{00b7} \u{26a0} pixtuoid-hook not on PATH");
}
s
}
pub fn format_disconnect_result(r: &UninstallReport, display_name: &str) -> String {
let mut s = match r.outcome {
UninstallOutcome::NothingToRemove | UninstallOutcome::Removed => {
format!("\u{2713} {display_name} disconnected")
}
};
if r.removed_backup.is_some() {
s.push_str(" \u{00b7} backup cleared");
}
s
}
#[cfg(test)]
mod tests;