use anyhow::{Context, Result};
use std::io::{IsTerminal, Write};
use tracing_subscriber::{EnvFilter, fmt};
pub fn init_logging() -> Result<()> {
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
fmt()
.with_env_filter(filter)
.with_target(false)
.with_writer(std::io::stderr)
.init();
Ok(())
}
pub fn init_logging_for_cli() -> Result<()> {
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("error"));
fmt()
.with_env_filter(filter)
.with_target(false)
.with_writer(std::io::stderr) .init();
Ok(())
}
#[inline(always)]
pub fn inject_custom_ca_certs(cert_path: &Option<String>) {
if let Some(path) = cert_path {
#[expect(
unsafe_code,
reason = "std::env::set_var is unsafe in Rust 2024. fastembed 5.13 and \
hf-hub expose no API to supply a CA bundle (InitOptions has no \
TLS options; ApiBuilder is constructed internally), so \
SSL_CERT_FILE is the only mechanism. Called once from main() \
before the Tokio runtime starts, so no other thread can observe \
the mutation."
)]
unsafe {
std::env::set_var("SSL_CERT_FILE", path);
}
tracing::info!("Injected custom CA certificate path: {}", path);
}
}
pub fn print_with_pager(content: &str) {
use std::process::{Command, Stdio};
if std::io::stdout().is_terminal()
&& let Ok(mut child) = Command::new("less")
.arg("-R")
.arg("-e")
.stdin(Stdio::piped())
.spawn()
{
if let Some(mut stdin) = child.stdin.take() {
let _ = stdin.write_all(content.as_bytes());
}
let _ = child.wait();
return;
}
println!("{}", content);
}
pub fn format_output(
json_value: serde_json::Value,
output_format: crate::config::OutputFormat,
) -> String {
match output_format {
crate::config::OutputFormat::Table => {
if json_value.is_null() {
return "No matching code found for your query.".to_string();
}
crate::cli_tools::formatters::format_search_table(&json_value)
}
crate::config::OutputFormat::Json => {
serde_json::to_string_pretty(&json_value).unwrap_or_default()
}
crate::config::OutputFormat::Markdown => {
crate::cli_tools::formatters::format_search_results(&json_value)
}
}
}
pub fn format_callers_output(
entity_name: &str,
json_value: serde_json::Value,
output_format: crate::config::OutputFormat,
) -> String {
match output_format {
crate::config::OutputFormat::Table => {
crate::cli_tools::formatters::format_callers_table(entity_name, &json_value)
}
crate::config::OutputFormat::Json => {
serde_json::to_string_pretty(&json_value).unwrap_or_default()
}
crate::config::OutputFormat::Markdown => {
crate::cli_tools::format_references_result(entity_name, &json_value)
}
}
}
pub fn format_explore_output(
file_path: &str,
json_value: serde_json::Value,
output_format: crate::config::OutputFormat,
) -> String {
match output_format {
crate::config::OutputFormat::Table => {
crate::cli_tools::formatters::format_explore_table(file_path, &json_value)
}
crate::config::OutputFormat::Json => {
serde_json::to_string_pretty(&json_value).unwrap_or_default()
}
crate::config::OutputFormat::Markdown => {
crate::cli_tools::format_file_entities(file_path, &json_value)
}
}
}
pub fn calculate_rayon_threads(threads: Option<usize>) -> usize {
let cpus = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4);
threads.unwrap_or(cpus.saturating_sub(1).max(2))
}
pub fn configure_rayon(threads: Option<usize>) -> Result<usize> {
let cpus = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4);
let thread_count = calculate_rayon_threads(threads);
rayon::ThreadPoolBuilder::new()
.num_threads(thread_count)
.build_global()
.context("Failed to initialize Rayon thread pool")?;
tracing::info!(
"Rayon thread pool initialized with {thread_count} threads ({cpus} logical CPUs)"
);
Ok(thread_count)
}
#[expect(
clippy::cognitive_complexity,
reason = "Banner printing is sequential formatting logic"
)]
pub fn print_startup_banner(cfg: &crate::config::Config, rayon_threads: usize) {
let cpus = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4);
tracing::info!(
"knot indexer starting (v{} - parallel streaming + watch mode)",
env!("CARGO_PKG_VERSION")
);
tracing::info!("Repository path : {}", cfg.repo_path);
tracing::info!("Repository name : {}", cfg.repo_name);
tracing::info!("Logical CPUs : {cpus}");
tracing::info!("Rayon threads : {rayon_threads}");
tracing::info!("Batch size : {}", cfg.batch_size);
tracing::info!("Ingest workers : {}", cfg.ingest_concurrency);
tracing::info!("Clean mode : {}", cfg.clean);
tracing::info!("Watch mode : {}", cfg.watch);
tracing::info!(
"Qdrant : {} / {}",
cfg.qdrant_url,
cfg.qdrant_collection
);
tracing::info!("Neo4j : {}", cfg.neo4j_uri);
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static ENV_MUTEX: Mutex<()> = Mutex::new(());
#[test]
fn test_inject_custom_ca_certs_none() {
let _lock = ENV_MUTEX.lock().unwrap();
temp_env::with_var("SSL_CERT_FILE", None::<&str>, || {
let original = std::env::var("SSL_CERT_FILE").ok();
inject_custom_ca_certs(&None);
assert_eq!(std::env::var("SSL_CERT_FILE").ok(), original);
});
}
#[test]
fn test_inject_custom_ca_certs_some() {
let _lock = ENV_MUTEX.lock().unwrap();
temp_env::with_var("SSL_CERT_FILE", None::<&str>, || {
let test_path = "/path/to/test/ca-bundle.crt".to_string();
inject_custom_ca_certs(&Some(test_path.clone()));
assert_eq!(std::env::var("SSL_CERT_FILE").ok(), Some(test_path));
});
}
#[test]
fn test_inject_custom_ca_certs_overwrites_previous() {
let _lock = ENV_MUTEX.lock().unwrap();
temp_env::with_var("SSL_CERT_FILE", None::<&str>, || {
let first = "/first/path.pem".to_string();
let second = "/second/path.pem".to_string();
inject_custom_ca_certs(&Some(first));
inject_custom_ca_certs(&Some(second.clone()));
assert_eq!(std::env::var("SSL_CERT_FILE").ok(), Some(second));
});
}
#[test]
fn test_format_output_null_returns_no_match_message() {
let result = format_output(serde_json::Value::Null, crate::config::OutputFormat::Table);
assert_eq!(result, "No matching code found for your query.");
}
#[test]
fn test_format_output_json_pretty_print() {
let json = serde_json::json!({"name": "Test", "kind": "class"});
let result = format_output(json.clone(), crate::config::OutputFormat::Json);
assert!(result.contains("Test"));
assert!(result.contains("class"));
}
#[test]
fn test_format_callers_output_json() {
let json = serde_json::json!({
"calls": [{"name": "caller1"}],
"extends": [],
"implements": [],
"references": []
});
let result = format_callers_output("MyEntity", json, crate::config::OutputFormat::Json);
assert!(result.contains("caller1"));
}
#[test]
fn test_format_explore_output_json() {
let json = serde_json::json!([{"name": "MyClass", "kind": "class"}]);
let result = format_explore_output("test.java", json, crate::config::OutputFormat::Json);
assert!(result.contains("MyClass"));
}
#[test]
fn test_calculate_rayon_threads_default_formula() {
let cpus = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4);
let thread_count = calculate_rayon_threads(None);
assert_eq!(thread_count, cpus.saturating_sub(1).max(2));
}
#[test]
fn test_calculate_rayon_threads_explicit_override() {
let thread_count = calculate_rayon_threads(Some(8));
assert_eq!(thread_count, 8);
}
#[test]
fn test_calculate_rayon_threads_explicit_zero() {
let thread_count = calculate_rayon_threads(Some(0));
assert_eq!(thread_count, 0);
}
}