use crate::{error::ProtoCliError, session::ProtoConsole};
use iocraft::prelude::element;
use starbase_console::ui::*;
use starbase_shell::{BoxedShell, ShellType};
use starbase_styles::color;
use starbase_utils::fs::{self, FsError};
use std::{
env::{self, consts},
io::{self, BufRead},
path::{Path, PathBuf},
};
use tracing::debug;
pub enum Export {
Path(Vec<String>),
Var(String, String),
}
pub fn find_profiles(shell: &BoxedShell, home_dir: &Path) -> Vec<PathBuf> {
debug!("Finding profile files for {}", shell);
if let Ok(profile_env) = env::var("PROTO_SHELL_PROFILE") {
return vec![PathBuf::from(profile_env)];
}
shell.get_profile_paths(home_dir)
}
pub fn find_first_profile(shell: &BoxedShell, home_dir: &Path) -> PathBuf {
for profile in find_profiles(shell, home_dir) {
if profile.exists() {
return profile;
}
}
shell.get_env_path(home_dir)
}
pub fn format_exports(shell: &BoxedShell, comment: &str, exports: Vec<Export>) -> String {
let newline = if consts::OS == "windows" {
"\r\n"
} else {
"\n"
};
let mut lines = vec![format!("{newline}# {comment}")];
for export in exports {
lines.push(match export {
Export::Path(paths) => shell.format_path_prepend(&paths),
Export::Var(key, value) => shell.format_env_set(&key, &value),
});
}
lines.join(newline)
}
pub fn update_profile(profile: &Path, contents: &str, env_var: &str) -> Result<(), FsError> {
debug!("Updating profile {} with {}", color::path(profile), env_var);
fs::append_file(profile, contents)?;
Ok(())
}
pub fn update_profile_if_not_setup(
profile: &Path,
contents: &str,
env_var: &str,
) -> Result<bool, FsError> {
if !profile.exists() {
update_profile(profile, contents, env_var)?;
return Ok(true);
}
debug!(
"Checking if profile {} has already been setup for {}",
color::path(profile),
env_var
);
let file = fs::open_file(profile)?;
let has_setup = io::BufReader::new(file)
.lines()
.any(|line| line.is_ok_and(|l| l.contains(env_var)));
if has_setup {
debug!("Profile already setup");
return Ok(false);
}
debug!("Not setup, continuing");
update_profile(profile, contents, env_var)?;
Ok(true)
}
pub async fn prompt_for_shell(console: &ProtoConsole) -> Result<ShellType, ProtoCliError> {
let options = ShellType::os_variants();
let mut selected_index = 0;
console
.render_interactive(element! {
Select(
label: "Which shell to use and configure?",
options: options
.iter()
.map(|item| SelectOption::new(item.to_string()))
.collect::<Vec<_>>(),
on_index: &mut selected_index,
)
})
.await?;
Ok(options[selected_index])
}
pub async fn prompt_for_shell_profile(
console: &ProtoConsole,
shell: &BoxedShell,
home_dir: &Path,
) -> Result<Option<PathBuf>, ProtoCliError> {
let profiles = find_profiles(shell, home_dir);
let mut options = profiles
.iter()
.map(|path| SelectOption::new(path.to_string_lossy()))
.collect::<Vec<_>>();
options.push(SelectOption::new("Other"));
options.push(SelectOption::new("None"));
let mut selected_index = 0;
let other_index = profiles.len();
let none_index = other_index + 1;
console
.render_interactive(element! {
Select(
label: "Which profile or config file to update?",
options,
on_index: Some(&mut selected_index),
)
})
.await?;
let selected_profile = if selected_index == none_index {
None
} else if selected_index == other_index {
let mut custom_path = String::new();
console
.render_interactive(element! {
Input(
label: "Custom profile path?",
on_value: &mut custom_path,
validate: |new_value: String| {
if new_value.is_empty() || !PathBuf::from(new_value).is_absolute() {
Some("An absolute path is required".into())
} else {
None
}
}
)
})
.await?;
Some(PathBuf::from(custom_path))
} else {
Some(profiles[selected_index].clone())
};
Ok(selected_profile)
}