From 83c927d23a97e32291ef1c63b84d4a5622338ffe Mon Sep 17 00:00:00 2001 From: SpookyDervish Date: Sun, 8 Mar 2026 06:56:18 +1100 Subject: [PATCH] added pair screen, home screen, and channels screen --- desktop_app/api/channel.py | 3 ++ desktop_app/main.py | 4 +- desktop_app/ui/app.py | 10 +++-- desktop_app/ui/assets/channels_banner.txt | 8 ++++ desktop_app/ui/assets/global.tcss | 9 ++++ desktop_app/ui/assets/pair_screen.tcss | 26 +++-------- desktop_app/ui/screens/main_screen.py | 17 +++++++ desktop_app/ui/screens/pair_screen.py | 41 ++++++++++------- desktop_app/ui/widgets/channels_list.py | 55 +++++++++++++++++++++++ desktop_app/ui/widgets/home_info.py | 25 +++++++++++ desktop_app/ui/widgets/home_sidebar.py | 39 ++++++++++++++++ 11 files changed, 197 insertions(+), 40 deletions(-) create mode 100644 desktop_app/api/channel.py create mode 100644 desktop_app/ui/assets/channels_banner.txt create mode 100644 desktop_app/ui/assets/global.tcss create mode 100644 desktop_app/ui/screens/main_screen.py create mode 100644 desktop_app/ui/widgets/channels_list.py create mode 100644 desktop_app/ui/widgets/home_info.py create mode 100644 desktop_app/ui/widgets/home_sidebar.py diff --git a/desktop_app/api/channel.py b/desktop_app/api/channel.py new file mode 100644 index 0000000..39c08b0 --- /dev/null +++ b/desktop_app/api/channel.py @@ -0,0 +1,3 @@ +class Channel: + def __init__(self, name: str, key: str): + self.name = name \ No newline at end of file diff --git a/desktop_app/main.py b/desktop_app/main.py index e0c454f..aa6a486 100644 --- a/desktop_app/main.py +++ b/desktop_app/main.py @@ -1,7 +1,7 @@ from api.node import MeshNode -from ui.app import Hive +from ui.app import mesh if __name__ == "__main__": - app = Hive() + app = mesh() app.run() \ No newline at end of file diff --git a/desktop_app/ui/app.py b/desktop_app/ui/app.py index 5fe72fb..736fab5 100644 --- a/desktop_app/ui/app.py +++ b/desktop_app/ui/app.py @@ -1,11 +1,15 @@ from textual.app import App, ComposeResult from textual.widgets import Header, Footer from ui.screens.pair_screen import PairScreen +from api.node import MeshNode -class Hive(App): - theme = "rose-pine" - ENABLE_COMMAND_PALETTE = False +class mesh(App): + CSS_PATH = "assets/global.tcss" + + def __init__(self, driver_class = None, css_path = None, watch_css = False, ansi_color = False): + super().__init__(driver_class, css_path, watch_css, ansi_color) + self.mesh_node: MeshNode = None def on_ready(self): self.push_screen(PairScreen()) \ No newline at end of file diff --git a/desktop_app/ui/assets/channels_banner.txt b/desktop_app/ui/assets/channels_banner.txt new file mode 100644 index 0000000..a282098 --- /dev/null +++ b/desktop_app/ui/assets/channels_banner.txt @@ -0,0 +1,8 @@ + _ _ + | | | | + ___| |__ __ _ _ __ _ __ ___| |___ + / __| '_ \ / _` | '_ \| '_ \ / _ \ / __| + | (__| | | | (_| | | | | | | | __/ \__ \ + \___|_| |_|\__,_|_| |_|_| |_|\___|_|___/ + + \ No newline at end of file diff --git a/desktop_app/ui/assets/global.tcss b/desktop_app/ui/assets/global.tcss new file mode 100644 index 0000000..6db09c5 --- /dev/null +++ b/desktop_app/ui/assets/global.tcss @@ -0,0 +1,9 @@ +.banner { + padding: 1; + width: 100%; + background: $primary 50%; + color: $primary-lighten-1; + text-align: center; + margin: 1; + text-style: bold underline; +} \ No newline at end of file diff --git a/desktop_app/ui/assets/pair_screen.tcss b/desktop_app/ui/assets/pair_screen.tcss index 261fb2c..1b7e258 100644 --- a/desktop_app/ui/assets/pair_screen.tcss +++ b/desktop_app/ui/assets/pair_screen.tcss @@ -1,17 +1,17 @@ PairScreen { align: center middle; -} -EffectLabel { - min-width: 50; - text-align: center; - text-style: bold; + EffectLabel { + min-width: 50; + text-align: center; + text-style: bold; + } } #middle { border: $success round; - width: 60; - height: 28; + width: 45; + height: 15; padding: 0 1 1 1; @@ -20,20 +20,8 @@ EffectLabel { margin: 0 1; } - DataTable { - margin-top: 1; - } - LoadingIndicator { height: 1; margin-top: 1; } - - DataTable { - height: 5; - } - - Button { - margin: 2 0 0 1; - } } \ No newline at end of file diff --git a/desktop_app/ui/screens/main_screen.py b/desktop_app/ui/screens/main_screen.py new file mode 100644 index 0000000..2b45a19 --- /dev/null +++ b/desktop_app/ui/screens/main_screen.py @@ -0,0 +1,17 @@ +from textual.screen import Screen +from textual.widgets import Header, Footer, ContentSwitcher +from ui.widgets.home_sidebar import HomeSidebar +from ui.widgets.home_info import HomeInfo +from ui.widgets.channels_list import ChannelsList + + +class MainScreen(Screen): + def compose(self): + yield Header(show_clock=True) + yield HomeSidebar() + + with ContentSwitcher(initial="home-info"): + yield HomeInfo(id="home-info") + yield ChannelsList(id="channels-list") + + yield Footer() \ No newline at end of file diff --git a/desktop_app/ui/screens/pair_screen.py b/desktop_app/ui/screens/pair_screen.py index be8bf50..c265004 100644 --- a/desktop_app/ui/screens/pair_screen.py +++ b/desktop_app/ui/screens/pair_screen.py @@ -1,12 +1,33 @@ from textual.screen import Screen -from textual.containers import VerticalScroll, Vertical -from textual.widgets import Static, Button, LoadingIndicator, DataTable +from textual.containers import Vertical +from textual.widgets import Static, LoadingIndicator, DataTable +from textual import work +from ui.screens.main_screen import MainScreen from textualeffects.widgets import EffectLabel +from api.node import MeshNode + class PairScreen(Screen): CSS_PATH = "../assets/pair_screen.tcss" + @work + async def connect_to_node(self, is_retry = False): + if not is_retry: + self.notify("This may take a moment...", title="Discovering nearby nodes...") + self.app.mesh_node = await MeshNode.discover() + + if self.app.mesh_node == None: + self.notify("Check your node is powered on and nearby.\nRetrying...", title="Failed to find a nearby node!", severity="warning") + return self.connect_to_node(True) + + self.notify("Hurray! You're on the mesh!", title="Node connected!") + self.app.switch_screen(MainScreen()) + + + async def on_compose(self): + self.connect_to_node() + def compose(self): with Vertical(id="middle") as center_window: @@ -15,18 +36,6 @@ class PairScreen(Screen): with open("ui/assets/banner.txt", "r") as f: yield EffectLabel(f.read(), effect="Print") - yield Static("Make sure your mesh network node is powered and ready to pair. When you're ready, click the \"Pair\" button to connect to the mesh!") + yield Static("Attempting to connect to a nearby node. Make sure your mesh network node is powered and ready to pair.") - table = DataTable() - table.add_columns("[b]Address", "RSSI") - yield table - - table.add_row("hi", "[green]󰢾[/] -34 dBm") - table.add_row("hi", "[green]󰢾[/] -52 dBm") - table.add_row("hi", "[yellow]󰢽[/] -64 dBm") - table.add_row("hi", "[red]󰢼[/] -98 dBm") - table.add_row("hi", "[red]󰢼[/] -101 dBm") - - yield LoadingIndicator() - - yield Button("Select a node to pair", disabled=True, variant="success", id="pair-btn") \ No newline at end of file + yield LoadingIndicator() \ No newline at end of file diff --git a/desktop_app/ui/widgets/channels_list.py b/desktop_app/ui/widgets/channels_list.py new file mode 100644 index 0000000..91c9903 --- /dev/null +++ b/desktop_app/ui/widgets/channels_list.py @@ -0,0 +1,55 @@ +from textual.containers import VerticalScroll, Vertical, HorizontalGroup +from textual.widgets import Static, Button, Rule + +from api.channel import Channel + + +class ChannelView(Button): + DEFAULT_CSS = """ + ChannelView { + margin: 0 1; + background: $boost; + border: $surface-lighten-1 tall; + content-align: left middle; + text-align: left; + width: 30; + } + """ + + def __init__(self, channel: Channel): + super().__init__(" [b]" + channel.name, flat=True) + self.channel = channel + +class ChannelsList(Vertical): + DEFAULT_CSS = """ + ChannelsList { + Rule { + color: $surface-lighten-1; + } + + #buttons { + margin-bottom: 1; + + Button { + margin: 0 1; + } + } + } + """ + + def compose(self): + with VerticalScroll(): + yield Static("channels", classes="banner") + yield ChannelView(Channel("test channel", "AQ==")) + yield ChannelView(Channel("test channel", "AQ==")) + yield ChannelView(Channel("test channel", "AQ==")) + yield ChannelView(Channel("test channel", "AQ==")) + yield ChannelView(Channel("test channel", "AQ==")) + yield ChannelView(Channel("test channel", "AQ==")) + yield ChannelView(Channel("test channel", "AQ==")) + + yield Rule() + + with HorizontalGroup(id="buttons"): + yield Button("Create Channel") + yield Button("Advertise") \ No newline at end of file diff --git a/desktop_app/ui/widgets/home_info.py b/desktop_app/ui/widgets/home_info.py new file mode 100644 index 0000000..a85a56a --- /dev/null +++ b/desktop_app/ui/widgets/home_info.py @@ -0,0 +1,25 @@ +from textual.containers import Center +from textual.widgets import Static +from textualeffects.widgets import EffectLabel + + +class HomeInfo(Center): + DEFAULT_CSS = """ + HomeInfo { + width: 100%; + padding: 0 3; + + EffectLabel { + min-width: 100%; + text-align: center; + } + } + """ + + def compose(self): + with open("ui/assets/banner.txt", "r") as f: + yield EffectLabel(f.read(), effect="Print") + + yield Static("[cyan]󰍦[/] [b blink]1[/] new message(s)") + yield Static("[cyan]󰑩[/] [b]2 of 3[/] nodes online") + yield Static("[lime]󰢾[/] [b]SNR:[/] 10.0 dBm [b]| RSSI:[/] -115.0 dBm") \ No newline at end of file diff --git a/desktop_app/ui/widgets/home_sidebar.py b/desktop_app/ui/widgets/home_sidebar.py new file mode 100644 index 0000000..4e5e768 --- /dev/null +++ b/desktop_app/ui/widgets/home_sidebar.py @@ -0,0 +1,39 @@ +from textual.containers import Vertical +from textual.widgets import Button, ContentSwitcher + + +class SidebarButton(Button): + DEFAULT_CSS = """ + SidebarButton { + max-width: 100%; + margin-bottom: 1; + } + """ + +class HomeSidebar(Vertical): + DEFAULT_CSS = """ + HomeSidebar { + width: 11; + background: $boost; + border-right: $surface-lighten-1 tall; + padding: 1; + dock: left; + margin-top: 1; + } + """ + + def on_button_pressed(self, event: Button.Pressed): + content_switcher: ContentSwitcher = self.screen.query_one(ContentSwitcher) + + match event.button.id: + case "home-btn": + content_switcher.current = "home-info" + case "channels-btn": + content_switcher.current = "channels-list" + case "settings-btn": + content_switcher.current = "settings" + + def compose(self): + yield SidebarButton("", tooltip="Home", id="home-btn") + yield SidebarButton("", tooltip="Channels", id="channels-btn") + yield SidebarButton("", tooltip="Settings", id="settings-btn") \ No newline at end of file