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
@@ -4,9 +4,9 @@ A module implementing a simple request-target (3.2) interface.
4
4
 
5
5
  # built-in
6
6
  import http
7
- from typing import Optional, Tuple
7
+ from typing import Optional
8
8
 
9
- PathMaybeQuery = Tuple[str, Optional[str]]
9
+ PathMaybeQuery = tuple[str, Optional[str]]
10
10
 
11
11
 
12
12
  class RequestTarget:
@@ -20,7 +20,7 @@ class RequestTarget:
20
20
  self.raw = request_target_raw
21
21
 
22
22
  # Host and port.
23
- self.authority_form: Optional[Tuple[str, int]] = None
23
+ self.authority_form: Optional[tuple[str, int]] = None
24
24
 
25
25
  # Path and optional query.
26
26
  self.origin_form: Optional[PathMaybeQuery] = None
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
 
@@ -40,6 +40,7 @@ def populate_tabs(app: AppInfo, tabs: TabbedContent) -> None:
40
40
  tabs,
41
41
  icon="arrow-repeat",
42
42
  markdown=task.markdown,
43
+ js_uris=task.config.get("js_uris", []),
43
44
  ).entry()
44
45
 
45
46
  # Struct tabs.
@@ -51,6 +52,7 @@ def populate_tabs(app: AppInfo, tabs: TabbedContent) -> None:
51
52
  tabs,
52
53
  icon="bucket",
53
54
  markdown=struct.markdown,
55
+ js_uris=struct.config.get("js_uris", []),
54
56
  ).entry()
55
57
 
56
58
  # Subprocess tabs.
@@ -109,16 +111,27 @@ def channel_environments(app: AppInfo, tabs: TabbedContent) -> None:
109
111
  # Remove tab-content scrolling.
110
112
  tabs.set_scroll(False)
111
113
 
112
- # Tab name filter.
113
- input_box(tabs.tabs, label="tab", description="Tab name filter.")
114
-
115
114
  centered_markdown(
116
115
  tabs.tabs,
117
116
  app.config_param("top_markdown", "configure `top_markdown`"),
118
117
  "border-start",
119
118
  "border-bottom",
120
119
  "border-end",
120
+ "bg-gradient-tertiary-to-top",
121
+ )
122
+
123
+ # Tab name filter.
124
+ _, label, box = input_box(
125
+ div(tag="form", autocomplete="off", parent=tabs.tabs),
126
+ label="tab",
127
+ description="Tab name filter.",
128
+ placement="bottom",
129
+ icon="funnel",
130
+ spellcheck="false",
131
+ pattern=".* $ @",
121
132
  )
133
+ label.add_class("border-top-0")
134
+ box.add_class("border-top-0")
122
135
 
123
136
  populate_tabs(app, tabs)
124
137
 
@@ -129,6 +142,18 @@ def channel_environments(app: AppInfo, tabs: TabbedContent) -> None:
129
142
  icon="table",
130
143
  id="channels-button",
131
144
  )
145
+ tabs.add_button(
146
+ "Open channel table",
147
+ "",
148
+ icon="arrow-bar-right",
149
+ id="open-channels-button",
150
+ )
151
+ tabs.add_button(
152
+ "Dedent channel table",
153
+ "",
154
+ icon="arrow-bar-left",
155
+ id="dedent-channels-button",
156
+ )
132
157
 
133
158
  # Plot settings modal.
134
159
  plot_settings(tabs)
@@ -149,11 +174,12 @@ def channel_environments(app: AppInfo, tabs: TabbedContent) -> None:
149
174
  app.config_param("bottom_markdown", "configure `bottom_markdown`"),
150
175
  "border-start",
151
176
  "border-end",
177
+ "bg-gradient-tertiary-to-bottom",
152
178
  )
153
179
 
154
180
  # Add splash screen element.
155
181
  div(
156
182
  id=f"{PKG_NAME}-splash",
157
183
  parent=tabs.container,
158
- class_str="bg-success-subtle bg-gradient",
184
+ class_str="bg-secondary-subtle bg-gradient",
159
185
  )
@@ -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",
@@ -28,12 +28,13 @@ class ChannelEnvironmentTabBase(Tab, LoggerMixin, MarkdownMixin):
28
28
  tabs: TabbedContent,
29
29
  icon: str = "alarm",
30
30
  markdown: str = None,
31
+ **kwargs,
31
32
  ) -> None:
32
33
  """Initialize this instance."""
33
34
 
34
35
  self.command = command
35
36
  self.set_markdown(markdown=markdown, package=PKG_NAME)
36
- super().__init__(name, app, tabs, source="env", icon=icon)
37
+ super().__init__(name, app, tabs, source="env", icon=icon, **kwargs)
37
38
 
38
39
  # Logging.
39
40
  LoggerMixin.__init__(self, logger=self.command.logger)
@@ -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")