diff --git a/kilo b/kilo index 6a28dcf..8b7d751 100755 --- a/kilo +++ b/kilo @@ -5,18 +5,23 @@ namespace Kilo; require_once __DIR__ . '/src/constants.php'; require_once __DIR__ . '/src/functions.php'; -require_once __DIR__ . '/src/editor.php'; +require_once __DIR__ . '/src/Editor.php'; function main(): int { + global $ffi; + enableRawMode(); - $editor = new Editor(); + // ob_start(); + $editor = new Editor($ffi); // Input Loop while (true) { + $editor->refreshScreen(); $editor->processKeypress(); + // ob_flush(); } return 0; diff --git a/src/Editor.php b/src/Editor.php new file mode 100644 index 0000000..cac2abc --- /dev/null +++ b/src/Editor.php @@ -0,0 +1,277 @@ +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; + } + + // ------------------------------------------------------------------------ + // ! Output + // ------------------------------------------------------------------------ + + protected function drawRows(): void + { + for ($y = 0; $y < $this->screenRows; $y++) + { + if ($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 .= '~'; + } + + $this->ab .= "\x1b[K"; // Clear the current line + if ($y < $this->screenRows - 1) + { + $this->ab .= "\r\n"; + } + } + } + + public function refreshScreen(): void + { + $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 + 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 !== $this->screenRows - 1) + { + $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: + { + $times = $this->screenRows; + while ($times--) + { + $this->moveCursor($c === Key::PAGE_UP ? Key::ARROW_UP : Key::ARROW_DOWN); + } + } + break; + + case Key::ARROW_UP: + case Key::ARROW_DOWN: + case Key::ARROW_LEFT: + case Key::ARROW_RIGHT: + $this->moveCursor($c); + break; + } + + return $c; + } +} \ No newline at end of file diff --git a/src/constants.php b/src/constants.php index 4c1f434..80b5b36 100644 --- a/src/constants.php +++ b/src/constants.php @@ -2,6 +2,8 @@ namespace Kilo; +define('KILO_VERSION', '0.0.1'); + define('STDIN_FILENO', 0); define('STDOUT_FILENO', 1); define('STDERR_FILENO', 2); @@ -129,4 +131,9 @@ define('VDISCARD', 15); /* Discard character [IEXTEN]. */ define('VMIN', 16); /* Minimum number of bytes read at once [!ICANON]. */ define('VTIME', 17); /* Time-out value (tenths of a second) [!ICANON]. */ define('VSTATUS', 18); /* Status character [ICANON]. */ -define('NCCS', 20); /* Value duplicated in . */ \ No newline at end of file +define('NCCS', 20); /* Value duplicated in . */ + +// ----------------------------------------------------------------------------- +// ! IOCTL constants +// ----------------------------------------------------------------------------- +define('TIOCGWINSZ', 0x5413); \ No newline at end of file diff --git a/src/editor.php b/src/editor.php deleted file mode 100644 index 8057c43..0000000 --- a/src/editor.php +++ /dev/null @@ -1,27 +0,0 @@ -readKey(); - - switch($c) - { - case chr(ctrl_key('q')): - exit(0); - break; - } - } - - protected function readKey(): string - { - return read_stdin(1); - } -} \ No newline at end of file diff --git a/src/ffi.h b/src/ffi.h index c419ae9..5821924 100644 --- a/src/ffi.h +++ b/src/ffi.h @@ -55,3 +55,16 @@ void cfmakeraw (struct termios *termios_p); //! // ----------------------------------------------------------------------------- int iscntrl (int); + +ssize_t read(int, void *, size_t); + +// ----------------------------------------------------------------------------- +//! +// ----------------------------------------------------------------------------- +struct winsize { + unsigned short ws_row; + unsigned short ws_col; + unsigned short ws_xpixel; + unsigned short ws_ypixel; +}; +int ioctl (int, int, ...); diff --git a/src/functions.php b/src/functions.php index b825691..ebb2896 100644 --- a/src/functions.php +++ b/src/functions.php @@ -15,7 +15,11 @@ function enableRawMode(): void register_shutdown_function('Kilo\\disableRawMode'); // Populate the original terminal settings - $ffi->tcgetattr(STDIN_FILENO, FFI::addr($original_termios)); + $res = $ffi->tcgetattr(STDIN_FILENO, FFI::addr($original_termios)); + if ($res === -1) + { + die('tcgetattr'); + } $termios = $ffi->new('struct termios'); $termios->c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); @@ -26,7 +30,11 @@ function enableRawMode(): void $termios->c_cc[VTIME] = 1; // Turn on raw mode - $ffi->tcsetattr(STDIN_FILENO, TCSAFLUSH, FFI::addr($termios)); + $res = $ffi->tcsetattr(STDIN_FILENO, TCSAFLUSH, FFI::addr($termios)); + if ($res === -1) + { + die('tcsetattr'); + } } function disableRawMode(): void @@ -34,7 +42,12 @@ function disableRawMode(): void global $ffi; global $original_termios; - $ffi->tcsetattr(STDIN_FILENO, TCSAFLUSH, FFI::addr($original_termios)); + $res = $ffi->tcsetattr(STDIN_FILENO, TCSAFLUSH, FFI::addr($original_termios)); + + if ($res === -1) + { + die('tcsetattr'); + } } function read_stdin(int $len = 128): string @@ -48,6 +61,29 @@ function read_stdin(int $len = 128): string return $input; } +function write_stdout(string $str, int $len = NULL): int +{ + $handle = fopen('php://stdout', 'ab'); + $res = (is_int($len)) + ? fwrite($handle, $str, $len) + : fwrite($handle, $str); + + fclose($handle); + + return $res; +} + +function read_stdout(int $len = 128): string +{ + $handle = fopen('php://stdout', 'rb'); + $input = fread($handle, $len); + + $input = rtrim($input); + fclose($handle); + + return $input; +} + function ctrl_key(string $char): int { return ord($char) & 0x1f;