squigit-brain 0.1.0

Gemini attachment preparation and title generation for Squigit
Documentation
// Copyright 2026 a7mddra
// SPDX-License-Identifier: Apache-2.0

use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};

use crate::provider::gemini::transport::types::GeminiRequest;

static REQUEST_LOG_SEQUENCE: AtomicU64 = AtomicU64::new(1);

pub(crate) struct GeminiRequestLogContext<'a> {
    pub(crate) kind: &'a str,
}

pub(crate) fn write_request_log(context: &GeminiRequestLogContext<'_>, request: &GeminiRequest) {
    let Some(path) = build_log_path(context) else {
        return;
    };

    let rendered = match serde_json::to_string_pretty(request) {
        Ok(value) => value,
        Err(error) => {
            if std::env::var_os("SQUIGIT_TUI_ACTIVE").is_none() {
                eprintln!("[SquigitBrain] Failed to render Gemini request log: {error}");
            }
            return;
        }
    };

    if let Err(error) = std::fs::write(&path, format!("{rendered}\n")) {
        if std::env::var_os("SQUIGIT_TUI_ACTIVE").is_none() {
            eprintln!(
                "[SquigitBrain] Failed to write Gemini request log {}: {}",
                path.display(),
                error
            );
        }
    }
}

fn build_log_path(context: &GeminiRequestLogContext<'_>) -> Option<PathBuf> {
    let logs_dir = std::env::var_os("SQUIGIT_LOG_DIR")
        .filter(|path| !path.is_empty())
        .map(PathBuf::from)?;
    if let Err(error) = std::fs::create_dir_all(&logs_dir) {
        if std::env::var_os("SQUIGIT_TUI_ACTIVE").is_none() {
            eprintln!(
                "[SquigitBrain] Failed to create logs directory {}: {}",
                logs_dir.display(),
                error
            );
        }
        return None;
    }
    let timestamp = chrono::Local::now().format("%Y%m%d-%H%M%S-%3f");
    let sequence = REQUEST_LOG_SEQUENCE.fetch_add(1, Ordering::Relaxed);
    let name = format!(
        "Squigit-LLM-{}-{}-{:04}.log",
        timestamp,
        sanitize_filename_component(context.kind),
        sequence
    );
    Some(logs_dir.join(name))
}

fn sanitize_filename_component(value: &str) -> String {
    let sanitized: String = value
        .chars()
        .map(|ch| {
            if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.') {
                ch
            } else {
                '_'
            }
        })
        .collect();

    let trimmed = sanitized.trim_matches('_');
    let final_value = if trimmed.is_empty() {
        "request"
    } else {
        trimmed
    };
    final_value.chars().take(48).collect()
}