pgntui 0.2.0__tar.gz → 0.2.2__tar.gz

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 (98) hide show
  1. {pgntui-0.2.0 → pgntui-0.2.2}/PKG-INFO +1 -1
  2. {pgntui-0.2.0 → pgntui-0.2.2}/packaging/homebrew/pgntui.rb +1 -1
  3. {pgntui-0.2.0 → pgntui-0.2.2}/packaging/winget/phobic.pgntui.yaml +2 -2
  4. {pgntui-0.2.0 → pgntui-0.2.2}/pyproject.toml +4 -1
  5. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/__init__.py +1 -1
  6. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/app.py +38 -4
  7. pgntui-0.2.2/src/pgntui/examples/containers/main.json +14 -0
  8. pgntui-0.2.2/src/pgntui/examples/signals/anchor_light.json +12 -0
  9. pgntui-0.2.2/src/pgntui/examples/signals/bilge_alarm.json +10 -0
  10. pgntui-0.2.2/src/pgntui/examples/signals/target_heading.json +14 -0
  11. pgntui-0.2.2/tests/test_app_empty_welcome.py +50 -0
  12. pgntui-0.2.2/tests/test_app_quit_binding.py +51 -0
  13. {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_example_workspace.py +25 -3
  14. {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_signals_loader.py +21 -11
  15. {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_smoke.py +1 -1
  16. pgntui-0.2.0/src/pgntui/examples/containers/main.json +0 -11
  17. {pgntui-0.2.0 → pgntui-0.2.2}/.github/workflows/ci.yml +0 -0
  18. {pgntui-0.2.0 → pgntui-0.2.2}/.github/workflows/release.yml +0 -0
  19. {pgntui-0.2.0 → pgntui-0.2.2}/.gitignore +0 -0
  20. {pgntui-0.2.0 → pgntui-0.2.2}/LICENSE +0 -0
  21. {pgntui-0.2.0 → pgntui-0.2.2}/README.md +0 -0
  22. {pgntui-0.2.0 → pgntui-0.2.2}/docs/superpowers/plans/2026-06-04-pgntui-implementation.md +0 -0
  23. {pgntui-0.2.0 → pgntui-0.2.2}/docs/superpowers/specs/2026-06-04-pgntui-design.md +0 -0
  24. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/__main__.py +0 -0
  25. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/config.py +0 -0
  26. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/containers/__init__.py +0 -0
  27. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/containers/loader.py +0 -0
  28. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/containers/screen.py +0 -0
  29. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/debug/__init__.py +0 -0
  30. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/debug/tab.py +0 -0
  31. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/decode/__init__.py +0 -0
  32. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/decode/canboat.py +0 -0
  33. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/decode/pgns.json +0 -0
  34. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/decode/router.py +0 -0
  35. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/drivers/__init__.py +0 -0
  36. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/drivers/actisense.py +0 -0
  37. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/drivers/base.py +0 -0
  38. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/drivers/replay.py +0 -0
  39. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/examples/__init__.py +0 -0
  40. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/examples/config.toml +0 -0
  41. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/examples/signals/depth.json +0 -0
  42. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/examples/signals/engine_rpm.json +0 -0
  43. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/examples/signals/speed.json +0 -0
  44. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/examples/signals/water_temp.json +0 -0
  45. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/logging/__init__.py +0 -0
  46. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/logging/csv.py +0 -0
  47. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/recording/__init__.py +0 -0
  48. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/recording/reader.py +0 -0
  49. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/recording/writer.py +0 -0
  50. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/replay_mode.py +0 -0
  51. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/signals/__init__.py +0 -0
  52. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/signals/base.py +0 -0
  53. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/signals/widgets.py +0 -0
  54. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/themes/__init__.py +0 -0
  55. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/themes/builtin/__init__.py +0 -0
  56. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/themes/builtin/amber-crt.json +0 -0
  57. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/themes/builtin/dark.json +0 -0
  58. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/themes/builtin/green-phosphor.json +0 -0
  59. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/themes/builtin/light.json +0 -0
  60. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/themes/builtin/mono-ascii.json +0 -0
  61. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/themes/builtin/rainbow-disco.json +0 -0
  62. {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/themes/loader.py +0 -0
  63. {pgntui-0.2.0 → pgntui-0.2.2}/tests/__init__.py +0 -0
  64. {pgntui-0.2.0 → pgntui-0.2.2}/tests/fixtures/__init__.py +0 -0
  65. {pgntui-0.2.0 → pgntui-0.2.2}/tests/fixtures/e2e_containers/engine.json +0 -0
  66. {pgntui-0.2.0 → pgntui-0.2.2}/tests/fixtures/e2e_containers/nav.json +0 -0
  67. {pgntui-0.2.0 → pgntui-0.2.2}/tests/fixtures/e2e_session.pgnlog +0 -0
  68. {pgntui-0.2.0 → pgntui-0.2.2}/tests/fixtures/e2e_signals/engine_rpm.json +0 -0
  69. {pgntui-0.2.0 → pgntui-0.2.2}/tests/fixtures/e2e_signals/wind_speed.json +0 -0
  70. {pgntui-0.2.0 → pgntui-0.2.2}/tests/fixtures/frames.py +0 -0
  71. {pgntui-0.2.0 → pgntui-0.2.2}/tests/fixtures/sample.pgnlog +0 -0
  72. {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_actisense_driver.py +0 -0
  73. {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_app_frame_loop.py +0 -0
  74. {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_app_on_write.py +0 -0
  75. {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_app_record_toggle.py +0 -0
  76. {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_app_shell.py +0 -0
  77. {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_builtin_themes.py +0 -0
  78. {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_canboat.py +0 -0
  79. {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_cli.py +0 -0
  80. {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_config.py +0 -0
  81. {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_container_screen.py +0 -0
  82. {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_containers_loader.py +0 -0
  83. {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_csv_logger.py +0 -0
  84. {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_debug_tab.py +0 -0
  85. {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_drivers_base.py +0 -0
  86. {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_e2e_replay.py +0 -0
  87. {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_main_replay.py +0 -0
  88. {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_packaging.py +0 -0
  89. {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_recording_writer.py +0 -0
  90. {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_replay_driver.py +0 -0
  91. {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_replay_mode.py +0 -0
  92. {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_router.py +0 -0
  93. {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_signals_base.py +0 -0
  94. {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_themes_loader.py +0 -0
  95. {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_widgets_analog_in.py +0 -0
  96. {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_widgets_analog_out.py +0 -0
  97. {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_widgets_digital_in.py +0 -0
  98. {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_widgets_digital_out.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pgntui
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Cross-platform TUI for NMEA 2000 with canboat decoding and pluggable drivers
5
5
  Author: phobicdotno
6
6
  License: MIT License
@@ -3,7 +3,7 @@
3
3
  class Pgntui < Formula
4
4
  desc "Cross-platform TUI for NMEA 2000 with canboat decoding"
5
5
  homepage "https://github.com/phobicdotno/pgntui"
6
- url "https://files.pythonhosted.org/packages/source/p/pgntui/pgntui-0.2.0.tar.gz"
6
+ url "https://files.pythonhosted.org/packages/source/p/pgntui/pgntui-0.2.2.tar.gz"
7
7
  sha256 "REPLACE_ON_RELEASE"
8
8
  license "MIT"
9
9
 
@@ -1,6 +1,6 @@
1
1
  # Stub manifest for microsoft/winget-pkgs PR
2
2
  PackageIdentifier: phobic.pgntui
3
- PackageVersion: 0.2.0
3
+ PackageVersion: 0.2.2
4
4
  PackageLocale: en-US
5
5
  Publisher: phobic
6
6
  PackageName: pgntui
@@ -15,7 +15,7 @@ Tags: [nmea, marine, tui, terminal]
15
15
  Installers:
16
16
  - Architecture: x64
17
17
  InstallerType: portable
18
- InstallerUrl: https://github.com/phobicdotno/pgntui/releases/download/v0.2.0/pgntui-windows-x86_64.exe
18
+ InstallerUrl: https://github.com/phobicdotno/pgntui/releases/download/v0.2.2/pgntui-windows-x86_64.exe
19
19
  InstallerSha256: REPLACE_ON_RELEASE
20
20
  ManifestType: singleton
21
21
  ManifestVersion: 1.6.0
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pgntui"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "Cross-platform TUI for NMEA 2000 with canboat decoding and pluggable drivers"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -64,6 +64,9 @@ packages = ["src/pgntui"]
64
64
  "src/pgntui/examples/signals/speed.json" = "pgntui/examples/signals/speed.json"
65
65
  "src/pgntui/examples/signals/depth.json" = "pgntui/examples/signals/depth.json"
66
66
  "src/pgntui/examples/signals/water_temp.json" = "pgntui/examples/signals/water_temp.json"
67
+ "src/pgntui/examples/signals/target_heading.json" = "pgntui/examples/signals/target_heading.json"
68
+ "src/pgntui/examples/signals/bilge_alarm.json" = "pgntui/examples/signals/bilge_alarm.json"
69
+ "src/pgntui/examples/signals/anchor_light.json" = "pgntui/examples/signals/anchor_light.json"
67
70
 
68
71
  [tool.ruff]
69
72
  line-length = 100
@@ -1,3 +1,3 @@
1
1
  """pgntui — NMEA 2000 TUI."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.2.2"
@@ -52,14 +52,19 @@ class PgntuiApp(App[None]):
52
52
  serial waits) never stall the UI event loop.
53
53
  """
54
54
 
55
- CSS = ""
55
+ CSS = """
56
+ TabbedContent { height: 1fr; }
57
+ #hotkey-strip { height: 1; dock: bottom; background: $primary; }
58
+ #status-bar { height: 1; dock: bottom; }
59
+ #welcome { padding: 1 2; }
60
+ """
56
61
 
57
62
  BINDINGS = [
58
63
  ("tab", "next_container", "Next"),
59
64
  ("shift+tab", "prev_container", "Prev"),
60
65
  ("d", "show_debug", "Debug"),
61
66
  ("r", "toggle_record", "Record"),
62
- ("q", "quit", "Quit"),
67
+ ("q,ctrl+q", "force_quit", "Quit"),
63
68
  ("question_mark", "help", "Help"),
64
69
  ]
65
70
 
@@ -129,12 +134,18 @@ class PgntuiApp(App[None]):
129
134
  self._views.append((container, view))
130
135
  yield view
131
136
  with TabPane("Debug", id="debug"):
137
+ if not self._containers and self._container_titles is None:
138
+ yield Static(_WELCOME_TEXT, id="welcome", markup=True)
132
139
  self._debug_log = DebugLog(
133
140
  highlight=False, markup=False, wrap=False, id="debug-log"
134
141
  )
135
142
  yield self._debug_log
136
- yield Static("[Tab] Next [D] Debug [R] Rec [Q] Quit", id="hotkey-strip")
137
- yield Static("status: idle", id="status-bar")
143
+ yield Static(
144
+ "[Tab] Next [D] Debug [R] Rec [Q] Quit",
145
+ id="hotkey-strip",
146
+ markup=False,
147
+ )
148
+ yield Static("status: idle", id="status-bar", markup=False)
138
149
 
139
150
  @property
140
151
  def _views(self) -> list[tuple[Container, ContainerView]]:
@@ -301,6 +312,29 @@ class PgntuiApp(App[None]):
301
312
  def action_help(self) -> None:
302
313
  self._set_status("help: Tab/D/R/Q")
303
314
 
315
+ def action_force_quit(self) -> None:
316
+ """Quit immediately, bypassing Textual's ctrl+q confirmation toast."""
317
+ # Make sure recording is flushed cleanly before we exit.
318
+ if self._writer is not None:
319
+ try:
320
+ self._stop_recording()
321
+ except Exception: # pragma: no cover — defensive
322
+ pass
323
+ self.exit()
324
+
325
+
326
+ _WELCOME_TEXT = """[b]pgntui[/b] — no workspace configured
327
+
328
+ To get started:
329
+ [b]pgntui --example[/b] scaffold an example workspace at ~/.config/pgntui/
330
+ [b]pgntui replay <file.pgnlog>[/b] play a recording into the TUI
331
+ [b]pgntui --help[/b] full options
332
+
333
+ Once a workspace exists, container tabs will appear above and incoming PGN
334
+ frames will scroll here.
335
+
336
+ Press [b]Q[/b] to quit."""
337
+
304
338
 
305
339
  def _encode_analog_payload(value: float) -> bytes:
306
340
  """Minimal encoder used by the analog write callback.
@@ -0,0 +1,14 @@
1
+ {
2
+ "id": "main",
3
+ "title": "Main",
4
+ "cols": 12,
5
+ "signals": [
6
+ {"ref": "engine_rpm", "row": 0, "col": 0, "w": 3},
7
+ {"ref": "speed", "row": 0, "col": 3, "w": 3},
8
+ {"ref": "depth", "row": 0, "col": 6, "w": 3},
9
+ {"ref": "water_temp", "row": 0, "col": 9, "w": 3},
10
+ {"ref": "target_heading", "row": 1, "col": 0, "w": 12},
11
+ {"ref": "bilge_alarm", "row": 2, "col": 0, "w": 6},
12
+ {"ref": "anchor_light", "row": 2, "col": 6, "w": 6}
13
+ ]
14
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "id": "anchor_light",
3
+ "type": "digital_out",
4
+ "title": "Anchor Light",
5
+ "pgn": 127502,
6
+ "field": "Switch1",
7
+ "on_label": "ON",
8
+ "off_label": "OFF",
9
+ "write_pgn": 127502,
10
+ "write_field": "Switch1",
11
+ "log": false
12
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "id": "bilge_alarm",
3
+ "type": "digital_in",
4
+ "title": "Bilge Alarm",
5
+ "pgn": 127501,
6
+ "field": "Indicator1",
7
+ "on_label": "WET",
8
+ "off_label": "DRY",
9
+ "log": true
10
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "id": "target_heading",
3
+ "type": "analog_out",
4
+ "title": "Target Heading",
5
+ "unit": "rad",
6
+ "pgn": 65360,
7
+ "field": "Target Heading Magnetic",
8
+ "min": 0,
9
+ "max": 6.2832,
10
+ "decimals": 2,
11
+ "write_pgn": 65360,
12
+ "write_field": "Target Heading Magnetic",
13
+ "log": false
14
+ }
@@ -0,0 +1,50 @@
1
+ """Empty-workspace welcome panel is shown in the Debug tab when no containers exist."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+ from textual.widgets import Static
7
+
8
+ from pgntui.app import PgntuiApp
9
+ from pgntui.themes.loader import load_builtin
10
+
11
+
12
+ @pytest.mark.asyncio
13
+ async def test_empty_workspace_shows_welcome_panel() -> None:
14
+ app = PgntuiApp(theme=load_builtin("dark"), containers=[])
15
+ async with app.run_test() as pilot:
16
+ await pilot.pause()
17
+ welcome = app.query_one("#welcome", Static)
18
+ assert welcome is not None
19
+ # The welcome blurb must point the user at --example to scaffold a workspace.
20
+ rendered = str(welcome.render())
21
+ assert "pgntui --example" in rendered
22
+ assert "pgntui" in rendered
23
+
24
+
25
+ @pytest.mark.asyncio
26
+ async def test_welcome_panel_hidden_when_containers_are_present() -> None:
27
+ """With at least one container, the welcome panel must not appear — the user
28
+ already has a working workspace."""
29
+ app = PgntuiApp(theme=load_builtin("dark"), container_titles=["Engine"])
30
+ async with app.run_test() as pilot:
31
+ await pilot.pause()
32
+ # query() returns DOMQuery — len == 0 means not mounted.
33
+ assert len(app.query("#welcome")) == 0
34
+
35
+
36
+ @pytest.mark.asyncio
37
+ async def test_bottom_strips_visible_on_empty_workspace() -> None:
38
+ """Hotkey strip and status bar must still be reachable when the workspace is
39
+ empty (the original bug had TabbedContent absorbing all vertical space)."""
40
+ app = PgntuiApp(theme=load_builtin("dark"), containers=[])
41
+ async with app.run_test() as pilot:
42
+ await pilot.pause()
43
+ hotkeys = app.query_one("#hotkey-strip", Static)
44
+ status = app.query_one("#status-bar", Static)
45
+ assert hotkeys is not None
46
+ assert status is not None
47
+ # Sanity: hotkey strip text matches the actual bindings.
48
+ rendered = str(hotkeys.render())
49
+ assert "[Tab]" in rendered
50
+ assert "[Q]" in rendered
@@ -0,0 +1,51 @@
1
+ """Pressing q quits immediately (no Textual confirmation toast)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+
7
+ from pgntui.app import PgntuiApp
8
+ from pgntui.themes.loader import load_builtin
9
+
10
+
11
+ @pytest.mark.asyncio
12
+ async def test_q_key_quits_app_immediately() -> None:
13
+ app = PgntuiApp(theme=load_builtin("dark"), containers=[])
14
+ async with app.run_test() as pilot:
15
+ await pilot.pause()
16
+ assert app.is_running
17
+ await pilot.press("q")
18
+ # Give the action loop a beat to process the exit.
19
+ for _ in range(5):
20
+ await pilot.pause()
21
+ if not app.is_running:
22
+ break
23
+ assert not app.is_running
24
+
25
+
26
+ @pytest.mark.asyncio
27
+ async def test_ctrl_q_key_also_quits_app() -> None:
28
+ app = PgntuiApp(theme=load_builtin("dark"), containers=[])
29
+ async with app.run_test() as pilot:
30
+ await pilot.pause()
31
+ assert app.is_running
32
+ await pilot.press("ctrl+q")
33
+ for _ in range(5):
34
+ await pilot.pause()
35
+ if not app.is_running:
36
+ break
37
+ assert not app.is_running
38
+
39
+
40
+ @pytest.mark.asyncio
41
+ async def test_force_quit_action_directly() -> None:
42
+ """The action_force_quit method should bypass any quit confirmation."""
43
+ app = PgntuiApp(theme=load_builtin("dark"), containers=[])
44
+ async with app.run_test() as pilot:
45
+ await pilot.pause()
46
+ app.action_force_quit()
47
+ for _ in range(5):
48
+ await pilot.pause()
49
+ if not app.is_running:
50
+ break
51
+ assert not app.is_running
@@ -6,7 +6,7 @@ from pathlib import Path
6
6
 
7
7
  from pgntui.__main__ import main
8
8
  from pgntui.containers.loader import load_container
9
- from pgntui.signals.base import load_signals_dir
9
+ from pgntui.signals.base import AnalogIn, AnalogOut, DigitalIn, DigitalOut, load_signals_dir
10
10
 
11
11
 
12
12
  def test_example_creates_expected_files(tmp_path: Path) -> None:
@@ -18,8 +18,17 @@ def test_example_creates_expected_files(tmp_path: Path) -> None:
18
18
  assert (ws / "containers").is_dir()
19
19
  signals = load_signals_dir(ws / "signals")
20
20
  ids = {s.id for s in signals}
21
- # We ship four sensible defaults.
22
- assert ids == {"engine_rpm", "speed", "depth", "water_temp"}
21
+ # Seven shipped defaults: four analog readouts plus one of each remaining
22
+ # widget kind so the scaffold exercises the full UI surface.
23
+ assert ids == {
24
+ "engine_rpm",
25
+ "speed",
26
+ "depth",
27
+ "water_temp",
28
+ "target_heading",
29
+ "bilge_alarm",
30
+ "anchor_light",
31
+ }
23
32
  # Containers reference signal ids that exist.
24
33
  main_container = load_container(ws / "containers" / "main.json", ids)
25
34
  refs = {p.ref for p in main_container.signals}
@@ -27,6 +36,19 @@ def test_example_creates_expected_files(tmp_path: Path) -> None:
27
36
  assert main_container.cols == 12
28
37
 
29
38
 
39
+ def test_example_includes_all_four_widget_types(tmp_path: Path) -> None:
40
+ """Scaffolded workspace must contain at least one signal of each kind."""
41
+ ws = tmp_path / "ws"
42
+ rc = main(["--workspace", str(ws), "--example"])
43
+ assert rc == 0
44
+ signals = load_signals_dir(ws / "signals")
45
+ kinds = {type(s) for s in signals}
46
+ assert AnalogIn in kinds
47
+ assert AnalogOut in kinds
48
+ assert DigitalIn in kinds
49
+ assert DigitalOut in kinds
50
+
51
+
30
52
  def test_example_refuses_when_workspace_non_empty(tmp_path: Path, capsys) -> None: # type: ignore[no-untyped-def]
31
53
  ws = tmp_path / "ws"
32
54
  ws.mkdir()
@@ -6,28 +6,38 @@ from importlib import resources
6
6
 
7
7
  import pytest
8
8
 
9
- from pgntui.signals.base import AnalogIn, AnalogOut, DigitalIn, DigitalOut, load_signal
9
+ from pgntui.signals.base import AnalogIn, AnalogOut, DigitalIn, DigitalOut, Signal, load_signal
10
10
 
11
+ _EXAMPLE_EXPECTED: dict[str, type[Signal]] = {
12
+ "engine_rpm": AnalogIn,
13
+ "speed": AnalogIn,
14
+ "depth": AnalogIn,
15
+ "water_temp": AnalogIn,
16
+ "target_heading": AnalogOut,
17
+ "bilge_alarm": DigitalIn,
18
+ "anchor_light": DigitalOut,
19
+ }
11
20
 
12
- @pytest.mark.parametrize(
13
- "name",
14
- ["engine_rpm", "speed", "depth", "water_temp"],
15
- )
21
+
22
+ @pytest.mark.parametrize("name", sorted(_EXAMPLE_EXPECTED))
16
23
  def test_each_example_signal_loads(name: str, tmp_path) -> None: # type: ignore[no-untyped-def]
17
- """Every bundled signal JSON parses into a real Signal subclass."""
24
+ """Every bundled signal JSON parses into the expected Signal subclass."""
18
25
  src = resources.files("pgntui.examples.signals").joinpath(f"{name}.json")
19
26
  target = tmp_path / f"{name}.json"
20
27
  with src.open("rb") as fh:
21
28
  target.write_bytes(fh.read())
22
29
  sig = load_signal(target)
23
- # The 4 bundled signals are all analog_in dashboards. If we ever ship a
24
- # digital example, extend this assertion.
25
- assert isinstance(sig, AnalogIn)
30
+ assert isinstance(sig, _EXAMPLE_EXPECTED[name])
26
31
  assert sig.id == name
27
32
  assert sig.pgn > 0
28
33
  assert sig.field
29
- assert sig.min < sig.max
30
- assert sig.smoothing >= 0
34
+ if isinstance(sig, AnalogIn | AnalogOut):
35
+ assert sig.min < sig.max
36
+ if isinstance(sig, AnalogIn):
37
+ assert sig.smoothing >= 0
38
+ if isinstance(sig, AnalogOut | DigitalOut):
39
+ assert sig.write_pgn > 0
40
+ assert sig.write_field
31
41
 
32
42
 
33
43
  def test_signal_load_round_trip_all_types(tmp_path) -> None: # type: ignore[no-untyped-def]
@@ -2,7 +2,7 @@ from pgntui import __version__
2
2
 
3
3
 
4
4
  def test_version() -> None:
5
- assert __version__ == "0.2.0"
5
+ assert __version__ == "0.2.2"
6
6
 
7
7
 
8
8
  def test_main_returns_zero() -> None:
@@ -1,11 +0,0 @@
1
- {
2
- "id": "main",
3
- "title": "Main",
4
- "cols": 12,
5
- "signals": [
6
- {"ref": "engine_rpm", "row": 0, "col": 0, "w": 6},
7
- {"ref": "speed", "row": 0, "col": 6, "w": 6},
8
- {"ref": "depth", "row": 1, "col": 0, "w": 6},
9
- {"ref": "water_temp", "row": 1, "col": 6, "w": 6}
10
- ]
11
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes