Complete chapter 3
This commit is contained in:
parent
e0a9d066ca
commit
5e6e6e7c7d
9
kilo
9
kilo
@ -5,18 +5,23 @@ namespace Kilo;
|
|||||||
|
|
||||||
require_once __DIR__ . '/src/constants.php';
|
require_once __DIR__ . '/src/constants.php';
|
||||||
require_once __DIR__ . '/src/functions.php';
|
require_once __DIR__ . '/src/functions.php';
|
||||||
require_once __DIR__ . '/src/editor.php';
|
require_once __DIR__ . '/src/Editor.php';
|
||||||
|
|
||||||
function main(): int
|
function main(): int
|
||||||
{
|
{
|
||||||
|
global $ffi;
|
||||||
|
|
||||||
enableRawMode();
|
enableRawMode();
|
||||||
|
|
||||||
$editor = new Editor();
|
// ob_start();
|
||||||
|
$editor = new Editor($ffi);
|
||||||
|
|
||||||
// Input Loop
|
// Input Loop
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
|
$editor->refreshScreen();
|
||||||
$editor->processKeypress();
|
$editor->processKeypress();
|
||||||
|
// ob_flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
277
src/Editor.php
Normal file
277
src/Editor.php
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Kilo;
|
||||||
|
|
||||||
|
use FFI;
|
||||||
|
|
||||||
|
class Key {
|
||||||
|
public const ARROW_LEFT = 'a';
|
||||||
|
public const ARROW_RIGHT = 'd';
|
||||||
|
public const ARROW_UP = 'w';
|
||||||
|
public const ARROW_DOWN = 's';
|
||||||
|
public const DEL_KEY = 'DEL';
|
||||||
|
public const HOME_KEY = 'HOME';
|
||||||
|
public const END_KEY = 'END';
|
||||||
|
public const PAGE_UP = 'PAGE_UP';
|
||||||
|
public const PAGE_DOWN = 'PAGE_DOWN';
|
||||||
|
}
|
||||||
|
|
||||||
|
class Editor {
|
||||||
|
private FFI $ffi;
|
||||||
|
|
||||||
|
protected int $cursorx = 0;
|
||||||
|
protected int $cursory = 0;
|
||||||
|
protected int $screenRows = 0;
|
||||||
|
protected int $screenCols = 0;
|
||||||
|
protected string $ab = '';
|
||||||
|
|
||||||
|
public 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// ! 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;
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace Kilo;
|
namespace Kilo;
|
||||||
|
|
||||||
|
define('KILO_VERSION', '0.0.1');
|
||||||
|
|
||||||
define('STDIN_FILENO', 0);
|
define('STDIN_FILENO', 0);
|
||||||
define('STDOUT_FILENO', 1);
|
define('STDOUT_FILENO', 1);
|
||||||
define('STDERR_FILENO', 2);
|
define('STDERR_FILENO', 2);
|
||||||
@ -130,3 +132,8 @@ define('VMIN', 16); /* Minimum number of bytes read at once [!ICANON]. */
|
|||||||
define('VTIME', 17); /* Time-out value (tenths of a second) [!ICANON]. */
|
define('VTIME', 17); /* Time-out value (tenths of a second) [!ICANON]. */
|
||||||
define('VSTATUS', 18); /* Status character [ICANON]. */
|
define('VSTATUS', 18); /* Status character [ICANON]. */
|
||||||
define('NCCS', 20); /* Value duplicated in <hurd/tioctl.defs>. */
|
define('NCCS', 20); /* Value duplicated in <hurd/tioctl.defs>. */
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// ! IOCTL constants
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
define('TIOCGWINSZ', 0x5413);
|
@ -1,27 +0,0 @@
|
|||||||
<?php declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Kilo;
|
|
||||||
|
|
||||||
class Editor {
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public function processKeypress(): void
|
|
||||||
{
|
|
||||||
$c = $this->readKey();
|
|
||||||
|
|
||||||
switch($c)
|
|
||||||
{
|
|
||||||
case chr(ctrl_key('q')):
|
|
||||||
exit(0);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function readKey(): string
|
|
||||||
{
|
|
||||||
return read_stdin(1);
|
|
||||||
}
|
|
||||||
}
|
|
13
src/ffi.h
13
src/ffi.h
@ -55,3 +55,16 @@ void cfmakeraw (struct termios *termios_p);
|
|||||||
//! <ctype.h>
|
//! <ctype.h>
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
int iscntrl (int);
|
int iscntrl (int);
|
||||||
|
|
||||||
|
ssize_t read(int, void *, size_t);
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
//! <sys/ioctl.h>
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
struct winsize {
|
||||||
|
unsigned short ws_row;
|
||||||
|
unsigned short ws_col;
|
||||||
|
unsigned short ws_xpixel;
|
||||||
|
unsigned short ws_ypixel;
|
||||||
|
};
|
||||||
|
int ioctl (int, int, ...);
|
||||||
|
@ -15,7 +15,11 @@ function enableRawMode(): void
|
|||||||
register_shutdown_function('Kilo\\disableRawMode');
|
register_shutdown_function('Kilo\\disableRawMode');
|
||||||
|
|
||||||
// Populate the original terminal settings
|
// 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 = $ffi->new('struct termios');
|
||||||
$termios->c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
|
$termios->c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
|
||||||
@ -26,7 +30,11 @@ function enableRawMode(): void
|
|||||||
$termios->c_cc[VTIME] = 1;
|
$termios->c_cc[VTIME] = 1;
|
||||||
|
|
||||||
// Turn on raw mode
|
// 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
|
function disableRawMode(): void
|
||||||
@ -34,7 +42,12 @@ function disableRawMode(): void
|
|||||||
global $ffi;
|
global $ffi;
|
||||||
global $original_termios;
|
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
|
function read_stdin(int $len = 128): string
|
||||||
@ -48,6 +61,29 @@ function read_stdin(int $len = 128): string
|
|||||||
return $input;
|
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
|
function ctrl_key(string $char): int
|
||||||
{
|
{
|
||||||
return ord($char) & 0x1f;
|
return ord($char) & 0x1f;
|
||||||
|
Loading…
Reference in New Issue
Block a user