Skip to main content

waos_dev_protocol/
messages.rs

1//! What `cargo waos` says to a WaOS, and what a WaOS says back, about one run; what `waos open`
2//! says, files to open with a program, and the answer; and what `cargo waos` asks of the windows of
3//! a program a run of its own runs, a screenshot or an input, and the answer (the design's decision
4//! 60).
5//!
6//! The invariant: a variant's place and a field's place is its encoding (the crate's doc). A
7//! program's output is bytes, not lines, so a terminal shows what the program wrote as it wrote it,
8//! a prompt without its newline among it. Bytes are encoded as a string of bytes (`serde_bytes`),
9//! which postcard writes as it writes a `Vec<u8>`, its length and then its bytes, and reads in one
10//! copy rather than one call a byte: a debug build otherwise took about 0.2 s to send forever's 7
11//! MB. A picture is a PNG's bytes, encoded the same way. Text typed into a program is left out of
12//! what a message's `Debug` writes, since messages are traced and it may be a password.
13
14use std::fmt;
15use std::path::PathBuf;
16
17use serde::{Deserialize, Serialize};
18use waos_package::ProgramId;
19
20/// What `cargo waos` says to a WaOS.
21#[derive(Clone, PartialEq, Serialize, Deserialize)]
22pub enum ToWaOS {
23    /// Run this component as the program `id` names, from its `[package.metadata.waos]`, with
24    /// that program's settings and folders, installed or not; the program running under that
25    /// id is stopped first.
26    Run {
27        id: ProgramId,
28        #[serde(with = "serde_bytes")]
29        component: Vec<u8>,
30    },
31    /// Open these files, absolute paths on this machine, with the program `id` names, as its
32    /// user would open them with it: the program running under that id, or the installed one,
33    /// started. Answered `Opened` or `Refused`, after which the connection ends.
34    Open { id: ProgramId, files: Vec<PathBuf> },
35    /// The last picture of each window of the program `id` names, while a run of `cargo waos
36    /// run` runs it. Answered `Screenshots`, or `Unreached`, after which the connection ends.
37    Screenshot { id: ProgramId },
38    /// `input` to the program `id` names, as its user would give it, in its window numbered
39    /// `window`, from 1 in the order `Screenshots` lists them, while a run of `cargo waos run` runs
40    /// it. Answered `InputDone` once the window shows its next picture, or `Unreached`, after
41    /// which the connection ends.
42    Input {
43        id: ProgramId,
44        window: u32,
45        input: Input,
46    },
47}
48
49/// Input to a program's window, as its user would give it. A point is in the window's physical
50/// pixels, a screenshot's, from its top-left.
51#[derive(Clone, PartialEq, Serialize, Deserialize)]
52pub enum Input {
53    /// A press and release at a point: a mouse's primary button, on a desktop.
54    Tap { x: f64, y: f64 },
55    /// Text, as typing it enters it into what the window edits.
56    Text(String),
57    /// A key, or a chord of keys pressed together, by name: `enter`, `escape`, `cmd+s`.
58    Key(String),
59    /// A scroll at a point, by `dx` and `dy` physical pixels, a mouse wheel's: a positive `dy`
60    /// scrolls towards the content's end, down.
61    Scroll { x: f64, y: f64, dx: f64, dy: f64 },
62}
63
64/// What a WaOS says back, about the run, the opening, the screenshot or the input the connection
65/// asked for.
66#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
67pub enum FromWaOS {
68    Started,
69    /// Bytes the program wrote, as it wrote them: a terminal shows them as they come.
70    Output {
71        stream: OutputStream,
72        #[serde(with = "serde_bytes")]
73        bytes: Vec<u8>,
74    },
75    Ended(RunEnd),
76    /// It could not be loaded or run, or the files could not be opened with it, and why.
77    Refused(String),
78    /// The files were handed to the program, which hears them once it listens.
79    Opened,
80    /// The last picture of each of the program's windows, in the order they opened.
81    Screenshots(Vec<WindowPicture>),
82    /// The program heard the input. `new_picture` says whether the window then showed a picture
83    /// the program drew after it, within the time WaOS waits for one: none comes for an input
84    /// that changed nothing, nor while the window is out of sight.
85    InputDone {
86        new_picture: bool,
87    },
88    /// The screenshot or the input reached no window, and why.
89    Unreached(Unreached),
90}
91
92/// One of a program's windows, as its user sees it: what `cargo waos screenshot` writes.
93#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
94pub struct WindowPicture {
95    /// The window's number, from 1 in the order the program's windows opened: what an input's
96    /// `window` names.
97    pub window: u32,
98    /// The title the program gave the window; empty for one it left untitled, which shows the
99    /// program's name, and for a view in no window.
100    pub title: String,
101    /// The size of the window's picture, its view's before it has one, in physical pixels: what
102    /// an input's point is in, from the top-left.
103    pub width: u32,
104    pub height: u32,
105    /// Whether the window is in sight. One minimized, hidden or covered all over shows no new
106    /// picture until it is in sight again, and keeps showing its last.
107    pub in_sight: bool,
108    /// The last picture the program presented in the window, as a PNG, its drawing alone, without
109    /// the composition layers beneath it; none before it presented one.
110    #[serde(with = "serde_bytes")]
111    pub png: Option<Vec<u8>>,
112}
113
114/// Why a screenshot or an input reached no window of the program.
115#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
116pub enum Unreached {
117    /// No run of `cargo waos run` runs the program: nothing runs under its id, or the program
118    /// running was opened otherwise, from WaOS's library, which no tool reads or drives.
119    NotRunByCargoWaos,
120    /// The program has no window numbered `window`: it has `windows`.
121    NoWindow { window: u32, windows: u32 },
122    /// The point is outside the window, which is `width` by `height` physical pixels.
123    OutsideWindow { width: u32, height: u32 },
124    /// Nothing in the window is being edited, so text typed there has nowhere to go.
125    NotEditing,
126    /// No key has this name, or the chord names one no key has.
127    UnknownKey(String),
128}
129
130/// Which of a program's two output streams some bytes came from.
131#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
132pub enum OutputStream {
133    Stdout,
134    Stderr,
135}
136
137/// How the run ended.
138#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
139pub enum RunEnd {
140    /// Its `main` returned; `success` is false for a run that reported failure.
141    Returned { success: bool },
142    /// It trapped, or could not run: the error.
143    Failed(String),
144    /// WaOS stopped it, as when the app quits.
145    Stopped,
146    /// A later run of the same name took its place.
147    Replaced,
148}
149
150/// A component is megabytes, and typed text may be a password: an event that prints the message
151/// says how many bytes and characters, not which.
152impl fmt::Debug for ToWaOS {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        match self {
155            ToWaOS::Run { id, component } => f
156                .debug_struct("Run")
157                .field("id", &format_args!("{id}"))
158                .field("component", &format_args!("{} bytes", component.len()))
159                .finish(),
160            ToWaOS::Open { id, files } => f
161                .debug_struct("Open")
162                .field("id", &format_args!("{id}"))
163                .field("files", files)
164                .finish(),
165            ToWaOS::Screenshot { id } => f
166                .debug_struct("Screenshot")
167                .field("id", &format_args!("{id}"))
168                .finish(),
169            ToWaOS::Input { id, window, input } => f
170                .debug_struct("Input")
171                .field("id", &format_args!("{id}"))
172                .field("window", window)
173                .field("input", input)
174                .finish(),
175        }
176    }
177}
178
179impl fmt::Debug for Input {
180    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181        match self {
182            Input::Tap { x, y } => f.debug_struct("Tap").field("x", x).field("y", y).finish(),
183            Input::Text(text) => f
184                .debug_tuple("Text")
185                .field(&format_args!("{} characters", text.chars().count()))
186                .finish(),
187            Input::Key(key) => f.debug_tuple("Key").field(key).finish(),
188            Input::Scroll { x, y, dx, dy } => f
189                .debug_struct("Scroll")
190                .field("x", x)
191                .field("y", y)
192                .field("dx", dx)
193                .field("dy", dy)
194                .finish(),
195        }
196    }
197}
198
199/// A picture is megabytes: an event that prints it says how many.
200impl fmt::Debug for WindowPicture {
201    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202        f.debug_struct("WindowPicture")
203            .field("window", &self.window)
204            .field("title", &self.title)
205            .field("width", &self.width)
206            .field("height", &self.height)
207            .field("in_sight", &self.in_sight)
208            .field("png_bytes", &self.png.as_ref().map(Vec::len))
209            .finish()
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    /// The messages as they were encoded before their bytes were a string of bytes: each byte an
218    /// element of a sequence.
219    #[derive(Debug, PartialEq, Serialize, Deserialize)]
220    enum ToWaOSAsSequence {
221        Run { id: ProgramId, component: Vec<u8> },
222    }
223
224    #[derive(Debug, PartialEq, Serialize, Deserialize)]
225    enum FromWaOSAsSequence {
226        Started,
227        Output {
228            stream: OutputStream,
229            bytes: Vec<u8>,
230        },
231    }
232
233    /// Every byte, more than once, so the length takes more than one byte of its own.
234    fn bytes() -> Vec<u8> {
235        (0..=u8::MAX).cycle().take(1000).collect()
236    }
237
238    #[test]
239    fn a_run_is_encoded_as_it_was_when_its_component_was_a_sequence() {
240        let id: ProgramId = "dev.example.triangle".parse().unwrap();
241        let run = ToWaOS::Run {
242            id: id.clone(),
243            component: bytes(),
244        };
245        let as_sequence = ToWaOSAsSequence::Run {
246            id,
247            component: bytes(),
248        };
249        let encoded = postcard::to_allocvec(&run).unwrap();
250        assert_eq!(encoded, postcard::to_allocvec(&as_sequence).unwrap());
251        assert_eq!(
252            postcard::from_bytes::<ToWaOSAsSequence>(&encoded).unwrap(),
253            as_sequence
254        );
255    }
256
257    #[test]
258    fn output_is_encoded_as_it_was_when_its_bytes_were_a_sequence() {
259        let output = FromWaOS::Output {
260            stream: OutputStream::Stderr,
261            bytes: bytes(),
262        };
263        let as_sequence = FromWaOSAsSequence::Output {
264            stream: OutputStream::Stderr,
265            bytes: bytes(),
266        };
267        let encoded = postcard::to_allocvec(&output).unwrap();
268        assert_eq!(encoded, postcard::to_allocvec(&as_sequence).unwrap());
269        assert_eq!(postcard::from_bytes::<FromWaOS>(&encoded).unwrap(), output);
270    }
271
272    #[test]
273    fn an_opening_reads_back_as_written_and_prints_its_files() {
274        let open = ToWaOS::Open {
275            id: "dev.example.notes".parse().unwrap(),
276            files: vec![PathBuf::from("/Users/me/notes.md")],
277        };
278        let encoded = postcard::to_allocvec(&open).unwrap();
279        assert_eq!(postcard::from_bytes::<ToWaOS>(&encoded).unwrap(), open);
280        assert_eq!(
281            format!("{open:?}"),
282            "Open { id: dev.example.notes, files: [\"/Users/me/notes.md\"] }"
283        );
284        let opened = postcard::to_allocvec(&FromWaOS::Opened).unwrap();
285        assert_eq!(
286            postcard::from_bytes::<FromWaOS>(&opened).unwrap(),
287            FromWaOS::Opened
288        );
289    }
290
291    #[test]
292    fn a_run_prints_its_component_as_a_count() {
293        let run = ToWaOS::Run {
294            id: "dev.example.triangle".parse().unwrap(),
295            component: vec![0; 1024],
296        };
297        assert_eq!(
298            format!("{run:?}"),
299            "Run { id: dev.example.triangle, component: 1024 bytes }"
300        );
301    }
302
303    #[test]
304    fn a_screenshot_and_each_input_read_back_as_written() {
305        let id: ProgramId = "dev.example.notes".parse().unwrap();
306        let inputs = [
307            Input::Tap { x: 120.5, y: 48.0 },
308            Input::Text("milk, eggs".into()),
309            Input::Key("cmd+s".into()),
310            Input::Scroll {
311                x: 10.0,
312                y: 20.0,
313                dx: 0.0,
314                dy: -200.0,
315            },
316        ];
317        let messages = std::iter::once(ToWaOS::Screenshot { id: id.clone() }).chain(
318            inputs.into_iter().map(|input| ToWaOS::Input {
319                id: id.clone(),
320                window: 2,
321                input,
322            }),
323        );
324        for message in messages {
325            let encoded = postcard::to_allocvec(&message).unwrap();
326            assert_eq!(postcard::from_bytes::<ToWaOS>(&encoded).unwrap(), message);
327        }
328    }
329
330    #[test]
331    fn typed_text_is_left_out_of_what_a_message_prints() {
332        let input = ToWaOS::Input {
333            id: "dev.example.notes".parse().unwrap(),
334            window: 1,
335            input: Input::Text("hunter2".into()),
336        };
337        let printed = format!("{input:?}");
338        assert!(!printed.contains("hunter2"), "{printed}");
339        assert!(printed.contains("7 characters"), "{printed}");
340    }
341
342    #[test]
343    fn a_picture_prints_its_png_as_a_count() {
344        let picture = WindowPicture {
345            window: 1,
346            title: "Notes".into(),
347            width: 1600,
348            height: 1200,
349            in_sight: true,
350            png: Some(vec![0; 2048]),
351        };
352        let printed = format!("{:?}", FromWaOS::Screenshots(vec![picture]));
353        assert!(printed.contains("png_bytes: Some(2048)"), "{printed}");
354        assert!(!printed.contains("0, 0"), "{printed}");
355    }
356}