From 56e476c58e74acaf95d43b0de6adace82ff7735f Mon Sep 17 00:00:00 2001 From: Sassan Haradji Date: Fri, 10 May 2024 19:37:16 +0400 Subject: [PATCH] refactor(vscode): flatten vscode menu items into its main menu #102 --- CHANGELOG.md | 1 + .../050-vscode/{checks.py => commands.py} | 89 ++++++++ ubo_app/services/050-vscode/login_page.py | 2 +- ubo_app/services/050-vscode/setup.py | 110 +++++++--- ubo_app/services/050-vscode/setup_page.kv | 59 ----- ubo_app/services/050-vscode/setup_page.py | 204 ------------------ .../services/050-vscode/vscode_qrcode_page.kv | 26 +++ .../services/050-vscode/vscode_qrcode_page.py | 22 ++ ubo_app/setup.py | 17 +- 9 files changed, 224 insertions(+), 306 deletions(-) rename ubo_app/services/050-vscode/{checks.py => commands.py} (59%) delete mode 100644 ubo_app/services/050-vscode/setup_page.kv delete mode 100644 ubo_app/services/050-vscode/setup_page.py create mode 100644 ubo_app/services/050-vscode/vscode_qrcode_page.kv create mode 100644 ubo_app/services/050-vscode/vscode_qrcode_page.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dbbaac2..5ab41500 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ in the service file so that user can easily add their custom services - fix(voice): remove "clear access key" item when access key is not set #97 - fix(voice): update pvorca to 2.1.0 as they suddenly yanked 1.4.0 in pypi #103 +- refactor(vscode): flatten vscode menu items into its main menu #102 ## Version 0.14.0 diff --git a/ubo_app/services/050-vscode/checks.py b/ubo_app/services/050-vscode/commands.py similarity index 59% rename from ubo_app/services/050-vscode/checks.py rename to ubo_app/services/050-vscode/commands.py index 4e93d142..51a34756 100644 --- a/ubo_app/services/050-vscode/checks.py +++ b/ubo_app/services/050-vscode/commands.py @@ -3,6 +3,7 @@ import asyncio import json +import socket import subprocess from typing import Literal, TypedDict @@ -121,3 +122,91 @@ async def check_status() -> None: ), ), ) + + +async def set_name() -> None: + try: + hostname = socket.gethostname() + process = await asyncio.create_subprocess_exec( + CODE_BINARY_PATH, + 'tunnel', + '--accept-server-license-terms', + 'rename', + hostname, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + await process.wait() + except subprocess.CalledProcessError: + dispatch( + NotificationsAddAction( + notification=Notification( + title='VSCode', + content='Failed to setup: renaming the tunnel', + display_type=NotificationDisplayType.STICKY, + color=DANGER_COLOR, + icon='󰜺', + chime=Chime.FAILURE, + ), + ), + ) + finally: + await check_status() + + +async def install_service() -> None: + try: + process = await asyncio.create_subprocess_exec( + CODE_BINARY_PATH, + 'tunnel', + '--accept-server-license-terms', + 'service', + 'install', + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + await process.wait() + except subprocess.CalledProcessError: + dispatch( + NotificationsAddAction( + notification=Notification( + title='VSCode', + content='Failed to setup: installing service', + display_type=NotificationDisplayType.STICKY, + color=DANGER_COLOR, + icon='󰜺', + chime=Chime.FAILURE, + ), + ), + ) + finally: + await check_status() + + +async def uninstall_service() -> None: + try: + process = await asyncio.create_subprocess_exec( + CODE_BINARY_PATH, + 'tunnel', + '--accept-server-license-terms', + 'service', + 'uninstall', + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + await process.wait() + except subprocess.CalledProcessError: + dispatch( + NotificationsAddAction( + notification=Notification( + title='VSCode', + content='Failed to setup: uninstalling service', + display_type=NotificationDisplayType.STICKY, + color=DANGER_COLOR, + icon='󰜺', + chime=Chime.FAILURE, + ), + ), + ) + finally: + await check_status() diff --git a/ubo_app/services/050-vscode/login_page.py b/ubo_app/services/050-vscode/login_page.py index 78e8ff01..832f2025 100644 --- a/ubo_app/services/050-vscode/login_page.py +++ b/ubo_app/services/050-vscode/login_page.py @@ -6,7 +6,7 @@ import re import subprocess -from checks import check_status +from commands import check_status from constants import CODE_BINARY_PATH from kivy.clock import mainthread from kivy.lang.builder import Builder diff --git a/ubo_app/services/050-vscode/setup.py b/ubo_app/services/050-vscode/setup.py index adad76ed..74f57324 100644 --- a/ubo_app/services/050-vscode/setup.py +++ b/ubo_app/services/050-vscode/setup.py @@ -5,12 +5,12 @@ import subprocess from typing import TYPE_CHECKING -from checks import check_status +from commands import check_status, install_service, uninstall_service from constants import CODE_BINARY_PATH, CODE_BINARY_URL, DOWNLOAD_PATH from login_page import LoginPage -from setup_page import SetupPage from ubo_gui.constants import DANGER_COLOR from ubo_gui.menu.types import ActionItem, ApplicationItem, HeadedMenu +from vscode_qrcode_page import CODE_TUNNEL_URL_PREFIX, VSCodeQRCodePage from ubo_app.constants import INSTALLATION_PATH from ubo_app.store import autorun, dispatch @@ -25,6 +25,7 @@ VSCodeDoneDownloadingAction, VSCodeStartDownloadingAction, VSCodeState, + VSCodeStatus, ) from ubo_app.utils.async_ import create_task @@ -112,41 +113,63 @@ async def act() -> None: create_task(act()) -@autorun( - lambda state: state.vscode, - comparator=lambda state: ( - state.vscode.is_binary_installed, - state.vscode.is_logged_in, - state.vscode.is_downloading, - ), -) +def status_based_actions(status: VSCodeStatus) -> list[ActionItem | ApplicationItem]: + actions = [] + if status.is_running: + actions.append( + ApplicationItem( + label='Show URL', + icon='󰐲', + application=VSCodeQRCodePage( + url=f'{CODE_TUNNEL_URL_PREFIX}{status.name}', + ), + ), + ) + actions.append( + ActionItem( + label='Uninstall Service' + if status.is_service_installed + else 'Install Service', + icon='󰫜' if status.is_service_installed else '󰫚', + action=(lambda: create_task(uninstall_service()) and None) + if status.is_service_installed + else (lambda: create_task(install_service()) and None), + ), + ) + return actions + + +def login_actions(*, is_logged_in: bool) -> list[ActionItem | ApplicationItem]: + actions = [] + if is_logged_in: + actions.extend( + [ + ActionItem( + label='Logout', + icon='󰍃', + action=logout, + ), + ], + ) + else: + actions.append( + ApplicationItem( + label='Login', + icon='󰍂', + application=LoginPage, + ), + ) + return actions + + +@autorun(lambda state: state.vscode) def vscode_menu(state: VSCodeState) -> HeadedMenu: actions = [] if not state.is_downloading: if state.is_binary_installed: - if state.is_logged_in: - actions.extend( - [ - ApplicationItem( - label='Setup Tunnel', - icon='󰒔', - application=SetupPage, - ), - ActionItem( - label='Logout', - icon='󰍃', - action=logout, - ), - ], - ) - else: - actions.append( - ApplicationItem( - label='Login', - icon='󰍂', - application=LoginPage, - ), - ) + if state.status: + actions.extend(status_based_actions(state.status)) + actions.extend(login_actions(is_logged_in=state.is_logged_in)) actions.append( ActionItem( @@ -158,10 +181,27 @@ def vscode_menu(state: VSCodeState) -> HeadedMenu: ), ) + status = '' + if state.status: + if state.status.is_running: + status = 'Service is running' + elif not state.status.is_service_installed: + status = 'Service not installed' + else: + status = 'Service installed but not running' + elif state.is_downloading: + status = 'Downloading...' + elif not state.is_binary_installed: + status = 'Code CLI not installed' + elif not state.is_logged_in: + status = 'Needs authentication' + else: + status = 'Unknown status' + return HeadedMenu( title='󰨞VSCode', - heading='Setup VSCode Tunnel', - sub_heading='Downloading...' if state.is_downloading else '', + heading='VSCode Remote Tunnel', + sub_heading=status, items=actions, ) diff --git a/ubo_app/services/050-vscode/setup_page.kv b/ubo_app/services/050-vscode/setup_page.kv deleted file mode 100644 index 92ec6d24..00000000 --- a/ubo_app/services/050-vscode/setup_page.kv +++ /dev/null @@ -1,59 +0,0 @@ -: - BoxLayout: - id: pager - orientation: 'vertical' - padding: dp(5), dp(5), dp(5), root.padding_bottom - spacing: dp(2) - - Label: - text: '󰔟' - size_hint: 1, 1 if root.stage == 0 else 0 - height: pager.height if root.stage == 0 else 0 - opacity: 1 if root.stage == 0 else 0 - font_size: dp(36) - halign: 'center' - valign: 'center' - - BoxLayout: - orientation: 'vertical' - spacing: UBO_GUI_MENU_ITEM_GAP - size_hint: 1, 1 if root.stage == 1 else 0 - height: pager.height if root.stage == 1 else 0 - opacity: 1 if root.stage == 1 else 0 - - ItemWidget: - item: root.items[0] - icon: '󰑕' - size_hint: 1, None - - ItemWidget: - item: root.items[1] - label: 'Uninstall Service' if root.is_installed else 'Install Service' - icon: '󰫜' if root.is_installed else '󰫚' - size_hint: 1, None - - ItemWidget: - item: root.items[2] if root.url else root.pending_url_item if root.is_installed else None - icon: '󰐲' - size_hint: 1, None - - BoxLayout: - orientation: 'vertical' - spacing: dp(2) - size_hint: 1, 1 if root.stage == 2 else 0 - height: pager.height if root.stage == 2 else 0 - opacity: 1 if root.stage == 2 else 0 - - QRCodeWidget: - content: root.url - size_hint: 1, 1 - height: self.width - pos_hint: {'left': 0.5, 'top': 1} - - Label: - text: root.url - size_hint: 1, None - height: self.texture_size[1] - font_size: dp(14) - halign: 'center' - valign: 'center' diff --git a/ubo_app/services/050-vscode/setup_page.py b/ubo_app/services/050-vscode/setup_page.py deleted file mode 100644 index ddd8bd6b..00000000 --- a/ubo_app/services/050-vscode/setup_page.py +++ /dev/null @@ -1,204 +0,0 @@ -# ruff: noqa: D100, D101, D102, D103, D104, D107, N999 -from __future__ import annotations - -import asyncio -import pathlib -import socket -import subprocess -from typing import TYPE_CHECKING - -from checks import check_status -from constants import CODE_BINARY_PATH -from kivy.clock import mainthread -from kivy.lang.builder import Builder -from kivy.properties import BooleanProperty, NumericProperty, StringProperty -from ubo_gui.constants import DANGER_COLOR -from ubo_gui.menu.types import ActionItem, Item -from ubo_gui.page import PageWidget - -from ubo_app.store import autorun, dispatch -from ubo_app.store.services.notifications import ( - Chime, - Notification, - NotificationDisplayType, - NotificationsAddAction, -) -from ubo_app.utils.async_ import create_task - -CODE_TUNNEL_URL_PREFIX = 'https://vscode.dev/tunnel/' - -if TYPE_CHECKING: - from ubo_app.store.services.vscode import VSCodeState - - -class SetupPage(PageWidget): - stage: int = NumericProperty(0) - is_installed = BooleanProperty() - url: str = StringProperty() - - pending_url_item = Item( - label='Waiting for URL', - icon='󰔟', - ) - - def go_back(self: SetupPage) -> bool: - if self.stage > 1: - self.stage = 1 - return True - return False - - def __init__( - self: SetupPage, - *args: object, - **kwargs: object, - ) -> None: - self.process = None - items = [ - ActionItem( - label='Set name', - action=lambda: create_task(self.set_name()) and None, - ), - ActionItem( - action=lambda: create_task( - self.uninstall_service() - if self.is_installed - else self.install_service(), - ) - and None, - ), - ActionItem( - label='Show URL', - icon='󰐲', - action=self.show_url, - ), - ] - super().__init__(*args, **kwargs, items=items) - - self.unsubscribe = autorun(lambda state: state.vscode)(self.sync) - - @mainthread - def reset(self: SetupPage) -> None: - self.stage = 0 - - def clean_process(self: SetupPage) -> None: - if self.process and self.process.returncode is None: - self.process.kill() - - def on_close(self: SetupPage) -> None: - self.clean_process() - self.unsubscribe() - - @mainthread - def sync(self: SetupPage, state: VSCodeState) -> None: - if state.status is None: - self.is_installed = False - self.url = '' - else: - self.is_installed = state.status.is_service_installed - if state.status.name: - self.url = f'{CODE_TUNNEL_URL_PREFIX}{state.status.name}' - else: - self.url = '' - if self.stage == 0: - self.stage = 1 - - @mainthread - def show_url(self: SetupPage) -> None: - if self.url: - self.stage = 2 - - async def set_name(self: SetupPage) -> None: - self.clean_process() - self.reset() - try: - hostname = socket.gethostname() - self.process = await asyncio.create_subprocess_exec( - CODE_BINARY_PATH, - 'tunnel', - '--accept-server-license-terms', - 'rename', - hostname, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - await self.process.wait() - except subprocess.CalledProcessError: - dispatch( - NotificationsAddAction( - notification=Notification( - title='VSCode', - content='Failed to setup: renaming the tunnel', - display_type=NotificationDisplayType.STICKY, - color=DANGER_COLOR, - icon='󰜺', - chime=Chime.FAILURE, - ), - ), - ) - finally: - await check_status() - - async def install_service(self: SetupPage) -> None: - self.clean_process() - self.reset() - try: - self.process = await asyncio.create_subprocess_exec( - CODE_BINARY_PATH, - 'tunnel', - '--accept-server-license-terms', - 'service', - 'install', - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - await self.process.wait() - except subprocess.CalledProcessError: - dispatch( - NotificationsAddAction( - notification=Notification( - title='VSCode', - content='Failed to setup: installing service', - display_type=NotificationDisplayType.STICKY, - color=DANGER_COLOR, - icon='󰜺', - chime=Chime.FAILURE, - ), - ), - ) - finally: - await check_status() - - async def uninstall_service(self: SetupPage) -> None: - self.clean_process() - self.reset() - try: - self.process = await asyncio.create_subprocess_exec( - CODE_BINARY_PATH, - 'tunnel', - '--accept-server-license-terms', - 'service', - 'uninstall', - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - await self.process.wait() - except subprocess.CalledProcessError: - dispatch( - NotificationsAddAction( - notification=Notification( - title='VSCode', - content='Failed to setup: uninstalling service', - display_type=NotificationDisplayType.STICKY, - color=DANGER_COLOR, - icon='󰜺', - chime=Chime.FAILURE, - ), - ), - ) - finally: - await check_status() - - -Builder.load_file( - pathlib.Path(__file__).parent.joinpath('setup_page.kv').resolve().as_posix(), -) diff --git a/ubo_app/services/050-vscode/vscode_qrcode_page.kv b/ubo_app/services/050-vscode/vscode_qrcode_page.kv new file mode 100644 index 00000000..059182da --- /dev/null +++ b/ubo_app/services/050-vscode/vscode_qrcode_page.kv @@ -0,0 +1,26 @@ +#:kivy 2.3.0 + +: + BoxLayout: + id: pager + orientation: 'vertical' + padding: dp(5), dp(5), dp(5), root.padding_bottom + spacing: dp(2) + + BoxLayout: + orientation: 'vertical' + spacing: dp(2) + + QRCodeWidget: + content: root.url + size_hint: 1, 1 + height: self.width + pos_hint: {'left': 0.5, 'top': 1} + + Label: + text: root.url + size_hint: 1, None + height: self.texture_size[1] + font_size: dp(14) + halign: 'center' + valign: 'center' diff --git a/ubo_app/services/050-vscode/vscode_qrcode_page.py b/ubo_app/services/050-vscode/vscode_qrcode_page.py new file mode 100644 index 00000000..9c62b9b5 --- /dev/null +++ b/ubo_app/services/050-vscode/vscode_qrcode_page.py @@ -0,0 +1,22 @@ +# ruff: noqa: D100, D101, D102, D103, D104, D107, N999 +from __future__ import annotations + +import pathlib + +from kivy.lang.builder import Builder +from kivy.properties import StringProperty +from ubo_gui.page import PageWidget + +CODE_TUNNEL_URL_PREFIX = 'https://vscode.dev/tunnel/' + + +class VSCodeQRCodePage(PageWidget): + url: str = StringProperty() + + +Builder.load_file( + pathlib.Path(__file__) + .parent.joinpath('vscode_qrcode_page.kv') + .resolve() + .as_posix(), +) diff --git a/ubo_app/setup.py b/ubo_app/setup.py index 3e58b411..8f92bc23 100644 --- a/ubo_app/setup.py +++ b/ubo_app/setup.py @@ -1,5 +1,6 @@ """Compatibility layer for different environments.""" +from pathlib import Path from typing import Any @@ -35,14 +36,16 @@ def fake_subprocess_run( original_asyncio_create_subprocess_exec = asyncio.create_subprocess_exec - def fake_create_subprocess_exec(*command: str, **kwargs: Any) -> object: # noqa: ANN401 - if any(i in command[0] for i in ('reboot', 'poweroff')): + def fake_create_subprocess_exec(*args: str, **kwargs: Any) -> object: # noqa: ANN401 + command = args[0] + if command == '/usr/bin/env': + command = args[1] + if isinstance(command, Path): + command = command.as_posix() + if any(i in command for i in ('reboot', 'poweroff')): return Fake() - if command[0] == '/usr/bin/env': - if command[1] in ('curl', 'tar'): - return original_asyncio_create_subprocess_exec(*command, **kwargs) - elif command[0].endswith('/code'): - return original_asyncio_create_subprocess_exec(*command, **kwargs) + if command in ('curl', 'tar') or command.endswith('/code'): + return original_asyncio_create_subprocess_exec(*args, **kwargs) return Fake( _Fake__await_value=Fake( _Fake__props={