diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-06-12 01:01:35 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-06-12 01:01:43 +0900 |
| commit | 1e44f5723e4c0e0903d00a61f254901e612fe5e1 (patch) | |
| tree | 9c2e1eec364bbf6418a4072ee7767b04c3df0297 /crates/shirabe-external-packages/src/symfony/console/output | |
| parent | ddf0a624145b618c05c2ee47414545b99b685f37 (diff) | |
| download | php-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')
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() + } +} |
