1use std::io::Write;
26use std::sync::OnceLock;
27use std::time::{SystemTime, UNIX_EPOCH};
28
29use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
30
31fn target() -> Option<&'static std::path::PathBuf> {
33 static TARGET: OnceLock<Option<std::path::PathBuf>> = OnceLock::new();
34 TARGET
35 .get_or_init(|| {
36 let value = std::env::var("RPI_DEBUG_KEYS").ok()?;
37 let value = value.trim();
38 if value.is_empty() || value == "0" {
39 return None;
40 }
41 if value == "1" || value.eq_ignore_ascii_case("true") {
42 Some(std::env::temp_dir().join("rpi-keys.log"))
43 } else {
44 Some(std::path::PathBuf::from(value))
45 }
46 })
47 .as_ref()
48}
49
50pub fn enabled() -> bool {
52 target().is_some()
53}
54
55pub fn path() -> Option<&'static std::path::Path> {
57 target().map(std::path::PathBuf::as_path)
58}
59
60pub fn key(event: &KeyEvent, note: &str) {
62 let Some(path) = target() else {
63 return;
64 };
65 let mut line = format!(
66 "{} code={} mods={} kind={:?}",
67 timestamp(),
68 code_name(event.code),
69 modifiers_name(event.modifiers),
70 event.kind
71 );
72 if !note.is_empty() {
73 line.push(' ');
74 line.push_str(note);
75 }
76 line.push('\n');
77 append(path, &line);
78}
79
80pub fn note(text: &str) {
82 let Some(path) = target() else {
83 return;
84 };
85 append(path, &format!("{} {text}\n", timestamp()));
86}
87
88pub fn paste_note(note: impl FnOnce() -> String) {
92 static TARGET: OnceLock<Option<std::path::PathBuf>> = OnceLock::new();
93 let path = TARGET.get_or_init(|| {
94 let value = std::env::var("RPI_DEBUG_PASTE").ok()?;
95 let value = value.trim();
96 let path = if value.is_empty() || value == "0" {
97 return None;
98 } else if value == "1" || value.eq_ignore_ascii_case("true") {
99 std::env::temp_dir().join("rpi-paste.log")
100 } else {
101 std::path::PathBuf::from(value)
102 };
103 append(
104 &path,
105 &format!(
106 "{} paste run pid={} version={}\n",
107 timestamp(),
108 std::process::id(),
109 env!("CARGO_PKG_VERSION")
110 ),
111 );
112 Some(path)
113 });
114 if let Some(path) = path {
115 append(path, &format!("{} paste {}\n", timestamp(), note()));
116 }
117}
118
119fn append(path: &std::path::Path, line: &str) {
120 if let Ok(mut file) = std::fs::OpenOptions::new()
121 .create(true)
122 .append(true)
123 .open(path)
124 {
125 let _ = file.write_all(line.as_bytes());
126 }
127}
128
129fn timestamp() -> String {
131 let now = SystemTime::now()
132 .duration_since(UNIX_EPOCH)
133 .unwrap_or_default();
134 let millis = now.as_millis();
135 let secs = (millis / 1000) as i64;
136 let sub = millis % 1000;
137 let (year, month, day, hour, min, sec) = civil_from_unix(secs);
138 format!("{year:04}-{month:02}-{day:02}T{hour:02}:{min:02}:{sec:02}.{sub:03}Z")
139}
140
141fn civil_from_unix(secs: i64) -> (i64, u32, u32, u32, u32, u32) {
144 let days = secs.div_euclid(86_400);
145 let secs_of_day = secs.rem_euclid(86_400);
146 let z = days + 719_468;
147 let era = z.div_euclid(146_097);
148 let doe = z.rem_euclid(146_097);
149 let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
150 let y = yoe + era * 400;
151 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
152 let mp = (5 * doy + 2) / 153;
153 let d = doy - (153 * mp + 2) / 5 + 1;
154 let m = if mp < 10 { mp + 3 } else { mp - 9 };
155 let y = if m <= 2 { y + 1 } else { y };
156 (
157 y,
158 m as u32,
159 d as u32,
160 (secs_of_day / 3600) as u32,
161 ((secs_of_day % 3600) / 60) as u32,
162 (secs_of_day % 60) as u32,
163 )
164}
165
166fn code_name(code: KeyCode) -> String {
169 match code {
170 KeyCode::Char(c) if c.is_control() => {
171 format!("Char({c:?}) [U+{:04X}]", c as u32)
172 }
173 KeyCode::Char(c) => format!("Char({c:?})"),
174 other => format!("{other:?}"),
175 }
176}
177
178fn modifiers_name(modifiers: KeyModifiers) -> String {
179 if modifiers.is_empty() {
180 return "NONE".to_string();
181 }
182 let mut parts = Vec::new();
183 for (flag, name) in [
184 (KeyModifiers::CONTROL, "CONTROL"),
185 (KeyModifiers::SHIFT, "SHIFT"),
186 (KeyModifiers::ALT, "ALT"),
187 (KeyModifiers::SUPER, "SUPER"),
188 (KeyModifiers::HYPER, "HYPER"),
189 (KeyModifiers::META, "META"),
190 ] {
191 if modifiers.contains(flag) {
192 parts.push(name);
193 }
194 }
195 parts.join("|")
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
203 fn timestamp_matches_the_unix_epoch() {
204 assert_eq!(civil_from_unix(0), (1970, 1, 1, 0, 0, 0));
205 assert_eq!(civil_from_unix(1_700_000_000), (2023, 11, 14, 22, 13, 20));
206 }
207
208 #[test]
209 fn control_characters_and_modifiers_are_spelled_out() {
210 assert_eq!(code_name(KeyCode::Char('j')), "Char('j')");
211 assert_eq!(code_name(KeyCode::Char('\n')), "Char('\\n') [U+000A]");
212 assert_eq!(code_name(KeyCode::Enter), "Enter");
213 assert_eq!(
214 modifiers_name(KeyModifiers::CONTROL | KeyModifiers::SHIFT),
215 "CONTROL|SHIFT"
216 );
217 assert_eq!(modifiers_name(KeyModifiers::NONE), "NONE");
218 }
219}