aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-symfony-console/src/style
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-09 11:19:03 +0900
committernsfisis <nsfisis@gmail.com>2026-08-09 11:19:03 +0900
commit6051927c8fa32cfffa102d2a170c5a6cf747a1b9 (patch)
tree3fe6769c90cb03ca8f1b9b825e38ad459182361e /crates/shirabe-symfony-console/src/style
parente3e8806aec771e482899ed3470e920f7b291fa95 (diff)
downloadphp-shirabe-6051927c8fa32cfffa102d2a170c5a6cf747a1b9.tar.gz
php-shirabe-6051927c8fa32cfffa102d2a170c5a6cf747a1b9.tar.zst
php-shirabe-6051927c8fa32cfffa102d2a170c5a6cf747a1b9.zip
refactor(symfony-console): extract symfony/console into the shirabe-symfony-console crate
Move `Symfony\Component\Console` out of shirabe-external-packages and into its own crate, so the path is `shirabe_symfony_console::application::Application` instead of `shirabe_external_packages::symfony::console::application::Application`. The `delegate_to_inner!` and `delegate_command_trait_impls_to_inner!` macros move with it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe-symfony-console/src/style')
-rw-r--r--crates/shirabe-symfony-console/src/style/output_style.rs122
-rw-r--r--crates/shirabe-symfony-console/src/style/style_interface.rs74
-rw-r--r--crates/shirabe-symfony-console/src/style/symfony_style.rs753
3 files changed, 949 insertions, 0 deletions
diff --git a/crates/shirabe-symfony-console/src/style/output_style.rs b/crates/shirabe-symfony-console/src/style/output_style.rs
new file mode 100644
index 00000000..17c9489e
--- /dev/null
+++ b/crates/shirabe-symfony-console/src/style/output_style.rs
@@ -0,0 +1,122 @@
+//! ref: composer/vendor/symfony/console/Style/OutputStyle.php
+
+use crate::formatter::OutputFormatterInterface;
+use crate::helper::ProgressBar;
+use crate::output::ConsoleOutputInterface;
+use crate::output::OutputInterface;
+use crate::output::output_interface::OUTPUT_NORMAL;
+
+/// Decorates output to add console style guide helpers.
+#[derive(Debug)]
+pub struct OutputStyle {
+ output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+}
+
+impl OutputStyle {
+ pub fn new(output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>) -> Self {
+ Self { output }
+ }
+
+ pub fn create_progress_bar(&self, max: i64) -> ProgressBar {
+ ProgressBar::new(self.output.clone(), max, 1.0 / 25.0)
+ }
+
+ pub fn new_line(&self, count: i64) {
+ self.output.borrow().write(
+ &[shirabe_php_shim::str_repeat(
+ shirabe_php_shim::PHP_EOL,
+ count as usize,
+ )],
+ false,
+ OUTPUT_NORMAL,
+ );
+ }
+
+ pub(crate) fn get_error_output(&self) -> std::rc::Rc<std::cell::RefCell<dyn OutputInterface>> {
+ // PHP checks `$this->output instanceof ConsoleOutputInterface`; this requires
+ // runtime type information that the OutputInterface trait object lacks.
+ if !Self::is_console_output_interface(&self.output) {
+ return self.output.clone();
+ }
+
+ Self::as_console_output_interface(&self.output)
+ .unwrap()
+ .get_error_output()
+ }
+
+ fn is_console_output_interface(
+ output: &std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ ) -> bool {
+ // ConsoleOutput is the only OutputInterface implementor that also implements
+ // ConsoleOutputInterface, so `instanceof ConsoleOutputInterface` reduces to this downcast.
+ shirabe_php_shim::AsAny::as_any(&*output.borrow())
+ .downcast_ref::<crate::output::console_output::ConsoleOutput>()
+ .is_some()
+ }
+
+ /// PHP casts to `ConsoleOutputInterface`; `ConsoleOutput` being its only implementor, a
+ /// borrow of the concrete type serves as the cast result.
+ fn as_console_output_interface(
+ output: &std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ ) -> Option<std::cell::Ref<'_, crate::output::console_output::ConsoleOutput>> {
+ std::cell::Ref::filter_map(output.borrow(), |output| {
+ output
+ .as_any()
+ .downcast_ref::<crate::output::console_output::ConsoleOutput>()
+ })
+ .ok()
+ }
+}
+
+impl OutputInterface for OutputStyle {
+ fn write(&self, messages: &[String], newline: bool, options: i64) {
+ self.output.borrow().write(messages, newline, options);
+ }
+
+ fn writeln(&self, messages: &[String], options: i64) {
+ self.output.borrow().writeln(messages, options);
+ }
+
+ fn set_verbosity(&self, level: i64) {
+ self.output.borrow().set_verbosity(level);
+ }
+
+ fn get_verbosity(&self) -> i64 {
+ self.output.borrow().get_verbosity()
+ }
+
+ fn is_quiet(&self) -> bool {
+ self.output.borrow().is_quiet()
+ }
+
+ fn is_verbose(&self) -> bool {
+ self.output.borrow().is_verbose()
+ }
+
+ fn is_very_verbose(&self) -> bool {
+ self.output.borrow().is_very_verbose()
+ }
+
+ fn is_debug(&self) -> bool {
+ self.output.borrow().is_debug()
+ }
+
+ fn set_decorated(&self, decorated: bool) {
+ self.output.borrow().set_decorated(decorated);
+ }
+
+ fn is_decorated(&self) -> bool {
+ self.output.borrow().is_decorated()
+ }
+
+ fn set_formatter(
+ &self,
+ formatter: std::rc::Rc<std::cell::RefCell<dyn OutputFormatterInterface>>,
+ ) {
+ self.output.borrow().set_formatter(formatter);
+ }
+
+ fn get_formatter(&self) -> std::rc::Rc<std::cell::RefCell<dyn OutputFormatterInterface>> {
+ self.output.borrow().get_formatter()
+ }
+}
diff --git a/crates/shirabe-symfony-console/src/style/style_interface.rs b/crates/shirabe-symfony-console/src/style/style_interface.rs
new file mode 100644
index 00000000..96745de1
--- /dev/null
+++ b/crates/shirabe-symfony-console/src/style/style_interface.rs
@@ -0,0 +1,74 @@
+//! ref: composer/vendor/symfony/console/Style/StyleInterface.php
+
+use shirabe_php_shim::PhpMixed;
+
+/// Output style helpers.
+pub trait StyleInterface {
+ /// Formats a command title.
+ fn title(&mut self, message: &str);
+
+ /// Formats a section title.
+ fn section(&mut self, message: &str);
+
+ /// Formats a list.
+ fn listing(&mut self, elements: Vec<PhpMixed>);
+
+ /// Formats informational text.
+ fn text(&mut self, message: PhpMixed);
+
+ /// Formats a success result bar.
+ fn success(&mut self, message: PhpMixed);
+
+ /// Formats an error result bar.
+ fn error(&mut self, message: PhpMixed);
+
+ /// Formats an warning result bar.
+ fn warning(&mut self, message: PhpMixed);
+
+ /// Formats a note admonition.
+ fn note(&mut self, message: PhpMixed);
+
+ /// Formats a caution admonition.
+ fn caution(&mut self, message: PhpMixed);
+
+ /// Formats a table.
+ fn table(&mut self, headers: Vec<PhpMixed>, rows: Vec<PhpMixed>);
+
+ /// Asks a question.
+ fn ask(
+ &mut self,
+ question: &str,
+ default: Option<&str>,
+ validator: Option<Box<dyn Fn(Option<PhpMixed>) -> anyhow::Result<PhpMixed>>>,
+ ) -> PhpMixed;
+
+ /// Asks a question with the user input hidden.
+ fn ask_hidden(
+ &mut self,
+ question: &str,
+ validator: Option<Box<dyn Fn(Option<PhpMixed>) -> anyhow::Result<PhpMixed>>>,
+ ) -> PhpMixed;
+
+ /// Asks for confirmation.
+ fn confirm(&mut self, question: &str, default: bool) -> bool;
+
+ /// Asks a choice question.
+ fn choice(
+ &mut self,
+ question: &str,
+ choices: Vec<PhpMixed>,
+ default: Option<PhpMixed>,
+ ) -> PhpMixed;
+
+ /// Add newline(s).
+ fn new_line(&mut self, count: i64);
+
+ /// Starts the progress output.
+ fn progress_start(&mut self, max: i64);
+
+ /// Advances the progress output X steps.
+ fn progress_advance(&mut self, step: i64);
+
+ /// Finishes the progress output.
+ fn progress_finish(&mut self);
+}
diff --git a/crates/shirabe-symfony-console/src/style/symfony_style.rs b/crates/shirabe-symfony-console/src/style/symfony_style.rs
new file mode 100644
index 00000000..486acf2e
--- /dev/null
+++ b/crates/shirabe-symfony-console/src/style/symfony_style.rs
@@ -0,0 +1,753 @@
+//! ref: composer/vendor/symfony/console/Style/SymfonyStyle.php
+
+use crate::exception::invalid_argument_exception::InvalidArgumentException;
+use crate::formatter::OutputFormatter;
+use crate::formatter::OutputFormatterInterface;
+use crate::helper::Helper;
+use crate::helper::ProgressBar;
+use crate::helper::SymfonyQuestionHelper;
+use crate::helper::Table;
+use crate::helper::TableCell;
+use crate::helper::TableSeparator;
+use crate::helper::question_helper::QuestionHelperInterface;
+use crate::helper::table::{Cell, Row};
+use crate::input::InputInterface;
+use crate::output::ConsoleOutputInterface;
+use crate::output::OutputInterface;
+use crate::output::TrimmedBufferOutput;
+use crate::output::console_output::ConsoleOutput;
+use crate::output::output_interface::OUTPUT_NORMAL;
+use crate::question::ChoiceQuestion;
+use crate::question::ConfirmationQuestion;
+use crate::question::Question;
+use crate::question::QuestionInterface;
+use crate::style::output_style::OutputStyle;
+use crate::style::style_interface::StyleInterface;
+use crate::terminal::Terminal;
+use shirabe_php_shim::PhpMixed;
+
+/// Output decorator helpers for the Symfony Style Guide.
+#[derive(Debug)]
+pub struct SymfonyStyle {
+ inner: OutputStyle,
+ input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
+ output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ question_helper: Option<SymfonyQuestionHelper>,
+ progress_bar: Option<ProgressBar>,
+ line_length: i64,
+ buffered_output: TrimmedBufferOutput,
+}
+
+pub const MAX_LINE_LENGTH: i64 = 120;
+
+/// A `definition_list` entry. PHP types it as `string|array|TableSeparator`; any other type is
+/// rejected with an `InvalidArgumentException` (a `LogicException`), which this enum makes
+/// unrepresentable.
+#[derive(Debug)]
+pub enum DefinitionListItem {
+ String(String),
+ Array(indexmap::IndexMap<String, PhpMixed>),
+ TableSeparator(TableSeparator),
+}
+
+impl SymfonyStyle {
+ pub fn new(
+ input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
+ output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ ) -> Self {
+ let buffered_output = TrimmedBufferOutput::new(
+ if cfg!(windows) { 4 } else { 2 },
+ Some(output.borrow().get_verbosity()),
+ false,
+ // TODO(plugin): clone of the formatter; PHP `clone $output->getFormatter()`.
+ Some(output.borrow().get_formatter()),
+ )
+ .unwrap();
+ // Windows cmd wraps lines as soon as the terminal width is reached, whether there are following chars or not.
+ let width = {
+ let w = Terminal::new().get_width();
+ if w != 0 { w } else { MAX_LINE_LENGTH }
+ };
+ let line_length = std::cmp::min(width - cfg!(windows) as i64, MAX_LINE_LENGTH);
+
+ let inner = OutputStyle::new(output.clone());
+
+ Self {
+ inner,
+ input,
+ output,
+ question_helper: None,
+ progress_bar: None,
+ line_length,
+ buffered_output,
+ }
+ }
+
+ /// Formats a message as a block of text.
+ pub fn block(
+ &mut self,
+ messages: PhpMixed,
+ r#type: Option<&str>,
+ style: Option<&str>,
+ prefix: &str,
+ padding: bool,
+ escape: bool,
+ ) {
+ let messages: Vec<PhpMixed> = if shirabe_php_shim::is_array(&messages) {
+ match messages {
+ PhpMixed::Array(entries) => entries.into_values().collect(),
+ PhpMixed::List(items) => items,
+ _ => unreachable!("value is an array past the is_array guard"),
+ }
+ } else {
+ vec![messages]
+ };
+
+ self.auto_prepend_block();
+ let block = self.create_block(messages, r#type, style, prefix, padding, escape);
+ self.writeln(
+ PhpMixed::List(block.into_iter().map(PhpMixed::String).collect()),
+ OUTPUT_NORMAL,
+ );
+ self.new_line(1);
+ }
+
+ /// Formats a command comment.
+ pub fn comment(&mut self, message: PhpMixed) {
+ self.block(
+ message,
+ None,
+ None,
+ "<fg=default;bg=default> // </>",
+ false,
+ false,
+ );
+ }
+
+ /// Formats an info message.
+ pub fn info(&mut self, message: PhpMixed) {
+ self.block(message, Some("INFO"), Some("fg=green"), " ", true, true);
+ }
+
+ /// Formats a horizontal table.
+ pub fn horizontal_table(&mut self, headers: Vec<Cell>, rows: Vec<Row>) {
+ self.create_table()
+ .set_horizontal(true)
+ .set_headers(headers)
+ .set_rows(rows)
+ .render();
+
+ self.new_line(1);
+ }
+
+ /// Formats a list of key/value horizontally.
+ ///
+ /// Each row can be one of:
+ /// * 'A title'
+ /// * ['key' => 'value']
+ /// * new TableSeparator()
+ pub fn definition_list(&mut self, list: Vec<DefinitionListItem>) {
+ let mut headers: Vec<Cell> = Vec::new();
+ let mut row: Vec<Cell> = Vec::new();
+ for value in list {
+ match value {
+ DefinitionListItem::TableSeparator(separator) => {
+ headers.push(Cell::Separator(separator.clone()));
+ row.push(Cell::Separator(separator));
+ }
+ DefinitionListItem::String(value) => {
+ headers.push(Cell::Cell(
+ TableCell::new(&value, {
+ let mut options = indexmap::IndexMap::new();
+ options.insert(
+ "colspan".to_string(),
+ crate::helper::table_cell::TableCellOption::Int(2),
+ );
+ options
+ })
+ .expect("colspan is a valid TableCell option"),
+ ));
+ row.push(Cell::Null);
+ }
+ DefinitionListItem::Array(value) => {
+ // $headers[] = key($value); $row[] = current($value);
+ let first_key = value
+ .keys()
+ .next()
+ .map(|k| PhpMixed::String(k.clone()))
+ .unwrap_or(PhpMixed::Null);
+ let first_value = value
+ .values()
+ .next()
+ .cloned()
+ .unwrap_or(PhpMixed::Bool(false));
+ headers.push(Cell::from(first_key));
+ row.push(Cell::from(first_value));
+ }
+ }
+ }
+
+ self.horizontal_table(headers, vec![Row::Cells(row)]);
+ }
+
+ /// @see ProgressBar::iterate()
+ ///
+ /// PHP returns a generator (`yield from`); this port evaluates eagerly, following
+ /// `ProgressBar::iterate`.
+ pub fn progress_iterate(
+ &mut self,
+ iterable: Vec<(PhpMixed, PhpMixed)>,
+ max: Option<i64>,
+ ) -> anyhow::Result<Vec<(PhpMixed, PhpMixed)>> {
+ let yielded = self.create_progress_bar(0).iterate(iterable, max)?;
+
+ self.new_line(2);
+
+ Ok(yielded)
+ }
+
+ pub fn ask_question(&mut self, question: &impl QuestionInterface) -> PhpMixed {
+ if self.input.borrow().is_interactive() {
+ self.auto_prepend_block();
+ }
+
+ if self.question_helper.is_none() {
+ self.question_helper = Some(SymfonyQuestionHelper::new());
+ }
+
+ // TODO(phase-c): PHP passes `$this` as the OutputInterface, so SymfonyQuestionHelper's
+ // write_error renders through SymfonyStyle::error; SymfonyStyle is not an OutputInterface
+ // trait object here, so the raw output is passed instead.
+ let answer = {
+ let input = self.input.clone();
+ let mut input = input.borrow_mut();
+ self.question_helper
+ .as_mut()
+ .unwrap()
+ .ask(&mut *input, self.output.clone(), question)
+ };
+ // PHP `askQuestion` returns the answer directly; exceptions propagate. The double
+ // `Result` is collapsed here by panicking on either error.
+ let answer = answer
+ .expect("question helper error")
+ .expect("missing input");
+
+ if self.input.borrow().is_interactive() {
+ self.new_line(1);
+ self.buffered_output
+ .write(&["\n".to_string()], false, OUTPUT_NORMAL);
+ }
+
+ answer
+ }
+
+ /// Returns a new instance which makes use of stderr if available.
+ pub fn get_error_style(&self) -> Self {
+ Self::new(self.input.clone(), self.inner.get_error_output())
+ }
+
+ pub fn create_table(&mut self) -> Table {
+ let output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>> =
+ if Self::is_console_output_interface(&self.output) {
+ Self::as_console_output_interface(&self.output)
+ .unwrap()
+ .section()
+ } else {
+ self.output.clone()
+ };
+ let mut style = Table::get_style_definition("symfony-style-guide".to_string())
+ .expect("style definition lookup")
+ .expect("undefined style definition");
+ style.set_cell_header_format("<info>%s</info>".to_string());
+
+ let mut table = Table::new(output);
+ let _ = table.set_style(crate::helper::table::StyleName::Style(style));
+ table
+ }
+
+ pub fn create_progress_bar(&self, max: i64) -> ProgressBar {
+ let mut progress_bar = self.inner.create_progress_bar(max);
+
+ if !cfg!(windows)
+ || shirabe_php_shim::getenv("TERM_PROGRAM").as_deref()
+ == Some(std::ffi::OsStr::new("Hyper"))
+ {
+ progress_bar.set_empty_bar_character("░"); // light shade character ░
+ progress_bar.set_progress_character("");
+ progress_bar.set_bar_character("▓"); // dark shade character ▓
+ }
+
+ progress_bar
+ }
+
+ fn get_progress_bar(&mut self) -> &mut ProgressBar {
+ // PHP throws RuntimeException('The ProgressBar is not started.'). Reaching this without a
+ // prior progress_start() call is a caller bug, and the StyleInterface signatures carry no
+ // Result, so panic.
+ self.progress_bar
+ .as_mut()
+ .expect("The ProgressBar is not started.")
+ }
+
+ fn auto_prepend_block(&mut self) {
+ let chars = shirabe_php_shim::substr(
+ &shirabe_php_shim::str_replace(
+ shirabe_php_shim::PHP_EOL,
+ "\n",
+ &self.buffered_output.fetch(),
+ ),
+ -2,
+ None,
+ );
+
+ if chars.is_empty() {
+ self.new_line(1); // empty history, so we should start with a new line.
+
+ return;
+ }
+ // Prepend new line for each non LF chars (This means no blank line was output before)
+ self.new_line(2 - shirabe_php_shim::substr_count(&chars, "\n"));
+ }
+
+ fn auto_prepend_text(&mut self) {
+ let fetched = self.buffered_output.fetch();
+ // Prepend new line if last char isn't EOL:
+ if !shirabe_php_shim::str_ends_with(&fetched, "\n") {
+ self.new_line(1);
+ }
+ }
+
+ fn write_buffer(&mut self, message: &str, new_line: bool, r#type: i64) {
+ // We need to know if the last chars are PHP_EOL
+ self.buffered_output
+ .write(&[message.to_string()], new_line, r#type);
+ }
+
+ fn create_block(
+ &mut self,
+ messages: Vec<PhpMixed>,
+ r#type: Option<&str>,
+ style: Option<&str>,
+ prefix: &str,
+ padding: bool,
+ escape: bool,
+ ) -> Vec<String> {
+ let mut indent_length: i64 = 0;
+ let prefix_length = Helper::width(&Helper::remove_decoration(
+ &mut *self.get_formatter().borrow_mut(),
+ prefix,
+ ));
+ let mut lines: Vec<String> = Vec::new();
+
+ let mut r#type = r#type.map(|t| t.to_string());
+ let mut line_indentation = String::new();
+ if let Some(t) = &r#type {
+ let formatted = format!("[{}] ", t.clone());
+ indent_length = shirabe_php_shim::strlen(&formatted);
+ line_indentation = shirabe_php_shim::str_repeat(" ", indent_length as usize);
+ r#type = Some(formatted);
+ }
+
+ let messages_count = messages.len() as i64;
+ // wrap and add newlines for each element
+ for (key, message) in messages.into_iter().enumerate() {
+ let key = key as i64;
+ let mut message = Self::php_string(&message);
+ if escape {
+ message = OutputFormatter::escape(&message).unwrap();
+ }
+
+ let decoration_length = Helper::width(&message)
+ - Helper::width(&Helper::remove_decoration(
+ &mut *self.get_formatter().borrow_mut(),
+ &message,
+ ));
+ let message_line_length = std::cmp::min(
+ self.line_length - prefix_length - indent_length + decoration_length,
+ self.line_length,
+ );
+ let message_lines = shirabe_php_shim::explode(
+ shirabe_php_shim::PHP_EOL,
+ &shirabe_php_shim::wordwrap(
+ &message,
+ message_line_length,
+ shirabe_php_shim::PHP_EOL,
+ true,
+ ),
+ );
+ for message_line in message_lines {
+ lines.push(message_line);
+ }
+
+ if messages_count > 1 && key < messages_count - 1 {
+ lines.push(String::new());
+ }
+ }
+
+ let mut first_line_index: i64 = 0;
+ if padding && self.inner.is_decorated() {
+ first_line_index = 1;
+ shirabe_php_shim::array_unshift(&mut lines, String::new());
+ lines.push(String::new());
+ }
+
+ for (i, line) in lines.iter_mut().enumerate() {
+ let i = i as i64;
+ if let Some(t) = &r#type {
+ *line = if first_line_index == i {
+ format!("{}{}", t, line)
+ } else {
+ format!("{}{}", line_indentation, line)
+ };
+ }
+
+ *line = format!("{}{}", prefix, line);
+ line.push_str(&shirabe_php_shim::str_repeat(
+ " ",
+ (self.line_length
+ - Helper::width(&Helper::remove_decoration(
+ &mut *self.output.borrow().get_formatter().borrow_mut(),
+ line,
+ )))
+ .max(0) as usize,
+ ));
+
+ if let Some(style) = style {
+ *line = format!("<{}>{}</>", style, line.clone());
+ }
+ }
+
+ lines
+ }
+
+ fn get_formatter(&self) -> std::rc::Rc<std::cell::RefCell<dyn OutputFormatterInterface>> {
+ self.output.borrow().get_formatter()
+ }
+
+ fn is_console_output_interface(
+ output: &std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ ) -> bool {
+ // ConsoleOutput is the only OutputInterface implementor that also implements
+ // ConsoleOutputInterface, so `instanceof ConsoleOutputInterface` reduces to this downcast.
+ shirabe_php_shim::AsAny::as_any(&*output.borrow())
+ .downcast_ref::<ConsoleOutput>()
+ .is_some()
+ }
+
+ /// PHP casts to `ConsoleOutputInterface`; `ConsoleOutput` being its only implementor, a
+ /// borrow of the concrete type serves as the cast result.
+ fn as_console_output_interface(
+ output: &std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ ) -> Option<std::cell::Ref<'_, ConsoleOutput>> {
+ std::cell::Ref::filter_map(output.borrow(), |output| {
+ output.as_any().downcast_ref::<ConsoleOutput>()
+ })
+ .ok()
+ }
+
+ fn php_string(value: &PhpMixed) -> String {
+ shirabe_php_shim::strval(value)
+ }
+
+ /// Bridges the `StyleInterface` validator (which yields `anyhow::Error`) to the
+ /// `Question::set_validator` validator (which yields `InvalidArgumentException`) by
+ /// converting any error into an `InvalidArgumentException` carrying its message.
+ #[allow(clippy::type_complexity)]
+ fn adapt_validator(
+ validator: Option<Box<dyn Fn(Option<PhpMixed>) -> anyhow::Result<PhpMixed>>>,
+ ) -> Option<Box<dyn Fn(Option<PhpMixed>) -> Result<PhpMixed, InvalidArgumentException>>> {
+ validator.map(|validator| {
+ Box::new(move |value: Option<PhpMixed>| {
+ validator(value).map_err(|e| InvalidArgumentException::new(e.to_string()))
+ })
+ as Box<dyn Fn(Option<PhpMixed>) -> Result<PhpMixed, InvalidArgumentException>>
+ })
+ }
+
+ /// {@inheritdoc}
+ pub fn writeln(&mut self, messages: PhpMixed, r#type: i64) {
+ let messages: Vec<PhpMixed> = if !shirabe_php_shim::is_iterable(&messages) {
+ vec![messages]
+ } else {
+ match messages {
+ PhpMixed::Array(entries) => entries.into_values().collect(),
+ PhpMixed::List(items) => items,
+ _ => unreachable!("value is iterable past the is_iterable guard"),
+ }
+ };
+
+ for message in messages {
+ let message = Self::php_string(&message);
+ self.inner.writeln(std::slice::from_ref(&message), r#type);
+ self.write_buffer(&message, true, r#type);
+ }
+ }
+
+ /// {@inheritdoc}
+ pub fn write(&mut self, messages: PhpMixed, newline: bool, r#type: i64) {
+ let messages: Vec<PhpMixed> = if !shirabe_php_shim::is_iterable(&messages) {
+ vec![messages]
+ } else {
+ match messages {
+ PhpMixed::Array(entries) => entries.into_values().collect(),
+ PhpMixed::List(items) => items,
+ _ => unreachable!("value is iterable past the is_iterable guard"),
+ }
+ };
+
+ for message in messages {
+ let message = Self::php_string(&message);
+ self.inner
+ .write(std::slice::from_ref(&message), newline, r#type);
+ self.write_buffer(&message, newline, r#type);
+ }
+ }
+}
+
+impl StyleInterface for SymfonyStyle {
+ /// {@inheritdoc}
+ fn title(&mut self, message: &str) {
+ self.auto_prepend_block();
+ self.writeln(
+ PhpMixed::List(vec![
+ PhpMixed::String(format!(
+ "<comment>{}</>",
+ OutputFormatter::escape_trailing_backslash(message),
+ )),
+ PhpMixed::String(format!(
+ "<comment>{}</>",
+ shirabe_php_shim::str_repeat(
+ "=",
+ Helper::width(&Helper::remove_decoration(
+ &mut *self.get_formatter().borrow_mut(),
+ message,
+ )) as usize,
+ ),
+ )),
+ ]),
+ OUTPUT_NORMAL,
+ );
+ self.new_line(1);
+ }
+
+ /// {@inheritdoc}
+ fn section(&mut self, message: &str) {
+ self.auto_prepend_block();
+ self.writeln(
+ PhpMixed::List(vec![
+ PhpMixed::String(format!(
+ "<comment>{}</>",
+ OutputFormatter::escape_trailing_backslash(message),
+ )),
+ PhpMixed::String(format!(
+ "<comment>{}</>",
+ shirabe_php_shim::str_repeat(
+ "-",
+ Helper::width(&Helper::remove_decoration(
+ &mut *self.get_formatter().borrow_mut(),
+ message,
+ )) as usize,
+ ),
+ )),
+ ]),
+ OUTPUT_NORMAL,
+ );
+ self.new_line(1);
+ }
+
+ /// {@inheritdoc}
+ fn listing(&mut self, elements: Vec<PhpMixed>) {
+ self.auto_prepend_text();
+ let elements: Vec<PhpMixed> = shirabe_php_shim::array_map(
+ |element: &PhpMixed| PhpMixed::String(format!(" * {}", element.clone())),
+ &elements,
+ );
+
+ self.writeln(
+ PhpMixed::List(elements.into_iter().collect()),
+ OUTPUT_NORMAL,
+ );
+ self.new_line(1);
+ }
+
+ /// {@inheritdoc}
+ fn text(&mut self, message: PhpMixed) {
+ self.auto_prepend_text();
+
+ let messages: Vec<PhpMixed> = if shirabe_php_shim::is_array(&message) {
+ match message {
+ PhpMixed::Array(entries) => entries.into_values().collect(),
+ PhpMixed::List(items) => items,
+ _ => unreachable!("value is an array past the is_array guard"),
+ }
+ } else {
+ vec![message]
+ };
+ for message in messages {
+ self.writeln(PhpMixed::String(format!(" {}", message)), OUTPUT_NORMAL);
+ }
+ }
+
+ /// {@inheritdoc}
+ fn success(&mut self, message: PhpMixed) {
+ self.block(
+ message,
+ Some("OK"),
+ Some("fg=black;bg=green"),
+ " ",
+ true,
+ true,
+ );
+ }
+
+ /// {@inheritdoc}
+ fn error(&mut self, message: PhpMixed) {
+ self.block(
+ message,
+ Some("ERROR"),
+ Some("fg=white;bg=red"),
+ " ",
+ true,
+ true,
+ );
+ }
+
+ /// {@inheritdoc}
+ fn warning(&mut self, message: PhpMixed) {
+ self.block(
+ message,
+ Some("WARNING"),
+ Some("fg=black;bg=yellow"),
+ " ",
+ true,
+ true,
+ );
+ }
+
+ /// {@inheritdoc}
+ fn note(&mut self, message: PhpMixed) {
+ self.block(message, Some("NOTE"), Some("fg=yellow"), " ! ", false, true);
+ }
+
+ /// {@inheritdoc}
+ fn caution(&mut self, message: PhpMixed) {
+ self.block(
+ message,
+ Some("CAUTION"),
+ Some("fg=white;bg=red"),
+ " ! ",
+ true,
+ true,
+ );
+ }
+
+ /// {@inheritdoc}
+ fn table(&mut self, headers: Vec<PhpMixed>, rows: Vec<PhpMixed>) {
+ self.create_table()
+ .set_headers(headers.into_iter().map(Cell::from).collect())
+ .set_rows(rows.into_iter().map(Row::from).collect())
+ .render();
+
+ self.new_line(1);
+ }
+
+ /// {@inheritdoc}
+ fn ask(
+ &mut self,
+ question: &str,
+ default: Option<&str>,
+ validator: Option<Box<dyn Fn(Option<PhpMixed>) -> anyhow::Result<PhpMixed>>>,
+ ) -> PhpMixed {
+ let mut question = Question::new(
+ question.to_string(),
+ default.map(|d| PhpMixed::String(d.to_string())),
+ );
+ question.set_validator(Self::adapt_validator(validator));
+
+ self.ask_question(&question)
+ }
+
+ /// {@inheritdoc}
+ fn ask_hidden(
+ &mut self,
+ question: &str,
+ validator: Option<Box<dyn Fn(Option<PhpMixed>) -> anyhow::Result<PhpMixed>>>,
+ ) -> PhpMixed {
+ let mut question = Question::new(question.to_string(), None);
+
+ question.set_hidden(true);
+ question.set_validator(Self::adapt_validator(validator));
+
+ self.ask_question(&question)
+ }
+
+ /// {@inheritdoc}
+ fn confirm(&mut self, question: &str, default: bool) -> bool {
+ let answer = self.ask_question(&ConfirmationQuestion::new(
+ question.to_string(),
+ default,
+ "/^y/i".to_string(),
+ ));
+
+ shirabe_php_shim::boolval(&answer)
+ }
+
+ /// {@inheritdoc}
+ fn choice(
+ &mut self,
+ question: &str,
+ choices: Vec<PhpMixed>,
+ default: Option<PhpMixed>,
+ ) -> PhpMixed {
+ let default = if let Some(default) = default {
+ let values = shirabe_php_shim::array_flip(&PhpMixed::List(choices.to_vec()));
+ // $default = $values[$default] ?? $default;
+ let resolved = match &values {
+ PhpMixed::Array(map) => map.get(&default.to_string()).cloned(),
+ _ => None,
+ };
+ Some(resolved.unwrap_or(default))
+ } else {
+ None
+ };
+
+ // PHP: return $this->askQuestion(new ChoiceQuestion($question, $choices, $default));
+ let choices_map: indexmap::IndexMap<String, PhpMixed> = choices
+ .into_iter()
+ .enumerate()
+ .map(|(i, c)| (i.to_string(), c))
+ .collect();
+ let choice_question = ChoiceQuestion::new(question.to_string(), choices_map, default)
+ .expect("choice() always provides at least one choice");
+ self.ask_question(&choice_question)
+ }
+
+ /// {@inheritdoc}
+ fn new_line(&mut self, count: i64) {
+ self.inner.new_line(count);
+ self.buffered_output.write(
+ &[shirabe_php_shim::str_repeat("\n", count as usize)],
+ false,
+ OUTPUT_NORMAL,
+ );
+ }
+
+ /// {@inheritdoc}
+ fn progress_start(&mut self, max: i64) {
+ let mut progress_bar = self.create_progress_bar(max);
+ progress_bar.start(None);
+ self.progress_bar = Some(progress_bar);
+ }
+
+ /// {@inheritdoc}
+ fn progress_advance(&mut self, step: i64) {
+ self.get_progress_bar().advance(step);
+ }
+
+ /// {@inheritdoc}
+ fn progress_finish(&mut self) {
+ self.get_progress_bar().finish();
+ self.new_line(2);
+ self.progress_bar = None;
+ }
+}