Move terminal handling to its own class
All checks were successful
timw4mail/php-kilo/pipeline/head This commit looks good

This commit is contained in:
Timothy Warren 2021-03-05 12:06:23 -05:00
parent 825966ac54
commit e6f7095a45
6 changed files with 178 additions and 142 deletions

View File

@ -46,7 +46,7 @@ class Editor {
{ {
$this->statusMsgTime = time(); $this->statusMsgTime = time();
[$this->screenRows, $this->screenCols] = get_window_size(); [$this->screenRows, $this->screenCols] = Terminal::getWindowSize();
// Remove a row for the status bar, and one for the message bar // Remove a row for the status bar, and one for the message bar
$this->screenRows -= 2; $this->screenRows -= 2;
@ -86,7 +86,7 @@ class Editor {
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
protected function readKey(): string protected function readKey(): string
{ {
$c = read_stdin(); $c = Terminal::read();
return match($c) return match($c)
{ {
@ -673,6 +673,8 @@ class Editor {
public function refreshScreen(): void public function refreshScreen(): void
{ {
Terminal::clear();
$this->scroll(); $this->scroll();
$this->outputBuffer = ''; $this->outputBuffer = '';
@ -692,7 +694,7 @@ class Editor {
$this->outputBuffer .= ANSI::SHOW_CURSOR; $this->outputBuffer .= ANSI::SHOW_CURSOR;
write_stdout($this->outputBuffer, strlen($this->outputBuffer)); Terminal::write($this->outputBuffer, strlen($this->outputBuffer));
} }
public function setStatusMessage(string $fmt, mixed ...$args): void public function setStatusMessage(string $fmt, mixed ...$args): void
@ -758,7 +760,7 @@ class Editor {
{ {
$this->cursorX--; $this->cursorX--;
} }
else if ($this->cursorX > 0) else if ($this->cursorY > 0)
{ {
$this->cursorY--; $this->cursorY--;
$this->cursorX = $this->rows[$this->cursorY]->size; $this->cursorX = $this->rows[$this->cursorY]->size;
@ -827,8 +829,8 @@ class Editor {
$quit_times--; $quit_times--;
return ''; return '';
} }
write_stdout(ANSI::CLEAR_SCREEN); Terminal::clear();
write_stdout(ANSI::RESET_CURSOR);
return NULL; return NULL;
break; break;

105
src/Terminal.php Normal file
View File

@ -0,0 +1,105 @@
<?php declare(strict_types=1);
namespace Aviat\Kilo;
class Terminal {
/**
* Get the size of the current terminal window
*
* @codeCoverageIgnore
* @return array
*/
public static function getWindowSize(): array
{
$ffiSize = Termios::getWindowSize();
if ($ffiSize !== NULL)
{
return $ffiSize;
}
// Try using tput
if (self::has_tput())
{
$rows = (int)trim((string)shell_exec('tput lines'));
$cols = (int)trim((string)shell_exec('tput cols'));
if ($rows > 0 && $cols > 0)
{
return [$rows, $cols];
}
}
// Worst-case, return an arbitrary 'standard' size
return [25, 80];
}
public static function clear(): void
{
self::write(ANSI::CLEAR_SCREEN . ANSI::RESET_CURSOR);
}
/**
* Pull input from the stdin stream.
*
* @codeCoverageIgnore
* @param int $len
* @return string
*/
public static function read(int $len = 128): string
{
$handle = fopen('php://stdin', 'rb');
if ($handle === false)
{
return '';
}
$input = fread($handle, $len);
fclose($handle);
return (is_string($input)) ? $input : '';
}
/**
* Write to the stdout stream
*
* @codeCoverageIgnore
* @param string $str
* @param int|NULL $len
* @return int|false
*/
public static function write(string $str, int $len = NULL): int|false
{
$handle = fopen('php://stdout', 'ab');
if ($handle === false)
{
return false;
}
$res = (is_int($len))
? fwrite($handle, $str, $len)
: fwrite($handle, $str);
fflush($handle);
fclose($handle);
return $res;
}
/**
* See if tput exists for fallback terminal size detection
*
* @return bool
* @codeCoverageIgnore
*/
private static function has_tput(): bool
{
$cmd = shell_exec('type tput');
if ( ! is_string($cmd))
{
return FALSE;
}
return str_contains($cmd, ' is ');
}
}

View File

@ -15,7 +15,7 @@ class Termios {
private function __construct() private function __construct()
{ {
$ffi = get_ffi(); $ffi = self::ffi();
$termios = $ffi->new('struct termios'); $termios = $ffi->new('struct termios');
if ($termios === NULL) if ($termios === NULL)
{ {
@ -67,7 +67,8 @@ class Termios {
$termios->c_cc[C::VTIME] = 1; $termios->c_cc[C::VTIME] = 1;
// Turn on raw mode // Turn on raw mode
$res = get_ffi()->tcsetattr(C::STDIN_FILENO, C::TCSAFLUSH, FFI::addr($termios)); $res = self::ffi()
->tcsetattr(C::STDIN_FILENO, C::TCSAFLUSH, FFI::addr($termios));
return $res !== -1; return $res !== -1;
} }
@ -85,15 +86,37 @@ class Termios {
$instance = self::getInstance(); $instance = self::getInstance();
// Cleanup // Cleanup
write_stdout(ANSI::CLEAR_SCREEN); Terminal::clear();
write_stdout(ANSI::RESET_CURSOR); Terminal::write("\n"); // New line, please
write_stdout("\n"); // New line, please
$res = get_ffi()->tcsetattr(C::STDIN_FILENO, C::TCSAFLUSH, FFI::addr($instance->originalTermios)); $res = self::ffi()->tcsetattr(C::STDIN_FILENO, C::TCSAFLUSH, FFI::addr($instance->originalTermios));
return $res !== -1; return $res !== -1;
} }
/**
* Get the size of the current terminal window
*
* @codeCoverageIgnore
* @return array|null
*/
public static function getWindowSize(): ?array
{
// First, try to get the answer from ioctl
$ffi = self::ffi();
$ws = $ffi->new('struct winsize');
if ($ws !== NULL)
{
$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 getInstance(): self private static function getInstance(): self
{ {
static $instance; static $instance;
@ -105,4 +128,21 @@ class Termios {
return $instance; return $instance;
} }
/**
* A 'singleton' function to replace a global variable
*
* @return FFI
*/
private static function ffi(): FFI
{
static $ffi;
if ($ffi === NULL)
{
$ffi = FFI::load(__DIR__ . '/ffi.h');
}
return $ffi;
}
} }

View File

@ -2,66 +2,7 @@
namespace Aviat\Kilo; namespace Aviat\Kilo;
use FFI; use Aviat\Kilo\Enum\{Color, Highlight, KeyCode};
use Aviat\Kilo\Enum\{C, Color, Highlight, KeyCode};
/**
* See if tput exists for fallback terminal size detection
*
* @return bool
* @codeCoverageIgnore
*/
function has_tput(): bool
{
$cmd = shell_exec('type tput');
if ( ! is_string($cmd))
{
return FALSE;
}
return str_contains($cmd, ' is ');
}
// ----------------------------------------------------------------------------
// ! Terminal size
// ----------------------------------------------------------------------------
/**
* Get the size of the current terminal window
*
* @codeCoverageIgnore
* @return array
*/
function get_window_size(): array
{
// First, try to get the answer from ioctl
$ffi = get_ffi();
$ws = $ffi->new('struct winsize');
if ($ws !== NULL)
{
$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];
}
}
// Try using tput
if (has_tput())
{
$rows = (int)trim(shell_exec('tput lines'));
$cols = (int)trim(shell_exec('tput cols'));
if ($rows > 0 && $cols > 0)
{
return [$rows, $cols];
}
}
// Worst-case, return an arbitrary 'standard' size
return [25, 80];
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// ! C function/macro equivalents // ! C function/macro equivalents
@ -151,23 +92,6 @@ function is_space(string $char): bool
// ! Helper functions // ! Helper functions
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
/**
* A 'singleton' function to replace a global variable
*
* @return FFI
*/
function get_ffi(): FFI
{
static $ffi;
if ($ffi === NULL)
{
$ffi = FFI::load(__DIR__ . '/ffi.h');
}
return $ffi;
}
/** /**
* Does the one-character string contain a character that separates tokens? * Does the one-character string contain a character that separates tokens?
* *
@ -186,52 +110,6 @@ function is_separator(string $char): bool
return is_space($char) || $char === KeyCode::NULL || $isSep; return is_space($char) || $char === KeyCode::NULL || $isSep;
} }
/**
* Pull input from the stdin stream.
*
* @codeCoverageIgnore
* @param int $len
* @return string
*/
function read_stdin(int $len = 128): string
{
$handle = fopen('php://stdin', 'rb');
if ($handle === false)
{
return '';
}
$input = fread($handle, $len);
fclose($handle);
return (is_string($input)) ? $input : '';
}
/**
* Write to the stdout stream
*
* @codeCoverageIgnore
* @param string $str
* @param int|NULL $len
* @return int|false
*/
function write_stdout(string $str, int $len = NULL): int|false
{
$handle = fopen('php://stdout', 'ab');
if ($handle === false)
{
return false;
}
$res = (is_int($len))
? fwrite($handle, $str, $len)
: fwrite($handle, $str);
fclose($handle);
return $res;
}
/** /**
* Replaces a slice of an array with the same value * Replaces a slice of an array with the same value
* *

View File

@ -9,7 +9,6 @@ use PHPUnit\Framework\TestCase;
use function Aviat\Kilo\array_replace_range; use function Aviat\Kilo\array_replace_range;
use function Aviat\Kilo\ctrl_key; use function Aviat\Kilo\ctrl_key;
use function Aviat\Kilo\get_file_syntax_map; use function Aviat\Kilo\get_file_syntax_map;
use function Aviat\Kilo\get_window_size;
use function Aviat\Kilo\is_ascii; use function Aviat\Kilo\is_ascii;
use function Aviat\Kilo\is_ctrl; use function Aviat\Kilo\is_ctrl;
use function Aviat\Kilo\is_digit; use function Aviat\Kilo\is_digit;
@ -20,12 +19,7 @@ use function Aviat\Kilo\syntax_to_color;
use function Aviat\Kilo\tabs_to_spaces; use function Aviat\Kilo\tabs_to_spaces;
class FunctionTest extends TestCase { class FunctionTest extends TestCase {
public function test_get_window_size(): void
{
[$rows, $cols] = get_window_size();
$this->assertGreaterThan(0, $rows);
$this->assertGreaterThan(0, $cols);
}
public function test_is_ascii(): void public function test_is_ascii(): void
{ {

17
tests/TerminalTest.php Normal file
View File

@ -0,0 +1,17 @@
<?php declare(strict_types=1);
namespace Aviat\Kilo\Tests;
use PHPUnit\Framework\TestCase;
use Aviat\Kilo\Terminal;
class TerminalTest extends TestCase {
public function test_getWindowSize(): void
{
[$rows, $cols] = Terminal::getWindowSize();
$this->assertGreaterThan(0, $rows);
$this->assertGreaterThan(0, $cols);
}
}