rostree 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.
rostree/tui/app.py ADDED
@@ -0,0 +1,541 @@
1
+ """Textual TUI for navigating ROS 2 package dependency trees."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from textual.app import App, ComposeResult
10
+ from textual.binding import Binding
11
+ from textual.containers import Horizontal, Vertical
12
+ from textual.screen import ModalScreen
13
+ from textual.widgets import Button, Footer, Header, Input, Static, Tree
14
+ from textual.widgets.tree import TreeNode
15
+
16
+ from rostree.api import build_tree, list_known_packages_by_source
17
+
18
+ # Welcome banner: ROSTREE
19
+ WELCOME_BANNER = """
20
+ [bold cyan]
21
+ ██████╗ ██████╗ ███████╗████████╗██████╗ ███████╗███████╗
22
+ ██╔══██╗██╔═══██╗██╔════╝╚══██╔══╝██╔══██╗██╔════╝██╔════╝
23
+ ██████╔╝██║ ██║███████╗ ██║ ██████╔╝█████╗ █████╗
24
+ ██╔══██╗██║ ██║╚════██║ ██║ ██╔══██╗██╔══╝ ██╔══╝
25
+ ██║ ██║╚██████╔╝███████║ ██║ ██║ ██║███████╗███████╗
26
+ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝
27
+ [/bold cyan]
28
+ """
29
+
30
+ WELCOME_BODY = """
31
+ [dim]Visualize ROS 2 package dependencies as a navigable tree.[/]
32
+
33
+ • [bold]CLI[/]: rostree scan, rostree list, rostree tree <pkg>
34
+ • [bold]TUI[/]: browse packages, expand/collapse, see details
35
+ • [bold]Library[/]: Python API for scripts and automation
36
+
37
+ [dim]Requires ROS 2 env (source install/setup.bash).[/]
38
+ """
39
+
40
+ # Limits to avoid huge trees and crashes
41
+ MAX_PACKAGES_PER_SOURCE = 80 # max package names per source section
42
+ MAX_TREE_DEPTH = 8
43
+ MAX_TREE_NODES = 500
44
+ EXPAND_DEPTH_DEFAULT = 2
45
+ # TUI uses runtime_only=True (depend + exec_depend only)
46
+ TUI_TREE_MAX_DEPTH = 6
47
+
48
+ # Colors: source sections and tree
49
+ COLOR_SYSTEM = "dim" # /opt/ros/...
50
+ COLOR_WORKSPACE = "bold green" # your workspace
51
+ COLOR_OTHER = "bold cyan" # third-party installs
52
+ COLOR_SOURCE = "bold yellow" # unbuilt source
53
+ COLOR_ADDED = "bold magenta" # user-added paths
54
+ COLOR_HEADER = "bold magenta"
55
+ COLOR_PKG = "white"
56
+ COLOR_STATS = "cyan"
57
+ COLOR_PATH = "dim"
58
+
59
+
60
+ def _count_nodes(node: Any) -> int:
61
+ """Count nodes in tree (for cap)."""
62
+ n = 1
63
+ for c in getattr(node, "children", []):
64
+ n += _count_nodes(c)
65
+ return n
66
+
67
+
68
+ def _node_stats(node: Any) -> tuple[int, int, int]:
69
+ """Return (direct_children, total_descendants, max_depth) for a node."""
70
+ children = getattr(node, "children", []) or []
71
+ direct = len(children)
72
+ total = 0
73
+ max_d = 0
74
+ for c in children:
75
+ sub_direct, sub_total, sub_depth = _node_stats(c)
76
+ total += 1 + sub_total
77
+ max_d = max(max_d, 1 + sub_depth)
78
+ return direct, total, max_d
79
+
80
+
81
+ def _populate_textual_tree(
82
+ tn: TreeNode,
83
+ node: Any,
84
+ *,
85
+ depth: int = 0,
86
+ max_depth: int = MAX_TREE_DEPTH,
87
+ max_nodes: int = MAX_TREE_NODES,
88
+ node_count: list[int] | None = None,
89
+ ) -> None:
90
+ """Recursively add DependencyNode children; cap depth and total nodes."""
91
+ if node_count is None:
92
+ node_count = [0]
93
+ for child in getattr(node, "children", []):
94
+ if node_count[0] >= max_nodes:
95
+ tn.add_leaf(f"[dim]… truncated ({max_nodes} nodes max)[/]")
96
+ return
97
+ if depth >= max_depth:
98
+ tn.add_leaf(f"[dim]{child.name} …[/]")
99
+ continue
100
+ node_count[0] += 1
101
+ label = f"[{COLOR_PKG}]{child.name}[/] [dim]v{child.version or '?'}[/]"
102
+ child_tn = tn.add(label, expand=False)
103
+ child_tn.data = child
104
+ _populate_textual_tree(
105
+ child_tn,
106
+ child,
107
+ depth=depth + 1,
108
+ max_depth=max_depth,
109
+ max_nodes=max_nodes,
110
+ node_count=node_count,
111
+ )
112
+
113
+
114
+ def _expand_to_depth(tn: TreeNode, depth: int, current: int = 0) -> None:
115
+ """Expand tree nodes up to given depth (0 = root only)."""
116
+ if current >= depth:
117
+ return
118
+ try:
119
+ tn.expand()
120
+ for child in tn.children:
121
+ _expand_to_depth(child, depth, current + 1)
122
+ except Exception:
123
+ pass
124
+
125
+
126
+ class WelcomeScreen(ModalScreen[bool]):
127
+ """Welcome / presentation screen. Modal so Enter/q always work."""
128
+
129
+ BINDINGS = [
130
+ Binding("enter", "start", "Start", show=True),
131
+ Binding("q", "quit", "Quit", show=True),
132
+ ]
133
+
134
+ DEFAULT_CSS = """
135
+ WelcomeScreen {
136
+ align: center middle;
137
+ padding: 2 4;
138
+ }
139
+ WelcomeScreen #banner {
140
+ text-align: center;
141
+ padding-bottom: 1;
142
+ }
143
+ WelcomeScreen #welcome_body {
144
+ padding: 1 2;
145
+ width: 60;
146
+ }
147
+ WelcomeScreen #welcome_footer {
148
+ text-align: center;
149
+ padding-top: 2;
150
+ }
151
+ """
152
+
153
+ def compose(self) -> ComposeResult:
154
+ yield Static(WELCOME_BANNER, id="banner", markup=True)
155
+ yield Static(WELCOME_BODY, id="welcome_body", markup=True)
156
+ yield Static(
157
+ "[bold]This screen has focus.[/] Press [cyan]Enter[/] to start · [dim]q[/] to quit",
158
+ id="welcome_footer",
159
+ markup=True,
160
+ )
161
+
162
+ def on_mount(self) -> None:
163
+ self.sub_title = "Enter = start · q = quit"
164
+
165
+ def action_start(self) -> None:
166
+ self.dismiss(True)
167
+
168
+ def action_quit(self) -> None:
169
+ self.dismiss(False)
170
+
171
+
172
+ class AddSourceScreen(ModalScreen[Path | None]):
173
+ """Modal to enter a path to add as an extra source root. Keyboard-only: type path, Enter to add, Escape to cancel."""
174
+
175
+ BINDINGS = [
176
+ Binding("escape", "cancel", "Cancel", show=True),
177
+ ]
178
+
179
+ DEFAULT_CSS = """
180
+ AddSourceScreen {
181
+ align: center middle;
182
+ padding: 2 4;
183
+ }
184
+ AddSourceScreen #add_source_title {
185
+ text-align: center;
186
+ padding-bottom: 1;
187
+ }
188
+ AddSourceScreen #add_source_input {
189
+ width: 60;
190
+ margin: 1 0;
191
+ }
192
+ AddSourceScreen #add_source_hint {
193
+ text-align: center;
194
+ padding-top: 1;
195
+ }
196
+ """
197
+
198
+ def __init__(self, **kwargs: Any) -> None:
199
+ super().__init__(**kwargs)
200
+ self._input: Input | None = None
201
+
202
+ def compose(self) -> ComposeResult:
203
+ with Vertical():
204
+ yield Static(
205
+ "[bold cyan]Add source path[/]\n\n"
206
+ "Type a directory path to scan for ROS 2 packages (e.g. /path/to/ws/src).",
207
+ id="add_source_title",
208
+ markup=True,
209
+ )
210
+ yield Input(
211
+ placeholder="/path/to/source/dir",
212
+ id="add_source_input",
213
+ )
214
+ yield Static(
215
+ "[dim]Enter[/] = Add · [dim]Escape[/] = Cancel",
216
+ id="add_source_hint",
217
+ markup=True,
218
+ )
219
+
220
+ def on_mount(self) -> None:
221
+ self._input = self.query_one("#add_source_input", Input)
222
+ self._input.focus()
223
+
224
+ def on_input_submitted(self, event: Input.Submitted) -> None:
225
+ """Submit on Enter so no mouse/click needed."""
226
+ if event.input.id != "add_source_input":
227
+ return
228
+ self._do_submit()
229
+
230
+ def _do_submit(self) -> None:
231
+ value = self._input.value.strip() if self._input else ""
232
+ if not value:
233
+ self.dismiss(None)
234
+ return
235
+ p = Path(value).expanduser().resolve()
236
+ if not p.exists():
237
+ self.notify(f"Path does not exist: {p}", severity="warning", timeout=3)
238
+ return
239
+ if not p.is_dir():
240
+ self.notify(f"Not a directory: {p}", severity="warning", timeout=3)
241
+ return
242
+ self.dismiss(p)
243
+
244
+ def action_cancel(self) -> None:
245
+ self.dismiss(None)
246
+
247
+
248
+ class DepTreeApp(App[None]):
249
+ """Terminal UI to explore ROS 2 package dependency trees."""
250
+
251
+ TITLE = "rostree"
252
+ BINDINGS = [
253
+ Binding("escape", "back", "Back", show=True),
254
+ Binding("b", "back", "Back", show=False),
255
+ Binding("a", "add_source", "Add source"),
256
+ Binding("q", "quit", "Quit"),
257
+ Binding("r", "refresh", "Refresh"),
258
+ Binding("e", "expand_all", "Expand all"),
259
+ Binding("c", "collapse_all", "Collapse"),
260
+ ]
261
+
262
+ def __init__(self, root_package: str | None = None, **kwargs: Any) -> None:
263
+ super().__init__(**kwargs)
264
+ self._root_package = root_package
265
+ self._root_node: Any = None
266
+ self._main_started = False
267
+ self._extra_source_roots: list[Path] = []
268
+
269
+ DEFAULT_CSS = """
270
+ #back_bar {
271
+ display: none;
272
+ height: auto;
273
+ padding: 0 1;
274
+ margin-bottom: 1;
275
+ }
276
+ #details {
277
+ padding: 1 2;
278
+ border: solid $primary;
279
+ height: auto;
280
+ min-height: 8;
281
+ }
282
+ """
283
+
284
+ def compose(self) -> ComposeResult:
285
+ yield Header(show_clock=False)
286
+ with Horizontal(id="back_bar"):
287
+ yield Button("← Back to package list", id="back_btn", variant="primary")
288
+ yield Tree("Dependencies", id="dep_tree")
289
+ yield Static(
290
+ "[dim]↑/↓[/] move · [dim]Enter[/]/[dim]Space[/] select · [dim]Tab[/] = switch focus · [dim]Esc[/]/[dim]b[/] = Back",
291
+ id="details",
292
+ )
293
+ yield Footer()
294
+
295
+ def on_mount(self) -> None:
296
+ self.sub_title = "Tab = move focus · Esc = Back · Keys shown in footer"
297
+ self.push_screen(WelcomeScreen(), self._on_welcome_done)
298
+
299
+ def _on_welcome_done(self, start: bool) -> None:
300
+ if not start:
301
+ self.exit(0)
302
+ return
303
+ self._main_started = True
304
+ self._start_main()
305
+
306
+ def _source_color(self, label: str) -> str:
307
+ if "System" in label:
308
+ return COLOR_SYSTEM
309
+ if "Workspace" in label:
310
+ return COLOR_WORKSPACE
311
+ if "Other" in label:
312
+ return COLOR_OTHER
313
+ if "Added" in label:
314
+ return COLOR_ADDED
315
+ return COLOR_SOURCE
316
+
317
+ def _start_main(self) -> None:
318
+ try:
319
+ try:
320
+ self.query_one("#back_bar").styles.display = "none"
321
+ except Exception:
322
+ pass
323
+ tree = self.query_one("#dep_tree", Tree)
324
+ if self._root_package:
325
+ self._load_tree(self._root_package)
326
+ else:
327
+ by_source = list_known_packages_by_source(
328
+ extra_source_roots=self._extra_source_roots or None,
329
+ )
330
+ if not by_source:
331
+ self._set_details(
332
+ "No ROS 2 packages found. Set AMENT_PREFIX_PATH or run from a workspace.\n\n"
333
+ "[dim]a[/] = Add source path"
334
+ )
335
+ tree.root.add_leaf("[dim]No packages in environment[/]")
336
+ try:
337
+ tree.focus()
338
+ except Exception:
339
+ pass
340
+ return
341
+ total = sum(len(names) for names in by_source.values())
342
+ tree.root.label = f"[{COLOR_HEADER}]Packages by source[/]"
343
+ # Order: System, Workspace, Other, Source, Added
344
+ order = ["System", "Workspace", "Other", "Source", "Added"]
345
+ sorted_keys = sorted(
346
+ by_source.keys(),
347
+ key=lambda k: next((i for i, o in enumerate(order) if o in k), 99),
348
+ )
349
+ recap_parts = []
350
+ for label in sorted_keys:
351
+ names = by_source[label]
352
+ color = self._source_color(label)
353
+ section_node = tree.root.add(
354
+ f"[{color}]{label} ({len(names)})[/]",
355
+ expand=True,
356
+ )
357
+ recap_parts.append(f"[{color}]{label.split('(')[0].strip()}: {len(names)}[/]")
358
+ for name in names[:MAX_PACKAGES_PER_SOURCE]:
359
+ child_tn = section_node.add(f"[{color}]{name}[/]", expand=False)
360
+ child_tn.data = name
361
+ if len(names) > MAX_PACKAGES_PER_SOURCE:
362
+ section_node.add_leaf(
363
+ f"[dim]… and {len(names) - MAX_PACKAGES_PER_SOURCE} more[/]"
364
+ )
365
+ self._set_details(
366
+ f"[{COLOR_HEADER}]Package list[/]\n\n"
367
+ f"Total: [{COLOR_STATS}]{total}[/] packages · "
368
+ + " · ".join(recap_parts)
369
+ + "\n\n"
370
+ "[dim]↑/↓[/] move · [dim]Enter[/] or [dim]Space[/] on a package = load tree · "
371
+ "[dim]a[/] = Add source · [dim]Esc[/]/[dim]b[/] = Back (when viewing a tree)"
372
+ )
373
+ try:
374
+ self.query_one("#dep_tree", Tree).focus()
375
+ except Exception:
376
+ pass
377
+ except Exception as e:
378
+ self._set_details(f"[red]Error: {e!s}[/]")
379
+ tree = self.query_one("#dep_tree", Tree)
380
+ tree.root.add_leaf("[dim]Error loading packages[/]")
381
+ try:
382
+ tree.focus()
383
+ except Exception:
384
+ pass
385
+
386
+ def _clear_tree(self, tree: Tree) -> None:
387
+ while tree.root.children:
388
+ tree.root.children[0].remove()
389
+
390
+ def _load_tree(self, root_package: str) -> None:
391
+ self._root_package = root_package
392
+ try:
393
+ self._root_node = build_tree(
394
+ root_package,
395
+ max_depth=TUI_TREE_MAX_DEPTH,
396
+ runtime_only=True,
397
+ extra_source_roots=self._extra_source_roots or None,
398
+ )
399
+ except Exception as e:
400
+ self._set_details(f"[red]Error building tree: {e!s}[/]")
401
+ return
402
+ if self._root_node is None:
403
+ self._set_details(f"Package not found: {root_package}")
404
+ return
405
+ tree = self.query_one("#dep_tree", Tree)
406
+ self._clear_tree(tree)
407
+ tree.root.label = (
408
+ f"[{COLOR_HEADER}]{self._root_node.name}[/] [dim]v{self._root_node.version or '?'}[/]"
409
+ )
410
+ tree.root.data = self._root_node
411
+ _populate_textual_tree(tree.root, self._root_node)
412
+ try:
413
+ _expand_to_depth(tree.root, EXPAND_DEPTH_DEFAULT)
414
+ except Exception:
415
+ pass
416
+ self._set_details(
417
+ self._format_node(self._root_node)
418
+ + "\n\n[dim]Esc[/] or [dim]b[/] = Back to package list · [dim]Tab[/] then [dim]Enter[/] = Back button"
419
+ )
420
+ try:
421
+ self.query_one("#back_bar").styles.display = "block"
422
+ self.query_one("#dep_tree", Tree).focus()
423
+ except Exception:
424
+ pass
425
+
426
+ def _format_node(self, node: Any) -> str:
427
+ name = getattr(node, "name", "?")
428
+ version = getattr(node, "version", "") or "?"
429
+ desc = getattr(node, "description", "") or "(no description)"
430
+ path = getattr(node, "path", "") or "(n/a)"
431
+
432
+ direct, total_desc, max_depth = _node_stats(node)
433
+
434
+ lines = [
435
+ f"[{COLOR_HEADER}]Package[/]",
436
+ f" [{COLOR_PKG}]{name}[/] [dim]v{version}[/]",
437
+ "",
438
+ f"[{COLOR_HEADER}]Description[/]",
439
+ f" {desc}",
440
+ "",
441
+ f"[{COLOR_HEADER}]Stats[/]",
442
+ f" Direct dependencies: [{COLOR_STATS}]{direct}[/]",
443
+ f" Total descendants: [{COLOR_STATS}]{total_desc}[/] [dim](indirect)[/]",
444
+ f" Max depth from here: [{COLOR_STATS}]{max_depth}[/] [dim]levels[/]",
445
+ "",
446
+ f"[{COLOR_HEADER}]Path[/]",
447
+ f" [{COLOR_PATH}]{path}[/]",
448
+ ]
449
+ return "\n".join(lines)
450
+
451
+ def _set_details(self, text: str) -> None:
452
+ details = self.query_one("#details", Static)
453
+ details.update(text)
454
+
455
+ def on_tree_node_selected(self, event: Tree.NodeSelected) -> None:
456
+ node = event.node.data
457
+ if node is None:
458
+ return
459
+ if hasattr(node, "name") and hasattr(node, "children"):
460
+ self._set_details(self._format_node(node))
461
+ elif isinstance(node, str):
462
+ self._load_tree(node)
463
+
464
+ def action_back(self) -> None:
465
+ """Return to the known packages list (only when viewing a tree)."""
466
+ if not self._main_started or not self._root_package:
467
+ return
468
+ self._root_package = None
469
+ self._root_node = None
470
+ try:
471
+ self.query_one("#back_bar").styles.display = "none"
472
+ except Exception:
473
+ pass
474
+ tree = self.query_one("#dep_tree", Tree)
475
+ self._clear_tree(tree)
476
+ self._start_main()
477
+
478
+ def on_button_pressed(self, event: Button.Pressed) -> None:
479
+ if event.button.id == "back_btn":
480
+ self.action_back()
481
+
482
+ def action_refresh(self) -> None:
483
+ if not self._main_started:
484
+ return
485
+ if self._root_package:
486
+ self._load_tree(self._root_package)
487
+ else:
488
+ tree = self.query_one("#dep_tree", Tree)
489
+ self._clear_tree(tree)
490
+ self._start_main()
491
+
492
+ def action_expand_all(self) -> None:
493
+ tree = self.query_one("#dep_tree", Tree)
494
+ try:
495
+ tree.root.expand_all()
496
+ except Exception:
497
+ tree.root.expand()
498
+
499
+ def action_collapse_all(self) -> None:
500
+ tree = self.query_one("#dep_tree", Tree)
501
+ try:
502
+ tree.root.collapse_all()
503
+ tree.root.expand()
504
+ except Exception:
505
+ pass
506
+
507
+ def action_add_source(self) -> None:
508
+ """Open modal to add an extra source path."""
509
+ if not self._main_started:
510
+ return
511
+ self.push_screen(AddSourceScreen(), self._on_add_source_done)
512
+
513
+ def _on_add_source_done(self, path: Path | None) -> None:
514
+ if path is None:
515
+ return
516
+ if path in self._extra_source_roots:
517
+ self.notify("Path already added", severity="information", timeout=2)
518
+ return
519
+ self._extra_source_roots.append(path)
520
+ self.notify(f"Added: {path}", severity="information", timeout=2)
521
+ self.action_refresh()
522
+ try:
523
+ self.query_one("#dep_tree", Tree).focus()
524
+ except Exception:
525
+ pass
526
+
527
+ def action_quit(self) -> None:
528
+ self.exit()
529
+
530
+
531
+ def main() -> None:
532
+ """Entry point for the rostree TUI."""
533
+ root = None
534
+ if len(sys.argv) > 1:
535
+ root = sys.argv[1].strip()
536
+ app = DepTreeApp(root_package=root)
537
+ app.run()
538
+
539
+
540
+ if __name__ == "__main__":
541
+ main()
@@ -0,0 +1,82 @@
1
+ Metadata-Version: 2.4
2
+ Name: rostree
3
+ Version: 0.1.0
4
+ Summary: Explore ROS 2 package dependencies from the command line (CLI, TUI, library)
5
+ Author: rostree contributors
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Keywords: cli,dependencies,ros,ros2,tui,visualization
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Console
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Scientific/Engineering :: Visualization
17
+ Requires-Python: >=3.10
18
+ Requires-Dist: textual>=0.47.0
19
+ Provides-Extra: dev
20
+ Requires-Dist: black==25.1.0; extra == 'dev'
21
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
22
+ Requires-Dist: pytest>=7.0; extra == 'dev'
23
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # rostree
27
+
28
+ [![CI](https://github.com/guilyx/rostree/actions/workflows/ci.yml/badge.svg)](https://github.com/guilyx/rostree/actions/workflows/ci.yml)
29
+ [![codecov](https://codecov.io/gh/guilyx/rostree/graph/badge.svg)](https://codecov.io/gh/guilyx/rostree)
30
+ [![PyPI version](https://img.shields.io/pypi/v/rostree.svg)](https://pypi.org/project/rostree/)
31
+ [![PyPI downloads](https://img.shields.io/pypi/dm/rostree.svg)](https://pypi.org/project/rostree/)
32
+ [![Python versions](https://img.shields.io/pypi/pyversions/rostree.svg)](https://pypi.org/project/rostree/)
33
+ [![License](https://img.shields.io/github/license/guilyx/rostree.svg)](https://github.com/guilyx/rostree/blob/main/LICENSE)
34
+
35
+ Explore ROS 2 package dependencies from the command line (CLI, TUI, library).
36
+
37
+ **Docs:** [docs/README.md](docs/README.md) — overview, package discovery, dependency trees, usage, development.
38
+
39
+ ## Quick start
40
+
41
+ ```bash
42
+ pip install -e . # or: uv pip install -e .
43
+ source /opt/ros/<distro>/setup.bash # and/or your workspace install/setup.bash
44
+ ```
45
+
46
+ ### CLI commands
47
+
48
+ ```bash
49
+ rostree # Launch interactive TUI
50
+ rostree scan # Scan host for ROS 2 workspaces
51
+ rostree scan ~/dev --depth 3 # Scan specific directories
52
+ rostree list # List known packages
53
+ rostree list --by-source # List packages grouped by source
54
+ rostree tree rclpy # Show dependency tree for a package
55
+ rostree tree rclpy --depth 3 # Limit tree depth
56
+ rostree tree rclpy --json # Output as JSON
57
+ ```
58
+
59
+ ### TUI mode
60
+
61
+ ```bash
62
+ rostree tui # Interactive terminal UI
63
+ rostree tui rclpy # Start TUI with a specific package tree
64
+ ```
65
+
66
+ ### Python API
67
+
68
+ ```python
69
+ from rostree import list_known_packages, get_package_info, build_tree, scan_workspaces
70
+
71
+ packages = list_known_packages()
72
+ root = build_tree("rclpy", max_depth=5, runtime_only=True)
73
+ workspaces = scan_workspaces() # Scan host for ROS 2 workspaces
74
+ ```
75
+
76
+ ## Links
77
+
78
+ - [How the system works](docs/overview.md)
79
+ - [How packages are found](docs/package-discovery.md) (workspaces, AMENT_PREFIX_PATH, COLCON_WORKSPACE)
80
+ - [Dependency trees](docs/dependency-trees.md) (package.xml, runtime_only)
81
+ - [Usage](docs/usage.md) (CLI, TUI keys, API)
82
+ - [Development](docs/development.md) (layout, pre-commit, CI)
@@ -0,0 +1,14 @@
1
+ rostree/__init__.py,sha256=6kFb52bjBJQMUbYeGOGuRDKpZjwKPEj1d91pw80xtD8,437
2
+ rostree/api.py,sha256=A2L7uuj3efJXwqazsL9j6y6hgkwxPBnwGv2dYNtFTwg,3646
3
+ rostree/cli.py,sha256=gdRS-Bj_25KQ95fZhD-HRbmQFEJGl7NqnAV4kSnGEGg,9073
4
+ rostree/core/__init__.py,sha256=glLk6MYQvGz0jTre7cVaQhQ6PiAAWa1lXFFMFnWbHbo,582
5
+ rostree/core/finder.py,sha256=lmfzjjHX-bDHprs3x-GKPztpCS4hVsZUq2fZOg64DTg,16801
6
+ rostree/core/parser.py,sha256=_Yhyjrg7Ir-N80KqJnxPYRcejWepDZDFbw_BTKLBySs,3094
7
+ rostree/core/tree.py,sha256=jBvd7_Pzk4ciO9dWV1r4NyU9q6BaXKiYRimbmK72oD4,4060
8
+ rostree/tui/__init__.py,sha256=hHtp0ZeBL6drxVxnBi1pm2uRds5O-UbNASQ6xRzOJrs,64
9
+ rostree/tui/app.py,sha256=_vj47K70w5ZJFANRisENHk9UXlwH8YBVh_qKwfxgQv8,18678
10
+ rostree-0.1.0.dist-info/METADATA,sha256=2uYP2pwyhSwBw5F1ye-p3jU5L3a3fk6YkiXLI_nggZg,3218
11
+ rostree-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
12
+ rostree-0.1.0.dist-info/entry_points.txt,sha256=3FqDola110oRFImx5-IF3CEnfCXA3CDSM2fTzbES2Zo,45
13
+ rostree-0.1.0.dist-info/licenses/LICENSE,sha256=mcuqLv_cT8O1n1fIsOkUNDMnKlrPgP5LVCMyyVl-tGk,1530
14
+ rostree-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ rostree = rostree.cli:main
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, The rosdep_viz authors
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.