1use serde::{Deserialize, Serialize};
3
4type Result<T> = std::result::Result<T, String>;
5
6pub const MAX_CHARS: usize = 256;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12pub enum Source {
13 #[serde(rename = "K")]
14 Keyboard,
15 #[serde(rename = "M")]
16 Mouse,
17 #[serde(rename = "F")]
18 Fsys,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
22pub enum Kind {
23 #[serde(rename = "I")]
24 InsertBody,
25 #[serde(rename = "D")]
26 DeleteBody,
27 #[serde(rename = "X")]
28 ExecuteBody,
29 #[serde(rename = "L")]
30 LoadBody,
31 #[serde(rename = "i")]
32 InsertScratch,
33 #[serde(rename = "d")]
34 DeleteScratch,
35 #[serde(rename = "x")]
36 ExecuteScratch,
37 #[serde(rename = "l")]
38 LoadScratch,
39 #[serde(rename = "A")]
40 ChordedArgument,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44pub struct FsysEvent {
45 pub source: Source,
46 pub kind: Kind,
47 pub ch_from: usize,
48 pub ch_to: usize,
49 pub truncated: bool,
50 pub txt: String,
51}
52
53impl FsysEvent {
54 pub fn new(source: Source, kind: Kind, ch_from: usize, ch_to: usize, raw_txt: &str) -> Self {
59 let (txt, truncated) = match kind {
60 Kind::DeleteScratch | Kind::DeleteBody => (String::new(), true),
61 _ => {
62 let txt = raw_txt.chars().take(MAX_CHARS).collect();
63 let truncated = txt != raw_txt;
64
65 (txt, truncated)
66 }
67 };
68
69 Self {
70 source,
71 kind,
72 ch_from,
73 ch_to,
74 truncated,
75 txt,
76 }
77 }
78
79 pub fn as_event_file_line(&self) -> String {
80 format!("{}\n", serde_json::to_string(self).unwrap())
81 }
82
83 pub fn try_from_str(s: &str) -> Result<Self> {
84 let evt: Self =
85 serde_json::from_str(s.trim()).map_err(|e| format!("invalid event: {e}"))?;
86 if evt.txt.chars().count() > MAX_CHARS {
87 return Err(format!("txt field too long: max chars = {MAX_CHARS}"));
88 }
89
90 Ok(evt)
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use simple_test_case::test_case;
98
99 fn evt(s: &str) -> FsysEvent {
100 let n_chars = s.chars().count();
101 FsysEvent::new(Source::Keyboard, Kind::InsertBody, 17, 17 + n_chars, s)
102 }
103
104 #[test]
105 fn as_event_file_line_works() {
106 let line = evt("a").as_event_file_line();
107 assert_eq!(
108 line,
109 "{\"source\":\"K\",\"kind\":\"I\",\"ch_from\":17,\"ch_to\":18,\"truncated\":false,\"txt\":\"a\"}\n"
110 );
111 }
112
113 #[test]
114 fn as_event_file_line_works_for_newline() {
115 let line = evt("\n").as_event_file_line();
116 assert_eq!(
117 line,
118 "{\"source\":\"K\",\"kind\":\"I\",\"ch_from\":17,\"ch_to\":18,\"truncated\":false,\"txt\":\"\\n\"}\n"
119 );
120 }
121
122 #[test]
123 fn txt_length_is_truncated_in_new() {
124 let long_txt = "a".repeat(MAX_CHARS + 10);
125 let e = FsysEvent::new(Source::Keyboard, Kind::InsertBody, 17, 283, &long_txt);
126 assert!(e.truncated);
127 }
128
129 #[test_case(Kind::DeleteBody; "delete in body")]
130 #[test_case(Kind::DeleteScratch; "delete in tag")]
131 #[test]
132 fn txt_is_removed_for_delete_events_if_provided(kind: Kind) {
133 let e = FsysEvent::new(Source::Keyboard, kind, 42, 42 + 17, "some deleted text");
134 assert!(e.truncated);
135 assert!(e.txt.is_empty());
136 }
137
138 #[test]
139 fn txt_length_is_checked_on_parse() {
140 let long_txt = "a".repeat(MAX_CHARS + 10);
141 let line = format!("K I 17 283 266 | {long_txt}");
142 let res = FsysEvent::try_from_str(&line);
143 assert!(res.is_err(), "expected error, got {res:?}");
144 }
145
146 #[test_case("a"; "single char")]
147 #[test_case("testing"; "multi char")]
148 #[test_case("testing testing 1 2 3"; "multi char with spaces")]
149 #[test_case("Hello, 世界"; "multi char with spaces and multi byte chars")]
150 #[test_case("testing testing\n1 2 3"; "multi char with spaces and internal newline")]
151 #[test_case("testing testing 1 2 3\n"; "multi char with spaces and trailing newline")]
152 #[test]
153 fn round_trip_single_works(s: &str) {
154 let e = evt(s);
155 let line = e.as_event_file_line();
156 let parsed = FsysEvent::try_from_str(&line).expect("to parse");
157
158 assert_eq!(parsed, e);
159 }
160}