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