runtimepy 5.14.2__py3-none-any.whl → 5.15.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.
- runtimepy/__init__.py +2 -2
- runtimepy/channel/__init__.py +1 -4
- runtimepy/channel/environment/__init__.py +93 -2
- runtimepy/channel/environment/create.py +16 -1
- runtimepy/channel/environment/sample.py +10 -2
- runtimepy/channel/registry.py +2 -3
- runtimepy/codec/protocol/base.py +34 -14
- runtimepy/codec/protocol/json.py +5 -3
- runtimepy/codec/system/__init__.py +6 -2
- runtimepy/control/source.py +1 -1
- runtimepy/data/404.md +16 -0
- runtimepy/data/base.yaml +3 -0
- runtimepy/data/css/bootstrap_extra.css +59 -44
- runtimepy/data/css/main.css +23 -4
- runtimepy/data/dummy_load.yaml +2 -2
- runtimepy/data/factories.yaml +1 -0
- runtimepy/data/js/classes/App.js +54 -2
- runtimepy/data/js/classes/ChannelTable.js +6 -8
- runtimepy/data/js/classes/Plot.js +9 -4
- runtimepy/data/js/classes/TabFilter.js +47 -9
- runtimepy/data/js/classes/TabInterface.js +106 -11
- runtimepy/data/js/classes/WindowHashManager.js +30 -15
- runtimepy/data/js/init.js +18 -1
- runtimepy/data/js/markdown_page.js +10 -0
- runtimepy/data/schemas/BitFields.yaml +9 -0
- runtimepy/data/schemas/RuntimeEnum.yaml +6 -0
- runtimepy/data/schemas/StructConfig.yaml +9 -1
- runtimepy/data/static/css/bootstrap-icons.min.css +4 -3
- runtimepy/data/static/css/bootstrap.min.css +3 -4
- runtimepy/data/static/css/fonts/bootstrap-icons.woff +0 -0
- runtimepy/data/static/css/fonts/bootstrap-icons.woff2 +0 -0
- runtimepy/data/static/js/bootstrap.bundle.min.js +5 -4
- runtimepy/data/static/js/webglplot.umd.min.js +2 -1
- runtimepy/data/static/svg/outline-dark.svg +22 -0
- runtimepy/data/static/svg/outline-light.svg +22 -0
- runtimepy/enum/__init__.py +13 -1
- runtimepy/enum/registry.py +13 -1
- runtimepy/message/__init__.py +3 -3
- runtimepy/mixins/logging.py +6 -1
- runtimepy/net/__init__.py +0 -2
- runtimepy/net/arbiter/info.py +36 -4
- runtimepy/net/arbiter/struct/__init__.py +3 -2
- runtimepy/net/connection.py +4 -5
- runtimepy/net/html/__init__.py +29 -11
- runtimepy/net/html/bootstrap/__init__.py +2 -2
- runtimepy/net/html/bootstrap/elements.py +44 -24
- runtimepy/net/html/bootstrap/tabs.py +18 -11
- runtimepy/net/http/__init__.py +3 -3
- runtimepy/net/http/request_target.py +3 -3
- runtimepy/net/mixin.py +4 -2
- runtimepy/net/server/__init__.py +16 -9
- runtimepy/net/server/app/__init__.py +1 -0
- runtimepy/net/server/app/create.py +3 -3
- runtimepy/net/server/app/env/__init__.py +28 -4
- runtimepy/net/server/app/env/settings.py +4 -7
- runtimepy/net/server/app/env/tab/controls.py +141 -27
- runtimepy/net/server/app/env/tab/html.py +68 -26
- runtimepy/net/server/app/env/widgets.py +115 -61
- runtimepy/net/server/app/landing_page.py +1 -1
- runtimepy/net/server/html.py +2 -2
- runtimepy/net/server/json.py +1 -1
- runtimepy/net/server/markdown.py +18 -12
- runtimepy/net/server/mux.py +29 -0
- runtimepy/net/stream/__init__.py +6 -5
- runtimepy/net/stream/base.py +4 -2
- runtimepy/net/tcp/connection.py +5 -3
- runtimepy/net/tcp/http/__init__.py +10 -9
- runtimepy/net/tcp/protocol.py +2 -2
- runtimepy/net/tcp/scpi/__init__.py +5 -2
- runtimepy/net/tcp/telnet/__init__.py +2 -1
- runtimepy/net/udp/connection.py +10 -6
- runtimepy/net/udp/protocol.py +5 -6
- runtimepy/net/udp/queue.py +5 -2
- runtimepy/net/udp/tftp/base.py +2 -1
- runtimepy/net/websocket/connection.py +50 -8
- runtimepy/primitives/array/__init__.py +7 -5
- runtimepy/primitives/base.py +3 -2
- runtimepy/primitives/field/__init__.py +35 -2
- runtimepy/primitives/field/fields.py +11 -2
- runtimepy/primitives/field/manager/base.py +19 -2
- runtimepy/primitives/serializable/base.py +5 -2
- runtimepy/primitives/serializable/fixed.py +5 -2
- runtimepy/primitives/serializable/prefixed.py +4 -1
- runtimepy/primitives/types/base.py +4 -1
- runtimepy/primitives/types/bounds.py +10 -4
- runtimepy/registry/__init__.py +20 -0
- runtimepy/registry/name.py +6 -0
- runtimepy/requirements.txt +2 -2
- runtimepy/ui/controls.py +20 -1
- {runtimepy-5.14.2.dist-info → runtimepy-5.15.0.dist-info}/METADATA +6 -6
- {runtimepy-5.14.2.dist-info → runtimepy-5.15.0.dist-info}/RECORD +95 -92
- {runtimepy-5.14.2.dist-info → runtimepy-5.15.0.dist-info}/WHEEL +1 -1
- runtimepy/data/404.html +0 -7
- {runtimepy-5.14.2.dist-info → runtimepy-5.15.0.dist-info}/entry_points.txt +0 -0
- {runtimepy-5.14.2.dist-info → runtimepy-5.15.0.dist-info}/licenses/LICENSE +0 -0
- {runtimepy-5.14.2.dist-info → runtimepy-5.15.0.dist-info}/top_level.txt +0 -0
runtimepy/net/mixin.py
CHANGED
|
@@ -11,10 +11,12 @@ from typing import Callable
|
|
|
11
11
|
from typing import Optional as _Optional
|
|
12
12
|
from typing import cast as _cast
|
|
13
13
|
|
|
14
|
+
# third-party
|
|
15
|
+
from vcorelib.io import BinaryMessage
|
|
16
|
+
|
|
14
17
|
# internal
|
|
15
18
|
from runtimepy.net import IpHost as _IpHost
|
|
16
19
|
from runtimepy.net import normalize_host as _normalize_host
|
|
17
|
-
from runtimepy.net.connection import BinaryMessage as _BinaryMessage
|
|
18
20
|
from runtimepy.net.mtu import ETHERNET_MTU, UDP_DEFAULT_MTU, host_discover_mtu
|
|
19
21
|
|
|
20
22
|
|
|
@@ -23,7 +25,7 @@ class BinaryMessageQueueMixin:
|
|
|
23
25
|
|
|
24
26
|
def __init__(self) -> None:
|
|
25
27
|
"""Initialize this protocol."""
|
|
26
|
-
self.queue: _asyncio.Queue[
|
|
28
|
+
self.queue: _asyncio.Queue[BinaryMessage] = _asyncio.Queue()
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
class TransportMixin:
|
runtimepy/net/server/__init__.py
CHANGED
|
@@ -9,6 +9,7 @@ import logging
|
|
|
9
9
|
import mimetypes
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import Any, Optional, TextIO, Union
|
|
12
|
+
from urllib.parse import urlencode
|
|
12
13
|
|
|
13
14
|
# third-party
|
|
14
15
|
from vcorelib import DEFAULT_ENCODING
|
|
@@ -24,7 +25,8 @@ from runtimepy.net.http.request_target import PathMaybeQuery
|
|
|
24
25
|
from runtimepy.net.http.response import AsyncResponse, ResponseHeader
|
|
25
26
|
from runtimepy.net.server.html import HtmlApp, HtmlApps, get_html, html_handler
|
|
26
27
|
from runtimepy.net.server.json import encode_json, json_handler
|
|
27
|
-
from runtimepy.net.server.markdown import markdown_for_dir
|
|
28
|
+
from runtimepy.net.server.markdown import DIR_FILE, markdown_for_dir
|
|
29
|
+
from runtimepy.net.server.mux import mux_app
|
|
28
30
|
from runtimepy.net.tcp.http import HttpConnection, HttpResult
|
|
29
31
|
from runtimepy.util import normalize_root, path_has_part, read_binary
|
|
30
32
|
|
|
@@ -43,7 +45,7 @@ class RuntimepyServerConnection(HttpConnection):
|
|
|
43
45
|
"""A class implementing a server-connection interface for this package."""
|
|
44
46
|
|
|
45
47
|
# Can register application methods to URL paths.
|
|
46
|
-
apps: HtmlApps = {}
|
|
48
|
+
apps: HtmlApps = {"/mux.html": mux_app}
|
|
47
49
|
default_app: Optional[HtmlApp] = None
|
|
48
50
|
|
|
49
51
|
# Can load additional data into this dictionary for easy HTTP access.
|
|
@@ -58,7 +60,7 @@ class RuntimepyServerConnection(HttpConnection):
|
|
|
58
60
|
# Set these to control meta attributes.
|
|
59
61
|
metadata: dict[str, Optional[str]] = {
|
|
60
62
|
"title": HttpConnection.identity,
|
|
61
|
-
"description":
|
|
63
|
+
"description": f"({HttpConnection.identity})",
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
def add_path(self, path: Pathlike, front: bool = False) -> None:
|
|
@@ -153,7 +155,6 @@ class RuntimepyServerConnection(HttpConnection):
|
|
|
153
155
|
|
|
154
156
|
meta: dict[str, str] = type(self).metadata.copy() # type: ignore
|
|
155
157
|
|
|
156
|
-
meta.setdefault("description", "")
|
|
157
158
|
meta["description"] += (
|
|
158
159
|
" This page was rendered from "
|
|
159
160
|
f"Markdown by {HttpConnection.identity}."
|
|
@@ -202,6 +203,10 @@ class RuntimepyServerConnection(HttpConnection):
|
|
|
202
203
|
candidates: list[Path] = []
|
|
203
204
|
for search in self.paths:
|
|
204
205
|
candidate = search.joinpath(path[0][1:])
|
|
206
|
+
|
|
207
|
+
if candidate.name == DIR_FILE:
|
|
208
|
+
candidate = candidate.parent
|
|
209
|
+
|
|
205
210
|
if candidate.is_dir():
|
|
206
211
|
directories.append((candidate, search))
|
|
207
212
|
candidates.append(candidate.joinpath("index.html"))
|
|
@@ -245,10 +250,9 @@ class RuntimepyServerConnection(HttpConnection):
|
|
|
245
250
|
|
|
246
251
|
# Handle a directory as a last resort.
|
|
247
252
|
if not result and directories:
|
|
248
|
-
candidate, search = directories[0]
|
|
249
253
|
result = self.render_markdown(
|
|
250
254
|
markdown_for_dir(
|
|
251
|
-
|
|
255
|
+
directories, {"applications": self.apps.keys()}
|
|
252
256
|
),
|
|
253
257
|
response,
|
|
254
258
|
path[1],
|
|
@@ -285,7 +289,7 @@ class RuntimepyServerConnection(HttpConnection):
|
|
|
285
289
|
self,
|
|
286
290
|
response: ResponseHeader,
|
|
287
291
|
request: RequestHeader,
|
|
288
|
-
request_data: Optional[
|
|
292
|
+
request_data: Optional[bytearray],
|
|
289
293
|
) -> HttpResult:
|
|
290
294
|
"""Handle POST requests."""
|
|
291
295
|
|
|
@@ -307,7 +311,7 @@ class RuntimepyServerConnection(HttpConnection):
|
|
|
307
311
|
self,
|
|
308
312
|
response: ResponseHeader,
|
|
309
313
|
request: RequestHeader,
|
|
310
|
-
request_data: Optional[
|
|
314
|
+
request_data: Optional[bytearray],
|
|
311
315
|
) -> HttpResult:
|
|
312
316
|
"""Handle GET requests."""
|
|
313
317
|
|
|
@@ -360,4 +364,7 @@ class RuntimepyServerConnection(HttpConnection):
|
|
|
360
364
|
if populated:
|
|
361
365
|
result = stream.getvalue().encode()
|
|
362
366
|
|
|
363
|
-
return result or self.redirect_to(
|
|
367
|
+
return result or self.redirect_to(
|
|
368
|
+
f"/404.html?{urlencode({'target': request.target.raw})}",
|
|
369
|
+
response,
|
|
370
|
+
)
|
|
@@ -74,6 +74,7 @@ async def setup(app: AppInfo) -> int:
|
|
|
74
74
|
html_app = create_app(app, getattr(_import_module(module), method))
|
|
75
75
|
target: str
|
|
76
76
|
for target in app.config_param("http_app_paths", []):
|
|
77
|
+
assert target not in RuntimepyServerConnection.apps, target
|
|
77
78
|
RuntimepyServerConnection.apps[target] = html_app
|
|
78
79
|
|
|
79
80
|
# Register redirects.
|
|
@@ -29,7 +29,7 @@ def config_param(
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
HtmlAppComposer = Callable[
|
|
32
|
-
[AppInfo, Html, RequestHeader, ResponseHeader, Optional[
|
|
32
|
+
[AppInfo, Html, RequestHeader, ResponseHeader, Optional[bytearray]], Html
|
|
33
33
|
]
|
|
34
34
|
|
|
35
35
|
|
|
@@ -43,7 +43,7 @@ def create_cacheable_app(app: AppInfo, compose: HtmlAppComposer) -> HtmlApp:
|
|
|
43
43
|
document: Html,
|
|
44
44
|
request: RequestHeader,
|
|
45
45
|
response: ResponseHeader,
|
|
46
|
-
request_data: Optional[
|
|
46
|
+
request_data: Optional[bytearray],
|
|
47
47
|
) -> Html:
|
|
48
48
|
"""A simple 'Hello, world!' application."""
|
|
49
49
|
|
|
@@ -82,7 +82,7 @@ def create_app(
|
|
|
82
82
|
document: Html,
|
|
83
83
|
request: RequestHeader,
|
|
84
84
|
response: ResponseHeader,
|
|
85
|
-
request_data: Optional[
|
|
85
|
+
request_data: Optional[bytearray],
|
|
86
86
|
) -> Html:
|
|
87
87
|
"""Main package web application."""
|
|
88
88
|
|
|
@@ -109,16 +109,27 @@ def channel_environments(app: AppInfo, tabs: TabbedContent) -> None:
|
|
|
109
109
|
# Remove tab-content scrolling.
|
|
110
110
|
tabs.set_scroll(False)
|
|
111
111
|
|
|
112
|
-
# Tab name filter.
|
|
113
|
-
input_box(tabs.tabs, label="tab", description="Tab name filter.")
|
|
114
|
-
|
|
115
112
|
centered_markdown(
|
|
116
113
|
tabs.tabs,
|
|
117
114
|
app.config_param("top_markdown", "configure `top_markdown`"),
|
|
118
115
|
"border-start",
|
|
119
116
|
"border-bottom",
|
|
120
117
|
"border-end",
|
|
118
|
+
"bg-gradient-tertiary-to-top",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Tab name filter.
|
|
122
|
+
_, label, box = input_box(
|
|
123
|
+
div(tag="form", autocomplete="off", parent=tabs.tabs),
|
|
124
|
+
label="tab",
|
|
125
|
+
description="Tab name filter.",
|
|
126
|
+
placement="bottom",
|
|
127
|
+
icon="funnel",
|
|
128
|
+
spellcheck="false",
|
|
129
|
+
pattern=".* $ @",
|
|
121
130
|
)
|
|
131
|
+
label.add_class("border-top-0")
|
|
132
|
+
box.add_class("border-top-0")
|
|
122
133
|
|
|
123
134
|
populate_tabs(app, tabs)
|
|
124
135
|
|
|
@@ -129,6 +140,18 @@ def channel_environments(app: AppInfo, tabs: TabbedContent) -> None:
|
|
|
129
140
|
icon="table",
|
|
130
141
|
id="channels-button",
|
|
131
142
|
)
|
|
143
|
+
tabs.add_button(
|
|
144
|
+
"Open channel table",
|
|
145
|
+
"",
|
|
146
|
+
icon="arrow-bar-right",
|
|
147
|
+
id="open-channels-button",
|
|
148
|
+
)
|
|
149
|
+
tabs.add_button(
|
|
150
|
+
"Dedent channel table",
|
|
151
|
+
"",
|
|
152
|
+
icon="arrow-bar-left",
|
|
153
|
+
id="dedent-channels-button",
|
|
154
|
+
)
|
|
132
155
|
|
|
133
156
|
# Plot settings modal.
|
|
134
157
|
plot_settings(tabs)
|
|
@@ -149,11 +172,12 @@ def channel_environments(app: AppInfo, tabs: TabbedContent) -> None:
|
|
|
149
172
|
app.config_param("bottom_markdown", "configure `bottom_markdown`"),
|
|
150
173
|
"border-start",
|
|
151
174
|
"border-end",
|
|
175
|
+
"bg-gradient-tertiary-to-bottom",
|
|
152
176
|
)
|
|
153
177
|
|
|
154
178
|
# Add splash screen element.
|
|
155
179
|
div(
|
|
156
180
|
id=f"{PKG_NAME}-splash",
|
|
157
181
|
parent=tabs.container,
|
|
158
|
-
class_str="bg-
|
|
182
|
+
class_str="bg-secondary-subtle bg-gradient",
|
|
159
183
|
)
|
|
@@ -15,17 +15,14 @@ from runtimepy.net.server.app.placeholder import under_construction
|
|
|
15
15
|
def plot_settings(tabs: TabbedContent) -> None:
|
|
16
16
|
"""Create the plot settings modal."""
|
|
17
17
|
|
|
18
|
-
modal = Modal(tabs, name="
|
|
18
|
+
modal = Modal(tabs, name="settings", icon="sliders")
|
|
19
19
|
under_construction(modal.footer)
|
|
20
20
|
|
|
21
|
-
div(tag="h1", text="
|
|
22
|
-
div(tag="hr", parent=modal.body)
|
|
23
|
-
|
|
24
|
-
div(tag="h2", text="plot status", parent=modal.body)
|
|
21
|
+
div(tag="h1", text="plot status", parent=modal.body)
|
|
25
22
|
div(id="plot-status-inner", parent=modal.body)
|
|
26
|
-
div(tag="hr", parent=modal.body)
|
|
27
23
|
|
|
28
|
-
div(tag="
|
|
24
|
+
div(tag="hr", parent=modal.body)
|
|
25
|
+
div(tag="h1", text="minimum transmit period (ms)", parent=modal.body)
|
|
29
26
|
|
|
30
27
|
div(
|
|
31
28
|
tag="p",
|
|
@@ -20,6 +20,7 @@ from runtimepy.net.server.app.env.widgets import (
|
|
|
20
20
|
enum_dropdown,
|
|
21
21
|
value_input_box,
|
|
22
22
|
)
|
|
23
|
+
from runtimepy.ui.controls import Controls, Default
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
def get_channel_kind_str(
|
|
@@ -39,7 +40,7 @@ def get_channel_kind_str(
|
|
|
39
40
|
def default_button(
|
|
40
41
|
parent: Element,
|
|
41
42
|
name: str,
|
|
42
|
-
|
|
43
|
+
default: Default,
|
|
43
44
|
*classes: str,
|
|
44
45
|
front: bool = True,
|
|
45
46
|
) -> Element:
|
|
@@ -49,14 +50,31 @@ def default_button(
|
|
|
49
50
|
parent,
|
|
50
51
|
id=name,
|
|
51
52
|
icon="arrow-counterclockwise",
|
|
52
|
-
title=f"Reset '{name}' to default value '{
|
|
53
|
-
value=
|
|
53
|
+
title=f"Reset '{name}' to default value '{default}'.",
|
|
54
|
+
value=default,
|
|
54
55
|
front=front,
|
|
55
56
|
)
|
|
56
57
|
button.add_class("default-button", *classes)
|
|
57
58
|
return button
|
|
58
59
|
|
|
59
60
|
|
|
61
|
+
def handle_controls(parent: Element, name: str, controls: Controls) -> None:
|
|
62
|
+
"""Add control elements."""
|
|
63
|
+
|
|
64
|
+
# Determine if a slider should be created.
|
|
65
|
+
if "slider" in controls:
|
|
66
|
+
elem = controls["slider"]
|
|
67
|
+
|
|
68
|
+
slider(
|
|
69
|
+
elem["min"], # type: ignore
|
|
70
|
+
elem["max"], # type: ignore
|
|
71
|
+
int(elem["step"]), # type: ignore
|
|
72
|
+
parent=parent,
|
|
73
|
+
id=name,
|
|
74
|
+
title=f"Value control for '{name}'.",
|
|
75
|
+
).add_class("bg-body", "rounded-pill", "me-2")
|
|
76
|
+
|
|
77
|
+
|
|
60
78
|
class ChannelEnvironmentTabControls(ChannelEnvironmentTabBase):
|
|
61
79
|
"""A channel-environment tab interface."""
|
|
62
80
|
|
|
@@ -74,12 +92,18 @@ class ChannelEnvironmentTabControls(ChannelEnvironmentTabBase):
|
|
|
74
92
|
# Add boolean/bit toggle button.
|
|
75
93
|
control = div(tag="td", parent=parent, class_str="p-0")
|
|
76
94
|
|
|
95
|
+
if chan.commandable:
|
|
96
|
+
control.add_class("border-start-info-subtle")
|
|
97
|
+
parent.add_class("channel-commandable")
|
|
98
|
+
else:
|
|
99
|
+
parent.add_class("channel-regular")
|
|
100
|
+
|
|
77
101
|
chan_type = div(
|
|
78
102
|
tag="td",
|
|
79
103
|
text=get_channel_kind_str(env, chan, enum),
|
|
80
104
|
parent=parent,
|
|
81
105
|
title=f"Underlying primitive type for '{name}'.",
|
|
82
|
-
class_str="p-0 ps-
|
|
106
|
+
class_str="p-0 ps-2 pe-1",
|
|
83
107
|
)
|
|
84
108
|
|
|
85
109
|
control_added = False
|
|
@@ -88,50 +112,82 @@ class ChannelEnvironmentTabControls(ChannelEnvironmentTabBase):
|
|
|
88
112
|
chan_type.add_class("fw-bold")
|
|
89
113
|
|
|
90
114
|
if chan.commandable and not chan.type.is_boolean:
|
|
91
|
-
enum_dropdown(
|
|
115
|
+
enum_dropdown(
|
|
116
|
+
control, name, enum, cast(int, chan.raw.value)
|
|
117
|
+
).add_class(
|
|
118
|
+
"border-0",
|
|
119
|
+
"text-secondary-emphasis",
|
|
120
|
+
"pt-0",
|
|
121
|
+
"pb-0",
|
|
122
|
+
"d-inline",
|
|
123
|
+
)
|
|
124
|
+
control.add_class("border-end-info-subtle")
|
|
92
125
|
control_added = True
|
|
93
126
|
|
|
127
|
+
if chan.default is not None:
|
|
128
|
+
default_button(
|
|
129
|
+
control,
|
|
130
|
+
name,
|
|
131
|
+
chan.default,
|
|
132
|
+
"p-0",
|
|
133
|
+
"d-inline",
|
|
134
|
+
*TABLE_BUTTON_CLASSES,
|
|
135
|
+
front=False,
|
|
136
|
+
)
|
|
137
|
+
|
|
94
138
|
if chan.type.is_boolean:
|
|
95
|
-
chan_type.add_class("text-primary
|
|
139
|
+
chan_type.add_class("text-primary")
|
|
96
140
|
if chan.commandable:
|
|
97
141
|
button = toggle_button(
|
|
98
142
|
control, id=name, title=f"Toggle '{name}'."
|
|
99
143
|
)
|
|
100
|
-
button.add_class(
|
|
144
|
+
button.add_class(
|
|
145
|
+
"toggle-value",
|
|
146
|
+
"pt-0",
|
|
147
|
+
"pb-0",
|
|
148
|
+
"fs-5",
|
|
149
|
+
"border-end-info-subtle",
|
|
150
|
+
*TABLE_BUTTON_CLASSES,
|
|
151
|
+
)
|
|
101
152
|
control_added = True
|
|
102
153
|
|
|
103
154
|
if chan.default is not None:
|
|
104
155
|
default_button(
|
|
105
|
-
control,
|
|
156
|
+
control,
|
|
157
|
+
name,
|
|
158
|
+
chan.default,
|
|
159
|
+
"p-0",
|
|
160
|
+
*TABLE_BUTTON_CLASSES,
|
|
161
|
+
front=False,
|
|
106
162
|
)
|
|
107
163
|
|
|
108
164
|
elif chan.type.is_float:
|
|
109
165
|
chan_type.add_class("text-secondary-emphasis")
|
|
110
166
|
else:
|
|
111
|
-
chan_type.add_class("text-primary")
|
|
167
|
+
chan_type.add_class("text-primary-emphasis")
|
|
112
168
|
|
|
113
169
|
# Input box with send button.
|
|
114
170
|
if not control_added and chan.commandable:
|
|
115
|
-
|
|
171
|
+
control.add_class("border-end-info-subtle")
|
|
172
|
+
|
|
173
|
+
container = value_input_box(name, control).add_class(
|
|
174
|
+
"justify-content-start"
|
|
175
|
+
)
|
|
116
176
|
|
|
117
177
|
# Reset-to-default button if a default value exists.
|
|
118
178
|
if chan.default is not None:
|
|
119
|
-
default_button(
|
|
179
|
+
default_button(
|
|
180
|
+
container,
|
|
181
|
+
name,
|
|
182
|
+
chan.default,
|
|
183
|
+
"pt-0",
|
|
184
|
+
"pb-0",
|
|
185
|
+
*TABLE_BUTTON_CLASSES,
|
|
186
|
+
front=False,
|
|
187
|
+
)
|
|
120
188
|
|
|
121
189
|
if chan.controls:
|
|
122
|
-
|
|
123
|
-
if "slider" in chan.controls:
|
|
124
|
-
elem = chan.controls["slider"]
|
|
125
|
-
|
|
126
|
-
slider(
|
|
127
|
-
elem["min"], # type: ignore
|
|
128
|
-
elem["max"], # type: ignore
|
|
129
|
-
int(elem["step"]), # type: ignore
|
|
130
|
-
parent=container,
|
|
131
|
-
id=name,
|
|
132
|
-
title=f"Value control for '{name}'.",
|
|
133
|
-
front=True,
|
|
134
|
-
)
|
|
190
|
+
handle_controls(container, name, chan.controls)
|
|
135
191
|
|
|
136
192
|
def _bit_field_controls(
|
|
137
193
|
self,
|
|
@@ -146,12 +202,70 @@ class ChannelEnvironmentTabControls(ChannelEnvironmentTabBase):
|
|
|
146
202
|
|
|
147
203
|
field = self.command.env.fields[name]
|
|
148
204
|
if field.commandable:
|
|
205
|
+
control.add_class("border-start-info-subtle")
|
|
206
|
+
parent.add_class("channel-commandable")
|
|
207
|
+
|
|
208
|
+
if not is_bit:
|
|
209
|
+
control.add_class("border-end-info-subtle")
|
|
210
|
+
|
|
149
211
|
if is_bit:
|
|
150
212
|
button = toggle_button(
|
|
151
213
|
control, id=name, title=f"Toggle '{name}'."
|
|
152
214
|
)
|
|
153
|
-
button.add_class(
|
|
215
|
+
button.add_class(
|
|
216
|
+
"toggle-value",
|
|
217
|
+
"pt-0",
|
|
218
|
+
"pb-0",
|
|
219
|
+
"fs-5",
|
|
220
|
+
"border-start-0",
|
|
221
|
+
"border-end-info-subtle",
|
|
222
|
+
*TABLE_BUTTON_CLASSES,
|
|
223
|
+
)
|
|
224
|
+
if field.default is not None:
|
|
225
|
+
default_button(
|
|
226
|
+
control,
|
|
227
|
+
name,
|
|
228
|
+
field.default, # type: ignore
|
|
229
|
+
"p-0",
|
|
230
|
+
*TABLE_BUTTON_CLASSES,
|
|
231
|
+
front=False,
|
|
232
|
+
)
|
|
233
|
+
|
|
154
234
|
elif enum:
|
|
155
|
-
enum_dropdown(control, name, enum, field())
|
|
235
|
+
enum_dropdown(control, name, enum, field()).add_class(
|
|
236
|
+
"border-0",
|
|
237
|
+
"text-secondary-emphasis",
|
|
238
|
+
"pt-0",
|
|
239
|
+
"pb-0",
|
|
240
|
+
"d-inline",
|
|
241
|
+
)
|
|
242
|
+
if field.default is not None:
|
|
243
|
+
default_button(
|
|
244
|
+
control,
|
|
245
|
+
name,
|
|
246
|
+
field.default, # type: ignore
|
|
247
|
+
"p-0",
|
|
248
|
+
"d-inline",
|
|
249
|
+
*TABLE_BUTTON_CLASSES,
|
|
250
|
+
front=False,
|
|
251
|
+
)
|
|
156
252
|
else:
|
|
157
|
-
value_input_box(name, control)
|
|
253
|
+
container = value_input_box(name, control).add_class(
|
|
254
|
+
"justify-content-start"
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
if field.default is not None:
|
|
258
|
+
default_button(
|
|
259
|
+
container,
|
|
260
|
+
name,
|
|
261
|
+
field.default, # type: ignore
|
|
262
|
+
"pt-0",
|
|
263
|
+
"pb-0",
|
|
264
|
+
*TABLE_BUTTON_CLASSES,
|
|
265
|
+
front=False,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
if field.controls:
|
|
269
|
+
handle_controls(container, name, field.controls)
|
|
270
|
+
else:
|
|
271
|
+
parent.add_class("channel-regular")
|
|
@@ -36,10 +36,18 @@ def channel_color_button(parent: Element, name: str) -> Element:
|
|
|
36
36
|
parent,
|
|
37
37
|
id=f"{name}-line-color",
|
|
38
38
|
icon="activity",
|
|
39
|
-
icon_classes=["fs-5"],
|
|
39
|
+
icon_classes=["border-0", "fs-5"],
|
|
40
40
|
tooltip=f"Change line color for '{name}'.",
|
|
41
41
|
)
|
|
42
|
-
button.add_class(
|
|
42
|
+
button.add_class(
|
|
43
|
+
"d-none",
|
|
44
|
+
"p-0",
|
|
45
|
+
"ps-2",
|
|
46
|
+
"pe-2",
|
|
47
|
+
"border-top-0",
|
|
48
|
+
"border-bottom-0",
|
|
49
|
+
"border-primary-subtle",
|
|
50
|
+
)
|
|
43
51
|
|
|
44
52
|
return button
|
|
45
53
|
|
|
@@ -79,7 +87,7 @@ class ChannelEnvironmentTabHtml(ChannelEnvironmentTabControls):
|
|
|
79
87
|
|
|
80
88
|
div(
|
|
81
89
|
tag="td",
|
|
82
|
-
class_str="channel-value p-0",
|
|
90
|
+
class_str="channel-value p-0 pe-2",
|
|
83
91
|
parent=parent,
|
|
84
92
|
title=f"Current value of '{name}'.",
|
|
85
93
|
)
|
|
@@ -102,7 +110,7 @@ class ChannelEnvironmentTabHtml(ChannelEnvironmentTabControls):
|
|
|
102
110
|
|
|
103
111
|
# Add boolean/bit toggle button.
|
|
104
112
|
is_bit = field.width == 1
|
|
105
|
-
kind_str = f"{'bit' if is_bit else 'bits'} {field.where_str()}"
|
|
113
|
+
kind_str = f"{'bit ' if is_bit else 'bits'} {field.where_str()}"
|
|
106
114
|
|
|
107
115
|
name_td = create_name_td(parent)
|
|
108
116
|
|
|
@@ -119,7 +127,7 @@ class ChannelEnvironmentTabHtml(ChannelEnvironmentTabControls):
|
|
|
119
127
|
|
|
120
128
|
div(
|
|
121
129
|
tag="td",
|
|
122
|
-
class_str="channel-value p-0",
|
|
130
|
+
class_str="channel-value p-0 pe-2",
|
|
123
131
|
parent=parent,
|
|
124
132
|
title=f"Current value of '{name}'.",
|
|
125
133
|
)
|
|
@@ -131,16 +139,26 @@ class ChannelEnvironmentTabHtml(ChannelEnvironmentTabControls):
|
|
|
131
139
|
text=kind_str,
|
|
132
140
|
parent=parent,
|
|
133
141
|
title=f"Field position for '{name}' within underlying primitive.",
|
|
134
|
-
class_str="text-
|
|
142
|
+
class_str="text-code text-nowrap p-0 ps-2 pe-1",
|
|
135
143
|
)
|
|
136
144
|
|
|
137
145
|
def channel_table(self, parent: Element) -> None:
|
|
138
146
|
"""Create the channel table."""
|
|
139
147
|
|
|
140
148
|
table = div(
|
|
141
|
-
tag="table",
|
|
149
|
+
tag="table",
|
|
150
|
+
parent=div(parent=parent).add_class(
|
|
151
|
+
"flex-shrink-0",
|
|
152
|
+
"overflow-x-scroll",
|
|
153
|
+
"overscroll-behavior-x-none",
|
|
154
|
+
),
|
|
155
|
+
)
|
|
156
|
+
table.add_class(
|
|
157
|
+
"table",
|
|
158
|
+
"table-hover",
|
|
159
|
+
"mb-0",
|
|
160
|
+
TEXT,
|
|
142
161
|
)
|
|
143
|
-
table.add_class("table", TEXT)
|
|
144
162
|
|
|
145
163
|
header = div(tag="thead", parent=table)
|
|
146
164
|
body = div(tag="tbody", parent=table)
|
|
@@ -151,11 +169,8 @@ class ChannelEnvironmentTabHtml(ChannelEnvironmentTabControls):
|
|
|
151
169
|
# Table for channels.
|
|
152
170
|
env = self.command.env
|
|
153
171
|
for name in env.names:
|
|
154
|
-
row = div(
|
|
155
|
-
|
|
156
|
-
parent=body,
|
|
157
|
-
id=name,
|
|
158
|
-
class_str="channel-row border-start border-end",
|
|
172
|
+
row = div(tag="tr", parent=body, id=name).add_class(
|
|
173
|
+
"channel-row", "border-start", "border-end"
|
|
159
174
|
)
|
|
160
175
|
|
|
161
176
|
plot_checkbox(row, name)
|
|
@@ -187,9 +202,12 @@ class ChannelEnvironmentTabHtml(ChannelEnvironmentTabControls):
|
|
|
187
202
|
def _compose_plot(self, parent: Element) -> None:
|
|
188
203
|
"""Compose plot elements."""
|
|
189
204
|
|
|
190
|
-
plot_container = div(
|
|
191
|
-
|
|
192
|
-
|
|
205
|
+
plot_container = div(parent=parent).add_class(
|
|
206
|
+
"w-100",
|
|
207
|
+
"h-100",
|
|
208
|
+
"border-start",
|
|
209
|
+
"position-relative",
|
|
210
|
+
"logo-outline-background",
|
|
193
211
|
)
|
|
194
212
|
|
|
195
213
|
# Plot.
|
|
@@ -227,27 +245,48 @@ class ChannelEnvironmentTabHtml(ChannelEnvironmentTabControls):
|
|
|
227
245
|
parent=container,
|
|
228
246
|
kind="column",
|
|
229
247
|
tag="form",
|
|
248
|
+
autocomplete="off",
|
|
249
|
+
)
|
|
250
|
+
vert_container.add_class(
|
|
251
|
+
"channel-column",
|
|
252
|
+
"flex-grow-0",
|
|
253
|
+
"flex-shrink-0",
|
|
254
|
+
"collapse",
|
|
255
|
+
"show",
|
|
256
|
+
"overflow-y-scroll",
|
|
257
|
+
"overflow-x-hidden",
|
|
258
|
+
"overscroll-behavior-none",
|
|
230
259
|
)
|
|
231
|
-
vert_container.add_class("channel-column", "collapse", "show")
|
|
232
260
|
|
|
233
|
-
input_box(
|
|
261
|
+
_, label, box = input_box(
|
|
234
262
|
vert_container,
|
|
235
|
-
label="command",
|
|
236
263
|
pattern="help",
|
|
237
264
|
description="Send a string command via this environment.",
|
|
265
|
+
placement="bottom",
|
|
266
|
+
label="command",
|
|
238
267
|
id=self.get_id("command"),
|
|
268
|
+
icon="terminal",
|
|
269
|
+
spellcheck="false",
|
|
239
270
|
)
|
|
240
271
|
|
|
272
|
+
label.add_class("border-top-0")
|
|
273
|
+
box.add_class("border-top-0")
|
|
274
|
+
|
|
241
275
|
# Text area.
|
|
242
276
|
logs = div(
|
|
243
277
|
tag="textarea",
|
|
244
278
|
parent=div(parent=vert_container, class_str="form-floating"),
|
|
245
|
-
class_str=(
|
|
246
|
-
f"form-control rounded-0 {TEXT} text-body-emphasis text-logs"
|
|
247
|
-
),
|
|
248
279
|
id=self.get_id("logs"),
|
|
249
280
|
title=f"Text logs for {self.name}.",
|
|
250
281
|
)
|
|
282
|
+
logs.add_class(
|
|
283
|
+
"form-control",
|
|
284
|
+
"rounded-0",
|
|
285
|
+
"text-logs",
|
|
286
|
+
"border-top-0",
|
|
287
|
+
"p-2",
|
|
288
|
+
"overscroll-behavior-none",
|
|
289
|
+
)
|
|
251
290
|
logs.booleans.add("readonly")
|
|
252
291
|
|
|
253
292
|
self.channel_table(vert_container)
|
|
@@ -258,13 +297,16 @@ class ChannelEnvironmentTabHtml(ChannelEnvironmentTabControls):
|
|
|
258
297
|
"border-start",
|
|
259
298
|
"border-top",
|
|
260
299
|
"border-end",
|
|
300
|
+
"bg-gradient-tertiary-to-bottom",
|
|
261
301
|
)
|
|
262
302
|
|
|
263
303
|
# Divider.
|
|
264
|
-
div(
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
304
|
+
div(id=self.get_id("divider"), parent=container).add_class(
|
|
305
|
+
"vertical-divider",
|
|
306
|
+
"flex-grow-0",
|
|
307
|
+
"flex-shrink-0",
|
|
308
|
+
"border-start",
|
|
309
|
+
"bg-dark-subtle",
|
|
268
310
|
)
|
|
269
311
|
|
|
270
312
|
self._compose_plot(container)
|