aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-cli/src/meshbay_cli/tui/app.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-14 22:20:15 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-14 22:20:15 +0200
commit422647e7898b8df67cd11f8fb060aec23dec84aa (patch)
tree7ecca0e8df1bc8a7d26cf4b471bd44906dd9cfd1 /packages/meshbay-cli/src/meshbay_cli/tui/app.py
parent28c88908b88c58cba4de08710f6bb268ede02d57 (diff)
downloadmeshbay-422647e7898b8df67cd11f8fb060aec23dec84aa.tar.gz
feat(cli): meshbay-cli, a command-line client for members
Signs in to the hub, lists the account's groups and their folders, and downloads and uploads files directly with each group's node, end-to-end encrypted. With no argument it opens a two-pane interactive mode (Textual). Session and device keys are kept encrypted under the system's key storage, or a 0600 key file where there is none; node keys are pinned at first contact; a new machine links as a device. Packaged on its own on Linux (deb and rpm, on meshbay-common's venv, with a man page generated from the parser) and frozen next to meshbay-node in the full Windows installer, not the light one, with a self-test the build runs. client/keys.py ports keyderive.js and is LGPL like it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-cli/src/meshbay_cli/tui/app.py')
-rw-r--r--packages/meshbay-cli/src/meshbay_cli/tui/app.py76
1 files changed, 76 insertions, 0 deletions
diff --git a/packages/meshbay-cli/src/meshbay_cli/tui/app.py b/packages/meshbay-cli/src/meshbay_cli/tui/app.py
new file mode 100644
index 0000000..dbb773c
--- /dev/null
+++ b/packages/meshbay-cli/src/meshbay_cli/tui/app.py
@@ -0,0 +1,76 @@
+"""
+`meshbay-cli` with no argument: a full-screen terminal interface.
+
+Textual, because it runs on the same asyncio loop as aiortc and httpx rather
+than beside them, and on Linux terminals and Windows' consoles alike. Signed in
+already, it opens on the two panes; otherwise on the sign-in screen first.
+
+Nothing is printed while it runs — a line on stderr would tear the screen — so
+the log goes to a file in the CLI's data directory instead.
+"""
+
+import logging
+
+from textual.app import App
+
+from ..client.account import Account, no_passphrase
+from ..client.hub import Hub
+from ..client.store import data_dir
+from ..commands.context import Context
+from ..errors import CliError
+from .screens import DeviceCode, MainScreen, SignInScreen
+
+
+class MeshBayApp(App):
+ TITLE = "MeshBay"
+
+ def __init__(self, ctx: Context):
+ super().__init__()
+ self.ctx = ctx
+ self.hub = ctx.hub
+
+ async def use_hub(self, url: str) -> None:
+ if url != self.hub.url:
+ await self.hub.aclose()
+ self.hub = Hub(url)
+ self.ctx.hub = self.hub
+ self.ctx.hub_url = url
+
+ def on_mount(self) -> None:
+ account = None
+ if self.ctx.store.peek().get("hub") == self.hub.url:
+ try:
+ account = Account.open(self.hub, None, no_passphrase, self.ctx.store)
+ except CliError as e:
+ self.notify(e.message, severity="warning", markup=False)
+ if account is not None and account.remembered:
+ self.start(account)
+ else:
+ self.push_screen(SignInScreen())
+
+ def start(self, account: Account) -> None:
+ account.on_device_code = DeviceCode(self)
+ if isinstance(self.screen, SignInScreen):
+ self.switch_screen(MainScreen(account))
+ else:
+ self.push_screen(MainScreen(account))
+
+
+def _log_to_file() -> None:
+ root = logging.getLogger()
+ for h in list(root.handlers):
+ root.removeHandler(h)
+ try:
+ directory = data_dir()
+ directory.mkdir(parents=True, exist_ok=True)
+ handler = logging.FileHandler(directory / "interactive.log", encoding="utf-8")
+ except OSError:
+ handler = logging.NullHandler()
+ handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s"))
+ root.addHandler(handler)
+
+
+async def run_interactive(ctx: Context) -> int:
+ _log_to_file()
+ await MeshBayApp(ctx).run_async()
+ return 0