rm-lisa 0.3.2

A logging library for rem-verse, with support for inputs, tasks, and more.
Documentation
//! Platforms that support Windows style APIs.

use crate::errors::LisaError;
use std::{mem::transmute_copy, sync::OnceLock};
use widestring::U16String;
use windows::{
	Win32::{
		Foundation::{GetLastError, HANDLE},
		System::{
			Console::{
				CONSOLE_MODE, CTRL_C_EVENT, ENABLE_ECHO_INPUT, ENABLE_LINE_INPUT,
				ENABLE_PROCESSED_INPUT, ENABLE_VIRTUAL_TERMINAL_INPUT, GenerateConsoleCtrlEvent,
				GetConsoleMode, GetStdHandle, INPUT_RECORD, KEY_EVENT, STD_INPUT_HANDLE,
				SetConsoleMode,
			},
			LibraryLoader::{GetModuleHandleW, GetProcAddress},
		},
	},
	core::{BOOL as WindowsBool, PCWSTR, s, w},
};

type ReadConsoleInputExW = unsafe extern "system" fn(
	h_console_input: HANDLE,
	lp_buffer: *mut INPUT_RECORD,
	n_length: u32,
	lp_number_of_events_read: *mut u32,
	w_flags: u16,
) -> WindowsBool;
static LOADED_READ_CONSOLE_INPUT: OnceLock<ReadConsoleInputExW> = OnceLock::new();

const CONSOLE_READ_NOWAIT: u16 = 0x0002;

/// The type of "Console Mode" to cache between enable/disable.
pub type CachedModeType = CONSOLE_MODE;

/// Get the 'default' cached console mode.
#[must_use]
pub fn default_cached_mode() -> CachedModeType {
	CONSOLE_MODE(0)
}

/// Enable the "raw" mode of a terminal for this OS.
///
/// In this case we want to turn off echo, and manually handle all particular
/// key-codes. Even those like Ctrl-C. Output processing _should_ still be
/// enabled however to keep output logic as simple as possible.
///
/// This should return the mode to set back when disable is called.
///
/// ## Errors
///
/// We will error if any of the underlying windows api calls fail, the ones we
/// make are:
///
/// - [`GetStdHandle`]
/// - [`GetConsoleMode`]
/// - [`SetConsoleMode`]
///
/// Please read windows documentation for more descriptions about when it may
/// fail.
pub fn enable_raw_mode() -> Result<CachedModeType, LisaError> {
	let mut current_console_mode = CONSOLE_MODE(0);
	let stdin = unsafe { GetStdHandle(STD_INPUT_HANDLE) }?;
	unsafe { GetConsoleMode(stdin, &raw mut current_console_mode) }?;
	let mut cloned_old_mode = current_console_mode.clone();

	current_console_mode &= !(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT);
	current_console_mode |= ENABLE_VIRTUAL_TERMINAL_INPUT;
	if current_console_mode != cloned_old_mode {
		unsafe { SetConsoleMode(stdin, current_console_mode) }?;
	} else {
		cloned_old_mode = CONSOLE_MODE(0);
	}

	Ok(cloned_old_mode)
}

/// Turn off the "raw" mode of a terminal for this OS.
///
/// This should set the mode we cached, and returned from [`enable_raw_mode`],
/// and not come up with it's own mode to set too.
///
/// ## Errors
///
/// We will error if any of the underlying windows api calls fail, the ones we
/// make are:
///
/// - [`GetStdHandle`]
/// - [`SetConsoleMode`]
///
/// Please read windows documentation for more descriptions about when it may
/// fail.
pub fn disable_raw_mode(cached: CachedModeType) -> Result<CachedModeType, LisaError> {
	if cached.0 != 0 {
		let stdin = unsafe { GetStdHandle(STD_INPUT_HANDLE) }?;
		unsafe { SetConsoleMode(stdin, cached) }?;
	}

	return Ok(CONSOLE_MODE(0));
}

/// Perform any initialization of OS pre-requisities that need to be loaded.
///
/// For windows APIs we need to load [`ReadConsoleInputExW`], a function that
/// is not loaded by default, and is present usually in the main kernel dll.
/// This is how we can read from STDIN without blocking.
///
/// ## Errors
///
/// If we cannot load the function [`ReadConsoleInputExW`] from one of the
/// following DLLs:
///
/// - `kernel32.dll`
/// - `kernelbase.dll`
///
/// This is what's recommended in the documentation, and done by microsoft's
/// own edit program, but you should check windows own documentation for
/// issues.
pub fn os_pre_reqs() -> Result<(), LisaError> {
	ensure_load_read_console()?;
	Ok(())
}

/// Attempt to raise a SIGINT to our calling process.
///
/// This may not succeed, but we ignore those as the user will always try to
/// cancel again when a cancellation may happen.
pub fn raise_sigint() {
	unsafe {
		_ = GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0);
	}
}

/// Attempt to read from standard-input but do so in a non-blocking way.
///
/// This assumes [`os_pre_reqs`] has been called at least once to load
/// [`ReadConsoleInputExW`] so we can read in a non blocking way.
///
/// ## Errors
///
/// This will error if the underlying [`ReadConsoleInputExW`] call fails, see
/// windows documentation issues for later.
pub fn read_non_blocking_stdin() -> Result<String, LisaError> {
	let stdin = unsafe { GetStdHandle(STD_INPUT_HANDLE) }?;
	let mut uninitialized_input_events: Vec<INPUT_RECORD> = vec![INPUT_RECORD::default(); 4096];
	let mut read = 0;
	let read_console_input_ex = unsafe {
		let Some(fn_ref) = LOADED_READ_CONSOLE_INPUT.get() else {
			return Err(LisaError::Win32(GetLastError()));
		};
		let fn_ptr = *fn_ref;

		fn_ptr(
			stdin,
			&raw mut uninitialized_input_events[0],
			4096,
			&mut read,
			CONSOLE_READ_NOWAIT,
		)
	};
	if read_console_input_ex.0 == 0 || read == 0 {
		return Ok(String::with_capacity(0));
	}
	let input_events = uninitialized_input_events
		.drain(..read as usize)
		.collect::<Vec<_>>();

	let wide_bytes = input_events
		.into_iter()
		.filter_map(|record| {
			if u32::from(record.EventType) != KEY_EVENT {
				None
			} else {
				let key_evt_data = unsafe { record.Event.KeyEvent };
				Some(unsafe { key_evt_data.uChar.UnicodeChar })
			}
		})
		.collect::<Vec<u16>>();

	Ok(U16String::from_vec(wide_bytes).to_string()?)
}

fn ensure_load_read_console() -> Result<(), LisaError> {
	// This isn't locked, so may cause a double load, but this is fine...
	if LOADED_READ_CONSOLE_INPUT.get().is_some() {
		return Ok(());
	}
	let fn_ptr = unsafe { load_read_func(w!("kernel32.dll")) }
		.or_else(|_| unsafe { load_read_func(w!("kernelbase.dll")) })?;
	_ = LOADED_READ_CONSOLE_INPUT.set(fn_ptr);

	Ok(())
}

unsafe fn load_read_func(module: PCWSTR) -> Result<ReadConsoleInputExW, LisaError> {
	let module_ptr = unsafe { GetModuleHandleW(module) }?;

	unsafe { GetProcAddress(module_ptr, s!("ReadConsoleInputExW")) }
		.ok_or_else(|| LisaError::Win32(unsafe { GetLastError() }))
		.map(|value| unsafe { transmute_copy(&value) })
}