new('struct termios'); if ($termios === NULL) { throw new TermiosException('Failed to create termios struct'); } $termiosAddr = FFI::addr($termios); $res = $ffi->tcgetattr(C::STDIN_FILENO, $termiosAddr); if ($res === -1) { throw new TermiosException('Failed to get existing terminal settings'); } $this->originalTermios = $termios; } /** * Put the current terminal into raw input mode * * Returns TRUE if successful. Will return NULL if run more than once, as * raw mode is pretty binary...there's no point in reapplying raw mode! * * @return bool|null */ public static function enableRawMode(): ?bool { static $run = FALSE; // Don't run this more than once! if ($run === TRUE) { return NULL; } $run = TRUE; $instance = self::getInstance(); // Make sure to restore normal mode on exit/die/crash register_shutdown_function([static::class, 'disableRawMode']); $termios = clone $instance->originalTermios; // Turn on raw mode self::ffi()->cfmakeraw(FFI::addr($termios)); $res = self::ffi() ->tcsetattr(C::STDIN_FILENO, C::TCSANOW, FFI::addr($termios)); return $res !== -1; } /** * Restores terminal settings that were changed when going into raw mode. * * Returns TRUE if settings are applied successfully. If raw mode was not * enabled, this will output a line of escape codes and a new line. * * @return bool */ public static function disableRawMode(): bool { $instance = self::getInstance(); // Cleanup Terminal::clear(); Terminal::write("\n"); // New line, please $res = self::ffi()->tcsetattr(C::STDIN_FILENO, C::TCSAFLUSH, FFI::addr($instance->originalTermios)); return $res !== -1; } /** * Get the size of the current terminal window * * @return array|null */ public static function getWindowSize(): ?array { $res = NULL; // First, try to get the answer from ioctl $ffi = self::ffi(); $ws = $ffi->new('struct winsize'); if ($ws !== NULL) { if (self::getLibType() === LibType::MUSL) { $res = $ffi->tcgetwinsize(C::STDOUT_FILENO, FFI::addr($ws)); } else { $res = $ffi->ioctl(C::STDOUT_FILENO, C::TIOCGWINSZ, FFI::addr($ws)); } if ($res === 0 && $ws->ws_col !== 0 && $ws->ws_row !== 0) { return [$ws->ws_row, $ws->ws_col]; } } return null; } private static function getLibType(): LibType { static $type; if ($type === NULL) { if (file_exists("/usr/lib/libc.so")) { $rawLibInfo = (string)shell_exec("/usr/lib/libc.so"); if (str_contains(strtolower($rawLibInfo), "musl")) { $type = LibType::MUSL; } } $type = LibType::GLIBC; } return $type; } private static function getInstance(): self { static $instance; if ($instance === NULL) { $instance = new self(); } return $instance; } /** * A 'singleton' function to replace a global variable * * @return FFI */ private static function ffi(): FFI { static $ffi; if ($ffi === NULL) { if (self::getLibType() === LibType::MUSL) { $ffi = FFI::load(__DIR__ . '/ffi_musl.h'); } else { $ffi = FFI::load(__DIR__ . '/ffi.h'); } } return $ffi; } }