#!/usr/bin/python3 # omarchy:summary=Pick files with the desktop file chooser # omarchy:args=[--title ] [--multiple] [--directory] [--extensions "<ext ext...>"] # omarchy:examples=omarchy file select --title "Send with Tailscale" --multiple | omarchy file select --title "Pick image" --extensions "png svg" | omarchy file select --title "Share folder" --directory # Python rather than bash, alone among the commands here, because the portal # answers a request with a Response signal addressed to the connection that # asked, and D-Bus delivers a directed signal only to that connection. Every # shell-callable client — gdbus call, busctl call, dbus-send — opens its own # connection and exits before the answer arrives, and gdbus monitor registers # with AddMatch rather than BecomeMonitor, so it never sees one either. Holding # a single connection across both the call and the wait is the whole job, and # bash has no way to hold one. import argparse import os import sys import gi gi.require_version("Gio", "2.0") from gi.repository import Gio, GLib # A dialog nobody ever answers would otherwise keep this process, and whatever # waits on its output, alive forever. ANSWER_TIMEOUT_SEC = 600 # Callers act on these: nothing picked is a decision, a chooser that never ran # is a fault, and the two want different handling. EXIT_NOTHING_PICKED = 1 EXIT_CHOOSER_FAILED = 2 def main(): parser = argparse.ArgumentParser(add_help=False) parser.add_argument("--title", default="Select file") parser.add_argument("--multiple", action="store_true") parser.add_argument("--directory", action="store_true") parser.add_argument("--extensions", default="") args, unknown = parser.parse_known_args() if unknown: print("omarchy-file-select: unknown option %s" % unknown[0], file=sys.stderr) return EXIT_CHOOSER_FAILED bus = Gio.bus_get_sync(Gio.BusType.SESSION, None) loop = GLib.MainLoop() uris = [] def on_response(connection, sender, path, interface, signal, params): code, results = params.unpack() if code == 0: uris.extend(results.get("uris", [])) loop.quit() def subscribe(path): bus.signal_subscribe( "org.freedesktop.portal.Desktop", "org.freedesktop.portal.Request", "Response", path, None, Gio.DBusSignalFlags.NONE, on_response, ) # The request path is derived from our bus name and the token we pass, so it # can be subscribed to up front. Asking first would race a dialog that gets # answered immediately. token = "omarchy%d" % os.getpid() sender = bus.get_unique_name()[1:].replace(".", "_") predicted = "/org/freedesktop/portal/desktop/request/%s/%s" % (sender, token) subscribe(predicted) options = { "handle_token": GLib.Variant("s", token), "multiple": GLib.Variant("b", args.multiple), } if args.directory: options["directory"] = GLib.Variant("b", True) # Filters name file formats, which a directory chooser has no use for. if args.extensions and not args.directory: # Glob matching in the chooser is case-sensitive, so cover both cases. exts = [ext.lstrip(".").lower() for ext in args.extensions.split()] patterns = [(0, "*." + ext) for ext in exts] + [(0, "*." + ext.upper()) for ext in exts] label = " ".join("*." + ext for ext in exts) filters = GLib.Variant("a(sa(us))", [(label, patterns)]) options["filters"] = filters options["current_filter"] = GLib.Variant("(sa(us))", (label, patterns)) handle = bus.call_sync( "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", "org.freedesktop.portal.FileChooser", "OpenFile", GLib.Variant("(ssa{sv})", ("", args.title, options)), None, Gio.DBusCallFlags.NONE, -1, None, ).unpack()[0] # Portals predating the token convention answer on a path of their choosing. if handle != predicted: subscribe(handle) GLib.timeout_add_seconds(ANSWER_TIMEOUT_SEC, loop.quit) loop.run() for uri in uris: print(GLib.filename_from_uri(uri)[0]) return 0 if uris else EXIT_NOTHING_PICKED if __name__ == "__main__": try: sys.exit(main()) except GLib.Error as error: print("omarchy-file-select: %s" % error.message, file=sys.stderr) sys.exit(EXIT_CHOOSER_FAILED)