use std::{
env, fs,
io::{self, IsTerminal, Write},
panic::AssertUnwindSafe,
path::Path,
process,
};
use color_eyre::{Result, eyre::Context};
use intelli_shell::{
app::App,
cli::{Cli, CliProcess, ConfigProcess, LogsProcess, Shell},
config::Config,
errors::{self, AppError},
format_error, logging,
process::{OutputInfo, ProcessOutput},
service::IntelliShellService,
storage::SqliteStorage,
utils::execute_shell_command_inherit,
};
use tokio_util::sync::CancellationToken;
const STATUS_DIRTY: &str = "DIRTY\n";
const STATUS_CLEAN: &str = "CLEAN\n";
const ACTION_EXECUTE: &str = "EXECUTE\n";
const ACTION_EXECUTED: &str = "EXECUTED\n";
const ACTION_REPLACE: &str = "REPLACE\n";
const BASH_INIT: &str = include_str!("./_shell/intelli-shell.bash");
const ZSH_INIT: &str = include_str!("./_shell/intelli-shell.zsh");
const FISH_INIT: &str = include_str!("./_shell/intelli-shell.fish");
const NUSHELL_INIT: &str = include_str!("./_shell/intelli-shell.nu");
const POWERSHELL_INIT: &str = include_str!("./_shell/intelli-shell.ps1");
#[tokio::main]
async fn main() -> Result<()> {
let (config, stats) = Config::init(env::var("INTELLI_CONFIG").ok().map(Into::into))?;
let (logs_path, logs_filter) = logging::resolve_path_and_filter(&config);
errors::init(
logs_filter.is_some().then(|| logs_path.clone()),
AssertUnwindSafe(async move {
let args = Cli::parse_extended();
let cancellation_token = CancellationToken::new();
let ctrl_c_token = cancellation_token.clone();
tokio::spawn(async move {
tokio::signal::ctrl_c().await.ok();
ctrl_c_token.cancel();
});
match args.process {
CliProcess::Init(init) => {
let script = match init.shell {
Shell::Bash => BASH_INIT,
Shell::Zsh => ZSH_INIT,
Shell::Fish => FISH_INIT,
Shell::Nushell => NUSHELL_INIT,
Shell::Powershell => POWERSHELL_INIT,
};
let output_info = OutputInfo {
stdout: Some(script.into()),
..Default::default()
};
return handle_output(
ProcessOutput::Output(output_info),
args.file_output,
args.skip_execution,
cancellation_token,
)
.await;
}
CliProcess::Config(ConfigProcess { path }) => {
if path {
println!("{}", stats.config_path.display());
} else {
edit::edit_file(&stats.config_path)
.wrap_err_with(|| format!("Failed to open config file: {}", stats.config_path.display()))?;
}
return Ok(());
}
CliProcess::Logs(LogsProcess { path }) => {
if path {
println!("{}", logs_path.display());
} else {
match fs::read_to_string(&logs_path) {
Ok(logs_content) if !logs_content.is_empty() => {
println!("{logs_content}");
}
_ => {
eprintln!(
"{}",
format_error!(
config.theme,
"No logs found on: {}\n\nMake sure logging is enabled in the config file: {}",
logs_path.display(),
stats.config_path.display()
)
)
}
}
}
return Ok(());
}
_ => (),
}
logging::init(logs_path, logs_filter)?;
tracing::info!("intelli-shell v{}", env!("CARGO_PKG_VERSION"));
match (stats.config_loaded, stats.default_config_path) {
(true, true) => tracing::info!("Loaded config from default path: {}", stats.config_path.display()),
(true, false) => tracing::info!("Loaded config from custom path: {}", stats.config_path.display()),
(false, true) => tracing::info!("No config found at default path: {}", stats.config_path.display()),
(false, false) => tracing::warn!("No config found at custom path: {}", stats.config_path.display()),
}
if stats.default_data_dir {
tracing::info!("Using default data dir: {}", config.data_dir.display());
} else {
tracing::info!("Using custom data dir: {}", config.data_dir.display());
}
let storage = SqliteStorage::new(&config.data_dir)
.await
.map_err(AppError::into_report)?;
let service = IntelliShellService::new(
storage,
config.tuning,
config.ai.clone(),
&config.data_dir,
config.check_updates,
);
let app_cancellation_token = cancellation_token.clone();
let output = App::new(app_cancellation_token)?
.run(config, service, args.process, args.extra_line)
.await?;
handle_output(output, args.file_output, args.skip_execution, cancellation_token).await
}),
)
.await
}
async fn handle_output(
output: ProcessOutput,
file_output_path: Option<String>,
skip_execution: bool,
cancellation_token: CancellationToken,
) -> Result<()> {
if let Some(path_str) = &file_output_path {
let mut file_content = String::new();
match &output {
ProcessOutput::Execute { cmd } => {
file_content.push_str(STATUS_CLEAN);
if skip_execution {
file_content.push_str(ACTION_EXECUTE);
file_content.push_str(cmd);
} else {
file_content.push_str(ACTION_EXECUTED);
}
}
ProcessOutput::Output(info) => {
if info.stderr.is_some() {
file_content.push_str(STATUS_DIRTY);
} else {
file_content.push_str(STATUS_CLEAN);
}
if let Some(cmd) = &info.fileout {
file_content.push_str(ACTION_REPLACE);
file_content.push_str(cmd);
}
}
}
let content = file_content.trim_end_matches('\n');
tracing::info!("[fileout]\n{content}");
let path_output = Path::new(&path_str);
if let Some(parent) = path_output.parent() {
fs::create_dir_all(parent)
.wrap_err_with(|| format!("Failed to create parent directories for: {}", parent.display()))?;
}
fs::write(path_output, content).wrap_err_with(|| format!("Failed to write to fileout path: {path_str}"))?;
}
match output {
ProcessOutput::Execute { cmd } => {
if !skip_execution {
let status =
execute_shell_command_inherit(&cmd, file_output_path.is_none(), cancellation_token).await?;
if !status.success() {
let code = status.code().unwrap_or(1);
tracing::info!("[exit code] {code}");
process::exit(code);
}
}
}
ProcessOutput::Output(info) => {
let use_color_stderr = should_use_color(io::stderr().is_terminal());
let use_color_stdout = should_use_color(io::stdout().is_terminal());
if let Some(stderr) = info.stderr {
let stderr_nocolor = strip_ansi_escapes::strip_str(&stderr);
tracing::info!("[stderr] {stderr_nocolor}");
let write_result = if use_color_stderr {
writeln!(io::stderr(), "{stderr}")
} else {
writeln!(io::stderr(), "{stderr_nocolor}")
};
if let Err(err) = write_result {
if err.kind() != io::ErrorKind::BrokenPipe {
return Err(err).wrap_err("Failed writing to stderr");
}
tracing::error!("Failed writing to stderr: Broken pipe");
}
}
if file_output_path.is_none()
&& let Some(stdout) = info.stdout
{
let stdout_nocolor = strip_ansi_escapes::strip_str(&stdout);
tracing::info!("[stdout] {stdout_nocolor}");
let write_result = if use_color_stdout {
writeln!(io::stdout(), "{stdout}")
} else {
writeln!(io::stdout(), "{stdout_nocolor}")
};
if let Err(err) = write_result {
if err.kind() != io::ErrorKind::BrokenPipe {
return Err(err).wrap_err("Failed writing to stdout");
}
tracing::error!("Failed writing to stdout: Broken pipe");
}
}
if info.failed {
tracing::info!("[exit code] 1");
process::exit(1);
}
}
}
Ok(())
}
fn should_use_color(stream_is_tty: bool) -> bool {
if env::var("NO_COLOR").is_ok() {
return false;
}
if let Ok(force_val) = env::var("CLICOLOR_FORCE")
&& !force_val.is_empty()
&& force_val != "0"
{
return true;
}
if let Ok(clicolor_val) = env::var("CLICOLOR")
&& clicolor_val == "0"
{
return false;
}
stream_is_tty
}