mod commands;
mod config;
mod execution;
mod rendering;
use crate::context::ContextRegistry;
use crate::setup::SetupError;
use crate::topics::{
display_with_pager, render_topic, render_topics_list, TopicRegistry, TopicRenderConfig,
};
use crate::TemplateRegistry;
use crate::{render_auto, OutputMode, Theme};
use clap::{Arg, ArgAction, ArgMatches, Command};
use serde::Serialize;
use std::cell::RefCell;
use std::collections::HashMap;
use std::path::PathBuf;
use std::rc::Rc;
use super::default_command::ParseFailure;
use super::dispatch::DispatchFn;
use super::group::CommandRecipe;
use super::handler::{CommandContext, Extensions, HandlerResult, Output as HandlerOutput};
use super::help::{render_help, render_help_with_topics, CommandGroup, HelpConfig, HelpLength};
use super::hooks::{ArtifactOutput, HookError, Hooks, RenderedOutput, TextOutput};
use super::questionnaire::QuestionnaireCommand;
use super::result::{HelpDisplay, HelpResult};
use standout_dispatch::verify::ExpectedArg;
struct PendingCommand {
recipe: Box<dyn CommandRecipe>,
template: String,
}
pub struct AppBuilder {
pub(crate) registry: TopicRegistry,
pub(crate) output_flag: Option<String>,
pub(crate) output_file_flag: Option<String>,
pub(crate) theme: Option<Theme>,
pub(crate) stylesheet_registry: Option<crate::StylesheetRegistry>,
pub(crate) template_registry: Option<Rc<TemplateRegistry>>,
pub(crate) default_theme_name: Option<String>,
pending_commands: RefCell<HashMap<String, PendingCommand>>,
finalized_commands: RefCell<Option<HashMap<String, DispatchFn>>>,
pub(crate) command_hooks: HashMap<String, Hooks>,
pub(crate) questionnaire_commands: HashMap<String, QuestionnaireCommand>,
pub(crate) context_registry: ContextRegistry,
pub(crate) template_dir: Option<PathBuf>,
pub(crate) template_ext: String,
pub(crate) default_command: Option<String>,
pub(crate) default_command_resolver: Option<crate::cli::DefaultCommandResolver>,
pub(crate) include_framework_templates: bool,
pub(crate) include_framework_styles: bool,
pub(crate) app_state: Rc<Extensions>,
pub(crate) template_engine: Rc<Box<dyn standout_render::template::TemplateEngine>>,
pub(crate) help_command_groups: Option<Vec<CommandGroup>>,
pub(crate) help_handling: bool,
pub(crate) help_word: bool,
pub(crate) ambiguous_width: crate::AmbiguousWidth,
pub(crate) version: Option<&'static str>,
}
impl Default for AppBuilder {
fn default() -> Self {
Self::new()
}
}
impl AppBuilder {
pub fn new() -> Self {
Self {
registry: TopicRegistry::new(),
output_flag: Some("output".to_string()), output_file_flag: Some("output-file-path".to_string()),
theme: None,
stylesheet_registry: None,
template_registry: None,
default_theme_name: None,
pending_commands: RefCell::new(HashMap::new()),
finalized_commands: RefCell::new(None),
command_hooks: HashMap::new(),
questionnaire_commands: HashMap::new(),
context_registry: ContextRegistry::new(),
template_dir: None,
template_ext: ".j2".to_string(),
default_command: None,
default_command_resolver: None,
include_framework_templates: true,
include_framework_styles: true,
app_state: Rc::new(Extensions::new()),
template_engine: Rc::new(Box::new(standout_render::template::MiniJinjaEngine::new())),
help_command_groups: None,
help_handling: false,
help_word: false,
ambiguous_width: crate::AmbiguousWidth::Narrow,
version: None,
}
}
pub fn builder() -> Self {
Self::new()
}
pub fn app_state<T: 'static>(mut self, value: T) -> Self {
Rc::get_mut(&mut self.app_state)
.expect("app_state Rc should be exclusively owned during builder phase")
.insert(value);
self
}
pub fn template_engine(
mut self,
engine: Box<dyn standout_render::template::TemplateEngine>,
) -> Self {
self.template_engine = Rc::new(engine);
self
}
fn ensure_commands_finalized(&self) {
if self.finalized_commands.borrow().is_some() {
return;
}
let context_registry = &self.context_registry;
let mut commands = HashMap::new();
for (path, pending) in self.pending_commands.borrow().iter() {
let dispatch = pending.recipe.create_dispatch(
&pending.template,
context_registry,
self.template_engine.clone(),
);
commands.insert(path.clone(), dispatch);
}
*self.finalized_commands.borrow_mut() = Some(commands);
}
fn get_commands(&self) -> std::cell::Ref<'_, HashMap<String, DispatchFn>> {
self.ensure_commands_finalized();
std::cell::Ref::map(self.finalized_commands.borrow(), |opt| {
opt.as_ref()
.expect("finalized_commands should be Some after ensure_commands_finalized")
})
}
#[cfg(test)]
pub(crate) fn has_command(&self, path: &str) -> bool {
self.pending_commands.borrow().contains_key(path)
}
pub fn build(mut self) -> Result<Self, SetupError> {
use crate::assets::FRAMEWORK_TEMPLATES;
if self.include_framework_templates {
match self.template_registry.as_mut() {
Some(arc) => {
if let Some(registry) = Rc::get_mut(arc) {
registry.add_framework_entries(FRAMEWORK_TEMPLATES);
} else {
panic!("template registry was shared before build completed");
}
}
None => {
let mut registry = TemplateRegistry::new();
registry.add_framework_entries(FRAMEWORK_TEMPLATES);
self.template_registry = Some(Rc::new(registry));
}
};
}
if let Some(registry) = &self.template_registry {
if let Some(engine_box) = Rc::get_mut(&mut self.template_engine) {
for name in registry.names() {
if let Ok(content) = registry.get_content(name) {
let _ = engine_box.add_template(name, &content);
}
}
}
}
if self.theme.is_none() {
if let Some(ref mut registry) = self.stylesheet_registry {
let resolved = if let Some(name) = &self.default_theme_name {
Some(
registry
.get(name)
.map_err(|_| SetupError::ThemeNotFound(name.to_string()))?,
)
} else {
registry
.get("default")
.or_else(|_| registry.get("theme"))
.or_else(|_| registry.get("base"))
.ok()
};
self.theme = resolved;
}
}
if !self.help_handling {
let has_groups = self.help_command_groups.is_some();
let has_topics = !self.registry.list_topics().is_empty();
if has_groups || has_topics {
let feature = if has_groups {
"command_groups"
} else {
"topics"
};
return Err(SetupError::Config(format!(
"{feature} requires .help_handling(true) — \
standout cannot render grouped/topic help without intercepting help"
)));
}
if self.help_word {
return Err(SetupError::Config(
"help_word requires .help_handling(true) — the `help` word is \
standout's own subcommand, so there is nothing to install without \
help interception"
.to_string(),
));
}
}
if self.help_handling {
let claim = self
.pending_commands
.borrow()
.keys()
.filter(|path| claims_root_help(path))
.min()
.cloned();
if let Some(path) = claim {
return Err(duplicate_help_word(®istered_claim(&path)));
}
}
self.ensure_commands_finalized();
Ok(self)
}
pub fn parse(self, cmd: clap::Command) -> clap::ArgMatches {
self.build().expect("Failed to build App").parse_with(cmd)
}
pub fn registry(&self) -> &TopicRegistry {
&self.registry
}
pub fn registry_mut(&mut self) -> &mut TopicRegistry {
&mut self.registry
}
pub fn output_mode(&self) -> OutputMode {
OutputMode::Auto
}
pub fn get_hooks(&self, path: &str) -> Option<&Hooks> {
self.command_hooks.get(path)
}
pub fn get_default_theme(&self) -> Option<&Theme> {
self.theme.as_ref()
}
pub fn get_theme(&mut self, name: &str) -> Result<Theme, SetupError> {
self.stylesheet_registry
.as_mut()
.ok_or_else(|| SetupError::Config("No stylesheet registry configured".into()))?
.get(name)
.map_err(|_| SetupError::ThemeNotFound(name.to_string()))
}
pub fn template_names(&self) -> impl Iterator<Item = &str> {
self.template_registry
.as_ref()
.map(|r| r.names())
.into_iter()
.flatten()
}
pub fn theme_names(&self) -> Vec<String> {
self.stylesheet_registry
.as_ref()
.map(|r| r.names().map(String::from).collect())
.unwrap_or_default()
}
pub fn parse_with(&self, cmd: Command) -> clap::ArgMatches {
self.parse_from(cmd, std::env::args())
}
pub fn parse_from<I, T>(&self, cmd: Command, itr: I) -> clap::ArgMatches
where
I: IntoIterator<Item = T>,
T: Into<std::ffi::OsString> + Clone,
{
match self.get_matches_from(cmd, itr) {
HelpResult::Matches(m) => m,
HelpResult::Help(h) => {
println!("{}", h);
std::process::exit(0);
}
HelpResult::PagedHelp(h) => {
if display_with_pager(&h).is_err() {
println!("{}", h);
}
std::process::exit(0);
}
HelpResult::Error(e) => e.exit(),
}
}
pub fn get_matches(&self, cmd: Command) -> HelpResult {
self.get_matches_from(cmd, std::env::args())
}
pub fn get_matches_from<I, T>(&self, cmd: Command, itr: I) -> HelpResult
where
I: IntoIterator<Item = T>,
T: Into<std::ffi::OsString> + Clone,
{
let mut cmd = self.augment_command_with_help(cmd);
if let Some(error) = self.help_word_collision(&cmd) {
return HelpResult::Error(clap::Error::raw(
clap::error::ErrorKind::InvalidSubcommand,
format!("{error}\n"),
));
}
let args: Vec<std::ffi::OsString> = itr.into_iter().map(Into::into).collect();
let matches = match self.parse_with_default_command(&cmd, &args) {
Ok(matches) => matches,
Err(ParseFailure::UnknownDefault(e)) => {
return HelpResult::Error(
cmd.clone()
.error(clap::error::ErrorKind::InvalidSubcommand, e.to_string()),
)
}
Err(ParseFailure::Clap(e)) => {
return match self.intercept_display_help(&mut cmd, &args, &e) {
Some(display) => display.into(),
None => HelpResult::Error(e),
}
}
};
match self.intercept_help_word(&mut cmd, &matches) {
Some(display) => display.into(),
None => HelpResult::Matches(matches),
}
}
pub(crate) fn intercept_help_word(
&self,
cmd: &mut Command,
matches: &ArgMatches,
) -> Option<HelpDisplay> {
if !self.help_handling {
return None;
}
let (name, sub_matches) = matches.subcommand()?;
(name == "help").then(|| self.render_help_word(cmd, matches, sub_matches))
}
pub(crate) fn intercept_display_help(
&self,
cmd: &mut Command,
args: &[std::ffi::OsString],
error: &clap::Error,
) -> Option<HelpDisplay> {
(self.help_handling && error.kind() == clap::error::ErrorKind::DisplayHelp)
.then(|| self.render_help_for_display_help_error(cmd, args))
}
fn render_help_word(
&self,
cmd: &mut Command,
matches: &ArgMatches,
sub_matches: &ArgMatches,
) -> HelpDisplay {
let config = HelpConfig {
output_mode: Some(self.extract_output_mode(matches)),
theme: self.theme.clone(),
command_groups: self.help_command_groups.clone(),
length: HelpLength::Long,
..Default::default()
};
let use_pager = sub_matches.get_flag("page");
if let Some(topic_args) = sub_matches.get_many::<String>("topic") {
let keywords: Vec<_> = topic_args.map(|s| s.as_str()).collect();
if !keywords.is_empty() {
return self.handle_help_request(cmd, &keywords, use_pager, Some(config));
}
}
self.render_root_help(cmd, Some(config), use_pager)
}
fn render_failure(cmd: &Command, error: impl std::fmt::Display) -> HelpDisplay {
HelpDisplay::RenderFailed(cmd.clone().error(
clap::error::ErrorKind::Io,
format!("failed to render help: {error}"),
))
}
fn render_root_help(
&self,
cmd: &Command,
config: Option<HelpConfig>,
use_pager: bool,
) -> HelpDisplay {
match render_help_with_topics(cmd, &self.registry, config) {
Ok(text) => HelpDisplay::Rendered {
text,
paged: use_pager,
},
Err(e) => Self::render_failure(cmd, e),
}
}
fn render_help_for_display_help_error(
&self,
cmd: &mut Command,
args: &[std::ffi::OsString],
) -> HelpDisplay {
let request = Self::help_request(cmd, args);
let config = HelpConfig {
theme: self.theme.clone(),
command_groups: self.help_command_groups.clone(),
length: request.length,
..Default::default()
};
if request.target.is_empty() {
return self.render_root_help(cmd, Some(config), false);
}
let keywords: Vec<&str> = request.target.iter().map(|s| s.as_str()).collect();
self.handle_help_request(cmd, &keywords, false, Some(config))
}
fn help_request(cmd: &Command, args: &[std::ffi::OsString]) -> HelpRequest {
HelpRequest {
target: Self::help_target(cmd, args),
length: Self::help_length(cmd, args),
}
}
fn help_length(cmd: &Command, args: &[std::ffi::OsString]) -> HelpLength {
let probe = cmd
.clone()
.disable_help_flag(true)
.ignore_errors(true)
.arg(
Arg::new(HELP_PROBE_SHORT)
.short('h')
.action(ArgAction::SetTrue)
.global(true)
.hide(true),
)
.arg(
Arg::new(HELP_PROBE_LONG)
.long("help")
.action(ArgAction::SetTrue)
.global(true)
.hide(true),
);
match probe.try_get_matches_from(args) {
Ok(matches) if matches.get_flag(HELP_PROBE_LONG) => HelpLength::Long,
_ => HelpLength::Short,
}
}
fn help_target(cmd: &Command, args: &[std::ffi::OsString]) -> Vec<String> {
let Ok(matches) = cmd
.clone()
.disable_help_flag(true)
.ignore_errors(true)
.try_get_matches_from(args)
else {
return Vec::new();
};
let mut chain = Vec::new();
let mut current = &matches;
while let Some((name, sub)) = current.subcommand() {
chain.push(name.to_string());
current = sub;
}
chain
}
fn handle_help_request(
&self,
cmd: &mut Command,
keywords: &[&str],
use_pager: bool,
config: Option<HelpConfig>,
) -> HelpDisplay {
let sub_name = keywords[0];
if sub_name == "topics" {
let topic_config = TopicRenderConfig {
output_mode: config.as_ref().and_then(|c| c.output_mode),
theme: config.as_ref().and_then(|c| c.theme.clone()),
..Default::default()
};
return match render_topics_list(
&self.registry,
&format!("{} help", cmd.get_name()),
Some(topic_config),
) {
Ok(text) => HelpDisplay::Rendered {
text,
paged: use_pager,
},
Err(e) => Self::render_failure(cmd, e),
};
}
if super::app::find_subcommand(cmd, sub_name).is_some() {
if let Some(target) = super::app::find_subcommand_recursive(cmd, keywords) {
return match render_help(target, config.clone()) {
Ok(text) => HelpDisplay::Rendered {
text,
paged: use_pager,
},
Err(e) => Self::render_failure(cmd, e),
};
}
}
if let Some(topic) = self.registry.get_topic(sub_name) {
let topic_config = TopicRenderConfig {
output_mode: config.as_ref().and_then(|c| c.output_mode),
theme: config.as_ref().and_then(|c| c.theme.clone()),
..Default::default()
};
return match render_topic(topic, Some(topic_config)) {
Ok(text) => HelpDisplay::Rendered {
text,
paged: use_pager,
},
Err(e) => Self::render_failure(cmd, e),
};
}
let err = cmd.error(
clap::error::ErrorKind::InvalidSubcommand,
format!("The subcommand or topic '{}' wasn't recognized", sub_name),
);
HelpDisplay::Clap(err)
}
pub fn augment_command_with_help(&self, cmd: Command) -> Command {
let cmd = self.augment_framework_surface(cmd);
if !self.help_handling {
return cmd;
}
let cmd = cmd.disable_help_subcommand(true);
if self.installs_help_word(&cmd) {
let has_subcommands = cmd.get_subcommands().next().is_some();
cmd.subcommand(help_word_command(has_subcommands))
.subcommand_negates_reqs(true)
} else {
cmd
}
}
pub(crate) fn help_word_collision(&self, augmented: &Command) -> Option<SetupError> {
if !self.help_handling {
return None;
}
let claims = augmented
.get_subcommands()
.filter(|sub| claims_help(sub))
.count();
(claims > 1).then(|| duplicate_help_word(DECLARED_CLAIM))
}
pub(crate) fn installs_help_word(&self, cmd: &Command) -> bool {
self.help_word
|| cmd.get_subcommands().next().is_some()
|| cmd.get_positionals().next().is_none()
}
pub fn extract_output_mode(&self, matches: &ArgMatches) -> OutputMode {
if self.output_flag.is_some() {
match matches
.get_one::<String>("_output_mode")
.map(|s| s.as_str())
{
Some("term") => OutputMode::Term,
Some("text") => OutputMode::Text,
Some("term-debug") => OutputMode::TermDebug,
Some("json") => OutputMode::Json,
Some("yaml") => OutputMode::Yaml,
Some("xml") => OutputMode::Xml,
Some("csv") => OutputMode::Csv,
_ => OutputMode::Auto,
}
} else {
OutputMode::Auto
}
}
pub fn run_command<F, T>(
&self,
path: &str,
matches: &ArgMatches,
handler: F,
template: &str,
) -> Result<RenderedOutput, HookError>
where
F: FnOnce(&ArgMatches, &CommandContext) -> HandlerResult<T>,
T: Serialize,
{
let mut ctx = CommandContext::new(
path.split('.').map(String::from).collect(),
self.app_state.clone(),
);
let hooks = self.command_hooks.get(path);
if let Some(hooks) = hooks {
hooks.run_pre_dispatch(matches, &mut ctx)?;
}
let result = handler(matches, &ctx);
let output = match result {
Ok(HandlerOutput::Render(data)) => {
let mut json_data = serde_json::to_value(&data)
.map_err(|e| HookError::post_dispatch("Serialization error").with_source(e))?;
if let Some(hooks) = hooks {
json_data = hooks.run_post_dispatch(matches, &ctx, json_data)?;
}
let theme = self.theme.clone().unwrap_or_default();
match render_auto(template, &json_data, &theme, OutputMode::Auto) {
Ok(rendered) => RenderedOutput::Text(TextOutput::plain(rendered)),
Err(e) => return Err(HookError::post_output("Render error").with_source(e)),
}
}
Err(e) => {
return Err(HookError::post_output("Handler error").with_source(e));
}
Ok(HandlerOutput::Silent) => RenderedOutput::Silent,
Ok(HandlerOutput::Binary { data, filename }) => RenderedOutput::Binary(data, filename),
Ok(HandlerOutput::Artifact(artifact)) => {
let (bytes, suggested_destination, stdout_allowed, report) = artifact.into_parts();
let report = match report {
Some(report) => {
let mut json = serde_json::to_value(&report).map_err(|e| {
HookError::post_dispatch("Serialization error").with_source(e)
})?;
if let Some(hooks) = hooks {
json = hooks.run_post_dispatch(matches, &ctx, json)?;
}
Some(json)
}
None => None,
};
RenderedOutput::Artifact(ArtifactOutput {
bytes,
suggested_destination,
stdout_allowed,
report,
})
}
Ok(_) => {
return Err(HookError::post_output(
"Unsupported handler output variant: this standout version cannot present it",
));
}
};
if let Some(hooks) = hooks {
hooks.run_post_output(matches, &ctx, output)
} else {
Ok(output)
}
}
pub fn verify_command(&self, cmd: &Command) -> Result<(), SetupError> {
self.validate_questionnaire_surfaces(cmd)?;
let expected_args: HashMap<String, Vec<ExpectedArg>> = self
.pending_commands
.borrow()
.iter()
.map(|(path, cmd)| (path.clone(), cmd.recipe.expected_args()))
.collect();
super::app::verify_recursive(cmd, &expected_args, &[], true)
}
}
fn claims_help(cmd: &Command) -> bool {
cmd.get_name() == "help" || cmd.get_all_aliases().any(|alias| alias == "help")
}
fn claims_root_help(path: &str) -> bool {
path == "help" || path.starts_with("help.")
}
const DECLARED_CLAIM: &str =
"this application's clap `Command` declares `help` (as a subcommand name or alias)";
fn registered_claim(path: &str) -> String {
if path == "help" {
"this application registers a `help` command".to_string()
} else {
format!("this application registers `{path}`, hanging a command off the same root word")
}
}
fn duplicate_help_word(claim: &str) -> SetupError {
SetupError::DuplicateCommand(format!(
"help — {claim}, and standout installs a `help` word of its own under \
.help_handling(true). Rename the application's command, or drop \
.help_handling(true) to keep the name (help is then clap's own, and \
command_groups and topics become unavailable)"
))
}
const HELP_PROBE_SHORT: &str = "__standout_help_short";
const HELP_PROBE_LONG: &str = "__standout_help_long";
#[derive(Debug, Default, PartialEq, Eq)]
struct HelpRequest {
target: Vec<String>,
length: HelpLength,
}
fn help_word_command(has_subcommands: bool) -> Command {
let (about, topic_help) = if has_subcommands {
(
"Print this message or the help of the given subcommand(s)",
"The subcommand or topic to print help for",
)
} else {
("Print this message", "The topic to print help for")
};
Command::new("help")
.about(about)
.arg(
Arg::new("topic")
.action(ArgAction::Set)
.num_args(1..)
.help(topic_help),
)
.arg(
Arg::new("page")
.long("page")
.action(ArgAction::SetTrue)
.help("Display help through a pager"),
)
}
#[cfg(test)]
mod tests {
use super::*;
fn probe_command() -> Command {
Command::new("app")
.arg(Arg::new("out").short('o').long("out"))
.arg(Arg::new("verbose").short('v').action(ArgAction::SetTrue))
.arg(Arg::new("range"))
.subcommand(Command::new("build").arg(Arg::new("target")))
}
fn request(args: &[&str]) -> HelpRequest {
let args: Vec<std::ffi::OsString> = args.iter().map(Into::into).collect();
AppBuilder::help_request(&probe_command(), &args)
}
#[test]
fn test_help_request_reads_the_spelling() {
assert_eq!(request(&["app", "--help"]).length, HelpLength::Long);
assert_eq!(request(&["app", "-h"]).length, HelpLength::Short);
}
#[test]
fn test_help_request_reads_the_target_command() {
let deep = request(&["app", "build", "--help"]);
assert_eq!(deep.target, vec!["build".to_string()]);
assert_eq!(deep.length, HelpLength::Long);
assert!(request(&["app", "--help"]).target.is_empty());
}
#[test]
fn test_help_request_separates_the_spelling_from_the_target() {
let early = request(&["app", "--help", "build"]);
assert!(
early.target.is_empty(),
"the walk must stop at the flag, got {:?}",
early.target
);
assert_eq!(early.length, HelpLength::Long);
let short = request(&["app", "-h", "build"]);
assert!(short.target.is_empty());
assert_eq!(short.length, HelpLength::Short);
}
#[test]
fn test_help_request_reads_short_flag_clusters() {
assert_eq!(request(&["app", "-vh"]).length, HelpLength::Short);
}
#[test]
fn test_help_request_reads_inline_values() {
assert_eq!(
request(&["app", "--out=x", "--help"]).length,
HelpLength::Long
);
}
#[test]
fn test_help_request_does_not_mistake_an_option_value_for_a_flag() {
assert_eq!(request(&["app", "-o", "h"]).length, HelpLength::Short);
assert!(request(&["app", "-o", "h"]).target.is_empty());
}
#[test]
fn test_help_request_respects_the_terminator() {
assert_eq!(request(&["app", "--", "--help"]).length, HelpLength::Short);
}
#[test]
fn test_help_request_defaults_to_the_root_and_short() {
assert_eq!(request(&["app"]), HelpRequest::default());
}
#[test]
fn test_builder_output_flag_enabled_by_default() {
let standout = AppBuilder::new().build().unwrap();
assert!(standout.output_flag.is_some());
assert_eq!(standout.output_flag.as_deref(), Some("output"));
}
#[test]
fn test_no_output_flag() {
let standout = AppBuilder::new().no_output_flag().build().unwrap();
assert!(standout.output_flag.is_none());
}
#[test]
fn test_custom_output_flag_name() {
let standout = AppBuilder::new()
.output_flag(Some("format"))
.build()
.unwrap();
assert_eq!(standout.output_flag.as_deref(), Some("format"));
}
#[test]
fn test_theme_fallback_precedence() {
use std::fs;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
fs::write(temp_dir.path().join("base.yaml"), "style: { fg: blue }").unwrap();
let app = AppBuilder::new()
.styles_dir(temp_dir.path())
.unwrap()
.build()
.unwrap();
assert!(app.theme.is_some());
let theme = app.theme.as_ref().unwrap();
assert_eq!(theme.name(), Some("base"));
fs::write(temp_dir.path().join("theme.yaml"), "style: { fg: red }").unwrap();
let app = AppBuilder::new()
.styles_dir(temp_dir.path())
.unwrap()
.build()
.unwrap();
assert_eq!(app.theme.as_ref().unwrap().name(), Some("theme"));
fs::write(temp_dir.path().join("default.yaml"), "style: { fg: green }").unwrap();
let app = AppBuilder::new()
.styles_dir(temp_dir.path())
.unwrap()
.build()
.unwrap();
assert_eq!(app.theme.as_ref().unwrap().name(), Some("default"));
}
#[test]
fn test_app_state_single_type() {
struct Database {
url: String,
}
let app = AppBuilder::new()
.app_state(Database {
url: "postgres://localhost".into(),
})
.build()
.unwrap();
let db = app.app_state.get::<Database>().unwrap();
assert_eq!(db.url, "postgres://localhost");
}
#[test]
fn test_app_state_multiple_types() {
struct Database {
url: String,
}
struct Config {
debug: bool,
}
let app = AppBuilder::new()
.app_state(Database {
url: "postgres://localhost".into(),
})
.app_state(Config { debug: true })
.build()
.unwrap();
let db = app.app_state.get::<Database>().unwrap();
assert_eq!(db.url, "postgres://localhost");
let config = app.app_state.get::<Config>().unwrap();
assert!(config.debug);
}
#[test]
fn test_app_state_replacement() {
struct Config {
value: i32,
}
let app = AppBuilder::new()
.app_state(Config { value: 1 })
.app_state(Config { value: 2 }) .build()
.unwrap();
let config = app.app_state.get::<Config>().unwrap();
assert_eq!(config.value, 2);
}
#[test]
fn test_app_state_empty_by_default() {
struct NotSet;
let app = AppBuilder::new().build().unwrap();
assert!(app.app_state.is_empty());
assert!(app.app_state.get::<NotSet>().is_none());
}
}