Move terminal handling to its own class
All checks were successful
timw4mail/php-kilo/pipeline/head This commit looks good
All checks were successful
timw4mail/php-kilo/pipeline/head This commit looks good
This commit is contained in:
parent
825966ac54
commit
e6f7095a45
@ -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
105
src/Terminal.php
Normal 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 ');
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
*
|
*
|
||||||
|
@ -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
17
tests/TerminalTest.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user