uncurses/terminal/raw.rs
1//! Raw mode and terminal state helpers.
2//!
3//! These free functions operate on terminal descriptors on Unix and console
4//! handles on Windows. [`make_raw_mode`] saves the current configuration,
5//! applies raw-mode settings immediately, and returns the previous [`State`].
6//! Pass that state to [`set_state`] with the same handles to restore it.
7//!
8//! ```text
9//! get_state() ── snapshot only ───────────────────────────────┐
10//! │
11//! make_raw_mode() ── returns previous State ── raw mode ── set_state()
12//! ```
13//!
14//! The input and output halves are tracked separately. Nothing requires them
15//! to be the same device: [`Terminal`](super::Terminal) is generic over its
16//! two descriptors, so a caller can pair two unrelated terminals, or pair a
17//! terminal with a pipe. Each half is read, rawified, and restored from its
18//! own attributes, and a half that is not a terminal is skipped.
19//!
20//! [`Terminal::make_raw`](super::Terminal::make_raw) and
21//! [`Terminal::restore`](super::Terminal::restore) wrap this same flow and keep
22//! one saved state inside the terminal handle.
23
24use std::io;
25
26#[cfg(unix)]
27use std::os::fd::{AsFd, AsRawFd};
28#[cfg(windows)]
29use std::os::windows::io::{AsHandle, AsRawHandle};
30#[cfg(windows)]
31use windows_sys::Win32::Foundation::HANDLE;
32
33/// Snapshot of a terminal's configuration.
34///
35/// On Unix this stores one `libc::termios` per half, because the input and
36/// output descriptors are not required to refer to the same terminal device. A
37/// half whose attributes cannot be read is stored as `None` and is skipped when
38/// the state is applied. On Windows it stores both input and output
39/// console-mode bitfields.
40///
41/// A `State` records each half separately, so it is bound to the pair it was
42/// read from. Apply it to that same `(input, output)` pair; applying it to a
43/// different pair writes each half's attributes to a descriptor they did not
44/// come from.
45///
46/// Use values returned by [`Terminal::get_state`](crate::terminal::Terminal::get_state)
47/// or [`Terminal::make_raw`](crate::terminal::Terminal::make_raw) with
48/// [`Terminal::set_state`](crate::terminal::Terminal::set_state) to restore a
49/// terminal to a previous configuration.
50///
51/// The fields are readable so a caller can inspect what was saved — a snapshot
52/// taken before raw mode is otherwise unrecoverable, since re-reading the
53/// descriptor afterwards reports the raw state rather than this one. Both
54/// platforms name the halves `input` and `output`, but what they hold is
55/// inherently platform-specific: a `termios` and a console mode are not the
56/// same concept, so there is no portable shape to offer instead and code that
57/// reads them is unix-only or Windows-only by nature. The unix types come from
58/// the [`libc`](crate::libc) re-export.
59///
60/// The struct is `non_exhaustive`: a `State` is only meaningful when it came
61/// from the terminal, so it is readable but not constructible from outside the
62/// crate.
63///
64/// # Examples
65///
66/// ```no_run
67/// # #[cfg(unix)] {
68/// use uncurses::libc;
69/// use uncurses::terminal::Terminal;
70///
71/// let mut term = Terminal::stdio();
72/// let saved = term.make_raw().unwrap();
73///
74/// // Reading the descriptor now would report the raw state, so this snapshot
75/// // is the only way to see what the terminal looked like beforehand.
76/// if let Some(input) = saved.input {
77/// println!("echo was on: {}", input.c_lflag & libc::ECHO != 0);
78/// }
79/// # }
80/// ```
81#[derive(Clone, Debug)]
82#[non_exhaustive]
83pub struct State {
84 #[cfg(unix)]
85 /// Saved attributes of the input descriptor, or `None` when they could not
86 /// be read — typically because it is not a terminal.
87 pub input: Option<libc::termios>,
88 #[cfg(unix)]
89 /// Saved attributes of the output descriptor, or `None` when they could not
90 /// be read — typically because it is not a terminal.
91 pub output: Option<libc::termios>,
92 #[cfg(windows)]
93 /// Saved console-mode bits of the input handle.
94 pub input: u32,
95 #[cfg(windows)]
96 /// Saved console-mode bits of the output handle.
97 pub output: u32,
98}
99
100#[cfg(windows)]
101unsafe impl Send for State {}
102#[cfg(windows)]
103unsafe impl Sync for State {}
104
105/// Read the current terminal state.
106///
107/// On Unix each descriptor is read independently, so a pair pointing at two
108/// different terminal devices is described in full and a half that is not a
109/// terminal is recorded as `None`. On Windows both input and output console
110/// modes are sampled.
111///
112/// # Parameters
113///
114/// * `input` — terminal input descriptor or handle.
115/// * `output` — terminal output descriptor or handle.
116///
117/// # Returns
118///
119/// A [`State`] describing the current terminal mode.
120///
121/// # Errors
122///
123/// Returns the OS error from the state query. On Unix, an error is returned
124/// only if the attributes of neither descriptor can be read — typically
125/// because neither is a terminal — and the output descriptor's error is the
126/// one reported.
127///
128/// # Panics
129///
130/// This function does not intentionally panic.
131#[cfg(unix)]
132pub fn get_state<I: AsFd, O: AsFd>(input: I, output: O) -> io::Result<State> {
133 use std::mem::MaybeUninit;
134
135 fn try_read(fd: i32) -> io::Result<libc::termios> {
136 let mut t = MaybeUninit::<libc::termios>::uninit();
137 if unsafe { libc::tcgetattr(fd, t.as_mut_ptr()) } != 0 {
138 return Err(io::Error::last_os_error());
139 }
140 Ok(unsafe { t.assume_init() })
141 }
142
143 let input = try_read(input.as_fd().as_raw_fd()).ok();
144 let output = try_read(output.as_fd().as_raw_fd());
145 match (input, output) {
146 // Neither half is a terminal. Report the output error, which is what
147 // this function surfaced when it fell back from input to output.
148 (None, Err(e)) => Err(e),
149 (input, output) => Ok(State {
150 input,
151 output: output.ok(),
152 }),
153 }
154}
155
156/// Apply `state` to the terminal immediately.
157///
158/// On Unix this uses `TCSANOW` and writes each half of `state` to its own
159/// descriptor; a half recorded as `None` is skipped. Both halves are attempted
160/// even if the first one fails, so a partial failure still restores as much as
161/// it can. On Windows both modes stored in [`State`] are written.
162///
163/// When the two descriptors refer to the same terminal, this applies identical
164/// attributes to that device twice. `tcsetattr` is idempotent, so the second
165/// call is a no-op.
166///
167/// # Parameters
168///
169/// * `input` — terminal input descriptor or handle.
170/// * `output` — terminal output descriptor or handle.
171/// * `state` — state previously returned by [`get_state`] or
172/// [`make_raw_mode`].
173///
174/// # Returns
175///
176/// `Ok(())` when every half recorded in `state` was applied.
177///
178/// # Errors
179///
180/// Returns the OS error from applying the state; the input descriptor's error
181/// is the one reported when both halves fail. A half recorded as `None` is not
182/// an error. If one half fails after the other succeeded, the successful half
183/// is not rolled back.
184///
185/// # Panics
186///
187/// This function does not intentionally panic.
188#[cfg(unix)]
189pub fn set_state<I: AsFd, O: AsFd>(input: I, output: O, state: &State) -> io::Result<()> {
190 fn try_write(fd: i32, termios: Option<&libc::termios>) -> io::Result<()> {
191 let Some(termios) = termios else {
192 return Ok(());
193 };
194 if unsafe { libc::tcsetattr(fd, libc::TCSANOW, termios) } != 0 {
195 return Err(io::Error::last_os_error());
196 }
197 Ok(())
198 }
199
200 let applied_input = try_write(input.as_fd().as_raw_fd(), state.input.as_ref());
201 let applied_output = try_write(output.as_fd().as_raw_fd(), state.output.as_ref());
202 applied_input.and(applied_output)
203}
204
205#[cfg(windows)]
206/// Read the current console modes.
207///
208/// Both input and output handles must support `GetConsoleMode`.
209///
210/// # Parameters
211///
212/// * `input` — console input handle.
213/// * `output` — console output handle.
214///
215/// # Returns
216///
217/// A [`State`] containing both console-mode bitfields.
218///
219/// # Errors
220///
221/// Returns the OS error if either console mode cannot be read.
222///
223/// # Panics
224///
225/// This function does not intentionally panic.
226pub fn get_state<I: AsHandle, O: AsHandle>(input: I, output: O) -> io::Result<State> {
227 use windows_sys::Win32::System::Console::GetConsoleMode;
228
229 let ih = input.as_handle().as_raw_handle() as HANDLE;
230 let oh = output.as_handle().as_raw_handle() as HANDLE;
231
232 let mut input_mode: u32 = 0;
233 if unsafe { GetConsoleMode(ih, &mut input_mode) } == 0 {
234 return Err(io::Error::last_os_error());
235 }
236 let mut output_mode: u32 = 0;
237 if unsafe { GetConsoleMode(oh, &mut output_mode) } == 0 {
238 return Err(io::Error::last_os_error());
239 }
240 Ok(State {
241 input: input_mode,
242 output: output_mode,
243 })
244}
245
246#[cfg(windows)]
247/// Apply console modes from `state`.
248///
249/// # Parameters
250///
251/// * `input` — console input handle.
252/// * `output` — console output handle.
253/// * `state` — console modes to apply.
254///
255/// # Returns
256///
257/// `Ok(())` when both input and output modes were applied.
258///
259/// # Errors
260///
261/// Returns the OS error if either `SetConsoleMode` call fails; the input
262/// handle's error is the one reported when both fail. Both handles are
263/// attempted even if the first one fails, so a partial failure still restores
264/// as much as it can. If one handle fails after the other succeeded, the
265/// successful one is not rolled back.
266///
267/// # Panics
268///
269/// This function does not intentionally panic.
270pub fn set_state<I: AsHandle, O: AsHandle>(input: I, output: O, state: &State) -> io::Result<()> {
271 use windows_sys::Win32::System::Console::SetConsoleMode;
272
273 fn try_write(handle: HANDLE, mode: u32) -> io::Result<()> {
274 if unsafe { SetConsoleMode(handle, mode) } == 0 {
275 return Err(io::Error::last_os_error());
276 }
277 Ok(())
278 }
279
280 let ih = input.as_handle().as_raw_handle() as HANDLE;
281 let oh = output.as_handle().as_raw_handle() as HANDLE;
282
283 let applied_input = try_write(ih, state.input);
284 let applied_output = try_write(oh, state.output);
285 applied_input.and(applied_output)
286}
287
288/// Place the terminal into raw mode.
289///
290/// On Unix this applies a `cfmakeraw(3)`-equivalent termios (`VMIN = 1`,
291/// `VTIME = 0`) using [`set_state`]. The flags match glibc's `cfmakeraw` on
292/// every platform, so raw mode behaves identically everywhere rather than
293/// following each libc's own variation. Each descriptor is rawified from its
294/// own saved attributes, so a pair pointing at two different terminal devices
295/// leaves both in raw mode. On Windows the input handle has cooked input flags
296/// and quick-edit cleared and virtual-terminal/window-input flags set; the
297/// output handle has processed output, virtual-terminal processing, and
298/// newline-auto-return disabling set.
299///
300/// # Parameters
301///
302/// * `input` — terminal input descriptor or handle.
303/// * `output` — terminal output descriptor or handle.
304///
305/// # Returns
306///
307/// The pre-call [`State`]. Pass it to [`set_state`] to restore.
308///
309/// # Errors
310///
311/// Returns any error from reading the current state or applying the raw state.
312/// When only one half of a split pair accepts the raw state, the pre-call state
313/// is written back to both halves before the error is returned, so a failure
314/// does not leave a descriptor raw with no way to restore it. That write-back
315/// is best-effort: if it fails too, the original error is still what surfaces.
316///
317/// # Panics
318///
319/// This function does not intentionally panic.
320#[cfg(unix)]
321pub fn make_raw_mode<I: AsFd, O: AsFd>(input: I, output: O) -> io::Result<State> {
322 fn rawify(mut t: libc::termios) -> libc::termios {
323 t.c_iflag &= !(libc::IGNBRK
324 | libc::BRKINT
325 | libc::PARMRK
326 | libc::ISTRIP
327 | libc::INLCR
328 | libc::IGNCR
329 | libc::ICRNL
330 | libc::IXON);
331 t.c_oflag &= !libc::OPOST;
332 t.c_lflag &= !(libc::ECHO | libc::ECHONL | libc::ICANON | libc::ISIG | libc::IEXTEN);
333 t.c_cflag &= !(libc::CSIZE | libc::PARENB);
334 t.c_cflag |= libc::CS8;
335 t.c_cc[libc::VMIN] = 1;
336 t.c_cc[libc::VTIME] = 0;
337 t
338 }
339
340 let original = get_state(&input, &output)?;
341 let raw = State {
342 input: original.input.map(rawify),
343 output: original.output.map(rawify),
344 };
345 if let Err(e) = set_state(&input, &output, &raw) {
346 // One half may have been rawified before the other failed, and the
347 // caller drops `original` along with the error. Put the pre-call state
348 // back so no descriptor is stranded in raw mode; the half that failed
349 // is simply rewritten its own unchanged attributes.
350 let _ = set_state(&input, &output, &original);
351 return Err(e);
352 }
353 Ok(original)
354}
355
356#[cfg(windows)]
357/// Place the console into raw mode.
358///
359/// The input handle has cooked input flags cleared and virtual-terminal/window
360/// input flags set; the output handle has virtual-terminal processing and
361/// newline-auto-return disabling set.
362///
363/// # Parameters
364///
365/// * `input` — console input handle.
366/// * `output` — console output handle.
367///
368/// # Returns
369///
370/// The pre-call [`State`]. Pass it to [`set_state`] to restore.
371///
372/// # Errors
373///
374/// Returns any error from reading the current modes or applying the raw modes.
375/// When only one handle accepts the raw modes, the pre-call modes are written
376/// back to both handles before the error is returned, so a failure does not
377/// leave a handle raw with no way to restore it. That write-back is
378/// best-effort: if it fails too, the original error is still what surfaces.
379///
380/// # Panics
381///
382/// This function does not intentionally panic.
383pub fn make_raw_mode<I: AsHandle, O: AsHandle>(input: I, output: O) -> io::Result<State> {
384 use windows_sys::Win32::System::Console::{
385 DISABLE_NEWLINE_AUTO_RETURN, ENABLE_ECHO_INPUT, ENABLE_EXTENDED_FLAGS, ENABLE_LINE_INPUT,
386 ENABLE_PROCESSED_INPUT, ENABLE_PROCESSED_OUTPUT, ENABLE_QUICK_EDIT_MODE,
387 ENABLE_VIRTUAL_TERMINAL_INPUT, ENABLE_VIRTUAL_TERMINAL_PROCESSING, ENABLE_WINDOW_INPUT,
388 };
389
390 let original = get_state(&input, &output)?;
391 // Clearing quick-edit while setting extended-flags is how the console API
392 // disables mouse selection, which would otherwise swallow mouse input.
393 let raw_input = (original.input
394 & !(ENABLE_ECHO_INPUT
395 | ENABLE_PROCESSED_INPUT
396 | ENABLE_LINE_INPUT
397 | ENABLE_QUICK_EDIT_MODE))
398 | ENABLE_VIRTUAL_TERMINAL_INPUT
399 | ENABLE_EXTENDED_FLAGS
400 | ENABLE_WINDOW_INPUT;
401 // Virtual-terminal processing requires processed output, so set it too.
402 let raw_output = original.output
403 | ENABLE_PROCESSED_OUTPUT
404 | ENABLE_VIRTUAL_TERMINAL_PROCESSING
405 | DISABLE_NEWLINE_AUTO_RETURN;
406
407 let raw = State {
408 input: raw_input,
409 output: raw_output,
410 };
411 if let Err(e) = set_state(&input, &output, &raw) {
412 // One handle may have been switched to raw mode before the other
413 // failed, and the caller drops `original` along with the error. Put the
414 // pre-call modes back so no handle is stranded in raw mode; the handle
415 // that failed is simply rewritten its own unchanged mode.
416 let _ = set_state(&input, &output, &original);
417 return Err(e);
418 }
419 Ok(original)
420}
421
422/// Return whether the descriptor is connected to a terminal.
423///
424/// # Parameters
425///
426/// * `fd` — descriptor to test.
427///
428/// # Returns
429///
430/// `true` when `fd` refers to a terminal.
431///
432/// # Errors and panics
433///
434/// This function does not fail or intentionally panic.
435#[cfg(unix)]
436pub fn is_terminal<F: AsFd>(fd: F) -> bool {
437 unsafe { libc::isatty(fd.as_fd().as_raw_fd()) != 0 }
438}
439
440#[cfg(windows)]
441/// Return whether the handle is connected to a console.
442///
443/// # Parameters
444///
445/// * `h` — handle to test.
446///
447/// # Returns
448///
449/// `true` when `h` supports `GetConsoleMode`.
450///
451/// # Errors and panics
452///
453/// This function does not fail or intentionally panic.
454pub fn is_terminal<H: AsHandle>(h: H) -> bool {
455 use windows_sys::Win32::System::Console::GetConsoleMode;
456 let handle = h.as_handle().as_raw_handle() as HANDLE;
457 let mut mode: u32 = 0;
458 unsafe { GetConsoleMode(handle, &mut mode) != 0 }
459}
460
461#[cfg(not(any(unix, windows)))]
462/// Return whether a handle is connected to a terminal.
463///
464/// On unsupported platforms this always returns `false`.
465pub fn is_terminal<T>(_: T) -> bool {
466 false
467}
468
469#[cfg(all(test, unix, not(target_os = "l4re")))]
470mod tests {
471 use super::*;
472 use crate::testutil::{ScriptedFd, attrs, open_pty_pair, opost, prime};
473 use std::fs::File;
474
475 /// Every flag group `rawify` modifies. Restoration is checked against these
476 /// alone: the tty driver owns the rest, and legitimately changes bits of its
477 /// own — re-enabling canonical mode sets `PENDIN`, for instance — which is
478 /// not something a restored state can or should undo.
479 const RAW_IFLAGS: libc::tcflag_t = libc::IGNBRK
480 | libc::BRKINT
481 | libc::PARMRK
482 | libc::ISTRIP
483 | libc::INLCR
484 | libc::IGNCR
485 | libc::ICRNL
486 | libc::IXON;
487 const RAW_LFLAGS: libc::tcflag_t =
488 libc::ECHO | libc::ECHONL | libc::ICANON | libc::ISIG | libc::IEXTEN;
489 const RAW_CFLAGS: libc::tcflag_t = libc::CSIZE | libc::PARENB;
490
491 /// Raw mode is more than `OPOST`, so check every flag group `rawify`
492 /// touches. A single sentinel bit would let most of it regress unnoticed.
493 fn assert_raw(f: &File, half: &str) {
494 let t = attrs(f);
495 assert_eq!(t.c_oflag & libc::OPOST, 0, "{half}: OPOST still set");
496 assert_eq!(
497 t.c_iflag & RAW_IFLAGS,
498 0,
499 "{half}: cooked input flags still set"
500 );
501 assert_eq!(
502 t.c_lflag & RAW_LFLAGS,
503 0,
504 "{half}: line-editing flags still set"
505 );
506 assert_eq!(
507 t.c_cflag & RAW_CFLAGS,
508 libc::CS8,
509 "{half}: not 8-bit, no parity"
510 );
511 assert_eq!(t.c_cc[libc::VMIN], 1, "{half}: VMIN");
512 assert_eq!(t.c_cc[libc::VTIME], 0, "{half}: VTIME");
513 }
514
515 fn assert_restored(before: &libc::termios, f: &File, half: &str) {
516 let now = attrs(f);
517 assert_eq!(
518 now.c_iflag & RAW_IFLAGS,
519 before.c_iflag & RAW_IFLAGS,
520 "{half}: input flags not restored"
521 );
522 assert_eq!(
523 now.c_oflag & libc::OPOST,
524 before.c_oflag & libc::OPOST,
525 "{half}: OPOST not restored"
526 );
527 assert_eq!(
528 now.c_lflag & RAW_LFLAGS,
529 before.c_lflag & RAW_LFLAGS,
530 "{half}: line-editing flags not restored"
531 );
532 assert_eq!(
533 now.c_cflag & RAW_CFLAGS,
534 before.c_cflag & RAW_CFLAGS,
535 "{half}: control flags not restored"
536 );
537 assert_eq!(
538 now.c_cc[libc::VMIN],
539 before.c_cc[libc::VMIN],
540 "{half}: VMIN not restored"
541 );
542 assert_eq!(
543 now.c_cc[libc::VTIME],
544 before.c_cc[libc::VTIME],
545 "{half}: VTIME not restored"
546 );
547 }
548
549 /// A pipe read end is a descriptor that is definitely not a terminal, and
550 /// unlike `/dev/null` it needs nothing from the filesystem. The write end is
551 /// dropped: nothing is ever read, and `tcgetattr`/`tcsetattr` reject it
552 /// either way.
553 fn not_a_terminal() -> std::io::PipeReader {
554 std::io::pipe().expect("pipe").0
555 }
556
557 /// The two halves are independent devices, so their attributes must be
558 /// sampled independently rather than one standing in for the other.
559 #[test]
560 fn get_state_reads_each_half_independently() {
561 let (Some((_ma, a)), Some((_mb, b))) = (open_pty_pair(), open_pty_pair()) else {
562 return;
563 };
564 prime(&a, true);
565 prime(&b, false);
566
567 let state = get_state(&a, &b).expect("both halves are terminals");
568 assert!(
569 state.input.expect("input half read").c_oflag & libc::OPOST != 0,
570 "input half must report the input device's flags"
571 );
572 assert!(
573 state.output.expect("output half read").c_oflag & libc::OPOST == 0,
574 "output half must report the output device's flags, not the input's"
575 );
576 }
577
578 /// Raw mode has to reach the device frames are written to. When the halves
579 /// are two different terminals, configuring only the input one leaves
580 /// `OPOST` set on the output, and the terminal keeps post-processing
581 /// everything the renderer emits.
582 #[test]
583 fn make_raw_and_restore_cover_both_terminals() {
584 let (Some((_ma, a)), Some((_mb, b))) = (open_pty_pair(), open_pty_pair()) else {
585 return;
586 };
587 prime(&a, true);
588 prime(&b, true);
589 let (before_a, before_b) = (attrs(&a), attrs(&b));
590
591 let original = make_raw_mode(&a, &b).expect("raw mode");
592 assert_raw(&a, "input half");
593 assert_raw(&b, "output half");
594
595 set_state(&a, &b, &original).expect("restore");
596 assert_restored(&before_a, &a, "input half");
597 assert_restored(&before_b, &b, "output half");
598 }
599
600 /// Pairing a terminal with a pipe is normal, and the terminal half must
601 /// still be configured whichever side it is on.
602 #[test]
603 fn non_terminal_half_is_skipped() {
604 let Some((_master, tty)) = open_pty_pair() else {
605 return;
606 };
607 prime(&tty, true);
608 let before = attrs(&tty);
609
610 let state = make_raw_mode(not_a_terminal(), &tty).expect("output half is a terminal");
611 assert!(state.input.is_none(), "a pipe is not a terminal");
612 assert!(state.output.is_some(), "the pty half must be recorded");
613 assert_raw(&tty, "output half");
614 set_state(not_a_terminal(), &tty, &state).expect("restore");
615 assert_restored(&before, &tty, "output half");
616
617 // ...and the same with the halves swapped.
618 let state = make_raw_mode(&tty, not_a_terminal()).expect("input half is a terminal");
619 assert!(state.input.is_some(), "the pty half must be recorded");
620 assert!(state.output.is_none(), "a pipe is not a terminal");
621 assert_raw(&tty, "input half");
622 set_state(&tty, not_a_terminal(), &state).expect("restore");
623 assert_restored(&before, &tty, "input half");
624 }
625
626 /// A half that was recorded as a terminal but rejects the write is a real
627 /// failure and must be reported — while the other half is still attempted,
628 /// so one bad descriptor cannot stop the other from being configured.
629 #[test]
630 fn a_half_that_fails_is_reported_and_the_other_is_still_applied() {
631 let Some((_master, tty)) = open_pty_pair() else {
632 return;
633 };
634 prime(&tty, true);
635
636 // Reading the same pty for both halves gives a state whose halves are
637 // both `Some`, so both are attempted below even though only one of the
638 // descriptors can accept them.
639 let mut target = get_state(&tty, &tty).expect("pty is a terminal");
640 let clear_opost = |mut t: libc::termios| {
641 t.c_oflag &= !libc::OPOST;
642 t
643 };
644 target.input = target.input.map(clear_opost);
645 target.output = target.output.map(clear_opost);
646
647 assert!(
648 set_state(not_a_terminal(), &tty, &target).is_err(),
649 "a failing input half must be reported"
650 );
651 assert!(
652 !opost(&tty),
653 "the output half must be attempted even after the input half failed"
654 );
655
656 prime(&tty, true);
657 assert!(
658 set_state(&tty, not_a_terminal(), &target).is_err(),
659 "a failing output half must be reported"
660 );
661 assert!(!opost(&tty), "the input half must still be applied");
662 }
663
664 #[test]
665 fn get_state_errors_when_neither_half_is_a_terminal() {
666 assert!(get_state(not_a_terminal(), not_a_terminal()).is_err());
667 }
668
669 /// A half can pass `tcgetattr` and then fail `tcsetattr` -- a pty whose
670 /// master closed mid-session, say. The other half is already raw by then,
671 /// and `make_raw_mode` consumes the only copy of the pre-call state on its
672 /// way out, so without the write-back that half is raw forever.
673 #[test]
674 fn a_failed_half_does_not_strand_the_half_that_succeeded() {
675 let (Some((_ma, a)), Some((_mb, b))) = (open_pty_pair(), open_pty_pair()) else {
676 return;
677 };
678 prime(&a, true);
679 prime(&b, true);
680 let (before_a, before_b) = (attrs(&a), attrs(&b));
681
682 // `get_state` sees a terminal, the raw `tcsetattr` does not, and the
683 // write-back sees the terminal again.
684 let pipe = not_a_terminal();
685 let output = ScriptedFd::new(&[&b as &dyn AsFd, &pipe, &b]);
686
687 let Err(err) = make_raw_mode(&a, &output) else {
688 panic!("a pipe cannot be rawified, so make_raw_mode must fail");
689 };
690 // Most systems report a non-terminal descriptor as `ENOTTY`; Solaris
691 // reports `EINVAL`. Either way the error has to come from the write to
692 // the pipe rather than from anywhere else.
693 assert!(
694 matches!(err.raw_os_error(), Some(libc::ENOTTY | libc::EINVAL)),
695 "expected a not-a-terminal error, got {err}"
696 );
697 assert_restored(&before_a, &a, "input half");
698 assert_restored(&before_b, &b, "output half");
699 }
700}