Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 38
Termios
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 5
240
0.00% covered (danger)
0.00%
0 / 38
 __construct
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 9
 enableRawMode
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 16
 disableRawMode
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 5
 getWindowSize
n/a
0 / 0
5
n/a
0 / 0
 getInstance
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 4
 ffi
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 4
1<?php declare(strict_types=1);
2
3namespace Aviat\Kilo;
4
5use FFI;
6use FFI\CData;
7
8use Aviat\Kilo\Enum\C;
9
10/**
11 * An implicit singleton wrapper around terminal settings to simplify enabling/disabling raw mode
12 */
13class Termios {
14    private CData $originalTermios;
15
16    private function __construct()
17    {
18        $ffi = self::ffi();
19        $termios = $ffi->new('struct termios');
20        if ($termios === NULL)
21        {
22            throw new TermiosException('Failed to create termios struct');
23        }
24
25        $termiosAddr = FFI::addr($termios);
26        $res = $ffi->tcgetattr(C::STDIN_FILENO, $termiosAddr);
27
28        if ($res === -1)
29        {
30            throw new TermiosException('Failed to get existing terminal settings');
31        }
32
33        $this->originalTermios = $termios;
34    }
35
36    /**
37     * Put the current terminal into raw input mode
38     *
39     * Returns TRUE if successful. Will return NULL if run more than once, as
40     * raw mode is pretty binary...there's no point in reapplying raw mode!
41     *
42     * @return bool|null
43     */
44    public static function enableRawMode(): ?bool
45    {
46        static $run = FALSE;
47
48        // Don't run this more than once!
49        if ($run === TRUE)
50        {
51            return NULL;
52        }
53
54        $run = TRUE;
55
56        $instance = self::getInstance();
57
58        // Make sure to restore normal mode on exit/die/crash
59        register_shutdown_function([static::class, 'disableRawMode']);
60
61        $termios = clone $instance->originalTermios;
62        $termios->c_iflag &= ~(C::BRKINT | C::ICRNL | C::INPCK | C::ISTRIP | C::IXON);
63        $termios->c_oflag = 0; // &= ~(C::OPOST);
64        $termios->c_cflag |= (C::CS8);
65        $termios->c_lflag &= ~( C::ECHO | C::ICANON | C::IEXTEN | C::ISIG );
66        $termios->c_cc[C::VMIN] = 0;
67        $termios->c_cc[C::VTIME] = 1;
68
69        // Turn on raw mode
70        $res = self::ffi()
71            ->tcsetattr(C::STDIN_FILENO, C::TCSAFLUSH, FFI::addr($termios));
72
73        return $res !== -1;
74    }
75
76    /**
77     * Restores terminal settings that were changed when going into raw mode.
78     *
79     * Returns TRUE if settings are applied successfully. If raw mode was not
80     * enabled, this will output a line of escape codes and a new line.
81     *
82     * @return bool
83     */
84    public static function disableRawMode(): bool
85    {
86        $instance = self::getInstance();
87
88        // Cleanup
89        Terminal::clear();
90        Terminal::write("\n"); // New line, please
91
92        $res = self::ffi()->tcsetattr(C::STDIN_FILENO, C::TCSAFLUSH, FFI::addr($instance->originalTermios));
93
94        return $res !== -1;
95    }
96
97    /**
98     * Get the size of the current terminal window
99     *
100     * @codeCoverageIgnore
101     * @return array|null
102     */
103    public static function getWindowSize(): ?array
104    {
105        // First, try to get the answer from ioctl
106        $ffi = self::ffi();
107        $ws = $ffi->new('struct winsize');
108        if ($ws !== NULL)
109        {
110            $res = $ffi->ioctl(C::STDOUT_FILENO, C::TIOCGWINSZ, FFI::addr($ws));
111            if ($res === 0 && $ws->ws_col !== 0 && $ws->ws_row !== 0)
112            {
113                return [$ws->ws_row, $ws->ws_col];
114            }
115        }
116
117        return null;
118    }
119
120    private static function getInstance(): self
121    {
122        static $instance;
123
124        if ($instance === NULL)
125        {
126            $instance = new self();
127        }
128
129        return $instance;
130    }
131
132    /**
133     * A 'singleton' function to replace a global variable
134     *
135     * @return FFI
136     */
137    private static function ffi(): FFI
138    {
139        static $ffi;
140
141        if ($ffi === NULL)
142        {
143            $ffi = FFI::load(__DIR__ . '/ffi.h');
144        }
145
146        return $ffi;
147    }
148}