Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
n/a
0 / 0
93.75% covered (success)
93.75%
15 / 16
CRAP
95.92% covered (success)
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% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
3 / 3
Aviat\Kilo\is_ascii
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
3 / 3
Aviat\Kilo\is_ctrl
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
2 / 2
Aviat\Kilo\is_digit
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
2 / 2
Aviat\Kilo\is_space
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
8 / 8
Aviat\Kilo\get_ffi
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 4
Aviat\Kilo\is_separator
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
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% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
5 / 5
Aviat\Kilo\str_contains
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
5 / 5
Aviat\Kilo\syntax_to_color
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
12 / 12
Aviat\Kilo\tabs_to_spaces
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
Aviat\Kilo\get_file_syntax_map
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
49 / 49
1<?php declare(strict_types=1);
2
3namespace Aviat\Kilo;
4
5use FFI;
6
7use 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 */
15function 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 */
30function 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 */
68function 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 */
87function 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 */
103function 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 */
115function 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 */
127function 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 */
149function 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 */
167function 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 */
186function 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 */
203function 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 */
223function 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 */
243function 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 */
261function 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 */
287function 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 */
297function 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}