bmux_cli 0.0.1-alpha.1

Command-line interface for bmux terminal multiplexer
use anyhow::{Context, Result};
use bmux_config::ConfigPaths;
use moosicbox_log_watch::{
    LogFilterCaseMode as InternalLogFilterCaseMode, LogFilterKind as InternalLogFilterKind,
};
use std::path::PathBuf;

#[cfg(test)]
pub use moosicbox_log_watch::{
    LogFilterCaseMode, LogFilterKind, LogFilterRule, compile_filter_regex, line_visible_in_watch,
    normalize_profile_name as normalize_logs_watch_profile,
    watch_filter_rule_to_state as logs_watch_filter_rule_to_state,
    watch_filter_state_to_rule as logs_watch_filter_state_to_rule,
};

const BMUX_LOG_FILE_PREFIX: &str = "bmux.log";
const BMUX_WATCH_TITLE: &str = "bmux logs watch";

pub fn run_logs_watch(
    lines: Option<usize>,
    since: Option<&str>,
    profile: Option<&str>,
    include: &[String],
    include_i: &[String],
    exclude: &[String],
    exclude_i: &[String],
) -> Result<u8> {
    moosicbox_log_watch::run_watch(moosicbox_log_watch::WatchRunConfig {
        title: BMUX_WATCH_TITLE.to_string(),
        log_dir: active_log_watch_dir(),
        log_file_prefix: active_log_watch_prefix().to_string(),
        lines,
        since: since.map(ToString::to_string),
        profile: profile.map(ToString::to_string),
        include: include.to_vec(),
        include_i: include_i.to_vec(),
        exclude: exclude.to_vec(),
        exclude_i: exclude_i.to_vec(),
        state_file: Some(logs_watch_state_file_path()),
    })?;
    Ok(0)
}

pub fn run_logs_profiles_list(as_json: bool) -> Result<u8> {
    let summaries = moosicbox_log_watch::profiles_list(&logs_watch_state_file_path())?;

    if as_json {
        let payload = summaries
            .iter()
            .map(|summary| {
                serde_json::json!({
                    "name": summary.name,
                    "active": summary.active,
                    "filter_count": summary.filter_count,
                })
            })
            .collect::<Vec<_>>();
        println!(
            "{}",
            serde_json::to_string_pretty(&payload)
                .context("failed encoding logs profiles list json")?
        );
        return Ok(0);
    }

    for summary in summaries {
        let marker = if summary.active { "*" } else { " " };
        println!(
            "{marker} {} ({} filters)",
            summary.name, summary.filter_count
        );
    }
    Ok(0)
}

pub fn run_logs_profiles_show(profile: Option<&str>, as_json: bool) -> Result<u8> {
    let details = moosicbox_log_watch::profile_show(&logs_watch_state_file_path(), profile)?;
    if as_json {
        println!(
            "{}",
            serde_json::to_string_pretty(&serde_json::json!({
                "name": details.name,
                "active": details.active,
                "quick_filter": details.quick_filter,
                "since": details.since,
                "lines": details.lines,
                "selected_filter_index": details.selected_filter_index,
                "filters": details.filters,
            }))
            .context("failed encoding logs profile json")?
        );
        return Ok(0);
    }

    println!("profile: {}", details.name);
    println!("active: {}", if details.active { "yes" } else { "no" });
    println!(
        "lines: {}",
        details
            .lines
            .map_or_else(|| "(default)".to_string(), |value| value.to_string())
    );
    println!("since: {}", details.since.as_deref().unwrap_or("(none)"));
    println!(
        "quick filter: {}",
        details.quick_filter.as_deref().unwrap_or("(none)")
    );
    println!("filters:");
    if details.filters.is_empty() {
        println!("  (none)");
    } else {
        for filter in &details.filters {
            let kind = match filter.kind {
                InternalLogFilterKind::Include => "include",
                InternalLogFilterKind::Exclude => "exclude",
            };
            let case = match filter.case_mode {
                InternalLogFilterCaseMode::Sensitive => "case-sensitive",
                InternalLogFilterCaseMode::Insensitive => "case-insensitive",
            };
            let enabled = if filter.enabled {
                "enabled"
            } else {
                "disabled"
            };
            println!("  - {kind} /{}/ ({case}, {enabled})", filter.pattern);
        }
    }
    Ok(0)
}

pub fn run_logs_profiles_delete(profile: &str) -> Result<u8> {
    moosicbox_log_watch::profile_delete(&logs_watch_state_file_path(), profile)?;
    println!("deleted profile '{profile}'");
    Ok(0)
}

pub fn run_logs_profiles_rename(from: &str, to: &str) -> Result<u8> {
    moosicbox_log_watch::profile_rename(&logs_watch_state_file_path(), from, to)?;
    println!("renamed profile '{from}' -> '{to}'");
    Ok(0)
}

pub fn active_log_file_path() -> PathBuf {
    let segmented = ConfigPaths::default()
        .logs_dir()
        .join("server")
        .join("latest")
        .join("latest.log");
    if segmented.exists() {
        return segmented;
    }
    moosicbox_log_watch::active_log_file_path(
        &ConfigPaths::default().logs_dir(),
        BMUX_LOG_FILE_PREFIX,
    )
}

fn active_log_watch_dir() -> PathBuf {
    let segmented_dir = ConfigPaths::default()
        .logs_dir()
        .join("server")
        .join("latest");
    if segmented_dir.exists() {
        segmented_dir
    } else {
        ConfigPaths::default().logs_dir()
    }
}

fn active_log_watch_prefix() -> &'static str {
    if ConfigPaths::default()
        .logs_dir()
        .join("server")
        .join("latest")
        .exists()
    {
        "server"
    } else {
        BMUX_LOG_FILE_PREFIX
    }
}

fn logs_watch_state_file_path() -> PathBuf {
    ConfigPaths::default()
        .state_dir()
        .join("runtime")
        .join("logs-watch-state.json")
}