use std::ffi::{c_int, c_ulong, c_void};
use std::fs::{File, OpenOptions};
use std::io::{self, Read};
use std::os::fd::AsRawFd;
use std::os::unix::fs::OpenOptionsExt;
use std::sync::mpsc::Sender;
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Key {
Up,
Down,
Left,
Right,
Select,
Back,
Home,
End,
PageUp,
PageDown,
Search,
Backspace,
Char(char),
Play,
Pause,
Stop,
Next,
Prev,
SkipNext, SkipPrev, Audio, Subtitle, Menu, Info, }
const EV_KEY: u16 = 0x01;
const KEY_COUNT: usize = 0x300;
const KEY_WORDS: usize = KEY_COUNT.div_ceil(c_ulong::BITS as usize);
type KeyBits = [c_ulong; KEY_WORDS];
const EVIOCGBIT_KEYS: c_ulong = ioc(2, b'E', 0x20 + EV_KEY as u32, size_of::<KeyBits>() as u32);
fn has_navigation_keys(bits: &KeyBits) -> bool {
(0..KEY_COUNT).any(|code| {
bits[code / c_ulong::BITS as usize] & (1 << (code % c_ulong::BITS as usize)) != 0
&& evdev_key(EV_KEY, code as u16, 1).is_some()
})
}
pub struct Sources {
available: bool,
stop: Arc<AtomicBool>,
terminal: Option<std::thread::JoinHandle<()>>,
}
impl Sources {
pub fn available(&self) -> bool {
self.available
}
}
impl Drop for Sources {
fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
if let Some(reader) = self.terminal.take() {
let _ = reader.join();
}
}
}
pub fn spawn(tx: Sender<Key>, console_keyboard: bool) -> Sources {
let stop = Arc::new(AtomicBool::new(false));
let terminal = spawn_text_reader(tx.clone(), stop.clone());
let terminal_started = terminal.is_some();
let terminal_owns_keys = terminal_started && console_keyboard;
let mut started = terminal_started;
for path in cec_paths() {
match Cec::open(&path) {
Ok(mut cec) => started |= spawn_reader(path, tx.clone(), move || cec.next()),
Err(error) => eprintln!("input: cannot open {path}: {error}"),
}
}
let entries = match std::fs::read_dir("/dev/input") {
Ok(entries) => entries,
Err(error) => {
eprintln!("input: cannot enumerate /dev/input: {error}");
return Sources {
available: started,
stop,
terminal,
};
}
};
let mut paths = Vec::new();
for entry in entries {
match entry {
Ok(entry) => {
let name = entry.file_name();
let name = name.to_string_lossy();
if name.strip_prefix("event").is_some_and(|suffix| {
!suffix.is_empty() && suffix.bytes().all(|b| b.is_ascii_digit())
}) {
paths.push(entry.path());
}
}
Err(error) => eprintln!("input: cannot read /dev/input entry: {error}"),
}
}
paths.sort();
for path in paths {
let path = path.to_string_lossy().into_owned();
let mut evdev = match Evdev::open(&path) {
Ok(evdev) => evdev,
Err(error) => {
eprintln!("input: cannot open {path}: {error}");
continue;
}
};
match evdev.has_navigation_keys() {
Ok(true) => {
started |= spawn_reader(path, tx.clone(), move || {
evdev.next_with_terminal(terminal_owns_keys)
})
}
Ok(false) => {}
Err(error) => eprintln!("input: cannot query key capabilities for {path}: {error}"),
}
}
Sources {
available: started,
stop,
terminal,
}
}
fn spawn_reader(
path: String,
tx: Sender<Key>,
mut next: impl FnMut() -> io::Result<Option<Key>> + Send + 'static,
) -> bool {
let reader_path = path.clone();
match std::thread::Builder::new()
.name(format!("input:{path}"))
.spawn(move || {
loop {
match next() {
Ok(Some(key)) if tx.send(key).is_err() => break,
Ok(_) => {}
Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
Err(error) => {
eprintln!("input: read failed for {reader_path}: {error}");
break;
}
}
}
}) {
Ok(_) => {
eprintln!("input: opened {path}");
true
}
Err(error) => {
eprintln!("input: cannot start reader for {path}: {error}");
false
}
}
}
const EVENT_SIZE: usize = 24;
pub fn evdev_key(type_: u16, code: u16, value: i32) -> Option<Key> {
if type_ != EV_KEY || !(value == 1 || value == 2 && matches!(code, 103..=109 | 102 | 14)) {
return None;
}
Some(match code {
103 => Key::Up,
108 => Key::Down,
105 => Key::Left,
106 => Key::Right,
28 | 96 => Key::Select, 1 | 158 => Key::Back, 102 => Key::Home,
107 => Key::End,
104 => Key::PageUp,
109 => Key::PageDown,
217 => Key::Search,
14 => Key::Backspace,
207 => Key::Play,
119 => Key::Pause,
128 => Key::Stop,
57 | 164 => Key::Play, 163 => Key::SkipNext, 165 => Key::SkipPrev, 392 => Key::Audio, 370 => Key::Subtitle, 398 => Key::Subtitle, 399 => Key::Audio, 401 => Key::Menu, 400 => Key::Info, 402 => Key::Next, 403 => Key::Prev, 139 => Key::Menu, 358 => Key::Info, 62 => Key::Info, _ => return None,
})
}
#[cfg(test)]
fn parse_evdev(buf: &[u8; EVENT_SIZE]) -> Option<Key> {
let type_ = u16::from_ne_bytes([buf[16], buf[17]]);
let code = u16::from_ne_bytes([buf[18], buf[19]]);
let value = i32::from_ne_bytes([buf[20], buf[21], buf[22], buf[23]]);
evdev_key(type_, code, value)
}
pub struct Evdev {
file: File,
}
impl Evdev {
pub fn open(path: &str) -> io::Result<Evdev> {
Ok(Evdev {
file: File::open(path)?,
})
}
fn has_navigation_keys(&self) -> io::Result<bool> {
let mut bits: KeyBits = [0; KEY_WORDS];
let result =
unsafe { ioctl(self.file.as_raw_fd(), EVIOCGBIT_KEYS, bits.as_mut_ptr().cast()) };
if result < 0 {
return Err(io::Error::last_os_error());
}
Ok(has_navigation_keys(&bits))
}
pub fn next(&mut self) -> io::Result<Option<Key>> {
self.next_with_terminal(false)
}
fn next_with_terminal(&mut self, terminal_owns_keys: bool) -> io::Result<Option<Key>> {
let mut buf = [0u8; EVENT_SIZE];
self.file.read_exact(&mut buf)?;
let type_ = u16::from_ne_bytes([buf[16], buf[17]]);
let code = u16::from_ne_bytes([buf[18], buf[19]]);
let value = i32::from_ne_bytes([buf[20], buf[21], buf[22], buf[23]]);
if terminal_owns_keys && terminal_keycode(code) {
return Ok(None);
}
if type_ == EV_KEY && matches!(value, 1 | 2) {
let k = evdev_key(type_, code, value);
if k.is_none() && value == 1 && code >= 256 {
eprintln!("evdev: unmapped key code {code}");
}
return Ok(k);
}
Ok(None)
}
}
fn terminal_keycode(code: u16) -> bool {
matches!(code, 1 | 14 | 28 | 57 | 62 | 96 | 102..=109)
}
#[derive(Default)]
struct TextDecoder {
escape: u8,
csi_start: bool,
utf8: Vec<u8>,
expected: usize,
sequence: Vec<u8>,
}
impl TextDecoder {
fn expire_escape(&mut self) -> Option<Key> {
let key = (self.escape == 1).then_some(Key::Back);
self.escape = 0;
self.sequence.clear();
key
}
fn byte(&mut self, byte: u8) -> Option<Key> {
if byte == 0x1b {
self.escape = 1;
self.utf8.clear();
return None;
}
match self.escape {
1 => {
self.escape = match byte {
b'[' => 2,
b'O' => 3,
_ => 0,
};
self.csi_start = true;
self.sequence.clear();
if self.escape != 0 {
return None;
}
}
2 | 3 => {
if self.escape == 2 && self.csi_start && byte == b'[' {
self.csi_start = false;
self.sequence.push(byte);
return None;
}
self.csi_start = false;
if (0x40..=0x7e).contains(&byte) {
let ss3 = self.escape == 3;
self.escape = 0;
let key = match byte {
b'A' if self.sequence.first() != Some(&b'[') => Some(Key::Up),
b'B' if self.sequence.first() != Some(&b'[') => Some(Key::Down),
b'C' if self.sequence.first() != Some(&b'[') => Some(Key::Right),
b'D' if self.sequence.first() != Some(&b'[') => Some(Key::Left),
b'S' if ss3 => Some(Key::Info),
b'D' if self.sequence == b"[" => Some(Key::Info),
b'H' => Some(Key::Home),
b'F' => Some(Key::End),
b'~' => match self
.sequence
.split(|&b| b == b';')
.next()
.unwrap_or_default()
{
b"1" | b"7" => Some(Key::Home),
b"4" | b"8" => Some(Key::End),
b"5" => Some(Key::PageUp),
b"6" => Some(Key::PageDown),
b"14" => Some(Key::Info), _ => None,
},
_ => None,
};
self.sequence.clear();
return key;
}
if self.sequence.len() < 64 {
self.sequence.push(byte);
}
return None;
}
_ => {}
}
if !self.utf8.is_empty() {
if byte & 0xc0 == 0x80 {
self.utf8.push(byte);
if self.utf8.len() < self.expected {
return None;
}
let character = std::str::from_utf8(&self.utf8)
.ok()
.and_then(|s| s.chars().next());
self.utf8.clear();
return character.filter(|c| !c.is_control()).map(Key::Char);
}
self.utf8.clear();
}
match byte {
b'\r' | b'\n' => Some(Key::Select),
0x08 | 0x7f => Some(Key::Backspace),
0x06 => Some(Key::Search),
0x20..=0x7e => Some(Key::Char(byte as char)),
0xc2..=0xf4 => {
self.expected = if byte < 0xe0 {
2
} else if byte < 0xf0 {
3
} else {
4
};
self.utf8.push(byte);
None
}
_ => None,
}
}
}
fn spawn_text_reader(
tx: Sender<Key>,
stop: Arc<AtomicBool>,
) -> Option<std::thread::JoinHandle<()>> {
let mut tty = match OpenOptions::new()
.read(true)
.custom_flags(libc::O_NONBLOCK)
.open("/dev/tty")
{
Ok(tty) => tty,
Err(error) => {
eprintln!("input: cannot open /dev/tty for text: {error}");
return None;
}
};
if !crate::console::terminal_is_foreground(tty.as_raw_fd()) {
eprintln!("input: skipping background terminal input");
return None;
}
let result = std::thread::Builder::new()
.name("input:text".into())
.spawn(move || {
let mut decoder = TextDecoder::default();
let mut last_byte = Instant::now();
let mut bytes = [0; 256];
while !stop.load(Ordering::Relaxed) && !crate::console::stopping() {
let mut poll = libc::pollfd {
fd: tty.as_raw_fd(),
events: libc::POLLIN,
revents: 0,
};
let ready = unsafe { libc::poll(&mut poll, 1, 50) };
if stop.load(Ordering::Relaxed) || crate::console::stopping() {
return;
}
if last_byte.elapsed() >= Duration::from_millis(100) {
if let Some(key) = decoder.expire_escape()
&& tx.send(key).is_err()
{
return;
}
}
if ready < 0 {
let error = io::Error::last_os_error();
if error.kind() == io::ErrorKind::Interrupted {
continue;
}
eprintln!("input: poll failed for /dev/tty: {error}");
break;
}
if ready == 0 {
continue;
}
match tty.read(&mut bytes) {
Ok(0) => break,
Ok(count) => {
last_byte = Instant::now();
for &byte in &bytes[..count] {
if let Some(key) = decoder.byte(byte)
&& tx.send(key).is_err()
{
return;
}
}
}
Err(error)
if matches!(
error.kind(),
io::ErrorKind::WouldBlock | io::ErrorKind::Interrupted
) => {}
Err(error) => {
eprintln!("input: read failed for /dev/tty: {error}");
break;
}
}
}
});
match result {
Ok(reader) => {
eprintln!("input: opened /dev/tty for keyboard input");
Some(reader)
}
Err(error) => {
eprintln!("input: cannot start terminal reader: {error}");
None
}
}
}
const CEC_MSG_USER_CONTROL_PRESSED: u8 = 0x44;
const CEC_MSG_ACTIVE_SOURCE: u8 = 0x82;
const CEC_MSG_REQUEST_ACTIVE_SOURCE: u8 = 0x85;
const CEC_MSG_SET_STREAM_PATH: u8 = 0x86;
const CEC_MSG_MENU_REQUEST: u8 = 0x8d;
const CEC_MSG_MENU_STATUS: u8 = 0x8e;
const CEC_MSG_GIVE_DEVICE_POWER_STATUS: u8 = 0x8f;
const CEC_MSG_REPORT_POWER_STATUS: u8 = 0x90;
const CEC_MODE_INITIATOR: u32 = 0x1;
const CEC_MODE_FOLLOWER: u32 = 0x1 << 4;
const CEC_PHYS_ADDR_INVALID: u16 = 0xffff;
const CEC_BROADCAST: u8 = 0xf;
fn cec_paths() -> Vec<String> {
let mut paths: Vec<String> = std::fs::read_dir("/dev")
.into_iter()
.flatten()
.flatten()
.filter_map(|e| e.file_name().into_string().ok())
.filter(|n| n.strip_prefix("cec").is_some_and(|d| !d.is_empty() && d.bytes().all(|b| b.is_ascii_digit())))
.map(|n| format!("/dev/{n}"))
.collect();
paths.sort();
paths
}
pub fn cec_ui_key(cmd: u8) -> Option<Key> {
Some(match cmd {
0x00 => Key::Select,
0x01 => Key::Up,
0x02 => Key::Down,
0x03 => Key::Left,
0x04 => Key::Right,
0x0d => Key::Back,
0x44 => Key::Play,
0x45 => Key::Stop,
0x46 => Key::Pause,
0x48 => Key::Prev, 0x49 => Key::Next, 0x4b => Key::SkipNext, 0x4c => Key::SkipPrev, 0x33 => Key::Audio, 0x72 => Key::Subtitle, 0x73 => Key::Audio, 0x71 => Key::Menu, 0x74 => Key::Info, 0x30 => Key::Next, 0x31 => Key::Prev, _ => return None,
})
}
#[repr(C)]
struct CecMsg {
tx_ts: u64,
rx_ts: u64,
len: u32,
timeout: u32,
sequence: u32,
flags: u32,
msg: [u8; 16],
reply: u8,
rx_status: u8,
tx_status: u8,
tx_arb_lost_cnt: u8,
tx_nack_cnt: u8,
tx_low_drive_cnt: u8,
tx_error_cnt: u8,
}
const fn ioc(dir: u32, typ: u8, nr: u32, size: u32) -> c_ulong {
((dir << 30) | ((typ as u32) << 8) | nr | (size << 16)) as c_ulong
}
#[repr(C)]
struct CecLogAddrs {
log_addr: [u8; 4],
log_addr_mask: u16,
cec_version: u8,
num_log_addrs: u8,
vendor_id: u32,
flags: u32,
osd_name: [u8; 15],
primary_device_type: [u8; 4],
log_addr_type: [u8; 4],
all_device_types: [u8; 4],
features: [[u8; 12]; 4],
}
const CEC_ADAP_G_PHYS_ADDR: c_ulong = ioc(2, b'a', 1, 2); const CEC_ADAP_G_LOG_ADDRS: c_ulong = ioc(2, b'a', 3, size_of::<CecLogAddrs>() as u32);
const CEC_ADAP_S_LOG_ADDRS: c_ulong = ioc(3, b'a', 4, size_of::<CecLogAddrs>() as u32);
const CEC_TRANSMIT: c_ulong = ioc(3, b'a', 5, size_of::<CecMsg>() as u32); const CEC_S_MODE: c_ulong = ioc(1, b'a', 9, 4); const CEC_RECEIVE: c_ulong = ioc(3, b'a', 6, size_of::<CecMsg>() as u32);
unsafe extern "C" {
fn ioctl(fd: c_int, request: c_ulong, arg: *mut c_void) -> c_int;
}
pub struct Cec {
file: File,
claimed: bool, active: bool, }
impl Cec {
pub fn open(path: &str) -> io::Result<Cec> {
let file = OpenOptions::new().read(true).write(true).open(path)?;
let mut mode: u32 = CEC_MODE_INITIATOR | CEC_MODE_FOLLOWER;
let r =
unsafe { ioctl(file.as_raw_fd(), CEC_S_MODE, &mut mode as *mut u32 as *mut c_void) };
if r < 0 {
return Err(io::Error::last_os_error());
}
Ok(Cec { file, claimed: false, active: false })
}
fn ioctl<T>(&self, request: c_ulong, arg: &mut T) -> io::Result<()> {
let r = unsafe { ioctl(self.file.as_raw_fd(), request, arg as *mut T as *mut c_void) };
if r < 0 { Err(io::Error::last_os_error()) } else { Ok(()) }
}
fn phys_addr(&self) -> u16 {
let mut pa = CEC_PHYS_ADDR_INVALID;
let _ = self.ioctl(CEC_ADAP_G_PHYS_ADDR, &mut pa);
pa
}
fn log_addrs(&self) -> io::Result<CecLogAddrs> {
let mut las: CecLogAddrs = unsafe { std::mem::zeroed() };
self.ioctl(CEC_ADAP_G_LOG_ADDRS, &mut las)?;
Ok(las)
}
fn own_la(&self) -> Option<u8> {
self.log_addrs().ok().filter(|l| l.log_addr_mask != 0).map(|l| l.log_addr[0])
}
fn transmit(&self, bytes: &[u8]) {
let mut msg: CecMsg = unsafe { std::mem::zeroed() };
msg.len = bytes.len() as u32;
msg.msg[..bytes.len()].copy_from_slice(bytes);
if let Err(error) = self.ioctl(CEC_TRANSMIT, &mut msg) {
eprintln!("cec: transmit {:#04x}: {error}", bytes.get(1).copied().unwrap_or(0));
}
}
fn announce_active_source(&mut self) {
let pa = self.phys_addr();
if let (Some(la), true) = (self.own_la(), pa != CEC_PHYS_ADDR_INVALID) {
self.transmit(&[la << 4 | CEC_BROADCAST, CEC_MSG_ACTIVE_SOURCE, (pa >> 8) as u8, pa as u8]);
self.active = true;
}
}
fn claim(&mut self) {
match self.log_addrs() {
Ok(las) if las.num_log_addrs == 0 => {
let mut las: CecLogAddrs = unsafe { std::mem::zeroed() };
las.cec_version = 5; las.num_log_addrs = 1;
las.vendor_id = 0xffff_ffff; las.osd_name[..6].copy_from_slice(b"medias");
las.primary_device_type[0] = 4; las.log_addr_type[0] = 3; las.all_device_types[0] = 0x10; match self.ioctl(CEC_ADAP_S_LOG_ADDRS, &mut las) {
Ok(()) => eprintln!("cec: claimed logical address mask {:#06x}", las.log_addr_mask),
Err(error) => eprintln!("cec: cannot claim a logical address: {error}"),
}
}
Ok(_) => {}
Err(error) => eprintln!("cec: cannot read logical addresses: {error}"),
}
self.announce_active_source();
}
fn follow(&mut self, msg: &CecMsg) {
if msg.len < 2 {
return;
}
let (from, to, op) = (msg.msg[0] >> 4, msg.msg[0] & 0xf, msg.msg[1]);
let directed = to != CEC_BROADCAST;
match op {
CEC_MSG_GIVE_DEVICE_POWER_STATUS if directed => {
self.transmit(&[to << 4 | from, CEC_MSG_REPORT_POWER_STATUS, 0x00]); }
CEC_MSG_MENU_REQUEST if directed => {
self.transmit(&[to << 4 | from, CEC_MSG_MENU_STATUS, 0x00]); }
CEC_MSG_REQUEST_ACTIVE_SOURCE if self.active => self.announce_active_source(),
CEC_MSG_SET_STREAM_PATH if msg.len >= 4 => {
if u16::from_be_bytes([msg.msg[2], msg.msg[3]]) == self.phys_addr() {
self.announce_active_source();
}
}
CEC_MSG_ACTIVE_SOURCE if msg.len >= 4 => {
self.active = u16::from_be_bytes([msg.msg[2], msg.msg[3]]) == self.phys_addr();
}
_ => {}
}
}
pub fn next(&mut self) -> io::Result<Option<Key>> {
if !self.claimed {
self.claimed = true;
self.claim();
}
let mut msg: CecMsg = unsafe { std::mem::zeroed() };
self.ioctl(CEC_RECEIVE, &mut msg)?;
if msg.len >= 3 && msg.msg[1] == CEC_MSG_USER_CONTROL_PRESSED {
let cmd = msg.msg[2];
let k = cec_ui_key(cmd);
if k.is_none() {
eprintln!("cec: unmapped ui command {cmd:#04x}");
}
Ok(k)
} else {
self.follow(&msg);
Ok(None)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn source_guard_stops_and_joins_terminal_reader() {
let stop = Arc::new(AtomicBool::new(false));
let finished = Arc::new(AtomicBool::new(false));
let reader_stop = stop.clone();
let reader_finished = finished.clone();
let terminal = std::thread::spawn(move || {
while !reader_stop.load(Ordering::Relaxed) {
std::thread::park_timeout(Duration::from_millis(1));
}
reader_finished.store(true, Ordering::Relaxed);
});
let sources = Sources { available: true, stop: stop.clone(), terminal: Some(terminal) };
assert!(sources.available());
drop(sources);
assert!(stop.load(Ordering::Relaxed));
assert!(finished.load(Ordering::Relaxed));
}
fn capabilities(codes: &[usize]) -> KeyBits {
let mut bits = [0; KEY_WORDS];
for &code in codes {
bits[code / c_ulong::BITS as usize] |= 1 << (code % c_ulong::BITS as usize);
}
bits
}
#[test]
fn capability_classification() {
assert!(!has_navigation_keys(&capabilities(&[])));
assert!(!has_navigation_keys(&capabilities(&[116]))); assert!(!has_navigation_keys(&capabilities(&[272, 273, 274]))); assert!(has_navigation_keys(&capabilities(&[28, 103])));
assert!(has_navigation_keys(&capabilities(&[164])));
assert!(has_navigation_keys(&capabilities(&[403])));
assert!(!has_navigation_keys(&capabilities(&[767])));
}
#[test]
fn evdev_capability_ioctl_abi() {
assert_eq!(size_of::<KeyBits>(), 96);
assert_eq!(EVIOCGBIT_KEYS, 0x8060_4521);
}
fn event(type_: u16, code: u16, value: i32) -> [u8; EVENT_SIZE] {
let mut b = [0u8; EVENT_SIZE];
b[16..18].copy_from_slice(&type_.to_ne_bytes());
b[18..20].copy_from_slice(&code.to_ne_bytes());
b[20..24].copy_from_slice(&value.to_ne_bytes());
b
}
#[test]
fn evdev_press_maps() {
assert_eq!(parse_evdev(&event(EV_KEY, 103, 1)), Some(Key::Up));
assert_eq!(parse_evdev(&event(EV_KEY, 28, 1)), Some(Key::Select));
assert_eq!(parse_evdev(&event(EV_KEY, 158, 1)), Some(Key::Back));
}
#[test]
fn evdev_release_and_transport_repeat_ignored() {
assert_eq!(parse_evdev(&event(EV_KEY, 103, 0)), None); for code in [28, 57, 164, 207, 217, 1] {
assert_eq!(parse_evdev(&event(EV_KEY, code, 2)), None);
}
assert_eq!(parse_evdev(&event(0x00, 103, 1)), None); }
#[test]
fn evdev_navigation_and_backspace_repeat() {
for (code, key) in [
(103, Key::Up),
(108, Key::Down),
(105, Key::Left),
(106, Key::Right),
(102, Key::Home),
(107, Key::End),
(104, Key::PageUp),
(109, Key::PageDown),
(14, Key::Backspace),
] {
assert_eq!(evdev_key(EV_KEY, code, 1), Some(key));
assert_eq!(evdev_key(EV_KEY, code, 2), Some(key));
}
assert_eq!(evdev_key(EV_KEY, 217, 1), Some(Key::Search));
}
#[test]
fn remote_terminal_keeps_local_navigation_and_console_avoids_duplicates() {
use std::io::Write;
use std::os::fd::FromRawFd;
for console_keyboard in [false, true] {
let mut fds = [-1; 2];
assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0);
let mut reader = Evdev { file: unsafe { File::from_raw_fd(fds[0]) } };
let mut writer = unsafe { File::from_raw_fd(fds[1]) };
writer.write_all(&event(EV_KEY, 103, 1)).unwrap();
writer.write_all(&event(EV_KEY, 28, 1)).unwrap();
writer.write_all(&event(EV_KEY, 164, 1)).unwrap();
assert_eq!(
reader.next_with_terminal(console_keyboard).unwrap(),
(!console_keyboard).then_some(Key::Up),
);
assert_eq!(
reader.next_with_terminal(console_keyboard).unwrap(),
(!console_keyboard).then_some(Key::Select),
);
assert_eq!(reader.next_with_terminal(console_keyboard).unwrap(), Some(Key::Play));
}
}
fn text(decoder: &mut TextDecoder, bytes: &[u8]) -> Vec<Key> {
bytes
.iter()
.filter_map(|&byte| decoder.byte(byte))
.collect()
}
#[test]
fn terminal_utf8_survives_split_reads() {
let mut decoder = TextDecoder::default();
assert_eq!(text(&mut decoder, &[0xd0]), []);
assert_eq!(text(&mut decoder, &[0x96, 0xe7, 0x95]), [Key::Char('Ж')]);
assert_eq!(text(&mut decoder, &[0x8c, 0xf0, 0x9f]), [Key::Char('界')]);
assert_eq!(text(&mut decoder, &[0x98, 0x80]), [Key::Char('😀')]);
assert_eq!(text(&mut decoder, &[0xff, b'a']), [Key::Char('a')]);
}
#[test]
fn terminal_navigation_is_ordered_with_text() {
let mut decoder = TextDecoder::default();
assert_eq!(
text(&mut decoder, b"\x1b[A\x1b[6~\x1bOH\x1b[[A\r\n\t\x7f\x08"),
[
Key::Up,
Key::PageDown,
Key::Home,
Key::Select,
Key::Select,
Key::Backspace,
Key::Backspace
]
);
assert_eq!(text(&mut decoder, b"\x1b[1;"), []);
assert_eq!(
text(&mut decoder, b"5D\x06a b"),
[
Key::Left,
Key::Search,
Key::Char('a'),
Key::Char(' '),
Key::Char('b')
]
);
assert_eq!(text(&mut decoder, &[0xe7, 0x95]), []);
assert_eq!(text(&mut decoder, &[0x8c, b'\r']), [Key::Char('界'), Key::Select]);
}
#[test]
fn terminal_escape_timeout_preserves_later_text() {
let mut decoder = TextDecoder::default();
assert_eq!(text(&mut decoder, b"\x1b"), []);
assert_eq!(decoder.expire_escape(), Some(Key::Back));
assert_eq!(
text(&mut decoder, b"Orange"),
"Orange".chars().map(Key::Char).collect::<Vec<_>>()
);
assert_eq!(text(&mut decoder, b"\x1b["), []);
assert_eq!(decoder.expire_escape(), None);
assert_eq!(text(&mut decoder, b"a"), [Key::Char('a')]);
}
#[test]
fn terminal_maps_common_navigation_sequences() {
let mut decoder = TextDecoder::default();
for (sequence, key) in [
("\x1b[A", Key::Up),
("\x1b[B", Key::Down),
("\x1b[C", Key::Right),
("\x1b[D", Key::Left),
("\x1bOA", Key::Up),
("\x1bOB", Key::Down),
("\x1bOC", Key::Right),
("\x1bOD", Key::Left),
("\x1b[H", Key::Home),
("\x1bOH", Key::Home),
("\x1b[1~", Key::Home),
("\x1b[7~", Key::Home),
("\x1b[F", Key::End),
("\x1bOF", Key::End),
("\x1b[4~", Key::End),
("\x1b[8~", Key::End),
("\x1b[5~", Key::PageUp),
("\x1b[6~", Key::PageDown),
("\x1b[6;2~", Key::PageDown),
("\x1bOS", Key::Info),
("\x1b[[D", Key::Info),
("\x1b[14~", Key::Info),
] {
assert_eq!(text(&mut decoder, sequence.as_bytes()), [key], "{sequence:?}");
}
assert_eq!(text(&mut decoder, b"\x1b[3~\x1bOP\x1b[[A\x1b[200~"), []);
}
#[test]
fn terminal_owned_keys_are_suppressed_only_in_combined_reader() {
for code in [1, 14, 28, 57, 62, 96, 102, 103, 104, 105, 106, 107, 108, 109] {
assert!(terminal_keycode(code));
assert!(evdev_key(EV_KEY, code, 1).is_some());
}
for code in [158, 164, 207, 217, 370, 392, 402] {
assert!(!terminal_keycode(code));
assert!(evdev_key(EV_KEY, code, 1).is_some());
}
}
#[test]
fn cec_ui_maps() {
assert_eq!(cec_ui_key(0x01), Some(Key::Up));
assert_eq!(cec_ui_key(0x00), Some(Key::Select));
assert_eq!(cec_ui_key(0x49), Some(Key::Next));
assert_eq!(cec_ui_key(0x4b), Some(Key::SkipNext));
assert_eq!(cec_ui_key(0xff), None);
}
#[test]
fn cec_msg_abi_size() {
assert_eq!(size_of::<CecMsg>(), 56);
assert_eq!(CEC_S_MODE, 0x4004_6109);
assert_eq!(CEC_RECEIVE, 0xC038_6106);
assert_eq!(size_of::<CecLogAddrs>(), 92);
assert_eq!(CEC_ADAP_G_PHYS_ADDR, 0x8002_6101);
assert_eq!(CEC_ADAP_G_LOG_ADDRS, 0x805C_6103);
assert_eq!(CEC_ADAP_S_LOG_ADDRS, 0xC05C_6104);
assert_eq!(CEC_TRANSMIT, 0xC038_6105);
}
}