Finish chapter 4, drawing issues exist, at least in tmux
This commit is contained in:
parent
15f36a960a
commit
77473f9602
1
kilo
1
kilo
@ -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)
|
||||
|
327
src/Editor.php
327
src/Editor.php
@ -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,21 +136,32 @@ 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])
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,15 +389,56 @@ 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 .= 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);
|
||||
}
|
||||
}
|
||||
|
||||
public function refreshScreen(): void
|
||||
@ -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,36 +479,59 @@ 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
|
||||
@ -329,18 +539,15 @@ class Editor {
|
||||
$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;
|
||||
$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,6 +561,12 @@ 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;
|
||||
@ -361,6 +574,20 @@ class Editor {
|
||||
|
||||
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--)
|
||||
{
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user