ros2-term 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ros2_term/__init__.py ADDED
File without changes
ros2_term/app.css ADDED
@@ -0,0 +1,76 @@
1
+ Screen {
2
+ layout: vertical;
3
+ }
4
+
5
+ /* Multi-screen layout */
6
+ .container {
7
+ height: 100%;
8
+ background: $panel;
9
+ }
10
+
11
+ #left-panel-container {
12
+ width: 50%;
13
+ }
14
+
15
+ NodeTreePanel, TopicTreePanel, RosEntityListPanel {
16
+ padding-left: 2;
17
+ width: 100%;
18
+ }
19
+
20
+ RosEntityInfoPanel, RosTypeDefinitionPanel {
21
+ padding: 1 2;
22
+ }
23
+
24
+ #main {
25
+ width: 70%;
26
+ border-left: inner $primary;
27
+ }
28
+
29
+ #main-upper {
30
+ border-bottom: inner $primary;
31
+ }
32
+
33
+ .main-half {
34
+ height: 50%;
35
+ }
36
+
37
+ /* ------------------------------------------------- */
38
+ SearchBar {
39
+ max-width: 100%;
40
+ height: 5;
41
+ padding: 0 1;
42
+ border: solid green;
43
+ }
44
+
45
+ SearchBar Input {
46
+ width: 1fr;
47
+ min-width: 10;
48
+ }
49
+
50
+ SearchBar .search-button {
51
+ width: 4;
52
+ margin-left: 1;
53
+ min-width: 4;
54
+ text-style: bold;
55
+ }
56
+
57
+ SearchBar .search-button.hover {
58
+ background: $surface;
59
+ }
60
+
61
+ SearchBar .search-button.active {
62
+ background: $secondary;
63
+ text-style: bold;
64
+ }
65
+
66
+ /* ------------------------------------------------- */
67
+
68
+ #close-button {
69
+ width: 30;
70
+ margin: 1;
71
+ color: white;
72
+ }
73
+
74
+ #button-container {
75
+ align-horizontal: center;
76
+ }
ros2_term/app.py ADDED
@@ -0,0 +1,558 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import warnings
5
+
6
+ from textual.app import App, ComposeResult
7
+ from textual.binding import Binding
8
+ from textual.containers import Horizontal, ScrollableContainer, Vertical
9
+ from textual.widgets import Footer, Static
10
+
11
+ from ros2_term.lib.config import ProfileConfig
12
+ from ros2_term.lib.entity import RosEntity, RosEntityType
13
+ from ros2_term.lib.history import History
14
+ from ros2_term.lib.messages import RosEntitySelected
15
+ from ros2_term.lib.ros2_client import ROS2Client
16
+ from ros2_term.lib.topic_watcher import TopicWatcher
17
+
18
+ from ros2_term.ui.search_bar import SearchBar, SearchChanged
19
+ from ros2_term.ui.help_screen import HelpScreen
20
+ from ros2_term.ui.profile_screen import ProfileSaveScreen
21
+ from ros2_term.ui.widgets import (
22
+ RosEntityInfoPanel,
23
+ RosEntityListPanel,
24
+ RosTypeDefinitionPanel,
25
+ NodeTreePanel,
26
+ TopicTreePanel,
27
+ )
28
+
29
+ warnings.simplefilter("ignore", ResourceWarning)
30
+
31
+
32
+ class ROS2Explorer(App):
33
+ """TUI app for exploring ROS2 nodes, topics, services, actions, and types."""
34
+
35
+ TITLE = "ROS2 Explorer"
36
+ CSS_PATH = "app.css"
37
+
38
+ DEFAULT_REFRESH_RATE = 2.0
39
+
40
+ BINDINGS = [
41
+ # Show in footer
42
+ Binding("?", "help", "Help", key_display="?"),
43
+ Binding("w", "watch_topic", "Watch", key_display="w"),
44
+ Binding("h", "show_hz", "Hz", key_display="h"),
45
+ Binding("e", "echo_topic", "Echo", key_display="e"),
46
+ Binding("s", "sample_topic", "Sample", key_display="s"),
47
+ Binding("a", "toggle_arrays", "Toggle Arrays", key_display="a"),
48
+ Binding("r", "reload", "Reload", key_display="r"),
49
+ # Hide from footer (still functional, documented in help)
50
+ Binding("b", "back", "Back", key_display="b", show=False),
51
+ Binding("f", "forward", "Forward", key_display="f", show=False),
52
+ Binding("v", "toggle_verbose", "Verbose", key_display="v", show=False),
53
+ Binding("p", "save_profile", "Save Profile", key_display="p", show=False),
54
+ Binding("c", "collapse_all", "Collapse All", key_display="c", show=False),
55
+ Binding("1", "show_nodes", "Nodes", key_display="1", show=False),
56
+ Binding("2", "show_topics", "Topics", key_display="2", show=False),
57
+ Binding("3", "show_services", "Services", key_display="3", show=False),
58
+ Binding("4", "show_actions", "Actions", key_display="4", show=False),
59
+ Binding("5", "show_msg_types", "Msg Types", key_display="5", show=False),
60
+ Binding("q", "quit", "Quit", key_display="q", show=False),
61
+ ]
62
+
63
+ def __init__(self, profile_name: str | None = None, refresh_rate: float = None) -> None:
64
+ super().__init__()
65
+ self._ros_client: ROS2Client | None = None
66
+ self._topic_watcher: TopicWatcher | None = None
67
+ self._profile_config = ProfileConfig()
68
+ self._current_profile: str | None = profile_name
69
+ self._watch_list: set[str] = set() # Topics that should be watched when discovered
70
+ self._refresh_rate = refresh_rate or self.DEFAULT_REFRESH_RATE
71
+ self._history: History[RosEntity] = History(20)
72
+ self._current_entity_type = RosEntityType.Node
73
+ self._left_panel_container: Vertical | None = None
74
+ self._current_tree_panel: Static | None = None
75
+ self._info_panel: RosEntityInfoPanel | None = None
76
+ self._definition_panel: RosTypeDefinitionPanel | None = None
77
+ self._verbose_mode: bool = False
78
+ self._current_topic: str | None = None # Track selected topic for hz/echo
79
+ # Store tree expansion state per panel type
80
+ self._tree_states: dict = {
81
+ RosEntityType.Node: {"expanded": set(), "selected": None},
82
+ RosEntityType.Topic: {"expanded": set(), "selected": None},
83
+ }
84
+
85
+ def compose(self) -> ComposeResult:
86
+ """Compose the app layout."""
87
+ yield SearchBar(id="search-bar")
88
+ with Horizontal(classes="container"):
89
+ # Left panel container (will be swapped based on view)
90
+ with Vertical(id="left-panel-container") as container:
91
+ self._left_panel_container = container
92
+ # Will be populated in on_mount
93
+
94
+ # Info/definition panels on the right
95
+ with Vertical(id="main"):
96
+ with ScrollableContainer(
97
+ id="main-upper", classes="main-half"
98
+ ) as info_container:
99
+ pass # Will be populated in on_mount
100
+ with ScrollableContainer(
101
+ id="def-container", classes="main-half"
102
+ ) as def_container:
103
+ pass # Will be populated in on_mount
104
+ yield Footer()
105
+
106
+ def on_mount(self) -> None:
107
+ """Initialize ROS2 client and create initial widgets."""
108
+ try:
109
+ self._ros_client = ROS2Client()
110
+ self._topic_watcher = TopicWatcher(self._ros_client)
111
+ except Exception as e:
112
+ self.exit(message=f"Failed to initialize ROS2 client: {e}")
113
+ return
114
+
115
+ # Load profile immediately to get view and watch list
116
+ if self._current_profile:
117
+ self._load_profile(self._current_profile)
118
+
119
+ # Create and mount initial tree panel based on current entity type
120
+ if self._current_entity_type == RosEntityType.Node:
121
+ self._current_tree_panel = NodeTreePanel(
122
+ self._ros_client, self._topic_watcher, refresh_rate=self._refresh_rate
123
+ )
124
+ elif self._current_entity_type == RosEntityType.Topic:
125
+ self._current_tree_panel = TopicTreePanel(
126
+ self._ros_client, self._topic_watcher, refresh_rate=self._refresh_rate
127
+ )
128
+ else:
129
+ self._current_tree_panel = RosEntityListPanel(
130
+ self._ros_client, self._current_entity_type, refresh_rate=self._refresh_rate
131
+ )
132
+ self._left_panel_container.mount(self._current_tree_panel)
133
+
134
+ # Mount info panel
135
+ self._info_panel = RosEntityInfoPanel(
136
+ self._ros_client, None, update_interval=5.0
137
+ )
138
+ self.query_one("#main-upper").mount(self._info_panel)
139
+
140
+ # Mount definition panel
141
+ self._definition_panel = RosTypeDefinitionPanel(
142
+ self._ros_client, self._topic_watcher
143
+ )
144
+ self.query_one("#def-container").mount(self._definition_panel)
145
+
146
+ # Set up interval to refresh propeller animation
147
+ self.set_interval(0.1, self._refresh_propellers)
148
+
149
+ # Set initial focus on the tree panel without auto-scrolling
150
+ self._current_tree_panel.focus(scroll_visible=False)
151
+
152
+ def should_watch_topic(self, topic_name: str) -> bool:
153
+ """Check if a topic should be watched (is in watch list)."""
154
+ return topic_name in self._watch_list
155
+
156
+ def watch_discovered_topic(self, topic_name: str) -> None:
157
+ """Watch a topic that was just discovered (called from tree panels)."""
158
+ if topic_name in self._watch_list and not self._topic_watcher.is_watched(topic_name):
159
+ try:
160
+ self._topic_watcher.watch_topic(topic_name)
161
+ except Exception:
162
+ pass # Silently fail, will retry on next discovery
163
+
164
+ def _save_tree_state(self) -> None:
165
+ """Save current tree panel's expanded state."""
166
+ if not self._current_tree_panel or self._current_entity_type not in self._tree_states:
167
+ return
168
+
169
+ if hasattr(self._current_tree_panel, 'root'):
170
+ expanded = set()
171
+ for node in self._current_tree_panel.root.children:
172
+ if node.is_expanded and node.data:
173
+ expanded.add(node.data.get("name"))
174
+ self._tree_states[self._current_entity_type]["expanded"] = expanded
175
+
176
+ # Save selected node path
177
+ if self._current_tree_panel.cursor_node and self._current_tree_panel.cursor_node.data:
178
+ # Build path from root to selected node
179
+ path = []
180
+ current = self._current_tree_panel.cursor_node
181
+ root = self._current_tree_panel.root
182
+ while current and current != root:
183
+ path.insert(0, {
184
+ "type": current.data.get("type") if current.data else None,
185
+ "name": current.data.get("name") if current.data else None,
186
+ "label": str(current.label)
187
+ })
188
+ current = current.parent
189
+
190
+ self._tree_states[self._current_entity_type]["selected"] = {
191
+ "path": path
192
+ }
193
+
194
+ def _restore_tree_state(self) -> None:
195
+ """Restore tree panel's expanded state after it's mounted."""
196
+ if not self._current_tree_panel or self._current_entity_type not in self._tree_states:
197
+ return
198
+
199
+ if hasattr(self._current_tree_panel, 'set_initial_state'):
200
+ state = self._tree_states[self._current_entity_type]
201
+ self._current_tree_panel.set_initial_state(
202
+ expanded=state.get("expanded", set()),
203
+ selected=state.get("selected")
204
+ )
205
+
206
+ def _switch_entity_type(self, entity_type: RosEntityType) -> None:
207
+ """Switch to viewing a different entity type."""
208
+ # Save state of current panel before switching
209
+ self._save_tree_state()
210
+
211
+ self._current_entity_type = entity_type
212
+
213
+ # Remove old tree panel
214
+ if self._current_tree_panel:
215
+ self._current_tree_panel.remove()
216
+
217
+ # Create appropriate new panel based on entity type
218
+ if entity_type == RosEntityType.Node:
219
+ self._current_tree_panel = NodeTreePanel(
220
+ self._ros_client, self._topic_watcher, refresh_rate=self._refresh_rate
221
+ )
222
+ elif entity_type == RosEntityType.Topic:
223
+ self._current_tree_panel = TopicTreePanel(
224
+ self._ros_client, self._topic_watcher, refresh_rate=self._refresh_rate
225
+ )
226
+ else:
227
+ # For services, actions, and types, use flat list
228
+ self._current_tree_panel = RosEntityListPanel(
229
+ self._ros_client, entity_type, refresh_rate=self._refresh_rate
230
+ )
231
+
232
+ # Mount the new panel
233
+ self._left_panel_container.mount(self._current_tree_panel)
234
+
235
+ # Restore saved state
236
+ self._restore_tree_state()
237
+
238
+ # Clear info panel
239
+ if self._info_panel:
240
+ self._info_panel._entity = None
241
+ self._info_panel.update("")
242
+ if self._definition_panel:
243
+ if entity_type.has_definition():
244
+ self._definition_panel.display = True
245
+ else:
246
+ self._definition_panel.display = False
247
+
248
+ def show_ros_entity(self, entity: RosEntity, append_history: bool = True) -> None:
249
+ """Navigate to a specific ROS entity - shows info without switching views."""
250
+ # Just show the entity info in the right panel, don't switch views
251
+ # This allows viewing topic details from node view, node details from topic view, etc.
252
+
253
+ # Track if this is a topic for hz/echo commands
254
+ if entity.type == RosEntityType.Topic:
255
+ self._current_topic = entity.name
256
+
257
+ # If topic is watched, automatically show hz stats
258
+ if self._topic_watcher and self._topic_watcher.is_watched(entity.name):
259
+ if self._definition_panel:
260
+ self._definition_panel.show_watched_topic_hz(entity.name)
261
+ self._definition_panel.display = True
262
+ else:
263
+ self._current_topic = None
264
+
265
+ if self._info_panel:
266
+ self._info_panel.set_entity(entity)
267
+ if self._definition_panel and entity.type.has_definition():
268
+ # Only set entity if it's not a watched topic (we already handled that above)
269
+ if not (entity.type == RosEntityType.Topic and
270
+ self._topic_watcher and self._topic_watcher.is_watched(entity.name)):
271
+ self._definition_panel.set_entity(entity)
272
+ self._definition_panel.display = True
273
+ else:
274
+ if self._definition_panel:
275
+ # Only hide if it's not a watched topic
276
+ if not (entity.type == RosEntityType.Topic and
277
+ self._topic_watcher and self._topic_watcher.is_watched(entity.name)):
278
+ self._definition_panel.display = False
279
+
280
+ if append_history:
281
+ self._history.append(entity)
282
+
283
+ def on_ros_entity_selected(self, event: RosEntitySelected) -> None:
284
+ """Handle entity selection from clickable links or tree."""
285
+ self.show_ros_entity(event.entity)
286
+
287
+ def on_search_changed(self, event: SearchChanged) -> None:
288
+ """Handle search criteria changes."""
289
+ search_bar = self.query_one("#search-bar", SearchBar)
290
+
291
+ # Set filter function on the current tree panel
292
+ if self._current_tree_panel and hasattr(self._current_tree_panel, "set_filter"):
293
+ self._current_tree_panel.set_filter(lambda name: search_bar.matches(name))
294
+
295
+ def action_back(self) -> None:
296
+ """Navigate back in history."""
297
+ if entity := self._history.back():
298
+ self.show_ros_entity(entity, append_history=False)
299
+
300
+ def action_forward(self) -> None:
301
+ """Navigate forward in history."""
302
+ if entity := self._history.forward():
303
+ self.show_ros_entity(entity, append_history=False)
304
+
305
+ def action_reload(self) -> None:
306
+ """Reload the current entity list."""
307
+ if self._current_tree_panel and hasattr(
308
+ self._current_tree_panel, "update_items"
309
+ ):
310
+ self._current_tree_panel.update_items()
311
+
312
+ def action_collapse_all(self) -> None:
313
+ """Collapse all top-level tree nodes."""
314
+ if self._current_tree_panel and hasattr(self._current_tree_panel, 'root'):
315
+ for node in self._current_tree_panel.root.children:
316
+ node.collapse()
317
+
318
+ def action_toggle_verbose(self) -> None:
319
+ """Toggle verbose mode to show/hide default ROS2 topics/services/actions."""
320
+ self._verbose_mode = not self._verbose_mode
321
+
322
+ # Update the tree panel's verbose mode
323
+ if self._current_tree_panel and hasattr(
324
+ self._current_tree_panel, "set_verbose_mode"
325
+ ):
326
+ self._current_tree_panel.set_verbose_mode(self._verbose_mode)
327
+
328
+ # Update the info panel's verbose mode
329
+ if self._info_panel and hasattr(self._info_panel, "set_verbose_mode"):
330
+ self._info_panel.set_verbose_mode(self._verbose_mode)
331
+
332
+ # Notify user
333
+ mode_text = "ON" if self._verbose_mode else "OFF"
334
+ self.notify(f"Verbose mode: {mode_text}")
335
+
336
+ def action_show_nodes(self) -> None:
337
+ """Show nodes view."""
338
+ self._switch_entity_type(RosEntityType.Node)
339
+ self._auto_save_profile()
340
+
341
+ def action_show_topics(self) -> None:
342
+ """Show topics view."""
343
+ self._switch_entity_type(RosEntityType.Topic)
344
+ self._auto_save_profile()
345
+
346
+ def action_show_services(self) -> None:
347
+ """Show services view."""
348
+ self._switch_entity_type(RosEntityType.Service)
349
+ self._auto_save_profile()
350
+
351
+ def action_show_actions(self) -> None:
352
+ """Show actions view."""
353
+ self._switch_entity_type(RosEntityType.Action)
354
+ self._auto_save_profile()
355
+
356
+ def action_show_msg_types(self) -> None:
357
+ """Show message types view."""
358
+ self._switch_entity_type(RosEntityType.MsgType)
359
+ self._auto_save_profile()
360
+
361
+ def action_show_hz(self) -> None:
362
+ """Show Hz (publishing rate) for the selected topic."""
363
+ if not self._current_topic:
364
+ self.notify("Please select a topic first", severity="warning")
365
+ return
366
+
367
+ if self._definition_panel:
368
+ # Check if topic is watched
369
+ if self._topic_watcher and self._topic_watcher.is_watched(self._current_topic):
370
+ # Show watched topic hz (uses existing subscription)
371
+ self._definition_panel.show_watched_topic_hz(self._current_topic)
372
+ else:
373
+ # Show hz for unwatched topic (creates temp subscription)
374
+ self._definition_panel.show_hz(self._current_topic)
375
+ self._definition_panel.display = True
376
+
377
+ def action_echo_topic(self) -> None:
378
+ """Start continuous echo of the selected topic."""
379
+ if not self._current_topic:
380
+ self.notify("Please select a topic first", severity="warning")
381
+ return
382
+
383
+ if self._definition_panel:
384
+ self._definition_panel.start_echo(self._current_topic, continuous=True)
385
+ self._definition_panel.display = True
386
+
387
+ def action_sample_topic(self) -> None:
388
+ """Sample a single message from the selected topic."""
389
+ if not self._current_topic:
390
+ self.notify("Please select a topic first", severity="warning")
391
+ return
392
+
393
+ if self._definition_panel:
394
+ self._definition_panel.start_echo(self._current_topic, continuous=False)
395
+ self._definition_panel.display = True
396
+
397
+ def action_toggle_arrays(self) -> None:
398
+ """Toggle array visibility in echo/sample output."""
399
+ if self._definition_panel:
400
+ self._definition_panel.toggle_arrays()
401
+
402
+ def action_watch_topic(self) -> None:
403
+ """Toggle watching the selected topic."""
404
+ if not self._current_topic:
405
+ self.notify("Please select a topic first", severity="warning")
406
+ return
407
+
408
+ if not self._topic_watcher:
409
+ self.notify("Topic watcher not initialized", severity="error")
410
+ return
411
+
412
+ try:
413
+ is_watched = self._topic_watcher.toggle_watch(self._current_topic)
414
+ if is_watched:
415
+ # Add to watch list
416
+ self._watch_list.add(self._current_topic)
417
+ # Automatically show hz display when watching
418
+ if self._definition_panel:
419
+ self._definition_panel.show_watched_topic_hz(self._current_topic)
420
+ self._definition_panel.display = True
421
+ else:
422
+ # Remove from watch list
423
+ self._watch_list.discard(self._current_topic)
424
+ # else: unwatched, no action needed (user sees propeller disappear)
425
+
426
+ # Auto-save to default profile
427
+ self._auto_save_profile()
428
+ except Exception as e:
429
+ self.notify(f"Failed to watch topic: {e}", severity="error")
430
+
431
+ def action_save_profile(self) -> None:
432
+ """Save current watched topics to a profile (prompts for name)."""
433
+ if not self._topic_watcher:
434
+ self.notify("Topic watcher not initialized", severity="error")
435
+ return
436
+
437
+ # Show profile save screen to get name
438
+ self.push_screen(
439
+ ProfileSaveScreen(self._current_profile),
440
+ self._handle_profile_save
441
+ )
442
+
443
+ def _handle_profile_save(self, profile_name: str | None) -> None:
444
+ """Handle the profile save after user enters name."""
445
+ if not profile_name:
446
+ return # User cancelled
447
+
448
+ watched = self._topic_watcher.get_watched_topics()
449
+ current_view = self._current_entity_type.name
450
+
451
+ try:
452
+ self._profile_config.save_profile(profile_name, watched, current_view)
453
+ self._current_profile = profile_name
454
+ self.notify(f"Saved profile '{profile_name}' ({len(watched)} topics)")
455
+ except Exception as e:
456
+ self.notify(f"Failed to save profile: {e}", severity="error")
457
+
458
+ def _auto_save_profile(self) -> None:
459
+ """Auto-save to default profile silently."""
460
+ if not self._topic_watcher:
461
+ return
462
+
463
+ watched = self._topic_watcher.get_watched_topics()
464
+ current_view = self._current_entity_type.name
465
+
466
+ try:
467
+ self._profile_config.save_profile("default", watched, current_view)
468
+ except Exception:
469
+ pass # Silently fail on auto-save
470
+
471
+ def _load_profile(self, profile_name: str) -> None:
472
+ """Load profile - sets view and watch list."""
473
+ try:
474
+ data = self._profile_config.load_profile(profile_name)
475
+ if not data:
476
+ return
477
+
478
+ current_view = data.get("current_view", "Node")
479
+ watched_topics = data.get("watched_topics", [])
480
+
481
+ # Restore the view
482
+ try:
483
+ entity_type = RosEntityType[current_view]
484
+ self._current_entity_type = entity_type
485
+ except (KeyError, ValueError):
486
+ pass
487
+
488
+ # Store watch list - topics will be watched as they're discovered
489
+ self._watch_list = set(watched_topics)
490
+ except Exception:
491
+ pass
492
+
493
+ def _refresh_propellers(self) -> None:
494
+ """Refresh the propeller display for watched topics."""
495
+ if self._current_tree_panel and hasattr(self._current_tree_panel, "refresh_propellers"):
496
+ self._current_tree_panel.refresh_propellers()
497
+
498
+ def action_help(self) -> None:
499
+ """Show help screen."""
500
+ self.push_screen(HelpScreen())
501
+
502
+ async def action_quit(self) -> None:
503
+ """Quit the application."""
504
+ if self._topic_watcher:
505
+ self._topic_watcher.cleanup()
506
+ if self._ros_client:
507
+ self._ros_client.terminate()
508
+ await super().action_quit()
509
+
510
+
511
+ def main():
512
+ # Get available profiles for help text
513
+ config = ProfileConfig()
514
+ profiles = config.list_profiles()
515
+ profile_list = ", ".join(profiles) if profiles else "none"
516
+
517
+ epilog = f"Available profiles: {profile_list}\nProfile location: {config.config_dir}"
518
+
519
+ parser = argparse.ArgumentParser(
520
+ description="ROS2 Terminal UI for exploring nodes, topics, services, and actions",
521
+ epilog=epilog,
522
+ formatter_class=argparse.RawDescriptionHelpFormatter
523
+ )
524
+ parser.add_argument(
525
+ "--profile",
526
+ type=str,
527
+ help="Profile name to load (without .json extension)",
528
+ )
529
+ parser.add_argument(
530
+ "--continue",
531
+ dest="continue_last",
532
+ action="store_true",
533
+ help="Continue from the last used profile",
534
+ )
535
+ parser.add_argument(
536
+ "--refresh-rate",
537
+ type=float,
538
+ default=2.0,
539
+ help="Refresh rate in seconds for updating topics/nodes list (default: 2.0)",
540
+ )
541
+
542
+ args = parser.parse_args()
543
+
544
+ # Determine which profile to use
545
+ profile_name = None
546
+ if args.continue_last:
547
+ config = ProfileConfig()
548
+ profile_name = config.get_last_profile()
549
+ if not profile_name:
550
+ profile_name = "default"
551
+ elif args.profile:
552
+ profile_name = args.profile
553
+
554
+ app = ROS2Explorer(profile_name=profile_name, refresh_rate=args.refresh_rate)
555
+ app.run()
556
+
557
+ if __name__ == "__main__":
558
+ main()
File without changes