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.
- {remoterf-2.0.9/src/remoterf.egg-info → remoterf-2.0.10}/PKG-INFO +1 -1
- {remoterf-2.0.9 → remoterf-2.0.10}/setup.py +1 -1
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/common/utils/__init__.py +13 -1
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/common/utils/ansi_codes.py +2 -0
- remoterf-2.0.10/src/remoteRF/common/utils/banner.py +362 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/core/app.py +44 -36
- remoterf-2.0.10/src/remoteRF/drivers/_virtual.py +145 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/adalm_pluto/pluto_remote.py +40 -13
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/dynamic_device.py +35 -7
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/remoterf_cli.py +42 -5
- {remoterf-2.0.9 → remoterf-2.0.10/src/remoterf.egg-info}/PKG-INFO +1 -1
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoterf.egg-info/SOURCES.txt +5 -0
- remoterf-2.0.10/tests/test_banner.py +124 -0
- remoterf-2.0.10/tests/test_pluto_virtual.py +58 -0
- remoterf-2.0.10/tests/test_remoterf_cli.py +53 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/LICENSE +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/MANIFEST.in +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/README.md +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/setup.cfg +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/__init__.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/common/__init__.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/common/grpc/__init__.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/common/grpc/grpc_pb2.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/common/grpc/grpc_pb2_grpc.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/common/grpc/v2_codec.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/common/tos_notice.txt +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/common/utils/api_token.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/common/utils/list_string.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/common/utils/process_arg.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/config/__init__.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/config/cert_fetcher.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/config/config.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/core/__init__.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/core/acc_login.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/core/banner_note.txt +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/core/dynamic_v2_transport.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/core/grpc_acc.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/core/grpc_client.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/core/v2_errors.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/__init__.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/adalm_pluto/__init__.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/dynamic_v2.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/hackrf/__init__.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/hackrf/hackrf_remote.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/rtl_sdr/__init__.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/rtl_sdr/rtl_sdr_remote.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/support/__init__.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/support/ti_mmwave.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/support/uhd.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/support/uhd_dsp.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/support/uhd_v2.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/ti_mmwave/__init__.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/ti_mmwave/ti_mmwave_remote.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/usrp/__init__.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/usrp/usrp_remote.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/drivers/usrp/usrp_remote.pyi +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoteRF/version.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoterf.egg-info/dependency_links.txt +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoterf.egg-info/entry_points.txt +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoterf.egg-info/requires.txt +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/src/remoterf.egg-info/top_level.txt +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/tests/test_dynamic_device_codegen.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/tests/test_dynamic_v2.py +0 -0
- {remoterf-2.0.9 → remoterf-2.0.10}/tests/test_process_arg.py +0 -0
- {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.
|
|
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.
|
|
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 .
|
|
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
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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():
|