Skip to main content

tattoy_protocol/
lib.rs

1//! These are all the types that a plugin needs to function.
2
3#![expect(clippy::pub_use, reason = "This seems to come from the `bon` crate")]
4
5/// An RGBA colour.
6pub type Colour = (f32, f32, f32, f32);
7
8/// A cell represents a single character in the terminal.
9///
10/// It can be sent from Tattoy to communicate the contents of the user's terminal.
11/// And it can also be sent from a plugin to communicate the contents to be composited
12/// in a Tattoy layer.
13#[derive(serde::Serialize, serde::Deserialize, bon::Builder, Clone, Copy, Debug)]
14#[non_exhaustive]
15pub struct Cell {
16    /// The cell's character.
17    pub character: char,
18    /// The coordinates of the cell. [0, 0] is in the top-left.
19    pub coordinates: (u32, u32),
20    /// An optional colour for the cell's background. If `None` (or `null` in the case of JSON) is
21    /// used then the terminal's default background colour will be used.
22    pub bg: Option<Colour>,
23    /// An optional colour for the cell's foreground. If `None` (or `null` in the case of JSON) is
24    /// used then the terminal's default foreground colour will be used.
25    pub fg: Option<Colour>,
26}
27
28/// Output from the plugin that renders pixels in the terminal.
29#[derive(serde::Serialize, serde::Deserialize, bon::Builder, Clone, Copy, Debug)]
30#[non_exhaustive]
31pub struct Pixel {
32    /// The coordinates of the pixel. [0, 0] is in the top-left. The y-axis is twice as long as the
33    /// number of rows in the terminal because 2 "pixels" can fit in a single TTY cell using the
34    /// UTF8 half-block trick: ▀▄▀▄
35    pub coordinates: (u32, u32),
36    /// An optional colour for the pixel. If `None` (or `null` in the case of JSON) is used then
37    /// the default foreground colour is used.
38    pub color: Option<Colour>,
39}
40
41/// The various kinds of messages that Tattoy can send to the plugin.
42#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
43#[serde(rename_all = "snake_case")]
44#[non_exhaustive]
45pub enum PluginInputMessages {
46    /// The current contents of the PTY screen. It does not contain any of the scrollback.
47    #[serde(rename = "pty_update")]
48    PTYUpdate {
49        /// The size of terminal in colums and rows.
50        size: (u16, u16),
51        /// All the cell data for the current terminal. Blank cells are not included.
52        cells: Vec<Cell>,
53        /// The current position of the cursor.
54        cursor: (u16, u16),
55    },
56    /// Sent whenever the terminal resizes.
57    #[serde(rename = "tty_resize")]
58    TTYResize {
59        /// The number of columns in the new terminal size.
60        width: u16,
61        /// The number of rows in the new terminal size.
62        height: u16,
63    },
64}
65
66/// All the message kinds that the plugin can send to Tattoy.
67#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
68#[serde(rename_all = "snake_case")]
69#[non_exhaustive]
70pub enum PluginOutputMessages {
71    /// Output from the plugin that renders text of arbitrary length in the terminal.
72    OutputText {
73        /// The text to display.
74        text: String,
75        /// The coordinates. [0, 0] is in the top-left.
76        coordinates: (u32, u32),
77        /// An optional colour for the text's background.
78        bg: Option<Colour>,
79        /// An optional colour for the text's foreground.
80        fg: Option<Colour>,
81    },
82
83    /// Output an arbitrary amount of cells to the terminal. It does not need to include blank
84    /// cells.
85    OutputCells(Vec<Cell>),
86
87    /// Output from the plugin that renders pixels in the terminal.
88    OutputPixels(Vec<Pixel>),
89}
90
91#[expect(clippy::default_numeric_fallback, reason = "Tests aren't so strict")]
92#[cfg(test)]
93mod test {
94    use super::*;
95
96    #[test]
97    fn output_text() {
98        let expected = serde_json::json!(
99            {
100                "output_text": {
101                    "text": "foo",
102                    "coordinates": [1, 2],
103                    "bg": null,
104                    "fg": [0.1, 0.2, 0.3, 0.4],
105                }
106            }
107        );
108
109        let output = PluginOutputMessages::OutputText {
110            text: "foo".to_owned(),
111            coordinates: (1, 2),
112            bg: None,
113            fg: Some((0.1, 0.2, 0.3, 0.4)),
114        };
115
116        assert_eq!(
117            expected.to_string(),
118            serde_json::to_string(&output).unwrap()
119        );
120    }
121
122    #[test]
123    fn output_cells() {
124        let expected = serde_json::json!(
125            {
126                "output_cells": [{
127                    "character": "f",
128                    "coordinates": [1, 2],
129                    "bg": null,
130                    "fg": [0.1, 0.2, 0.3, 0.4],
131                }]
132            }
133        );
134
135        let output = PluginOutputMessages::OutputCells(vec![Cell {
136            character: 'f',
137            coordinates: (1, 2),
138            bg: None,
139            fg: Some((0.1, 0.2, 0.3, 0.4)),
140        }]);
141
142        assert_eq!(
143            expected.to_string(),
144            serde_json::to_string(&output).unwrap()
145        );
146    }
147
148    #[test]
149    fn output_pixels() {
150        let expected = serde_json::json!(
151            {
152                "output_pixels": [{
153                    "coordinates": [1, 2],
154                    "color": [0.1, 0.2, 0.3, 0.4],
155                }]
156            }
157        );
158
159        let output = PluginOutputMessages::OutputPixels(vec![Pixel {
160            coordinates: (1, 2),
161            color: Some((0.1, 0.2, 0.3, 0.4)),
162        }]);
163
164        assert_eq!(
165            expected.to_string(),
166            serde_json::to_string(&output).unwrap()
167        );
168    }
169
170    #[test]
171    fn input_pty_update() {
172        let expected = serde_json::json!(
173            {
174                "pty_update": {
175                    "size": [1, 2],
176                    "cells": [{
177                        "character": "f",
178                        "coordinates": [1, 2],
179                        "bg": null,
180                        "fg": [0.1, 0.2, 0.3, 0.4],
181                    }],
182                    "cursor": [9, 10],
183                }
184            }
185        );
186
187        let output = PluginInputMessages::PTYUpdate {
188            size: (1, 2),
189            cells: vec![Cell {
190                character: 'f',
191                coordinates: (1, 2),
192                bg: None,
193                fg: Some((0.1, 0.2, 0.3, 0.4)),
194            }],
195            cursor: (9, 10),
196        };
197
198        assert_eq!(
199            expected.to_string(),
200            serde_json::to_string(&output).unwrap()
201        );
202    }
203
204    #[test]
205    fn input_tty_resize() {
206        let expected = serde_json::json!(
207            {
208                "tty_resize": {
209                    "width": 1,
210                    "height": 2,
211                }
212            }
213        );
214
215        let output = PluginInputMessages::TTYResize {
216            width: 1,
217            height: 2,
218        };
219
220        assert_eq!(
221            expected.to_string(),
222            serde_json::to_string(&output).unwrap()
223        );
224    }
225}