multistack 1.0.0-rc1

Open source lightweight TUI for parallel agent management
use std::io::Read;
use std::os::unix::net::UnixListener;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicU8, Ordering};
use std::thread::JoinHandle;
use std::time::{Duration, Instant};

use parking_lot::Mutex;

pub const STATUS_NOT_YET: u8 = 0;
pub const STATUS_WORKING: u8 = 1;
pub const STATUS_FINISHED: u8 = 2;
pub const STATUS_DEAD: u8 = 3;

pub fn status_prefix(status: u8) -> &'static str {
    match status {
        STATUS_NOT_YET => "[ ]",
        STATUS_WORKING => "[~]",
        STATUS_FINISHED => "[✓]",
        STATUS_DEAD => "[X]",
        _ => "[ ]",
    }
}

pub fn format_timer(active_ms: u64, cycle_start: &Option<Instant>) -> String {
    let total_ms = active_ms
        + cycle_start
            .map(|s| s.elapsed().as_millis() as u64)
            .unwrap_or(0);
    let total_secs = total_ms / 1000;
    let hours = total_secs / 3600;
    let mins = (total_secs % 3600) / 60;
    let secs = total_secs % 60;
    if hours > 0 {
        format!("{}:{:02}:{:02}", hours, mins, secs)
    } else {
        format!("{}:{:02}", mins, secs)
    }
}

pub fn spawn_status_listener(
    status: Arc<AtomicU8>,
    active_ms: Arc<AtomicU64>,
    cycle_start: Arc<Mutex<Option<Instant>>>,
    socket_path: String,
) -> (Arc<AtomicBool>, JoinHandle<()>) {
    let shutdown = Arc::new(AtomicBool::new(false));
    let shutdown_clone = shutdown.clone();

    let handle = std::thread::spawn(move || {
        let _ = std::fs::remove_file(&socket_path);
        let listener = match UnixListener::bind(&socket_path) {
            Ok(l) => l,
            Err(_) => return,
        };
        listener.set_nonblocking(true).ok();

        let mut buf = vec![0u8; 1024];

        loop {
            match listener.accept() {
                Ok((mut stream, _)) => {
                    let n = match stream.read(&mut buf) {
                        Ok(n) if n > 0 => n,
                        _ => continue,
                    };

                    let text = String::from_utf8_lossy(&buf[..n]);
                    for line in text.lines() {
                        match line {
                            "start" => {
                                status.store(STATUS_WORKING, Ordering::SeqCst);
                                *cycle_start.lock() = Some(Instant::now());
                            }
                            "stop" => {
                                if let Some(start) = cycle_start.lock().take() {
                                    let elapsed = start.elapsed();
                                    active_ms.fetch_add(
                                        elapsed.as_millis() as u64,
                                        Ordering::SeqCst,
                                    );
                                }
                                if status.load(Ordering::SeqCst) == STATUS_WORKING {
                                    status.store(STATUS_FINISHED, Ordering::SeqCst);
                                }
                            }
                            _ => {}
                        }
                    }
                }
                Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
                    if shutdown_clone.load(Ordering::SeqCst) {
                        return;
                    }
                    std::thread::sleep(Duration::from_millis(100));
                }
                Err(_) => return,
            }
        }
    });

    (shutdown, handle)
}