summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-cli/src/meshbay_cli/tui
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-15 01:12:16 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-15 01:12:16 +0200
commit382fcc5f675f9942350013780335315757c245c5 (patch)
treea24ed8732c6d3971b071ed6af3c399d6142b79f4 /packages/meshbay-cli/src/meshbay_cli/tui
parent6f68bb6c1970e26de1f66c073ea5d7fecd21840d (diff)
downloadmeshbay-382fcc5f675f9942350013780335315757c245c5.tar.gz
feat(cli): rm, and Delete in the interactive mode
rm deletes files, resolved as get resolves paths and patterns, with the node's rule (design §5.4): the uploading account's devices, or the node's operator account. The whole run is checked against the index first (the entry's uploader_id, the role the join result gives this key): a file outside the rule, or a folder, refuses it before anything is asked or sent. Then one confirmation, or --yes. The node's challenge is checked against what was asked before it is signed. In the interactive mode, Delete on the right pane does the same, asked once per batch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-cli/src/meshbay_cli/tui')
-rw-r--r--packages/meshbay-cli/src/meshbay_cli/tui/screens.py59
1 files changed, 59 insertions, 0 deletions
diff --git a/packages/meshbay-cli/src/meshbay_cli/tui/screens.py b/packages/meshbay-cli/src/meshbay_cli/tui/screens.py
index e1ab620..0306325 100644
--- a/packages/meshbay-cli/src/meshbay_cli/tui/screens.py
+++ b/packages/meshbay-cli/src/meshbay_cli/tui/screens.py
@@ -6,6 +6,10 @@ with the rules `get` and `put` follow: an upload is decided whole before it
starts (names the node refuses stop it, names already there are asked about
once for the batch), nothing on either side is replaced without being asked,
and a name from a group is made writable here before it is written.
+
+Delete removes the selection from a group with the rules `rm` follows: files
+only, every one of them this account's to delete (uploaded by it, or this key
+the node's operator) or none is, then asked once for the batch.
"""
import asyncio
@@ -24,6 +28,7 @@ from ..client.account import Account
from ..client.names import UnsafeName, local_path
from ..client.paths import handle
from ..client.pool import Pool
+from ..client.remove import delete_file, may_delete
from ..client.store import FILE_KEY_NOTICE, KEY_FILE
from ..client.transfers import PART_SUFFIX, TransferError, create_dir, download, upload
from ..client.tree import Tree
@@ -95,6 +100,7 @@ class SignInScreen(Screen):
class MainScreen(Screen):
BINDINGS = [
Binding("f5", "copy", "Copy"),
+ Binding("delete", "delete", "Delete"),
Binding("q", "app.quit", "Quit"),
Binding("tab", "app.focus_next", "Switch", show=False),
]
@@ -331,6 +337,59 @@ class MainScreen(Screen):
except TransferError as e:
self.strip.update_job(job, state="failed", note=e.message)
+ # ── Delete ───────────────────────────────────────────────────────────────
+
+ def action_delete(self) -> None:
+ focused = self.app.focused
+ if focused is None or self.remote not in focused.ancestors_with_self:
+ self.app.notify("Delete removes files from a group: select them on the "
+ "right.", timeout=4)
+ return
+ self.run_worker(self._delete(), group="delete")
+
+ async def _delete(self) -> None:
+ remote = self.remote
+ rows = remote.selection()
+ if remote.group is None or not rows:
+ self.app.notify("Open a group and choose the files to delete.", timeout=4)
+ return
+ group = remote.group
+ try:
+ session = await self.pool.session(group)
+ except CliError as e:
+ self.app.notify(e.message, severity="error", markup=False)
+ return
+ folders = [r.name for r in rows if r.is_dir]
+ not_yours = [r.name for r in rows if not r.is_dir
+ and not may_delete(session, r.data["entry"], self.account.user_id)]
+ if folders or not_yours:
+ why = (["Folders are not deleted here: " + ", ".join(map(safe, folders))]
+ if folders else [])
+ if not_yours:
+ why.append("You did not upload these, and this machine's key is not the "
+ "node's operator: " + ", ".join(map(safe, not_yours)))
+ await self._ask("Nothing was deleted.\n" + "\n".join(why), [("ok", "OK")])
+ return
+ names = ", ".join(safe(r.name) for r in rows[:5]) + (
+ f" and {len(rows) - 5} more" if len(rows) > 5 else "")
+ answer = await self._ask(
+ f"Delete {len(rows)} file(s) from {safe(handle(group))} for every member? "
+ f"{names}", [("delete", "Delete"), ("cancel", "Cancel")])
+ if answer != "delete":
+ return
+ remote.marks.clear()
+ deleted = 0
+ for row in rows:
+ try:
+ await delete_file(session, row.data["entry"]["id"])
+ deleted += 1
+ except CliError as e:
+ self.app.notify(f"{safe(row.name)}: {e.message}", severity="error",
+ markup=False, timeout=8)
+ if deleted:
+ self.app.notify(f"Deleted {deleted} file(s).", timeout=4)
+ self._changed(group)
+
async def on_unmount(self) -> None:
with contextlib.suppress(Exception):
await self.pool.close()