Landin library reference discuss source

platform/posix/terminals.ldn

1--  Terminals: whether a descriptor is one, its size in characters, its
2--  settings and raw mode, and pseudo-terminals.  A terminal's settings are
3--  each system's own record, the kernel's `struct termios` on Linux and the
4--  C library's on Darwin and FreeBSD, which this module reads, changes and
5--  writes back whole, so a program restores exactly what it found.
6
7import core/text
8import platform/libc
9import platform/linux
10
11--- A terminal's size: rows and columns of characters, and its width and
12--- height in pixels, which most terminals leave zero.
13public window_size: type = struct
14    rows: u16
15    columns: u16
16    width: u16
17    height: u16
18end window_size
19
20--- Whether `opened` is a terminal, POSIX's `isatty`: false for a file, a
21--- pipe, a socket, and a descriptor that is not open.
22public link(symbol: "_landin_posix_is_terminal") is_terminal:
23    (opened: descriptor) -> (yes: bool) =
24    yes = system_is_terminal(opened)
25end is_terminal
26
27--- The size of the terminal `opened`, `TIOCGWINSZ`: `enotty` for anything
28--- but a terminal.  A terminal nothing has sized is zero by zero.
29public link(symbol: "_landin_posix_window_size_of") window_size_of:
30    (opened: descriptor) -> (size: window_size) ! errno =
31    size = try system_window_size(opened)
32end window_size_of
33
34--- Set the size of the terminal `opened`, `TIOCSWINSZ`, as a terminal
35--- emulator does for the program it runs, which the system tells of the
36--- change with SIGWINCH: `enotty` for anything but a terminal.
37public link(symbol: "_landin_posix_set_window_size") set_window_size:
38    (opened: descriptor, size: window_size) -> none ! errno =
39    try system_set_window_size(opened, size)
40end set_window_size
41
42fixed if compiler.os == linux then
43    terminal_settings: type = linux.terminal_record
44else
45    terminal_settings: type = libc.terminal_record
46end if
47
48--- A terminal's settings, POSIX's `struct termios`, in the system's own
49--- record: read one with `terminal_mode_of`, make a raw one from it with
50--- `raw_mode`, and give either back with `set_terminal_mode`.  Its field
51--- belongs to this module.
52public terminal_mode: type = struct
53    settings: terminal_settings
54end terminal_mode
55
56--- Change a terminal's settings at once, `TCSANOW`.
57public immediately: atom
58--- Change them once everything written has been sent, `TCSADRAIN`.
59public after_output: atom
60--- Change them once everything written has been sent, discarding what was
61--- typed and not yet read, `TCSAFLUSH`: how a program enters raw mode and
62--- leaves it.
63public discarding_input: atom
64--- When `set_terminal_mode` changes the settings.
65public mode_change: type = immediately | after_output | discarding_input
66
67--- The settings of the terminal `opened`, POSIX's `tcgetattr`: `enotty`
68--- for anything but a terminal.
69public link(symbol: "_landin_posix_terminal_mode_of") terminal_mode_of:
70    (opened: descriptor) -> (mode: terminal_mode) ! errno =
71    mode = try system_terminal_mode(opened)
72end terminal_mode_of
73
74--- Give the terminal `opened` the settings `mode`, when `change` says,
75--- POSIX's `tcsetattr`.  The system may take some of them and not others,
76--- which reading them back shows.  A program that changes a terminal
77--- gives it back the settings it found, on every way out: there are no
78--- destructors, so `defer` is the idiom, and a program ended by a signal
79--- leaves the terminal as it was changed.
80public link(symbol: "_landin_posix_set_terminal_mode") set_terminal_mode:
81    (opened: descriptor, mode: terminal_mode, change: mode_change)
82    -> none ! errno =
83    try system_set_terminal_mode(opened, mode, change)
84end set_terminal_mode
85
86--- `mode` made raw, as C's `cfmakeraw` makes it: input read a byte at a
87--- time as it is typed, with no echo, no line editing, no signal from a
88--- control character, no translation of carriage returns and eight-bit
89--- characters; output passed through unprocessed, so a newline moves down
90--- without returning; and a read waiting for at least one byte.
91public link(symbol: "_landin_posix_raw_mode") raw_mode:
92    (mode: terminal_mode) -> (raw: terminal_mode) =
93    raw = system_raw_mode(mode)
94end raw_mode
95
96--- Whether `mode` reads a line at a time with echo, a terminal's ordinary
97--- settings, `ICANON` and `ECHO`: false for one `raw_mode` made.
98public link(symbol: "_landin_posix_reads_lines") reads_lines:
99    (mode: terminal_mode) -> (yes: bool) =
100    yes = system_reads_lines(mode)
101end reads_lines
102
103--- Whether two settings are the same in every mode and character, as a
104--- program restoring a terminal checks it.
105public link(symbol: "_landin_posix_same_terminal_mode") same_terminal_mode:
106    (left: terminal_mode, right: terminal_mode) -> (same: bool) =
107    same = system_same_mode(left, right)
108end same_terminal_mode
109
110--- A pseudo-terminal's two descriptors: the `controller`, which a
111--- terminal emulator reads what the program writes from and writes what
112--- is typed to, and the `terminal`, which the program uses as a terminal
113--- like any other.
114public pseudo_terminal: type = struct
115    controller: descriptor
116    terminal: descriptor
117end pseudo_terminal
118
119--- Open a new pseudo-terminal, POSIX's `posix_openpt`, `grantpt`,
120--- `unlockpt` and the terminal's path, both ends closed on exec and
121--- neither made the process's controlling terminal: `enoent` or `enodev`
122--- where the system has none, as a container without /dev/pts may not.
123--- On Linux it is /dev/ptmx and `TIOCGPTPEER`, Linux 4.13's; on Darwin the
124--- controller is marked closed on exec just after it is opened.  Close
125--- both when done.
126public link(symbol: "_landin_posix_open_pseudo_terminal") open_pseudo_terminal:
127    () -> (pair: pseudo_terminal) ! errno =
128    pair = try system_open_pseudo_terminal()
129end open_pseudo_terminal
130
131fixed if compiler.os == linux then
132    system_is_terminal: (opened: descriptor) -> (yes: bool) =
133        mut record: linux.terminal_record = zeroed
134        yes = linux.ioctl(i32(opened), linux.tcgets,
135                          i64(usize(addr record))) == 0
136    end system_is_terminal
137
138    system_window_size: (opened: descriptor) -> (size: window_size) ! errno =
139        mut record: linux.window_record = zeroed
140        result: i32 = linux.ioctl(i32(opened), linux.tiocgwinsz,
141                                  i64(usize(addr record)))
142        fail errno_of(-result) when result < 0
143        size = (rows: record.rows, columns: record.columns,
144                width: record.width, height: record.height)
145    end system_window_size
146
147    system_set_window_size: (opened: descriptor, size: window_size)
148                            -> none ! errno =
149        record: linux.window_record = (rows: size.rows, columns: size.columns,
150                                       width: size.width,
151                                       height: size.height)
152        result: i32 = linux.ioctl(i32(opened), linux.tiocswinsz,
153                                  i64(usize(addr record)))
154        fail errno_of(-result) when result < 0
155    end system_set_window_size
156
157    system_terminal_mode: (opened: descriptor) -> (mode: terminal_mode)
158                          ! errno =
159        mode = (settings: zeroed)
160        result: i32 = linux.ioctl(i32(opened), linux.tcgets,
161                                  i64(usize(addr mode.settings)))
162        fail errno_of(-result) when result < 0
163    end system_terminal_mode
164
165    system_set_terminal_mode: (opened: descriptor, mode: terminal_mode,
166                               change: mode_change) -> none ! errno =
167        request: u64 = match change
168            immediately: linux.tcsets
169            after_output: linux.tcsetsw
170            discarding_input: linux.tcsetsf
171        end match
172        result: i32 = linux.ioctl(i32(opened), request,
173                                  i64(usize(addr mode.settings)))
174        fail errno_of(-result) when result < 0
175    end system_set_terminal_mode
176
177    system_raw_mode: (mode: terminal_mode) -> (raw: terminal_mode) =
178        input: u32 = linux.ignbrk | linux.brkint | linux.parmrk | linux.istrip
179                     | linux.inlcr | linux.igncr | linux.icrnl | linux.ixon
180        local: u32 = linux.echo | linux.echonl | linux.icanon | linux.isig
181                     | linux.iexten
182        control: u32 = linux.csize | linux.parenb
183        raw = mode
184        raw.settings.input_modes = raw.settings.input_modes & ~input
185        raw.settings.output_modes = raw.settings.output_modes & ~linux.opost
186        raw.settings.local_modes = raw.settings.local_modes & ~local
187        raw.settings.control_modes = (raw.settings.control_modes & ~control)
188                                      | linux.cs8
189        raw.settings.characters[linux.vmin] = 1
190        raw.settings.characters[linux.vtime] = 0
191    end system_raw_mode
192
193    system_reads_lines: (mode: terminal_mode) -> (yes: bool) =
194        wanted: u32 = linux.icanon | linux.echo
195        yes = (mode.settings.local_modes & wanted) == wanted
196    end system_reads_lines
197
198    system_same_mode: (left: terminal_mode, right: terminal_mode)
199                      -> (same: bool) =
200        same = left.settings.input_modes == right.settings.input_modes
201               and left.settings.output_modes == right.settings.output_modes
202               and left.settings.control_modes == right.settings.control_modes
203               and left.settings.local_modes == right.settings.local_modes
204               and left.settings.line == right.settings.line
205        for index in usize(0)..<19 do
206            if left.settings.characters[index]
207              <> right.settings.characters[index] then
208                same = false
209            end if
210        end for
211    end system_same_mode
212
213    ptmx: []u8 = "/dev/ptmx"
214
215    system_open_pseudo_terminal: () -> (pair: pseudo_terminal) ! errno =
216        controller: descriptor =
217            try open_at(working_directory, text.host_over(ptmx),
218                        (access: read_write, create: false, exclusive: false,
219                         truncate: false, append: false, directory: false),
220                        0)
221        unlocked: i32 = 0
222        result: i32 = linux.ioctl(i32(controller), linux.tiocsptlck,
223                                  i64(usize(addr unlocked)))
224        if result < 0 then
225            _ = linux.close(i32(controller))
226            fail errno_of(-result)
227        end if
228        peer: i32 = linux.ioctl(i32(controller), linux.tiocgptpeer,
229                                i64(linux.o_rdwr | linux.o_noctty
230                                    | linux.o_cloexec))
231        if peer >= 0 then
232            pair = (controller: controller, terminal: descriptor(peer))
233            return
234        end if
235        --  A kernel before 4.13, or an emulator that does not pass the
236        --  request on, opens the terminal by its number's path instead.
237        terminal: descriptor = named_terminal(controller) else (problem)
238            _ = linux.close(i32(controller))
239            fail problem
240        end
241        pair = (controller: controller, terminal: terminal)
242    end system_open_pseudo_terminal
243
244    --  Open the terminal of CONTROLLER at /dev/pts/N.
245    named_terminal: (controller: descriptor) -> (terminal: descriptor)
246                    ! errno =
247        mut number: i32 = 0
248        result: i32 = linux.ioctl(i32(controller), linux.tiocgptn,
249                                  i64(usize(addr number)))
250        fail errno_of(-result) when result < 0
251        fail unlisted when number < 0
252        mut room: [32]u8 = zeroed
253        mut path := text.over(room[0..<32])
254        text.append(path, "/dev/pts/") else (problem)
255            _ = problem
256            fail enametoolong
257        end
258        text.append_u32(path, u32(number)) else (problem)
259            _ = problem
260            fail enametoolong
261        end
262        terminal = try open_at(working_directory,
263                               text.host_over(text.used(path)),
264                               (access: read_write, create: false,
265                                exclusive: false, truncate: false,
266                                append: false, directory: false), 0)
267    end named_terminal
268else
269    --  The errno the calling thread's last failing call left, as an atom.
270    terminal_failure: () -> (problem: errno) =
271        cell: ptr mut i32 = libc.errno_cell()
272        problem = errno_of(cell.val)
273    end terminal_failure
274
275    --  Why a terminal's request on OPENED failed, as `terminal_failure`,
276    --  except that a descriptor open on something else is `enotty`, as
277    --  Darwin's `isatty` reports it: Darwin's /dev/null and other devices
278    --  refuse with ENODEV, and its sockets with EOPNOTSUPP.
279    refusal: (opened: descriptor) -> (problem: errno) =
280        problem = terminal_failure()
281        return when problem == ebadf
282        if libc.isatty(i32(opened)) <> 1 then problem = enotty end if
283    end refusal
284
285    system_is_terminal: (opened: descriptor) -> (yes: bool) =
286        yes = libc.isatty(i32(opened)) == 1
287    end system_is_terminal
288
289    system_window_size: (opened: descriptor) -> (size: window_size) ! errno =
290        mut record: libc.window_record = zeroed
291        fail refusal(opened) when libc.ioctl(i32(opened), libc.tiocgwinsz,
292                                             addr record) <> 0
293        size = (rows: record.rows, columns: record.columns,
294                width: record.width, height: record.height)
295    end system_window_size
296
297    system_set_window_size: (opened: descriptor, size: window_size)
298                            -> none ! errno =
299        record: libc.window_record = (rows: size.rows, columns: size.columns,
300                                      width: size.width, height: size.height)
301        fail refusal(opened) when libc.ioctl(i32(opened), libc.tiocswinsz,
302                                             addr record) <> 0
303    end system_set_window_size
304
305    system_terminal_mode: (opened: descriptor) -> (mode: terminal_mode)
306                          ! errno =
307        mode = (settings: zeroed)
308        fail refusal(opened) when libc.tcgetattr(i32(opened),
309                                                 addr mode.settings) <> 0
310    end system_terminal_mode
311
312    system_set_terminal_mode: (opened: descriptor, mode: terminal_mode,
313                               change: mode_change) -> none ! errno =
314        timing: i32 = match change
315            immediately: libc.tcsanow
316            after_output: libc.tcsadrain
317            discarding_input: libc.tcsaflush
318        end match
319        fail refusal(opened) when libc.tcsetattr(i32(opened), timing,
320                                                 addr mode.settings) <> 0
321    end system_set_terminal_mode
322
323    fixed if compiler.os == darwin then
324        --  Darwin's modes are C `long`s.
325        flags: type = u64
326    else
327        flags: type = u32
328    end if
329
330    system_raw_mode: (mode: terminal_mode) -> (raw: terminal_mode) =
331        input: flags = flags(libc.ignbrk | libc.brkint | libc.parmrk
332                             | libc.istrip | libc.inlcr | libc.igncr
333                             | libc.icrnl | libc.ixon)
334        local: flags = flags(libc.echo | libc.echonl | libc.icanon
335                             | libc.isig | libc.iexten)
336        control: flags = flags(libc.csize | libc.parenb)
337        raw = mode
338        raw.settings.input_modes = raw.settings.input_modes & ~input
339        raw.settings.output_modes = raw.settings.output_modes
340                                    & ~flags(libc.opost)
341        raw.settings.local_modes = raw.settings.local_modes & ~local
342        raw.settings.control_modes = (raw.settings.control_modes & ~control)
343                                      | flags(libc.cs8)
344        raw.settings.characters[libc.vmin] = 1
345        raw.settings.characters[libc.vtime] = 0
346    end system_raw_mode
347
348    system_reads_lines: (mode: terminal_mode) -> (yes: bool) =
349        wanted: flags = flags(libc.icanon | libc.echo)
350        yes = (mode.settings.local_modes & wanted) == wanted
351    end system_reads_lines
352
353    system_same_mode: (left: terminal_mode, right: terminal_mode)
354                      -> (same: bool) =
355        same = left.settings.input_modes == right.settings.input_modes
356               and left.settings.output_modes == right.settings.output_modes
357               and left.settings.control_modes == right.settings.control_modes
358               and left.settings.local_modes == right.settings.local_modes
359               and left.settings.input_speed == right.settings.input_speed
360               and left.settings.output_speed == right.settings.output_speed
361        for index in usize(0)..<20 do
362            if left.settings.characters[index]
363              <> right.settings.characters[index] then
364                same = false
365            end if
366        end for
367    end system_same_mode
368
369    system_open_pseudo_terminal: () -> (pair: pseudo_terminal) ! errno =
370        made: i32 = libc.posix_openpt(libc.o_rdwr | libc.o_noctty)
371        fail terminal_failure() when made < 0
372        controller: descriptor = descriptor(made)
373        undo forgotten(controller)
374        if libc.fcntl(made, libc.f_setfd, libc.fd_cloexec) <> 0
375          or libc.grantpt(made) <> 0 or libc.unlockpt(made) <> 0 then
376            fail terminal_failure()
377        end if
378        mut name: path_room = zeroed
379        status: i32 = libc.ptsname_r(made, addr name[0], lenof name)
380        if status <> 0 then
381            fail errno_of(status) when status > 0
382            fail terminal_failure()
383        end if
384        mut length: usize = 0
385        while length < lenof name and name[length] <> 0 do
386            inc length
387        end while
388        terminal: descriptor =
389            try open_at(working_directory, text.host_over(name[0..<length]),
390                        (access: read_write, create: false, exclusive: false,
391                         truncate: false, append: false, directory: false),
392                        0)
393        pair = (controller: controller, terminal: terminal)
394    end system_open_pseudo_terminal
395
396    --  Close a descriptor just opened when what follows it fails.
397    forgotten: (opened: descriptor) -> none =
398        close(opened) else (problem)
399            _ = problem
400        end
401    end forgotten
402end if