rm-lisa 0.3.2

A logging library for rem-verse, with support for inputs, tasks, and more.
Documentation
//! Platforms that support libc APIs directly.
//!
//! These are usually unix and linux platforms, such as linux, haiku, bsd, etc.

use crate::{
	errors::LisaError,
	termios::{Termios, cfmakeraw, os::target::TCSADRAIN, tcsetattr},
};
use libc::{F_GETFL, F_SETFL, O_NONBLOCK, OPOST, SIGINT, fcntl, raise, read};
use std::{
	cmp::Ordering as CmpOrdering,
	ffi::c_void,
	io::{Error as IOError, ErrorKind as IOErrorKind},
};

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

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

/// 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 libc api calls fail, the ones we
/// make are:
///
/// - [`cfmakeraw`]
/// - [`crate::termios::tcgetattr`]
/// - [`tcsetattr`]
/// - [`fcntl`]
///
/// We also assume STDIN is always fd 0. You should check your OS's
/// documentation for when this calls may fail and why.
pub fn enable_raw_mode() -> Result<CachedModeType, LisaError> {
	let mut original_termios = Termios::from_fd(0)?;
	let cached_mode_type = original_termios;
	cfmakeraw(&mut original_termios);
	// Re-enable output processing...
	original_termios.c_oflag |= OPOST;
	tcsetattr(0, TCSADRAIN, &original_termios)?;

	let flags = unsafe { fcntl(0, F_GETFL, 0) };
	if flags < 0 {
		return Err(LisaError::IOError(IOError::last_os_error()));
	}
	if flags & O_NONBLOCK == 0 {
		let rc = unsafe { fcntl(0, F_SETFL, flags | O_NONBLOCK) };
		if rc < 0 {
			return Err(LisaError::IOError(IOError::last_os_error()));
		}
	}

	Ok(cached_mode_type)
}

/// 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 libc calls fail, the ones we make
/// are:
///
/// - [`tcsetattr`]
/// - [`fcntl`]
///
/// We also assume STDIN is always fd 0. You should check your OS's
/// documentation for when this calls may fail and why.
pub fn disable_raw_mode(cached: CachedModeType) -> Result<CachedModeType, LisaError> {
	tcsetattr(0, TCSADRAIN, &cached)?;

	let flags = unsafe { fcntl(0, F_GETFL, 0) };
	if flags < 0 {
		return Err(LisaError::IOError(IOError::last_os_error()));
	}
	if flags & O_NONBLOCK != 0 {
		let rc = unsafe { fcntl(0, F_SETFL, flags & !O_NONBLOCK) };
		if rc < 0 {
			return Err(LisaError::IOError(IOError::last_os_error()));
		}
	}

	Ok(Termios::empty())
}

/// Perform any initialization of OS pre-requisities that need to be loaded.
///
/// For libc systems we don't actually need to do any initialization at all.
///
/// ## Errors
///
/// Never for libc platforms, kept as a result for compatability with other
/// OS's.
#[allow(
	// Is necessary to match with other OS's.
	clippy::unnecessary_wraps,
)]
pub fn os_pre_reqs() -> Result<(), LisaError> {
	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 {
		raise(SIGINT);
	}
}

/// Attempt to read from standard-input but do so in a non-blocking way.
///
/// In our case [`enable_raw_mode`] enables non-blocking, so we don't have to
/// do anything special ourselves. Specifically this happens with a call to
/// [`fcntl`] at enablement time. This just calls read as normal.
///
/// ## Errors
///
/// This will error if the underlying [`read`] call fails, and it's not a
/// [`IOErrorKind::WouldBlock`] kind of error.
pub fn read_non_blocking_stdin() -> Result<String, LisaError> {
	let mut final_bytes: Vec<u8> = Vec::new();

	let mut current_buf: Vec<u8> = vec![0; 8192];
	loop {
		let read_bytes: isize = unsafe { read(0, current_buf.as_mut_ptr().cast::<c_void>(), 16) };

		match read_bytes.cmp(&0) {
			CmpOrdering::Less => {
				let error = IOError::last_os_error();
				if error.kind() == IOErrorKind::WouldBlock {
					break;
				}
				return Err(LisaError::IOError(error));
			}
			CmpOrdering::Equal => {
				break;
			}
			CmpOrdering::Greater => {
				final_bytes
					.extend(current_buf.drain(..usize::try_from(read_bytes).unwrap_or(usize::MAX)));
				current_buf = vec![0; 16];
			}
		}
	}

	if final_bytes.is_empty() {
		Ok(String::with_capacity(0))
	} else {
		Ok(String::from_utf8(final_bytes)?)
	}
}