added pair screen, home screen, and channels screen

This commit is contained in:
2026-03-08 06:56:18 +11:00
parent 3bcab4414a
commit 83c927d23a
11 changed files with 197 additions and 40 deletions

View File

@@ -0,0 +1,3 @@
class Channel:
def __init__(self, name: str, key: str):
self.name = name

View File

@@ -1,7 +1,7 @@
from api.node import MeshNode from api.node import MeshNode
from ui.app import Hive from ui.app import mesh
if __name__ == "__main__": if __name__ == "__main__":
app = Hive() app = mesh()
app.run() app.run()

View File

@@ -1,11 +1,15 @@
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.widgets import Header, Footer from textual.widgets import Header, Footer
from ui.screens.pair_screen import PairScreen from ui.screens.pair_screen import PairScreen
from api.node import MeshNode
class Hive(App): class mesh(App):
theme = "rose-pine" CSS_PATH = "assets/global.tcss"
ENABLE_COMMAND_PALETTE = False
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): def on_ready(self):
self.push_screen(PairScreen()) self.push_screen(PairScreen())

View File

@@ -0,0 +1,8 @@
_ _
| | | |
___| |__ __ _ _ __ _ __ ___| |___
/ __| '_ \ / _` | '_ \| '_ \ / _ \ / __|
| (__| | | | (_| | | | | | | | __/ \__ \
\___|_| |_|\__,_|_| |_|_| |_|\___|_|___/

View File

@@ -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;
}

View File

@@ -1,17 +1,17 @@
PairScreen { PairScreen {
align: center middle; align: center middle;
}
EffectLabel { EffectLabel {
min-width: 50; min-width: 50;
text-align: center; text-align: center;
text-style: bold; text-style: bold;
} }
}
#middle { #middle {
border: $success round; border: $success round;
width: 60; width: 45;
height: 28; height: 15;
padding: 0 1 1 1; padding: 0 1 1 1;
@@ -20,20 +20,8 @@ EffectLabel {
margin: 0 1; margin: 0 1;
} }
DataTable {
margin-top: 1;
}
LoadingIndicator { LoadingIndicator {
height: 1; height: 1;
margin-top: 1; margin-top: 1;
} }
DataTable {
height: 5;
}
Button {
margin: 2 0 0 1;
}
} }

View File

@@ -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()

View File

@@ -1,12 +1,33 @@
from textual.screen import Screen from textual.screen import Screen
from textual.containers import VerticalScroll, Vertical from textual.containers import Vertical
from textual.widgets import Static, Button, LoadingIndicator, DataTable from textual.widgets import Static, LoadingIndicator, DataTable
from textual import work
from ui.screens.main_screen import MainScreen
from textualeffects.widgets import EffectLabel from textualeffects.widgets import EffectLabel
from api.node import MeshNode
class PairScreen(Screen): class PairScreen(Screen):
CSS_PATH = "../assets/pair_screen.tcss" 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): def compose(self):
with Vertical(id="middle") as center_window: with Vertical(id="middle") as center_window:
@@ -15,18 +36,6 @@ class PairScreen(Screen):
with open("ui/assets/banner.txt", "r") as f: with open("ui/assets/banner.txt", "r") as f:
yield EffectLabel(f.read(), effect="Print") 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 LoadingIndicator()
yield Button("Select a node to pair", disabled=True, variant="success", id="pair-btn")

View File

@@ -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")

View File

@@ -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")

View File

@@ -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")