diff options
Diffstat (limited to 'crates/shirabe-external-packages/src/symfony/console/terminal.rs')
| -rw-r--r-- | crates/shirabe-external-packages/src/symfony/console/terminal.rs | 250 |
1 files changed, 247 insertions, 3 deletions
diff --git a/crates/shirabe-external-packages/src/symfony/console/terminal.rs b/crates/shirabe-external-packages/src/symfony/console/terminal.rs index 251d71e..8163b28 100644 --- a/crates/shirabe-external-packages/src/symfony/console/terminal.rs +++ b/crates/shirabe-external-packages/src/symfony/console/terminal.rs @@ -1,3 +1,12 @@ +use shirabe_php_shim::PhpMixed; +use std::cell::Cell; + +thread_local! { + static WIDTH: Cell<Option<i64>> = const { Cell::new(None) }; + static HEIGHT: Cell<Option<i64>> = const { Cell::new(None) }; + static STTY: Cell<Option<bool>> = const { Cell::new(None) }; +} + #[derive(Debug)] pub struct Terminal; @@ -9,14 +18,249 @@ impl Default for Terminal { impl Terminal { pub fn new() -> Self { - todo!() + Terminal } + /// Gets the terminal width. pub fn get_width(&self) -> i64 { - todo!() + let width = shirabe_php_shim::getenv("COLUMNS"); + if let Some(width) = width { + return shirabe_php_shim::intval(&PhpMixed::String(shirabe_php_shim::trim( + &width, None, + ))); + } + + if WIDTH.with(|w| w.get()).is_none() { + Self::init_dimensions(); + } + + WIDTH.with(|w| w.get()).filter(|&v| v != 0).unwrap_or(80) } + /// Gets the terminal height. pub fn get_height(&self) -> i64 { - todo!() + let height = shirabe_php_shim::getenv("LINES"); + if let Some(height) = height { + return shirabe_php_shim::intval(&PhpMixed::String(shirabe_php_shim::trim( + &height, None, + ))); + } + + if HEIGHT.with(|h| h.get()).is_none() { + Self::init_dimensions(); + } + + HEIGHT.with(|h| h.get()).filter(|&v| v != 0).unwrap_or(50) + } + + pub fn has_stty_available() -> bool { + if let Some(stty) = STTY.with(|s| s.get()) { + return stty; + } + + // skip check if shell_exec function is disabled + if !shirabe_php_shim::function_exists("shell_exec") { + return false; + } + + let result = shirabe_php_shim::shell_exec(&format!( + "stty 2> {}", + if shirabe_php_shim::DIRECTORY_SEPARATOR == "\\" { + "NUL" + } else { + "/dev/null" + } + )) + .is_some(); + STTY.with(|s| s.set(Some(result))); + result + } + + fn init_dimensions() { + if shirabe_php_shim::DIRECTORY_SEPARATOR == "\\" { + let ansicon = shirabe_php_shim::getenv("ANSICON"); + let mut matches: Vec<Option<String>> = Vec::new(); + if let Some(ansicon) = &ansicon { + if shirabe_php_shim::preg_match( + "/^(\\d+)x(\\d+)(?: \\((\\d+)x(\\d+)\\))?$/", + &shirabe_php_shim::trim(ansicon, None), + &mut matches, + ) != 0 + { + // extract [w, H] from "wxh (WxH)" + // or [w, h] from "wxh" + WIDTH.with(|w| { + w.set(Some(shirabe_php_shim::intval(&PhpMixed::String( + matches[1].clone().unwrap_or_default(), + )))) + }); + HEIGHT.with(|h| { + let value = if matches.get(4).map(|m| m.is_some()).unwrap_or(false) { + shirabe_php_shim::intval(&PhpMixed::String( + matches[4].clone().unwrap_or_default(), + )) + } else { + shirabe_php_shim::intval(&PhpMixed::String( + matches[2].clone().unwrap_or_default(), + )) + }; + h.set(Some(value)); + }); + return; + } + } + + if !Self::has_vt100_support() && Self::has_stty_available() { + // only use stty on Windows if the terminal does not support vt100 (e.g. Windows 7 + git-bash) + // testing for stty in a Windows 10 vt100-enabled console will implicitly disable vt100 support on STDOUT + Self::init_dimensions_using_stty(); + } else if let Some(dimensions) = Self::get_console_mode() { + // extract [w, h] from "wxh" + WIDTH.with(|w| w.set(Some(dimensions[0]))); + HEIGHT.with(|h| h.set(Some(dimensions[1]))); + } + } else { + Self::init_dimensions_using_stty(); + } + } + + /// Returns whether STDOUT has vt100 support (some Windows 10+ configurations). + fn has_vt100_support() -> bool { + shirabe_php_shim::function_exists("sapi_windows_vt100_support") && { + // PHP passes the `fopen('php://stdout', 'w')` resource. The shim's `fopen` + // returns `PhpMixed` but `sapi_windows_vt100_support` expects + // `&PhpResource`; bridge with a placeholder resource. + let _ = shirabe_php_shim::fopen("php://stdout", "w"); + shirabe_php_shim::sapi_windows_vt100_support(&shirabe_php_shim::PhpResource) + } + } + + /// Initializes dimensions using the output of an stty columns line. + fn init_dimensions_using_stty() { + if let Some(stty_string) = Self::get_stty_columns() { + if stty_string.is_empty() { + return; + } + let mut matches: Vec<Option<String>> = Vec::new(); + if shirabe_php_shim::preg_match( + "/rows.(\\d+);.columns.(\\d+);/i", + &stty_string, + &mut matches, + ) != 0 + { + // extract [w, h] from "rows h; columns w;" + WIDTH.with(|w| { + w.set(Some(shirabe_php_shim::intval(&PhpMixed::String( + matches[2].clone().unwrap_or_default(), + )))) + }); + HEIGHT.with(|h| { + h.set(Some(shirabe_php_shim::intval(&PhpMixed::String( + matches[1].clone().unwrap_or_default(), + )))) + }); + } else if shirabe_php_shim::preg_match( + "/;.(\\d+).rows;.(\\d+).columns/i", + &stty_string, + &mut matches, + ) != 0 + { + // extract [w, h] from "; h rows; w columns" + WIDTH.with(|w| { + w.set(Some(shirabe_php_shim::intval(&PhpMixed::String( + matches[2].clone().unwrap_or_default(), + )))) + }); + HEIGHT.with(|h| { + h.set(Some(shirabe_php_shim::intval(&PhpMixed::String( + matches[1].clone().unwrap_or_default(), + )))) + }); + } + } + } + + /// Runs and parses mode CON if it's available, suppressing any error output. + /// + /// Returns an array composed of the width and the height or null if it could not be parsed. + fn get_console_mode() -> Option<Vec<i64>> { + let info = Self::read_from_process("mode CON"); + + let info = info?; + let mut matches: Vec<Option<String>> = Vec::new(); + if shirabe_php_shim::preg_match( + "/--------+\\r?\\n.+?(\\d+)\\r?\\n.+?(\\d+)\\r?\\n/", + &info, + &mut matches, + ) == 0 + { + return None; + } + + Some(vec![ + shirabe_php_shim::intval(&PhpMixed::String(matches[2].clone().unwrap_or_default())), + shirabe_php_shim::intval(&PhpMixed::String(matches[1].clone().unwrap_or_default())), + ]) + } + + /// Runs and parses stty -a if it's available, suppressing any error output. + fn get_stty_columns() -> Option<String> { + Self::read_from_process("stty -a | grep columns") + } + + fn read_from_process(command: &str) -> Option<String> { + if !shirabe_php_shim::function_exists("proc_open") { + return None; + } + + let descriptorspec: Vec<PhpMixed> = vec![ + PhpMixed::List(vec![ + Box::new(PhpMixed::String("pipe".to_string())), + Box::new(PhpMixed::String("w".to_string())), + ]), + PhpMixed::List(vec![ + Box::new(PhpMixed::String("pipe".to_string())), + Box::new(PhpMixed::String("w".to_string())), + ]), + ]; + + let cp = if shirabe_php_shim::function_exists("sapi_windows_cp_set") { + shirabe_php_shim::sapi_windows_cp_get(None) + } else { + 0 + }; + + let mut pipes = PhpMixed::Null; + let process = shirabe_php_shim::proc_open(command, &descriptorspec, &mut pipes); + if !process { + return None; + } + + // $pipes[1] and $pipes[2] from proc_open's output array. + let info = shirabe_php_shim::stream_get_contents(php_pipe(&pipes, 1)); + shirabe_php_shim::fclose(php_pipe(&pipes, 1)); + shirabe_php_shim::fclose(php_pipe(&pipes, 2)); + shirabe_php_shim::proc_close(process); + + if cp != 0 { + shirabe_php_shim::sapi_windows_cp_set(cp); + } + + info + } +} + +/// Indexes into the `$pipes` array populated by proc_open. +fn php_pipe(pipes: &PhpMixed, index: i64) -> PhpMixed { + match pipes { + PhpMixed::List(list) => list + .get(index as usize) + .map(|v| (**v).clone()) + .unwrap_or(PhpMixed::Null), + PhpMixed::Array(array) => array + .get(&index.to_string()) + .map(|v| (**v).clone()) + .unwrap_or(PhpMixed::Null), + _ => PhpMixed::Null, } } |
