Skip to main content

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}