Skip to main content

rerun/
clap.rs

1//! Integration with integration with the [`clap`](https://crates.io/crates/clap) command line argument parser.
2
3use std::path::PathBuf;
4
5use re_sdk::{RecordingStream, RecordingStreamBuilder};
6
7// ---
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10enum RerunBehavior {
11    Connect(String),
12
13    Save(PathBuf),
14
15    Stdout,
16
17    #[cfg(feature = "web_viewer")]
18    Serve,
19
20    Spawn,
21}
22
23/// This struct implements a `clap::Parser` that defines all the arguments that a typical Rerun
24/// application might use, and provides helpers to evaluate those arguments and behave
25/// consequently.
26///
27/// Integrate it into your own `clap::Parser` by flattening it:
28/// ```no_run
29/// #[derive(Debug, clap::Parser)]
30/// #[clap(author, version, about)]
31/// struct MyArgs {
32///     #[command(flatten)]
33///     rerun: rerun::clap::RerunArgs,
34///
35///     #[clap(long)]
36///     my_arg: bool,
37/// }
38/// ```
39///
40/// Checkout the official examples to see it used in practice.
41#[derive(Clone, Debug, clap::Args)]
42#[clap(author, version, about)]
43pub struct RerunArgs {
44    /// Start a new Rerun Viewer process and feed it data in real-time.
45    #[clap(long, default_value = "true")]
46    spawn: bool,
47
48    /// Saves the data to an rrd file rather than visualizing it immediately.
49    #[clap(long)]
50    save: Option<PathBuf>,
51
52    /// Log data to standard output, to be piped into a Rerun Viewer.
53    #[clap(long, short = 'o')]
54    stdout: bool,
55
56    /// Connects and sends the logged data to a remote Rerun viewer.
57    ///
58    /// Optionally takes a URL to connect to.
59    ///
60    /// The scheme must be one of `rerun://`, `rerun+http://`, or `rerun+https://`,
61    /// and the pathname must be `/proxy`.
62    ///
63    /// The default is `rerun+http://127.0.0.1:9876/proxy`.
64    #[clap(long)]
65    #[expect(clippy::option_option)]
66    connect: Option<Option<String>>,
67
68    /// Connects and sends the logged data to a web-based Rerun viewer.
69    #[cfg(feature = "web_viewer")]
70    #[clap(long)]
71    serve: bool,
72
73    /// An upper limit on how much memory the gRPC server should use.
74    ///
75    /// The server buffers log messages for the benefit of late-arriving viewers.
76    ///
77    /// When this limit is reached, Rerun will drop the oldest data.
78    /// Example: `16GB` or `50%` (of system total).
79    ///
80    /// Defaults to `25%`.
81    #[clap(long, default_value = "25%")]
82    server_memory_limit: String,
83
84    /// If true, play back the most recent data first when new clients connect.
85    #[clap(long)]
86    newest_first: bool,
87
88    /// What bind address IP to use.
89    #[clap(long, default_value = "0.0.0.0")]
90    bind: String,
91}
92
93/// [`RerunArgs::init`] might have to spawn a bunch of background tasks depending on what arguments
94/// were passed in.
95/// This object makes sure they live long enough and get polled as needed.
96#[doc(hidden)]
97#[derive(Default)]
98pub struct ServeGuard {
99    block_on_drop: bool,
100}
101
102impl Drop for ServeGuard {
103    fn drop(&mut self) {
104        if self.block_on_drop {
105            eprintln!("Sleeping indefinitely while serving web viewer… Press ^C when done.");
106            // TODO(andreas): It would be a lot better if we had a handle to the web server and could call `block_until_shutdown` on it.
107            std::thread::sleep(std::time::Duration::from_secs(u64::MAX));
108        }
109    }
110}
111
112impl RerunArgs {
113    /// Creates a new [`RecordingStream`] according to the CLI parameters.
114    #[track_caller] // track_caller so that we can see if we are being called from an official example.
115    pub fn init(&self, application_id: &str) -> anyhow::Result<(RecordingStream, ServeGuard)> {
116        self.init_with_blueprint_opts(application_id, None)
117    }
118
119    /// Creates a new [`RecordingStream`] with a [`re_sdk::blueprint::Blueprint`] that activates immediately.
120    ///
121    /// For a default blueprint that only activates on reset, see [`Self::init_with_default_blueprint`].
122    #[track_caller]
123    pub fn init_with_blueprint(
124        &self,
125        application_id: &str,
126        blueprint: re_sdk::blueprint::Blueprint,
127    ) -> anyhow::Result<(RecordingStream, ServeGuard)> {
128        let activation = re_sdk::blueprint::BlueprintActivation {
129            make_active: true,
130            make_default: true,
131        };
132        self.init_with_blueprint_opts(
133            application_id,
134            Some(re_sdk::blueprint::BlueprintOpts {
135                blueprint,
136                activation,
137            }),
138        )
139    }
140
141    /// Creates a new [`RecordingStream`] with a default [`re_sdk::blueprint::Blueprint`].
142    ///
143    /// The blueprint activates only when the user resets. For immediate activation, see [`Self::init_with_blueprint`].
144    #[track_caller]
145    pub fn init_with_default_blueprint(
146        &self,
147        application_id: &str,
148        blueprint: re_sdk::blueprint::Blueprint,
149    ) -> anyhow::Result<(RecordingStream, ServeGuard)> {
150        let activation = re_sdk::blueprint::BlueprintActivation {
151            make_active: false,
152            make_default: true,
153        };
154        self.init_with_blueprint_opts(
155            application_id,
156            Some(re_sdk::blueprint::BlueprintOpts {
157                blueprint,
158                activation,
159            }),
160        )
161    }
162
163    /// Internal helper that handles blueprint options.
164    #[track_caller]
165    fn init_with_blueprint_opts(
166        &self,
167        application_id: &str,
168        blueprint_opts: Option<re_sdk::blueprint::BlueprintOpts>,
169    ) -> anyhow::Result<(RecordingStream, ServeGuard)> {
170        let mut builder = RecordingStreamBuilder::new(application_id);
171
172        // Add blueprint to builder if provided
173        if let Some(re_sdk::blueprint::BlueprintOpts {
174            blueprint,
175            activation,
176        }) = blueprint_opts
177        {
178            builder = if activation.make_active {
179                builder.with_blueprint(blueprint)
180            } else {
181                builder.with_default_blueprint(blueprint)
182            };
183        }
184
185        match self.to_behavior()? {
186            RerunBehavior::Stdout => Ok((builder.stdout()?, Default::default())),
187
188            RerunBehavior::Connect(url) => {
189                Ok((builder.connect_grpc_opts(url)?, Default::default()))
190            }
191
192            RerunBehavior::Save(path) => Ok((builder.save(path)?, Default::default())),
193
194            RerunBehavior::Spawn => Ok((builder.spawn()?, Default::default())),
195
196            #[cfg(feature = "web_viewer")]
197            RerunBehavior::Serve => {
198                let server_options = re_sdk::ServerOptions {
199                    playback_behavior: re_sdk::PlaybackBehavior::from_newest_first(
200                        self.newest_first,
201                    ),
202
203                    memory_limit: re_sdk::MemoryLimit::parse(&self.server_memory_limit)
204                        .map_err(|err| anyhow::format_err!("Bad --server-memory-limit: {err}"))?,
205
206                    cors_allowed_origins: vec![],
207                };
208
209                let rec = builder.serve_grpc_opts(
210                    &self.bind,
211                    crate::DEFAULT_SERVER_PORT,
212                    server_options,
213                )?;
214
215                crate::serve_web_viewer(crate::web_viewer::WebViewerConfig {
216                    open_browser: true,
217                    connect_to: vec!["rerun+http://localhost:9876/proxy".to_owned()],
218                    ..Default::default()
219                })?
220                .detach();
221
222                // Ensure the server stays alive until the end of the program.
223                let sleep_guard = ServeGuard {
224                    block_on_drop: true,
225                };
226
227                Ok((rec, sleep_guard))
228            }
229        }
230    }
231
232    #[expect(clippy::unnecessary_wraps)] // False positive on some feature flags
233    fn to_behavior(&self) -> anyhow::Result<RerunBehavior> {
234        if self.stdout {
235            return Ok(RerunBehavior::Stdout);
236        }
237
238        if let Some(path) = self.save.as_ref() {
239            return Ok(RerunBehavior::Save(path.clone()));
240        }
241
242        #[cfg(feature = "web_viewer")]
243        if self.serve {
244            return Ok(RerunBehavior::Serve);
245        }
246
247        match &self.connect {
248            Some(Some(url)) => return Ok(RerunBehavior::Connect(url.clone())),
249            Some(None) => {
250                return Ok(RerunBehavior::Connect(
251                    re_sdk::DEFAULT_CONNECT_URL.to_owned(),
252                ));
253            }
254            None => {}
255        }
256
257        Ok(RerunBehavior::Spawn)
258    }
259}