runtimepy 5.14.2__py3-none-any.whl → 5.15.1__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 +5 -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/js/sample.js +1 -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 +6 -7
- 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 +30 -4
- runtimepy/net/server/app/env/settings.py +4 -7
- runtimepy/net/server/app/env/tab/base.py +2 -1
- 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/app/tab.py +12 -3
- runtimepy/net/server/html.py +2 -2
- runtimepy/net/server/json.py +1 -1
- runtimepy/net/server/markdown.py +29 -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 +58 -9
- 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.1.dist-info}/METADATA +6 -6
- {runtimepy-5.14.2.dist-info → runtimepy-5.15.1.dist-info}/RECORD +98 -94
- {runtimepy-5.14.2.dist-info → runtimepy-5.15.1.dist-info}/WHEEL +1 -1
- runtimepy/data/404.html +0 -7
- {runtimepy-5.14.2.dist-info → runtimepy-5.15.1.dist-info}/entry_points.txt +0 -0
- {runtimepy-5.14.2.dist-info → runtimepy-5.15.1.dist-info}/licenses/LICENSE +0 -0
- {runtimepy-5.14.2.dist-info → runtimepy-5.15.1.dist-info}/top_level.txt +0 -0
|
@@ -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)
|
|
@@ -14,6 +14,7 @@ from runtimepy.channel.environment.command.processor import (
|
|
|
14
14
|
ChannelCommandProcessor,
|
|
15
15
|
)
|
|
16
16
|
from runtimepy.enum import RuntimeEnum
|
|
17
|
+
from runtimepy.net.html.bootstrap import icon_str
|
|
17
18
|
from runtimepy.net.html.bootstrap.elements import (
|
|
18
19
|
flex,
|
|
19
20
|
input_box,
|
|
@@ -25,7 +26,9 @@ from runtimepy.net.html.bootstrap.elements import (
|
|
|
25
26
|
def plot_checkbox(parent: Element, name: str) -> None:
|
|
26
27
|
"""Add a checkbox for individual channel plot status."""
|
|
27
28
|
|
|
28
|
-
container = div(tag="td", parent=parent, class_str="text-center p-0")
|
|
29
|
+
container = div(tag="td", parent=parent, class_str="text-center p-0 fs-5")
|
|
30
|
+
|
|
31
|
+
title = f"Enable plotting channel '{name}'."
|
|
29
32
|
|
|
30
33
|
set_tooltip(
|
|
31
34
|
div(
|
|
@@ -34,10 +37,11 @@ def plot_checkbox(parent: Element, name: str) -> None:
|
|
|
34
37
|
value="",
|
|
35
38
|
id=f"plot-{name}",
|
|
36
39
|
allow_no_end_tag=True,
|
|
37
|
-
parent=container,
|
|
38
|
-
class_str="form-check-input",
|
|
40
|
+
parent=div(tag="label", parent=container),
|
|
41
|
+
class_str="form-check-input rounded-0",
|
|
42
|
+
title=title,
|
|
39
43
|
),
|
|
40
|
-
|
|
44
|
+
title,
|
|
41
45
|
placement="left",
|
|
42
46
|
)
|
|
43
47
|
|
|
@@ -45,7 +49,10 @@ def plot_checkbox(parent: Element, name: str) -> None:
|
|
|
45
49
|
def select_element(**kwargs) -> Element:
|
|
46
50
|
"""Create a select element."""
|
|
47
51
|
|
|
48
|
-
select = div(tag="select",
|
|
52
|
+
select = div(tag="select", **kwargs)
|
|
53
|
+
select.add_class(
|
|
54
|
+
"form-select", "rounded-0", "border-top-0", "border-bottom-0"
|
|
55
|
+
)
|
|
49
56
|
if "title" in kwargs:
|
|
50
57
|
select["aria-label"] = kwargs["title"]
|
|
51
58
|
return select
|
|
@@ -53,7 +60,7 @@ def select_element(**kwargs) -> Element:
|
|
|
53
60
|
|
|
54
61
|
def enum_dropdown(
|
|
55
62
|
parent: Element, name: str, enum: RuntimeEnum, current: int | bool
|
|
56
|
-
) ->
|
|
63
|
+
) -> Element:
|
|
57
64
|
"""Implement a drop down for enumeration options."""
|
|
58
65
|
|
|
59
66
|
select = select_element(
|
|
@@ -67,8 +74,10 @@ def enum_dropdown(
|
|
|
67
74
|
if current == val:
|
|
68
75
|
opt.booleans.add("selected")
|
|
69
76
|
|
|
77
|
+
return select
|
|
78
|
+
|
|
70
79
|
|
|
71
|
-
TABLE_BUTTON_CLASSES = ("
|
|
80
|
+
TABLE_BUTTON_CLASSES = ("border-top-0", "border-bottom-0")
|
|
72
81
|
|
|
73
82
|
|
|
74
83
|
def channel_table_header(
|
|
@@ -78,73 +87,45 @@ def channel_table_header(
|
|
|
78
87
|
|
|
79
88
|
env = command.env
|
|
80
89
|
|
|
81
|
-
# Add header.
|
|
82
|
-
header_row = div(
|
|
83
|
-
tag="tr", parent=parent, class_str="border-start border-end"
|
|
84
|
-
)
|
|
85
|
-
for heading, desc in [
|
|
86
|
-
("plot", "Toggle plotting for channels."),
|
|
87
|
-
("name", "Channel names."),
|
|
88
|
-
("value", "Channel values."),
|
|
89
|
-
("ctl", "Type-specific channel controls."),
|
|
90
|
-
("type", "Channel types."),
|
|
91
|
-
]:
|
|
92
|
-
set_tooltip(
|
|
93
|
-
div(
|
|
94
|
-
tag="th",
|
|
95
|
-
scope="col",
|
|
96
|
-
parent=header_row,
|
|
97
|
-
text=heading,
|
|
98
|
-
class_str="text-secondary p-1",
|
|
99
|
-
),
|
|
100
|
-
desc,
|
|
101
|
-
placement="left",
|
|
102
|
-
)
|
|
103
|
-
|
|
104
90
|
# Add some controls.
|
|
105
|
-
ctl_row = div(
|
|
91
|
+
ctl_row = div(
|
|
92
|
+
tag="tr",
|
|
93
|
+
parent=parent,
|
|
94
|
+
class_str="bg-body-tertiary border-start border-end",
|
|
95
|
+
)
|
|
106
96
|
|
|
107
97
|
# Button for clearing plotted channels.
|
|
108
98
|
toggle_button(
|
|
109
99
|
div(tag="th", parent=ctl_row, class_str="text-center p-0"),
|
|
110
100
|
tooltip="Clear plotted channels.",
|
|
111
101
|
icon="x-lg",
|
|
102
|
+
placement="left",
|
|
112
103
|
id="clear-plotted-channels",
|
|
113
104
|
title="button for clearing plotted channels",
|
|
114
|
-
).add_class(
|
|
105
|
+
).add_class(*TABLE_BUTTON_CLASSES)
|
|
115
106
|
|
|
116
|
-
input_box(
|
|
117
|
-
div(tag="th", parent=ctl_row, class_str="p-0
|
|
107
|
+
_, label, box = input_box(
|
|
108
|
+
div(tag="th", parent=ctl_row, colspan="2", class_str="p-0"),
|
|
118
109
|
description="Channel name filter.",
|
|
110
|
+
pattern=".* ! @ $",
|
|
111
|
+
label="filter",
|
|
119
112
|
id="channel-filter",
|
|
113
|
+
icon="funnel",
|
|
114
|
+
spellcheck="false",
|
|
120
115
|
)
|
|
116
|
+
label.add_class("border-top-0", "border-bottom-0")
|
|
117
|
+
box.add_class("border-top-0", "border-bottom-0")
|
|
121
118
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
icon="trash",
|
|
126
|
-
tooltip="Clear all plot points.",
|
|
127
|
-
id="clear-plotted-points",
|
|
128
|
-
title="button for clearing plot point data",
|
|
129
|
-
).add_class("pb-2")
|
|
130
|
-
|
|
131
|
-
cell = flex(tag="th", parent=ctl_row)
|
|
132
|
-
cell.add_class("p-0")
|
|
133
|
-
|
|
134
|
-
# Button for 'reset all defaults' if this tab has more than one channel
|
|
135
|
-
# with a default value.
|
|
136
|
-
if env.num_defaults > 1:
|
|
137
|
-
toggle_button(
|
|
138
|
-
cell,
|
|
139
|
-
id="set-defaults",
|
|
140
|
-
icon="arrow-counterclockwise",
|
|
141
|
-
tooltip="Reset all channels to their default values.",
|
|
142
|
-
).add_class(*TABLE_BUTTON_CLASSES)
|
|
119
|
+
cell = flex(
|
|
120
|
+
parent=div(tag="th", parent=ctl_row, colspan="2", class_str="p-0")
|
|
121
|
+
)
|
|
143
122
|
|
|
144
123
|
# Add a selection menu for custom commands.
|
|
145
124
|
select = select_element(
|
|
146
125
|
parent=cell, id="custom-commands", title="Custom command selector."
|
|
147
126
|
)
|
|
127
|
+
select.add_class("border-start-0")
|
|
128
|
+
|
|
148
129
|
if command.custom_commands:
|
|
149
130
|
for key in command.custom_commands:
|
|
150
131
|
opt = div(tag="option", value=key, text=key, parent=select)
|
|
@@ -156,7 +137,8 @@ def channel_table_header(
|
|
|
156
137
|
cell,
|
|
157
138
|
icon="send",
|
|
158
139
|
id="send-custom-commands",
|
|
159
|
-
title="
|
|
140
|
+
title="send selected command button",
|
|
141
|
+
tooltip="Send selected command (left dropdown).",
|
|
160
142
|
).add_class(*TABLE_BUTTON_CLASSES)
|
|
161
143
|
else:
|
|
162
144
|
div(
|
|
@@ -167,8 +149,79 @@ def channel_table_header(
|
|
|
167
149
|
)
|
|
168
150
|
select.booleans.add("disabled")
|
|
169
151
|
|
|
170
|
-
#
|
|
171
|
-
|
|
152
|
+
# Button for 'reset all defaults' if this tab has more than one channel
|
|
153
|
+
# with a default value.
|
|
154
|
+
if env.num_defaults > 1:
|
|
155
|
+
toggle_button(
|
|
156
|
+
cell,
|
|
157
|
+
id="set-defaults",
|
|
158
|
+
icon="arrow-counterclockwise",
|
|
159
|
+
tooltip="Reset all channels to their default values.",
|
|
160
|
+
).add_class(*TABLE_BUTTON_CLASSES)
|
|
161
|
+
|
|
162
|
+
toggle_button(
|
|
163
|
+
cell,
|
|
164
|
+
icon="eye-slash",
|
|
165
|
+
tooltip="Toggle command channels.",
|
|
166
|
+
id="toggle-command-channels",
|
|
167
|
+
title="button for toggling command-channel visibility",
|
|
168
|
+
icon_classes=["text-info-emphasis"],
|
|
169
|
+
).add_class(*TABLE_BUTTON_CLASSES)
|
|
170
|
+
toggle_button(
|
|
171
|
+
cell,
|
|
172
|
+
icon="eye-slash-fill",
|
|
173
|
+
tooltip="Toggle regular channels.",
|
|
174
|
+
id="toggle-regular-channels",
|
|
175
|
+
title="button for toggling regular-channel visibility",
|
|
176
|
+
).add_class(*TABLE_BUTTON_CLASSES)
|
|
177
|
+
toggle_button(
|
|
178
|
+
cell,
|
|
179
|
+
icon="trash",
|
|
180
|
+
tooltip="Clear all plot points.",
|
|
181
|
+
id="clear-plotted-points",
|
|
182
|
+
title="button for clearing plot point data",
|
|
183
|
+
).add_class("me-auto", *TABLE_BUTTON_CLASSES)
|
|
184
|
+
|
|
185
|
+
# Add header.
|
|
186
|
+
header_row = div(
|
|
187
|
+
tag="tr",
|
|
188
|
+
parent=parent,
|
|
189
|
+
class_str="border-end text-center bg-body-tertiary",
|
|
190
|
+
)
|
|
191
|
+
icon_classes = ["text-primary-emphasis"]
|
|
192
|
+
for heading, desc in [
|
|
193
|
+
(
|
|
194
|
+
icon_str("activity", classes=icon_classes) + "?",
|
|
195
|
+
"Toggle plotting for channels.",
|
|
196
|
+
),
|
|
197
|
+
(
|
|
198
|
+
icon_str("pen", classes=icon_classes) + " name",
|
|
199
|
+
"Channel names.",
|
|
200
|
+
),
|
|
201
|
+
(
|
|
202
|
+
icon_str("database", classes=icon_classes) + " value",
|
|
203
|
+
"Channel values.",
|
|
204
|
+
),
|
|
205
|
+
(
|
|
206
|
+
icon_str("controller", classes=icon_classes) + " controls",
|
|
207
|
+
"Type-specific channel controls.",
|
|
208
|
+
),
|
|
209
|
+
(
|
|
210
|
+
icon_str("braces", classes=icon_classes) + " type",
|
|
211
|
+
"Channel types.",
|
|
212
|
+
),
|
|
213
|
+
]:
|
|
214
|
+
set_tooltip(
|
|
215
|
+
div(
|
|
216
|
+
tag="th",
|
|
217
|
+
scope="col",
|
|
218
|
+
parent=header_row,
|
|
219
|
+
text=heading,
|
|
220
|
+
class_str="text-secondary p-1 border-start text-nowrap",
|
|
221
|
+
),
|
|
222
|
+
"(column) " + desc,
|
|
223
|
+
placement="left",
|
|
224
|
+
)
|
|
172
225
|
|
|
173
226
|
|
|
174
227
|
def value_input_box(name: str, parent: Element) -> Element:
|
|
@@ -186,15 +239,16 @@ def value_input_box(name: str, parent: Element) -> Element:
|
|
|
186
239
|
"rounded-0",
|
|
187
240
|
"font-monospace",
|
|
188
241
|
"form-control",
|
|
189
|
-
"m-1",
|
|
190
242
|
"p-0",
|
|
191
|
-
"ps-
|
|
243
|
+
"ps-2",
|
|
244
|
+
"border-0",
|
|
245
|
+
"text-secondary-emphasis",
|
|
192
246
|
)
|
|
193
247
|
toggle_button(
|
|
194
248
|
input_container,
|
|
195
249
|
icon="send",
|
|
196
250
|
id=name,
|
|
197
251
|
title=f"Send command value for '{name}'.",
|
|
198
|
-
).add_class(*TABLE_BUTTON_CLASSES)
|
|
252
|
+
).add_class("pt-0", "pb-0", *TABLE_BUTTON_CLASSES)
|
|
199
253
|
|
|
200
254
|
return input_container
|
runtimepy/net/server/app/tab.py
CHANGED
|
@@ -4,7 +4,7 @@ A module implementing an application tab interface.
|
|
|
4
4
|
|
|
5
5
|
# built-in
|
|
6
6
|
from io import StringIO
|
|
7
|
-
from typing import cast
|
|
7
|
+
from typing import Iterable, cast
|
|
8
8
|
|
|
9
9
|
# third-party
|
|
10
10
|
from svgen.element import Element
|
|
@@ -29,6 +29,7 @@ class Tab:
|
|
|
29
29
|
source: str = None,
|
|
30
30
|
subdir: str = "tab",
|
|
31
31
|
icon: str = None,
|
|
32
|
+
js_uris: Iterable[str] = None,
|
|
32
33
|
) -> None:
|
|
33
34
|
"""Initialize this instance."""
|
|
34
35
|
|
|
@@ -46,6 +47,10 @@ class Tab:
|
|
|
46
47
|
button_str += self.name
|
|
47
48
|
self.button.text = button_str
|
|
48
49
|
|
|
50
|
+
if not js_uris:
|
|
51
|
+
js_uris = []
|
|
52
|
+
self.js_uris: list[str] = list(js_uris)
|
|
53
|
+
|
|
49
54
|
self.init()
|
|
50
55
|
|
|
51
56
|
self.compose(self.content)
|
|
@@ -59,8 +64,12 @@ class Tab:
|
|
|
59
64
|
def write_js(self, writer: IndentedFileWriter, **kwargs) -> bool:
|
|
60
65
|
"""Write JavaScript code for the tab."""
|
|
61
66
|
|
|
62
|
-
return
|
|
63
|
-
writer,
|
|
67
|
+
return all(
|
|
68
|
+
write_found_file(writer, uri)
|
|
69
|
+
for uri in [
|
|
70
|
+
kind_url("js", self.source, subdir=self.subdir, **kwargs)
|
|
71
|
+
]
|
|
72
|
+
+ self.js_uris
|
|
64
73
|
)
|
|
65
74
|
|
|
66
75
|
def entry(self) -> None:
|
runtimepy/net/server/html.py
CHANGED
|
@@ -16,7 +16,7 @@ from runtimepy.net.http.response import ResponseHeader
|
|
|
16
16
|
from runtimepy.net.tcp.http import HttpConnection
|
|
17
17
|
|
|
18
18
|
HtmlApp = Callable[
|
|
19
|
-
[Html, RequestHeader, ResponseHeader, Optional[
|
|
19
|
+
[Html, RequestHeader, ResponseHeader, Optional[bytearray]], Awaitable[Html]
|
|
20
20
|
]
|
|
21
21
|
HtmlApps = dict[str, HtmlApp]
|
|
22
22
|
|
|
@@ -58,7 +58,7 @@ async def html_handler(
|
|
|
58
58
|
stream: TextIO,
|
|
59
59
|
request: RequestHeader,
|
|
60
60
|
response: ResponseHeader,
|
|
61
|
-
request_data: Optional[
|
|
61
|
+
request_data: Optional[bytearray],
|
|
62
62
|
default_app: HtmlApp = None,
|
|
63
63
|
**kwargs,
|
|
64
64
|
) -> bool:
|
runtimepy/net/server/json.py
CHANGED
|
@@ -87,7 +87,7 @@ def json_handler(
|
|
|
87
87
|
stream: TextIO,
|
|
88
88
|
request: RequestHeader,
|
|
89
89
|
response: ResponseHeader,
|
|
90
|
-
request_data: Optional[
|
|
90
|
+
request_data: Optional[bytearray],
|
|
91
91
|
data: JsonObject,
|
|
92
92
|
) -> None:
|
|
93
93
|
"""Create an HTTP response from some JSON object data."""
|
runtimepy/net/server/markdown.py
CHANGED
|
@@ -9,13 +9,17 @@ from typing import Iterable, cast
|
|
|
9
9
|
|
|
10
10
|
# third-party
|
|
11
11
|
from vcorelib.io.file_writer import IndentedFileWriter
|
|
12
|
+
from vcorelib.math.time import byte_count_str
|
|
12
13
|
from vcorelib.paths import rel
|
|
14
|
+
from vcorelib.paths import stats as _stats
|
|
13
15
|
|
|
14
16
|
LOGO_MARKDOWN = "[](/)"
|
|
17
|
+
DIR_FILE = "dir.html"
|
|
15
18
|
|
|
16
19
|
|
|
17
20
|
def markdown_for_dir(
|
|
18
|
-
|
|
21
|
+
paths_bases: Iterable[tuple[Path, Path]],
|
|
22
|
+
extra_links: dict[str, Iterable[str]] = None,
|
|
19
23
|
) -> str:
|
|
20
24
|
"""Get markdown data for a directory."""
|
|
21
25
|
|
|
@@ -32,22 +36,35 @@ def markdown_for_dir(
|
|
|
32
36
|
for app in apps:
|
|
33
37
|
writer.write(f"* [{app}]({app})")
|
|
34
38
|
|
|
35
|
-
|
|
36
|
-
writer.write(f"## `{curr_dir}`")
|
|
39
|
+
writer.write("## directories")
|
|
37
40
|
writer.empty()
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
writer.write(f"
|
|
42
|
+
for path, base in paths_bases:
|
|
43
|
+
curr_dir = rel(path, base=base)
|
|
44
|
+
writer.write(f"### `{base.name}/{curr_dir}`")
|
|
45
|
+
writer.empty()
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
# Link to go up a directory.
|
|
48
|
+
if curr_dir != Path():
|
|
49
|
+
writer.write(f"* [..](/{curr_dir.parent}/{DIR_FILE})")
|
|
45
50
|
|
|
46
|
-
name
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
writer.write("| name | size |")
|
|
52
|
+
writer.write("|------|------|")
|
|
53
|
+
for item in sorted(path.iterdir()):
|
|
54
|
+
curr = rel(item, base=base)
|
|
49
55
|
|
|
50
|
-
|
|
56
|
+
name = f"`{curr.name}`"
|
|
57
|
+
if item.is_dir():
|
|
58
|
+
name = f"**{name}**"
|
|
59
|
+
|
|
60
|
+
stats = _stats(item)
|
|
61
|
+
assert stats
|
|
62
|
+
size_str = (
|
|
63
|
+
byte_count_str(stats.st_size) if item.is_file() else ""
|
|
64
|
+
)
|
|
65
|
+
writer.write(f"| [{name}](/{curr}) | {size_str} |")
|
|
66
|
+
|
|
67
|
+
writer.empty()
|
|
51
68
|
|
|
52
69
|
result: str = cast(StringIO, writer.stream).getvalue()
|
|
53
70
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A module implementing an interface for serving a web application that allows
|
|
3
|
+
iframe-based grid multiplexing.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# built-in
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
# third-party
|
|
10
|
+
from svgen.element.html import Html
|
|
11
|
+
|
|
12
|
+
# internal
|
|
13
|
+
from runtimepy.net.http.header import RequestHeader
|
|
14
|
+
from runtimepy.net.http.response import ResponseHeader
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def mux_app(
|
|
18
|
+
document: Html,
|
|
19
|
+
request: RequestHeader,
|
|
20
|
+
response: ResponseHeader,
|
|
21
|
+
request_data: Optional[bytearray],
|
|
22
|
+
) -> Html:
|
|
23
|
+
"""An iframe multiplexing application."""
|
|
24
|
+
|
|
25
|
+
del request
|
|
26
|
+
del response
|
|
27
|
+
del request_data
|
|
28
|
+
|
|
29
|
+
return document
|
runtimepy/net/stream/__init__.py
CHANGED
|
@@ -4,10 +4,11 @@ A module aggregating stream-oriented connection interfaces.
|
|
|
4
4
|
|
|
5
5
|
# built-in
|
|
6
6
|
from typing import BinaryIO as _BinaryIO
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
# third-party
|
|
9
|
+
from vcorelib.io import BinaryMessage
|
|
8
10
|
|
|
9
11
|
# internal
|
|
10
|
-
from runtimepy.net.connection import BinaryMessage
|
|
11
12
|
from runtimepy.net.stream.base import PrefixedMessageConnection
|
|
12
13
|
from runtimepy.net.stream.string import StringMessageConnection
|
|
13
14
|
from runtimepy.net.tcp.connection import TcpConnection
|
|
@@ -34,14 +35,14 @@ class UdpPrefixedMessageConnection(PrefixedMessageConnection, UdpConnection):
|
|
|
34
35
|
"""A UDP implementation for size-prefixed messages."""
|
|
35
36
|
|
|
36
37
|
async def process_datagram(
|
|
37
|
-
self, data:
|
|
38
|
+
self, data: BinaryMessage, addr: tuple[str, int]
|
|
38
39
|
) -> bool:
|
|
39
40
|
"""Process a datagram."""
|
|
40
41
|
|
|
41
42
|
return await self.process_binary(data, addr=addr)
|
|
42
43
|
|
|
43
44
|
def _send_message(
|
|
44
|
-
self, data: BinaryMessage, addr:
|
|
45
|
+
self, data: BinaryMessage, addr: tuple[str, int] = None
|
|
45
46
|
) -> None:
|
|
46
47
|
"""Underlying data send."""
|
|
47
48
|
|
|
@@ -52,7 +53,7 @@ class EchoMessageConnection(PrefixedMessageConnection):
|
|
|
52
53
|
"""A connection that just echoes what it was sent."""
|
|
53
54
|
|
|
54
55
|
async def process_single(
|
|
55
|
-
self, stream: _BinaryIO, addr:
|
|
56
|
+
self, stream: _BinaryIO, addr: tuple[str, int] = None
|
|
56
57
|
) -> bool:
|
|
57
58
|
"""Process a single message."""
|
|
58
59
|
|