diff --git a/kilo b/kilo index 2f3cd90..d8b5f64 100755 --- a/kilo +++ b/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) diff --git a/src/Editor.php b/src/Editor.php index 5e7eb9b..3e768ee 100644 --- a/src/Editor.php +++ b/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,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--) { diff --git a/src/constants.php b/src/constants.php index 80b5b36..e345ff6 100644 --- a/src/constants.php +++ b/src/constants.php @@ -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);