Skip to content

Commit

Permalink
Add warpinator-send utility.
Browse files Browse the repository at this point in the history
- Add minimal dbus interface for getting a list of remote machines
  and sending files
- if a remote identity string is passed to warpinator-send, the
  file will be sent immediately.
- If no remote is supplied, a popup will allow you to select the
  target remote.
- Added nemo action.
- A notification is spawned for the sender when files are sent in
  this manner
- A window id can be provided to the warpinator-send utility, to
  allow the interactive window to be a transient child.
  • Loading branch information
mtwebster committed Mar 5, 2023
1 parent 8da5441 commit 1fb784f
Show file tree
Hide file tree
Showing 14 changed files with 478 additions and 7 deletions.
9 changes: 8 additions & 1 deletion bin/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,12 @@ bin_file = configure_file(
output: 'warpinator',
configuration: prefix_info,
)

install_data(bin_file, install_dir: get_option('bindir'))


send_bin_file = configure_file(
input : 'warpinator-send.in',
output: 'warpinator-send',
configuration: prefix_info,
)
install_data(send_bin_file, install_dir: get_option('bindir'))
247 changes: 247 additions & 0 deletions bin/warpinator-send.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
#!/usr/bin/python3

import signal
import argparse
import gettext
import setproctitle
import json
import os
import sys
import locale
import functools

import gi
gi.require_version('Gtk', '3.0')

from gi.repository import GLib, Gio, Gtk, Gdk, GdkX11

src = os.path.join("@prefix@", "libexec/warpinator/")
sys.path.insert(0, src)

import config
import dbus_service
import util

signal.signal(signal.SIGINT, signal.SIG_DFL)

# i18n
locale.bindtextdomain(config.PACKAGE, config.localedir)
gettext.bindtextdomain(config.PACKAGE, config.localedir)
gettext.textdomain(config.PACKAGE)
_ = gettext.gettext

setproctitle.setproctitle("warpinator-send")

class WarpinatorSender:
"""
This is a standalone executable that provides a simple way
of controlling the screensaver via its dbus interface.
"""
def __init__(self):
self.proxy = None
self.ecode = 0

parser = argparse.ArgumentParser(description='warpinator-send')
parser.add_argument("--list-remotes", dest="list_remotes", action='store_true',
help="List online remotes as JSON (any other arguments are ignored)")
parser.add_argument('--remote-ident', dest="ident", action='store', default="",
help="Destination remote's UUID/identity, as retrieved by --list-remotes. If omitted, you will be prompted for a destination.")
parser.add_argument("--xid", dest="parent_window", action='store',
help="Window ID of the calling application (optional)")
parser.add_argument("uris", type=str, nargs="*",
help="List of file URIs to be sent (required unless '--list-remotes' is called).")

args = parser.parse_args()

if not args.list_remotes and len(args.uris) == 0:
print("Must have at least 1 uri if --get-live-remotes is not set", file=sys.stderr)
parser.print_help()
exit(1)

self.ident = args.ident
self.files = args.uris
self.list_remotes = args.list_remotes

if args.parent_window:
self.parent_window = int(args.parent_window, 0) # maybe hex, detect the base
else:
self.parent_window = None

self.popup = None

Gio.DBusProxy.new_for_bus(
Gio.BusType.SESSION,
Gio.DBusProxyFlags.DO_NOT_AUTO_START,
dbus_service.interface_node_info.interfaces[0],
"org.x.Warpinator",
"/org/x/Warpinator",
"org.x.Warpinator",
None,
self._on_proxy_ready
)

def _on_proxy_ready(self, object, result, data=None):
try:
self.proxy = Gio.DBusProxy.new_for_bus_finish(result);
except GLib.Error as e:
print("Can't create warpinator proxy (%d): %s" % (e.code, e.message), file=sys.stderr)
self.ecode = 1
Gtk.main_quit()

self.handle_command()

def handle_command(self):
if self.list_remotes:
self.proxy.call(
"ListRemotes",
None,
Gio.DBusCallFlags.NO_AUTO_START,
-1,
None,
self.list_remotes_command_finished
)
elif self.ident != "":
self.send_files_to(self.ident)
else:
self.proxy.call(
"ListRemotes",
None,
Gio.DBusCallFlags.NO_AUTO_START,
-1,
None,
self.list_remotes_for_send_finished
)

def send_files_command_finished(self, source, res, data=None):
try:
var = source.call_finish(res)
except GLib.Error as e:
print("Could not send files: %s" % str(e), file=sys.stderr)
self.ecode = 1

Gtk.main_quit()

def list_remotes_command_finished(self, source, res, data=None):
try:
var = source.call_finish(res)
remote_list = var[0]

dump = json.dumps(remote_list, indent=2)
print(dump, file=sys.stdout)
except GLib.Error as e:
print("Could not list remotes: %s" % str(e), file=sys.stderr)
self.ecode = 1

Gtk.main_quit()

def list_remotes_for_send_finished(self, source, res, data=None):
try:
var = source.call_finish(res)
remote_list = var[0]
except GLib.Error as e:
print("Could not list remotes to ask user: %s" % str(e), file=sys.stderr)
self.ecode = 1
Gtk.main_quit()
return

popup = Gtk.Window(type_hint=Gdk.WindowTypeHint.POPUP_MENU,
destroy_with_parent=True,

resizable=False)

if self.parent_window is None:
popup.set_keep_above(True)

hb = Gtk.HeaderBar(title="Warpinator",
subtitle=_("Choose a recipient..."),
show_close_button=True)
popup.set_titlebar(hb)

seat = Gdk.Display.get_default().get_default_seat()
pointer = seat.get_pointer()
screen, x, y = pointer.get_position()

popup.move(x + 5, y + 5)

remote_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, margin=4)
popup.add(remote_box)

button_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4, homogeneous=True)
remote_box.pack_start(button_box, True, True, 0)

class RemoteInfo:
def __init__(self, remote):
self.uuid = remote["uuid"]
self.display_name = remote["display-name"]
self.username = remote["username"]
self.hostname = remote["hostname"]
self.favorite = remote["favorite"]
self.recent_time = remote["recent-time"]

infos = []
for remote in remote_list:
infos.append(RemoteInfo(remote))

sorted_remotes = sorted(infos, key=functools.cmp_to_key(util.sort_remote_machines))

for info in sorted_remotes:
label = "<b>%s</b> %s@%s" % (info.display_name, info.username, info.hostname)

button = Gtk.Button(always_show_image=True, image_position=Gtk.PositionType.RIGHT)

if info.favorite:
image = Gtk.Image.new_from_icon_name("starred-symbolic", Gtk.IconSize.BUTTON)
elif info.recent_time > 0:
image = Gtk.Image.new_from_icon_name("document-open-recent-symbolic", Gtk.IconSize.BUTTON)
else:
image = None

box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
box.pack_start(Gtk.Label(label=label, use_markup=True), True, True, 6)
if image is not None:
box.pack_end(image, False, False, 6)
box.show_all()

button.set_image(box)
button_box.add(button)
button.connect("clicked", self.remote_selected, info.uuid)

self.popup = popup
self.popup.connect("delete-event", self.on_delete_popup)
self.popup.connect("realize", self.on_popup_realize)
self.popup.show_all()
self.popup.present()

def on_popup_realize(self, window):
if self.parent_window:
parent = GdkX11.X11Window.foreign_new_for_display(Gdk.Display.get_default(), self.parent_window)
if parent is not None:
window.get_window().set_transient_for(parent)

def on_delete_popup(self, window, event, data=None):
self.ecode = 1
Gtk.main_quit()

return Gdk.EVENT_PROPAGATE

def remote_selected(self, button, ident):
self.popup.destroy()
self.send_files_to(ident)

def send_files_to(self, ident):
files_var = GLib.Variant.new_array(None, [GLib.Variant.new_string(uri) for uri in self.files])
full_var = GLib.Variant("(sas)", [ident, files_var])

self.proxy.call(
"SendFiles",
full_var,
Gio.DBusCallFlags.NO_AUTO_START,
-1,
None,
self.send_files_command_finished
)

if __name__ == "__main__":
sender = WarpinatorSender()
Gtk.main()
exit(sender.ecode)
13 changes: 13 additions & 0 deletions data/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,17 @@ appdata = i18n.merge_file(
po_dir: join_paths(meson_source_root, 'po'),
install_dir: join_paths(get_option('datadir'), 'metainfo'),
install: true,
)

nemo_action = i18n.merge_file(
input: 'warpinator-send.nemo_action.in',
output: 'warpinator-send.nemo_action',
type: 'desktop',
po_dir: join_paths(meson_source_root, 'po'),
install_dir: join_paths(get_option('datadir'), 'nemo', 'actions'),
install: true,
)

install_data('warpinator-send-check',
install_dir: join_paths(get_option('datadir'), 'nemo', 'actions')
)
15 changes: 15 additions & 0 deletions data/org.x.Warpinator.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"https://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<!-- GDBus 2.48.1 -->
<node>
<interface name='org.x.Warpinator'>
<method name='ListRemotes'>
<!-- ident, display_name, user_name, hostname -->
<arg type='a(ssss)' name='remotes' direction='out'/>
</method>
<method name='SendFiles'>
<arg type='s' name='remote_uuid' direction='in'/>
<arg type='as' name='uri_list' direction='in'/>
</method>
</interface>
</node>
29 changes: 29 additions & 0 deletions data/warpinator-send-check
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/python3

import sys
import json
import subprocess

from gi.repository import Gio, GLib

try:
reply = Gio.DBusConnection.call_sync(
Gio.bus_get_sync(Gio.BusType.SESSION, None),
"org.x.Warpinator",
"/org/x/Warpinator",
"org.x.Warpinator",
"ListRemotes",
None,
GLib.VariantType("(aa{sv})"),
Gio.DBusCallFlags.NO_AUTO_START,
2000,
None
)

remote_list = reply[0]
exit(0 if len(remote_list) > 0 else 1)
except GLib.Error as e:
if e.code != Gio.DBusError.NAME_HAS_NO_OWNER:
print("warpinator-send-check error %d: %s" % (e.code, e.message), file=sys.stderr)

exit(1)
9 changes: 9 additions & 0 deletions data/warpinator-send.nemo_action.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[Nemo Action]
Active=true
Name=Send with Warpinator
Comment=Send a file to someone using Warpinator
Exec=warpinator-send --xid %X %U
Selection=notnone
Extensions=any
Icon-Name=org.x.Warpinator-symbolic
Conditions=exec <warpinator-send-check>;
2 changes: 2 additions & 0 deletions install-scripts/meson_install_bin_script.sh
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#!/bin/sh

chmod a+x "${DESTDIR}/${MESON_INSTALL_PREFIX}/bin/warpinator"
chmod a+x "${DESTDIR}/${MESON_INSTALL_PREFIX}/bin/warpinator-send"
chmod a+x "${DESTDIR}/${MESON_INSTALL_PREFIX}/share/nemo/actions/warpinator-send-check"
2 changes: 2 additions & 0 deletions makepot
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ xgettext --package-name=warpinator --language=Python -c --join-existing --add-co
--output=warpinator.pot --files-from=src/gettext_files
xgettext --package-name=warpinator --language=Desktop --join-existing \
-k --keyword=Comment --output=warpinator.pot data/org.x.Warpinator.desktop.in.in
xgettext --package-name=warpinator --language=Desktop --join-existing \
--output=warpinator.pot data/warpinator-send.nemo_action.in
xgettext --package-name=warpinator --its=/usr/share/gettext/its/polkit.its --join-existing --add-comments \
--output=warpinator.pot data/org.x.warpinator.policy.in.in
xgettext --package-name=warpinator --its=/usr/share/gettext/its/metainfo.its --join-existing --add-comments \
Expand Down
Loading

0 comments on commit 1fb784f

Please sign in to comment.