chars = $chars; } } class Editor { private FFI $ffi; protected int $cursorx = 0; protected int $cursory = 0; protected int $rowoff = 0; protected int $coloff = 0; protected int $screenRows = 0; protected int $screenCols = 0; protected string $ab = ''; /** * Array of Row objects */ protected array $rows = []; public static function new(FFI $ffi): Editor { return new self($ffi); } private function __construct($ffi) { $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; } // ------------------------------------------------------------------------ // ! 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); } // ------------------------------------------------------------------------ // ! Output // ------------------------------------------------------------------------ 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; } } protected function drawRows(): void { for ($y = 0; $y < $this->screenRows; $y++) { $filerow = $y + $this->rowoff; if ($filerow >= count($this->rows)) { if (count($this->rows) === 0 && $y === $this->screenRows / 3) { $welcome = sprintf('PHP Kilo editor -- version %s', KILO_VERSION); $welcomelen = strlen($welcome); if ($welcomelen > $this->screenCols) { $welcomelen = $this->screenCols; } $padding = ($this->screenCols - $welcomelen) / 2; if ($padding) { $this->ab .= '~'; $padding--; } while ($padding--) { $this->ab .= ' '; } $this->ab .= substr($welcome, 0, $welcomelen); } else { $this->ab .= '~'; } } else { $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); } $this->ab .= "\x1b[K"; // Clear the current line if ($y < $this->screenRows - 1) { $this->ab .= "\r\n"; } } } public function refreshScreen(): void { $this->scroll(); $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 $this->ab .= sprintf("\x1b[%d;%dH", ($this->cursory - $this->rowoff) + 1, $this->cursorx + 1); $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: if ($this->cursory < count($this->rows)) { $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: $this->pageUpOrDown($c); break; case Key::ARROW_UP: case Key::ARROW_DOWN: case Key::ARROW_LEFT: case Key::ARROW_RIGHT: $this->moveCursor($c); break; } return $c; } private function pageUpOrDown(string $c):void { $times = $this->screenRows; while ($times--) { $this->moveCursor($c === Key::PAGE_UP ? Key::ARROW_UP : Key::ARROW_DOWN); } } }