willdo 0.0.1

Task manager with DAG
Documentation
use core::time::Duration;
use futures_lite::StreamExt;
use std::collections::BTreeMap;
use std::io::{stdout, IsTerminal};
use willdo::{
    execution::progress::{Observation, Progress},
    job::JobId,
};

fn main() {
    let mut show = Show::default();
    let code = futures_lite::future::block_on(async move {
        let graph = willdo::cli::build()?;
        let mut desired_goals = willdo::cli::goals();
        let mut actual_goals = vec![];
        println!("------- Schedule --------");
        for (indent, job, info) in graph.tree(&desired_goals) {
            print!("{}", "  ".repeat(indent));
            println!("{job} ({info})");
            desired_goals.retain(|g| g.as_ref() != job.to_string());
            if indent == 0 {
                actual_goals.push(job.clone());
            }
        }
        if !desired_goals.is_empty() {
            println!("------- Summary --------");
            println!("Some goals were not available: {desired_goals:?}");
            return Ok(desired_goals.len() as i32);
        }
        println!("------- Execution --------");
        let mut events = willdo::cli::run(graph);
        while let Some((j, p)) = events.next().await {
            if let Progress::Observation(Observation::Completed(0)) = &p {
                actual_goals.retain(|g| g != &j)
            }
            show.update(j, p);
            std::thread::sleep(Duration::from_millis(100));
        }
        println!("------- Summary --------");
        if actual_goals.is_empty() {
            println!("All good!");
            Ok(0)
        } else {
            println!(
                "Some goals were not reached: {:?}",
                actual_goals
                    .iter()
                    .map(|j| j.to_string())
                    .collect::<Vec<_>>()
            );
            Ok(actual_goals.len() as i32)
        }
    })
    .unwrap_or_else(|e: willdo::cli::Error| {
        eprintln!("WillDo: {e}");
        1
    });
    std::process::exit(code);
}

#[derive(Debug, Default)]
pub struct Show {
    jobs: BTreeMap<JobId, (usize, String)>,
    printer: Printer,
}
impl Show {
    fn update(&mut self, job: JobId, progress: Progress) {
        let line: String = format!("{job}: {progress:?}");
        let (offset, entry) = self.entry(job);
        *entry = line.clone();
        self.print(offset, &line)
    }
    fn entry(&mut self, job: JobId) -> (usize, &mut String) {
        let count = self.jobs.len();
        let (pos, entry) = self.jobs.entry(job).or_insert((count, "".into()));
        let offset = count - *pos;
        (offset, entry)
    }
    fn print(&mut self, offset: usize, entry: &str) {
        if offset != 0 {
            self.printer.line_up(offset);
        }
        self.printer.line_wipe();
        use std::io::Write;
        writeln!(self.printer, "{entry}").expect("print");
        if offset != 0 {
            self.printer.line_down(offset.saturating_sub(1));
        }
    }
}

struct Printer {
    pub ansi: bool,
    pub out: Box<dyn std::io::Write>,
}
impl core::fmt::Debug for Printer {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("Printer")
            .field("ansi", &self.ansi)
            .field("out", &"...")
            .finish()
    }
}
impl Default for Printer {
    fn default() -> Self {
        Self::new()
    }
}
impl Printer {
    pub fn ansi(mut self) -> Self {
        self.ansi = true;
        self
    }
    pub fn plain(mut self) -> Self {
        self.ansi = false;
        self
    }
    pub fn new() -> Self {
        //consider https://docs.rs/raw_tty/latest/raw_tty/
        match std::fs::OpenOptions::new()
            .read(true)
            .write(true)
            .open("/dev/tty")
        {
            Ok(out) => Self {
                ansi: true,
                out: Box::new(out),
            },
            Err(_) => {
                let out = stdout();
                Self {
                    ansi: out.is_terminal(),
                    out: Box::new(out.lock()),
                }
            }
        }
    }
    pub fn line_up(&mut self, n: usize) {
        if self.ansi {
            use std::io::Write;
            let _silence = write!(self, "\x1b[{n}F");
        }
    }
    pub fn line_down(&mut self, n: usize) {
        if self.ansi {
            use std::io::Write;
            let _silence = write!(self, "\x1b[{n}E");
        }
    }
    pub fn line_wipe(&mut self) {
        if self.ansi {
            use std::io::Write;
            let _silence = write!(self, "\x1b[2K");
        }
    }
}
impl std::io::Write for Printer {
    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
        let size = self.out.write(buf)?;
        self.flush()?;
        Ok(size)
    }

    fn flush(&mut self) -> std::io::Result<()> {
        self.out.flush()
    }
}