php-kilo/src/Editor.php

370 lines
7.2 KiB
PHP
Raw Normal View History

2019-10-14 16:21:41 -04:00
<?php declare(strict_types=1);
namespace Kilo;
use FFI;
class Key {
public const ARROW_LEFT = 'a';
public const ARROW_RIGHT = 'd';
public const ARROW_UP = 'w';
public const ARROW_DOWN = 's';
public const DEL_KEY = 'DEL';
public const HOME_KEY = 'HOME';
public const END_KEY = 'END';
public const PAGE_UP = 'PAGE_UP';
public const PAGE_DOWN = 'PAGE_DOWN';
}
2019-10-15 13:23:25 -04:00
class Row {
public string $chars;
public static function new(string $chars): self
{
return new self($chars);
}
private function __construct($chars)
{
$this->chars = $chars;
}
}
2019-10-14 16:21:41 -04:00
class Editor {
private FFI $ffi;
protected int $cursorx = 0;
protected int $cursory = 0;
2019-10-15 13:23:25 -04:00
protected int $rowoff = 0;
protected int $coloff = 0;
2019-10-14 16:21:41 -04:00
protected int $screenRows = 0;
protected int $screenCols = 0;
protected string $ab = '';
2019-10-15 13:23:25 -04:00
/**
* Array of Row objects
*/
protected array $rows = [];
public static function new(FFI $ffi): Editor
{
return new self($ffi);
}
private function __construct($ffi)
2019-10-14 16:21:41 -04:00
{
$this->ffi = $ffi;
if ( ! $this->getWindowSize())
{
die('Failed to get screen size');
}
}
// ------------------------------------------------------------------------
// ! Terminal
// ------------------------------------------------------------------------
protected function readKey(): string
{
$c = read_stdin(1);
if ($c === '\x1b')
{
$seq = read_stdin();
if (strpos($seq, '[') === 0)
{
if ((int)$seq[1] >= 0 && (int)$seq[1] <= 9)
{
if (strpos($seq, '~') === 2)
{
switch($seq[1])
{
case '1': return Key::HOME_KEY;
case '3': return Key::DEL_KEY;
case '4': return Key::END_KEY;
case '5': return Key::PAGE_UP;
case '6': return Key::PAGE_DOWN;
case '7': return Key::HOME_KEY;
case '8': return Key::END_KEY;
}
}
}
else
{
switch($seq[1])
{
case 'A': return Key::ARROW_UP;
case 'B': return Key::ARROW_DOWN;
case 'C': return Key::ARROW_RIGHT;
case 'D': return Key::ARROW_LEFT;
case 'H': return Key::HOME_KEY;
case 'F': return Key::END_KEY;
}
}
}
else if (strpos($seq, 'O') === 0)
{
switch($seq[1])
{
case 'H': return Key::HOME_KEY;
case 'F': return Key::END_KEY;
}
}
return '\x1b';
}
return $c;
}
/**
* @TODO fix
*/
private function getCursorPosition(): bool
{
write_stdout("\x1b[999C\x1b[999B");
write_stdout("\x1b[6n");
$buffer = read_stdout(32);
$rows = 0;
$cols = 0;
$res = sscanf($buffer, '\x1b[%d;%dR', $rows, $cols);
if ($res === -1 || $buffer[0] !== '\x1b' || $buffer[1] !== '[')
{
die('Failed to get screen size');
return false;
}
$this->screenRows = $rows;
$this->screenCols = $cols;
return true;
}
private function getWindowSize(): bool
{
$ws = $this->ffi->new('struct winsize');
$res = $this->ffi->ioctl(STDOUT_FILENO, TIOCGWINSZ, FFI::addr($ws));
if ($res === -1 || $ws->ws_col === 0)
{
return $this->getCursorPosition();
}
$this->screenCols = $ws->ws_col;
$this->screenRows = $ws->ws_row;
return true;
}
2019-10-15 13:23:25 -04:00
// ------------------------------------------------------------------------
// ! Row Operations
// ------------------------------------------------------------------------
protected function appendRow(string $s): void
{
$this->rows[] = Row::new($s);
}
// ------------------------------------------------------------------------
// ! File I/O
// ------------------------------------------------------------------------
public function open(string $filename): void
{
$baseFile = basename($filename);
$basePath = str_replace($baseFile, '', $filename);
$path = (is_dir($basePath)) ? $basePath : getcwd();
$fullname = $path . '/' . $baseFile;
$handle = fopen($fullname, 'rb');
while (($line = fgets($handle)) !== FALSE)
{
$this->appendRow($line);
}
fclose($handle);
}
2019-10-14 16:21:41 -04:00
// ------------------------------------------------------------------------
// ! Output
// ------------------------------------------------------------------------
2019-10-15 13:23:25 -04:00
protected function scroll(): void
{
if ($this->cursory < $this->rowoff)
{
$this->rowoff = $this->cursory;
}
if ($this->cursory >= $this->rowoff + $this->screenRows)
{
$this->rowoff = $this->cursory - $this->screenRows + 1;
}
}
2019-10-14 16:21:41 -04:00
protected function drawRows(): void
{
for ($y = 0; $y < $this->screenRows; $y++)
{
2019-10-15 13:23:25 -04:00
$filerow = $y + $this->rowoff;
if ($filerow >= count($this->rows))
2019-10-14 16:21:41 -04:00
{
2019-10-15 13:23:25 -04:00
if (count($this->rows) === 0 && $y === $this->screenRows / 3)
2019-10-14 16:21:41 -04:00
{
2019-10-15 13:23:25 -04:00
$welcome = sprintf('PHP Kilo editor -- version %s', KILO_VERSION);
$welcomelen = strlen($welcome);
if ($welcomelen > $this->screenCols)
{
$welcomelen = $this->screenCols;
}
2019-10-14 16:21:41 -04:00
2019-10-15 13:23:25 -04:00
$padding = ($this->screenCols - $welcomelen) / 2;
if ($padding)
{
$this->ab .= '~';
$padding--;
}
while ($padding--)
{
$this->ab .= ' ';
}
$this->ab .= substr($welcome, 0, $welcomelen);
2019-10-14 16:21:41 -04:00
}
2019-10-15 13:23:25 -04:00
else
2019-10-14 16:21:41 -04:00
{
2019-10-15 13:23:25 -04:00
$this->ab .= '~';
2019-10-14 16:21:41 -04:00
}
}
else
{
2019-10-15 13:23:25 -04:00
$len = strlen($this->rows[$filerow]->chars) - $this->coloff;
if ($len < 0)
{
$len = 0;
}
if ($len > $this->screenCols)
{
$len = $this->screenCols;
}
$this->ab .= substr($this->rows[$filerow]->chars, $this->coloff, $len);
2019-10-14 16:21:41 -04:00
}
$this->ab .= "\x1b[K"; // Clear the current line
if ($y < $this->screenRows - 1)
{
$this->ab .= "\r\n";
}
}
}
public function refreshScreen(): void
{
2019-10-15 13:23:25 -04:00
$this->scroll();
2019-10-14 16:21:41 -04:00
$this->ab = '';
$this->ab .= "\x1b[?25l"; // Hide the cursor
$this->ab .= "\x1b[H"; // Reposition cursor to top-left
$this->drawRows();
// Specify the current cursor position
2019-10-15 13:23:25 -04:00
$this->ab .= sprintf("\x1b[%d;%dH", ($this->cursory - $this->rowoff) + 1, $this->cursorx + 1);
2019-10-14 16:21:41 -04:00
$this->ab .= "\x1b[?25h"; // Show the cursor
write_stdout($this->ab);
}
// ------------------------------------------------------------------------
// ! Input
// ------------------------------------------------------------------------
protected function moveCursor(string $key): void
{
switch ($key)
{
case Key::ARROW_LEFT:
if ($this->cursorx !== 0)
{
$this->cursorx--;
}
break;
case Key::ARROW_RIGHT:
if ($this->cursorx !== $this->screenCols - 1)
{
$this->cursorx++;
}
break;
case Key::ARROW_UP:
if ($this->cursory !== 0)
{
$this->cursory--;
}
break;
case Key::ARROW_DOWN:
2019-10-15 13:23:25 -04:00
if ($this->cursory < count($this->rows))
2019-10-14 16:21:41 -04:00
{
$this->cursory++;
}
break;
}
}
public function processKeypress(): string
{
$c = $this->readKey();
switch($c)
{
case chr(ctrl_key('q')):
write_stdout("\x1b[2J"); // Clear the screen
write_stdout("\x1b[H"); // Reposition cursor to top-left
exit(0);
break;
case Key::HOME_KEY:
$this->cursorx = 0;
break;
case Key::END_KEY:
$this->cursorx = $this->screenCols - 1;
break;
case Key::PAGE_UP:
case Key::PAGE_DOWN:
2019-10-15 13:23:25 -04:00
$this->pageUpOrDown($c);
2019-10-14 16:21:41 -04:00
break;
case Key::ARROW_UP:
case Key::ARROW_DOWN:
case Key::ARROW_LEFT:
case Key::ARROW_RIGHT:
$this->moveCursor($c);
break;
}
return $c;
}
2019-10-15 13:23:25 -04:00
private function pageUpOrDown(string $c):void
{
$times = $this->screenRows;
while ($times--)
{
$this->moveCursor($c === Key::PAGE_UP ? Key::ARROW_UP : Key::ARROW_DOWN);
}
}
2019-10-14 16:21:41 -04:00
}