use std::fs::{File, OpenOptions};
use std::os::fd::{AsFd, BorrowedFd};
use std::path::Path;
use rustix::ioctl::{self, Getter, IntegerSetter, Opcode, opcode};
const KDGKBTYPE: Opcode = opcode::none(0x4B, 0x33);
const KDGKBMODE: Opcode = opcode::none(0x4B, 0x44);
const KDSKBMODE: Opcode = opcode::none(0x4B, 0x45);
const KDGETMODE: Opcode = opcode::none(0x4B, 0x3B);
const KDSETMODE: Opcode = opcode::none(0x4B, 0x3A);
pub const K_RAW: u32 = 0x00;
pub const K_XLATE: u32 = 0x01;
pub const K_MEDIUMRAW: u32 = 0x02;
pub const K_UNICODE: u32 = 0x03;
pub const K_OFF: u32 = 0x04;
pub const KD_TEXT: u32 = 0x00;
pub const KD_GRAPHICS: u32 = 0x01;
const CANDIDATES: [&str; 3] = ["/dev/tty", "/dev/tty0", "/dev/console"];
#[derive(Debug, thiserror::Error)]
pub enum ConsoleError {
#[error("no console found (tried {}); over SSH there is no VT to mute", CANDIDATES.join(", "))]
NoConsole,
#[error("console ioctl failed: {0}")]
Ioctl(#[source] std::io::Error),
#[error("could not open {path}: {source}")]
Open {
path: String,
#[source]
source: std::io::Error,
},
}
#[derive(Debug)]
pub struct Console {
file: File,
keyboard: Option<u32>,
screen: Option<u32>,
}
impl Console {
pub fn open() -> Result<Self, ConsoleError> {
for path in CANDIDATES {
if let Ok(console) = Self::open_path(Path::new(path)) {
return Ok(console);
}
}
Err(ConsoleError::NoConsole)
}
pub fn open_if_present() -> Option<Self> {
Self::open().ok()
}
pub fn open_path(path: &Path) -> Result<Self, ConsoleError> {
let file = OpenOptions::new()
.read(true)
.write(true)
.open(path)
.map_err(|source| ConsoleError::Open {
path: path.display().to_string(),
source,
})?;
let kind = unsafe { ioctl::ioctl(file.as_fd(), Getter::<KDGKBTYPE, u8>::new()) };
if kind.is_err() {
return Err(ConsoleError::NoConsole);
}
Ok(Self {
file,
keyboard: None,
screen: None,
})
}
pub fn mute_keyboard(&mut self) -> Result<(), ConsoleError> {
if self.keyboard.is_none() {
self.keyboard = Some(self.keyboard_mode()?);
}
self.set_keyboard_mode(K_OFF)
}
pub fn graphics_mode(&mut self) -> Result<(), ConsoleError> {
if self.screen.is_none() {
self.screen = Some(self.screen_mode()?);
}
self.set_screen_mode(KD_GRAPHICS)
}
pub fn restore(&mut self) -> Result<(), ConsoleError> {
let mut result = Ok(());
if let Some(mode) = self.keyboard.take() {
result = result.and(self.set_keyboard_mode(mode));
}
if let Some(mode) = self.screen.take() {
result = result.and(self.set_screen_mode(mode));
}
result
}
pub fn keyboard_mode(&self) -> Result<u32, ConsoleError> {
let mode = unsafe { ioctl::ioctl(self.file.as_fd(), Getter::<KDGKBMODE, u32>::new()) };
mode.map_err(|e| ConsoleError::Ioctl(e.into()))
}
pub fn screen_mode(&self) -> Result<u32, ConsoleError> {
let mode = unsafe { ioctl::ioctl(self.file.as_fd(), Getter::<KDGETMODE, u32>::new()) };
mode.map_err(|e| ConsoleError::Ioctl(e.into()))
}
fn set_keyboard_mode(&self, mode: u32) -> Result<(), ConsoleError> {
let result = unsafe {
ioctl::ioctl(
self.file.as_fd(),
IntegerSetter::<KDSKBMODE>::new_usize(mode as usize),
)
};
result.map_err(|e| ConsoleError::Ioctl(e.into()))
}
fn set_screen_mode(&self, mode: u32) -> Result<(), ConsoleError> {
let result = unsafe {
ioctl::ioctl(
self.file.as_fd(),
IntegerSetter::<KDSETMODE>::new_usize(mode as usize),
)
};
result.map_err(|e| ConsoleError::Ioctl(e.into()))
}
}
impl AsFd for Console {
fn as_fd(&self) -> BorrowedFd<'_> {
self.file.as_fd()
}
}
impl Drop for Console {
fn drop(&mut self) {
let _ = self.restore();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn opcodes_match_the_kernel_headers() {
assert_eq!(KDGKBTYPE, 0x4B33);
assert_eq!(KDSETMODE, 0x4B3A);
assert_eq!(KDGETMODE, 0x4B3B);
assert_eq!(KDGKBMODE, 0x4B44);
assert_eq!(KDSKBMODE, 0x4B45);
}
#[test]
fn mode_constants_match_the_kernel_headers() {
assert_eq!(
[K_RAW, K_XLATE, K_MEDIUMRAW, K_UNICODE, K_OFF],
[0, 1, 2, 3, 4]
);
assert_eq!([KD_TEXT, KD_GRAPHICS], [0, 1]);
}
#[test]
fn a_character_device_that_is_not_a_console_is_refused() {
let result = Console::open_path(Path::new("/dev/null"));
assert!(
matches!(result, Err(ConsoleError::NoConsole)),
"expected NoConsole, got {result:?}"
);
}
}