tilepad_plugin_sdk/
lib.rs

1//! # Tilepad SDK
2//!
3//! ```no_run
4//! use tilepad_plugin_sdk::{Plugin, PluginSessionHandle, start_plugin, setup_tracing};
5//! use tokio::task::LocalSet;
6//!
7//! #[derive(Default)]
8//! struct MyPlugin {}
9//!
10//! impl Plugin for MyPlugin {
11//!     // TODO: Implement your desired methods
12//! }
13//!
14//! #[tokio::main(flavor = "current_thread")]
15//! async fn main() {
16//!     setup_tracing();
17//!
18//!     let local_set = LocalSet::new();
19//!     let plugin = MyPlugin::default();
20//!
21//!     local_set.run_until(start_plugin(plugin)).await;
22//! }
23//! ```
24
25use clap::Parser;
26use futures_util::StreamExt;
27use protocol::ServerPluginMessage;
28use session::PluginSessionRx;
29use subscription::Subscriptions;
30use tokio::join;
31use tokio_tungstenite::{connect_async, tungstenite::client::IntoClientRequest};
32
33use tracing_subscriber::EnvFilter;
34use ws::WebSocketFuture;
35
36// Provide tracing modules to the implementor
37pub use tracing;
38pub use tracing_subscriber;
39
40// Module re-exports
41pub use display::Display;
42pub use inspector::Inspector;
43pub use plugin::Plugin;
44pub use protocol::*;
45pub use session::{PluginSessionHandle, SessionError};
46
47mod display;
48mod inspector;
49mod plugin;
50mod protocol;
51mod session;
52mod subscription;
53mod ws;
54
55#[derive(Parser, Debug)]
56#[command(version, about, long_about = None)]
57struct Args {
58    /// ID of the plugin to connect as
59    #[arg(long)]
60    plugin_id: String,
61
62    /// Plugin server connection host URL
63    #[arg(long)]
64    connect_url: String,
65}
66
67pub async fn start_plugin<P>(plugin: P)
68where
69    P: Plugin,
70{
71    // Accept the command line arguments
72    let args = Args::parse();
73
74    // Connect to the server socket
75    let client_request = args
76        .connect_url
77        .into_client_request()
78        .expect("failed to create client request");
79    let (socket, _response) = connect_async(client_request)
80        .await
81        .expect("failed to connect to plugin server");
82
83    // Create and spawn a future for the websocket
84    let (ws_future, ws_rx, ws_tx) = WebSocketFuture::new(socket);
85
86    // Create message subscriptions store
87    let subscriptions = Subscriptions::default();
88
89    // Wrap the websocket handle with the custom protocol
90    let handle = PluginSessionHandle::new(ws_tx, subscriptions.clone());
91
92    // Send registration message
93    handle
94        .register(args.plugin_id)
95        .expect("failed to register plugin");
96
97    let msg_rx = PluginSessionRx::new(ws_rx);
98
99    let socket_future = run_websocket(ws_future);
100    let handle_future = run_handler(plugin, handle, subscriptions, msg_rx);
101
102    join!(socket_future, handle_future);
103}
104
105/// Helper to run the websocket and emit a log in the case of error
106async fn run_websocket(ws_future: WebSocketFuture) {
107    if let Err(cause) = ws_future.await {
108        tracing::error!(?cause, "error running device websocket future");
109    }
110}
111
112/// Handle all incoming messages from the websocket
113async fn run_handler<P>(
114    mut plugin: P,
115    handle: PluginSessionHandle,
116    subscriptions: Subscriptions,
117    mut msg_rx: PluginSessionRx,
118) where
119    P: Plugin,
120{
121    while let Some(msg) = msg_rx.next().await {
122        let msg = match msg {
123            Ok(value) => value,
124            Err(cause) => {
125                tracing::error!(?cause, "error processing server message");
126                return;
127            }
128        };
129
130        // Handle subscriptions
131        subscriptions.apply(&msg);
132
133        match msg {
134            ServerPluginMessage::Registered { .. } => {
135                handle
136                    .request_properties()
137                    .expect("failed to request initial properties");
138
139                plugin.on_registered(&handle);
140            }
141            ServerPluginMessage::Properties { properties } => {
142                plugin.on_properties(&handle, properties);
143            }
144            ServerPluginMessage::TileClicked { ctx, properties } => {
145                plugin.on_tile_clicked(&handle, ctx, properties);
146            }
147            ServerPluginMessage::RecvFromInspector { ctx, message } => {
148                plugin.on_inspector_message(
149                    &handle,
150                    Inspector {
151                        ctx,
152                        session: handle.clone(),
153                    },
154                    message,
155                );
156            }
157            ServerPluginMessage::RecvFromDisplay { ctx, message } => {
158                plugin.on_display_message(
159                    &handle,
160                    Display {
161                        ctx,
162                        session: handle.clone(),
163                    },
164                    message,
165                );
166            }
167            ServerPluginMessage::InspectorOpen { ctx } => {
168                plugin.on_inspector_open(
169                    &handle,
170                    Inspector {
171                        ctx,
172                        session: handle.clone(),
173                    },
174                );
175            }
176            ServerPluginMessage::InspectorClose { ctx } => {
177                plugin.on_inspector_close(
178                    &handle,
179                    Inspector {
180                        ctx,
181                        session: handle.clone(),
182                    },
183                );
184            }
185            ServerPluginMessage::DeepLink { ctx } => {
186                plugin.on_deep_link(&handle, ctx);
187            }
188            ServerPluginMessage::TileProperties {
189                tile_id,
190                properties,
191            } => {
192                plugin.on_tile_properties(&handle, tile_id, properties);
193            }
194
195            ServerPluginMessage::DeviceTiles { device_id, tiles } => {
196                plugin.on_device_tiles(&handle, device_id, tiles);
197            }
198
199            ServerPluginMessage::VisibleTiles { tiles } => {
200                plugin.on_visible_tiles(&handle, tiles);
201            }
202        }
203    }
204
205    subscriptions.clear();
206}
207
208pub fn setup_tracing() {
209    let filter = EnvFilter::from_default_env();
210    let subscriber = tracing_subscriber::fmt()
211        .compact()
212        .with_file(true)
213        .with_env_filter(filter)
214        .with_line_number(true)
215        .with_thread_ids(false)
216        .with_target(false)
217        .with_ansi(false)
218        .without_time()
219        .finish();
220
221    // use that subscriber to process traces emitted after this point
222    tracing::subscriber::set_global_default(subscriber).expect("failed to setup tracing");
223}