pybricks-cli 1.0.0b1__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.
- pb_cli/__init__.py +8 -0
- pb_cli/__main__.py +7 -0
- pb_cli/app.py +127 -0
- pb_cli/banner.py +338 -0
- pb_cli/cli.py +123 -0
- pb_cli/commands/__init__.py +16 -0
- pb_cli/commands/hub.py +637 -0
- pb_cli/commands/program.py +374 -0
- pb_cli/commands/repl.py +146 -0
- pb_cli/commands/service.py +369 -0
- pb_cli/config.py +213 -0
- pb_cli/console/__init__.py +117 -0
- pb_cli/console/posix.py +125 -0
- pb_cli/console/windows.py +586 -0
- pb_cli/errors.py +680 -0
- pb_cli/escape.py +258 -0
- pb_cli/keys.py +332 -0
- pb_cli/picker.py +124 -0
- pb_cli/py.typed +0 -0
- pb_cli/session.py +813 -0
- pb_cli/ui.py +454 -0
- pybricks_cli-1.0.0b1.dist-info/METADATA +59 -0
- pybricks_cli-1.0.0b1.dist-info/RECORD +26 -0
- pybricks_cli-1.0.0b1.dist-info/WHEEL +4 -0
- pybricks_cli-1.0.0b1.dist-info/entry_points.txt +3 -0
- pybricks_cli-1.0.0b1.dist-info/licenses/LICENSE +21 -0
pb_cli/picker.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 The Pybricks Authors
|
|
3
|
+
|
|
4
|
+
"""Choosing a hub from the ones the radio can hear.
|
|
5
|
+
|
|
6
|
+
The list is being written while it is being read: hubs appear as they are
|
|
7
|
+
heard, and a hub already listed changes as its signal does. So the display is
|
|
8
|
+
redrawn on both counts, and the selection follows the hub it is on rather than
|
|
9
|
+
the row number, or switching a second hub on would move the highlight off the
|
|
10
|
+
one the user had settled on.
|
|
11
|
+
|
|
12
|
+
There is no time limit. The person may be fetching the hub from another room,
|
|
13
|
+
and a deadline on a person only ever expires at the worst moment. Leaving
|
|
14
|
+
without choosing is an ordinary outcome rather than an error, so Escape, ``q``
|
|
15
|
+
and Ctrl-C all simply return nothing.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
from collections.abc import AsyncIterator, Callable
|
|
22
|
+
|
|
23
|
+
import anyio
|
|
24
|
+
from rich.console import Console, Group, RenderableType
|
|
25
|
+
from rich.text import Text
|
|
26
|
+
|
|
27
|
+
import pb_client
|
|
28
|
+
from pb_cli import keys, ui
|
|
29
|
+
from pb_cli.keys import Key
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
__all__ = ["choose_hub"]
|
|
34
|
+
|
|
35
|
+
# The line under the list. What Enter does depends on who is asking --
|
|
36
|
+
# connecting to a hub and choosing which one to work with are different
|
|
37
|
+
# answers -- so the verb is the caller's to supply. A comment rather than a
|
|
38
|
+
# docstring: a documented attribute is rendered into the API reference, and
|
|
39
|
+
# the arrows in it cannot be encoded by the formatter the renderer shells out
|
|
40
|
+
# to on a Windows machine with a legacy code page.
|
|
41
|
+
HELP = "↑↓ to choose, Enter to {verb}, Ctrl-C to stop looking"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def choose_hub(
|
|
45
|
+
err: Console,
|
|
46
|
+
devices: AsyncIterator[pb_client.DeviceInfo],
|
|
47
|
+
render: Callable[[list[pb_client.DeviceInfo], int], RenderableType],
|
|
48
|
+
verb: str = "connect",
|
|
49
|
+
) -> pb_client.DeviceInfo | None:
|
|
50
|
+
"""Show hubs as they are found and let the user pick one.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
err: The console to draw on. Not stdout: a chooser is not data, and
|
|
54
|
+
the hub that comes out of it may well be piped somewhere.
|
|
55
|
+
devices: Hubs as they are heard, one item per advertisement.
|
|
56
|
+
render: Draws the list, given the hubs in the order first seen and the
|
|
57
|
+
index of the selected one. Passed in so that this module does not
|
|
58
|
+
have to know what a hub looks like on screen.
|
|
59
|
+
verb: What pressing Enter will do, for the help line.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
The hub the user chose, or ``None`` if they left without choosing.
|
|
63
|
+
"""
|
|
64
|
+
seen: dict[str, pb_client.DeviceInfo] = {}
|
|
65
|
+
order: list[str] = []
|
|
66
|
+
selected = 0
|
|
67
|
+
chosen: pb_client.DeviceInfo | None = None
|
|
68
|
+
|
|
69
|
+
def frame() -> RenderableType:
|
|
70
|
+
listed = [seen[device_id] for device_id in order]
|
|
71
|
+
return Group(
|
|
72
|
+
render(listed, selected),
|
|
73
|
+
Text(HELP.format(verb=verb), style="dim"),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
with keys.reading_keys(), ui.live(frame(), err) as live:
|
|
77
|
+
async with anyio.create_task_group() as tg:
|
|
78
|
+
|
|
79
|
+
async def watch() -> None:
|
|
80
|
+
async for device in devices:
|
|
81
|
+
if device.id not in seen:
|
|
82
|
+
order.append(device.id)
|
|
83
|
+
|
|
84
|
+
seen[device.id] = device
|
|
85
|
+
live.update(frame(), refresh=True)
|
|
86
|
+
|
|
87
|
+
tg.start_soon(watch)
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
while True:
|
|
91
|
+
key = keys.poll_key()
|
|
92
|
+
if key is Key.CANCEL:
|
|
93
|
+
break
|
|
94
|
+
|
|
95
|
+
if key is Key.ENTER and order:
|
|
96
|
+
chosen = seen[order[selected]]
|
|
97
|
+
break
|
|
98
|
+
|
|
99
|
+
if key in (Key.UP, Key.DOWN) and order:
|
|
100
|
+
step = -1 if key is Key.UP else 1
|
|
101
|
+
# Wrapping, because a list of two with the highlight
|
|
102
|
+
# stuck at one end is a worse surprise than either
|
|
103
|
+
# choice here.
|
|
104
|
+
selected = (selected + step) % len(order)
|
|
105
|
+
live.update(frame(), refresh=True)
|
|
106
|
+
elif key is None:
|
|
107
|
+
# Nothing typed, so wait for something to be. Waiting
|
|
108
|
+
# rather than sleeping costs nothing here and is what
|
|
109
|
+
# the house rule asks for; the list goes on filling
|
|
110
|
+
# in either way, because the task above redraws it as
|
|
111
|
+
# each hub is heard rather than on a timer.
|
|
112
|
+
await keys.wait_for_key()
|
|
113
|
+
except KeyboardInterrupt:
|
|
114
|
+
# Caught here rather than left to the top level, because the
|
|
115
|
+
# task group would wrap it and because giving up on finding a
|
|
116
|
+
# hub is not a failure. Caught inside the group so that it is
|
|
117
|
+
# never wrapped in the first place.
|
|
118
|
+
logger.debug("nothing chosen: interrupted")
|
|
119
|
+
finally:
|
|
120
|
+
# The scan belongs to the caller, but nothing started here
|
|
121
|
+
# should outlive the choosing.
|
|
122
|
+
tg.cancel_scope.cancel()
|
|
123
|
+
|
|
124
|
+
return chosen
|
pb_cli/py.typed
ADDED
|
File without changes
|