From 4b012ba8fc1ca5ae95866ff0667772979a13e79b Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Thu, 3 Aug 2023 13:42:56 +0100 Subject: [PATCH 01/24] add plugins to readme (#27) --- README.md | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 2d5269e..4718cd5 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,17 @@ GUI messagebus service, manages GUI state and implements the [gui protocol](./pr GUI clients (the application that actually draws the GUI) connect to this service +# Plugins + +plugins provide platform specific GUI functionality, such as determining when to show a homescreen or close a window + +you should usually not need any of these unless instructed to install it from a GUI client application + +- https://github.com/OpenVoiceOS/ovos-gui-plugin-shell-companion +- https://github.com/OpenVoiceOS/ovos-gui-plugin-mobile +- https://github.com/OpenVoiceOS/ovos-gui-plugin-plasmoid +- https://github.com/OpenVoiceOS/ovos-gui-plugin-bigscreen + # Configuration under mycroft.conf @@ -16,22 +27,21 @@ under mycroft.conf // Uncomment or add "idle_display_skill" to set initial homescreen // "idle_display_skill": "skill-ovos-homescreen.openvoiceos", - // Extensions provide additional GUI platform support for specific devices - // Currently supported devices: smartspeaker, bigscreen or generic + // Extensions are plugins that provide additional GUI platform support for specific devices + // eg, if using ovos-shell you should set extension to "ovos-gui-plugin-shell-companion" "extension": "generic", - // Generic extension can additionaly provide homescreen functionality - // homescreen support is disabled by default for generic extension + // Default generic extension can provide homescreen functionality if enabled "generic": { "homescreen_supported": false - } + }, // Optional file server support for remote clients // "gui_file_server": true, // "file_server_port": 8000, - // Optionally specify a default qt version for connected clients - // "default_qt_version": 5, + // Optionally specify a default qt version for connected clients that don't report it + "default_qt_version": 5 }, // The GUI messagebus websocket. Once port is created per connected GUI @@ -42,4 +52,4 @@ under mycroft.conf "ssl": false } } -``` \ No newline at end of file +``` From eb41f359e95b42a310cc2b02fb4dddb224baf9c1 Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Thu, 28 Sep 2023 16:35:47 -0700 Subject: [PATCH 02/24] Check if Skills service is running on GUI service init (#29) Co-authored-by: Daniel McKnight --- ovos_gui/namespace.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ovos_gui/namespace.py b/ovos_gui/namespace.py index 54bff59..04da53b 100644 --- a/ovos_gui/namespace.py +++ b/ovos_gui/namespace.py @@ -463,6 +463,12 @@ def __init__(self, core_bus: MessageBusClient): self._init_gui_server() self._define_message_handlers() + resp = self.core_bus.wait_for_response(Message("mycroft.skills.is_ready", + context={"source": "gui", "destination": ["skills"]})) + if resp and resp.data.get("status"): + LOG.debug("Skills service already running") + self.handle_ready() + @property def _active_homescreen(self) -> str: return Configuration().get('gui', {}).get('idle_display_skill') @@ -495,7 +501,7 @@ def _define_message_handlers(self): self.core_bus.on("gui.page_gained_focus", self.handle_page_gained_focus) self.core_bus.on("mycroft.skills.trained", self.handle_ready) - def handle_ready(self, message): + def handle_ready(self, message=None): self._ready_event.set() self.core_bus.on("gui.volunteer_page_upload", self.handle_gui_pages_available) From 5b99c6cfb26660389acbd230cffbc8a8895099f2 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 28 Sep 2023 23:36:05 +0000 Subject: [PATCH 03/24] Increment Version to 0.0.4a1 --- ovos_gui/version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ovos_gui/version.py b/ovos_gui/version.py index 23532c1..441e978 100644 --- a/ovos_gui/version.py +++ b/ovos_gui/version.py @@ -1,6 +1,6 @@ # START_VERSION_BLOCK VERSION_MAJOR = 0 VERSION_MINOR = 0 -VERSION_BUILD = 3 -VERSION_ALPHA = 0 +VERSION_BUILD = 4 +VERSION_ALPHA = 1 # END_VERSION_BLOCK From b9a6b4ff19076e6672ccc78a1070042b00301e56 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 28 Sep 2023 23:36:28 +0000 Subject: [PATCH 04/24] Update Changelog --- CHANGELOG.md | 83 ++++------------------------------------------------ 1 file changed, 6 insertions(+), 77 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6571ad8..9ae27e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,88 +1,17 @@ # Changelog -## [0.0.3a11](https://github.com/OpenVoiceOS/ovos-gui/tree/0.0.3a11) (2023-07-20) +## [0.0.4a1](https://github.com/OpenVoiceOS/ovos-gui/tree/0.0.4a1) (2023-09-28) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-gui/compare/0.0.3a10...0.0.3a11) - -**Merged pull requests:** - -- Add description to setup.py to fix \#21 [\#22](https://github.com/OpenVoiceOS/ovos-gui/pull/22) ([NeonDaniel](https://github.com/NeonDaniel)) - -## [0.0.3a10](https://github.com/OpenVoiceOS/ovos-gui/tree/0.0.3a10) (2023-07-20) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-gui/compare/V0.0.3a9...0.0.3a10) - -**Merged pull requests:** - -- Update automation to current standards [\#21](https://github.com/OpenVoiceOS/ovos-gui/pull/21) ([NeonDaniel](https://github.com/NeonDaniel)) - -## [V0.0.3a9](https://github.com/OpenVoiceOS/ovos-gui/tree/V0.0.3a9) (2023-07-19) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-gui/compare/V0.0.3a8...V0.0.3a9) - -**Merged pull requests:** - -- Fix local system resource resolution [\#20](https://github.com/OpenVoiceOS/ovos-gui/pull/20) ([NeonDaniel](https://github.com/NeonDaniel)) - -## [V0.0.3a8](https://github.com/OpenVoiceOS/ovos-gui/tree/V0.0.3a8) (2023-07-08) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-gui/compare/V0.0.3a7...V0.0.3a8) - -**Implemented enhancements:** - -- GUI File Server and Alternate GUI Framework Support [\#9](https://github.com/OpenVoiceOS/ovos-gui/pull/9) ([JarbasAl](https://github.com/JarbasAl)) - -## [V0.0.3a7](https://github.com/OpenVoiceOS/ovos-gui/tree/V0.0.3a7) (2023-07-04) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-gui/compare/V0.0.3a6...V0.0.3a7) - -**Merged pull requests:** - -- Update dependencies to stable versions [\#16](https://github.com/OpenVoiceOS/ovos-gui/pull/16) ([NeonDaniel](https://github.com/NeonDaniel)) - -## [V0.0.3a6](https://github.com/OpenVoiceOS/ovos-gui/tree/V0.0.3a6) (2023-06-23) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-gui/compare/V0.0.3a5...V0.0.3a6) - -**Merged pull requests:** - -- Unit Tests and Documentation [\#15](https://github.com/OpenVoiceOS/ovos-gui/pull/15) ([NeonDaniel](https://github.com/NeonDaniel)) - -## [V0.0.3a5](https://github.com/OpenVoiceOS/ovos-gui/tree/V0.0.3a5) (2023-06-13) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-gui/compare/V0.0.3a4...V0.0.3a5) - -**Merged pull requests:** - -- Remove unused ovos-backend-client dependency [\#14](https://github.com/OpenVoiceOS/ovos-gui/pull/14) ([NeonDaniel](https://github.com/NeonDaniel)) - -## [V0.0.3a4](https://github.com/OpenVoiceOS/ovos-gui/tree/V0.0.3a4) (2023-06-09) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-gui/compare/V0.0.3a3...V0.0.3a4) - -**Merged pull requests:** - -- Minor logging and method annotation changes [\#13](https://github.com/OpenVoiceOS/ovos-gui/pull/13) ([NeonDaniel](https://github.com/NeonDaniel)) - -## [V0.0.3a3](https://github.com/OpenVoiceOS/ovos-gui/tree/V0.0.3a3) (2023-06-09) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-gui/compare/V0.0.3a2...V0.0.3a3) - -**Implemented enhancements:** - -- :tada: - GUI plugin [\#11](https://github.com/OpenVoiceOS/ovos-gui/pull/11) ([JarbasAl](https://github.com/JarbasAl)) - -## [V0.0.3a2](https://github.com/OpenVoiceOS/ovos-gui/tree/V0.0.3a2) (2023-05-01) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-gui/compare/V0.0.3a1...V0.0.3a2) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-gui/compare/0.0.3...0.0.4a1) **Fixed bugs:** -- interfaces/ folder not getting packaged [\#8](https://github.com/OpenVoiceOS/ovos-gui/issues/8) +- ovos-core must be restarted to re-upload QML files [\#28](https://github.com/OpenVoiceOS/ovos-gui/issues/28) +- Check if Skills service is running on GUI service init [\#29](https://github.com/OpenVoiceOS/ovos-gui/pull/29) ([NeonDaniel](https://github.com/NeonDaniel)) -## [V0.0.3a1](https://github.com/OpenVoiceOS/ovos-gui/tree/V0.0.3a1) (2023-04-30) +**Merged pull requests:** -[Full Changelog](https://github.com/OpenVoiceOS/ovos-gui/compare/V0.0.2...V0.0.3a1) +- add plugins to readme [\#27](https://github.com/OpenVoiceOS/ovos-gui/pull/27) ([JarbasAl](https://github.com/JarbasAl)) From ac81a30c301f65eab067b6e9c4d322c682c00679 Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Thu, 28 Sep 2023 18:59:13 -0700 Subject: [PATCH 05/24] Add option to use host paths in containers in addition to http URLs (#25) Co-authored-by: Daniel McKnight --- README.md | 9 +++++++- ovos_gui/bus.py | 6 ++++-- ovos_gui/gui_file_server.py | 6 ++++++ ovos_gui/namespace.py | 37 +++++++++++++++++++++----------- ovos_gui/page.py | 11 +++++++--- test/unittests/test_bus.py | 21 ++++++++++++++++++ test/unittests/test_namespace.py | 6 +++++- test/unittests/test_page.py | 16 ++++++++++++++ 8 files changed, 93 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 4718cd5..45de849 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,14 @@ under mycroft.conf // Optional file server support for remote clients // "gui_file_server": true, // "file_server_port": 8000, - + + // Optional support for collecting GUI files for container support + // The ovos-gui container path for these files will be {XDG_CACHE_HOME}/ovos_gui_file_server. + // With the below configuration, the GUI client will have files prefixed with the configured host path, + // so the example below describes a situation where `{XDG_CACHE_HOME}/ovos_gui_file_server` maps + // to `/tmp/gui_files` on the filesystem where the GUI client is running. + // "gui_file_host_path": "/tmp/gui_files", + // Optionally specify a default qt version for connected clients that don't report it "default_qt_version": 5 }, diff --git a/ovos_gui/bus.py b/ovos_gui/bus.py index 4450a4e..0c82233 100644 --- a/ovos_gui/bus.py +++ b/ovos_gui/bus.py @@ -132,7 +132,8 @@ def get_client_pages(self, namespace): """ client_pages = [] server_url = self.ns_manager.gui_file_server.url if \ - self.ns_manager.gui_file_server else None + self.ns_manager.gui_file_server else \ + self.ns_manager.gui_file_host_path for page in namespace.pages: uri = page.get_uri(self.framework, server_url) client_pages.append(uri) @@ -260,7 +261,8 @@ def send_gui_pages(self, pages: List[GuiPage], namespace: str, @param position: position to insert pages at """ server_url = self.ns_manager.gui_file_server.url if \ - self.ns_manager.gui_file_server else None + self.ns_manager.gui_file_server else \ + self.ns_manager.gui_file_host_path framework = self.framework message = { diff --git a/ovos_gui/gui_file_server.py b/ovos_gui/gui_file_server.py index 50abaf4..8b352c1 100644 --- a/ovos_gui/gui_file_server.py +++ b/ovos_gui/gui_file_server.py @@ -23,6 +23,12 @@ def end_headers(self) -> None: def start_gui_http_server(qml_path: str, port: int = None): + """ + Start an http server to host GUI Resources + @param qml_path: Local file path to server + @param port: Host port to run file server on + @return: Initialized HTTP Server + """ port = port or Configuration().get("gui", {}).get("file_server_port", 8089) if os.path.exists(qml_path): diff --git a/ovos_gui/namespace.py b/ovos_gui/namespace.py index 04da53b..9903e55 100644 --- a/ovos_gui/namespace.py +++ b/ovos_gui/namespace.py @@ -49,7 +49,7 @@ from ovos_bus_client import Message, MessageBusClient from ovos_config.config import Configuration -from ovos_utils.log import LOG +from ovos_utils.log import LOG, log_deprecation from ovos_gui.bus import ( create_gui_service, @@ -458,9 +458,10 @@ def __init__(self, core_bus: MessageBusClient): self._system_res_dir = join(dirname(__file__), "res", "gui") self._ready_event = Event() self.gui_file_server = None - self.gui_file_path = None + self.gui_file_path = None # HTTP Server local path + self.gui_file_host_path = None # Docker host path self._connected_frameworks: List[str] = list() - self._init_gui_server() + self._init_gui_file_share() self._define_message_handlers() resp = self.core_bus.wait_for_response(Message("mycroft.skills.is_ready", @@ -473,16 +474,26 @@ def __init__(self, core_bus: MessageBusClient): def _active_homescreen(self) -> str: return Configuration().get('gui', {}).get('idle_display_skill') - def _init_gui_server(self): + def _init_gui_file_share(self): """ - Initialize a GUI HTTP file server if enabled in configuration + Initialize optional GUI file collection. if `gui_file_path` is + defined, resources are assumed to be referenced outside this container. + If `gui_file_server` is defined, resources will be served via HTTP """ config = Configuration().get("gui", {}) - if config.get("gui_file_server", False): - from ovos_utils.file_utils import get_temp_path + self.gui_file_host_path = config.get("gui_file_host_path") + + # Check for GUI file sharing via HTTP server or mounted host path + if config.get("gui_file_server") or self.gui_file_host_path: + from ovos_utils.xdg_utils import xdg_cache_home + if config.get("server_path"): + log_deprecation("`server_path` configuration is deprecated. " + "Files will always be saved to " + "XDG_CACHE_HOME/ovos_gui_file_server", "0.1.0") self.gui_file_path = config.get("server_path") or \ - get_temp_path("ovos_gui_file_server") - self.gui_file_server = start_gui_http_server(self.gui_file_path) + join(xdg_cache_home(), "ovos_gui_file_server") + if config.get("gui_file_server"): + self.gui_file_server = start_gui_http_server(self.gui_file_path) self._upload_system_resources() def _define_message_handlers(self): @@ -513,8 +524,8 @@ def handle_gui_pages_available(self, message: Message): GUI framework. @param message: `gui.volunteer_page_upload` message """ - if not self.gui_file_path: - LOG.debug("No GUI file server running") + if not any((self.gui_file_host_path, self.gui_file_server)): + LOG.debug("No GUI file server running or host path configured") return LOG.debug(f"Requesting resources for {self._connected_frameworks}") @@ -921,16 +932,18 @@ def handle_client_connected(self, message: Message): dict(port=port, gui_id=gui_id)) self.core_bus.emit(message) - if self.gui_file_path: + if self.gui_file_path or self.gui_file_host_path: if not self._ready_event.wait(90): LOG.warning("Not reported ready after 90s") if framework not in self._connected_frameworks: + LOG.debug(f"Requesting page upload for {framework}") self.core_bus.emit(Message("gui.request_page_upload", {'framework': framework}, {"source": "gui", "destination": ["skills", "PHAL"]})) if framework not in self._connected_frameworks: + LOG.debug(f"Connecting framework: {framework}") self._connected_frameworks.append(framework) def handle_page_interaction(self, message: Message): diff --git a/ovos_gui/page.py b/ovos_gui/page.py index 9ec77ce..641f867 100644 --- a/ovos_gui/page.py +++ b/ovos_gui/page.py @@ -50,7 +50,8 @@ def get_uri(self, framework: str = "qt5", server_url: str = None) -> str: """ Get a valid URI for this Page. @param framework: String GUI framework to get resources for - @param server_url: String server URL if available + @param server_url: String server URL if available; this could be for a + web server (http://), or a container host path (file://) @return: Absolute path to the requested resource """ if self.url: @@ -62,8 +63,12 @@ def get_uri(self, framework: str = "qt5", server_url: str = None) -> str: self.namespace if server_url: if "://" not in server_url: - LOG.debug(f"No schema in server_url, assuming 'http'") - server_url = f"http://{server_url}" + if server_url.startswith("/"): + LOG.debug(f"No schema in server_url, assuming 'file'") + server_url = f"file://{server_url}" + else: + LOG.debug(f"No schema in server_url, assuming 'http'") + server_url = f"http://{server_url}" path = f"{server_url}/{res_namespace}/{framework}/{res_filename}" LOG.info(f"Resolved server URI: {path}") return path diff --git a/test/unittests/test_bus.py b/test/unittests/test_bus.py index 862d8bd..c6ecd41 100644 --- a/test/unittests/test_bus.py +++ b/test/unittests/test_bus.py @@ -89,6 +89,9 @@ def test_get_client_pages(self): page_2.get_uri.return_value = "page_2_uri" test_namespace.pages = [page_1, page_2] + # Specify no host path mapping + self.handler.ns_manager.gui_file_host_path = None + # Test no server_url self.handler.ns_manager.gui_file_server = None pages = self.handler.get_client_pages(test_namespace) @@ -96,6 +99,14 @@ def test_get_client_pages(self): page_2.get_uri.assert_called_once_with(self.handler.framework, None) self.assertEqual(pages, ["page_1_uri", "page_2_uri"]) + # Test host path mapping + test_path = "/test/ovos-gui-file-server" + self.handler.ns_manager.gui_file_host_path = test_path + pages = self.handler.get_client_pages(test_namespace) + page_1.get_uri.assert_called_with(self.handler.framework, test_path) + page_2.get_uri.assert_called_with(self.handler.framework, test_path) + self.assertEqual(pages, ["page_1_uri", "page_2_uri"]) + # Test with server_url self.handler.ns_manager.gui_file_server = Mock() self.handler.ns_manager.gui_file_server.url = "server_url" @@ -129,6 +140,9 @@ def test_send_gui_pages(self): page_2 = GuiPage(None, "", False, False) page_2.get_uri = Mock(return_value="page_2") + # Specify no host path mapping + self.handler.ns_manager.gui_file_host_path = None + # Test no server_url self.handler.ns_manager.gui_file_server = None self.handler._framework = "qt5" @@ -141,6 +155,13 @@ def test_send_gui_pages(self): "position": test_pos, "data": [{"url": "page_1"}, {"url": "page_2"}]}) + # Test host path mapping + test_path = "/test/ovos-gui-file-server" + self.handler.ns_manager.gui_file_host_path = test_path + self.handler.send_gui_pages([page_1, page_2], test_ns, test_pos) + page_1.get_uri.assert_called_with(self.handler.framework, test_path) + page_2.get_uri.assert_called_with(self.handler.framework, test_path) + # Test with server_url self.handler.ns_manager.gui_file_server = Mock() self.handler.ns_manager.gui_file_server.url = "server_url" diff --git a/test/unittests/test_namespace.py b/test/unittests/test_namespace.py index cdd08f3..44e5055 100644 --- a/test/unittests/test_namespace.py +++ b/test/unittests/test_namespace.py @@ -44,6 +44,10 @@ class TestNamespace(TestCase): def setUp(self): self.namespace = Namespace("foo") + def test_init_gui_file_share(self): + # TODO: Test init with/without server and host config + pass + def test_add(self): add_namespace_message = dict( type="mycroft.session.list.insert", @@ -202,7 +206,7 @@ def setUp(self): with mock.patch(PATCH_MODULE + ".create_gui_service"): self.namespace_manager = NamespaceManager(FakeBus()) - def test_init_gui_server(self): + def test_init_gui_file_share(self): # TODO pass diff --git a/test/unittests/test_page.py b/test/unittests/test_page.py index 366c923..b7756c9 100644 --- a/test/unittests/test_page.py +++ b/test/unittests/test_page.py @@ -36,6 +36,22 @@ def test_gui_page_from_server(self): self.assertEqual(qt6, f"https://files.local/{namespace}/qt5/{page_id}.qml") + def test_gui_page_from_mapped_path(self): + name = "test_page" + persistent = False + duration = 60 + page_id = "test_page" + namespace = "skill.test" + + page = GuiPage(None, name, persistent, duration, page_id, namespace) + qt5 = page.get_uri(server_url="/path/for/gui/client") + self.assertEqual(qt5, + f"file:///path/for/gui/client/{namespace}/qt5/{page_id}.qml") + + qt6 = page.get_uri(server_url="/path/for/gui/client") + self.assertEqual(qt6, + f"file:///path/for/gui/client/{namespace}/qt5/{page_id}.qml") + def test_gui_page_from_local_path(self): name = "test" persistent = True From 4bedf1859fdc15f1e41cb29ae2efb77ba69cb40f Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 29 Sep 2023 01:59:28 +0000 Subject: [PATCH 06/24] Increment Version to 0.0.4a2 --- ovos_gui/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_gui/version.py b/ovos_gui/version.py index 441e978..388be45 100644 --- a/ovos_gui/version.py +++ b/ovos_gui/version.py @@ -2,5 +2,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 4 -VERSION_ALPHA = 1 +VERSION_ALPHA = 2 # END_VERSION_BLOCK From 462cbd603fcea9023965f9efc976ac66a10a169a Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 29 Sep 2023 02:00:00 +0000 Subject: [PATCH 07/24] Update Changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ae27e0..e0d3e18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [0.0.4a2](https://github.com/OpenVoiceOS/ovos-gui/tree/0.0.4a2) (2023-09-29) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-gui/compare/0.0.4a1...0.0.4a2) + +**Implemented enhancements:** + +- Add option to use host paths in containers in addition to http URLs [\#25](https://github.com/OpenVoiceOS/ovos-gui/pull/25) ([NeonDaniel](https://github.com/NeonDaniel)) + ## [0.0.4a1](https://github.com/OpenVoiceOS/ovos-gui/tree/0.0.4a1) (2023-09-28) [Full Changelog](https://github.com/OpenVoiceOS/ovos-gui/compare/0.0.3...0.0.4a1) From 62e0332e32baab255401d324935e3acaa31f5bb4 Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Fri, 29 Sep 2023 09:44:55 -0700 Subject: [PATCH 08/24] Refactor skills status check to resolve init bug (#31) Co-authored-by: Daniel McKnight --- ovos_gui/namespace.py | 8 +------- ovos_gui/service.py | 11 ++++++++++- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/ovos_gui/namespace.py b/ovos_gui/namespace.py index 9903e55..3cde48f 100644 --- a/ovos_gui/namespace.py +++ b/ovos_gui/namespace.py @@ -464,12 +464,6 @@ def __init__(self, core_bus: MessageBusClient): self._init_gui_file_share() self._define_message_handlers() - resp = self.core_bus.wait_for_response(Message("mycroft.skills.is_ready", - context={"source": "gui", "destination": ["skills"]})) - if resp and resp.data.get("status"): - LOG.debug("Skills service already running") - self.handle_ready() - @property def _active_homescreen(self) -> str: return Configuration().get('gui', {}).get('idle_display_skill') @@ -512,7 +506,7 @@ def _define_message_handlers(self): self.core_bus.on("gui.page_gained_focus", self.handle_page_gained_focus) self.core_bus.on("mycroft.skills.trained", self.handle_ready) - def handle_ready(self, message=None): + def handle_ready(self, message): self._ready_event.set() self.core_bus.on("gui.volunteer_page_upload", self.handle_gui_pages_available) diff --git a/ovos_gui/service.py b/ovos_gui/service.py index 8481e1a..c30e1e3 100644 --- a/ovos_gui/service.py +++ b/ovos_gui/service.py @@ -1,4 +1,4 @@ -from ovos_bus_client import MessageBusClient +from ovos_bus_client import MessageBusClient, Message from ovos_utils.log import LOG from ovos_utils.process_utils import ProcessStatus, StatusCallbackMap, ProcessState from ovos_config.config import Configuration @@ -60,6 +60,15 @@ def run(self): # if they may cause the Service to fail. self.status.set_alive() self._init_bus_client() + + # Bus is connected, check if the skills service is ready + resp = self.bus.wait_for_response( + Message("mycroft.skills.is_ready", + context={"source": "gui", "destination": ["skills"]})) + if resp and resp.data.get("status"): + LOG.debug("Skills service already running") + self.gui.handle_ready(resp) + self.extension_manager = ExtensionsManager("EXTENSION_SERVICE", self.bus, self.gui) self.status.set_ready() From ebb1cf25c1f033eed5cac7a5e4a1232cb2c8ef89 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 29 Sep 2023 16:45:14 +0000 Subject: [PATCH 09/24] Increment Version to 0.0.4a3 --- ovos_gui/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_gui/version.py b/ovos_gui/version.py index 388be45..8cb0779 100644 --- a/ovos_gui/version.py +++ b/ovos_gui/version.py @@ -2,5 +2,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 4 -VERSION_ALPHA = 2 +VERSION_ALPHA = 3 # END_VERSION_BLOCK From e8d9ca6afb854e96effc7f6f7aa1965708a8f61a Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 29 Sep 2023 16:45:38 +0000 Subject: [PATCH 10/24] Update Changelog --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0d3e18..7b76245 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [0.0.4a3](https://github.com/OpenVoiceOS/ovos-gui/tree/0.0.4a3) (2023-09-29) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-gui/compare/0.0.4a2...0.0.4a3) + +**Fixed bugs:** + +- Refactor skills status check to resolve init bug [\#31](https://github.com/OpenVoiceOS/ovos-gui/pull/31) ([NeonDaniel](https://github.com/NeonDaniel)) + +**Closed issues:** + +- You must execute run\_forever\(\) before emitting messages [\#30](https://github.com/OpenVoiceOS/ovos-gui/issues/30) + ## [0.0.4a2](https://github.com/OpenVoiceOS/ovos-gui/tree/0.0.4a2) (2023-09-29) [Full Changelog](https://github.com/OpenVoiceOS/ovos-gui/compare/0.0.4a1...0.0.4a2) From 3efdb6796c28e9b0c048a750a5dbe5a41710b2f4 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Fri, 6 Oct 2023 18:04:34 +0100 Subject: [PATCH 11/24] Update protocol.md (#32) --- protocol.md | 209 ++++++++++++++++++++++++++++------------------------ 1 file changed, 111 insertions(+), 98 deletions(-) diff --git a/protocol.md b/protocol.md index dcbd8cd..eae7a00 100644 --- a/protocol.md +++ b/protocol.md @@ -2,28 +2,30 @@ This protocol defines how ovos-gui communicates with connected clients -- [ACTIVE SKILLS LIST](#active-skills-list) -- [EVENTS](#events) +- [CONNECTION - mycroft.gui.connected](#connection---mycroftguiconnected) +- [NAMESPACES](#namespaces) + * [Active Skills - mycroft.system.active_skills](#active-skills---mycroftsystemactive-skills) +- [PAGES - mycroft.gui.list.xxx](#pages---mycroftguilistxxx) + * [Insert new page at position](#insert-new-page-at-position) + * [Move pages within the list](#move-pages-within-the-list) + * [Remove pages from the list](#remove-pages-from-the-list) +- [EVENTS - mycroft.events.triggered](#events---mycrofteventstriggered) * [SPECIAL EVENT: page_gained_focus](#special-event--page-gained-focus) -- [SKILL DATA](#skill-data) +- [SKILL DATA - mycroft.session.xxx](#skill-data---mycroftsessionxxx) * [Sets a new key/value in the sessionData dictionary](#sets-a-new-key-value-in-the-sessiondata-dictionary) * [Deletes a key/value pair from the sessionData dictionary](#deletes-a-key-value-pair-from-the-sessiondata-dictionary) -- [MODELS](#models) - * [Inserts new items at position](#inserts-new-items-at-position) - * [Updates item values starting at the given position, as many items as there are in the array](#updates-item-values-starting-at-the-given-position--as-many-items-as-there-are-in-the-array) - * [Move items within the list](#move-items-within-the-list) - * [Remove items from the list](#remove-items-from-the-list) -- [GUI MODEL](#gui-model) - * [Inserts new GUI items at position](#inserts-new-gui-items-at-position) - * [Move items within the list](#move-items-within-the-list-1) - * [Remove items from the list](#remove-items-from-the-list-1) + * [Lists](#lists) + + [Inserts new items at position](#inserts-new-items-at-position) + + [Updates item values starting at the given position, as many items as there are in the array](#updates-item-values-starting-at-the-given-position--as-many-items-as-there-are-in-the-array) + + [Move items within the list](#move-items-within-the-list) + + [Remove items from the list](#remove-items-from-the-list) -# CONNECTION +# CONNECTION - mycroft.gui.connected on connection gui clients announce themselves -This is an extension by OVOS to the original mycroft protocol +This is an extension by OVOS to the [original mycroft protocol](https://github.com/MycroftAI/mycroft-gui/blob/master/transportProtocol.md) ```javascript @@ -33,25 +35,87 @@ This is an extension by OVOS to the original mycroft protocol } ``` -# ACTIVE SKILLS LIST +# NAMESPACES -The active skill data, described in the section MODELS is mandatory for the rest of the protocol to work. I.e. if some data or an event arrives with namespace "mycroft.weather", the skill id "mycroft.weather" must have been advertised as recently used in the recent skills model beforehand, otherwise all requests on that namespace will be ignored on both client and serverside and considered a protocol error. -Recent skills are ordered from the last used to the oldest, so the first item of the model will always be the the one showing any QML GUI, if available. +ovos-gui maintains a list of namespaces with GUI data, namespaces usually correspond to a skill_id + +Every message in the gui protocol specifies a namespace it belongs to + +gui clients usualy display all namespaces, but can be requested to display a single one, + +eg, have a dedicated window to show a skill as a [traditional desktop app](https://github.com/OpenVoiceOS/ovos-ocp-audio-plugin/blob/dev/ovos_plugin_common_play/ocp/res/desktop/OCP.desktop) + + +## Active Skills - mycroft.system.active_skills + +a reserved namespace is "mycroft.system.active_skills", the data contained in this namespace defines the namespace display priority + +Recent skills are ordered from the last used to the oldest, so the first item of the list will always be the the one showing any GUI page, if available. + +see the section about [lists](https://github.com/OpenVoiceOS/ovos-gui/blob/dev/protocol.md#lists) if you need to modify active skills + + +# PAGES - mycroft.gui.list.xxx + +Each active skill is associated with a list of uris to the QML files of all gui items that are supposed to be visible. + +Non QT GUIS get sent other file extensions such as .jsx or .html using the same message format + +## Insert new page at position +```javascript +{ + "type": "mycroft.gui.list.insert", + "namespace": "mycroft.weather" + "position": 2 + "values": [{"url": "file://..../currentWeather.qml"}, ...] //values must always be in array form +} +``` + +## Move pages within the list +```javascript +{ + "type": "mycroft.gui.list.move", + "namespace": "mycroft.weather" + "from": 2 + "to": 5 + "items_number": 2 //optional in case we want to move a big chunk of list at once +} +``` + +## Remove pages from the list +```javascript +{ + "type": "mycroft.gui.list.remove", + "namespace": "mycroft.weather" + "position": 2 + "items_number": 5 //optional in case we want to get rid a big chunk of list at once +} +``` + + +# EVENTS - mycroft.events.triggered + +Events can either be emitted by a gui client (eg, some element clicked) or by the skill (eg, in response to a voice command) -# EVENTS ```javascript { "type": "mycroft.events.triggered" - "namespace": "weather.mycroft" - "event_name": "system.pick", + "namespace": "my_skill_id" + "event_name": "my.gui.event", "parameters": {"item": 3} } ``` -* event names that start with "system." are available to all skills, like previous/next/pick. the skill author can have as many custom events as he wants -* same message format goes both ways python->gui and gui->python - ## SPECIAL EVENT: page_gained_focus + +This event is used when the ovos-gui wants a page of a particular skill to gain user attention focus and become the current active view and "focus of attention" of the user. + +when a GUI client receives it, it should render the requested GUI page + +GUI clients can also emit this event, if a new page was rendered (eg, in response to a user swipping left) + +NOTE: for responsiveness it is recommened this message is only emitted after the rendering has actually been done, skills may be waiting for this event to initiate some actons + ```javascript { "type": "mycroft.events.triggered", @@ -60,17 +124,24 @@ Recent skills are ordered from the last used to the oldest, so the first item of "data": {"number": 0} } ``` -This event is used when the server wants a page of the gui model of a particular skill to gain user attention focus and become the current active view and "focus of attention" of the user. This event is supported on both directions of communication between server and gui. -The parameter "number" is the position (starting from zero) of the page in the gui model (see the section gui model). +The parameter "number" is the position (starting from zero) of the page -# SKILL DATA +# SKILL DATA - mycroft.session.xxx -At the center of data sharing there is a key/value dictionary that is kept synchronized between the server and the GUI client. -Values can either be simple strings, numbers and booleans or be more complicated data models as described in the MODELS section. +At the center of data sharing there is a key/value dictionary that is kept synchronized between ovos-gui and the GUI client. + +Values can either be simple strings, numbers and booleans or be more complicated data types + +this event can be sent from gui clients (eg, in response to a dropdown selection) or from skills (eg, change weather data) + +NOTE: Once a new gui client connects to ovos-gui, all existing session data is sent to the client, +after that the client gets live updates via these events ## Sets a new key/value in the sessionData dictionary + Either sets a new key/value pair or replace an existing old value. + ```javascript { "type": "mycroft.session.set", @@ -78,7 +149,7 @@ Either sets a new key/value pair or replace an existing old value. "data": { "temperature": "28", "icon": "cloudy", - "forecast": [{...},...] //if it's a list a model gets created, or resetted if it was already existing, see the MODELS section + "forecast": [{...},...] //if it's a list see below for more message types } } ``` @@ -92,61 +163,35 @@ Either sets a new key/value pair or replace an existing old value. } ``` -All properties already in the dictionary need to be sent as soon as a new client connects to the web socket - -The exact message format would be in both direction both server->gui and gui->server - +## Lists -# MODELS -Models are for both skill data and active skills, distinction is just between "namespace": "mycroft.system.active_skills" and "namespace": "mycroft.weather" -All operations have a type which starts by "type": "mycroft.session.list." - -The format of the data passed via the socket is of an ordered map -```javascript -[ -{ - "key1": "value1", - "key2": "value2" -}, -{ - "key1": "value3", - "key2": "value4" -} -] -``` - -It must always be an array, even if contains a single item, and each item must contain the exact same set of keys, even if some of them could be empty. -If subsequent inserts of items will contain a different set of keys, or toehr keys more, those new keys will be ignored by the GUI. - -## Inserts new items at position +### Inserts new items at position ```javascript { "type": "mycroft.session.list.insert", - "namespace": "mycroft.system.active_skills" // skill: mycroft.weather + "namespace": "weather.mycroft" "property": "forecast" //the key of the main data map this list in contained into "position": 2 "values": [{"date": "tomorrow", "temperature" : 13, ...}, ...] //values must always be in array form } ``` -Values is an ordered dict, for a shopping cart it would need multiple roles like product name, price, image - -## Updates item values starting at the given position, as many items as there are in the array +### Updates item values starting at the given position, as many items as there are in the array ```javascript { "type": "mycroft.session.list.update", - "namespace": "mycroft.system.active_skills" // skill: mycroft.weather - "property": "forecast" //in the future this can become a path if we want lists of lists + "namespace": "weather.mycroft" + "property": "forecast" "position": 2 "values": [{"date": "tomorrow", "temperature" : 13, ...}, ...] //values must always be in array form } ``` -## Move items within the list +### Move items within the list ```javascript { "type": "mycroft.session.list.move", - "namespace": "mycroft.system.active_skills" // skill: mycroft.weather + "namespace": "weather.mycroft" "property": "forecast" "from": 2 "to": 5 @@ -154,48 +199,16 @@ Values is an ordered dict, for a shopping cart it would need multiple roles like } ``` -## Remove items from the list +### Remove items from the list ```javascript { "type": "mycroft.session.list.remove", - "namespace": "mycroft.system.active_skills" // skill: mycroft.weather - "property": "forecast" //in the future this can become a path if we want lists of lists + "namespace": "weather.mycroft" + "property": "forecast" "position": 2 "items_number": 5 //optional in case we want to get rid a big chunk of list at once } ``` -# GUI MODEL -Each active skill is associated with a model with urls to the QML files of all gui items that are supposed to be visible. -## Inserts new GUI items at position -```javascript -{ - "type": "mycroft.gui.list.insert", - "namespace": "mycroft.weather" - "position": 2 - "values": [{"url": "file://..../currentWeather.qml"}, ...] //values must always be in array form -} -``` - -## Move items within the list -```javascript -{ - "type": "mycroft.gui.list.move", - "namespace": "mycroft.weather" - "from": 2 - "to": 5 - "items_number": 2 //optional in case we want to move a big chunk of list at once -} -``` - -## Remove items from the list -```javascript -{ - "type": "mycroft.gui.list.remove", - "namespace": "mycroft.weather" - "position": 2 - "items_number": 5 //optional in case we want to get rid a big chunk of list at once -} -``` From 3a808698185c3eaf2bf3565a0b74bed400c2da69 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 6 Oct 2023 17:04:50 +0000 Subject: [PATCH 12/24] Increment Version to 0.0.4a4 --- ovos_gui/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_gui/version.py b/ovos_gui/version.py index 8cb0779..a7d90f3 100644 --- a/ovos_gui/version.py +++ b/ovos_gui/version.py @@ -2,5 +2,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 4 -VERSION_ALPHA = 3 +VERSION_ALPHA = 4 # END_VERSION_BLOCK From 1675c314256b4631ca00753402073611202443c8 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 6 Oct 2023 17:05:19 +0000 Subject: [PATCH 13/24] Update Changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b76245..e12f7a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [0.0.4a4](https://github.com/OpenVoiceOS/ovos-gui/tree/0.0.4a4) (2023-10-06) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-gui/compare/0.0.4a3...0.0.4a4) + +**Merged pull requests:** + +- Update protocol.md [\#32](https://github.com/OpenVoiceOS/ovos-gui/pull/32) ([JarbasAl](https://github.com/JarbasAl)) + ## [0.0.4a3](https://github.com/OpenVoiceOS/ovos-gui/tree/0.0.4a3) (2023-09-29) [Full Changelog](https://github.com/OpenVoiceOS/ovos-gui/compare/0.0.4a2...0.0.4a3) From 4cf6a1107e2d8b9b0826f71650b26c0cb88012d1 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Fri, 29 Dec 2023 00:54:03 +0000 Subject: [PATCH 14/24] Update requirements.txt (#33) --- requirements.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 0a9675d..ce9c033 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -ovos-bus-client~=0.0, >=0.0.5 -ovos-utils~=0.0, >=0.0.34 -ovos-config~=0.0,>=0.0.10 +ovos-bus-client~=0.0, >=0.0.7 +ovos-utils~=0.0, >=0.0.37 +ovos-config~=0.0,>=0.0.12 tornado~=6.0, >=6.0.3 -ovos-plugin-manager>=0.0.23 +ovos-plugin-manager>=0.0.24 From 12cfb55c7c7053a6a008656fe659f2c4b29ccbef Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 29 Dec 2023 00:54:22 +0000 Subject: [PATCH 15/24] Increment Version to 0.0.4a5 --- ovos_gui/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_gui/version.py b/ovos_gui/version.py index a7d90f3..4883eff 100644 --- a/ovos_gui/version.py +++ b/ovos_gui/version.py @@ -2,5 +2,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 4 -VERSION_ALPHA = 4 +VERSION_ALPHA = 5 # END_VERSION_BLOCK From bc654c192a16f5da014ed9851d938b61f29f1ecf Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 29 Dec 2023 00:54:44 +0000 Subject: [PATCH 16/24] Update Changelog --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e12f7a1..b13e75f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [0.0.4a5](https://github.com/OpenVoiceOS/ovos-gui/tree/0.0.4a5) (2023-12-29) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-gui/compare/0.0.4a4...0.0.4a5) + +**Closed issues:** + +- "OSError: \[Errno 98\] Address already in use" when restarted too fast [\#24](https://github.com/OpenVoiceOS/ovos-gui/issues/24) + +**Merged pull requests:** + +- Update requirements.txt [\#33](https://github.com/OpenVoiceOS/ovos-gui/pull/33) ([JarbasAl](https://github.com/JarbasAl)) + ## [0.0.4a4](https://github.com/OpenVoiceOS/ovos-gui/tree/0.0.4a4) (2023-10-06) [Full Changelog](https://github.com/OpenVoiceOS/ovos-gui/compare/0.0.4a3...0.0.4a4) From 64d1f041fdc318e4fb2cd7c23444923971f9439d Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Sun, 28 Jan 2024 13:43:37 +0000 Subject: [PATCH 17/24] Create dependabot.yml --- .github/dependabot.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..9d866e3 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "pip" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" From 676ffd7edf8c7828840457355da709ba28e15b94 Mon Sep 17 00:00:00 2001 From: NeonJarbas <59943014+NeonJarbas@users.noreply.github.com> Date: Thu, 8 Feb 2024 14:18:37 +0000 Subject: [PATCH 18/24] house cleaning (#39) * fix/remove_pages * :tada: * tests * signal breaking change * restore mycroft.ready refactor in follow up PR * docstr * fix/back_button * fix/ready race condition now allows core/gui to restart and load independently of each other * fix homescreen on boot, it wouldnt show if ovos-gui loaded after ovos-core refactor classes * compat * fix tests * fix tests --------- Co-authored-by: JarbasAi --- ovos_gui/extensions.py | 14 +- ovos_gui/homescreen.py | 109 ++++------ ovos_gui/namespace.py | 321 +++++++++++++++++------------- ovos_gui/page.py | 2 - ovos_gui/service.py | 10 +- ovos_gui/version.py | 6 +- test/unittests/test_extensions.py | 16 +- test/unittests/test_homescreen.py | 4 +- test/unittests/test_namespace.py | 45 +++-- test/unittests/test_page.py | 1 - 10 files changed, 263 insertions(+), 265 deletions(-) diff --git a/ovos_gui/extensions.py b/ovos_gui/extensions.py index c8ebf1d..150d7fa 100644 --- a/ovos_gui/extensions.py +++ b/ovos_gui/extensions.py @@ -1,24 +1,23 @@ from ovos_bus_client import Message, MessageBusClient from ovos_config.config import Configuration -from ovos_gui.namespace import NamespaceManager from ovos_utils.log import LOG from ovos_plugin_manager.gui import OVOSGuiFactory +from ovos_gui.homescreen import HomescreenManager class ExtensionsManager: - def __init__(self, name: str, bus: MessageBusClient, gui: NamespaceManager): + def __init__(self, name: str, bus: MessageBusClient): """ Constructor for the Extension Manager. The Extension Manager is responsible for managing the extensions that define additional GUI behaviours for specific platforms. @param name: Name of the extension manager @param bus: MessageBus instance - @param gui: GUI instance """ self.name = name self.bus = bus - self.gui = gui + self.homescreen_manager = HomescreenManager(self.bus) core_config = Configuration() enclosure_config = core_config.get("gui") or {} self.active_extension = enclosure_config.get("extension", "generic") @@ -53,10 +52,11 @@ def activate_extension(self, extension_id: str): f"falling back to 'generic'") cfg["module"] = "generic" self.extension = OVOSGuiFactory.create(cfg, bus=self.bus) - self.extension.bind_homescreen() - LOG.info(f"Extensions Manager: Activated Extension {extension_id} " - f"({self.extension.__class__})") + self.extension.bind_homescreen(self.homescreen_manager) + + LOG.info(f"Extensions Manager - Activated: {extension_id} " + f"({self.extension.__class__.__name__})") self.bus.emit( Message("extension.manager.activated", {"id": extension_id})) diff --git a/ovos_gui/homescreen.py b/ovos_gui/homescreen.py index 6512530..247914c 100644 --- a/ovos_gui/homescreen.py +++ b/ovos_gui/homescreen.py @@ -1,35 +1,26 @@ +from threading import Thread from typing import List, Optional -from ovos_bus_client import Message, MessageBusClient -from ovos_bus_client.message import dig_for_message from ovos_config.config import Configuration, update_mycroft_config +from ovos_utils.log import LOG, log_deprecation -from ovos_utils.log import LOG, deprecated, log_deprecation - -from ovos_gui.namespace import NamespaceManager -from threading import Thread +from ovos_bus_client import Message, MessageBusClient +from ovos_bus_client.message import dig_for_message class HomescreenManager(Thread): - def __init__(self, bus: MessageBusClient, gui: NamespaceManager): + def __init__(self, bus: MessageBusClient): super().__init__() self.bus = bus - self.gui = gui self.homescreens: List[dict] = [] self.mycroft_ready = False - # TODO: If service starts after `mycroft_ready`, - # homescreen is never shown + self.bus.on('homescreen.manager.add', self.add_homescreen) self.bus.on('homescreen.manager.remove', self.remove_homescreen) self.bus.on('homescreen.manager.list', self.get_homescreens) - self.bus.on("homescreen.manager.get_active", - self.handle_get_active_homescreen) - self.bus.on("homescreen.manager.set_active", - self.handle_set_active_homescreen) - self.bus.on("homescreen.manager.disable_active", - self.disable_active_homescreen) - self.bus.on("mycroft.mark2.register_idle", - self.register_old_style_homescreen) + self.bus.on("homescreen.manager.get_active", self.handle_get_active_homescreen) + self.bus.on("homescreen.manager.set_active", self.handle_set_active_homescreen) + self.bus.on("homescreen.manager.disable_active", self.disable_active_homescreen) self.bus.on("homescreen.manager.show_active", self.show_homescreen) self.bus.on("mycroft.ready", self.set_mycroft_ready) @@ -43,10 +34,9 @@ def add_homescreen(self, message: Message): """ Handle `homescreen.manager.add` and add the requested homescreen if it has not yet been added. - @param message: Message containing homescreen id/class to add + @param message: Message containing homescreen id to add """ homescreen_id = message.data["id"] - homescreen_class = message.data["class"] if any((homescreen['id'] == homescreen_id for homescreen in self.homescreens)): @@ -55,7 +45,7 @@ def add_homescreen(self, message: Message): LOG.info(f"Homescreen Manager: Adding Homescreen {homescreen_id}") self.homescreens.append(message.data) - self.show_homescreen_on_add(homescreen_id, homescreen_class) + self.show_homescreen_on_add(homescreen_id) def remove_homescreen(self, message: Message): """ @@ -103,10 +93,14 @@ def get_active_homescreen(self) -> Optional[dict]: """ gui_config = Configuration().get("gui") or {} active_homescreen = gui_config.get("idle_display_skill") - LOG.debug(f"Homescreen Manager: Active Homescreen {active_homescreen}") + if not active_homescreen: + LOG.info("No homescreen enabled in mycroft.conf") + return + LOG.info(f"Active Homescreen: {active_homescreen}") for h in self.homescreens: if h["id"] == active_homescreen: return active_homescreen + LOG.error(f"{active_homescreen} not loaded!") def set_active_homescreen(self, homescreen_id: str): """ @@ -126,35 +120,24 @@ def reload_homescreens_list(self): Emit a request for homescreens to register via the Messagebus """ LOG.info("Homescreen Manager: Reloading Homescreen List") - self.collect_old_style_homescreens() self.bus.emit(Message("homescreen.manager.reload.list")) - def show_homescreen_on_add(self, homescreen_id: str, homescreen_class: str): + def show_homescreen_on_add(self, homescreen_id: str): """ Check if a homescreen should be displayed immediately upon addition @param homescreen_id: ID of added homescreen - @param homescreen_class: "class" (IdleDisplaySkill, MycroftSkill) - of homescreen """ if not self.mycroft_ready: - LOG.debug("Not ready yet, don't display") + LOG.debug("Not ready yet, don't display homescreen") return LOG.debug(f"Checking {homescreen_id}") if self.get_active_homescreen() != homescreen_id: # Added homescreen isn't the configured one, do nothing return - if homescreen_class == "IdleDisplaySkill": - LOG.debug(f"Displaying Homescreen {homescreen_id}") - self.bus.emit(Message("homescreen.manager.activate.display", - {"homescreen_id": homescreen_id})) - elif homescreen_class == "MycroftSkill": - log_deprecation(f"Homescreen skills should register listeners for " - f"`homescreen.manager.activate.display`. " - f"`{homescreen_id}.idle` messages will be removed.", - "0.1.0") - LOG.debug(f"Displaying Homescreen {homescreen_id}") - self.bus.emit(Message(f"{homescreen_id}.idle")) + LOG.info(f"Displaying Homescreen {homescreen_id}") + self.bus.emit(Message("homescreen.manager.activate.display", + {"homescreen_id": homescreen_id})) def disable_active_homescreen(self, message: Message): """ @@ -162,7 +145,6 @@ def disable_active_homescreen(self, message: Message): `idle_display_skill` as None. @param message: Message requesting homescreen disable """ - # TODO: Is this valid behavior? if Configuration().get("gui", {}).get("idle_display_skill"): LOG.info(f"Disabling idle_display_skill!") new_config = {"gui": {"idle_display_skill": None}} @@ -174,24 +156,22 @@ def show_homescreen(self, message: Optional[Message] = None): @param message: Optional `homescreen.manager.show_active` Message """ active_homescreen = self.get_active_homescreen() - LOG.debug(f"Requesting activation of {active_homescreen}") + if not active_homescreen: + LOG.info("No active homescreen to display") + return + LOG.info(f"Requesting activation of {active_homescreen}") for h in self.homescreens: if h.get("id") == active_homescreen: LOG.debug(f"matched homescreen skill: {h}") message = message or dig_for_message() or Message("") - if h["class"] == "IdleDisplaySkill": - LOG.debug(f"Displaying Homescreen {active_homescreen}") - self.bus.emit(message.forward( - "homescreen.manager.activate.display", - {"homescreen_id": active_homescreen})) - elif h["class"] == "MycroftSkill": - LOG.debug(f"Displaying Homescreen {active_homescreen}") - self.bus.emit(message.forward(f"{active_homescreen}.idle")) - else: - LOG.error(f"Requested homescreen has an invalid class: {h}") - return - LOG.warning(f"Requested {active_homescreen} not found in: " - f"{self.homescreens}") + LOG.debug(f"Displaying Homescreen {active_homescreen}") + self.bus.emit(message.forward( + "homescreen.manager.activate.display", + {"homescreen_id": active_homescreen})) + break + else: + LOG.warning(f"Requested {active_homescreen} not found in: " + f"{self.homescreens}") def set_mycroft_ready(self, message: Message): """ @@ -199,26 +179,5 @@ def set_mycroft_ready(self, message: Message): @param message: `mycroft.ready` Message """ self.mycroft_ready = True + self.reload_homescreens_list() self.show_homescreen() - - # Add compabitility with older versions of the Resting Screen Class - - def collect_old_style_homescreens(self): - """Trigger collection of older resting screens.""" - # TODO: Deprecate in 0.1.0 - self.bus.emit(Message("mycroft.mark2.collect_idle")) - - @deprecated("`mycroft.mark2.collect_idle` responses are deprecated", - "0.1.0") - def register_old_style_homescreen(self, message): - if "name" in message.data and "id" in message.data: - super_class_name = "MycroftSkill" - super_class_object = message.data["name"] - skill_id = message.data["id"] - _homescreen_entry = {"class": super_class_name, - "name": super_class_object, "id": skill_id} - LOG.debug(f"Homescreen Manager: Adding OLD Homescreen {skill_id}") - self.add_homescreen( - Message("homescreen.manager.add", _homescreen_entry)) - else: - LOG.error("Malformed idle screen registration received") diff --git a/ovos_gui/namespace.py b/ovos_gui/namespace.py index 3cde48f..b06cc10 100644 --- a/ovos_gui/namespace.py +++ b/ovos_gui/namespace.py @@ -42,23 +42,21 @@ import shutil from os import makedirs from os.path import join, dirname, isfile, exists -from threading import Event -from threading import Lock, Timer -from time import sleep +from threading import Event, Lock, Timer from typing import List, Union, Optional, Dict -from ovos_bus_client import Message, MessageBusClient from ovos_config.config import Configuration from ovos_utils.log import LOG, log_deprecation +from ovos_bus_client import Message, MessageBusClient from ovos_gui.bus import ( create_gui_service, determine_if_gui_connected, get_gui_websocket_config, send_message_to_gui, GUIWebsocketHandler ) -from ovos_gui.page import GuiPage from ovos_gui.gui_file_server import start_gui_http_server +from ovos_gui.page import GuiPage namespace_lock = Lock() @@ -84,7 +82,7 @@ def _validate_page_message(message: Message) -> bool: else: action = "removed" LOG.error(f"Page will not be {action} due to malformed data in the" - f"{message.msg_type} message") + f" {message.msg_type} message") return valid @@ -96,7 +94,7 @@ def _get_idle_display_config() -> str: config = Configuration() enclosure_config = config.get("gui") or {} idle_display_skill = enclosure_config.get("idle_display_skill") - LOG.info(f"Got idle_display_skill from config: {idle_display_skill}") + LOG.info(f"Configured homescreen: {idle_display_skill}") return idle_display_skill @@ -108,7 +106,7 @@ def _get_active_gui_extension() -> str: config = Configuration() enclosure_config = config.get("gui") or {} gui_extension = enclosure_config.get("extension", "generic") - LOG.info(f"Got extension from config: {gui_extension}") + LOG.info(f"Configured GUI extension: {gui_extension}") return gui_extension.lower() @@ -130,6 +128,7 @@ class Namespace: displayed at the same time data: a key/value pair representing the data used to populate the GUI """ + def __init__(self, skill_id: str): self.skill_id = skill_id self.persistent = False @@ -139,11 +138,23 @@ def __init__(self, skill_id: str): self.page_number = 0 self.session_set = False + @property + def page_names(self): + return [page.name for page in self.pages] + + @property + def active_page(self): + if len(self.pages): + if self.page_number >= len(self.pages): + return None # TODO - error ? + return self.pages[self.page_number] + return None + def add(self): """ Adds this namespace to the list of active namespaces. """ - LOG.info(f"Adding \"{self.skill_id}\" to active GUI namespaces") + LOG.info(f"GUI PROTOCOL - Adding \"{self.skill_id}\" to active namespaces") message = dict( type="mycroft.session.list.insert", namespace="mycroft.system.active_skills", @@ -157,7 +168,11 @@ def activate(self, position: int): Activate this namespace if its already in the list of active namespaces. @param position: position to move this namespace FROM """ - LOG.info(f"Activating GUI namespace \"{self.skill_id}\"") + if not len(self.pages): + LOG.error(f"Tried to activate namespace without loaded pages: \"{self.skill_id}\"") + return + + LOG.info(f"GUI PROTOCOL - Activating namespace \"{self.skill_id}\"") message = { "type": "mycroft.session.list.move", "namespace": "mycroft.system.active_skills", @@ -173,8 +188,7 @@ def remove(self, position: int): any session data. @param position: position to remove this namespace FROM """ - LOG.info(f"Removing {self.skill_id} from active GUI namespaces") - + LOG.info(f"GUI PROTOCOL - Removing \"{self.skill_id}\" from active namespaces") # unload the data first before removing the namespace # use the keys of the data to unload the data for key in self.data: @@ -199,13 +213,12 @@ def load_data(self, name: str, value: str): name: The name of the attribute value: The attribute's value """ + LOG.info(f"GUI PROTOCOL - Sending \"{self.skill_id}\" data -- {name} : {value} ") message = dict( type="mycroft.session.set", namespace=self.skill_id, data={name: value} ) - - # LOG.info(f"Setting data {message} in GUI namespace {self.skill_id}") send_message_to_gui(message) def unload_data(self, name: str): @@ -213,12 +226,12 @@ def unload_data(self, name: str): Delete data from the namespace @param name: name of property to delete """ + LOG.info(f"GUI PROTOCOL - Deleting namespace \"{self.skill_id}\" key: {name}") message = dict( type="mycroft.session.delete", property=name, namespace=self.skill_id ) - # LOG.info(f"Deleting data {message} from GUI namespace {self.skill_id}") send_message_to_gui(message) def get_position_of_last_item_in_data(self) -> int: @@ -242,8 +255,7 @@ def set_persistence(self, skill_type: str): else: # get the active page in the namespace - active_page = self.get_active_page() - + active_page = self.active_page # if type(persistence) == int: # Get the duration of the active page if it is not persistent if active_page is not None and not active_page.persistent: @@ -258,6 +270,7 @@ def set_persistence(self, skill_type: str): # else use the default duration of 30 seconds else: + LOG.warning(f"No active page, reset persistence for {self.skill_id}") self.persistent = False self.duration = 30 @@ -273,10 +286,14 @@ def load_pages(self, pages: List[GuiPage], show_index: int = 0): @param pages: list of pages to be displayed @param show_index: index of page to display (default 0) """ + if not pages: + LOG.error("No pages to load ?") + return if show_index is None: LOG.warning(f"Expected int show_index but got `None`. Default to 0") show_index = 0 new_pages = list() + target_page = pages[show_index] for page in pages: if page.id not in [p.id for p in self.pages]: @@ -285,15 +302,19 @@ def load_pages(self, pages: List[GuiPage], show_index: int = 0): self.pages.extend(new_pages) if new_pages: self._add_pages(new_pages) - - self._activate_page(pages[show_index]) + if show_index >= len(pages): + LOG.error( + f"Invalid page index requested: {show_index} , only {len(pages)} pages available for \"{self.skill_id}\"") + else: + LOG.info(f"Activating page {show_index} from: {[p.name for p in pages]} for \"{self.skill_id}\"") + self._activate_page(target_page) def _add_pages(self, new_pages: List[GuiPage]): """ Adds one or more pages to the active page list. @param new_pages: pages to add to the active page list """ - LOG.debug(f"namespace {self.skill_id} current pages: {self.pages}") + LOG.debug(f"namespace \"{self.skill_id}\" current pages: {self.pages}") LOG.debug(f"new_pages={new_pages}") # Find position of new page in self.pages @@ -305,41 +326,49 @@ def _add_pages(self, new_pages: List[GuiPage]): except Exception as e: LOG.exception(f"Error updating {client.framework} client: {e}") - def _activate_page(self, page: GuiPage): + def focus_page(self, page): """ Returns focus to a page already in the active page list. @param page: the page that will gain focus """ - LOG.info(f"Activating page {page.name} in GUI namespace {self.skill_id}") - LOG.debug(f"Current pages from _activate_page: {self.pages}") - # TODO: Simplify two loops into one (with unit test) - # get the index of the page in the self.pages list - page_index = 0 + # set the index of the page in the self.pages list + page_index = None for i, p in enumerate(self.pages): if p.id == page.id: + # save page index page_index = i break - self.page_number = page_index + # handle missing page (TODO, can this happen?) + if page_index is None: + LOG.warning("tried to activate page missing from pages list, inserting it at index 0") + page_index = 0 + self.pages.insert(0, page) + # update page data + else: + self.pages[page_index] = page - # set the page active attribute to True and update the self.pages list, - # mark all other pages as inactive - page.active = True + if page_index != self.page_number: + self.page_number = page_index + LOG.info(f"Focusing page {page.name} -- namespace \"{self.skill_id}\"") - for p in self.pages: - if p != page: - p.active = False - # update the self.pages list with the page active status changes - self.pages[self.pages.index(p)] = p + def _activate_page(self, page: GuiPage): + """ + Tells mycroft-gui to returns focus to a page - self.pages[page_index] = page + @param page: the page that will gain focus + """ + LOG.debug(f"Current pages from _activate_page: {self.pages}") + self.focus_page(page) + LOG.info( + f"GUI PROTOCOL - Sending event 'page_gained_focus' -- page: {page.name} -- namespace: \"{self.skill_id}\"") message = dict( type="mycroft.events.triggered", namespace=self.skill_id, event_name="page_gained_focus", - data=dict(number=page_index) + data={"number": self.page_number} ) send_message_to_gui(message) @@ -349,11 +378,10 @@ def remove_pages(self, positions: List[int]): @param positions: list of int page positions to remove """ - LOG.info(f"Removing pages from GUI namespace {self.skill_id}: {positions}") positions.sort(reverse=True) for position in positions: page = self.pages.pop(position) - LOG.info(f"Deleting {page} from GUI namespace {self.skill_id}") + LOG.info(f"GUI PROTOCOL - Deleting {page.name} -- namespace: \"{self.skill_id}\"") message = dict( type="mycroft.gui.list.remove", namespace=self.skill_id, @@ -367,68 +395,15 @@ def page_gained_focus(self, page_number: int): Updates the active page in `self.pages`. @param page_number: the index of the page that will gain focus """ - LOG.info( - f"Page {page_number} gained focus in GUI namespace {self.skill_id}") - self._activate_page(self.pages[page_number]) - - def page_update_interaction(self, page_number: int): - """ - Update the interaction of the page_number. - @param page_number: the index of the page to update - """ - - LOG.info(f"Page {page_number} update interaction in GUI namespace " - f"{self.skill_id}") - - page = self.pages[page_number] - if not page.persistent and page.duration > 0: - page.duration = page.duration / 2 - - # update the self.pages list with the page interaction status changes - self.pages[page_number] = page - self.set_persistence(skill_type="genericSkill") - - def get_page_at_position(self, position: int) -> GuiPage: - """ - Returns the page at the requested position in the active page list. - Requesting a position out of range will raise an IndexError. - @param position: index of the page to get - """ - return self.pages[position] - - def get_active_page(self) -> Optional[GuiPage]: - """ - Returns the currently active page from `self.pages` where the page - attribute `active` is true. - @returns: Active GuiPage if any, else None - """ - for page in self.pages: - if page.active: - return page - return None - - def get_active_page_index(self) -> Optional[int]: - """ - Get the active page index in `self.pages`. - @return: index of the active page if any, else None - """ - active_page = self.get_active_page() - if active_page is not None: - return self.pages.index(active_page) - - def index_in_pages_list(self, index: int) -> bool: - """ - Check if the active index is in the pages list - @param index: index to check - @return: True if index is valid in `self.pages - """ - return 0 < index < len(self.pages) + LOG.info(f"Page {page_number} gained focus -- namespace \"{self.skill_id}\"") + self.page_number = page_number + self._activate_page(self.active_page) def global_back(self): """ Returns to the previous page in the active page list. """ - if self.page_number > 0: + if self.page_number > 0: # go back 1 page self.remove_pages([self.page_number]) self.page_gained_focus(self.page_number - 1) @@ -485,7 +460,7 @@ def _init_gui_file_share(self): "Files will always be saved to " "XDG_CACHE_HOME/ovos_gui_file_server", "0.1.0") self.gui_file_path = config.get("server_path") or \ - join(xdg_cache_home(), "ovos_gui_file_server") + join(xdg_cache_home(), "ovos_gui_file_server") if config.get("gui_file_server"): self.gui_file_server = start_gui_http_server(self.gui_file_path) self._upload_system_resources() @@ -497,6 +472,7 @@ def _define_message_handlers(self): self.core_bus.on("gui.clear.namespace", self.handle_clear_namespace) self.core_bus.on("gui.event.send", self.handle_send_event) self.core_bus.on("gui.page.delete", self.handle_delete_page) + self.core_bus.on("gui.page.delete.all", self.handle_delete_all_pages) self.core_bus.on("gui.page.show", self.handle_show_page) self.core_bus.on("gui.page.upload", self.handle_receive_gui_pages) self.core_bus.on("gui.status.request", self.handle_status_request) @@ -505,6 +481,7 @@ def _define_message_handlers(self): self.core_bus.on("gui.page_interaction", self.handle_page_interaction) self.core_bus.on("gui.page_gained_focus", self.handle_page_gained_focus) self.core_bus.on("mycroft.skills.trained", self.handle_ready) + self.core_bus.on("mycroft.gui.screen.close", self.handle_namespace_global_back) def handle_ready(self, message): self._ready_event.set() @@ -569,8 +546,7 @@ def handle_receive_gui_pages(self, message: Message): LOG.exception(f"Failed to write {page}: {e}") if message.data["__from"] == self._active_homescreen: # Configured home screen skill just uploaded pages, show it again - self.core_bus.emit( - message.forward("homescreen.manager.show_active")) + self.core_bus.emit(message.forward("homescreen.manager.show_active")) def handle_clear_namespace(self, message: Message): """ @@ -584,8 +560,9 @@ def handle_clear_namespace(self, message: Message): "Request to delete namespace failed: no namespace specified" ) else: - with namespace_lock: - self._remove_namespace(namespace_name) + if self.loaded_namespaces.get(namespace_name): + with namespace_lock: + self._remove_namespace(namespace_name) @staticmethod def handle_send_event(message: Message): @@ -595,16 +572,38 @@ def handle_send_event(message: Message): message bus. """ try: + skill_id = message.data.get('__from') + event = message.data.get('event_name') + LOG.info(f"GUI PROTOCOL - Sending event '{event}' for namespace: {skill_id}") message = dict( type='mycroft.events.triggered', - namespace=message.data.get('__from'), - event_name=message.data.get('event_name'), + namespace=skill_id, + event_name=event, data=message.data.get('params') ) send_message_to_gui(message) except Exception: LOG.exception('Could not send event trigger') + def handle_delete_all_pages(self, message: Message): + """ + Handles request to remove all current pages from a namespace. + @param message: the message requesting page removal + """ + namespace_name = message.data["__from"] + except_pages = message.data.get("except") or [] + + if except_pages: + LOG.info(f"Got {namespace_name} request to delete all pages except: {except_pages}") + else: + LOG.info(f"Got {namespace_name} request to delete all pages") + + with namespace_lock: + namespace = self.loaded_namespaces.get(namespace_name) + if namespace: + to_rm = [p.name for p in namespace.pages if p.name not in except_pages] + self._remove_pages(namespace_name, to_rm) + def handle_delete_page(self, message: Message): """ Handles request to remove one or more pages from a namespace. @@ -613,7 +612,9 @@ def handle_delete_page(self, message: Message): message_is_valid = _validate_page_message(message) if message_is_valid: namespace_name = message.data["__from"] - pages_to_remove = message.data["page"] + pages_to_remove = message.data.get("page_names") or \ + message.data.get("page") # backwards compat + LOG.debug(f"Got {namespace_name} request to delete: {pages_to_remove}") with namespace_lock: self._remove_pages(namespace_name, pages_to_remove) @@ -627,12 +628,13 @@ def _remove_pages(self, namespace_name: str, pages_to_remove: List[str]): namespace = self.loaded_namespaces.get(namespace_name) if namespace is not None and namespace in self.active_namespaces: page_positions = [] - for index, page in enumerate(pages_to_remove): - if page == namespace.pages[index].id: + for index, page in enumerate(namespace.pages): + if page.name in pages_to_remove: page_positions.append(index) - page_positions.sort(reverse=True) - namespace.remove_pages(page_positions) + if page_positions: + page_positions.sort(reverse=True) + namespace.remove_pages(page_positions) @staticmethod def _parse_persistence(persistence: Optional[Union[int, bool]]) -> \ @@ -649,7 +651,7 @@ def _parse_persistence(persistence: Optional[Union[int, bool]]) -> \ elif isinstance(persistence, int): if persistence < 0: raise ValueError("Requested negative persistence") - return False, persistence + return False, persistence else: # Defines default behavior as displaying for 30 seconds return False, 30 @@ -688,12 +690,14 @@ def handle_show_page(self, message: Message): persistence = message.data["__idle"] show_index = message.data.get("index", None) + LOG.debug(f"Got {namespace_name} request to show: {page_ids_to_show} at index: {show_index}") + if not page_resource_dirs and page_ids_to_show and \ all((x.startswith("SYSTEM") for x in page_ids_to_show)): page_resource_dirs = {"all": self._system_res_dir} if not all((page_ids_to_show, page_resource_dirs)): - LOG.info(f"Handling legacy page request: data={message.data}") + LOG.warning(f"GUI resources have not yet been uploaded for namespace: {namespace_name}") pages = self._legacy_show_page(message) else: pages = list() @@ -712,8 +716,19 @@ def handle_show_page(self, message: Message): pages.append(GuiPage(url, name, persist, duration, page, namespace_name, page_resource_dirs)) + if not pages: + LOG.error(f"Activated namespace '{namespace_name}' has no pages! " + f"Did you provide 'ui_directories' ?") + LOG.error(f"Can't show page, bad message: {message.data}") + return + with namespace_lock: - self._activate_namespace(namespace_name) + if not self.active_namespaces: + self._activate_namespace(namespace_name) + else: + active_namespace = self.active_namespaces[0] + if active_namespace.skill_id != namespace_name: + self._activate_namespace(namespace_name) self._load_pages(pages, show_index) self._update_namespace_persistence(persistence) @@ -724,19 +739,22 @@ def _activate_namespace(self, namespace_name: str): @param namespace_name: the name of the namespace to load """ namespace = self._ensure_namespace_exists(namespace_name) - LOG.debug(f"Activating namespace: {namespace_name}") if namespace in self.active_namespaces: namespace_position = self.active_namespaces.index(namespace) namespace.activate(namespace_position) - self.active_namespaces.insert( - 0, self.active_namespaces.pop(namespace_position) - ) + if namespace_position != 0: + LOG.info(f"Activating namespace: {namespace_name}") + self.active_namespaces.insert( + 0, self.active_namespaces.pop(namespace_position) + ) else: + LOG.info(f"New namespace: {namespace_name}") namespace.add() self.active_namespaces.insert(0, namespace) - for key, value in namespace.data.items(): - namespace.load_data(key, value) + # sync initial state + for key, value in namespace.data.items(): + namespace.load_data(key, value) self._emit_namespace_displayed_event() @@ -760,8 +778,18 @@ def _load_pages(self, pages_to_show: List[GuiPage], show_index: int): @param pages_to_show: list of pages to be loaded @param show_index: index to load pages at """ + if not len(pages_to_show) or show_index >= len(pages_to_show): + LOG.error(f"requested invalid page index: {show_index}, defaulting to last page") + show_index = len(pages_to_show) - 1 + active_namespace = self.active_namespaces[0] + oldp = [p.name for p in active_namespace.pages] active_namespace.load_pages(pages_to_show, show_index) + # LOG only on change + if oldp != [p.name for p in active_namespace.pages]: + pn = active_namespace.page_number + LOG.info(f"Loaded {active_namespace.skill_id} at index: {pn} " + f"pages: {[p.name for p in active_namespace.pages]}") def _update_namespace_persistence(self, persistence: Union[bool, int]): """ @@ -774,17 +802,19 @@ def _update_namespace_persistence(self, persistence: Union[bool, int]): the skill is showing the pages. @param persistence: length of time the namespace should be displayed """ - LOG.debug(f"Setting namespace persistence to {persistence}") - for position, namespace in enumerate(self.active_namespaces): - if position: + for idx, namespace in enumerate(self.active_namespaces): + if idx: if not namespace.persistent: self._remove_namespace(namespace.skill_id) else: + if namespace.persistent != persistence: + LOG.info(f"Setting namespace '{namespace.skill_id}' persistence to: {persistence}") + namespace.persistent = persistence + if namespace.skill_id == self.idle_display_skill: namespace.set_persistence(skill_type="idleDisplaySkill") else: namespace.set_persistence(skill_type="genericSkill") - # check if there is a scheduled remove_namespace_timer # and cancel it if namespace.persistent and namespace.skill_id in \ @@ -793,9 +823,10 @@ def _update_namespace_persistence(self, persistence: Union[bool, int]): self._del_namespace_in_remove_timers(namespace.skill_id) if not namespace.persistent: - LOG.info("It is being scheduled here") self._schedule_namespace_removal(namespace) + self.active_namespaces[idx] = namespace + def _schedule_namespace_removal(self, namespace: Namespace): """ Uses a timer thread to remove the namespace. @@ -810,8 +841,8 @@ def _schedule_namespace_removal(self, namespace: Namespace): self._remove_namespace_via_timer, args=(namespace.skill_id,) ) - LOG.debug(f"Scheduled removal of namespace {namespace.skill_id} in " - f"duration {namespace.duration}") + LOG.info(f"Removal of namespace {namespace.skill_id} in " + f"{namespace.duration} seconds") remove_namespace_timer.start() self.remove_namespace_timers[namespace.skill_id] = remove_namespace_timer @@ -828,8 +859,6 @@ def _remove_namespace(self, namespace_name: str): Removes a namespace from the active namespace stack. @param namespace_name: name of namespace to remove """ - LOG.debug(f"Removing namespace {namespace_name}") - # Remove all timers associated with the namespace if namespace_name in self.remove_namespace_timers: self.remove_namespace_timers[namespace_name].cancel() @@ -837,12 +866,9 @@ def _remove_namespace(self, namespace_name: str): namespace: Namespace = self.loaded_namespaces.get(namespace_name) if namespace is not None and namespace in self.active_namespaces: + LOG.info(f"Removing namespace {namespace_name}") self.core_bus.emit(Message("gui.namespace.removed", data={"skill_id": namespace.skill_id})) - if self.active_extension == "Bigscreen": - # TODO: Define callback or event instead of arbitrary sleep - # wait for window management in bigscreen extension to finish - sleep(1) namespace_position = self.active_namespaces.index(namespace) namespace.remove(namespace_position) self.active_namespaces.remove(namespace) @@ -856,6 +882,7 @@ def _emit_namespace_displayed_event(self): if self.active_namespaces: displaying_namespace = self.active_namespaces[0] message_data = dict(skill_id=displaying_namespace.skill_id) + # TODO - no known listeners ? self.core_bus.emit( Message("gui.namespace.displayed", data=message_data) ) @@ -896,8 +923,6 @@ def _update_namespace_data(self, namespace_name: str, data: dict): namespace = self._ensure_namespace_exists(namespace_name) for key, value in data.items(): if key not in RESERVED_KEYS and namespace.data.get(key) != value: - LOG.debug( - f"Setting {key} to {value} in namespace {namespace.skill_id}") namespace.data[key] = value if namespace in self.active_namespaces: namespace.load_data(key, value) @@ -948,16 +973,21 @@ def handle_page_interaction(self, message: Message): # GUI has interacted with a page # Update and increase the namespace duration and reset the remove timer namespace_name = message.data.get("skill_id") - LOG.debug(f"GUI interacted with page in namespace {namespace_name}") - if namespace_name == self.idle_display_skill: - return - else: - namespace = self.loaded_namespaces.get(namespace_name) - if not namespace.persistent: - if self.remove_namespace_timers[namespace.skill_id]: - self.remove_namespace_timers[namespace.skill_id].cancel() - self._del_namespace_in_remove_timers(namespace.skill_id) - self._schedule_namespace_removal(namespace) + pidx = message.data.get('page_number') + LOG.info(f"GUI interacted with page in namespace {namespace_name}") + namespace = self.loaded_namespaces.get(namespace_name) + + if namespace and pidx is not None and pidx != namespace.page_number: + # update focused page + namespace.page_gained_focus(pidx) + + # reschedule namespace timeout + if namespace_name != self.idle_display_skill and \ + not namespace.persistent and \ + self.remove_namespace_timers[namespace.skill_id]: + self.remove_namespace_timers[namespace.skill_id].cancel() + self._del_namespace_in_remove_timers(namespace.skill_id) + self._schedule_namespace_removal(namespace) def handle_page_gained_focus(self, message: Message): """ @@ -984,7 +1014,12 @@ def handle_namespace_global_back(self, message: Optional[Message]): namespace_name = self.active_namespaces[0].skill_id namespace = self.loaded_namespaces.get(namespace_name) if namespace in self.active_namespaces: - namespace.global_back() + # prev page + if namespace.page_number > 0: + namespace.global_back() + # homescreen + else: + self.core_bus.emit(Message("homescreen.manager.show_active")) def _del_namespace_in_remove_timers(self, namespace_name: str): """ diff --git a/ovos_gui/page.py b/ovos_gui/page.py index 641f867..b6eeef7 100644 --- a/ovos_gui/page.py +++ b/ovos_gui/page.py @@ -26,8 +26,6 @@ class GuiPage: namespace: Optional[str] = None resource_dirs: Optional[dict] = None - active: bool = False - @property def id(self): """ diff --git a/ovos_gui/service.py b/ovos_gui/service.py index c30e1e3..ac34451 100644 --- a/ovos_gui/service.py +++ b/ovos_gui/service.py @@ -32,7 +32,7 @@ def __init__(self, alive_hook=on_alive, started_hook=on_started, stopping_hook=on_stopping): self.bus = MessageBusClient() self.extension_manager = None - self.gui = NamespaceManager(self.bus) + self.namespace_manager = None callbacks = StatusCallbackMap(on_started=started_hook, on_alive=alive_hook, on_ready=ready_hook, @@ -61,16 +61,18 @@ def run(self): self.status.set_alive() self._init_bus_client() + self.extension_manager = ExtensionsManager("EXTENSION_SERVICE", self.bus) + self.namespace_manager = NamespaceManager(self.bus) + # Bus is connected, check if the skills service is ready resp = self.bus.wait_for_response( Message("mycroft.skills.is_ready", context={"source": "gui", "destination": ["skills"]})) if resp and resp.data.get("status"): LOG.debug("Skills service already running") - self.gui.handle_ready(resp) + self.namespace_manager.handle_ready(resp) + self.extension_manager.homescreen_manager.set_mycroft_ready(resp) - self.extension_manager = ExtensionsManager("EXTENSION_SERVICE", - self.bus, self.gui) self.status.set_ready() LOG.info(f"GUI Service Ready") diff --git a/ovos_gui/version.py b/ovos_gui/version.py index 4883eff..be3eeb2 100644 --- a/ovos_gui/version.py +++ b/ovos_gui/version.py @@ -1,6 +1,6 @@ # START_VERSION_BLOCK VERSION_MAJOR = 0 -VERSION_MINOR = 0 -VERSION_BUILD = 4 -VERSION_ALPHA = 5 +VERSION_MINOR = 1 +VERSION_BUILD = 0 +VERSION_ALPHA = 1 # END_VERSION_BLOCK diff --git a/test/unittests/test_extensions.py b/test/unittests/test_extensions.py index 907ac65..ea2ba9a 100644 --- a/test/unittests/test_extensions.py +++ b/test/unittests/test_extensions.py @@ -3,6 +3,8 @@ import ovos_gui.extensions from ovos_utils.messagebus import FakeBus +from ovos_gui.homescreen import HomescreenManager +from ovos_gui.extensions import ExtensionsManager from .mocks import base_config PATCH_MODULE = "ovos_gui.extensions" @@ -24,23 +26,17 @@ class TestExtensionManager(unittest.TestCase): name = "TestManager" @classmethod - @patch("ovos_gui.namespace.create_gui_service") - def setUpClass(cls, create_gui) -> None: - from ovos_gui.extensions import ExtensionsManager - from ovos_gui.namespace import NamespaceManager + def setUpClass(cls) -> None: ovos_gui.extensions.Configuration = Mock(return_value=_MOCK_CONFIG) - cls.extension_manager = ExtensionsManager(cls.name, cls.bus, - NamespaceManager(cls.bus)) - create_gui.assert_called_once_with(cls.extension_manager.gui) + cls.extension_manager = ExtensionsManager(cls.name, cls.bus) def test_00_extensions_manager_init(self): - from ovos_gui.namespace import NamespaceManager self.assertEqual(self.extension_manager.name, self.name) self.assertEqual(self.extension_manager.bus, self.bus) - self.assertIsInstance(self.extension_manager.gui, NamespaceManager) - self.assertEqual(self.extension_manager.gui.core_bus, self.bus) + self.assertIsInstance(self.extension_manager.homescreen_manager, HomescreenManager) + self.assertEqual(self.extension_manager.homescreen_manager.bus, self.bus) self.assertIsInstance(self.extension_manager.active_extension, str) @patch("ovos_gui.extensions.OVOSGuiFactory.create") diff --git a/test/unittests/test_homescreen.py b/test/unittests/test_homescreen.py index 5660260..7752844 100644 --- a/test/unittests/test_homescreen.py +++ b/test/unittests/test_homescreen.py @@ -9,12 +9,10 @@ class TestHomescreenManager(unittest.TestCase): from ovos_gui.homescreen import HomescreenManager bus = FakeBus() - gui = NamespaceManager(bus) - homescreen_manager = HomescreenManager(bus, gui) + homescreen_manager = HomescreenManager(bus) def test_00_homescreen_manager_init(self): self.assertEqual(self.homescreen_manager.bus, self.bus) - self.assertEqual(self.homescreen_manager.gui, self.gui) self.assertFalse(self.homescreen_manager.mycroft_ready) self.assertIsInstance(self.homescreen_manager.homescreens, list) # TODO: Test messagebus handlers diff --git a/test/unittests/test_namespace.py b/test/unittests/test_namespace.py index 44e5055..a4a33a4 100644 --- a/test/unittests/test_namespace.py +++ b/test/unittests/test_namespace.py @@ -14,29 +14,31 @@ # """Tests for the GUI namespace helper class.""" from os import makedirs +from os.path import join, dirname, isdir, isfile from shutil import rmtree from unittest import TestCase, mock from unittest.mock import Mock -from os.path import join, dirname, isdir, isfile + from ovos_bus_client.message import Message from ovos_utils.messagebus import FakeBus -from ovos_gui.page import GuiPage + from ovos_gui.namespace import Namespace +from ovos_gui.page import GuiPage PATCH_MODULE = "ovos_gui.namespace" class TestNamespaceFunctions(TestCase): def test_validate_page_message(self): - from ovos_gui.namespace import _validate_page_message + pass # TODO def test_get_idle_display_config(self): - from ovos_gui.namespace import _get_idle_display_config + pass # TODO def test_get_active_gui_extension(self): - from ovos_gui.namespace import _get_active_gui_extension + pass # TODO @@ -61,6 +63,13 @@ def test_add(self): send_message_mock.assert_called_with(add_namespace_message) def test_activate(self): + self.namespace.load_pages([ + GuiPage(name="foo", url="", persistent=False, duration=False), + GuiPage(name="bar", url="", persistent=False, duration=False), + GuiPage(name="foobar", url="", persistent=False, duration=False), + GuiPage(name="baz", url="", persistent=False, duration=False), + GuiPage(name="foobaz", url="", persistent=False, duration=False) + ]) activate_namespace_message = { "type": "mycroft.session.list.move", "namespace": "mycroft.system.active_skills", @@ -120,9 +129,9 @@ def test_set_persistence_boolean(self): self.assertTrue(self.namespace.persistent) def test_load_pages_new(self): - self.namespace.pages = [GuiPage("foo", "foo.qml", True, 0), - GuiPage("bar", "bar.qml", False, 30)] - new_pages = [GuiPage("foobar", "foobar.qml", False, 30)] + self.namespace.pages = [GuiPage(name="foo", url="foo.qml", persistent=True, duration=0), + GuiPage(name="bar", url="bar.qml", persistent=False, duration=30)] + new_pages = [GuiPage(name="foobar", url="foobar.qml", persistent=False, duration=30)] load_page_message = dict( type="mycroft.events.triggered", namespace="foo", @@ -137,9 +146,9 @@ def test_load_pages_new(self): self.assertListEqual(self.namespace.pages, self.namespace.pages) def test_load_pages_existing(self): - self.namespace.pages = [GuiPage("foo", "foo.qml", True, 0), - GuiPage("bar", "bar.qml", False, 30)] - new_pages = [GuiPage("foo", "foo.qml", True, 0)] + self.namespace.pages = [GuiPage(name="foo", url="foo.qml", persistent=True, duration=0), + GuiPage(name="bar", url="bar.qml", persistent=False, duration=30)] + new_pages = [GuiPage(name="foo", url="foo.qml", persistent=True, duration=0)] load_page_message = dict( type="mycroft.events.triggered", namespace="foo", @@ -162,7 +171,9 @@ def test_activate_page(self): pass def test_remove_pages(self): - self.namespace.pages = ["foo", "bar", "foobar"] + self.namespace.pages = [GuiPage(name="foo", url="", persistent=False, duration=False), + GuiPage(name="bar", url="", persistent=False, duration=False), + GuiPage(name="foobar", url="", persistent=False, duration=False)] remove_page_message = dict( type="mycroft.gui.list.remove", namespace="foo", @@ -173,7 +184,7 @@ def test_remove_pages(self): with mock.patch(patch_function) as send_message_mock: self.namespace.remove_pages([2]) send_message_mock.assert_called_with(remove_page_message) - self.assertListEqual(["foo", "bar"], self.namespace.pages) + self.assertListEqual(["foo", "bar"], self.namespace.page_names) def test_page_gained_focus(self): # TODO @@ -262,7 +273,7 @@ def test_handle_send_event(self): def test_handle_delete_page_active_namespace(self): namespace = Namespace("foo") - namespace.pages = [GuiPage("bar", "bar.qml", True, 0)] + namespace.pages = [GuiPage(name="bar", url="bar.qml", persistent=True, duration=0)] namespace.remove_pages = mock.Mock() self.namespace_manager.loaded_namespaces = dict(foo=namespace) self.namespace_manager.active_namespaces = [namespace] @@ -326,7 +337,7 @@ def test_handle_show_page(self): self.namespace_manager._legacy_show_page.assert_called_once_with(message) self.namespace_manager._activate_namespace.assert_called_with("foo") self.namespace_manager._load_pages.assert_called_with(["pages"], None) - self.namespace_manager._update_namespace_persistence.\ + self.namespace_manager._update_namespace_persistence. \ assert_called_with(10) # With resource info @@ -347,7 +358,7 @@ def test_handle_show_page(self): self.namespace_manager._load_pages.assert_called_with([expected_page1, expected_page2], 1) - self.namespace_manager._update_namespace_persistence.\ + self.namespace_manager._update_namespace_persistence. \ assert_called_with(False) # System resources @@ -365,7 +376,7 @@ def test_handle_show_page(self): "skill_no_res") self.namespace_manager._load_pages.assert_called_with([expected_page], 2) - self.namespace_manager._update_namespace_persistence.\ + self.namespace_manager._update_namespace_persistence. \ assert_called_with(True) # TODO: Test page_names with files and URIs diff --git a/test/unittests/test_page.py b/test/unittests/test_page.py index b7756c9..fe3abec 100644 --- a/test/unittests/test_page.py +++ b/test/unittests/test_page.py @@ -14,7 +14,6 @@ def test_gui_page_legacy(self): self.assertEqual(page.name, name) self.assertEqual(page.persistent, persistent) self.assertEqual(page.duration, 0) - self.assertFalse(page.active) self.assertEqual(page.id, page.url) self.assertEqual(page.get_uri(), page.url) self.assertEqual(page.get_uri("qt6", "http://0.0.0.0:80"), page.url) From 8149301a3891457d483cd0c4ddf58d7b6d60de06 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 8 Feb 2024 14:18:51 +0000 Subject: [PATCH 19/24] Increment Version to 0.1.0a2 --- ovos_gui/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_gui/version.py b/ovos_gui/version.py index be3eeb2..a1795ef 100644 --- a/ovos_gui/version.py +++ b/ovos_gui/version.py @@ -2,5 +2,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 1 VERSION_BUILD = 0 -VERSION_ALPHA = 1 +VERSION_ALPHA = 2 # END_VERSION_BLOCK From 90324f878225c7313907e983195a6f84f510d41b Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 8 Feb 2024 14:19:17 +0000 Subject: [PATCH 20/24] Update Changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b13e75f..8f1d0e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [0.1.0a2](https://github.com/OpenVoiceOS/ovos-gui/tree/0.1.0a2) (2024-02-08) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-gui/compare/0.0.4a5...0.1.0a2) + +**Implemented enhancements:** + +- house cleaning [\#39](https://github.com/OpenVoiceOS/ovos-gui/pull/39) ([NeonJarbas](https://github.com/NeonJarbas)) + ## [0.0.4a5](https://github.com/OpenVoiceOS/ovos-gui/tree/0.0.4a5) (2023-12-29) [Full Changelog](https://github.com/OpenVoiceOS/ovos-gui/compare/0.0.4a4...0.0.4a5) From 068ef6ad4adaa07461695fc705b8d21d88986ec3 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Thu, 8 Feb 2024 14:57:10 +0000 Subject: [PATCH 21/24] Create coverage.yml --- .github/workflows/coverage.yml | 42 ++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .github/workflows/coverage.yml diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..86e1843 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,42 @@ +name: Run CodeCov +on: + push: + branches: + - dev + workflow_dispatch: + +jobs: + run: + runs-on: ubuntu-latest + env: + PYTHON: '3.9' + steps: + - uses: actions/checkout@master + - name: Setup Python + uses: actions/setup-python@master + with: + python-version: 3.9 + - name: Install System Dependencies + run: | + sudo apt-get update + sudo apt install python3-dev libssl-dev + python -m pip install build wheel + - name: Install test dependencies + run: | + pip install -r test/requirements.txt + - name: Install core repo + run: | + pip install -e . + - name: Generate coverage report + run: | + pytest --cov=ovos_gui --cov-report xml test/unittests + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + directory: ./coverage/reports/ + fail_ci_if_error: true + files: ./coverage.xml,!./cache + flags: unittests + name: codecov-umbrella + verbose: true From 5fa075b778b82d9ce300e906b962f7a60f4a1431 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 2 Sep 2024 16:18:41 +0000 Subject: [PATCH 22/24] Increment Version to 0.2.0 --- ovos_gui/version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ovos_gui/version.py b/ovos_gui/version.py index a1795ef..45c1596 100644 --- a/ovos_gui/version.py +++ b/ovos_gui/version.py @@ -1,6 +1,6 @@ # START_VERSION_BLOCK VERSION_MAJOR = 0 -VERSION_MINOR = 1 +VERSION_MINOR = 2 VERSION_BUILD = 0 -VERSION_ALPHA = 2 +VERSION_ALPHA = 0 # END_VERSION_BLOCK From d6c6b11207cf34438d27d554e8ef5334426b2af0 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 2 Sep 2024 16:19:05 +0000 Subject: [PATCH 23/24] Update Changelog --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f1d0e2..dbd0df3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [0.2.0](https://github.com/OpenVoiceOS/ovos-gui/tree/0.2.0) (2024-09-02) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-gui/compare/0.1.0a2...0.2.0) + +**Fixed bugs:** + +- Stuck on OVOS logo [\#42](https://github.com/OpenVoiceOS/ovos-gui/issues/42) + +**Closed issues:** + +- Gui roadmap [\#2](https://github.com/OpenVoiceOS/ovos-gui/issues/2) + ## [0.1.0a2](https://github.com/OpenVoiceOS/ovos-gui/tree/0.1.0a2) (2024-02-08) [Full Changelog](https://github.com/OpenVoiceOS/ovos-gui/compare/0.0.4a5...0.1.0a2) From 2085be98f5c29cb92c68d084ee73cbafd839a5cc Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Mon, 2 Sep 2024 17:21:07 +0100 Subject: [PATCH 24/24] Update version.py --- ovos_gui/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_gui/version.py b/ovos_gui/version.py index 45c1596..cbb1e3f 100644 --- a/ovos_gui/version.py +++ b/ovos_gui/version.py @@ -1,6 +1,6 @@ # START_VERSION_BLOCK VERSION_MAJOR = 0 -VERSION_MINOR = 2 +VERSION_MINOR = 1 VERSION_BUILD = 0 VERSION_ALPHA = 0 # END_VERSION_BLOCK