aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-external-packages/src/symfony/console/output
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-12 01:01:35 +0900
committernsfisis <nsfisis@gmail.com>2026-06-12 01:01:43 +0900
commit1e44f5723e4c0e0903d00a61f254901e612fe5e1 (patch)
tree9c2e1eec364bbf6418a4072ee7767b04c3df0297 /crates/shirabe-external-packages/src/symfony/console/output
parentddf0a624145b618c05c2ee47414545b99b685f37 (diff)
downloadphp-shirabe-1e44f5723e4c0e0903d00a61f254901e612fe5e1.tar.gz
php-shirabe-1e44f5723e4c0e0903d00a61f254901e612fe5e1.tar.zst
php-shirabe-1e44f5723e4c0e0903d00a61f254901e612fe5e1.zip
feat(symfony-console): port Symfony Console and make the workspace compile
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe-external-packages/src/symfony/console/output')
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/output/buffered_output.rs82
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/output/console_output.rs183
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/output/console_output_interface.rs9
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/output/console_section_output.rs209
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/output/mod.rs8
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/output/output.rs157
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/output/output_interface.rs70
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/output/stream_output.rs174
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/output/trimmed_buffer_output.rs108
9 files changed, 909 insertions, 91 deletions
diff --git a/crates/shirabe-external-packages/src/symfony/console/output/buffered_output.rs b/crates/shirabe-external-packages/src/symfony/console/output/buffered_output.rs
new file mode 100644
index 0000000..517b985
--- /dev/null
+++ b/crates/shirabe-external-packages/src/symfony/console/output/buffered_output.rs
@@ -0,0 +1,82 @@
+use crate::symfony::console::formatter::OutputFormatterInterface;
+use crate::symfony::console::output::OutputInterface;
+use crate::symfony::console::output::output::{DoWrite, Output};
+
+#[derive(Debug)]
+pub struct BufferedOutput {
+ inner: Output,
+ buffer: std::cell::RefCell<String>,
+}
+
+impl BufferedOutput {
+ pub fn new(
+ verbosity: Option<i64>,
+ decorated: bool,
+ formatter: Option<std::rc::Rc<std::cell::RefCell<dyn OutputFormatterInterface>>>,
+ ) -> Self {
+ Self {
+ inner: Output::new(verbosity, decorated, formatter),
+ buffer: std::cell::RefCell::new(String::new()),
+ }
+ }
+
+ /// Empties buffer and returns its content.
+ pub fn fetch(&self) -> String {
+ let content = self.buffer.borrow().clone();
+ *self.buffer.borrow_mut() = String::new();
+
+ content
+ }
+}
+
+impl DoWrite for BufferedOutput {
+ fn do_write(&self, message: &str, newline: bool) {
+ self.buffer.borrow_mut().push_str(message);
+
+ if newline {
+ self.buffer.borrow_mut().push_str(shirabe_php_shim::PHP_EOL);
+ }
+ }
+}
+
+impl OutputInterface for BufferedOutput {
+ fn write(&self, messages: &[String], newline: bool, options: i64) {
+ self.inner.write(self, messages, newline, options);
+ }
+ fn writeln(&self, messages: &[String], options: i64) {
+ self.inner.writeln(self, messages, options);
+ }
+ fn set_verbosity(&self, level: i64) {
+ self.inner.set_verbosity(level);
+ }
+ fn get_verbosity(&self) -> i64 {
+ self.inner.get_verbosity()
+ }
+ fn is_quiet(&self) -> bool {
+ self.inner.is_quiet()
+ }
+ fn is_verbose(&self) -> bool {
+ self.inner.is_verbose()
+ }
+ fn is_very_verbose(&self) -> bool {
+ self.inner.is_very_verbose()
+ }
+ fn is_debug(&self) -> bool {
+ self.inner.is_debug()
+ }
+ fn set_decorated(&self, decorated: bool) {
+ self.inner.set_decorated(decorated);
+ }
+ fn is_decorated(&self) -> bool {
+ self.inner.is_decorated()
+ }
+ fn set_formatter(
+ &self,
+ formatter: std::rc::Rc<std::cell::RefCell<dyn OutputFormatterInterface>>,
+ ) {
+ self.inner.set_formatter(formatter);
+ }
+ fn get_formatter(&self) -> std::rc::Rc<std::cell::RefCell<dyn OutputFormatterInterface>> {
+ self.inner.get_formatter()
+ }
+}
diff --git a/crates/shirabe-external-packages/src/symfony/console/output/console_output.rs b/crates/shirabe-external-packages/src/symfony/console/output/console_output.rs
index b3010b9..c6788c0 100644
--- a/crates/shirabe-external-packages/src/symfony/console/output/console_output.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/output/console_output.rs
@@ -1,76 +1,191 @@
use crate::symfony::console::formatter::OutputFormatterInterface;
use crate::symfony::console::output::ConsoleOutputInterface;
use crate::symfony::console::output::OutputInterface;
+use crate::symfony::console::output::console_section_output::ConsoleSectionOutput;
+use crate::symfony::console::output::output_interface::VERBOSITY_NORMAL;
+use crate::symfony::console::output::stream_output::StreamOutput;
+/// ConsoleOutput is the default class for all CLI output. It uses STDOUT and STDERR.
+///
+/// This class is a convenient wrapper around `StreamOutput` for both STDOUT and STDERR.
+///
+/// $output = new ConsoleOutput();
+///
+/// This is equivalent to:
+///
+/// $output = new StreamOutput(fopen('php://stdout', 'w'));
+/// $stdErr = new StreamOutput(fopen('php://stderr', 'w'));
#[derive(Debug)]
-pub struct ConsoleOutput;
+pub struct ConsoleOutput {
+ inner: StreamOutput,
+ stderr: std::cell::RefCell<std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>>,
+ console_section_outputs:
+ std::rc::Rc<std::cell::RefCell<Vec<std::rc::Rc<std::cell::RefCell<ConsoleSectionOutput>>>>>,
+}
impl ConsoleOutput {
+ /// `$verbosity` defaults to `self::VERBOSITY_NORMAL`; pass `None` to use it.
pub fn new(
- _verbosity: i64,
- _decorated: Option<bool>,
- _formatter: Option<std::rc::Rc<std::cell::RefCell<dyn OutputFormatterInterface>>>,
- ) -> Self {
- todo!()
+ verbosity: Option<i64>,
+ decorated: Option<bool>,
+ formatter: Option<std::rc::Rc<std::cell::RefCell<dyn OutputFormatterInterface>>>,
+ ) -> anyhow::Result<Self> {
+ let verbosity = verbosity.unwrap_or(VERBOSITY_NORMAL);
+
+ let inner = StreamOutput::new(
+ Self::open_output_stream(),
+ Some(verbosity),
+ decorated,
+ formatter.clone(),
+ )?
+ .expect("ConsoleOutput stdout stream is always valid");
+
+ let this = if formatter.is_none() {
+ // for BC reasons, stdErr has it own Formatter only when user don't inject a specific formatter.
+ let stderr =
+ StreamOutput::new(Self::open_error_stream(), Some(verbosity), decorated, None)?
+ .expect("ConsoleOutput stderr stream is always valid");
+ Self {
+ inner,
+ stderr: std::cell::RefCell::new(std::rc::Rc::new(std::cell::RefCell::new(stderr))),
+ console_section_outputs: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
+ }
+ } else {
+ let actual_decorated = inner.is_decorated();
+ let stderr = StreamOutput::new(
+ Self::open_error_stream(),
+ Some(verbosity),
+ decorated,
+ Some(inner.get_formatter()),
+ )?
+ .expect("ConsoleOutput stderr stream is always valid");
+ let stderr_decorated = stderr.is_decorated();
+ let this = Self {
+ inner,
+ stderr: std::cell::RefCell::new(std::rc::Rc::new(std::cell::RefCell::new(stderr))),
+ console_section_outputs: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
+ };
+
+ if decorated.is_none() {
+ this.set_decorated(actual_decorated && stderr_decorated);
+ }
+
+ this
+ };
+
+ Ok(this)
}
-}
-impl ConsoleOutputInterface for ConsoleOutput {
- fn get_error_output(&self) -> std::rc::Rc<std::cell::RefCell<dyn OutputInterface>> {
- todo!()
+ /// Returns true if current environment supports writing console output to
+ /// STDOUT.
+ fn has_stdout_support() -> bool {
+ false == Self::is_running_os400()
+ }
+
+ /// Returns true if current environment supports writing console output to
+ /// STDERR.
+ fn has_stderr_support() -> bool {
+ false == Self::is_running_os400()
}
- fn set_error_output(&mut self, _error: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>) {
- todo!()
+ /// Checks if current executing environment is IBM iSeries (OS400), which
+ /// doesn't properly convert character-encodings between ASCII to EBCDIC.
+ fn is_running_os400() -> bool {
+ let checks = [
+ if shirabe_php_shim::function_exists("php_uname") {
+ shirabe_php_shim::php_uname("s")
+ } else {
+ String::new()
+ },
+ shirabe_php_shim::getenv("OSTYPE").unwrap_or_default(),
+ shirabe_php_shim::PHP_OS.to_string(),
+ ];
+
+ shirabe_php_shim::stripos(&shirabe_php_shim::implode(";", &checks), "OS400").is_some()
+ }
+
+ fn open_output_stream() -> shirabe_php_shim::PhpResource {
+ if !Self::has_stdout_support() {
+ return shirabe_php_shim::php_fopen_resource("php://output", "w");
+ }
+
+ // Use STDOUT when possible to prevent from opening too many file descriptors
+ shirabe_php_shim::php_stdout_resource()
+ }
+
+ fn open_error_stream() -> shirabe_php_shim::PhpResource {
+ if !Self::has_stderr_support() {
+ return shirabe_php_shim::php_fopen_resource("php://output", "w");
+ }
+
+ // Use STDERR when possible to prevent from opening too many file descriptors
+ shirabe_php_shim::php_stderr_resource()
}
}
-impl OutputInterface for ConsoleOutput {
- fn is_console_output_interface(&self) -> bool {
- true
+impl ConsoleOutputInterface for ConsoleOutput {
+ /// Creates a new output section.
+ fn section(&self) -> std::rc::Rc<std::cell::RefCell<ConsoleSectionOutput>> {
+ // ConsoleSectionOutput::new pushes itself into the shared sections list.
+ ConsoleSectionOutput::new(
+ self.inner.get_stream().clone(),
+ &self.console_section_outputs,
+ self.get_verbosity(),
+ self.is_decorated(),
+ self.get_formatter(),
+ )
+ }
+
+ fn get_error_output(&self) -> std::rc::Rc<std::cell::RefCell<dyn OutputInterface>> {
+ self.stderr.borrow().clone()
}
- fn as_console_output_interface(&self) -> Option<&dyn ConsoleOutputInterface> {
- Some(self)
+ fn set_error_output(&self, error: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>) {
+ *self.stderr.borrow_mut() = error;
}
+}
- fn write(&self, _messages: &str, _newline: bool, _type: i64) {
- todo!()
+impl OutputInterface for ConsoleOutput {
+ fn write(&self, messages: &[String], newline: bool, options: i64) {
+ self.inner.write(messages, newline, options);
}
- fn writeln(&self, _messages: &str, _type: i64) {
- todo!()
+ fn writeln(&self, messages: &[String], options: i64) {
+ self.inner.writeln(messages, options);
}
- fn set_verbosity(&self, _level: i64) {
- todo!()
+ fn set_verbosity(&self, level: i64) {
+ self.inner.set_verbosity(level);
+ self.stderr.borrow().borrow().set_verbosity(level);
}
fn get_verbosity(&self) -> i64 {
- todo!()
+ self.inner.get_verbosity()
}
fn is_quiet(&self) -> bool {
- todo!()
+ self.inner.is_quiet()
}
fn is_verbose(&self) -> bool {
- todo!()
+ self.inner.is_verbose()
}
fn is_very_verbose(&self) -> bool {
- todo!()
+ self.inner.is_very_verbose()
}
fn is_debug(&self) -> bool {
- todo!()
+ self.inner.is_debug()
}
- fn set_decorated(&self, _decorated: bool) {
- todo!()
+ fn set_decorated(&self, decorated: bool) {
+ self.inner.set_decorated(decorated);
+ self.stderr.borrow().borrow().set_decorated(decorated);
}
fn is_decorated(&self) -> bool {
- todo!()
+ self.inner.is_decorated()
}
fn set_formatter(
&self,
- _formatter: std::rc::Rc<std::cell::RefCell<dyn OutputFormatterInterface>>,
+ formatter: std::rc::Rc<std::cell::RefCell<dyn OutputFormatterInterface>>,
) {
- todo!()
+ self.inner.set_formatter(formatter.clone());
+ self.stderr.borrow().borrow().set_formatter(formatter);
}
fn get_formatter(&self) -> std::rc::Rc<std::cell::RefCell<dyn OutputFormatterInterface>> {
- todo!()
+ self.inner.get_formatter()
}
}
diff --git a/crates/shirabe-external-packages/src/symfony/console/output/console_output_interface.rs b/crates/shirabe-external-packages/src/symfony/console/output/console_output_interface.rs
index 0b5b432..055f7a0 100644
--- a/crates/shirabe-external-packages/src/symfony/console/output/console_output_interface.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/output/console_output_interface.rs
@@ -1,6 +1,13 @@
+use crate::symfony::console::output::ConsoleSectionOutput;
use crate::symfony::console::output::OutputInterface;
+/// ConsoleOutputInterface is the interface implemented by ConsoleOutput class.
+/// This adds information about stderr and section output stream.
pub trait ConsoleOutputInterface: OutputInterface {
+ /// Gets the OutputInterface for errors.
fn get_error_output(&self) -> std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>;
- fn set_error_output(&mut self, error: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>);
+
+ fn set_error_output(&self, error: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>);
+
+ fn section(&self) -> std::rc::Rc<std::cell::RefCell<ConsoleSectionOutput>>;
}
diff --git a/crates/shirabe-external-packages/src/symfony/console/output/console_section_output.rs b/crates/shirabe-external-packages/src/symfony/console/output/console_section_output.rs
new file mode 100644
index 0000000..586752d
--- /dev/null
+++ b/crates/shirabe-external-packages/src/symfony/console/output/console_section_output.rs
@@ -0,0 +1,209 @@
+use crate::symfony::console::formatter::OutputFormatterInterface;
+use crate::symfony::console::helper::Helper;
+use crate::symfony::console::output::OutputInterface;
+use crate::symfony::console::output::output::DoWrite;
+use crate::symfony::console::output::stream_output::StreamOutput;
+use crate::symfony::console::terminal::Terminal;
+
+type Sections =
+ std::rc::Rc<std::cell::RefCell<Vec<std::rc::Rc<std::cell::RefCell<ConsoleSectionOutput>>>>>;
+
+#[derive(Debug)]
+pub struct ConsoleSectionOutput {
+ inner: StreamOutput,
+ content: std::cell::RefCell<Vec<String>>,
+ lines: std::cell::Cell<i64>,
+ sections: Sections,
+ terminal: Terminal,
+}
+
+impl ConsoleSectionOutput {
+ /// `$sections` is shared by reference (PHP `array &$sections`); the new instance
+ /// is unshifted into it.
+ pub fn new(
+ stream: shirabe_php_shim::PhpResource,
+ sections: &Sections,
+ verbosity: i64,
+ decorated: bool,
+ formatter: std::rc::Rc<std::cell::RefCell<dyn OutputFormatterInterface>>,
+ ) -> std::rc::Rc<std::cell::RefCell<Self>> {
+ let inner = StreamOutput::new(stream, Some(verbosity), Some(decorated), Some(formatter))
+ .expect("ConsoleSectionOutput stream operation must not fatal")
+ .expect("ConsoleSectionOutput stream is valid");
+
+ let this = std::rc::Rc::new(std::cell::RefCell::new(Self {
+ inner,
+ content: std::cell::RefCell::new(Vec::new()),
+ lines: std::cell::Cell::new(0),
+ sections: sections.clone(),
+ terminal: Terminal::new(),
+ }));
+
+ shirabe_php_shim::array_unshift(&mut sections.borrow_mut(), this.clone());
+
+ this
+ }
+
+ /// Clears previous output for this section.
+ ///
+ /// `$lines` is the number of lines to clear. If null, then the entire output
+ /// of this section is cleared.
+ pub fn clear(&self, lines: Option<i64>) {
+ if self.content.borrow().is_empty() || !self.is_decorated() {
+ return;
+ }
+
+ let lines = if let Some(lines) = lines.filter(|l| *l != 0) {
+ // Multiply lines by 2 to cater for each new line added between content
+ shirabe_php_shim::array_splice(
+ &mut self.content.borrow_mut(),
+ -(lines * 2),
+ None,
+ Vec::new(),
+ );
+ lines
+ } else {
+ let lines = self.lines.get();
+ *self.content.borrow_mut() = Vec::new();
+ lines
+ };
+
+ self.lines.set(self.lines.get() - lines);
+
+ let erased = self.pop_stream_content_until_current_section(lines);
+ self.inner.do_write(&erased, false);
+ }
+
+ /// Overwrites the previous output with a new message.
+ pub fn overwrite(&self, message: &[String]) {
+ self.clear(None);
+ self.writeln(
+ message,
+ crate::symfony::console::output::output_interface::OUTPUT_NORMAL,
+ );
+ }
+
+ pub fn get_content(&self) -> String {
+ self.content.borrow().join("")
+ }
+
+ /// @internal
+ pub fn add_content(&self, input: &str) {
+ for line_content in shirabe_php_shim::explode(shirabe_php_shim::PHP_EOL, input) {
+ let count = shirabe_php_shim::ceil(
+ self.get_display_length(&line_content) as f64 / self.terminal.get_width() as f64,
+ );
+ self.lines
+ .set(self.lines.get() + if count != 0.0 { count as i64 } else { 1 });
+ self.content.borrow_mut().push(line_content);
+ self.content
+ .borrow_mut()
+ .push(shirabe_php_shim::PHP_EOL.to_string());
+ }
+ }
+
+ /// At initial stage, cursor is at the end of stream output. This method makes cursor crawl upwards until it hits
+ /// current section. Then it erases content it crawled through. Optionally, it erases part of current section too.
+ ///
+ /// `$numberOfLinesToClearFromCurrentSection` defaults to 0 in PHP.
+ fn pop_stream_content_until_current_section(
+ &self,
+ number_of_lines_to_clear_from_current_section: i64,
+ ) -> String {
+ let mut number_of_lines_to_clear = number_of_lines_to_clear_from_current_section;
+ let mut erased_content: Vec<String> = Vec::new();
+
+ for section in self.sections.borrow().iter() {
+ // PHP: `if ($section === $this) break;` — identity comparison against $this.
+ // The current section is the same object stored in the shared list.
+ if section.as_ptr() == (self as *const Self).cast_mut() {
+ break;
+ }
+
+ let section_ref = section.borrow();
+ number_of_lines_to_clear += section_ref.lines.get();
+ erased_content.push(section_ref.get_content());
+ }
+
+ if number_of_lines_to_clear > 0 {
+ // move cursor up n lines
+ self.inner.do_write(
+ &shirabe_php_shim::sprintf(
+ "\x1b[%dA",
+ &[shirabe_php_shim::PhpMixed::Int(number_of_lines_to_clear)],
+ ),
+ false,
+ );
+ // erase to end of screen
+ self.inner.do_write("\x1b[0J", false);
+ }
+
+ shirabe_php_shim::array_reverse(&erased_content, false).join("")
+ }
+
+ fn get_display_length(&self, text: &str) -> i64 {
+ Helper::width(&Helper::remove_decoration(
+ &mut *self.get_formatter().borrow_mut(),
+ &shirabe_php_shim::str_replace("\t", " ", text),
+ ))
+ }
+}
+
+impl DoWrite for ConsoleSectionOutput {
+ fn do_write(&self, message: &str, newline: bool) {
+ if !self.is_decorated() {
+ self.inner.do_write(message, newline);
+
+ return;
+ }
+
+ let erased_content = self.pop_stream_content_until_current_section(0);
+
+ self.add_content(message);
+
+ self.inner.do_write(message, true);
+ self.inner.do_write(&erased_content, false);
+ }
+}
+
+impl OutputInterface for ConsoleSectionOutput {
+ fn write(&self, messages: &[String], newline: bool, options: i64) {
+ self.inner.inner().write(self, messages, newline, options);
+ }
+ fn writeln(&self, messages: &[String], options: i64) {
+ self.inner.inner().writeln(self, messages, options);
+ }
+ fn set_verbosity(&self, level: i64) {
+ self.inner.set_verbosity(level);
+ }
+ fn get_verbosity(&self) -> i64 {
+ self.inner.get_verbosity()
+ }
+ fn is_quiet(&self) -> bool {
+ self.inner.is_quiet()
+ }
+ fn is_verbose(&self) -> bool {
+ self.inner.is_verbose()
+ }
+ fn is_very_verbose(&self) -> bool {
+ self.inner.is_very_verbose()
+ }
+ fn is_debug(&self) -> bool {
+ self.inner.is_debug()
+ }
+ fn set_decorated(&self, decorated: bool) {
+ self.inner.set_decorated(decorated);
+ }
+ fn is_decorated(&self) -> bool {
+ self.inner.is_decorated()
+ }
+ fn set_formatter(
+ &self,
+ formatter: std::rc::Rc<std::cell::RefCell<dyn OutputFormatterInterface>>,
+ ) {
+ self.inner.set_formatter(formatter);
+ }
+ fn get_formatter(&self) -> std::rc::Rc<std::cell::RefCell<dyn OutputFormatterInterface>> {
+ self.inner.get_formatter()
+ }
+}
diff --git a/crates/shirabe-external-packages/src/symfony/console/output/mod.rs b/crates/shirabe-external-packages/src/symfony/console/output/mod.rs
index ac2b687..23a8e15 100644
--- a/crates/shirabe-external-packages/src/symfony/console/output/mod.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/output/mod.rs
@@ -1,9 +1,17 @@
+pub mod buffered_output;
pub mod console_output;
pub mod console_output_interface;
+pub mod console_section_output;
+pub mod output;
pub mod output_interface;
pub mod stream_output;
+pub mod trimmed_buffer_output;
+pub use buffered_output::*;
pub use console_output::*;
pub use console_output_interface::*;
+pub use console_section_output::*;
+pub use output::*;
pub use output_interface::*;
pub use stream_output::*;
+pub use trimmed_buffer_output::*;
diff --git a/crates/shirabe-external-packages/src/symfony/console/output/output.rs b/crates/shirabe-external-packages/src/symfony/console/output/output.rs
new file mode 100644
index 0000000..e2469e9
--- /dev/null
+++ b/crates/shirabe-external-packages/src/symfony/console/output/output.rs
@@ -0,0 +1,157 @@
+use crate::symfony::console::formatter::OutputFormatter;
+use crate::symfony::console::formatter::OutputFormatterInterface;
+use crate::symfony::console::output::output_interface::{
+ OUTPUT_NORMAL, OUTPUT_PLAIN, OUTPUT_RAW, VERBOSITY_DEBUG, VERBOSITY_NORMAL, VERBOSITY_QUIET,
+ VERBOSITY_VERBOSE, VERBOSITY_VERY_VERBOSE,
+};
+
+/// Base class for output classes.
+///
+/// There are five levels of verbosity:
+///
+/// * normal: no option passed (normal output)
+/// * verbose: -v (more output)
+/// * very verbose: -vv (highly extended output)
+/// * debug: -vvv (all debug output)
+/// * quiet: -q (no output)
+pub struct Output {
+ verbosity: std::cell::Cell<i64>,
+ formatter: std::cell::RefCell<std::rc::Rc<std::cell::RefCell<dyn OutputFormatterInterface>>>,
+}
+
+impl std::fmt::Debug for Output {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("Output")
+ .field("verbosity", &self.verbosity)
+ .finish_non_exhaustive()
+ }
+}
+
+/// Subclasses provide the concrete sink by implementing `do_write`.
+///
+/// This mirrors the PHP `abstract protected function doWrite(string $message, bool $newline)`.
+pub trait DoWrite {
+ fn do_write(&self, message: &str, newline: bool);
+}
+
+impl Output {
+ pub fn new(
+ verbosity: Option<i64>,
+ decorated: bool,
+ formatter: Option<std::rc::Rc<std::cell::RefCell<dyn OutputFormatterInterface>>>,
+ ) -> Self {
+ let verbosity = verbosity.unwrap_or(VERBOSITY_NORMAL);
+ let formatter = formatter.unwrap_or_else(|| {
+ std::rc::Rc::new(std::cell::RefCell::new(OutputFormatter::new(
+ false,
+ indexmap::IndexMap::new(),
+ )))
+ });
+ formatter.borrow_mut().set_decorated(decorated);
+ Self {
+ verbosity: std::cell::Cell::new(verbosity),
+ formatter: std::cell::RefCell::new(formatter),
+ }
+ }
+
+ pub fn set_formatter(
+ &self,
+ formatter: std::rc::Rc<std::cell::RefCell<dyn OutputFormatterInterface>>,
+ ) {
+ *self.formatter.borrow_mut() = formatter;
+ }
+
+ pub fn get_formatter(&self) -> std::rc::Rc<std::cell::RefCell<dyn OutputFormatterInterface>> {
+ self.formatter.borrow().clone()
+ }
+
+ pub fn set_decorated(&self, decorated: bool) {
+ self.formatter
+ .borrow()
+ .borrow_mut()
+ .set_decorated(decorated);
+ }
+
+ pub fn is_decorated(&self) -> bool {
+ self.formatter.borrow().borrow().is_decorated()
+ }
+
+ pub fn set_verbosity(&self, level: i64) {
+ self.verbosity.set(level);
+ }
+
+ pub fn get_verbosity(&self) -> i64 {
+ self.verbosity.get()
+ }
+
+ pub fn is_quiet(&self) -> bool {
+ VERBOSITY_QUIET == self.verbosity.get()
+ }
+
+ pub fn is_verbose(&self) -> bool {
+ VERBOSITY_VERBOSE <= self.verbosity.get()
+ }
+
+ pub fn is_very_verbose(&self) -> bool {
+ VERBOSITY_VERY_VERBOSE <= self.verbosity.get()
+ }
+
+ pub fn is_debug(&self) -> bool {
+ VERBOSITY_DEBUG <= self.verbosity.get()
+ }
+
+ pub fn writeln(&self, do_writer: &dyn DoWrite, messages: &[String], options: i64) {
+ self.write(do_writer, messages, true, options);
+ }
+
+ pub fn write(&self, do_writer: &dyn DoWrite, messages: &[String], newline: bool, options: i64) {
+ let types = OUTPUT_NORMAL | OUTPUT_RAW | OUTPUT_PLAIN;
+ let r#type = {
+ let masked = types & options;
+ if masked != 0 { masked } else { OUTPUT_NORMAL }
+ };
+
+ let verbosities = VERBOSITY_QUIET
+ | VERBOSITY_NORMAL
+ | VERBOSITY_VERBOSE
+ | VERBOSITY_VERY_VERBOSE
+ | VERBOSITY_DEBUG;
+ let verbosity = {
+ let masked = verbosities & options;
+ if masked != 0 {
+ masked
+ } else {
+ VERBOSITY_NORMAL
+ }
+ };
+
+ if verbosity > self.get_verbosity() {
+ return;
+ }
+
+ for message in messages {
+ let message = match r#type {
+ OUTPUT_NORMAL => self
+ .formatter
+ .borrow()
+ .borrow_mut()
+ .format(Some(message))
+ .unwrap()
+ .unwrap_or_default(),
+ OUTPUT_RAW => message.clone(),
+ OUTPUT_PLAIN => shirabe_php_shim::strip_tags(
+ &self
+ .formatter
+ .borrow()
+ .borrow_mut()
+ .format(Some(message))
+ .unwrap()
+ .unwrap_or_default(),
+ ),
+ _ => message.clone(),
+ };
+
+ do_writer.do_write(&message, newline);
+ }
+ }
+}
diff --git a/crates/shirabe-external-packages/src/symfony/console/output/output_interface.rs b/crates/shirabe-external-packages/src/symfony/console/output/output_interface.rs
index 661cd7f..1b52c20 100644
--- a/crates/shirabe-external-packages/src/symfony/console/output/output_interface.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/output/output_interface.rs
@@ -1,48 +1,54 @@
use crate::symfony::console::formatter::OutputFormatterInterface;
-use crate::symfony::console::output::ConsoleOutputInterface;
-pub trait OutputInterface: std::fmt::Debug {
- // PHP class semantics: OutputInterface methods take &self with interior mutability,
- // because output objects are shared by reference across the PHP code.
- fn write(&self, messages: &str, newline: bool, r#type: i64);
- fn writeln(&self, messages: &str, r#type: i64);
+pub const VERBOSITY_QUIET: i64 = 16;
+pub const VERBOSITY_NORMAL: i64 = 32;
+pub const VERBOSITY_VERBOSE: i64 = 64;
+pub const VERBOSITY_VERY_VERBOSE: i64 = 128;
+pub const VERBOSITY_DEBUG: i64 = 256;
+
+pub const OUTPUT_NORMAL: i64 = 1;
+pub const OUTPUT_RAW: i64 = 2;
+pub const OUTPUT_PLAIN: i64 = 4;
+
+/// OutputInterface is the interface implemented by all Output classes.
+pub trait OutputInterface: std::fmt::Debug + shirabe_php_shim::AsAny {
+ /// Writes a message to the output.
+ ///
+ /// `$messages` is a single string or an iterable of strings.
+ fn write(&self, messages: &[String], newline: bool, options: i64);
+
+ /// Writes a message to the output and adds a newline at the end.
+ fn writeln(&self, messages: &[String], options: i64);
+
+ /// Sets the verbosity of the output.
fn set_verbosity(&self, level: i64);
+
+ /// Gets the current verbosity of the output.
fn get_verbosity(&self) -> i64;
+
+ /// Returns whether verbosity is quiet (-q).
fn is_quiet(&self) -> bool;
+
+ /// Returns whether verbosity is verbose (-v).
fn is_verbose(&self) -> bool;
+
+ /// Returns whether verbosity is very verbose (-vv).
fn is_very_verbose(&self) -> bool;
+
+ /// Returns whether verbosity is debug (-vvv).
fn is_debug(&self) -> bool;
+
+ /// Sets the decorated flag.
fn set_decorated(&self, decorated: bool);
+
+ /// Gets the decorated flag.
fn is_decorated(&self) -> bool;
+
fn set_formatter(
&self,
formatter: std::rc::Rc<std::cell::RefCell<dyn OutputFormatterInterface>>,
);
- fn get_formatter(&self) -> std::rc::Rc<std::cell::RefCell<dyn OutputFormatterInterface>>;
-
- /// PHP: `$output instanceof ConsoleOutputInterface`. Default false; ConsoleOutput overrides.
- fn is_console_output_interface(&self) -> bool {
- false
- }
- /// PHP: `$output instanceof ConsoleOutputInterface`. Returns the output as a
- /// ConsoleOutputInterface trait object when it is one. Default None; ConsoleOutput overrides.
- fn as_console_output_interface(&self) -> Option<&dyn ConsoleOutputInterface> {
- None
- }
-
- /// PHP: only StreamOutput exposes `getStream()`. Default panics for outputs without one.
- fn get_stream(&self) -> shirabe_php_shim::PhpResource {
- todo!("get_stream not available on this OutputInterface implementation")
- }
+ /// Returns current output formatter instance.
+ fn get_formatter(&self) -> std::rc::Rc<std::cell::RefCell<dyn OutputFormatterInterface>>;
}
-
-pub const VERBOSITY_QUIET: i64 = 16;
-pub const VERBOSITY_NORMAL: i64 = 32;
-pub const VERBOSITY_VERBOSE: i64 = 64;
-pub const VERBOSITY_VERY_VERBOSE: i64 = 128;
-pub const VERBOSITY_DEBUG: i64 = 256;
-
-pub const OUTPUT_NORMAL: i64 = 1;
-pub const OUTPUT_RAW: i64 = 2;
-pub const OUTPUT_PLAIN: i64 = 4;
diff --git a/crates/shirabe-external-packages/src/symfony/console/output/stream_output.rs b/crates/shirabe-external-packages/src/symfony/console/output/stream_output.rs
index da7dbf4..46ae4dc 100644
--- a/crates/shirabe-external-packages/src/symfony/console/output/stream_output.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/output/stream_output.rs
@@ -1,57 +1,183 @@
+use crate::symfony::console::exception::InvalidArgumentException;
use crate::symfony::console::formatter::OutputFormatterInterface;
use crate::symfony::console::output::OutputInterface;
-use shirabe_php_shim::PhpMixed;
+use crate::symfony::console::output::output::{DoWrite, Output};
+use crate::symfony::console::output::output_interface::VERBOSITY_NORMAL;
+/// StreamOutput writes the output to a given stream.
+///
+/// Usage:
+///
+/// $output = new StreamOutput(fopen('php://stdout', 'w'));
+///
+/// As `StreamOutput` can use any stream, you can also use a file:
+///
+/// $output = new StreamOutput(fopen('/path/to/output.log', 'a', false));
#[derive(Debug)]
-pub struct StreamOutput;
+pub struct StreamOutput {
+ inner: Output,
+ stream: shirabe_php_shim::PhpResource,
+}
impl StreamOutput {
- pub fn new(_stream: PhpMixed, _verbosity: i64, _decorated: Option<bool>) -> Self {
- todo!()
+ /// `$verbosity` defaults to `self::VERBOSITY_NORMAL`; pass `None` to use it.
+ pub fn new(
+ stream: shirabe_php_shim::PhpResource,
+ verbosity: Option<i64>,
+ decorated: Option<bool>,
+ formatter: Option<std::rc::Rc<std::cell::RefCell<dyn OutputFormatterInterface>>>,
+ ) -> anyhow::Result<Result<Self, InvalidArgumentException>> {
+ let verbosity = verbosity.unwrap_or(VERBOSITY_NORMAL);
+
+ if !shirabe_php_shim::is_resource_value(&stream)
+ || "stream" != shirabe_php_shim::get_resource_type(&stream)
+ {
+ return Ok(Err(InvalidArgumentException(
+ shirabe_php_shim::InvalidArgumentException {
+ message: "The StreamOutput class needs a stream as its first argument."
+ .to_string(),
+ code: 0,
+ },
+ )));
+ }
+
+ let decorated = match decorated {
+ None => Some(Self::has_color_support(&stream)),
+ other => other,
+ };
+
+ let inner = Output::new(Some(verbosity), decorated.unwrap_or(false), formatter);
+
+ Ok(Ok(Self { inner, stream }))
+ }
+
+ pub(crate) fn inner(&self) -> &Output {
+ &self.inner
+ }
+
+ /// Gets the stream attached to this StreamOutput instance.
+ pub fn get_stream(&self) -> &shirabe_php_shim::PhpResource {
+ &self.stream
+ }
+
+ /// Returns true if the stream supports colorization.
+ ///
+ /// Colorization is disabled if not supported by the stream:
+ ///
+ /// This is tricky on Windows, because Cygwin, Msys2 etc emulate pseudo
+ /// terminals via named pipes, so we can only check the environment.
+ ///
+ /// Reference: Composer\XdebugHandler\Process::supportsColor
+ /// https://github.com/composer/xdebug-handler
+ pub(crate) fn has_color_support(stream: &shirabe_php_shim::PhpResource) -> bool {
+ // Follow https://no-color.org/
+ if "" != no_color_first_char() {
+ return false;
+ }
+
+ // Detect msysgit/mingw and assume this is a tty because detection
+ // does not work correctly, see https://github.com/composer/composer/issues/9690
+ if !shirabe_php_shim::stream_isatty_resource(stream)
+ && !["MINGW32", "MINGW64"].contains(
+ &shirabe_php_shim::strtoupper(
+ &shirabe_php_shim::getenv("MSYSTEM").unwrap_or_default(),
+ )
+ .as_str(),
+ )
+ {
+ return false;
+ }
+
+ if "\\" == shirabe_php_shim::DIRECTORY_SEPARATOR
+ && shirabe_php_shim::sapi_windows_vt100_support(stream)
+ {
+ return true;
+ }
+
+ if Some("Hyper".to_string()) == shirabe_php_shim::getenv("TERM_PROGRAM")
+ || shirabe_php_shim::getenv("COLORTERM").is_some()
+ || shirabe_php_shim::getenv("ANSICON").is_some()
+ || Some("ON".to_string()) == shirabe_php_shim::getenv("ConEmuANSI")
+ {
+ return true;
+ }
+
+ let term = shirabe_php_shim::getenv("TERM").unwrap_or_default();
+ if "dumb" == term {
+ return false;
+ }
+
+ // See https://github.com/chalk/supports-color/blob/d4f413efaf8da045c5ab440ed418ef02dbb28bf1/index.js#L157
+ let mut matches: Vec<Option<String>> = Vec::new();
+ shirabe_php_shim::preg_match(
+ "/^((screen|xterm|vt100|vt220|putty|rxvt|ansi|cygwin|linux).*)|(.*-256(color)?(-bce)?)$/",
+ &term,
+ &mut matches,
+ ) != 0
+ }
+}
+
+/// PHP: `(($_SERVER['NO_COLOR'] ?? getenv('NO_COLOR'))[0] ?? '')`.
+fn no_color_first_char() -> String {
+ let value = shirabe_php_shim::getenv("NO_COLOR").unwrap_or_default();
+ value
+ .chars()
+ .next()
+ .map(|c| c.to_string())
+ .unwrap_or_default()
+}
+
+impl DoWrite for StreamOutput {
+ fn do_write(&self, message: &str, newline: bool) {
+ let mut message = message.to_string();
+ if newline {
+ message.push_str(shirabe_php_shim::PHP_EOL);
+ }
+
+ shirabe_php_shim::fwrite_resource(&self.stream, &message);
+
+ shirabe_php_shim::fflush_resource(&self.stream);
}
}
impl OutputInterface for StreamOutput {
- fn write(&self, _messages: &str, _newline: bool, _type: i64) {
- todo!()
+ fn write(&self, messages: &[String], newline: bool, options: i64) {
+ self.inner.write(self, messages, newline, options);
}
- fn writeln(&self, _messages: &str, _type: i64) {
- todo!()
+ fn writeln(&self, messages: &[String], options: i64) {
+ self.inner.writeln(self, messages, options);
}
- fn set_verbosity(&self, _level: i64) {
- todo!()
+ fn set_verbosity(&self, level: i64) {
+ self.inner.set_verbosity(level);
}
fn get_verbosity(&self) -> i64 {
- todo!()
+ self.inner.get_verbosity()
}
fn is_quiet(&self) -> bool {
- todo!()
+ self.inner.is_quiet()
}
fn is_verbose(&self) -> bool {
- todo!()
+ self.inner.is_verbose()
}
fn is_very_verbose(&self) -> bool {
- todo!()
+ self.inner.is_very_verbose()
}
fn is_debug(&self) -> bool {
- todo!()
+ self.inner.is_debug()
}
- fn set_decorated(&self, _decorated: bool) {
- todo!()
+ fn set_decorated(&self, decorated: bool) {
+ self.inner.set_decorated(decorated);
}
fn is_decorated(&self) -> bool {
- todo!()
+ self.inner.is_decorated()
}
fn set_formatter(
&self,
- _formatter: std::rc::Rc<std::cell::RefCell<dyn OutputFormatterInterface>>,
+ formatter: std::rc::Rc<std::cell::RefCell<dyn OutputFormatterInterface>>,
) {
- todo!()
+ self.inner.set_formatter(formatter);
}
fn get_formatter(&self) -> std::rc::Rc<std::cell::RefCell<dyn OutputFormatterInterface>> {
- todo!()
- }
- fn get_stream(&self) -> shirabe_php_shim::PhpResource {
- todo!()
+ self.inner.get_formatter()
}
}
diff --git a/crates/shirabe-external-packages/src/symfony/console/output/trimmed_buffer_output.rs b/crates/shirabe-external-packages/src/symfony/console/output/trimmed_buffer_output.rs
new file mode 100644
index 0000000..e603999
--- /dev/null
+++ b/crates/shirabe-external-packages/src/symfony/console/output/trimmed_buffer_output.rs
@@ -0,0 +1,108 @@
+use crate::symfony::console::exception::InvalidArgumentException;
+use crate::symfony::console::formatter::OutputFormatterInterface;
+use crate::symfony::console::output::OutputInterface;
+use crate::symfony::console::output::output::{DoWrite, Output};
+
+/// A BufferedOutput that keeps only the last N chars.
+#[derive(Debug)]
+pub struct TrimmedBufferOutput {
+ inner: Output,
+ max_length: i64,
+ buffer: std::cell::RefCell<String>,
+}
+
+impl TrimmedBufferOutput {
+ pub fn new(
+ max_length: i64,
+ verbosity: Option<i64>,
+ decorated: bool,
+ formatter: Option<std::rc::Rc<std::cell::RefCell<dyn OutputFormatterInterface>>>,
+ ) -> Result<Self, InvalidArgumentException> {
+ if max_length <= 0 {
+ return Err(InvalidArgumentException(
+ shirabe_php_shim::InvalidArgumentException {
+ message: shirabe_php_shim::sprintf(
+ "\"%s()\" expects a strictly positive maxLength. Got %d.",
+ &[
+ shirabe_php_shim::PhpMixed::String(
+ "Symfony\\Component\\Console\\Output\\TrimmedBufferOutput::__construct"
+ .to_string(),
+ ),
+ shirabe_php_shim::PhpMixed::Int(max_length),
+ ],
+ ),
+ code: 0,
+ },
+ ));
+ }
+
+ Ok(Self {
+ inner: Output::new(verbosity, decorated, formatter),
+ max_length,
+ buffer: std::cell::RefCell::new(String::new()),
+ })
+ }
+
+ /// Empties buffer and returns its content.
+ pub fn fetch(&self) -> String {
+ let content = self.buffer.borrow().clone();
+ *self.buffer.borrow_mut() = String::new();
+
+ content
+ }
+}
+
+impl DoWrite for TrimmedBufferOutput {
+ fn do_write(&self, message: &str, newline: bool) {
+ self.buffer.borrow_mut().push_str(message);
+
+ if newline {
+ self.buffer.borrow_mut().push_str(shirabe_php_shim::PHP_EOL);
+ }
+
+ let trimmed = shirabe_php_shim::substr(&self.buffer.borrow(), 0 - self.max_length, None);
+ *self.buffer.borrow_mut() = trimmed;
+ }
+}
+
+impl OutputInterface for TrimmedBufferOutput {
+ fn write(&self, messages: &[String], newline: bool, options: i64) {
+ self.inner.write(self, messages, newline, options);
+ }
+ fn writeln(&self, messages: &[String], options: i64) {
+ self.inner.writeln(self, messages, options);
+ }
+ fn set_verbosity(&self, level: i64) {
+ self.inner.set_verbosity(level);
+ }
+ fn get_verbosity(&self) -> i64 {
+ self.inner.get_verbosity()
+ }
+ fn is_quiet(&self) -> bool {
+ self.inner.is_quiet()
+ }
+ fn is_verbose(&self) -> bool {
+ self.inner.is_verbose()
+ }
+ fn is_very_verbose(&self) -> bool {
+ self.inner.is_very_verbose()
+ }
+ fn is_debug(&self) -> bool {
+ self.inner.is_debug()
+ }
+ fn set_decorated(&self, decorated: bool) {
+ self.inner.set_decorated(decorated);
+ }
+ fn is_decorated(&self) -> bool {
+ self.inner.is_decorated()
+ }
+ fn set_formatter(
+ &self,
+ formatter: std::rc::Rc<std::cell::RefCell<dyn OutputFormatterInterface>>,
+ ) {
+ self.inner.set_formatter(formatter);
+ }
+ fn get_formatter(&self) -> std::rc::Rc<std::cell::RefCell<dyn OutputFormatterInterface>> {
+ self.inner.get_formatter()
+ }
+}