systemprompt-logging 0.2.0

Core logging module for systemprompt.io OS
Documentation
use std::io::Write;

use crate::services::cli::display::{CollectionDisplay, Display, DisplayUtils, ModuleItemDisplay};
use crate::services::cli::prompts::Prompts;
use crate::services::cli::theme::{ItemStatus, MessageLevel, ModuleType, Theme};
use anyhow::Result;

fn stdout_writeln(args: std::fmt::Arguments<'_>) {
    let mut out = std::io::stdout();
    let _ = writeln!(out, "{args}");
}

#[derive(Debug, Copy, Clone)]
pub struct ModuleDisplay;

impl ModuleDisplay {
    pub fn missing_schemas(module_name: &str, schemas: &[(String, String)]) {
        if schemas.is_empty() {
            return;
        }

        DisplayUtils::module_status(
            module_name,
            &format!("{} schemas need application", schemas.len()),
        );

        DisplayUtils::count_message(MessageLevel::Warning, schemas.len(), "schemas");

        let items: Vec<ModuleItemDisplay> = schemas
            .iter()
            .map(|(file, table)| {
                ModuleItemDisplay::new(ModuleType::Schema, file, table, ItemStatus::Missing)
            })
            .collect();

        for item in &items {
            item.display();
        }
    }

    pub fn missing_seeds(module_name: &str, seeds: &[(String, String)]) {
        if seeds.is_empty() {
            return;
        }

        DisplayUtils::module_status(
            module_name,
            &format!("{} seeds need application", seeds.len()),
        );

        DisplayUtils::count_message(MessageLevel::Warning, seeds.len(), "seeds");

        let items: Vec<ModuleItemDisplay> = seeds
            .iter()
            .map(|(file, table)| {
                ModuleItemDisplay::new(ModuleType::Seed, file, table, ItemStatus::Missing)
            })
            .collect();

        for item in &items {
            item.display();
        }
    }

    pub fn prompt_apply_schemas(module_name: &str, schemas: &[(String, String)]) -> Result<bool> {
        if schemas.is_empty() {
            return Ok(false);
        }

        Self::missing_schemas(module_name, schemas);
        stdout_writeln(format_args!(""));
        Prompts::confirm_schemas()
    }

    pub fn prompt_apply_seeds(module_name: &str, seeds: &[(String, String)]) -> Result<bool> {
        if seeds.is_empty() {
            return Ok(false);
        }

        Self::missing_seeds(module_name, seeds);
        stdout_writeln(format_args!(""));
        Prompts::confirm_seeds()
    }
}

#[derive(Debug, Clone)]
pub struct ModuleUpdate {
    pub name: String,
    pub old_version: String,
    pub new_version: String,
    pub changes: Vec<String>,
}

impl ModuleUpdate {
    pub fn new(
        name: impl Into<String>,
        old_version: impl Into<String>,
        new_version: impl Into<String>,
    ) -> Self {
        Self {
            name: name.into(),
            old_version: old_version.into(),
            new_version: new_version.into(),
            changes: Vec::new(),
        }
    }

    #[must_use]
    pub fn with_change(mut self, change: impl Into<String>) -> Self {
        self.changes.push(change.into());
        self
    }

    #[must_use]
    pub fn with_changes(mut self, changes: Vec<String>) -> Self {
        self.changes = changes;
        self
    }
}

impl Display for ModuleUpdate {
    fn display(&self) {
        stdout_writeln(format_args!(
            "   {} {} {}",
            Theme::icon(crate::services::cli::theme::ActionType::Update),
            Theme::color(&self.name, crate::services::cli::theme::EmphasisType::Bold),
            Theme::color(
                &format!("{} \u{2192} {}", self.old_version, self.new_version),
                crate::services::cli::theme::EmphasisType::Dim
            )
        ));

        for change in &self.changes {
            stdout_writeln(format_args!(
                "     \u{2022} {}",
                Theme::color(change, crate::services::cli::theme::EmphasisType::Dim)
            ));
        }
    }
}

#[derive(Debug, Clone)]
pub struct ModuleInstall {
    pub name: String,
    pub version: String,
    pub description: Option<String>,
}

impl ModuleInstall {
    pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            version: version.into(),
            description: None,
        }
    }

    #[must_use]
    pub fn with_description(mut self, description: impl Into<String>) -> Self {
        self.description = Some(description.into());
        self
    }
}

impl Display for ModuleInstall {
    fn display(&self) {
        let detail = self.description.as_ref().map_or_else(
            || format!("v{}", self.version),
            |desc| format!("v{} - {}", self.version, desc),
        );

        stdout_writeln(format_args!(
            "   {} {} {}",
            Theme::icon(crate::services::cli::theme::ActionType::Install),
            Theme::color(&self.name, crate::services::cli::theme::EmphasisType::Bold),
            Theme::color(&detail, crate::services::cli::theme::EmphasisType::Dim)
        ));
    }
}

#[derive(Debug, Copy, Clone)]
pub struct BatchModuleOperations;

impl BatchModuleOperations {
    pub fn prompt_install_multiple(modules: &[ModuleInstall]) -> Result<bool> {
        if modules.is_empty() {
            return Ok(false);
        }

        let collection =
            CollectionDisplay::new("New modules available for installation", modules.to_vec());
        collection.display();

        Prompts::confirm("Install all these modules?", false)
    }

    pub fn prompt_update_multiple(updates: &[ModuleUpdate]) -> Result<bool> {
        if updates.is_empty() {
            return Ok(false);
        }

        let collection = CollectionDisplay::new("Module updates available", updates.to_vec());
        collection.display();

        Prompts::confirm("Update all these modules?", false)
    }
}