use std::path::{Path, PathBuf};
use console::style;
use topcoat_font::fontsource::families;
use super::{
check_destination,
choice::{Database, Font, Icons, Interaction, Routing},
input::{
Answer, DEFAULT_ICON_SET, Input, Origin, Preset, Question, Sourced, font_family_id,
icon_set_id,
},
};
use crate::common::prompt;
const POPULAR_ICON_SETS: &[&str] = &[
"lucide",
"tabler",
"ph",
"heroicons",
"mdi",
"material-symbols",
"carbon",
"ri",
"bi",
"simple-icons",
];
const BRING_YOUR_OWN: &str = "set it up yourself";
pub fn destination(named: bool) -> Result<PathBuf, String> {
let path: String = cliclack::input("Directory")
.default_input("my-app")
.validate(move |path: &String| check_destination(Path::new(path), named))
.interact()
.map_err(|error| prompt::error(&error))?;
Ok(PathBuf::from(path))
}
pub fn ask(input: &mut Input) -> Result<(), String> {
let recommended = Preset::Recommended.choices();
while let Some(&question) = input.questions().first() {
let answer = match question {
Question::Routing => Answer::Routing(select(
"Routing style",
&[
(
Routing::Module,
"Module-based",
"route paths are derived from the Rust module tree",
),
(
Routing::Discover,
"Discovery",
"route paths are specified explicitly; handlers are registered \
automatically",
),
(
Routing::Manual,
"Manual",
"route paths are specified explicitly; handlers are registered by hand",
),
],
recommended.routing,
)?),
Question::Database => Answer::Database(select(
"Database integration",
&[
(Database::Toasty, "Toasty", "async ORM"),
(Database::None, "None", BRING_YOUR_OWN),
],
recommended.database,
)?),
Question::Interaction => Answer::Interaction(select(
"Browser interaction",
&[
(
Interaction::Topcoat,
"Topcoat runtime",
"signals, shards, and procedures",
),
(
Interaction::Htmx,
"htmx",
"hx-* attributes and HTML fragments",
),
(
Interaction::Datastar,
"Datastar",
"signals and streamed patches",
),
(
Interaction::AlpineAjax,
"Alpine AJAX",
"Alpine with AJAX form targets",
),
(Interaction::None, "None", BRING_YOUR_OWN),
],
recommended.interaction,
)?),
Question::Ui => Answer::Ui(confirm(
&format!(
"Topcoat UI components\n{}",
style("Includes Tailwind CSS, Lucide icons, and the Geist font").dim()
),
recommended.ui,
)?),
Question::Tailwind => {
Answer::Tailwind(confirm("Tailwind CSS", recommended.tailwind)?)
}
Question::Icons => Answer::Icons(select(
"Icon integration",
&[
(
Icons::Iconify,
"Iconify",
"icon sets checked at compile time",
),
(Icons::Custom, "Custom", "hand-written SVG icons"),
(Icons::None, "None", BRING_YOUR_OWN),
],
recommended.icons,
)?),
Question::Font => Answer::Font(select(
"Font integration",
&[
(
Font::Fontsource,
"Fontsource",
"web fonts from the Fontsource catalog",
),
(Font::None, "None", BRING_YOUR_OWN),
],
recommended.font,
)?),
Question::Example => Answer::Example(confirm(
&format!(
"Todo example\n{}",
style("A small todo list that shows how pages, data, and interactions fit together").dim()
),
recommended.example,
)?),
};
input.answer(answer);
}
if let Some(Sourced {
value: Icons::Iconify,
origin: Origin::Prompt,
}) = input.icons
&& input.icon_set.is_none()
{
let suggestions = POPULAR_ICON_SETS.iter().map(ToString::to_string).collect();
input.icon_set = Some(text(
"Iconify set",
DEFAULT_ICON_SET,
suggestions,
icon_set_id,
)?);
}
if let Some(Sourced {
value: Font::Fontsource,
origin: Origin::Prompt,
}) = input.font
&& input.font_family.is_none()
{
let suggestions = families::ALL
.iter()
.map(|family| family.id.to_string())
.collect();
input.font_family = Some(text(
"Fontsource family",
input.default_font_family(),
suggestions,
font_family_id,
)?);
}
Ok(())
}
fn select<T: Copy + Eq + 'static>(
prompt: &str,
items: &[(T, &str, &str)],
recommended: T,
) -> Result<T, String> {
let mut select = cliclack::select(prompt).initial_value(recommended);
for &(value, label, hint) in items {
select = if value == recommended {
select.item(value, label, format!("{hint}, recommended"))
} else {
select.item(value, label, hint)
};
}
select.interact().map_err(|error| prompt::error(&error))
}
fn confirm(prompt: &str, recommended: bool) -> Result<bool, String> {
cliclack::confirm(prompt)
.initial_value(recommended)
.interact()
.map_err(|error| prompt::error(&error))
}
fn text(
prompt: &str,
default: &str,
suggestions: Vec<String>,
parse: fn(&str) -> Result<String, String>,
) -> Result<String, String> {
let value: String = cliclack::input(prompt)
.default_input(default)
.autocomplete(suggestions)
.validate(move |value: &String| parse(value).map(drop))
.interact()
.map_err(|error| prompt::error(&error))?;
parse(&value)
}