RemoteRF 2.0.9__tar.gz → 2.0.10__tar.gz

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.
Files changed (65) hide show
  1. {remoterf-2.0.9/src/remoterf.egg-info → remoterf-2.0.10}/PKG-INFO +1 -1
  2. {remoterf-2.0.9 → remoterf-2.0.10}/setup.py +1 -1
  3. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/common/utils/__init__.py +13 -1
  4. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/common/utils/ansi_codes.py +2 -0
  5. remoterf-2.0.10/src/remoteRF/common/utils/banner.py +362 -0
  6. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/core/app.py +44 -36
  7. remoterf-2.0.10/src/remoteRF/drivers/_virtual.py +145 -0
  8. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/adalm_pluto/pluto_remote.py +40 -13
  9. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/dynamic_device.py +35 -7
  10. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/remoterf_cli.py +42 -5
  11. {remoterf-2.0.9 → remoterf-2.0.10/src/remoterf.egg-info}/PKG-INFO +1 -1
  12. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoterf.egg-info/SOURCES.txt +5 -0
  13. remoterf-2.0.10/tests/test_banner.py +124 -0
  14. remoterf-2.0.10/tests/test_pluto_virtual.py +58 -0
  15. remoterf-2.0.10/tests/test_remoterf_cli.py +53 -0
  16. {remoterf-2.0.9 → remoterf-2.0.10}/LICENSE +0 -0
  17. {remoterf-2.0.9 → remoterf-2.0.10}/MANIFEST.in +0 -0
  18. {remoterf-2.0.9 → remoterf-2.0.10}/README.md +0 -0
  19. {remoterf-2.0.9 → remoterf-2.0.10}/setup.cfg +0 -0
  20. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/__init__.py +0 -0
  21. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/common/__init__.py +0 -0
  22. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/common/grpc/__init__.py +0 -0
  23. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/common/grpc/grpc_pb2.py +0 -0
  24. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/common/grpc/grpc_pb2_grpc.py +0 -0
  25. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/common/grpc/v2_codec.py +0 -0
  26. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/common/tos_notice.txt +0 -0
  27. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/common/utils/api_token.py +0 -0
  28. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/common/utils/list_string.py +0 -0
  29. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/common/utils/process_arg.py +0 -0
  30. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/config/__init__.py +0 -0
  31. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/config/cert_fetcher.py +0 -0
  32. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/config/config.py +0 -0
  33. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/core/__init__.py +0 -0
  34. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/core/acc_login.py +0 -0
  35. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/core/banner_note.txt +0 -0
  36. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/core/dynamic_v2_transport.py +0 -0
  37. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/core/grpc_acc.py +0 -0
  38. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/core/grpc_client.py +0 -0
  39. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/core/v2_errors.py +0 -0
  40. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/__init__.py +0 -0
  41. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/adalm_pluto/__init__.py +0 -0
  42. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/dynamic_v2.py +0 -0
  43. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/hackrf/__init__.py +0 -0
  44. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/hackrf/hackrf_remote.py +0 -0
  45. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/rtl_sdr/__init__.py +0 -0
  46. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/rtl_sdr/rtl_sdr_remote.py +0 -0
  47. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/support/__init__.py +0 -0
  48. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/support/ti_mmwave.py +0 -0
  49. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/support/uhd.py +0 -0
  50. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/support/uhd_dsp.py +0 -0
  51. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/support/uhd_v2.py +0 -0
  52. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/ti_mmwave/__init__.py +0 -0
  53. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/ti_mmwave/ti_mmwave_remote.py +0 -0
  54. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/usrp/__init__.py +0 -0
  55. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/usrp/usrp_remote.py +0 -0
  56. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/usrp/usrp_remote.pyi +0 -0
  57. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/version.py +0 -0
  58. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoterf.egg-info/dependency_links.txt +0 -0
  59. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoterf.egg-info/entry_points.txt +0 -0
  60. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoterf.egg-info/requires.txt +0 -0
  61. {remoterf-2.0.9 → remoterf-2.0.10}/src/remoterf.egg-info/top_level.txt +0 -0
  62. {remoterf-2.0.9 → remoterf-2.0.10}/tests/test_dynamic_device_codegen.py +0 -0
  63. {remoterf-2.0.9 → remoterf-2.0.10}/tests/test_dynamic_v2.py +0 -0
  64. {remoterf-2.0.9 → remoterf-2.0.10}/tests/test_process_arg.py +0 -0
  65. {remoterf-2.0.9 → remoterf-2.0.10}/tests/test_ti_mmwave.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoterf
3
- Version: 2.0.9
3
+ Version: 2.0.10
4
4
  Summary: A python API to remotely access signal centric hardware. Client-side only! Courtesy of Wireless Lab @ UCLA & Prof. Ian Roberts.
5
5
  Author: Ethan Ge
6
6
  Author-email: ethoGalaxy@gmail.com
@@ -21,7 +21,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
21
21
 
22
22
  setup(
23
23
  name="remoterf",
24
- version="2.0.9",
24
+ version="2.0.10",
25
25
  author="Ethan Ge",
26
26
  author_email="ethoGalaxy@gmail.com",
27
27
  description="A python API to remotely access signal centric hardware. Client-side only! Courtesy of Wireless Lab @ UCLA & Prof. Ian Roberts.",
@@ -16,4 +16,16 @@
16
16
  from .api_token import validate_token, generate_token, hash_token
17
17
  from .process_arg import unmap_arg, map_arg
18
18
  from .ansi_codes import printf, stylize, Sty
19
- from .list_string import list_to_str, str_to_list
19
+ from .banner import (
20
+ ASCII_BANNER,
21
+ ASCII_BANNER_MIN_COLUMNS,
22
+ ASCII_BANNER_WIDTH,
23
+ banner_text,
24
+ client_banner_text,
25
+ internal_banner_text,
26
+ print_banner,
27
+ print_client_banner,
28
+ print_internal_banner,
29
+ supports_unicode,
30
+ )
31
+ from .list_string import list_to_str, str_to_list
@@ -39,6 +39,7 @@ class Sty(Enum):
39
39
  BRIGHT_RED = 'bright-red'
40
40
  BRIGHT_GREEN = 'bright-green'
41
41
  BRIGHT_BLUE = 'bright-blue'
42
+ BRIGHT_MAGENTA = 'bright-magenta'
42
43
 
43
44
  # Formatting
44
45
  BOLD = 'bold'
@@ -70,6 +71,7 @@ _COLOR_STYLE_MAP = {
70
71
  'bright-red': 'fg:ansibrightred',
71
72
  'bright-green': 'fg:ansibrightgreen',
72
73
  'bright-blue': 'fg:ansibrightblue',
74
+ 'bright-magenta': 'fg:ansibrightmagenta',
73
75
  }
74
76
 
75
77
  _MONO_STYLES = {
@@ -0,0 +1,362 @@
1
+ # Copyright (C) 2026 RemoteRF
2
+ #
3
+ # This program is free software: you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by
5
+ # the Free Software Foundation, either version 3 of the License, or
6
+ # (at your option) any later version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
15
+
16
+ from __future__ import annotations
17
+
18
+ import os
19
+ import shutil
20
+ import sys
21
+
22
+ from .ansi_codes import Sty, printf
23
+
24
+
25
+ ASCII_BANNER = "\n".join(
26
+ line.ljust(69)
27
+ for line in (
28
+ "██████╗ ███████╗███╗ ███╗ ██████╗ ████████╗███████╗██████╗ ███████╗",
29
+ "██╔══██╗██╔════╝████╗ ████║██╔═══██╗╚══██╔══╝██╔════╝██╔══██╗██╔════╝",
30
+ "██████╔╝█████╗ ██╔████╔██║██║ ██║ ██║ █████╗ ██████╔╝█████╗",
31
+ "██╔══██╗██╔══╝ ██║╚██╔╝██║██║ ██║ ██║ ██╔══╝ ██╔══██╗██╔══╝",
32
+ "██║ ██║███████╗██║ ╚═╝ ██║╚██████╔╝ ██║ ███████╗██║ ██║██║",
33
+ "╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝",
34
+ )
35
+ )
36
+
37
+ ASCII_COMPAT_BANNER = r""" ____ _ ____ _____
38
+ | _ \ ___ _ __ ___ ___ | |_ ___| _ \| ___|
39
+ | |_) / _ \ '_ ` _ \ / _ \| __/ _ \ |_) | |_
40
+ | _ < __/ | | | | | (_) | || __/ _ <| _|
41
+ |_| \_\___|_| |_| |_|\___/ \__\___|_| \_\_|"""
42
+
43
+ BRANDED_ASCII_BANNER = ASCII_BANNER
44
+ BRANDED_ASCII_COMPAT_BANNER = ASCII_COMPAT_BANNER
45
+
46
+ COMPACT_BANNER = "RemoteRF"
47
+ ASCII_WORDMARK_WIDTH = max(len(line) for line in ASCII_BANNER.splitlines())
48
+ ASCII_BANNER_WIDTH = max(len(line) for line in BRANDED_ASCII_BANNER.splitlines())
49
+ ASCII_BANNER_MIN_COLUMNS = ASCII_BANNER_WIDTH + 1
50
+ PANEL_CONTENT_WIDTH = 30
51
+ PANEL_INNER_WIDTH = PANEL_CONTENT_WIDTH + 2
52
+ PANEL_WIDTH = PANEL_INNER_WIDTH + 2
53
+ PANEL_GAP = 1
54
+ SIDE_BY_SIDE_MIN_COLUMNS = ASCII_BANNER_WIDTH + PANEL_GAP + PANEL_WIDTH + 1
55
+ ASCII_RF_COLUMN = 53
56
+ ASCII_COMPAT_RF_COLUMN = 34
57
+
58
+
59
+ def supports_unicode(encoding: str | None = None) -> bool:
60
+ """Return whether the output encoding can represent the rich banner."""
61
+ if os.environ.get("REMOTERF_ASCII", "").strip().lower() in {
62
+ "1",
63
+ "true",
64
+ "yes",
65
+ "on",
66
+ }:
67
+ return False
68
+ encoding = encoding or getattr(sys.stdout, "encoding", None) or "utf-8"
69
+ try:
70
+ BRANDED_ASCII_BANNER.encode(encoding)
71
+ except (LookupError, UnicodeEncodeError):
72
+ return False
73
+ return True
74
+
75
+
76
+ def _logo(unicode_supported: bool) -> str:
77
+ return BRANDED_ASCII_BANNER if unicode_supported else BRANDED_ASCII_COMPAT_BANNER
78
+
79
+
80
+ def _logo_width(unicode_supported: bool) -> int:
81
+ return max(len(line) for line in _logo(unicode_supported).splitlines())
82
+
83
+
84
+ def _side_by_side_min_columns(unicode_supported: bool) -> int:
85
+ return _logo_width(unicode_supported) + PANEL_GAP + PANEL_WIDTH + 1
86
+
87
+
88
+ def _side_by_side_logo(columns: int, unicode_supported: bool) -> str | None:
89
+ if columns >= _side_by_side_min_columns(unicode_supported):
90
+ return _logo(unicode_supported)
91
+ return None
92
+
93
+
94
+ def _fit_panel_text(text: str) -> str:
95
+ text = str(text).replace("\r", " ").replace("\n", " ").strip()
96
+ if len(text) > PANEL_CONTENT_WIDTH:
97
+ text = f"{text[:PANEL_CONTENT_WIDTH - 1]}…"
98
+ return text
99
+
100
+
101
+ def _panel_lines(
102
+ version: str,
103
+ server: str,
104
+ *,
105
+ unicode_supported: bool = True,
106
+ ) -> list[str]:
107
+ horizontal = "─" if unicode_supported else "-"
108
+ top_left, top_right = ("╭", "╮") if unicode_supported else ("+", "+")
109
+ bottom_left, bottom_right = ("╰", "╯") if unicode_supported else ("+", "+")
110
+ vertical = "│" if unicode_supported else "|"
111
+ marker = "◆" if unicode_supported else "*"
112
+ label = f"{horizontal} REMOTERF CLIENT "
113
+ top = f"{top_left}{label}{horizontal * (PANEL_INNER_WIDTH - len(label))}{top_right}"
114
+ content = (
115
+ f"{marker} www.remoterf.net",
116
+ f"VERSION {version}",
117
+ f"SERVER {server or 'NA'}",
118
+ )
119
+ body = [
120
+ f"{vertical} {_fit_panel_text(line):<{PANEL_CONTENT_WIDTH}} {vertical}"
121
+ for line in content
122
+ ]
123
+ bottom = f"{bottom_left}{horizontal * PANEL_INNER_WIDTH}{bottom_right}"
124
+ return [top, *body, bottom]
125
+
126
+
127
+ def _internal_panel_lines(
128
+ version: str,
129
+ server: str,
130
+ username: str,
131
+ tos_url: str,
132
+ *,
133
+ unicode_supported: bool = True,
134
+ ) -> list[str]:
135
+ horizontal = "─" if unicode_supported else "-"
136
+ top_left, top_right = ("╭", "╮") if unicode_supported else ("+", "+")
137
+ bottom_left, bottom_right = ("╰", "╯") if unicode_supported else ("+", "+")
138
+ vertical = "│" if unicode_supported else "|"
139
+ marker = "◆" if unicode_supported else "*"
140
+ label = f"{horizontal} REMOTERF SESSION "
141
+ top = f"{top_left}{label}{horizontal * (PANEL_INNER_WIDTH - len(label))}{top_right}"
142
+ display_tos = str(tos_url).removeprefix("https://").removeprefix("http://")
143
+ content = (
144
+ f"{marker} www.remoterf.net",
145
+ f"VERSION {version}",
146
+ f"SERVER {server or 'NA'}",
147
+ f"ACCOUNT {username or 'NA'}",
148
+ f"TOS {display_tos}",
149
+ )
150
+ body = [
151
+ f"{vertical} {_fit_panel_text(line):<{PANEL_CONTENT_WIDTH}} {vertical}"
152
+ for line in content
153
+ ]
154
+ bottom = f"{bottom_left}{horizontal * PANEL_INNER_WIDTH}{bottom_right}"
155
+ return [top, *body, bottom]
156
+
157
+
158
+ def _compact_lines(
159
+ version: str,
160
+ server: str,
161
+ columns: int,
162
+ *,
163
+ unicode_supported: bool,
164
+ ) -> list[str]:
165
+ width = max(1, int(columns))
166
+ marker = "◆" if unicode_supported else "*"
167
+ separator = "·" if unicode_supported else "-"
168
+ heading = f"{marker} REMOTERF CLIENT"
169
+ if len(heading) > width:
170
+ heading = COMPACT_BANNER[:width]
171
+
172
+ short_version = str(version).split(maxsplit=1)[0]
173
+ server = str(server or "NA")
174
+ detail_options = (
175
+ f"v{version} {separator} {server}",
176
+ f"v{short_version} {separator} {server}",
177
+ f"v{short_version}",
178
+ )
179
+ details = next((line for line in detail_options if len(line) <= width), "")
180
+ if not details:
181
+ details = detail_options[-1][:width]
182
+ return [heading] if not details else [heading, details]
183
+
184
+
185
+ def _styled_panel_args(line: str, index: int, total: int) -> tuple:
186
+ """Return printf arguments with a default-color frame and gray contents."""
187
+ if not line:
188
+ return ("", Sty.DEFAULT)
189
+ if index in {0, total - 1}:
190
+ return (line, Sty.DEFAULT)
191
+ return (
192
+ line[0],
193
+ Sty.DEFAULT,
194
+ line[1:-1],
195
+ Sty.GRAY,
196
+ line[-1],
197
+ Sty.DEFAULT,
198
+ )
199
+
200
+
201
+ def _styled_logo_args(
202
+ line: str,
203
+ index: int,
204
+ *,
205
+ unicode_supported: bool,
206
+ ) -> tuple:
207
+ """Return printf arguments for the pink REMOTE / blue RF wordmark."""
208
+ wordmark_width = (
209
+ ASCII_WORDMARK_WIDTH
210
+ if unicode_supported
211
+ else max(len(part) for part in ASCII_COMPAT_BANNER.splitlines())
212
+ )
213
+ rf_column = ASCII_RF_COLUMN if unicode_supported else ASCII_COMPAT_RF_COLUMN
214
+ return (
215
+ line[:rf_column],
216
+ (Sty.BOLD, Sty.BRIGHT_MAGENTA),
217
+ line[rf_column:wordmark_width],
218
+ (Sty.BOLD, Sty.BRIGHT_BLUE),
219
+ )
220
+
221
+
222
+ def banner_text(
223
+ columns: int | None = None,
224
+ *,
225
+ unicode_supported: bool | None = None,
226
+ ) -> str:
227
+ """Return branding that fits without wrapping in the current terminal."""
228
+ if columns is None:
229
+ columns = shutil.get_terminal_size(fallback=(80, 24)).columns
230
+ if unicode_supported is None:
231
+ unicode_supported = supports_unicode()
232
+ logo = _logo(unicode_supported)
233
+ return logo if columns > _logo_width(unicode_supported) else COMPACT_BANNER
234
+
235
+
236
+ def print_banner(columns: int | None = None) -> None:
237
+ printf(banner_text(columns), (Sty.BOLD, Sty.BRIGHT_BLUE))
238
+
239
+
240
+ def client_banner_text(
241
+ version: str,
242
+ columns: int,
243
+ *,
244
+ server: str = "",
245
+ unicode_supported: bool = True,
246
+ ) -> str:
247
+ """Build a plain-text preview of the responsive first-login header."""
248
+ logo_text = _side_by_side_logo(columns, unicode_supported)
249
+ panel = _panel_lines(version, server, unicode_supported=unicode_supported)
250
+ if logo_text is not None:
251
+ logo_width = max(len(line) for line in logo_text.splitlines())
252
+ logo = logo_text.splitlines()
253
+ height = max(len(logo), len(panel))
254
+ logo.extend([""] * (height - len(logo)))
255
+ panel_top = (height - len(panel)) // 2
256
+ panel = ([""] * panel_top) + panel + ([""] * (height - panel_top - len(panel)))
257
+ return "\n".join(
258
+ f"{logo_line:<{logo_width}}{' ' * PANEL_GAP}{panel_line}".rstrip()
259
+ for logo_line, panel_line in zip(logo, panel)
260
+ )
261
+ if columns > PANEL_WIDTH:
262
+ return "\n".join(panel)
263
+
264
+ return "\n".join(
265
+ _compact_lines(
266
+ version,
267
+ server,
268
+ columns,
269
+ unicode_supported=unicode_supported,
270
+ )
271
+ )
272
+
273
+
274
+ def print_client_banner(
275
+ version: str,
276
+ columns: int | None = None,
277
+ *,
278
+ server: str = "",
279
+ ) -> None:
280
+ """Print the responsive, styled header shown once before client login."""
281
+ if columns is None:
282
+ columns = shutil.get_terminal_size(fallback=(80, 24)).columns
283
+
284
+ unicode_supported = supports_unicode()
285
+ logo_text = _side_by_side_logo(columns, unicode_supported)
286
+ panel = _panel_lines(version, server, unicode_supported=unicode_supported)
287
+ if logo_text is not None:
288
+ logo_width = max(len(line) for line in logo_text.splitlines())
289
+ logo = logo_text.splitlines()
290
+ panel_height = len(panel)
291
+ height = max(len(logo), len(panel))
292
+ logo.extend([""] * (height - len(logo)))
293
+ panel_top = (height - len(panel)) // 2
294
+ panel = ([""] * panel_top) + panel + ([""] * (height - panel_top - len(panel)))
295
+ for index, (logo_line, panel_line) in enumerate(zip(logo, panel)):
296
+ panel_index = index - panel_top
297
+ printf(
298
+ *_styled_logo_args(
299
+ f"{logo_line:<{logo_width}}",
300
+ index,
301
+ unicode_supported=unicode_supported,
302
+ ),
303
+ " " * PANEL_GAP,
304
+ Sty.DEFAULT,
305
+ *_styled_panel_args(panel_line, panel_index, panel_height),
306
+ )
307
+ return
308
+
309
+ if columns > PANEL_WIDTH:
310
+ for index, panel_line in enumerate(panel):
311
+ printf(*_styled_panel_args(panel_line, index, len(panel)))
312
+ return
313
+
314
+ compact = _compact_lines(
315
+ version,
316
+ server,
317
+ columns,
318
+ unicode_supported=unicode_supported,
319
+ )
320
+ printf(compact[0], (Sty.BOLD, Sty.DEFAULT))
321
+ if len(compact) > 1:
322
+ printf(compact[1], Sty.GRAY)
323
+
324
+
325
+ def internal_banner_text(
326
+ version: str,
327
+ *,
328
+ server: str = "",
329
+ username: str = "",
330
+ tos_url: str = "https://remoterf.net/tos",
331
+ unicode_supported: bool = True,
332
+ ) -> str:
333
+ """Build the boxed header displayed after a successful login."""
334
+ return "\n".join(
335
+ _internal_panel_lines(
336
+ version,
337
+ server,
338
+ username,
339
+ tos_url,
340
+ unicode_supported=unicode_supported,
341
+ )
342
+ )
343
+
344
+
345
+ def print_internal_banner(
346
+ version: str,
347
+ *,
348
+ server: str = "",
349
+ username: str = "",
350
+ tos_url: str = "https://remoterf.net/tos",
351
+ ) -> None:
352
+ """Print the styled boxed header displayed after a successful login."""
353
+ unicode_supported = supports_unicode()
354
+ lines = _internal_panel_lines(
355
+ version,
356
+ server,
357
+ username,
358
+ tos_url,
359
+ unicode_supported=unicode_supported,
360
+ )
361
+ for index, line in enumerate(lines):
362
+ printf(*_styled_panel_args(line, index, len(lines)))
@@ -13,7 +13,7 @@
13
13
  # You should have received a copy of the GNU General Public License
14
14
  # along with this program. If not, see <https://www.gnu.org/licenses/>.
15
15
 
16
- from remoteRF.core.grpc_client import handle_admin_command
16
+ from remoteRF.core.grpc_client import addr as server_addr, handle_admin_command
17
17
  from . import *
18
18
  from ..common.utils import *
19
19
 
@@ -29,23 +29,17 @@ from prompt_toolkit import PromptSession
29
29
  account = RemoteRFAccount()
30
30
  session = PromptSession()
31
31
 
32
- DEFAULT_TIMEZONE_NOTE = "All times are in Pacific Time (Los Angeles)"
33
32
  DEFAULT_TOS_URL = "https://remoterf.net/tos"
34
33
  SERVER_TIME_FORMAT = "%Y-%m-%d %H:%M:%S"
35
- BANNER_CREDIT = "Created at UCLA by E. Ge and I. Roberts."
36
34
 
37
35
 
38
- def _banner_note() -> str:
39
- note_path = Path(__file__).with_name("banner_note.txt")
40
- try:
41
- text = note_path.read_text(encoding="utf-8").strip()
42
- except OSError:
43
- return DEFAULT_TIMEZONE_NOTE
44
- return text or DEFAULT_TIMEZONE_NOTE
45
-
46
-
47
- def _banner_details() -> str:
48
- return f"\n{BANNER_CREDIT}\nCurrent version: {print_my_version()}\n{_banner_note()}"
36
+ def _clear_terminal() -> None:
37
+ """Clear without forking after gRPC has started background threads."""
38
+ if os.name == "nt":
39
+ os.system("cls")
40
+ print()
41
+ return
42
+ print("\033[2J\033[H")
49
43
 
50
44
 
51
45
  def _tos_notice_text() -> str:
@@ -85,8 +79,9 @@ def _format_reservation_range(start_time: datetime.datetime, end_time: datetime.
85
79
  end = f"{end_time.strftime('%Y-%m-%d')} {end}"
86
80
  return f"{start} - {end}"
87
81
 
88
- def welcome():
89
- printf(f"Welcome to the RemoteRF Platform", (Sty.BOLD, Sty.BLUE), _banner_details(), (Sty.GRAY))
82
+ def welcome(*, show_banner: bool = True):
83
+ if show_banner:
84
+ print_client_banner(print_my_version(), server=server_addr)
90
85
  try:
91
86
  inpu = session.prompt(stylize("Please ", Sty.DEFAULT, "login", Sty.GREEN, " or ", Sty.DEFAULT, "register", Sty.RED, " to continue. (", Sty.DEFAULT, 'l', Sty.GREEN, "/", Sty.DEFAULT, 'r', Sty.RED, "): ", Sty.DEFAULT))
92
87
  if inpu == 'r':
@@ -105,28 +100,31 @@ def welcome():
105
100
  account.password = password
106
101
  account.email = input("Email: ") # TODO: Email verification.
107
102
  # check if login was valid
108
- os.system('cls' if os.name == 'nt' else 'clear')
103
+ _clear_terminal()
109
104
 
110
105
  if not account.create_user():
111
- welcome()
106
+ welcome(show_banner=False)
112
107
  else:
113
108
  account.username = input("Username: ")
114
109
  account.password = getpass.getpass("Password (Hidden): ")
115
110
  # check if login was valid
116
111
  if not account.login_user():
117
- os.system('cls' if os.name == 'nt' else 'clear')
112
+ _clear_terminal()
118
113
  print("Invalid login. Try again. Contact admin(s) if you forgot your password.")
119
- welcome()
114
+ welcome(show_banner=False)
120
115
  except KeyboardInterrupt:
121
116
  exit()
122
117
  except EOFError:
123
118
  exit()
124
119
 
125
120
  def title():
126
- printf(f"Welcome to the RemoteRF Platform", (Sty.BOLD, Sty.BLUE), _banner_details(), (Sty.GRAY))
127
- printf("Terms of Service: ", Sty.BOLD, _tos_url(), Sty.DEFAULT)
128
- # printf(f"Logged in as: ", Sty.DEFAULT, f'{account.username}', Sty.MAGENTA)
129
- printf(f"Input ", Sty.DEFAULT, "'help' ", Sty.BRIGHT_GREEN, "for a list of avaliable commands.", Sty.DEFAULT)
121
+ print_internal_banner(
122
+ print_my_version(),
123
+ server=server_addr,
124
+ username=account.username,
125
+ tos_url=_tos_url(),
126
+ )
127
+ printf("Input ", Sty.DEFAULT, "'help' ", Sty.BRIGHT_GREEN, "for a list of available commands.", Sty.DEFAULT)
130
128
 
131
129
  def commands():
132
130
  printf("Commands:", (Sty.BOLD, Sty.BLUE))
@@ -158,7 +156,7 @@ def commands():
158
156
 
159
157
 
160
158
  def clear():
161
- os.system('cls' if os.name == 'nt' else 'clear')
159
+ _clear_terminal()
162
160
  title()
163
161
 
164
162
  def print_my_version():
@@ -169,23 +167,33 @@ def print_my_version():
169
167
  top = __name__.split('.')[0]
170
168
  # Try mapping package → distribution (Py3.10+); fall back to same name.
171
169
  for dist in getattr(md, "packages_distributions", lambda: {})().get(top, []):
172
- if (latest == md.version(dist)):
173
- return f"{md.version(dist)} (latest)"
174
- else:
175
- return f"{md.version(dist)} (OUTDATED)"
170
+ installed = md.version(dist)
171
+ if latest is None:
172
+ return installed
173
+ if latest == installed:
174
+ return f"{installed} (latest)"
175
+ return f"{installed} (OUTDATED)"
176
176
  return md.version(top)
177
177
  except Exception:
178
178
  # Last resort: __version__ attribute if you define it.
179
179
  return getattr(sys.modules.get(__name__.split('.')[0]), "__version__", "unknown")
180
180
 
181
181
  def newest_version_pip(project="remoterf"):
182
- import sys, subprocess, re
183
- out = subprocess.check_output(
184
- [sys.executable, "-m", "pip", "index", "versions", project],
185
- text=True, stderr=subprocess.STDOUT
186
- )
187
- m = re.search(r"(?i)\blatest\s*:\s*([^\s,]+)", out)
188
- return m.group(1) if m else None
182
+ """Fetch the published version without spawning a process beside gRPC."""
183
+ import json
184
+ from urllib.parse import quote
185
+ from urllib.request import Request, urlopen
186
+
187
+ try:
188
+ request = Request(
189
+ f"https://pypi.org/pypi/{quote(project)}/json",
190
+ headers={"User-Agent": "RemoteRF-Client"},
191
+ )
192
+ with urlopen(request, timeout=2.0) as response:
193
+ payload = json.load(response)
194
+ return payload.get("info", {}).get("version")
195
+ except (OSError, ValueError, TypeError):
196
+ return None
189
197
 
190
198
 
191
199
  def reservations():