Some checks failed
timw4mail/php-kilo/pipeline/head There was a failure building this commit
173 lines
3.5 KiB
PHP
173 lines
3.5 KiB
PHP
<?php declare(strict_types=1);
|
|
|
|
namespace Aviat\Kilo\Terminal;
|
|
|
|
use Aviat\Kilo\Terminal\Enum\LibType;
|
|
use FFI;
|
|
use FFI\CData;
|
|
|
|
/**
|
|
* An implicit singleton wrapper around terminal settings to simplify enabling/disabling raw mode
|
|
*/
|
|
class Termios {
|
|
private CData $originalTermios;
|
|
|
|
private function __construct()
|
|
{
|
|
$ffi = self::ffi();
|
|
$termios = $ffi->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;
|
|
|
|
// Use the stdlib func to set raw mode parameters
|
|
self::ffi()->cfmakeraw(FFI::addr($termios));
|
|
|
|
// Apply raw mode to the terminal
|
|
$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)
|
|
{
|
|
$res = (self::getLibType() === LibType::MUSL)
|
|
? $ffi->tcgetwinsize(C::STDOUT_FILENO, FFI::addr($ws))
|
|
: $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)
|
|
{
|
|
$maybeLibc = "/usr/lib/libc.so";
|
|
if (file_exists($maybeLibc))
|
|
{
|
|
$rawLibInfo = (string)shell_exec($maybeLibc);
|
|
if (str_contains(strtolower($rawLibInfo), "musl"))
|
|
{
|
|
$type = LibType::MUSL;
|
|
}
|
|
}
|
|
|
|
$type = LibType::GLIBC;
|
|
}
|
|
|
|
return $type;
|
|
}
|
|
|
|
private static function getInstance(): self
|
|
{
|
|
static $instance;
|
|
|
|
$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');
|
|
return $ffi;
|
|
}
|
|
|
|
$ffi = FFI::load(__DIR__ . '/ffi.h');
|
|
}
|
|
|
|
return $ffi;
|
|
}
|
|
} |