use std::io::Write;
use std::sync::OnceLock;
use std::time::{SystemTime, UNIX_EPOCH};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
fn target() -> Option<&'static std::path::PathBuf> {
static TARGET: OnceLock<Option<std::path::PathBuf>> = OnceLock::new();
TARGET
.get_or_init(|| {
let value = std::env::var("RPI_DEBUG_KEYS").ok()?;
let value = value.trim();
if value.is_empty() || value == "0" {
return None;
}
if value == "1" || value.eq_ignore_ascii_case("true") {
Some(std::env::temp_dir().join("rpi-keys.log"))
} else {
Some(std::path::PathBuf::from(value))
}
})
.as_ref()
}
pub fn enabled() -> bool {
target().is_some()
}
pub fn path() -> Option<&'static std::path::Path> {
target().map(std::path::PathBuf::as_path)
}
pub fn key(event: &KeyEvent, note: &str) {
let Some(path) = target() else {
return;
};
let mut line = format!(
"{} code={} mods={} kind={:?}",
timestamp(),
code_name(event.code),
modifiers_name(event.modifiers),
event.kind
);
if !note.is_empty() {
line.push(' ');
line.push_str(note);
}
line.push('\n');
append(path, &line);
}
pub fn note(text: &str) {
let Some(path) = target() else {
return;
};
append(path, &format!("{} {text}\n", timestamp()));
}
fn append(path: &std::path::Path, line: &str) {
if let Ok(mut file) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
{
let _ = file.write_all(line.as_bytes());
}
}
fn timestamp() -> String {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
let millis = now.as_millis();
let secs = (millis / 1000) as i64;
let sub = millis % 1000;
let (year, month, day, hour, min, sec) = civil_from_unix(secs);
format!("{year:04}-{month:02}-{day:02}T{hour:02}:{min:02}:{sec:02}.{sub:03}Z")
}
fn civil_from_unix(secs: i64) -> (i64, u32, u32, u32, u32, u32) {
let days = secs.div_euclid(86_400);
let secs_of_day = secs.rem_euclid(86_400);
let z = days + 719_468;
let era = z.div_euclid(146_097);
let doe = z.rem_euclid(146_097);
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
(
y,
m as u32,
d as u32,
(secs_of_day / 3600) as u32,
((secs_of_day % 3600) / 60) as u32,
(secs_of_day % 60) as u32,
)
}
fn code_name(code: KeyCode) -> String {
match code {
KeyCode::Char(c) if c.is_control() => {
format!("Char({c:?}) [U+{:04X}]", c as u32)
}
KeyCode::Char(c) => format!("Char({c:?})"),
other => format!("{other:?}"),
}
}
fn modifiers_name(modifiers: KeyModifiers) -> String {
if modifiers.is_empty() {
return "NONE".to_string();
}
let mut parts = Vec::new();
for (flag, name) in [
(KeyModifiers::CONTROL, "CONTROL"),
(KeyModifiers::SHIFT, "SHIFT"),
(KeyModifiers::ALT, "ALT"),
(KeyModifiers::SUPER, "SUPER"),
(KeyModifiers::HYPER, "HYPER"),
(KeyModifiers::META, "META"),
] {
if modifiers.contains(flag) {
parts.push(name);
}
}
parts.join("|")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn timestamp_matches_the_unix_epoch() {
assert_eq!(civil_from_unix(0), (1970, 1, 1, 0, 0, 0));
assert_eq!(civil_from_unix(1_700_000_000), (2023, 11, 14, 22, 13, 20));
}
#[test]
fn control_characters_and_modifiers_are_spelled_out() {
assert_eq!(code_name(KeyCode::Char('j')), "Char('j')");
assert_eq!(code_name(KeyCode::Char('\n')), "Char('\\n') [U+000A]");
assert_eq!(code_name(KeyCode::Enter), "Enter");
assert_eq!(
modifiers_name(KeyModifiers::CONTROL | KeyModifiers::SHIFT),
"CONTROL|SHIFT"
);
assert_eq!(modifiers_name(KeyModifiers::NONE), "NONE");
}
}