ad_event/
lib.rs

1//! A shared event message format between ad and clients
2use serde::{Deserialize, Serialize};
3
4type Result<T> = std::result::Result<T, String>;
5
6pub const MAX_CHARS: usize = 256;
7
8/// acme makes a distinction between direct writes to /body and /tag vs
9/// text entering the buffer via one of the other fsys files but I'm not
10/// sure if I need that initially? As and when it looks useful I can add it.
11#[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    /// Construct a new [FsysEvent].
55    ///
56    /// The `txt` field of events is limited to [MAX_CHARS] or up until the first newline character
57    /// and will be truncated if larger. Delete events are always truncated to zero length.
58    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}