diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-08-09 11:19:03 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-08-09 11:19:03 +0900 |
| commit | 6051927c8fa32cfffa102d2a170c5a6cf747a1b9 (patch) | |
| tree | 3fe6769c90cb03ca8f1b9b825e38ad459182361e /crates/shirabe-symfony-console/src/output/stream_output.rs | |
| parent | e3e8806aec771e482899ed3470e920f7b291fa95 (diff) | |
| download | php-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/output/stream_output.rs')
| -rw-r--r-- | crates/shirabe-symfony-console/src/output/stream_output.rs | 200 |
1 files changed, 200 insertions, 0 deletions
diff --git a/crates/shirabe-symfony-console/src/output/stream_output.rs b/crates/shirabe-symfony-console/src/output/stream_output.rs new file mode 100644 index 00000000..1ed90cac --- /dev/null +++ b/crates/shirabe-symfony-console/src/output/stream_output.rs @@ -0,0 +1,200 @@ +//! ref: composer/vendor/symfony/console/Output/StreamOutput.php + +use crate::exception::InvalidArgumentException; +use crate::formatter::OutputFormatterInterface; +use crate::output::OutputInterface; +use crate::output::output::{DoWrite, Output}; +use crate::output::output_interface::VERBOSITY_NORMAL; +use shirabe_php_shim::php_regex; + +/// StreamOutput writes the output to a given stream. +/// +/// Usage: +/// +/// ```php +/// $output = new StreamOutput(fopen('php://stdout', 'w')); +/// ``` +/// +/// As `StreamOutput` can use any stream, you can also use a file: +/// +/// ```php +/// $output = new StreamOutput(fopen('/path/to/output.log', 'a', false)); +/// ``` +#[derive(Debug)] +pub struct StreamOutput { + inner: Output, + stream: shirabe_php_shim::PhpResource, +} + +impl StreamOutput { + /// `$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::get_resource_type(&stream) != "stream" { + return Ok(Err(InvalidArgumentException::new( + "The StreamOutput class needs a stream as its first argument.".to_string(), + ))); + } + + 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 + } + + /// For testing only. Overwrites the private `stream` field, mirroring what + /// `Symfony\Component\Console\Tester\TesterTrait::initOutput` does via reflection on the + /// `StreamOutput::$stream` property. + pub fn __set_stream(&mut self, stream: shirabe_php_shim::PhpResource) { + self.stream = 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().is_empty() { + 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() + .to_string_lossy(), + ) + .as_str(), + ) + { + return false; + } + + if cfg!(windows) && shirabe_php_shim::sapi_windows_vt100_support(stream) { + return true; + } + + if shirabe_php_shim::getenv("TERM_PROGRAM").as_deref() + == Some(std::ffi::OsStr::new("Hyper")) + || shirabe_php_shim::getenv("COLORTERM").is_some() + || shirabe_php_shim::getenv("ANSICON").is_some() + || shirabe_php_shim::getenv("ConEmuANSI").as_deref() == Some(std::ffi::OsStr::new("ON")) + { + return true; + } + + let term = shirabe_php_shim::getenv("TERM") + .unwrap_or_default() + .to_string_lossy() + .into_owned(); + 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( + php_regex!( + "/^((screen|xterm|vt100|vt220|putty|rxvt|ansi|cygwin|linux).*)|(.*-256(color)?(-bce)?)$/" + ), + &term, + &mut matches, + ) + } +} + +/// 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() + .to_string_lossy() + .into_owned(); + 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: &[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() + } +} |
