openlatch-client 0.1.8

The open-source security layer for AI agents — client forwarder
//! Shared "OpenLatch" header used by CLI commands.
//!
//! Two-line compact header:
//!
//! ```text
//! ▄▄▄ OpenLatch ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
//!     v0.0.1 · running · uptime 33s
//! ```
//!
//! Line 1 is the brand rule (ANSI cyan when color is enabled, plain `===`
//! fallback otherwise). Line 2 is `v{version}` followed by caller-supplied
//! context segments joined with ` · `.
//!
//! Silent in JSON mode and in quiet mode. Writes to stderr so it never
//! contaminates piped stdout (`--json`, redirected output).

use crate::cli::output::{OutputConfig, OutputFormat};

/// Brand label rendered between the leading and trailing fill. 11 columns
/// including the padding spaces (` OpenLatch `).
const LABEL: &str = " OpenLatch ";
/// Leading fill count — always 3 glyphs so the rule reads `▄▄▄ OpenLatch ▄…`.
const LEAD: usize = 3;

/// Fallback terminal width when detection fails (non-TTY, redirected stderr).
const FALLBACK_WIDTH: usize = 44;
/// Upper bound — past this the rule becomes visual noise rather than a frame.
const MAX_WIDTH: usize = 120;

/// Build the brand rule sized to the current terminal width. Renders as
/// `▄▄▄ OpenLatch ▄▄▄…▄` (Unicode) or `=== OpenLatch ===…=` (ASCII fallback),
/// with the trailing fill extended to fit the terminal.
fn build_rule(color: bool) -> String {
    let detected = terminal_size::terminal_size()
        .map(|(w, _)| w.0 as usize)
        .unwrap_or(FALLBACK_WIDTH);
    let min_width = LEAD + LABEL.chars().count() + 1;
    let width = detected.clamp(min_width, MAX_WIDTH);

    let fill = if color { '' } else { '=' };
    let lead: String = std::iter::repeat_n(fill, LEAD).collect();
    let tail_len = width - LEAD - LABEL.chars().count();
    let tail: String = std::iter::repeat_n(fill, tail_len).collect();

    if color {
        format!("\x1b[36m{lead}{LABEL}{tail}\x1b[0m")
    } else {
        format!("{lead}{LABEL}{tail}")
    }
}

/// Print the full three-line root banner (used by the no-subcommand path).
/// Matches the `--help` preamble but with a terminal-width rule.
pub fn print_full_banner(output: &OutputConfig) {
    if output.format == OutputFormat::Json || output.quiet {
        return;
    }
    println!("{}", build_rule(output.color));
    println!("    hook events → envelope → cloud");
    println!("    localhost:7443  ·  fail-open");
}

/// Print the two-line header.
///
/// - `parts`: context segments appended after the version on line 2. Empty
///   segments are skipped so callers can pass conditional values without
///   guarding at each call site.
///
/// Header is suppressed in JSON mode and when `--quiet` is set.
pub fn print(output: &OutputConfig, parts: &[&str]) {
    if output.format == OutputFormat::Json || output.quiet {
        return;
    }

    eprintln!("{}", build_rule(output.color));

    let version = env!("CARGO_PKG_VERSION");
    let mut line = format!("    v{version}");
    for part in parts.iter().filter(|p| !p.is_empty()) {
        line.push_str(" · ");
        line.push_str(part);
    }
    eprintln!("{line}");
}