Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | n/a |
0 / 0 |
|
93.75% |
15 / 16 |
CRAP | |
95.92% |
94 / 98 |
|
Aviat\Kilo\has_tput | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
Aviat\Kilo\get_window_size | n/a |
0 / 0 |
7 | n/a |
0 / 0 |
|||||
Aviat\Kilo\ctrl_key | |
100.00% |
1 / 1 |
2 | |
100.00% |
3 / 3 |
|||
Aviat\Kilo\is_ascii | |
100.00% |
1 / 1 |
2 | |
100.00% |
3 / 3 |
|||
Aviat\Kilo\is_ctrl | |
100.00% |
1 / 1 |
3 | |
100.00% |
2 / 2 |
|||
Aviat\Kilo\is_digit | |
100.00% |
1 / 1 |
3 | |
100.00% |
2 / 2 |
|||
Aviat\Kilo\is_space | |
100.00% |
1 / 1 |
2 | |
100.00% |
8 / 8 |
|||
Aviat\Kilo\get_ffi | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 4 |
|||
Aviat\Kilo\is_separator | |
100.00% |
1 / 1 |
4 | |
100.00% |
4 / 4 |
|||
Aviat\Kilo\read_stdin | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
Aviat\Kilo\write_stdout | n/a |
0 / 0 |
2 | n/a |
0 / 0 |
|||||
Aviat\Kilo\array_replace_range | |
100.00% |
1 / 1 |
2 | |
100.00% |
5 / 5 |
|||
Aviat\Kilo\str_contains | |
100.00% |
1 / 1 |
3 | |
100.00% |
5 / 5 |
|||
Aviat\Kilo\syntax_to_color | |
100.00% |
1 / 1 |
1 | |
100.00% |
12 / 12 |
|||
Aviat\Kilo\tabs_to_spaces | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
Aviat\Kilo\get_file_syntax_map | |
100.00% |
1 / 1 |
2 | |
100.00% |
49 / 49 |
1 | <?php declare(strict_types=1); |
2 | |
3 | namespace Aviat\Kilo; |
4 | |
5 | use FFI; |
6 | |
7 | use Aviat\Kilo\Enum\{C, Color, Highlight, KeyCode}; |
8 | |
9 | /** |
10 | * See if tput exists for fallback terminal size detection |
11 | * |
12 | * @return bool |
13 | * @codeCoverageIgnore |
14 | */ |
15 | function has_tput(): bool |
16 | { |
17 | return str_contains(shell_exec('type tput'), ' is '); |
18 | } |
19 | |
20 | // ---------------------------------------------------------------------------- |
21 | // ! Terminal size |
22 | // ---------------------------------------------------------------------------- |
23 | |
24 | /** |
25 | * Get the size of the current terminal window |
26 | * |
27 | * @codeCoverageIgnore |
28 | * @return array |
29 | */ |
30 | function get_window_size(): array |
31 | { |
32 | // First, try to get the answer from ioctl |
33 | $ffi = get_ffi(); |
34 | $ws = $ffi->new('struct winsize'); |
35 | $res = $ffi->ioctl(C::STDOUT_FILENO, C::TIOCGWINSZ, FFI::addr($ws)); |
36 | if ($res === 0 && $ws->ws_col !== 0 && $ws->ws_row !== 0) |
37 | { |
38 | return [$ws->ws_row, $ws->ws_col]; |
39 | } |
40 | |
41 | // Try using tput |
42 | if (has_tput()) |
43 | { |
44 | $rows = (int)trim(shell_exec('tput lines')); |
45 | $cols = (int)trim(shell_exec('tput cols')); |
46 | |
47 | if ($rows > 0 && $cols > 0) |
48 | { |
49 | return [$rows, $cols]; |
50 | } |
51 | } |
52 | |
53 | // Worst-case, return an arbitrary 'standard' size |
54 | return [25, 80]; |
55 | } |
56 | |
57 | // ---------------------------------------------------------------------------- |
58 | // ! C function/macro equivalents |
59 | // ---------------------------------------------------------------------------- |
60 | |
61 | /** |
62 | * Do bit twiddling to convert a letter into |
63 | * its Ctrl-letter equivalent ordinal ascii value |
64 | * |
65 | * @param string $char |
66 | * @return int |
67 | */ |
68 | function ctrl_key(string $char): int |
69 | { |
70 | if ( ! is_ascii($char)) |
71 | { |
72 | return -1; |
73 | } |
74 | |
75 | // b1,100,001 (a) & b0,011,111 (0x1f) = b0,000,001 (SOH) |
76 | // b1,100,010 (b) & b0,011,111 (0x1f) = b0,000,010 (STX) |
77 | // ...and so on |
78 | return ord($char) & 0x1f; |
79 | } |
80 | |
81 | /** |
82 | * Does the one-character string contain an ascii ordinal value? |
83 | * |
84 | * @param string $single_char |
85 | * @return bool |
86 | */ |
87 | function is_ascii(string $single_char): bool |
88 | { |
89 | if (strlen($single_char) > 1) |
90 | { |
91 | return FALSE; |
92 | } |
93 | |
94 | return ord($single_char) < 0x80; |
95 | } |
96 | |
97 | /** |
98 | * Does the one-character string contain an ascii control character? |
99 | * |
100 | * @param string $char |
101 | * @return bool |
102 | */ |
103 | function is_ctrl(string $char): bool |
104 | { |
105 | $c = ord($char); |
106 | return is_ascii($char) && ( $c === 0x7f || $c < 0x20 ); |
107 | } |
108 | |
109 | /** |
110 | * Does the one-character string contain an ascii number? |
111 | * |
112 | * @param string $char |
113 | * @return bool |
114 | */ |
115 | function is_digit(string $char): bool |
116 | { |
117 | $c = ord($char); |
118 | return is_ascii($char) && ( $c > 0x2f && $c < 0x3a ); |
119 | } |
120 | |
121 | /** |
122 | * Does the one-character string contain ascii whitespace? |
123 | * |
124 | * @param string $char |
125 | * @return bool |
126 | */ |
127 | function is_space(string $char): bool |
128 | { |
129 | $ws = [ |
130 | KeyCode::CARRIAGE_RETURN, |
131 | KeyCode::FORM_FEED, |
132 | KeyCode::NEWLINE, |
133 | KeyCode::SPACE, |
134 | KeyCode::TAB, |
135 | KeyCode::VERTICAL_TAB, |
136 | ]; |
137 | return is_ascii($char) && in_array($char, $ws, TRUE); |
138 | } |
139 | |
140 | // ---------------------------------------------------------------------------- |
141 | // ! Helper functions |
142 | // ---------------------------------------------------------------------------- |
143 | |
144 | /** |
145 | * A 'singleton' function to replace a global variable |
146 | * |
147 | * @return FFI |
148 | */ |
149 | function get_ffi(): FFI |
150 | { |
151 | static $ffi; |
152 | |
153 | if ($ffi === NULL) |
154 | { |
155 | $ffi = FFI::load(__DIR__ . '/ffi.h'); |
156 | } |
157 | |
158 | return $ffi; |
159 | } |
160 | |
161 | /** |
162 | * Does the one-character string contain a character that separates tokens? |
163 | * |
164 | * @param string $char |
165 | * @return bool |
166 | */ |
167 | function is_separator(string $char): bool |
168 | { |
169 | if ( ! is_ascii($char)) |
170 | { |
171 | return FALSE; |
172 | } |
173 | |
174 | $isSep = str_contains(',.()+-/*=~%<>[];', $char); |
175 | |
176 | return is_space($char) || $char === KeyCode::NULL || $isSep; |
177 | } |
178 | |
179 | /** |
180 | * Pull input from the stdin stream. |
181 | * |
182 | * @codeCoverageIgnore |
183 | * @param int $len |
184 | * @return string |
185 | */ |
186 | function read_stdin(int $len = 128): string |
187 | { |
188 | $handle = fopen('php://stdin', 'rb'); |
189 | $input = fread($handle, $len); |
190 | fclose($handle); |
191 | |
192 | return $input; |
193 | } |
194 | |
195 | /** |
196 | * Write to the stdout stream |
197 | * |
198 | * @codeCoverageIgnore |
199 | * @param string $str |
200 | * @param int|NULL $len |
201 | * @return int |
202 | */ |
203 | function write_stdout(string $str, int $len = NULL): int |
204 | { |
205 | $handle = fopen('php://stdout', 'ab'); |
206 | $res = (is_int($len)) |
207 | ? fwrite($handle, $str, $len) |
208 | : fwrite($handle, $str); |
209 | |
210 | fclose($handle); |
211 | |
212 | return $res; |
213 | } |
214 | |
215 | /** |
216 | * Replaces a slice of an array with the same value |
217 | * |
218 | * @param array $array The array to update |
219 | * @param int $offset The index of the first location to update |
220 | * @param int $length The number of indices to update |
221 | * @param mixed $value The value to replace in the range |
222 | */ |
223 | function array_replace_range(array &$array, int $offset, int $length, $value):void |
224 | { |
225 | if ($length === 1) |
226 | { |
227 | $array[$offset] = $value; |
228 | return; |
229 | } |
230 | |
231 | $replacement = array_fill(0, $length, $value); |
232 | array_splice($array, $offset, $length, $replacement); |
233 | } |
234 | |
235 | /** |
236 | * Does the string $haystack contain $str, optionally searching from $offset? |
237 | * |
238 | * @param string $haystack |
239 | * @param string $str |
240 | * @param int|null $offset |
241 | * @return bool |
242 | */ |
243 | function str_contains(string $haystack, string $str, ?int $offset = NULL): bool |
244 | { |
245 | if (empty($str)) |
246 | { |
247 | return FALSE; |
248 | } |
249 | |
250 | return ($offset !== NULL) |
251 | ? strpos($haystack, $str, $offset) !== FALSE |
252 | : \str_contains($haystack, $str); |
253 | } |
254 | |
255 | /** |
256 | * Get the ASCII color escape number for the specified syntax type |
257 | * |
258 | * @param int $hl |
259 | * @return int |
260 | */ |
261 | function syntax_to_color(int $hl): int |
262 | { |
263 | return match ($hl) |
264 | { |
265 | Highlight::COMMENT => Color::FG_CYAN, |
266 | Highlight::ML_COMMENT => Color::FG_BRIGHT_BLACK, |
267 | Highlight::KEYWORD1 => Color::FG_YELLOW, |
268 | Highlight::KEYWORD2 => Color::FG_GREEN, |
269 | Highlight::STRING => Color::FG_MAGENTA, |
270 | Highlight::NUMBER => Color::FG_RED, |
271 | Highlight::OPERATOR => Color::FG_BRIGHT_GREEN, |
272 | Highlight::VARIABLE => Color::FG_BRIGHT_CYAN, |
273 | Highlight::DELIMITER => Color::FG_BLUE, |
274 | Highlight::INVALID => Color::BG_BRIGHT_RED, |
275 | Highlight::MATCH => Color::INVERT, |
276 | default => Color::FG_WHITE, |
277 | }; |
278 | } |
279 | |
280 | /** |
281 | * Replace tabs with the specified number of spaces. |
282 | * |
283 | * @param string $str |
284 | * @param int|null $number |
285 | * @return string |
286 | */ |
287 | function tabs_to_spaces(string $str, ?int $number = KILO_TAB_STOP): string |
288 | { |
289 | return str_replace(KeyCode::TAB, str_repeat(KeyCode::SPACE, $number), $str); |
290 | } |
291 | |
292 | /** |
293 | * Generate/Get the syntax highlighting objects |
294 | * |
295 | * @return array |
296 | */ |
297 | function get_file_syntax_map(): array |
298 | { |
299 | static $db = []; |
300 | |
301 | if (count($db) === 0) |
302 | { |
303 | $db = [ |
304 | Syntax::new( |
305 | 'C', |
306 | ['.c', '.h', '.cpp'], |
307 | [ |
308 | 'continue', 'typedef', 'switch', 'return', 'static', 'while', 'break', 'struct', |
309 | 'union', 'class', 'else', 'enum', 'for', 'case', 'if', |
310 | ], |
311 | [ |
312 | '#include', 'unsigned', '#define', '#ifndef', 'double', 'signed', '#endif', |
313 | '#ifdef', 'float', '#error', '#undef', 'long', 'char', 'int', 'void', '#if', |
314 | ], |
315 | '//', |
316 | '/*', |
317 | '*/', |
318 | Syntax::HIGHLIGHT_NUMBERS | Syntax::HIGHLIGHT_STRINGS, |
319 | ), |
320 | Syntax::new( |
321 | 'CSS', |
322 | ['.css', '.less', '.sass', 'scss'], |
323 | [], |
324 | [], |
325 | '', |
326 | '/*', |
327 | '*/', |
328 | Syntax::HIGHLIGHT_NUMBERS | Syntax::HIGHLIGHT_STRINGS, |
329 | ), |
330 | Syntax::new( |
331 | 'JavaScript', |
332 | ['.js', '.jsx', '.ts', '.tsx', '.jsm', '.mjs', '.es'], |
333 | [ |
334 | 'instanceof', |
335 | 'continue', |
336 | 'debugger', |
337 | 'function', |
338 | 'default', |
339 | 'extends', |
340 | 'finally', |
341 | 'delete', |
342 | 'export', |
343 | 'import', |
344 | 'return', |
345 | 'switch', |
346 | 'typeof', |
347 | 'break', |
348 | 'catch', |
349 | 'class', |
350 | 'const', |
351 | 'super', |
352 | 'throw', |
353 | 'while', |
354 | 'yield', |
355 | 'case', |
356 | 'else', |
357 | 'this', |
358 | 'void', |
359 | 'with', |
360 | 'from', |
361 | 'for', |
362 | 'new', |
363 | 'try', |
364 | 'var', |
365 | 'do', |
366 | 'if', |
367 | 'in', |
368 | 'as', |
369 | ], |
370 | [ |
371 | '=>', 'Number', 'String', 'Object', 'Math', 'JSON', 'Boolean', |
372 | ], |
373 | '//', |
374 | '/*', |
375 | '*/', |
376 | Syntax::HIGHLIGHT_NUMBERS | Syntax::HIGHLIGHT_STRINGS, |
377 | ), |
378 | Syntax::new( |
379 | 'PHP', |
380 | ['.php', 'kilo'], |
381 | [ |
382 | '?php', '$this', '__halt_compiler', 'abstract', 'and', 'array', 'as', 'break', |
383 | 'callable', 'case', 'catch', 'class', 'clone', 'const', 'continue', 'declare', |
384 | 'default', 'die', 'do', 'echo', 'else', 'elseif', 'empty', 'enddeclare', 'endfor', |
385 | 'endforeach', 'endif', 'endswitch', 'endwhile', 'eval', 'exit', 'extends', |
386 | 'final', 'finally', 'for', 'foreach', 'function', 'global', 'goto', 'if', 'implements', |
387 | 'include', 'include_once', 'instanceof', 'insteadof', 'interface', 'isset', 'list', |
388 | 'namespace', 'new', 'or', 'print', 'private', 'protected', 'public', 'require', 'require_once', |
389 | 'return', 'static', 'switch', 'throw', 'trait', 'try', 'unset', 'use', 'var', 'while', 'xor', |
390 | 'yield', 'yield from', '__CLASS__', '__DIR__', '__FILE__', '__FUNCTION__', '__LINE__', |
391 | '__METHOD__', '__NAMESPACE__', '__TRAIT__', |
392 | ], |
393 | [ |
394 | 'int', 'float', 'bool', 'string', 'true', 'TRUE', 'false', 'FALSE', 'null', 'NULL', |
395 | 'void', 'iterable', 'object', 'strict_types' |
396 | ], |
397 | '//', |
398 | '/*', |
399 | '*/', |
400 | Syntax::HIGHLIGHT_NUMBERS | Syntax::HIGHLIGHT_STRINGS, |
401 | ), |
402 | Syntax::new( |
403 | 'Rust', |
404 | ['.rs'], |
405 | [ |
406 | 'continue', 'return', 'static', 'struct', 'unsafe', 'break', 'const', 'crate', |
407 | 'extern', 'match', 'super', 'trait', 'where', 'else', 'enum', 'false', 'impl', |
408 | 'loop', 'move', 'self', 'type', 'while', 'for', 'let', 'mod', 'pub', 'ref', 'true', |
409 | 'use', 'mut', 'as', 'fn', 'if', 'in', |
410 | ], |
411 | [ |
412 | 'DoubleEndedIterator', |
413 | 'ExactSizeIterator', |
414 | 'IntoIterator', |
415 | 'PartialOrd', |
416 | 'PartialEq', |
417 | 'Iterator', |
418 | 'ToString', |
419 | 'Default', |
420 | 'ToOwned', |
421 | 'Extend', |
422 | 'FnOnce', |
423 | 'Option', |
424 | 'String', |
425 | 'AsMut', |
426 | 'AsRef', |
427 | 'Clone', |
428 | 'Debug', |
429 | 'FnMut', |
430 | 'Sized', |
431 | 'Unpin', |
432 | 'array', |
433 | 'isize', |
434 | 'usize', |
435 | '&str', |
436 | 'Copy', |
437 | 'Drop', |
438 | 'From', |
439 | 'Into', |
440 | 'None', |
441 | 'Self', |
442 | 'Send', |
443 | 'Some', |
444 | 'Sync', |
445 | 'Sync', |
446 | 'bool', |
447 | 'char', |
448 | 'i128', |
449 | 'u128', |
450 | 'Box', |
451 | 'Err', |
452 | 'Ord', |
453 | 'Vec', |
454 | 'dyn', |
455 | 'f32', |
456 | 'f64', |
457 | 'i16', |
458 | 'i32', |
459 | 'i64', |
460 | 'str', |
461 | 'u16', |
462 | 'u32', |
463 | 'u64', |
464 | 'Eq', |
465 | 'Fn', |
466 | 'Ok', |
467 | 'i8', |
468 | 'u8', |
469 | ], |
470 | '//', |
471 | '/*', |
472 | '*/', |
473 | Syntax::HIGHLIGHT_NUMBERS | Syntax::HIGHLIGHT_STRINGS, |
474 | ), |
475 | ]; |
476 | } |
477 | |
478 | return $db; |
479 | } |