monothink 0.5.2

A totally non-agentic coding assistant
/*
 * This file is part of monothink.
 *
 * monothink is free software: you can redistribute it and/or modify it under the terms
 * of the GNU General Public License as published by the Free Software Foundation,
 * either version 3 of the License, or (at your option) any later version.
 *
 * monothink is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with monothink.
 * If not, see <https://www.gnu.org/licenses/>.
 */
mod common;
mod config;
mod llm;
mod session;
mod ui;

use crate::{llm::Llm, session::Session, ui::Ui};
use anyhow::Result;
use crossterm::{
    event::{KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags},
    execute,
};
use std::io::stdout;
use tracing_appender::non_blocking::WorkerGuard;
use tracing_appender::rolling::{RollingFileAppender, Rotation};
use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};

#[tokio::main]
async fn main() -> Result<()> {
    if std::env::args().any(|arg| arg == "--version" || arg == "-V") {
        println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
        return Ok(());
    }
    let _tracing_guard = init_tracing();
    tracing::info!("monothink starting");

    let base_dir = std::env::current_dir().expect("Cannot get current directory");
    let config = config::load_config()?;

    let terminal = ratatui::init();
    let mut stdout = stdout();
    execute!(
        stdout,
        PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES)
    )?;

    // NOTE: We wire everything together at top level to make it transparent
    // For such a simple actor model this is fine and easier to get an overview.
    let (session_tx, session_rx) = tokio::sync::mpsc::channel(128);
    let (ui_tx, ui_rx) = tokio::sync::mpsc::channel(128);
    let (llm_tx, llm_rx) = tokio::sync::mpsc::channel(32);

    let mut ui = Ui::new(session_tx.clone(), ui_rx, config.symbols.clone());
    let mut session = Session::new(ui_tx, llm_tx, session_rx, config.clone(), base_dir.clone());
    let mut llm = Llm::new(session_tx, llm_rx, config.general, config.tools, base_dir).await;

    tokio::spawn(async move {
        session.run().await;
    });
    tokio::spawn(async move {
        llm.run().await;
    });
    let app_result = ui.run(terminal).await;

    execute!(stdout, PopKeyboardEnhancementFlags)?;
    ratatui::restore();

    app_result
}

fn init_tracing() -> WorkerGuard {
    let project_dirs = directories::ProjectDirs::from("org", "codemonk", "monothink")
        .expect("Cannot locate project directories");
    let log_dir = project_dirs
        .state_dir()
        .unwrap_or_else(|| project_dirs.data_dir())
        .join("logs");
    std::fs::create_dir_all(&log_dir).expect("Cannot create log directory");

    let file_appender = RollingFileAppender::builder()
        .rotation(Rotation::DAILY)
        .filename_prefix("monothink.log")
        .max_log_files(7)
        .build(&log_dir)
        .expect("failed to initialize rolling file appender");
    let (writer, guard) = tracing_appender::non_blocking(file_appender);

    let filter =
        EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("monothink=info"));

    tracing_subscriber::registry()
        .with(
            tracing_subscriber::fmt::layer()
                .with_writer(writer)
                .with_ansi(false),
        )
        .with(filter)
        .init();

    guard
}