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.
- {pgntui-0.2.0 → pgntui-0.2.2}/PKG-INFO +1 -1
- {pgntui-0.2.0 → pgntui-0.2.2}/packaging/homebrew/pgntui.rb +1 -1
- {pgntui-0.2.0 → pgntui-0.2.2}/packaging/winget/phobic.pgntui.yaml +2 -2
- {pgntui-0.2.0 → pgntui-0.2.2}/pyproject.toml +4 -1
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/__init__.py +1 -1
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/app.py +38 -4
- pgntui-0.2.2/src/pgntui/examples/containers/main.json +14 -0
- pgntui-0.2.2/src/pgntui/examples/signals/anchor_light.json +12 -0
- pgntui-0.2.2/src/pgntui/examples/signals/bilge_alarm.json +10 -0
- pgntui-0.2.2/src/pgntui/examples/signals/target_heading.json +14 -0
- pgntui-0.2.2/tests/test_app_empty_welcome.py +50 -0
- pgntui-0.2.2/tests/test_app_quit_binding.py +51 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_example_workspace.py +25 -3
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_signals_loader.py +21 -11
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_smoke.py +1 -1
- pgntui-0.2.0/src/pgntui/examples/containers/main.json +0 -11
- {pgntui-0.2.0 → pgntui-0.2.2}/.github/workflows/ci.yml +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/.github/workflows/release.yml +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/.gitignore +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/LICENSE +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/README.md +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/docs/superpowers/plans/2026-06-04-pgntui-implementation.md +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/docs/superpowers/specs/2026-06-04-pgntui-design.md +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/__main__.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/config.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/containers/__init__.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/containers/loader.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/containers/screen.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/debug/__init__.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/debug/tab.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/decode/__init__.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/decode/canboat.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/decode/pgns.json +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/decode/router.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/drivers/__init__.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/drivers/actisense.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/drivers/base.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/drivers/replay.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/examples/__init__.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/examples/config.toml +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/examples/signals/depth.json +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/examples/signals/engine_rpm.json +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/examples/signals/speed.json +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/examples/signals/water_temp.json +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/logging/__init__.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/logging/csv.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/recording/__init__.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/recording/reader.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/recording/writer.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/replay_mode.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/signals/__init__.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/signals/base.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/signals/widgets.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/themes/__init__.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/themes/builtin/__init__.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/themes/builtin/amber-crt.json +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/themes/builtin/dark.json +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/themes/builtin/green-phosphor.json +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/themes/builtin/light.json +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/themes/builtin/mono-ascii.json +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/themes/builtin/rainbow-disco.json +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/src/pgntui/themes/loader.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/__init__.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/fixtures/__init__.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/fixtures/e2e_containers/engine.json +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/fixtures/e2e_containers/nav.json +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/fixtures/e2e_session.pgnlog +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/fixtures/e2e_signals/engine_rpm.json +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/fixtures/e2e_signals/wind_speed.json +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/fixtures/frames.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/fixtures/sample.pgnlog +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_actisense_driver.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_app_frame_loop.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_app_on_write.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_app_record_toggle.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_app_shell.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_builtin_themes.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_canboat.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_cli.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_config.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_container_screen.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_containers_loader.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_csv_logger.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_debug_tab.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_drivers_base.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_e2e_replay.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_main_replay.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_packaging.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_recording_writer.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_replay_driver.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_replay_mode.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_router.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_signals_base.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_themes_loader.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_widgets_analog_in.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_widgets_analog_out.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_widgets_digital_in.py +0 -0
- {pgntui-0.2.0 → pgntui-0.2.2}/tests/test_widgets_digital_out.py +0 -0
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
@@ -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", "
|
|
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(
|
|
137
|
-
|
|
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,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
|
-
#
|
|
22
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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]
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|