new('struct termios'); // ---------------------------------------------------------------------------- // ! Raw mode / Terminal size // ---------------------------------------------------------------------------- function enable_raw_mode(): void { global $original_termios; // Make sure to restore normal mode on exit/die/crash register_shutdown_function('Kilo\disable_raw_mode'); $ffi = get_ffi(); // Populate the original terminal settings $res = $ffi->tcgetattr(C::STDIN_FILENO, FFI::addr($original_termios)); if ($res === -1) { die('tcgetattr'); } // So, the only thing that seems to really matter here is that c_oflag is 0... $termios = clone $original_termios; $termios->c_iflag = 0; //$termios->c_iflag & ~(C::BRKINT | C::ICRNL | C::INPCK | C::ISTRIP | C::IXON); $termios->c_oflag = 0; // $termios->c_oflag && ~(C::OPOST); $termios->c_cflag |= (C::CS8); $termios->c_lflag = $termios->c_lflag & ~( C::ECHO | C::ICANON | C::IEXTEN | C::ISIG); $termios->c_cc[C::VMIN] = 0; $termios->c_cc[C::VTIME] = 1; // Turn on raw mode $res = $ffi->tcsetattr(C::STDIN_FILENO, C::TCSAFLUSH, FFI::addr($termios)); if ($res === -1) { die('tcsetattr'); } } function disable_raw_mode(): void { global $original_termios; $ffi = get_ffi(); $res = $ffi->tcsetattr(C::STDIN_FILENO, C::TCSAFLUSH, FFI::addr($original_termios)); echo "\n"; if ($res === -1) { die('tcsetattr'); } } /** * @TODO fix */ function get_cursor_position() { write_stdout("\x1b[999C\x1b[999B"); write_stdout("\x1b[6n"); $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 [$rows, $cols]; } function get_window_size() { $ffi = get_ffi(); $ws = $ffi->new('struct winsize'); $res = $ffi->ioctl(C::STDOUT_FILENO, C::TIOCGWINSZ, FFI::addr($ws)); if ($res === -1 || $ws->ws_col === 0) { return get_cursor_position(); } return [$ws->ws_row, $ws->ws_col]; } // ---------------------------------------------------------------------------- // ! C function/macro equivalents // ---------------------------------------------------------------------------- /** * Do bit twiddling to convert a letter into * its Ctrl-letter equivalent ordinal ascii value * * @param string $char * @return int */ function ctrl_key(string $char): int { if ( ! is_ascii($char)) { return -1; } // b1,100,001 (a) & b0,011,111 (0x1f) = b0,000,001 (SOH) // b1,100,010 (b) & b0,011,111 (0x1f) = b0,000,010 (STX) // ...and so on return ord($char) & 0x1f; } /** * Does the one-character string contain an ascii ordinal value? * * @param string $single_char * @return bool */ function is_ascii(string $single_char): bool { if (strlen($single_char) > 1) { return FALSE; } return ord($single_char) < 0x80; } /** * Does the one-character string contain an ascii control character? * * @param string $char * @return bool */ function is_cntrl(string $char): bool { $c = ord($char); return is_ascii($char) && ( $c < 0x20 || $c === 0x7f ); } /** * Does the one-character string contain an ascii number? * * @param string $char * @return bool */ function is_digit(string $char): bool { $c = ord($char); return is_ascii($char) && ( $c > 0x2f && $c < 0x3a ); } /** * Does the one-character string contain ascii whitespace? * * @param string $char * @return bool */ function is_space(string $char): bool { $ws = [' ', "\t", "\n", "\r", "\xa", "\xb", "\xc"]; return is_ascii($char) && in_array($char, $ws, TRUE); } // ---------------------------------------------------------------------------- // ! Helper functions // ---------------------------------------------------------------------------- /** * A 'singleton' function to replace a global variable * * @return FFI */ function get_ffi(): FFI { static $ffi; if ($ffi === NULL) { $ffi = FFI::load(__DIR__ . '/ffi.h'); } return $ffi; } /** * Does the one-character string contain a character that separates tokens? * * @param string $char * @return bool */ function is_separator(string $char): bool { if ( ! is_ascii($char)) { return FALSE; } // `strpos` is used instead of `strchr`/`strstr` as we don't care about the actual match // while `strchr` would match the C version, it also returns the match $isSep = (strpos(',.()+-/*=~%<>[];', $char) !== FALSE); return is_space($char) || $char === "\0" || $isSep; } /** * Pull input from the stdin stream. * * @param int $len * @return string */ function read_stdin(int $len = 128): string { $handle = fopen('php://stdin', 'rb'); $input = fread($handle, $len); fclose($handle); return $input; } /** * Write to the stdout stream * * @param string $str * @param int|NULL $len * @return int */ 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; } /** * Replaces a slice of an array with the same value * * @param array $array The array to update * @param int $offset The index of the first location to update * @param int $length The number of indices to update * @param mixed $value The value to replace in the range */ function array_replace_range(array &$array, int $offset, int $length, $value):void { if ($length === 1) { $array[$offset] = $value; return; } $replacement = array_fill(0, $length, $value); array_splice($array, $offset, $length, $replacement); /* $end = $offset + $length; for ($i = $offset; $i < $end; $i++) { $array[$i] = $value; } */ } /** * Get the ASCII color escape number for the specified syntax type * * @param int $hl * @return int */ function syntax_to_color(int $hl): int { $map = [ Highlight::COMMENT => Color::FG_CYAN, Highlight::ML_COMMENT => Color::FG_BRIGHT_BLACK, Highlight::KEYWORD1 => Color::FG_YELLOW, Highlight::KEYWORD2 => Color::FG_GREEN, Highlight::STRING => Color::FG_MAGENTA, Highlight::NUMBER => Color::FG_RED, Highlight::OPERATOR => Color::FG_BRIGHT_GREEN, Highlight::VARIABLE => Color::FG_BRIGHT_CYAN, Highlight::DELIMITER => Color::FG_BLUE, Highlight::INVALID => Color::BG_BRIGHT_RED, Highlight::MATCH => Color::INVERT, ]; return (array_key_exists($hl, $map)) ? $map[$hl] : Color::FG_WHITE; } /** * Use 'token_get_all' to get the tokens for a file, * organized by row number * * @param string $code * @return array */ function get_php_tokens(string $code): array { $raw_tokens = token_get_all($code); $tokens = []; $lineNum = 1; $line = []; foreach($raw_tokens as $token) { // Simple characters, usually delimiters or single character operators if ( ! is_array($token)) { $line[] = [ 'type' => -1, 'typeName' => 'RAW', 'char' => $token, ]; continue; } [$type, $char, $currentLine] = $token; // Only return the first line of a multi-line token if ($char !== "\n" && strpos($char, "\n") !== FALSE) { $char = explode("\n", $char)[0]; } $current = [ 'type' => $type, 'typeName' => token_name($type), 'char' => $char, 'line' => $currentLine, ]; if ($currentLine !== $lineNum) { $tokens[$lineNum] = $line; // Make sure to insert empty arrays for empty lines // So the array of tokens isn't sparse for ($i = $lineNum; $i < $currentLine; $i++) { if ( ! array_key_exists($i, $tokens)) { $tokens[$i] = []; } } $lineNum = $currentLine; $line = []; } $line[] = $current; } $tokens[$lineNum] = $line; return $tokens; }