1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::Path;
5
6#[derive(Debug, Clone, Default, Serialize, Deserialize)]
7pub struct RecipeDesktop {
8 pub name: Option<String>,
9 pub generic_name: Option<String>,
10 pub categories: Option<Vec<String>>,
11 pub terminal: Option<bool>,
12 pub comment: Option<String>,
13}
14
15#[derive(Debug, Clone, Default, Serialize, Deserialize)]
16pub struct AppRecipe {
17 pub id: Option<String>,
18 pub name: Option<String>,
19 pub version: Option<String>,
20 pub probe_version: Option<bool>,
21 pub exec: Option<String>,
22 pub command: Option<String>,
23 pub icon: Option<String>,
24 pub desktop: Option<RecipeDesktop>,
25 pub args: Option<Vec<String>>,
26 pub env: Option<std::collections::BTreeMap<String, String>>,
27 pub working_dir: Option<String>,
28}
29
30#[derive(Debug, Clone, Default)]
31pub struct InstallInput {
32 pub id: Option<String>,
33 pub name: Option<String>,
34 pub version: Option<String>,
35 pub probe_version: Option<bool>,
36 pub exec: Option<String>,
37 pub command: Option<String>,
38 pub icon: Option<String>,
39 pub recipe: Option<AppRecipe>,
40 pub force: bool,
41 pub interactive_config: bool,
42}
43
44pub fn load_recipe(path: &Path) -> Result<AppRecipe> {
45 let text = fs::read_to_string(path).with_context(|| format!("failed to read recipe: {}", path.display()))?;
46 let recipe: AppRecipe = serde_yaml::from_str(&text)
47 .with_context(|| format!("failed to parse YAML recipe: {}", path.display()))?;
48 Ok(recipe)
49}
50
51pub fn sanitize_id(value: &str) -> String {
52 let lowered = value.trim().to_ascii_lowercase();
53 let mut out = String::new();
54 let mut last_dash = false;
55 for ch in lowered.chars() {
56 if ch.is_ascii_alphanumeric() || ch == '.' || ch == '_' {
57 out.push(ch);
58 last_dash = false;
59 } else if !last_dash {
60 out.push('-');
61 last_dash = true;
62 }
63 }
64 out.trim_matches('-').to_string()
65}
66
67pub fn sanitize_command(value: &str) -> String {
68 let lowered = value.trim().to_ascii_lowercase();
69 let mut out = String::new();
70 let mut last_dash = false;
71 for ch in lowered.chars() {
72 if ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' {
73 out.push(ch);
74 last_dash = false;
75 } else if !last_dash {
76 out.push('-');
77 last_dash = true;
78 }
79 }
80 out.trim_matches('-').to_string()
81}
82
83pub fn display_name_from_id(id: &str) -> String {
84 id.split(['-', '_', '.'])
85 .filter(|s| !s.is_empty())
86 .map(|s| {
87 let mut chars = s.chars();
88 match chars.next() {
89 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
90 None => String::new(),
91 }
92 })
93 .collect::<Vec<_>>()
94 .join(" ")
95}