liso 1.3.2

Line Input with Simultaneous Output: input lines are editable, output lines are never scrambled, and all of it thread safe.
Documentation
//! This module contains utilities required for proper functioning on Windows.

use std::{
    sync::atomic::{AtomicBool, Ordering},
    thread::JoinHandle,
};

use libc::isatty;
use windows::{
    core::BOOL,
    Win32::{
        System::Console::{
            FlushConsoleInputBuffer, GetConsoleMode, GetStdHandle,
            SetConsoleMode, WriteConsoleInputW, CONSOLE_MODE,
            ENABLE_LVB_GRID_WORLDWIDE, ENABLE_PROCESSED_OUTPUT,
            ENABLE_QUICK_EDIT_MODE, ENABLE_VIRTUAL_TERMINAL_INPUT,
            ENABLE_VIRTUAL_TERMINAL_PROCESSING, ENABLE_WINDOW_INPUT,
            INPUT_RECORD, INPUT_RECORD_0, KEY_EVENT, KEY_EVENT_RECORD,
            KEY_EVENT_RECORD_0, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE,
        },
        UI::Input::KeyboardAndMouse::VK_RETURN,
    },
};

#[cfg(debug_assertions)]
static IS_RAW: AtomicBool = AtomicBool::new(false);

static STDIN_BEING_INTERRUPTED: AtomicBool = AtomicBool::new(false);

/// Wraps a JoinHandle on a thread that will be reading from stdin. Creates a
/// flimsy way for us to interrupt it, by taking away its stdin file descriptor
/// and sending it a "special APC". Very icky.
pub(crate) struct InterruptibleStdinThread {
    join_handle: Option<JoinHandle<()>>,
}

impl InterruptibleStdinThread {
    pub fn new(join_handle: JoinHandle<()>) -> InterruptibleStdinThread {
        InterruptibleStdinThread {
            join_handle: Some(join_handle),
        }
    }
    pub fn interrupt(&mut self) {
        let Some(join_handle) = self.join_handle.take() else {
            return;
        };
        if join_handle.is_finished() {
            return;
        }
        unsafe {
            STDIN_BEING_INTERRUPTED.store(true, Ordering::SeqCst);
            // We can't use the fd-0 switcheroo we use on UNIX to do this on
            // Windows, because Windows Rust uses NtReadFile to read the stdin
            // handle, not libc. Fortunately, we have the option to simulate
            // input instead.
            let buf = [
                INPUT_RECORD {
                    EventType: KEY_EVENT as u16,
                    Event: INPUT_RECORD_0 {
                        KeyEvent: KEY_EVENT_RECORD {
                            bKeyDown: BOOL(1),
                            wRepeatCount: 1,
                            wVirtualKeyCode: VK_RETURN.0,
                            wVirtualScanCode: 0x0A,
                            uChar: KEY_EVENT_RECORD_0 { UnicodeChar: 0x1B },
                            dwControlKeyState: 0,
                        },
                    },
                },
                INPUT_RECORD {
                    EventType: KEY_EVENT as u16,
                    Event: INPUT_RECORD_0 {
                        KeyEvent: KEY_EVENT_RECORD {
                            bKeyDown: BOOL(0),
                            wRepeatCount: 0,
                            wVirtualKeyCode: VK_RETURN.0,
                            wVirtualScanCode: 0x0A,
                            uChar: KEY_EVENT_RECORD_0 { UnicodeChar: 0x1B },
                            dwControlKeyState: 0,
                        },
                    },
                },
            ];
            let Ok(stdin_handle) = GetStdHandle(STD_INPUT_HANDLE) else {
                return;
            };
            loop {
                // This loop SHOULD eventually terminate...
                let _ = FlushConsoleInputBuffer(stdin_handle);
                let mut wrote = 0;
                let _ = WriteConsoleInputW(stdin_handle, &buf, &raw mut wrote);
                if wrote == 2 {
                    break;
                } else {
                    std::thread::yield_now();
                }
            }
            join_handle.join().expect("unable to join stdin thread");
            STDIN_BEING_INTERRUPTED.store(false, Ordering::SeqCst);
            // We can't be sure that Rust has consumed all the input. Or any of
            // it. Make sure none is left over for the next reader with one
            // last flush.
            let _ = FlushConsoleInputBuffer(stdin_handle);
        }
    }
    pub fn placebo_check() {
        // do nothing, as we are not a placebo
    }
}

static mut OLD_STDIN_MODE: CONSOLE_MODE = CONSOLE_MODE(0);
static mut OLD_STDOUT_MODE: CONSOLE_MODE = CONSOLE_MODE(0);

/// Puts the terminal into raw mode. We can assume we will not be called twice
/// without raw mode being disabled in between. Return true if the input is a
/// tty and raw input is possible.
pub fn enter_raw_mode(ansi: bool) -> bool {
    unsafe {
        let Ok(stdin_handle) = GetStdHandle(STD_INPUT_HANDLE) else {
            return false;
        };
        let Ok(stdout_handle) = GetStdHandle(STD_OUTPUT_HANDLE) else {
            return false;
        };
        #[cfg(debug_assertions)]
        loop {
            match IS_RAW.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) {
                Ok(_) => break,
                Err(true) => panic!("BUG IN LISO: enter_raw_mode() called twice without exit_raw_mode() in between!"),
                Err(false) => continue,
            }
        }
        let Ok(_) = GetConsoleMode(stdin_handle, &raw mut OLD_STDIN_MODE)
        else {
            #[cfg(debug_assertions)]
            IS_RAW.store(false, Ordering::Release);
            return false;
        };
        let Ok(_) = GetConsoleMode(stdout_handle, &raw mut OLD_STDOUT_MODE)
        else {
            #[cfg(debug_assertions)]
            IS_RAW.store(false, Ordering::Release);
            return false;
        };
        let mut in_mode = ENABLE_QUICK_EDIT_MODE | ENABLE_WINDOW_INPUT;
        if ansi {
            in_mode |= ENABLE_VIRTUAL_TERMINAL_INPUT;
        }
        let mut out_mode = ENABLE_PROCESSED_OUTPUT | ENABLE_LVB_GRID_WORLDWIDE;
        if ansi {
            out_mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
        }
        let Ok(_) = SetConsoleMode(stdin_handle, in_mode) else {
            #[cfg(debug_assertions)]
            IS_RAW.store(false, Ordering::Release);
            return false;
        };
        let Ok(_) = SetConsoleMode(stdout_handle, out_mode) else {
            panic!("we were able to set the console input mode, but not the console output mode");
        };
        true
    }
}

/// Restores the previous terminal mode, whatever that was. Guaranteed to only
/// be called if enter_raw_mode() has previously succeeded.
pub fn exit_raw_mode() {
    #[cfg(debug_assertions)]
    if !IS_RAW.load(Ordering::Relaxed) {
        panic!("BUG IN LISO: exit_raw_mode() called without preceding enter_raw_mode()!")
    } else {
        IS_RAW.store(false, Ordering::Release);
    }
    unsafe {
        let stdin_handle = GetStdHandle(STD_INPUT_HANDLE).unwrap();
        let stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE).unwrap();
        let _ = SetConsoleMode(stdin_handle, OLD_STDIN_MODE);
        let _ = SetConsoleMode(stdout_handle, OLD_STDOUT_MODE);
    }
}

pub fn stdin_and_stdout_are_tty() -> bool {
    unsafe { isatty(0) != 0 && isatty(1) != 0 }
}

pub fn stdin_being_interrupted() -> bool {
    STDIN_BEING_INTERRUPTED.load(Ordering::Relaxed)
}