use bkmr::infrastructure::di::ServiceContainer;
use bkmr::config::{load_settings, Settings, ConfigSource};
use bkmr::cli::args::{Cli, Commands};
use bkmr::infrastructure::repositories::sqlite::{migration, repository::SqliteBookmarkRepository};
use bkmr::infrastructure::embeddings::DummyEmbedding;
use bkmr::cli::bookmark_commands::pre_fill_database;
use bkmr::util::helper::confirm;
use bkmr::exitcode;
use clap::Parser;
use crossterm::style::Stylize;
use termcolor::{ColorChoice, StandardStream};
use tracing::{debug, info, instrument};
use std::path::Path;
use std::fs;
use tracing_subscriber::{
filter::{filter_fn, LevelFilter},
fmt::{self, format::FmtSpan},
prelude::*,
};
#[instrument]
fn main() {
let stderr = StandardStream::stderr(ColorChoice::Always);
let cli = Cli::parse();
let no_color = cli.no_color || matches!(cli.command, Some(Commands::Lsp { .. }));
setup_logging(cli.debug, no_color);
let config_path_ref = cli.config.as_deref();
let settings = load_settings(config_path_ref)
.unwrap_or_else(|e| {
debug!("Failed to load settings: {}. Using defaults.", e);
Settings::default()
});
if cli.openai {
debug!("OpenAI embeddings requested via CLI flag");
}
if let Some(result) = handle_database_independent_operations(cli.clone(), &settings) {
if let Err(e) = result {
eprintln!("{}", format!("Error: {}", e).red());
std::process::exit(exitcode::USAGE);
}
return;
}
let service_container = match ServiceContainer::new(&settings, cli.openai) {
Ok(container) => container,
Err(e) => {
eprintln!("{}: {}", "Failed to create service container".red(), e);
std::process::exit(exitcode::USAGE);
}
};
if let Err(e) = execute_command_with_services(stderr, cli, service_container, settings) {
eprintln!("{}", format!("Error: {}", e).red());
std::process::exit(exitcode::USAGE);
}
}
fn handle_create_db_command(cli: Cli, settings: &Settings) -> Result<(), Box<dyn std::error::Error>> {
if let Commands::CreateDb { path, pre_fill } = cli.command.unwrap() {
let db_path = match path {
Some(p) => p,
None => {
let configured_path = &settings.db_url;
if settings.config_source == ConfigSource::Default {
eprintln!(
"{}",
"Warning: Using default database path. No configuration found.".yellow()
);
eprintln!("Default path: {}", configured_path);
eprintln!(
"Consider creating a configuration file at ~/.config/bkmr/config.toml"
);
eprintln!("or setting the BKMR_DB_URL environment variable.");
if !confirm("Continue with default database location?") {
eprintln!("Database creation cancelled.");
return Ok(());
}
}
configured_path.clone()
}
};
if Path::new(&db_path).exists() {
return Err(format!(
"Database already exists at: {}. Please choose a different path or delete the existing file.",
db_path
).into());
}
if let Some(parent) = Path::new(&db_path).parent() {
if !parent.exists() {
fs::create_dir_all(parent).map_err(|e| {
format!("Failed to create parent directories: {}", e)
})?;
}
}
eprintln!("Creating new database at: {}", db_path);
let repository = SqliteBookmarkRepository::from_url(&db_path)
.map_err(|e| format!("Failed to create repository: {}", e))?;
let mut conn = repository.get_connection()
.map_err(|e| format!("Failed to get database connection: {}", e))?;
migration::init_db(&mut conn)
.map_err(|e| format!("Failed to initialize database: {}", e))?;
repository.empty_bookmark_table()
.map_err(|e| format!("Failed to empty bookmark table: {}", e))?;
eprintln!("Database created successfully at: {}", db_path);
if pre_fill {
eprintln!("Pre-filling database with demo entries...");
let embedder = DummyEmbedding;
pre_fill_database(&repository, &embedder)
.map_err(|e| format!("Failed to pre-fill database: {}", e))?;
eprintln!("Database pre-filled with demo entries.");
}
}
Ok(())
}
fn handle_database_independent_operations(
cli: Cli,
settings: &Settings
) -> Option<Result<(), Box<dyn std::error::Error>>> {
if cli.generate_config {
return Some(handle_generate_config());
}
match cli.command.as_ref() {
Some(Commands::CreateDb { .. }) => {
Some(handle_create_db_command(cli, settings))
},
Some(Commands::Completion { shell }) => {
Some(handle_completion_command(shell.clone()))
},
_ => None, }
}
fn handle_generate_config() -> Result<(), Box<dyn std::error::Error>> {
println!("{}", bkmr::config::generate_default_config());
Ok(())
}
fn handle_completion_command(shell: String) -> Result<(), Box<dyn std::error::Error>> {
match shell.to_lowercase().as_str() {
"bash" => {
eprintln!("# Outputting bash completion script for bkmr");
eprintln!("# To use, run one of:");
eprintln!("# - eval \"$(bkmr completion bash)\" # one-time use");
eprintln!("# - bkmr completion bash >> ~/.bashrc # add to bashrc");
eprintln!(
"# - bkmr completion bash > /etc/bash_completion.d/bkmr # system-wide install"
);
eprintln!("#");
}
"zsh" => {
eprintln!("# Outputting zsh completion script for bkmr");
eprintln!("# To use, run one of:");
eprintln!("# - eval \"$(bkmr completion zsh)\" # one-time use");
eprintln!(
"# - bkmr completion zsh > ~/.zfunc/_bkmr # save to fpath directory"
);
eprintln!("# - echo 'fpath=(~/.zfunc $fpath)' >> ~/.zshrc # add dir to fpath if needed");
eprintln!("# - echo 'autoload -U compinit && compinit' >> ~/.zshrc # load completions");
eprintln!("#");
}
"fish" => {
eprintln!("# Outputting fish completion script for bkmr");
eprintln!("# To use, run one of:");
eprintln!("# - bkmr completion fish | source # one-time use");
eprintln!("# - bkmr completion fish > ~/.config/fish/completions/bkmr.fish # permanent install");
eprintln!("#");
}
_ => {}
}
match bkmr::cli::completion::generate_completion(&shell) {
Ok(_) => Ok(()),
Err(e) => Err(format!("Failed to generate completion script: {}", e).into()),
}
}
fn execute_command_with_services(
stderr: StandardStream,
cli: Cli,
services: ServiceContainer,
settings: Settings,
) -> Result<(), Box<dyn std::error::Error>> {
bkmr::cli::execute_command_with_services(stderr, cli, services, &settings)
.map_err(|e| format!("Command execution failed: {}", e).into())
}
fn setup_logging(verbosity: u8, no_color: bool) {
debug!("INIT: Attempting logger init from main.rs");
let filter = match verbosity {
0 => LevelFilter::WARN,
1 => LevelFilter::INFO,
2 => LevelFilter::DEBUG,
3 => LevelFilter::TRACE,
_ => {
eprintln!("Don't be crazy, max is -d -d -d");
LevelFilter::TRACE
}
};
let noisy_modules = [
"skim",
"html5ever",
"reqwest",
"mio",
"want",
"tuikit",
"hyper_util",
];
let module_filter = filter_fn(move |metadata| {
!noisy_modules
.iter()
.any(|name| metadata.target().starts_with(name))
});
let fmt_layer = fmt::layer()
.with_writer(std::io::stderr) .with_target(true)
.with_ansi(!no_color) .with_thread_names(false)
.with_span_events(FmtSpan::ENTER)
.with_span_events(FmtSpan::CLOSE);
let filtered_layer = fmt_layer.with_filter(filter).with_filter(module_filter);
tracing_subscriber::registry().with(filtered_layer).init();
match filter {
LevelFilter::INFO => info!("Debug mode: info"),
LevelFilter::DEBUG => debug!("Debug mode: debug"),
LevelFilter::TRACE => debug!("Debug mode: trace"),
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn given_cli_command_when_verify_then_debug_asserts_pass() {
use clap::CommandFactory;
Cli::command().debug_assert()
}
}