mod collect;
mod render;
mod resolve;
use crate::config::Config;
use crate::status::FileStatus;
use crate::{apps, colors, path_display, shells};
use anyhow::{Context, Result, bail};
use resolve::InfoRef;
pub(crate) async fn print_app_inspection_diff(
config: &Config,
inspection: &shine_core::runtime::AppFileInspection,
) -> Result<()> {
let item = collect::app_info_file_from_inspection(inspection.clone())
.context("App inspection destination is unavailable")?;
render::print_app_expected_diff(config, &item).await
}
pub(crate) struct UpdateDiffs {
app_files: Vec<collect::AppInfoFile>,
shell_files: Vec<collect::ShellInfoFile>,
}
impl UpdateDiffs {
pub(crate) async fn collect(config: &Config, run_generators: bool) -> Result<Self> {
Ok(Self {
app_files: collect::collect_app_files(config, run_generators, Vec::new()).await?,
shell_files: collect::collect_shell_files(config).await?,
})
}
pub(crate) async fn collect_with_app_inspections(
config: &Config,
inspections: Vec<shine_core::runtime::AppFileInspection>,
) -> Result<Self> {
Ok(Self {
app_files: collect::app_info_files_from_inspections(inspections),
shell_files: collect::collect_shell_files(config).await?,
})
}
pub(crate) async fn print_shell_for_row(&self, config: &Config, label: &str) -> Result<()> {
for file in self.shell_files.iter().filter(|file| {
file.status == "update available"
&& format!("{}/{}", file.category.name, file.file.command_name) == label
}) {
render::print_shell_update_diff(config, file).await?;
}
Ok(())
}
pub(crate) async fn print_app_for_row(&self, config: &Config, label: &str) -> Result<()> {
for file in self.app_files.iter().filter(|file| {
if file.status != FileStatus::UpdateAvail {
return false;
}
if file.category.has_explicit_files
&& file.category.list_mode == crate::apps::AppListMode::Files
{
app_file_label(file) == label
} else {
file.category.name == label
}
}) {
render::print_app_update_diff(config, file).await?;
}
Ok(())
}
}
fn app_file_label(file: &collect::AppInfoFile) -> String {
file.file
.display_name
.clone()
.unwrap_or_else(|| format!("{}/{}", file.category.name, file.file.source_rel.display()))
}
fn app_categories_for_refs(refs: &[InfoRef]) -> Vec<String> {
refs.iter()
.filter_map(|item| match item {
InfoRef::AppCategory(category) | InfoRef::AppFile { category, .. } => {
Some(category.clone())
}
InfoRef::ShellCategory(_) | InfoRef::ShellFile { .. } => None,
})
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect()
}
fn print_generator_notice(
files: &[collect::AppInfoFile],
categories: &[String],
run_generators: bool,
) -> bool {
let relevant = files
.iter()
.filter(|file| categories.is_empty() || categories.contains(&file.category.name))
.collect::<Vec<_>>();
let not_evaluated = relevant
.iter()
.any(|file| file.assessment_diagnostic == Some("app_generator_not_evaluated"));
let failures = relevant
.iter()
.filter(|file| {
matches!(
file.assessment_diagnostic,
Some("app_generator_evaluation_failed" | "app_generator_trust_required")
)
})
.collect::<Vec<_>>();
if !run_generators && not_evaluated {
println!();
println!(
"{}",
colors::yellow("! Generated configuration was not evaluated.")
);
println!(
"{}",
colors::dim(
" Update status may be incomplete. Re-run with `--run-generators` to evaluate generator output."
)
);
}
for file in &failures {
println!();
let reason = if file.assessment_diagnostic == Some("app_generator_trust_required") {
"generator trust required"
} else {
"generator evaluation failed"
};
println!(
"{}",
colors::yellow(&format!(
"! {}/{}: {reason}",
file.category.name,
file.file.source_rel.display()
))
);
if let Some(error) = &file.assessment_error {
println!("{}", colors::dim(&format!(" {error}")));
}
}
!failures.is_empty()
}
pub async fn handle_update_target(
config: &Config,
target: &str,
run_generators: bool,
) -> Result<()> {
crate::config::print_presets_note(config);
let mut diffs = UpdateDiffs::collect(config, false).await?;
if diffs.app_files.is_empty() && diffs.shell_files.is_empty() {
bail!("nothing installed yet. Run `shine shell install` or `shine app install`.");
}
let candidates = resolve::build_candidates(&diffs.app_files, &diffs.shell_files);
let refs = resolve::resolve_target(target, &candidates)?;
if run_generators {
let categories = app_categories_for_refs(&refs);
if categories.is_empty() {
bail!("--run-generators requires an App target");
}
diffs.app_files = collect::collect_app_files(config, true, categories).await?;
}
let generator_categories = app_categories_for_refs(&refs);
let mut printed = false;
for item in refs {
match item {
InfoRef::AppCategory(category) => {
let mut files = diffs
.app_files
.iter()
.filter(|file| {
file.category.name == category && file.status == FileStatus::UpdateAvail
})
.collect::<Vec<_>>();
files.sort_by_key(|file| file.file.source_rel.clone());
for file in files {
print_update_separator(printed);
print_app_update_row(config, file);
render::print_app_update_diff(config, file).await?;
printed = true;
}
}
InfoRef::AppFile { category, source } => {
if let Some(file) = diffs.app_files.iter().find(|file| {
file.category.name == category
&& file.file.source_rel == source
&& file.status == FileStatus::UpdateAvail
}) {
print_app_update_row(config, file);
render::print_app_update_diff(config, file).await?;
printed = true;
}
}
InfoRef::ShellCategory(category) => {
let mut files = diffs
.shell_files
.iter()
.filter(|file| {
file.category.name == category && file.status == "update available"
})
.collect::<Vec<_>>();
files.sort_by_key(|file| file.file.command_name.clone());
for file in files {
print_update_separator(printed);
print_shell_update_row(file);
render::print_shell_update_diff(config, file).await?;
printed = true;
}
}
InfoRef::ShellFile { category, command } => {
if let Some(file) = diffs.shell_files.iter().find(|file| {
file.category.name == category
&& file.file.command_name == command
&& file.status == "update available"
}) {
print_shell_update_row(file);
render::print_shell_update_diff(config, file).await?;
printed = true;
}
}
}
}
let generator_failed =
print_generator_notice(&diffs.app_files, &generator_categories, run_generators);
let generator_incomplete = diffs.app_files.iter().any(|file| {
generator_categories.contains(&file.category.name)
&& file.assessment_diagnostic == Some("app_generator_not_evaluated")
});
if !printed && !generator_incomplete {
println!(
"{}",
colors::dim(&format!("No update available for {target}."))
);
}
if generator_failed {
bail!("one or more App generators could not be evaluated");
}
Ok(())
}
fn print_update_separator(printed: bool) {
if printed {
println!();
}
}
fn print_shell_update_row(file: &collect::ShellInfoFile) {
println!(
" {} {}/{} {}",
colors::symbol("↑"),
file.category.name,
file.file.command_name,
colors::status_label("update available", "↑"),
);
}
fn print_app_update_row(config: &Config, file: &collect::AppInfoFile) {
println!(
" {} {} {} {} {}",
colors::symbol("↑"),
app_file_label(file),
colors::dim("→"),
colors::dim(&path_display::format_home(
&file.destination,
&config.home_dir
)),
colors::status_label("update available", "↑"),
);
}
pub async fn handle_info(
config: &Config,
target: &str,
diff: bool,
verbose: bool,
run_generators: bool,
) -> Result<()> {
let mut app_files = collect::collect_app_files(config, false, Vec::new()).await?;
let shell_files = collect::collect_shell_files(config).await?;
let candidates = resolve::build_candidates(&app_files, &shell_files);
let refs = match resolve::resolve_target(target, &candidates) {
Ok(refs) => refs,
Err(installed_error) => {
if verbose || (diff && !run_generators) {
return Err(installed_error.context(
"--verbose requires an installed target; --diff for an available App also requires --run-generators",
));
}
return handle_available_info(config, target, installed_error, run_generators, diff)
.await;
}
};
if run_generators {
let categories = app_categories_for_refs(&refs);
if categories.is_empty() {
bail!("--run-generators requires an App target");
}
app_files = collect::collect_app_files(config, true, categories).await?;
}
let generator_categories = app_categories_for_refs(&refs);
crate::config::print_presets_note(config);
let mut first = true;
for item in refs {
if !first {
println!();
}
first = false;
match item {
InfoRef::AppCategory(category) => {
let mut files: Vec<_> = app_files
.iter()
.filter(|f| f.category.name == category)
.cloned()
.collect();
files.sort_by_key(|f| f.file.source_rel.clone());
for (index, file) in files.iter().enumerate() {
if index > 0 {
println!();
}
render::print_app_file(config, file, diff, verbose).await?;
}
}
InfoRef::AppFile { category, source } => {
let file = app_files
.iter()
.find(|f| f.category.name == category && f.file.source_rel == source)
.ok_or_else(|| anyhow::anyhow!("installed app config not found"))?;
render::print_app_file(config, file, diff, verbose).await?;
}
InfoRef::ShellCategory(category) => {
let mut files: Vec<_> = shell_files
.iter()
.filter(|f| f.category.name == category)
.cloned()
.collect();
files.sort_by_key(|f| f.file.command_name.clone());
for (index, file) in files.iter().enumerate() {
if index > 0 {
println!();
}
render::print_shell_file(config, file, diff, verbose).await?;
}
}
InfoRef::ShellFile { category, command } => {
let file = shell_files
.iter()
.find(|f| f.category.name == category && f.file.command_name == command)
.ok_or_else(|| anyhow::anyhow!("installed shell preset not found"))?;
render::print_shell_file(config, file, diff, verbose).await?;
}
}
}
let generator_failed =
print_generator_notice(&app_files, &generator_categories, run_generators);
if generator_failed {
bail!("one or more App generators could not be evaluated");
}
Ok(())
}
async fn handle_available_info(
config: &Config,
target: &str,
installed_error: anyhow::Error,
run_generators: bool,
diff: bool,
) -> Result<()> {
let target = target.trim();
if let Some(rest) = target.strip_prefix("app/") {
let category = rest.split('/').next().unwrap_or_default();
if category.is_empty() || rest.contains('/') {
return Err(installed_error);
}
return Box::pin(apps::handle_info(config, category, run_generators, diff)).await;
}
if let Some(rest) = target.strip_prefix("shell/") {
if rest.is_empty() {
return Err(installed_error);
}
if run_generators {
bail!("--run-generators requires an App target");
}
return Box::pin(shells::handle_info(config, rest)).await;
}
let app_matches = apps::load_active_categories(config, Some(target))
.await?
.into_iter()
.any(|category| category.name == target);
let shell_categories = shells::metadata::load_active_categories(config, None).await?;
let shell_matches = shell_categories.iter().any(|category| {
category.name == target
|| category
.files
.iter()
.any(|file| file.command_name == target)
});
match (app_matches, shell_matches) {
(true, false) => Box::pin(apps::handle_info(config, target, run_generators, diff)).await,
(false, true) if run_generators => bail!("--run-generators requires an App target"),
(false, true) => Box::pin(shells::handle_info(config, target)).await,
(true, true) => {
bail!("ambiguous available target `{target}`; use `app/{target}` or `shell/{target}`")
}
(false, false) => Err(installed_error),
}
}