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.
Files changed (99) hide show
  1. runtimepy/__init__.py +2 -2
  2. runtimepy/channel/__init__.py +1 -4
  3. runtimepy/channel/environment/__init__.py +93 -2
  4. runtimepy/channel/environment/create.py +16 -1
  5. runtimepy/channel/environment/sample.py +10 -2
  6. runtimepy/channel/registry.py +2 -3
  7. runtimepy/codec/protocol/base.py +34 -14
  8. runtimepy/codec/protocol/json.py +5 -3
  9. runtimepy/codec/system/__init__.py +6 -2
  10. runtimepy/control/source.py +1 -1
  11. runtimepy/data/404.md +16 -0
  12. runtimepy/data/base.yaml +3 -0
  13. runtimepy/data/css/bootstrap_extra.css +59 -44
  14. runtimepy/data/css/main.css +23 -4
  15. runtimepy/data/dummy_load.yaml +5 -2
  16. runtimepy/data/factories.yaml +1 -0
  17. runtimepy/data/js/classes/App.js +54 -2
  18. runtimepy/data/js/classes/ChannelTable.js +6 -8
  19. runtimepy/data/js/classes/Plot.js +9 -4
  20. runtimepy/data/js/classes/TabFilter.js +47 -9
  21. runtimepy/data/js/classes/TabInterface.js +106 -11
  22. runtimepy/data/js/classes/WindowHashManager.js +30 -15
  23. runtimepy/data/js/init.js +18 -1
  24. runtimepy/data/js/markdown_page.js +10 -0
  25. runtimepy/data/js/sample.js +1 -0
  26. runtimepy/data/schemas/BitFields.yaml +9 -0
  27. runtimepy/data/schemas/RuntimeEnum.yaml +6 -0
  28. runtimepy/data/schemas/StructConfig.yaml +9 -1
  29. runtimepy/data/static/css/bootstrap-icons.min.css +4 -3
  30. runtimepy/data/static/css/bootstrap.min.css +3 -4
  31. runtimepy/data/static/css/fonts/bootstrap-icons.woff +0 -0
  32. runtimepy/data/static/css/fonts/bootstrap-icons.woff2 +0 -0
  33. runtimepy/data/static/js/bootstrap.bundle.min.js +5 -4
  34. runtimepy/data/static/js/webglplot.umd.min.js +2 -1
  35. runtimepy/data/static/svg/outline-dark.svg +22 -0
  36. runtimepy/data/static/svg/outline-light.svg +22 -0
  37. runtimepy/enum/__init__.py +13 -1
  38. runtimepy/enum/registry.py +13 -1
  39. runtimepy/message/__init__.py +3 -3
  40. runtimepy/mixins/logging.py +6 -1
  41. runtimepy/net/__init__.py +0 -2
  42. runtimepy/net/arbiter/info.py +36 -4
  43. runtimepy/net/arbiter/struct/__init__.py +3 -2
  44. runtimepy/net/connection.py +6 -7
  45. runtimepy/net/html/__init__.py +29 -11
  46. runtimepy/net/html/bootstrap/__init__.py +2 -2
  47. runtimepy/net/html/bootstrap/elements.py +44 -24
  48. runtimepy/net/html/bootstrap/tabs.py +18 -11
  49. runtimepy/net/http/__init__.py +3 -3
  50. runtimepy/net/http/request_target.py +3 -3
  51. runtimepy/net/mixin.py +4 -2
  52. runtimepy/net/server/__init__.py +16 -9
  53. runtimepy/net/server/app/__init__.py +1 -0
  54. runtimepy/net/server/app/create.py +3 -3
  55. runtimepy/net/server/app/env/__init__.py +30 -4
  56. runtimepy/net/server/app/env/settings.py +4 -7
  57. runtimepy/net/server/app/env/tab/base.py +2 -1
  58. runtimepy/net/server/app/env/tab/controls.py +141 -27
  59. runtimepy/net/server/app/env/tab/html.py +68 -26
  60. runtimepy/net/server/app/env/widgets.py +115 -61
  61. runtimepy/net/server/app/landing_page.py +1 -1
  62. runtimepy/net/server/app/tab.py +12 -3
  63. runtimepy/net/server/html.py +2 -2
  64. runtimepy/net/server/json.py +1 -1
  65. runtimepy/net/server/markdown.py +29 -12
  66. runtimepy/net/server/mux.py +29 -0
  67. runtimepy/net/stream/__init__.py +6 -5
  68. runtimepy/net/stream/base.py +4 -2
  69. runtimepy/net/tcp/connection.py +5 -3
  70. runtimepy/net/tcp/http/__init__.py +10 -9
  71. runtimepy/net/tcp/protocol.py +2 -2
  72. runtimepy/net/tcp/scpi/__init__.py +5 -2
  73. runtimepy/net/tcp/telnet/__init__.py +2 -1
  74. runtimepy/net/udp/connection.py +10 -6
  75. runtimepy/net/udp/protocol.py +5 -6
  76. runtimepy/net/udp/queue.py +5 -2
  77. runtimepy/net/udp/tftp/base.py +2 -1
  78. runtimepy/net/websocket/connection.py +58 -9
  79. runtimepy/primitives/array/__init__.py +7 -5
  80. runtimepy/primitives/base.py +3 -2
  81. runtimepy/primitives/field/__init__.py +35 -2
  82. runtimepy/primitives/field/fields.py +11 -2
  83. runtimepy/primitives/field/manager/base.py +19 -2
  84. runtimepy/primitives/serializable/base.py +5 -2
  85. runtimepy/primitives/serializable/fixed.py +5 -2
  86. runtimepy/primitives/serializable/prefixed.py +4 -1
  87. runtimepy/primitives/types/base.py +4 -1
  88. runtimepy/primitives/types/bounds.py +10 -4
  89. runtimepy/registry/__init__.py +20 -0
  90. runtimepy/registry/name.py +6 -0
  91. runtimepy/requirements.txt +2 -2
  92. runtimepy/ui/controls.py +20 -1
  93. {runtimepy-5.14.2.dist-info → runtimepy-5.15.1.dist-info}/METADATA +6 -6
  94. {runtimepy-5.14.2.dist-info → runtimepy-5.15.1.dist-info}/RECORD +98 -94
  95. {runtimepy-5.14.2.dist-info → runtimepy-5.15.1.dist-info}/WHEEL +1 -1
  96. runtimepy/data/404.html +0 -7
  97. {runtimepy-5.14.2.dist-info → runtimepy-5.15.1.dist-info}/entry_points.txt +0 -0
  98. {runtimepy-5.14.2.dist-info → runtimepy-5.15.1.dist-info}/licenses/LICENSE +0 -0
  99. {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("d-none", "p-1")
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-info-emphasis text-nowrap p-0 ps-1 pe-1",
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", parent=div(parent=parent, class_str="table-container")
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
- tag="tr",
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
- parent=parent,
192
- class_str="w-100 h-100 border-start position-relative",
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
- id=self.get_id("divider"),
266
- parent=container,
267
- class_str="vertical-divider border-start bg-dark-subtle",
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
- f"Enable plotting channel '{name}'.",
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", class_str="form-select m-1", **kwargs)
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
- ) -> None:
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 = ("p-1", "pt-2")
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(tag="tr", parent=parent, class_str="border-start border-end")
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("pb-2")
105
+ ).add_class(*TABLE_BUTTON_CLASSES)
115
106
 
116
- input_box(
117
- div(tag="th", parent=ctl_row, class_str="p-0 pb-1"),
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
- # Button for clearing plot points.
123
- toggle_button(
124
- div(tag="th", parent=ctl_row, class_str="p-0"),
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="Send selected command.",
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
- # Empty for now.
171
- div(tag="th", parent=ctl_row, class_str="p-0")
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-1",
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
@@ -20,7 +20,7 @@ def landing_page(
20
20
  document: Html,
21
21
  request: RequestHeader,
22
22
  response: ResponseHeader,
23
- request_data: Optional[bytes],
23
+ request_data: Optional[bytearray],
24
24
  ) -> Html:
25
25
  """Create a landing page application"""
26
26
 
@@ -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 write_found_file(
63
- writer, kind_url("js", self.source, subdir=self.subdir, **kwargs)
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:
@@ -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[bytes]], Awaitable[Html]
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[bytes],
61
+ request_data: Optional[bytearray],
62
62
  default_app: HtmlApp = None,
63
63
  **kwargs,
64
64
  ) -> bool:
@@ -87,7 +87,7 @@ def json_handler(
87
87
  stream: TextIO,
88
88
  request: RequestHeader,
89
89
  response: ResponseHeader,
90
- request_data: Optional[bytes],
90
+ request_data: Optional[bytearray],
91
91
  data: JsonObject,
92
92
  ) -> None:
93
93
  """Create an HTTP response from some JSON object data."""
@@ -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 = "[![logo](/static/png/chip-circle-bootstrap/128x128.png)](/)"
17
+ DIR_FILE = "dir.html"
15
18
 
16
19
 
17
20
  def markdown_for_dir(
18
- path: Path, base: Path, extra_links: dict[str, Iterable[str]] = None
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
- curr_dir = rel(path, base=base)
36
- writer.write(f"## `{curr_dir}`")
39
+ writer.write("## directories")
37
40
  writer.empty()
38
41
 
39
- # Link to go up a directory.
40
- if curr_dir != Path():
41
- writer.write(f"* [..](/{curr_dir.parent})")
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
- for item in path.iterdir():
44
- curr = rel(item, base=base)
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 = f"`{curr.name}`"
47
- if item.is_dir():
48
- name = f"**{name}**"
51
+ writer.write("| name | size |")
52
+ writer.write("|------|------|")
53
+ for item in sorted(path.iterdir()):
54
+ curr = rel(item, base=base)
49
55
 
50
- writer.write(f"* [{name}](/{curr})")
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
@@ -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
- from typing import Tuple
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: bytes, addr: Tuple[str, int]
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: Tuple[str, int] = None
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: Tuple[str, int] = None
56
+ self, stream: _BinaryIO, addr: tuple[str, int] = None
56
57
  ) -> bool:
57
58
  """Process a single message."""
58
59