use std::io::{self, Write};
use crate::{
envelope::{self, EnvelopeConfig, EnvelopeFields, EnvelopeLayout, EnvelopeMode, Meta},
output::render,
Config, Format, Output, Result,
};
#[derive(Debug, Clone)]
pub struct Ui {
config: Config,
envelope: EnvelopeConfig,
#[cfg(feature = "prompt")]
prompt_theme: crate::prompt::PromptTheme,
}
impl Ui {
pub fn new() -> Self {
Self {
config: Config::default(),
envelope: EnvelopeConfig::default(),
#[cfg(feature = "prompt")]
prompt_theme: crate::prompt::PromptTheme::default(),
}
}
pub fn with_config(config: Config) -> Self {
Self {
config,
envelope: EnvelopeConfig::default(),
#[cfg(feature = "prompt")]
prompt_theme: crate::prompt::PromptTheme::default(),
}
}
pub fn config(&self) -> &Config {
&self.config
}
pub fn envelope(&self) -> &EnvelopeConfig {
&self.envelope
}
pub fn with_envelope(mut self, config: EnvelopeConfig) -> Self {
self.envelope = config;
self
}
pub fn with_envelope_mode(mut self, mode: EnvelopeMode) -> Self {
self.envelope.mode = mode;
self
}
pub fn with_envelope_layout(mut self, layout: EnvelopeLayout) -> Self {
self.envelope.layout = layout;
self
}
pub fn with_envelope_fields(mut self, fields: EnvelopeFields) -> Self {
self.envelope.fields = fields;
self
}
#[cfg(feature = "prompt")]
pub fn with_prompt_theme(mut self, theme: crate::prompt::PromptTheme) -> Self {
self.prompt_theme = theme;
self
}
#[cfg(feature = "prompt")]
pub fn prompt_theme(&self) -> &crate::prompt::PromptTheme {
&self.prompt_theme
}
pub fn with_format(mut self, format: Format) -> Self {
self.config.format = format;
self
}
pub fn interactive(mut self, value: bool) -> Self {
self.config.interactive = value;
self
}
pub fn auto_yes(mut self, value: bool) -> Self {
self.config.auto_yes = value;
self
}
#[cfg(feature = "logger")]
pub fn logger(&self) -> crate::logger::Logger<'_> {
crate::logger::Logger::new(&self.config)
}
#[cfg(feature = "prompt")]
pub fn text(&self, message: &str, default: Option<&str>, help: Option<&str>) -> Result<String> {
crate::prompt::text(&self.config, message, default, help, &self.prompt_theme)
}
#[cfg(feature = "prompt")]
pub fn confirm(&self, message: &str, default: bool) -> Result<bool> {
crate::prompt::confirm(&self.config, message, default, &self.prompt_theme)
}
#[cfg(feature = "prompt")]
pub fn select(&self, request: &crate::prompt::SelectRequest) -> Result<String> {
crate::prompt::select(&self.config, request, &self.prompt_theme)
}
#[cfg(feature = "prompt")]
pub fn multiselect(&self, request: &crate::prompt::MultiSelectRequest) -> Result<Vec<String>> {
crate::prompt::multiselect(&self.config, request, &self.prompt_theme)
}
pub fn render(&self, output: &Output) -> Result<String> {
render::render_output(self.config.format, output)
}
pub fn print(&self, output: &Output) -> Result<()> {
self.print_with_meta(output, None, true)
}
pub fn print_with_meta(
&self,
output: &Output,
meta: Option<&Meta>,
ok: bool,
) -> Result<()> {
let text = if self.envelope.mode.is_json() {
let content = render::render_output_value(self.config.format, output)?;
let wrapped = envelope::wrap(
&self.envelope,
self.config.format.as_str(),
content,
meta,
ok,
);
serde_json::to_string_pretty(&wrapped)? + "\n"
} else {
self.render(output)?
};
let mut stdout = io::stdout();
stdout.write_all(text.as_bytes())?;
stdout.flush()?;
Ok(())
}
}
impl Default for Ui {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ui_new_creates_default_config() {
let ui = Ui::new();
let config = ui.config();
assert!(!config.interactive);
assert!(!config.auto_yes);
assert_eq!(config.format, Format::Text);
}
#[test]
fn ui_with_config_uses_provided_config() {
let custom_config = Config {
interactive: true,
auto_yes: true,
format: Format::Markdown,
color: crate::ColorMode::Always,
level: crate::Level::Debug,
};
let ui = Ui::with_config(custom_config);
assert_eq!(ui.config(), &custom_config);
}
#[test]
fn ui_with_format_changes_format() {
let ui = Ui::new()
.with_format(Format::Markdown)
.with_format(Format::Json);
assert_eq!(ui.config().format, Format::Json);
}
#[test]
fn ui_interactive_true() {
let ui = Ui::new().interactive(true);
assert!(ui.config().interactive);
}
#[test]
fn ui_interactive_false() {
let ui = Ui::new().interactive(true).interactive(false);
assert!(!ui.config().interactive);
}
#[test]
fn ui_auto_yes_true() {
let ui = Ui::new().auto_yes(true);
assert!(ui.config().auto_yes);
}
#[test]
fn ui_auto_yes_false() {
let ui = Ui::new().auto_yes(true).auto_yes(false);
assert!(!ui.config().auto_yes);
}
#[test]
fn ui_builder_is_fluent() {
let ui = Ui::new()
.with_format(Format::Markdown)
.interactive(true)
.auto_yes(true);
assert_eq!(ui.config().format, Format::Markdown);
assert!(ui.config().interactive);
assert!(ui.config().auto_yes);
}
#[test]
fn ui_render_plain_format() {
let ui = Ui::new().with_format(Format::Plain);
let output = Output::new().plain("test");
let rendered = ui.render(&output).unwrap();
assert_eq!(rendered, "test\n");
}
#[test]
fn ui_render_text_format() {
let ui = Ui::new().with_format(Format::Text);
let output = Output::new().paragraph("Hello");
let rendered = ui.render(&output).unwrap();
assert!(rendered.contains("Hello"));
}
#[test]
fn ui_render_markdown_format() {
let ui = Ui::new().with_format(Format::Markdown);
let output = Output::new().heading(1, "Title");
let rendered = ui.render(&output).unwrap();
assert!(rendered.contains("# Title"));
}
#[test]
fn ui_render_json_format() {
let ui = Ui::new().with_format(Format::Json);
let output = Output::new().data("key", "value");
let rendered = ui.render(&output).unwrap();
assert!(rendered.contains("key"));
assert!(rendered.contains("value"));
}
#[test]
fn ui_render_jsonl_format() {
let ui = Ui::new().with_format(Format::Jsonl);
let output = Output::new()
.jsonl_record(serde_json::json!({"a": 1}))
.jsonl_record(serde_json::json!({"b": 2}));
let rendered = ui.render(&output).unwrap();
assert!(rendered.contains("\"a\""));
assert!(rendered.contains("\"b\""));
}
#[test]
fn ui_render_title_in_text() {
let ui = Ui::new().with_format(Format::Text);
let output = Output::new().title("Status");
let rendered = ui.render(&output).unwrap();
assert!(rendered.contains("Status"));
assert!(rendered.contains("======"));
}
#[test]
fn ui_render_subtitle_in_markdown() {
let ui = Ui::new().with_format(Format::Markdown);
let output = Output::new().subtitle("Subtitle text");
let rendered = ui.render(&output).unwrap();
assert!(rendered.contains("_Subtitle text_"));
}
#[test]
fn ui_render_empty_output() {
let ui = Ui::new().with_format(Format::Markdown);
let output = Output::new();
let rendered = ui.render(&output).unwrap();
assert_eq!(rendered.trim(), "");
}
#[test]
fn ui_render_multiple_blocks() {
let ui = Ui::new().with_format(Format::Markdown);
let output = Output::new()
.heading(1, "H1")
.paragraph("P1")
.heading(2, "H2")
.paragraph("P2");
let rendered = ui.render(&output).unwrap();
assert!(rendered.contains("# H1"));
assert!(rendered.contains("P1"));
assert!(rendered.contains("## H2"));
assert!(rendered.contains("P2"));
}
#[test]
fn ui_default_is_same_as_new() {
let ui1 = Ui::new();
let ui2 = Ui::default();
assert_eq!(ui1.config(), ui2.config());
}
#[test]
fn ui_copy() {
let ui1 = Ui::new().with_format(Format::Json);
let ui2 = ui1.clone();
assert_eq!(ui2.config().format, Format::Json);
}
#[cfg(feature = "prompt")]
#[test]
fn ui_with_prompt_theme_dark() {
use crate::prompt::PromptTheme;
let ui = Ui::new().with_prompt_theme(PromptTheme::dark());
assert_eq!(ui.prompt_theme().name, "dark");
assert_eq!(ui.prompt_theme().question_color, "bright_magenta");
}
#[cfg(feature = "prompt")]
#[test]
fn ui_with_prompt_theme_custom() {
use crate::prompt::PromptTheme;
let theme = PromptTheme::default().with_question_color("magenta");
let ui = Ui::new().with_prompt_theme(theme);
assert_eq!(ui.prompt_theme().question_color, "magenta");
}
#[cfg(feature = "prompt")]
#[test]
fn ui_prompt_theme_default_on_new() {
let ui = Ui::new();
assert_eq!(ui.prompt_theme().name, "default");
}
}