# Right-click a file, send it to your phone. # # macOS has a Share sheet in Finder and Windows has "Send to". Both are among # the few context-menu entries people genuinely reach for, and neither exists # on a stock Linux file manager. # # This adds no new machinery. Panama already talks to KDE Connect through # scripts/panama-kdeconnect, which the Home & Phone settings page and the quick # settings panel both use; this is the same `send-file` verb from the file # manager instead of from a panel. # # The entry appears only when a phone is actually reachable. A menu item that # is present and fails is worse than one that is absent, because the absence # is self-explaining: no phone, no send. import json import os import subprocess from urllib.parse import unquote, urlparse from gi.repository import GObject, Nautilus, Notify HELPER = os.path.expanduser("~/.config/quickshell/scripts/panama-kdeconnect") def _status(): """What panama-kdeconnect reports, or None when it cannot be asked.""" try: result = subprocess.run( [HELPER, "status"], capture_output=True, text=True, timeout=4, check=False ) return json.loads(result.stdout) if result.returncode == 0 else None except (OSError, ValueError, subprocess.SubprocessError): return None def _local_path(file_info): """A real filesystem path, or None for anything that is not one. Nautilus hands out URIs, and a remote or trashed file has one that no amount of unquoting turns into something sendable. """ uri = file_info.get_uri() parsed = urlparse(uri) if parsed.scheme != "file": return None path = unquote(parsed.path) return path if os.path.isfile(path) else None class PanamaShareExtension(GObject.GObject, Nautilus.MenuProvider): def __init__(self): super().__init__() Notify.init("Panama") def get_file_items(self, files): # One file at a time. KDE Connect's --share takes a single path, and # looping it for a selection of forty would be forty transfers and # forty notifications on the phone. if len(files) != 1: return [] path = _local_path(files[0]) if path is None: return [] status = _status() if not status or not status.get("devices"): return [] device = next( (d for d in status["devices"] if d.get("reachable") and d.get("id")), None ) if device is None: return [] item = Nautilus.MenuItem( name="Panama::SendToPhone", label=f"Send to {device.get('name') or 'phone'}", tip=f"Send this file to {device.get('name') or 'your phone'} with KDE Connect", ) item.connect("activate", self._send, path, device["id"], device.get("name")) return [item] def _send(self, _menu, path, device_id, device_name): try: result = subprocess.run( [HELPER, "send-file", device_id, path], capture_output=True, text=True, timeout=30, check=False, ) ok = result.returncode == 0 except (OSError, subprocess.SubprocessError): ok = False name = os.path.basename(path) target = device_name or "your phone" notification = Notify.Notification.new( "Sent" if ok else "Could not send", f"{name} → {target}" if ok else f"{name} did not reach {target}", "phone-symbolic" if ok else "dialog-error-symbolic", ) try: notification.show() except GObject.GError: pass