use anyhow::{Result, anyhow, bail, ensure};
use colored::Colorize;
use indexmap::IndexMap;
use std::{fs, time::Duration};
use crate::{
autocomplete,
backup::{Backup, load_backup, save_backup},
blueprints::{engine::BlueprintEngine, storage::Blueprints},
cli::{BackupArgs, CloneArgs, ImportArgs, ListArgs, NewArgs, OpenArgs, RemoveArgs, RenameArgs},
config::Config,
library::{CloneOptions, Library, validate_project_name},
platform::{self},
program::{LaunchOptions, launch_program},
terminal::{ask_dialog, generate_progress, is_terminal, print_done, print_error, print_title},
};
fn resolve_project_name(
project_name: &str,
config: &Config,
projects: &Library,
skip: bool,
) -> Option<String> {
if project_name == "-" && config.recent.enabled {
Some(config.recent.recent_project.clone())
} else if config.autocomplete.enabled && !skip {
let projects_list: Vec<&str> = projects.get_names().iter().map(|i| i.as_str()).collect();
autocomplete::autocomplete(project_name, &projects_list, config)
} else {
Some(project_name.to_string())
}
}
pub fn handle_new(args: NewArgs) -> Result<()> {
if args.template.is_some() {
bail!(
"Templates are no longer supported and has been replaced with blueprints. If you want to migrate your templates, use `kanri blueprints migrate-templates`."
);
}
let config = Config::load(platform::config_file())?;
let projects_dir = &config.options.projects_directory;
let mut projects = Library::new(projects_dir, config.options.display_hidden)?;
validate_project_name(&args.name)?;
projects.create(&args.name)?;
if let Some(blueprint) = args.blueprint {
let blueprints_dir = platform::blueprints_dir();
let blueprints = Blueprints::load_from_path(&blueprints_dir)?;
let blueprint_code = blueprints.get_blueprint(blueprint.clone()).map_err(|e| {
if let Err(er) = projects.delete(&args.name) {
return anyhow!(er.to_string());
}
anyhow!(e.to_string())
})?;
let project_dir = projects_dir.join(&args.name);
let engine = BlueprintEngine::init(
project_dir,
format!("{}.lua", blueprint),
args.name.clone(),
args.quiet,
)
.map_err(|e| anyhow!(e.to_string()))?;
if !args.quiet {
println!("Running blueprint engine for '{}' blueprint...", blueprint);
}
if let mlua::Result::Err(e) = engine.run(&blueprint_code) {
print_error(&format!("An error occurred in Lua engine: {}", e));
projects.delete(&args.name)?;
bail!("Failed to generate project from blueprint. See Lua error above.")
}
if !args.quiet {
print_done(
format!("Generated '{}' from blueprint '{}'.", args.name, blueprint,).as_str(),
);
}
return Ok(());
}
print_done(&format!(
"Created an empty project with name '{}'.",
args.name
));
Ok(())
}
pub fn handle_clone(args: CloneArgs) -> Result<()> {
let config = Config::load(platform::config_file())?;
let clone_options = CloneOptions {
remote: args.remote,
name: args.name,
branch: args.branch,
};
let projects = Library::new(
&config.options.projects_directory,
config.options.display_hidden,
)?;
projects.clone(&clone_options)?;
print_done("Repository has been cloned.");
Ok(())
}
pub fn handle_open(args: OpenArgs) -> Result<()> {
let config_path = platform::config_file();
let mut config = Config::load(&config_path)?;
let projects = Library::new(
&config.options.projects_directory,
config.options.display_hidden,
)?;
let name = resolve_project_name(&args.name, &config, &projects, args.skip_autocomplete)
.ok_or_else(|| anyhow!("Project not found."))?;
let project = projects.get(&name);
if project.is_none() {
return Err(anyhow!("Project not found."));
}
let path = project.unwrap();
if args.path {
println!("{}", path.to_string_lossy());
return Ok(());
}
if !is_terminal() {
return Err(anyhow!(
"Opening projects in non-interactive mode is not supported"
));
}
let profile = config.get_profile(&config.options.current_profile)?;
let (program, launch_args, fork_mode) = if args.shell {
(&profile.shell, Vec::<String>::new(), false)
} else {
(
&profile.editor,
profile.editor_args.clone(),
profile.editor_fork_mode,
)
};
ensure!(
!program.is_empty(),
"Required program is not specified in configuration file."
);
let mut launch_options = LaunchOptions {
program,
args: launch_args,
cwd: Some(path),
fork_mode,
quiet: false,
env: None,
};
if args.shell {
let env_map = Vec::from([(String::from("KANRI_SESSION"), "1".to_string())]);
launch_options.env = Some(env_map);
println!(
"{}",
"======== STARTING SHELL SESSION ========".bold().white()
);
}
launch_program(launch_options)?;
if args.shell {
println!(
"{}",
"======== SHELL SESSION ENDED ========".bold().white()
);
}
if config.recent.enabled && name != config.recent.recent_project {
config.recent.recent_project = name;
config.save(config_path)?;
}
if fork_mode {
print_done("Editor launched.");
return Ok(());
}
Ok(())
}
pub fn handle_list(args: ListArgs) -> Result<()> {
let config = Config::load(platform::config_file())?;
let projects = Library::new(
&config.options.projects_directory,
config.options.display_hidden,
)?;
if projects.is_empty() {
println!("No projects found.");
return Ok(());
}
let recent = &config.recent.recent_project;
if !args.pure {
print_title("Your projects");
}
for name in projects.get_names() {
if args.pure {
println!("{}", name);
} else {
let is_recent = if name == recent.as_str() {
"(recent)".dimmed()
} else {
"".dimmed()
};
println!(" {} {is_recent}", name);
}
}
Ok(())
}
pub fn handle_rename(args: RenameArgs) -> Result<()> {
let config = Config::load(platform::config_file())?;
let mut projects = Library::new(
&config.options.projects_directory,
config.options.display_hidden,
)?;
validate_project_name(&args.new_name)?;
projects.rename(&args.old_name, &args.new_name)?;
print_done(&format!(
"Project '{}' has been renamed to '{}'.",
args.old_name, args.new_name
));
Ok(())
}
pub fn handle_remove(args: RemoveArgs) -> Result<()> {
let config = Config::load(platform::config_file())?;
let mut projects = Library::new(
&config.options.projects_directory,
config.options.display_hidden,
)?;
let project_name = resolve_project_name(&args.name, &config, &projects, false)
.ok_or_else(|| anyhow!("Project not found."))?;
if is_terminal() {
if !args.yes
&& !ask_dialog(
&format!("Do you want to delete '{}'?", project_name),
false,
false,
)?
{
print_done("Canceled.");
return Ok(());
}
} else {
if !args.yes {
return Err(anyhow!(
"Confirmation with `--yes` is required for non-interactive sessions"
));
}
}
let spinner = generate_progress().with_message("Removing project...");
spinner.enable_steady_tick(Duration::from_millis(100));
let result = projects.delete(&project_name);
if let Err(e) = result {
spinner.finish_and_clear();
return Err(anyhow!(e));
}
spinner.finish_and_clear();
print_done(&format!("Project '{project_name}' has been removed."));
Ok(())
}
pub fn handle_backup(args: BackupArgs) -> Result<()> {
let config = Config::load(platform::config_file())?;
let blueprints_dir = platform::blueprints_dir();
let blueprints_list =
fs::read_dir(&blueprints_dir).map_err(|_| anyhow!("failed to read blueprints dir"))?;
let mut blueprints_map: IndexMap<String, String> = IndexMap::new();
for item in blueprints_list {
let entry = item.unwrap();
let entry_path = entry.path();
let entry_name_tmp = entry.file_name();
let entry_name = entry_name_tmp.to_string_lossy();
if entry_path.is_dir() || entry_path.extension().unwrap_or_default() != "lua" {
continue;
}
let blueprint_content = fs::read_to_string(&entry_path)
.map_err(|_| anyhow!("failed to read '{}' content", entry_name))?;
blueprints_map.insert(entry_name.to_string(), blueprint_content);
}
let backup_file_path = args.output_file.unwrap_or("kanri_backup.json".to_string());
let backup = Backup {
config,
blueprints: blueprints_map,
};
save_backup(&backup_file_path, backup)?;
print_done(&format!("Backup saved to `{backup_file_path}`."));
Ok(())
}
pub fn handle_import(args: ImportArgs) -> Result<()> {
if !args.yes
&& !ask_dialog(
"This action will overwrite your current configuration and will overwrite existing blueprints. Continue?",
false,
true,
)?
{
print_done("Aborting.");
return Ok(());
}
let backup = load_backup(args.file)?;
backup.config.save(platform::config_file())?;
let mut failed_blueprints: Vec<&str> = Vec::new();
if !backup.blueprints.is_empty() {
let blueprints_path = platform::blueprints_dir();
for (k, v) in backup.blueprints.iter() {
if k.is_empty() {
continue;
}
if k.contains('/') || k.contains('\\') {
print_error(&format!(
"Security error: blueprint '{}' contains path separators in it's name. Skipping it.",
k
));
failed_blueprints.push(k);
continue;
}
let blueprint_path = blueprints_path.join(k);
if let Err(e) = fs::write(&blueprint_path, v) {
print_error(&format!("Failed to write blueprint '{}': {}", k, e));
failed_blueprints.push(k);
}
}
}
print_done("Backup has been imported.");
Ok(())
}
const KANRI_ZEN: [&str; 10] = [
"Projects should be simple.",
"Each command does one thing well.",
"Configuration is explicit.",
"Sensible defaults guide the way.",
"The shell is a friend.",
"Blueprints accelerate your workflow.",
"Cross-platform by design.",
"Clear messages beat surprises.",
"Your editor is respected.",
"Enjoy your work.",
];
pub fn handle_zen() -> Result<()> {
println!("{}", "======== THE ZEN OF KANRI ========".bold().white());
for line in KANRI_ZEN.iter() {
println!(" {} {line}", "*".dimmed());
}
Ok(())
}