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.
Files changed (96) 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 +2 -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/schemas/BitFields.yaml +9 -0
  26. runtimepy/data/schemas/RuntimeEnum.yaml +6 -0
  27. runtimepy/data/schemas/StructConfig.yaml +9 -1
  28. runtimepy/data/static/css/bootstrap-icons.min.css +4 -3
  29. runtimepy/data/static/css/bootstrap.min.css +3 -4
  30. runtimepy/data/static/css/fonts/bootstrap-icons.woff +0 -0
  31. runtimepy/data/static/css/fonts/bootstrap-icons.woff2 +0 -0
  32. runtimepy/data/static/js/bootstrap.bundle.min.js +5 -4
  33. runtimepy/data/static/js/webglplot.umd.min.js +2 -1
  34. runtimepy/data/static/svg/outline-dark.svg +22 -0
  35. runtimepy/data/static/svg/outline-light.svg +22 -0
  36. runtimepy/enum/__init__.py +13 -1
  37. runtimepy/enum/registry.py +13 -1
  38. runtimepy/message/__init__.py +3 -3
  39. runtimepy/mixins/logging.py +6 -1
  40. runtimepy/net/__init__.py +0 -2
  41. runtimepy/net/arbiter/info.py +36 -4
  42. runtimepy/net/arbiter/struct/__init__.py +3 -2
  43. runtimepy/net/connection.py +4 -5
  44. runtimepy/net/html/__init__.py +29 -11
  45. runtimepy/net/html/bootstrap/__init__.py +2 -2
  46. runtimepy/net/html/bootstrap/elements.py +44 -24
  47. runtimepy/net/html/bootstrap/tabs.py +18 -11
  48. runtimepy/net/http/__init__.py +3 -3
  49. runtimepy/net/http/request_target.py +3 -3
  50. runtimepy/net/mixin.py +4 -2
  51. runtimepy/net/server/__init__.py +16 -9
  52. runtimepy/net/server/app/__init__.py +1 -0
  53. runtimepy/net/server/app/create.py +3 -3
  54. runtimepy/net/server/app/env/__init__.py +28 -4
  55. runtimepy/net/server/app/env/settings.py +4 -7
  56. runtimepy/net/server/app/env/tab/controls.py +141 -27
  57. runtimepy/net/server/app/env/tab/html.py +68 -26
  58. runtimepy/net/server/app/env/widgets.py +115 -61
  59. runtimepy/net/server/app/landing_page.py +1 -1
  60. runtimepy/net/server/html.py +2 -2
  61. runtimepy/net/server/json.py +1 -1
  62. runtimepy/net/server/markdown.py +18 -12
  63. runtimepy/net/server/mux.py +29 -0
  64. runtimepy/net/stream/__init__.py +6 -5
  65. runtimepy/net/stream/base.py +4 -2
  66. runtimepy/net/tcp/connection.py +5 -3
  67. runtimepy/net/tcp/http/__init__.py +10 -9
  68. runtimepy/net/tcp/protocol.py +2 -2
  69. runtimepy/net/tcp/scpi/__init__.py +5 -2
  70. runtimepy/net/tcp/telnet/__init__.py +2 -1
  71. runtimepy/net/udp/connection.py +10 -6
  72. runtimepy/net/udp/protocol.py +5 -6
  73. runtimepy/net/udp/queue.py +5 -2
  74. runtimepy/net/udp/tftp/base.py +2 -1
  75. runtimepy/net/websocket/connection.py +50 -8
  76. runtimepy/primitives/array/__init__.py +7 -5
  77. runtimepy/primitives/base.py +3 -2
  78. runtimepy/primitives/field/__init__.py +35 -2
  79. runtimepy/primitives/field/fields.py +11 -2
  80. runtimepy/primitives/field/manager/base.py +19 -2
  81. runtimepy/primitives/serializable/base.py +5 -2
  82. runtimepy/primitives/serializable/fixed.py +5 -2
  83. runtimepy/primitives/serializable/prefixed.py +4 -1
  84. runtimepy/primitives/types/base.py +4 -1
  85. runtimepy/primitives/types/bounds.py +10 -4
  86. runtimepy/registry/__init__.py +20 -0
  87. runtimepy/registry/name.py +6 -0
  88. runtimepy/requirements.txt +2 -2
  89. runtimepy/ui/controls.py +20 -1
  90. {runtimepy-5.14.2.dist-info → runtimepy-5.15.0.dist-info}/METADATA +6 -6
  91. {runtimepy-5.14.2.dist-info → runtimepy-5.15.0.dist-info}/RECORD +95 -92
  92. {runtimepy-5.14.2.dist-info → runtimepy-5.15.0.dist-info}/WHEEL +1 -1
  93. runtimepy/data/404.html +0 -7
  94. {runtimepy-5.14.2.dist-info → runtimepy-5.15.0.dist-info}/entry_points.txt +0 -0
  95. {runtimepy-5.14.2.dist-info → runtimepy-5.15.0.dist-info}/licenses/LICENSE +0 -0
  96. {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[_BinaryMessage] = _asyncio.Queue()
28
+ self.queue: _asyncio.Queue[BinaryMessage] = _asyncio.Queue()
27
29
 
28
30
 
29
31
  class TransportMixin:
@@ -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": None,
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
- candidate, search, {"applications": self.apps.keys()}
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[bytes],
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[bytes],
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("/404.html", response)
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[bytes]], Html
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[bytes],
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[bytes],
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-success-subtle bg-gradient",
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="plot", icon="graph-up")
18
+ modal = Modal(tabs, name="settings", icon="sliders")
19
19
  under_construction(modal.footer)
20
20
 
21
- div(tag="h1", text="settings", parent=modal.body)
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="h2", text="minimum transmit period (ms)", parent=modal.body)
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
- chan: AnyChannel,
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 '{chan.default}'.",
53
- value=chan.default,
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-1 pe-1",
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(control, name, enum, cast(int, chan.raw.value))
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-emphasis")
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("toggle-value", *TABLE_BUTTON_CLASSES)
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, name, chan, *TABLE_BUTTON_CLASSES, front=False
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
- container = value_input_box(name, control)
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(container, name, chan, *TABLE_BUTTON_CLASSES)
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
- # Determine if a slider should be created.
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("toggle-value", *TABLE_BUTTON_CLASSES)
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("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)