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->open($argv[1]);
} }
$editor->setStatusMessage("HELP: Ctrl-Q = quit");
// Input Loop // Input Loop
while (true) while (true)

View File

@ -4,11 +4,28 @@ namespace Kilo;
use FFI; 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 { class Key {
public const ARROW_LEFT = 'a'; public const ARROW_LEFT = 'ARROW_LEFT';
public const ARROW_RIGHT = 'd'; public const ARROW_RIGHT = 'ARROW_RIGHT';
public const ARROW_UP = 'w'; public const ARROW_UP = 'ARROW_UP';
public const ARROW_DOWN = 's'; public const ARROW_DOWN = 'ARROW_DOWN';
public const DEL_KEY = 'DEL'; public const DEL_KEY = 'DEL';
public const HOME_KEY = 'HOME'; public const HOME_KEY = 'HOME';
public const END_KEY = 'END'; public const END_KEY = 'END';
@ -16,8 +33,15 @@ class Key {
public const PAGE_DOWN = 'PAGE_DOWN'; public const PAGE_DOWN = 'PAGE_DOWN';
} }
/**
* @property-read int size
* @property-read int rsize
*/
class Row { class Row {
public string $chars; use MagicProperties;
public string $chars = '';
public string $render = '';
public static function new(string $chars): self public static function new(string $chars): self
{ {
@ -28,24 +52,49 @@ class Row {
{ {
$this->chars = $chars; $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 { class Editor {
private FFI $ffi; use MagicProperties;
protected int $cursorx = 0; private FFI $ffi;
protected int $cursory = 0; private string $ab = '';
protected int $rowoff = 0;
protected int $coloff = 0; 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 $screenRows = 0;
protected int $screenCols = 0; protected int $screenCols = 0;
protected string $ab = '';
/** /**
* Array of Row objects * Array of Row objects
*/ */
protected array $rows = []; protected array $rows = [];
protected string $filename = '';
protected string $statusMsg = '';
protected int $statusMsgTime;
public static function new(FFI $ffi): Editor public static function new(FFI $ffi): Editor
{ {
return new self($ffi); return new self($ffi);
@ -54,11 +103,26 @@ class Editor {
private function __construct($ffi) private function __construct($ffi)
{ {
$this->ffi = $ffi; $this->ffi = $ffi;
$this->statusMsgTime = time();
if ( ! $this->getWindowSize()) if ( ! $this->getWindowSize())
{ {
die('Failed to get screen size'); 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(); $seq = read_stdin();
if (strlen($seq) < 3)
{
return '\x1b';
}
if (strpos($seq, '[') === 0) 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) 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 '3': return Key::DEL_KEY;
case '4': return Key::END_KEY;
case '5': return Key::PAGE_UP; case '5': return Key::PAGE_UP;
case '6': return Key::PAGE_DOWN; case '6': return Key::PAGE_DOWN;
case '7': return Key::HOME_KEY;
case '8': return Key::END_KEY;
} }
} }
} }
else else
{ {
switch($seq[1]) switch ($seq[1])
{ {
case 'A': return Key::ARROW_UP; case 'A': return Key::ARROW_UP;
case 'B': return Key::ARROW_DOWN; case 'B': return Key::ARROW_DOWN;
@ -105,7 +180,7 @@ class Editor {
} }
else if (strpos($seq, 'O') === 0) else if (strpos($seq, 'O') === 0)
{ {
switch($seq[1]) switch ($seq[1])
{ {
case 'H': return Key::HOME_KEY; case 'H': return Key::HOME_KEY;
case 'F': return Key::END_KEY; case 'F': return Key::END_KEY;
@ -126,23 +201,22 @@ class Editor {
write_stdout("\x1b[999C\x1b[999B"); write_stdout("\x1b[999C\x1b[999B");
write_stdout("\x1b[6n"); write_stdout("\x1b[6n");
$buffer = read_stdout(32);
$rows = 0; $rows = 0;
$cols = 0; $cols = 0;
$buffer = read_stdout();
$res = sscanf($buffer, '\x1b[%d;%dR', $rows, $cols); $res = sscanf($buffer, '\x1b[%d;%dR', $rows, $cols);
if ($res === -1 || $buffer[0] !== '\x1b' || $buffer[1] !== '[') if ($res === -1 || $buffer[0] !== '\x1b' || $buffer[1] !== '[')
{ {
die('Failed to get screen size'); die('Failed to get screen size');
return false;
} }
$this->screenRows = $rows; $this->screenRows = $rows;
$this->screenCols = $cols; $this->screenCols = $cols;
return true; return TRUE;
} }
private function getWindowSize(): bool private function getWindowSize(): bool
@ -165,9 +239,47 @@ class Editor {
// ! Row Operations // ! 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 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 public function open(string $filename): void
{ {
// Copy filename for display
$this->filename = $filename;
// Determine the full path to the file
$baseFile = basename($filename); $baseFile = basename($filename);
$basePath = str_replace($baseFile, '', $filename); $basePath = str_replace($baseFile, '', $filename);
$path = (is_dir($basePath)) ? $basePath : getcwd(); $path = (is_dir($basePath)) ? $basePath : getcwd();
@ -183,6 +299,7 @@ class Editor {
$fullname = $path . '/' . $baseFile; $fullname = $path . '/' . $baseFile;
// #TODO gracefully handle issues with loading a file
$handle = fopen($fullname, 'rb'); $handle = fopen($fullname, 'rb');
while (($line = fgets($handle)) !== FALSE) while (($line = fgets($handle)) !== FALSE)
@ -199,14 +316,30 @@ class Editor {
protected function scroll(): void 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++) for ($y = 0; $y < $this->screenRows; $y++)
{ {
$filerow = $y + $this->rowoff; $filerow = $y + $this->rowOffset;
if ($filerow >= count($this->rows)) 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); $welcome = sprintf('PHP Kilo editor -- version %s', KILO_VERSION);
$welcomelen = strlen($welcome); $welcomelen = strlen($welcome);
@ -227,7 +360,7 @@ class Editor {
} }
$padding = ($this->screenCols - $welcomelen) / 2; $padding = ($this->screenCols - $welcomelen) / 2;
if ($padding) if ($padding > 0)
{ {
$this->ab .= '~'; $this->ab .= '~';
$padding--; $padding--;
@ -246,7 +379,7 @@ class Editor {
} }
else else
{ {
$len = strlen($this->rows[$filerow]->chars) - $this->coloff; $len = $this->rows[$filerow]->rsize - $this->colOffset;
if ($len < 0) if ($len < 0)
{ {
$len = 0; $len = 0;
@ -256,14 +389,55 @@ class Editor {
$len = $this->screenCols; $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 $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->ab .= "\x1b[H"; // Reposition cursor to top-left
$this->drawRows(); $this->drawRows();
$this->drawStatusBar();
$this->drawMessageBar();
// Specify the current cursor position // 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 $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 protected function moveCursor(string $key): void
{ {
$row = ($this->cursorY >= $this->numRows)
? NULL
: $this->rows[$this->cursorY];
switch ($key) switch ($key)
{ {
case Key::ARROW_LEFT: 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; break;
case Key::ARROW_RIGHT: 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; break;
case Key::ARROW_UP: case Key::ARROW_UP:
if ($this->cursory !== 0) if ($this->cursorY !== 0)
{ {
$this->cursory--; $this->cursorY--;
} }
break; break;
case Key::ARROW_DOWN: case Key::ARROW_DOWN:
if ($this->cursory < count($this->rows)) if ($this->cursorY < $this->numRows)
{ {
$this->cursory++; $this->cursorY++;
} }
break; 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 public function processKeypress(): string
{ {
$c = $this->readKey(); $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: case Key::HOME_KEY:
$this->cursorx = 0; $this->cursorX = 0;
break; break;
case Key::END_KEY: case Key::END_KEY:
$this->cursorx = $this->screenCols - 1; if ($this->cursorY < $this->numRows)
{
$this->cursorX = $this->rows[$this->cursorY]->size;
}
break; break;
case Key::PAGE_UP: case Key::PAGE_UP:
@ -354,13 +561,33 @@ class Editor {
case Key::ARROW_RIGHT: case Key::ARROW_RIGHT:
$this->moveCursor($c); $this->moveCursor($c);
break; 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; 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; $times = $this->screenRows;
while ($times--) while ($times--)
{ {

View File

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