Finish chapter 4, drawing issues exist, at least in tmux

This commit is contained in:
Timothy Warren 2019-10-16 16:43:15 -04:00
parent 15f36a960a
commit 77473f9602
3 changed files with 291 additions and 55 deletions

1
kilo
View File

@ -21,6 +21,7 @@ function main(int $argc, array $argv): int
$editor->open($argv[1]);
}
$editor->setStatusMessage("HELP: Ctrl-Q = quit");
// Input Loop
while (true)

View File

@ -4,11 +4,28 @@ namespace Kilo;
use FFI;
trait MagicProperties {
abstract public function __get(string $name);
public function __set(string $name, $value)
{
if (property_exists($this, $name))
{
$this->$name = $value;
}
}
public function __isset(string $name): bool
{
return isset($this->$name);
}
}
class Key {
public const ARROW_LEFT = 'a';
public const ARROW_RIGHT = 'd';
public const ARROW_UP = 'w';
public const ARROW_DOWN = 's';
public const ARROW_LEFT = 'ARROW_LEFT';
public const ARROW_RIGHT = 'ARROW_RIGHT';
public const ARROW_UP = 'ARROW_UP';
public const ARROW_DOWN = 'ARROW_DOWN';
public const DEL_KEY = 'DEL';
public const HOME_KEY = 'HOME';
public const END_KEY = 'END';
@ -16,8 +33,15 @@ class Key {
public const PAGE_DOWN = 'PAGE_DOWN';
}
/**
* @property-read int size
* @property-read int rsize
*/
class Row {
public string $chars;
use MagicProperties;
public string $chars = '';
public string $render = '';
public static function new(string $chars): self
{
@ -28,24 +52,49 @@ class Row {
{
$this->chars = $chars;
}
public function __get(string $name)
{
switch ($name)
{
case 'size':
return strlen($this->chars);
case 'rsize':
return strlen($this->render);
default:
return NULL;
}
}
}
/**
* @property-read int numRows
*/
class Editor {
private FFI $ffi;
use MagicProperties;
protected int $cursorx = 0;
protected int $cursory = 0;
protected int $rowoff = 0;
protected int $coloff = 0;
private FFI $ffi;
private string $ab = '';
protected int $cursorX = 0;
protected int $cursorY = 0;
protected int $renderX = 0;
protected int $rowOffset = 0;
protected int $colOffset = 0;
protected int $screenRows = 0;
protected int $screenCols = 0;
protected string $ab = '';
/**
* Array of Row objects
*/
protected array $rows = [];
protected string $filename = '';
protected string $statusMsg = '';
protected int $statusMsgTime;
public static function new(FFI $ffi): Editor
{
return new self($ffi);
@ -54,11 +103,26 @@ class Editor {
private function __construct($ffi)
{
$this->ffi = $ffi;
$this->statusMsgTime = time();
if ( ! $this->getWindowSize())
{
die('Failed to get screen size');
}
$this->screenRows -= 2;
// print_r($this); die();
}
public function __get(string $name)
{
if ($name === 'numRows')
{
return count($this->rows);
}
return NULL;
}
// ------------------------------------------------------------------------
@ -72,27 +136,38 @@ class Editor {
{
$seq = read_stdin();
if (strlen($seq) < 3)
{
return '\x1b';
}
if (strpos($seq, '[') === 0)
{
if ((int)$seq[1] >= 0 && (int)$seq[1] <= 9)
$seq1 = (int)$seq[1];
if ($seq1 >= 0 && $seq1 <= 9)
{
if (strpos($seq, '~') === 2)
{
switch($seq[1])
switch ($seq[1])
{
case '1': return Key::HOME_KEY;
case '1':
case '7':
return Key::HOME_KEY;
case '4':
case '8':
return Key::END_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])
switch ($seq[1])
{
case 'A': return Key::ARROW_UP;
case 'B': return Key::ARROW_DOWN;
@ -105,7 +180,7 @@ class Editor {
}
else if (strpos($seq, 'O') === 0)
{
switch($seq[1])
switch ($seq[1])
{
case 'H': return Key::HOME_KEY;
case 'F': return Key::END_KEY;
@ -126,23 +201,22 @@ class Editor {
write_stdout("\x1b[999C\x1b[999B");
write_stdout("\x1b[6n");
$buffer = read_stdout(32);
$rows = 0;
$cols = 0;
$buffer = read_stdout();
$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;
return TRUE;
}
private function getWindowSize(): bool
@ -165,9 +239,47 @@ class Editor {
// ! Row Operations
// ------------------------------------------------------------------------
protected function rowCxToRx(Row $row, int $cx): int
{
$rx = 0;
for ($i = 0; $i < $cx; $i++)
{
if ($row->chars[$i] === "\t")
{
$rx += (KILO_TAB_STOP - 1) - ($rx % KILO_TAB_STOP);
}
$rx++;
}
return $rx;
}
protected function updateRow(Row $row): void
{
$idx = 0;
for ($i = 0; $i < $row->size; $i++)
{
if ($row->chars[$i] === "\t")
{
$row->render[$idx++] = ' ';
while ($idx % KILO_TAB_STOP !== 0)
{
$row->render[$idx++] = ' ';
}
}
else
{
$row->render[$idx++] = $row->chars[$i];
}
}
}
protected function appendRow(string $s): void
{
$this->rows[] = Row::new($s);
$at = $this->numRows;
$this->rows[$at] = Row::new($s);
$this->updateRow($this->rows[$at]);
}
// ------------------------------------------------------------------------
@ -176,6 +288,10 @@ class Editor {
public function open(string $filename): void
{
// Copy filename for display
$this->filename = $filename;
// Determine the full path to the file
$baseFile = basename($filename);
$basePath = str_replace($baseFile, '', $filename);
$path = (is_dir($basePath)) ? $basePath : getcwd();
@ -183,6 +299,7 @@ class Editor {
$fullname = $path . '/' . $baseFile;
// #TODO gracefully handle issues with loading a file
$handle = fopen($fullname, 'rb');
while (($line = fgets($handle)) !== FALSE)
@ -199,14 +316,30 @@ class Editor {
protected function scroll(): void
{
if ($this->cursory < $this->rowoff)
$this->renderX = 0;
if ($this->cursorY < $this->numRows)
{
$this->rowoff = $this->cursory;
$this->renderX = $this->rowCxToRx($this->rows[$this->cursorY], $this->cursorX);
}
if ($this->cursory >= $this->rowoff + $this->screenRows)
// Vertical Scrolling
if ($this->cursorY < $this->rowOffset)
{
$this->rowoff = $this->cursory - $this->screenRows + 1;
$this->rowOffset = $this->cursorY;
}
if ($this->cursorY >= $this->rowOffset + $this->screenRows)
{
$this->rowOffset = $this->cursorY - $this->screenRows + 1;
}
// Horizontal Scrolling
if ($this->renderX < $this->colOffset)
{
$this->colOffset = $this->renderX;
}
if ($this->renderX >= $this->colOffset + $this->screenCols)
{
$this->colOffset = $this->renderX - $this->screenCols + 1;
}
}
@ -214,10 +347,10 @@ class Editor {
{
for ($y = 0; $y < $this->screenRows; $y++)
{
$filerow = $y + $this->rowoff;
$filerow = $y + $this->rowOffset;
if ($filerow >= count($this->rows))
{
if (count($this->rows) === 0 && $y === $this->screenRows / 3)
if ($this->numRows === 0 && $y === $this->screenRows / 3)
{
$welcome = sprintf('PHP Kilo editor -- version %s', KILO_VERSION);
$welcomelen = strlen($welcome);
@ -227,7 +360,7 @@ class Editor {
}
$padding = ($this->screenCols - $welcomelen) / 2;
if ($padding)
if ($padding > 0)
{
$this->ab .= '~';
$padding--;
@ -246,7 +379,7 @@ class Editor {
}
else
{
$len = strlen($this->rows[$filerow]->chars) - $this->coloff;
$len = $this->rows[$filerow]->rsize - $this->colOffset;
if ($len < 0)
{
$len = 0;
@ -256,14 +389,55 @@ class Editor {
$len = $this->screenCols;
}
$this->ab .= substr($this->rows[$filerow]->chars, $this->coloff, $len);
$this->ab .= substr($this->rows[$filerow]->render, $this->colOffset, $len);
}
$this->ab .= "\x1b[K"; // Clear the current line
if ($y < $this->screenRows - 1)
$this->ab .= "\r\n";
}
}
protected function drawStatusBar(): void
{
$this->ab .= "\x1b[7m";
$statusFilename = $this->filename !== '' ? $this->filename : '[No Name]';
$status = sprintf("%.20s - %d lines", $statusFilename, count($this->rows));
$rstatus = sprintf("%d/%d", $this->cursorY + 1, count($this->rows));
$len = strlen($status);
$rlen = strlen($rstatus);
if ($len > $this->screenCols)
{
$len = $this->screenCols;
}
$this->ab .= substr($status, 0, $len);
while ($len < $this->screenCols)
{
if ($this->screenCols - $len === $rlen)
{
$this->ab .= "\r\n";
$this->ab .= substr($rstatus, 0, $rlen);
break;
}
$this->ab .= ' ';
$len++;
}
$this->ab .= "\x1b[m";
$this->ab .= "\r\n";
}
protected function drawMessageBar(): void
{
$this->ab .= "\x1b[K";
$len = strlen($this->statusMsg);
if ($len > $this->screenCols)
{
$len = $this->screenCols;
}
if ($len > 0 && (time() - $this->statusMsgTime) < 5)
{
$this->ab .= substr($this->statusMsg, 0, $len);
}
}
@ -277,13 +451,26 @@ class Editor {
$this->ab .= "\x1b[H"; // Reposition cursor to top-left
$this->drawRows();
$this->drawStatusBar();
$this->drawMessageBar();
// Specify the current cursor position
$this->ab .= sprintf("\x1b[%d;%dH", ($this->cursory - $this->rowoff) + 1, $this->cursorx + 1);
$this->ab .= sprintf("\x1b[%d;%dH",
($this->cursorY - $this->rowOffset) + 1,
($this->renderX - $this->colOffset) + 1
);
$this->ab .= "\x1b[?25h"; // Show the cursor
write_stdout($this->ab);
echo $this->ab;
}
public function setStatusMessage(string $fmt, ...$args): void
{
$this->statusMsg = (count($args) > 0)
? sprintf($fmt, ...$args)
: $fmt;
$this->statusMsgTime = time();
}
// ------------------------------------------------------------------------
@ -292,55 +479,75 @@ class Editor {
protected function moveCursor(string $key): void
{
$row = ($this->cursorY >= $this->numRows)
? NULL
: $this->rows[$this->cursorY];
switch ($key)
{
case Key::ARROW_LEFT:
if ($this->cursorx !== 0)
if ($this->cursorX !== 0)
{
$this->cursorx--;
$this->cursorX--;
}
else if ($this->cursorX > 0)
{
$this->cursorY--;
$this->cursorX = $this->rows[$this->cursorY]->size;
}
break;
case Key::ARROW_RIGHT:
if ($this->cursorx !== $this->screenCols - 1)
if ($row && $this->cursorX < $row->size)
{
$this->cursorx++;
$this->cursorX++;
}
else if ($row && $this->cursorX === $row->size)
{
$this->cursorY++;
$this->cursorX = 0;
}
break;
case Key::ARROW_UP:
if ($this->cursory !== 0)
if ($this->cursorY !== 0)
{
$this->cursory--;
$this->cursorY--;
}
break;
case Key::ARROW_DOWN:
if ($this->cursory < count($this->rows))
if ($this->cursorY < $this->numRows)
{
$this->cursory++;
$this->cursorY++;
}
break;
}
$row = ($this->cursorY >= $this->numRows)
? NULL
: $this->rows[$this->cursorY];
$rowlen = $row ? $row->size : 0;
if ($this->cursorX > $rowlen)
{
$this->cursorX = $rowlen;
}
}
public function processKeypress(): string
{
$c = $this->readKey();
switch($c)
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;
$this->cursorX = 0;
break;
case Key::END_KEY:
$this->cursorx = $this->screenCols - 1;
if ($this->cursorY < $this->numRows)
{
$this->cursorX = $this->rows[$this->cursorY]->size;
}
break;
case Key::PAGE_UP:
@ -354,13 +561,33 @@ class Editor {
case Key::ARROW_RIGHT:
$this->moveCursor($c);
break;
case chr(ctrl_key('q')):
write_stdout("\x1b[2J"); // Clear the screen
write_stdout("\x1b[H"); // Reposition cursor to top-left
exit(0);
break;
}
return $c;
}
private function pageUpOrDown(string $c):void
private function pageUpOrDown(string $c): void
{
if ($c === Key::PAGE_UP)
{
$this->cursorY = $this->rowOffset;
}
else if ($c === Key::PAGE_DOWN)
{
$this->cursorY = $this->rowOffset + $this->screenRows - 1;
if ($this->cursorY > $this->numRows)
{
$this->cursorY = $this->numRows;
}
}
$times = $this->screenRows;
while ($times--)
{

View File

@ -2,7 +2,15 @@
namespace Kilo;
// -----------------------------------------------------------------------------
// ! App Constants
// -----------------------------------------------------------------------------
define('KILO_VERSION', '0.0.1');
define('KILO_TAB_STOP', 4);
// -----------------------------------------------------------------------------
// ! Misc I/O constants
// -----------------------------------------------------------------------------
define('STDIN_FILENO', 0);
define('STDOUT_FILENO', 1);