Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/build_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,10 @@ jobs:
build_tests:
uses: OpenVoiceOS/gh-automations/.github/workflows/build-tests.yml@dev
secrets: inherit
with:
install_extras: "test"
test_path: "test/"
# The opm.gui_adapter loader (OVOSGUIAdapterFactory) is not yet on PyPI; it
# lives on the ovos-plugin-manager `gui` branch. Resolve it from git until it
# publishes, after which this line drops and the pyproject floor takes over.
pre_install_pip: "git+https://github.com/OpenVoiceOS/ovos-plugin-manager@gui"
7 changes: 7 additions & 0 deletions .github/workflows/unit_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ jobs:
sudo apt-get update
sudo apt install python3-dev swig
python -m pip install build wheel
- name: Install ovos-plugin-manager (gui branch)
# The opm.gui_adapter loader (OVOSGUIAdapterFactory) is not yet on PyPI;
# it lives on the ovos-plugin-manager `gui` branch. Resolve it from git
# until it publishes, after which this step drops and the pyproject floor
# takes over.
run: |
pip install git+https://github.com/OpenVoiceOS/ovos-plugin-manager@gui
- name: Install repo
run: |
pip install -e .
Expand Down
181 changes: 97 additions & 84 deletions GUI_DESIGN.md

Large diffs are not rendered by default.

116 changes: 116 additions & 0 deletions docs/adapter-development/CONTRACT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Adapter Interface Contract

The binding contract a GUI adapter (`opm.gui_adapter` plugin) must honor to work
with ovos-gui. Adapters subclass
`ovos_plugin_manager.templates.gui.AbstractGUIPlugin`.

Routing is keyed solely by `session_id`. A shared/multi-room screen is expressed
by clients sharing the same `session_id`; the on-device default is
`session_id == "default"`. There is no `site_id` and no separate `routing_key`.

## Construction

```python
def __init__(self, config: dict, bus: MessageBusClient | None = None):
super().__init__(config, bus)
```

- `config` — the adapter section from `mycroft.conf → gui.adapters.<entry-point-name>`. May be empty.
- `bus` — the shared OVOS `MessageBusClient`. May be `None` in tests. Use it via `self.bus`.

Initialize only lightweight resources and return quickly: adapters load at GUI
startup, so a slow `__init__` blocks the service. Start servers/render pipelines
on a daemon thread.

## Template handlers

```python
def handle_show_weather(self, skill_id: str, data: dict, session_id: str = "default") -> None: ...
```

- `skill_id` — the namespace that requested the display.
- `data` — the full session-data dict for that namespace at call time (reserved keys `__from`/`__idle` already stripped).
- `session_id` — the target session; deliver only to clients on that session.

Override only the handlers you support; all default to no-ops. The full mapping
of `SYSTEM_*` template names to handler methods is
`AbstractGUIPlugin._TEMPLATE_HANDLERS`; dispatch goes through
`dispatch_template(template, skill_id, data, session_id)`, which catches and
logs exceptions.

## Lifecycle hooks

```python
def on_namespace_activated(self, skill_id: str, session_id: str = "default") -> None: ...
def on_namespace_deactivated(self, skill_id: str, session_id: str = "default") -> None: ...
def on_idle(self) -> None: ...
def on_session_update(self, skill_id: str, data: dict, session_id: str = "default") -> None: ...
def on_status_event(self, event_name: str, data: dict, session_id: str = "default") -> None: ...
```

`on_status_event` carries system-wide signals (e.g. `recognizer_loop:wakeword`,
`speak`); broadcast these to all clients regardless of `session_id`.

## Connection status

```python
def any_client_connected(self) -> bool: ...
```

Return `True` when at least one client is connected. ovos-gui answers
`gui.status.request` with `True` if any adapter reports a connected client.

## Behavioral requirements

- **Exception-safe.** Never let an exception escape a handler or hook; catch and
log. ovos-gui wraps calls defensively, but adapters must not rely on it.
- **Non-blocking.** Adapters are called sequentially on the caller's thread. Do
blocking I/O (network, disk, sleep) on a background thread.
- **No shared state mutation.** Do not modify `NamespaceManager` state. Keep only
your own per-`session_id`/per-`skill_id` state and clean it up on
`on_namespace_deactivated`. Do not cache the `data` dict; re-read fresh state
via the query API when needed later.

## State query API

ovos-gui exposes read-only queries on `NamespaceManager` for crash recovery:

```python
get_active_namespace(session_id="default") -> Namespace | None
get_namespace_data(namespace_name, session_id="default") -> dict | None # a copy
get_all_sessions() -> list[str]
is_namespace_active(namespace_name, session_id="default") -> bool
```

Use sparingly and handle `None` (the namespace may have been removed).

## Registration

```toml
# pyproject.toml
[project.entry-points."opm.gui_adapter"]
"my-adapter" = "my_package:MyAdapterClass"
```

`ovos-gui` discovers and loads every installed `opm.gui_adapter` plugin at
startup via `OVOSGUIAdapterFactory.create_all(bus=..., config=...)` and dispatches
every template event to all of them (multi-modal by default). A device with no
adapters installed runs headless: dispatch is a silent no-op.

## Minimal adapter

```python
from ovos_plugin_manager.templates.gui import AbstractGUIPlugin


class TerminalGUIPlugin(AbstractGUIPlugin):
def handle_show_text(self, skill_id, data, session_id="default"):
print(f"[{skill_id}@{session_id}] "
f"{data.get('title', '')}: {data.get('text', '')}")

def on_status_event(self, event_name, data, session_id="default"):
print(f"[status] {event_name}") # broadcast; ignore session_id

def any_client_connected(self) -> bool:
return True
```
49 changes: 49 additions & 0 deletions docs/adapter-development/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# ovos-gui Architecture

ovos-gui is the GUI **state and dispatch hub**. It runs no display backend and no
WebSocket server. Skills declare *what* to show via semantic templates; ovos-gui
fans every template event out to all installed adapter plugins, which render it
on their respective surfaces (Qt, browser, terminal, ...).

```
Skill (ovos-gui-api-client GUIInterface)
│ gui.value.set / gui.page.show(SYSTEM_*) [MessageBus]
ovos-gui / NamespaceManager
• per-session LIFO namespace stack
• dispatches SYSTEM_* templates to every adapter (session_id-routed)
│ dispatch_template(...) to each adapter
├── ovos-legacy-mycroft-gui-plugin (Tornado WS -> Qt/QML)
└── ovos-gui-plugin-pyhtmx (FastAPI/SSE -> browser)
```

## Components

- **`GUIService`** (`ovos_gui/service.py`) — connects to the bus and loads
adapters via `OVOSGUIAdapterFactory.create_all(bus, config)`. Zero adapters is
a valid headless state (no-op dispatch), never an error.
- **`NamespaceManager`** (`ovos_gui/namespace.py`) — owns the active-namespace
stack per `session_id`, parses persistence (`__idle`), schedules timed
removals, and invokes adapter handlers/hooks. Exposes the read-only state
query API for adapters.
- **`AbstractGUIPlugin`** (`ovos_plugin_manager.templates.gui`) — the adapter
base class and `opm.gui_adapter` entry-point contract. See
[CONTRACT.md](CONTRACT.md).

## Routing

The sole routing key is `session_id`
(`message.context["session"]["session_id"]`, default `"default"`). Each
`session_id` has an independent namespace stack. Clients that should mirror the
same content share a `session_id`. Template events and session-data updates are
routed by `session_id`; status events are broadcast to all clients.

## Invariants

1. ovos-gui runs no WebSocket server; transports live entirely in adapters.
2. With no adapter installed, every dispatch is a silent no-op — skills never
crash on headless devices.
3. A failing adapter is isolated (logged) and never blocks other adapters or the
service.
4. All display goes through `SYSTEM_*` templates; custom QML page names are
rejected.
21 changes: 6 additions & 15 deletions ovos_gui/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
# Copyright 2019 Mycroft AI Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
""" Interface for interacting with the Mycroft gui qml viewer. """
"""OpenVoiceOS GUI framework - adapter management and namespace routing."""

from ovos_gui.message_types import GUIMessageType

__all__ = [
"GUIMessageType",
]
Loading
Loading