Sharing a file or folder over LocalSend opened a terminal to run an fzf pick over a find of the whole home directory, which is slow on a large home, shows no previews, and looks nothing like the rest of the desktop. The portal chooser is already how the other pickers here ask. The chooser has a directory mode, so folder sharing asks for one the same way, and neither entry needs a terminal to host a picker anymore. A chooser that never opens is told apart from nobody picking anything, so a portal failure says so rather than passing for a cancelled share. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
125 lines
4.4 KiB
Python
Executable File
125 lines
4.4 KiB
Python
Executable File
#!/usr/bin/python3
|
|
|
|
# omarchy:summary=Pick files with the desktop file chooser
|
|
# omarchy:args=[--title <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)
|