diff --git a/docs/reference/gio/Makefile.am b/docs/reference/gio/Makefile.am index 112626b3f..42907caef 100644 --- a/docs/reference/gio/Makefile.am +++ b/docs/reference/gio/Makefile.am @@ -146,6 +146,7 @@ man_MANS = if ENABLE_MAN man_MANS += \ + gapplication.1 \ gio-querymodules.1 \ glib-compile-schemas.1 \ glib-compile-resources.1 \ diff --git a/docs/reference/gio/gapplication.xml b/docs/reference/gio/gapplication.xml new file mode 100644 index 000000000..13e3f2300 --- /dev/null +++ b/docs/reference/gio/gapplication.xml @@ -0,0 +1,352 @@ + + + gapplication + GIO + + + Developer + Ryan + Lortie + + + + + + gapplication + 1 + User Commands + + + + gapplication + D-Bus application launcher + + + + + gapplication + help + COMMAND + + + gapplication + version + + + gapplication + list-apps + + + gapplication + launch + APPID + + + gapplication + launch + APPID + FILE + + + gapplication + list-actions + APPID + + + gapplication + action + APPID + ACTION + PARAMETER + + + + + Description + + + gapplication is a commandline implementation of the client-side of the + org.freedesktop.Application interface as specified by the freedesktop.org + Desktop Entry Specification. + + + + gapplication can be used to start applications that have + DBusActivatable set to true in their .desktop + files and can be used to send messages to already-running instances of other applications. + + + + It is possible for applications to refer to gapplication in the Exec + line of their .desktop file to maintain backwards compatibility + with implementations that do not directly support DBusActivatable. + + + + gapplication ships as part of GLib. + + + + + Commands + + + Global commands + + + + + help + COMMAND + + + + Displays a short synopsis of the available commands or provides detailed help on a specific + command. + + + + + + + version + + + + Prints the GLib version whence gapplication came. + + + + + + + list-apps + + + + Prints a list of all application IDs that are known to support D-Bus activation. This list is + generated by scanning .desktop files as per the current + XDG_DATA_DIRS. + + + + + + + launch + APPID + FILE + + + + Launches an application. + + + The first parameter is the application ID in the familiar "reverse DNS" style (eg: + 'org.gnome.app') without the .desktop + suffix. + + + Optionally, if additional parameters are given, they are treated as the names of files to open and + may be filenames or URIs. If no files are given then the application is simply activated. + + + + + + + list-actions + APPID + + + + List the actions declared in the application's .desktop + file. The parameter is the application ID, as above. + + + + + + + action + APPID + ACTION + PARAMETER + + + + Invokes the named action (in the same way as would occur when activating an action specified in + the .desktop file). + + + The application ID (as above) is the first parameter. The action name follows. + + + Optionally, following the action name can be one parameter, in GVariant format, given as a single + argument. Make sure to use sufficient quoting. + + + + + + + + + + Examples + + + From the commandline + + + Launching an application: + + + + gapplication launch org.example.fooview + + + + Opening a file with an application: + + + + gapplication launch org.example.fooview ~/file.foo + + + + Opening many files with an application: + + + + gapplication launch org.example.fooview ~/foos/*.foo + + + + Invoking an action on an application: + + + + gapplication action org.example.fooview create + + + + Invoking an action on an application, with an action: + + + + gapplication action org.example.fooview show-item '"item_id_828739"' + + + + + + From the <varname>Exec</varname> lines of a <filename class='extension'>.desktop</filename> file + + + + The commandline interface of gapplication was designed so that it could be used + directly from the Exec line of a .desktop + file. + + + + You might want to do this to allow for backwards compatibility with implementations of the specification + that do not understand how to do D-Bus activation, without having to install a separate utility program. + + + + Consider the following example: + + + + [Desktop Entry] + Version=1.1 + Type=Application + Name=Foo Viewer + DBusActivatable=true + MimeType=image/x-foo; + Exec=gapplication launch org.example.fooview %F + Actions=gallery;create; + + [Desktop Action gallery] + Name=Browse Gallery + Exec=gapplication action org.example.fooview gallery + + [Desktop Action create] + Name=Create a new Foo! + Exec=gapplication action org.example.fooview create + + + + + From a script + + + If installing an application that supports D-Bus activation you may still want to put a file in + /usr/bin so that your program can be started from a terminal. + + + + It is possible for this file to be a shell script. The script can handle arguments such as --help and + --version directly. It can also parse other command line arguments and convert them to uses of + gapplication to activate the application, open files, or invoke actions. + + + + Here is a simplified example, as may be installed in /usr/bin/fooview: + + + + #!/bin/sh + + case "$1" in + --help) + echo "see 'man fooview' for more information" + ;; + + --version) + echo "fooview 1.2" + ;; + + --gallery) + gapplication action org.example.fooview gallery + ;; + + --create) + gapplication action org.example.fooview create + ;; + + -*) + echo "unrecognised commandline argument" + exit 1 + ;; + + *) + gapplication launch org.example.fooview "$@" + ;; + esac + + + + + + See also + + Desktop Entry Specification, + + gdbus + 1 + , + + xdg-open + 1 + , + + desktop-file-validate + 1 + + + + + diff --git a/gio/.gitignore b/gio/.gitignore index be25b26e7..5aa5588f3 100644 --- a/gio/.gitignore +++ b/gio/.gitignore @@ -14,3 +14,4 @@ gnetworking.h gresource gschema-compile gsettings +gapplication diff --git a/gio/Makefile.am b/gio/Makefile.am index ba596e502..7d2496626 100644 --- a/gio/Makefile.am +++ b/gio/Makefile.am @@ -720,8 +720,17 @@ gdbus_LDADD = libgio-2.0.la \ $(top_builddir)/glib/libglib-2.0.la \ $(top_builddir)/gobject/libgobject-2.0.la +# ------------------------------------------------------------------------ +# gapplication(1) tool +bin_PROGRAMS += gapplication +gapplication_SOURCES = gapplication-tool.c +gapplication_LDADD = libgio-2.0.la \ + $(top_builddir)/glib/libglib-2.0.la \ + $(top_builddir)/gobject/libgobject-2.0.la + completiondir = $(datadir)/bash-completion/completions completion_DATA = \ + completion/gapplication \ completion/gdbus \ completion/gsettings \ completion/gresource diff --git a/gio/completion/gapplication b/gio/completion/gapplication new file mode 100644 index 000000000..565025bd8 --- /dev/null +++ b/gio/completion/gapplication @@ -0,0 +1,55 @@ + +# Check for bash +[ -z "$BASH_VERSION" ] && return + +#################################################################################################### + +__app() { + case "${COMP_CWORD}" in + 1) + COMPREPLY=($(compgen -W "help version list-apps launch action list-actions" -- "${COMP_WORDS[1]}")) + return 0 + ;; + + 2) + case "${COMP_WORDS[1]}" in + launch|action|list-actions) + COMPREPLY=($(compgen -W "`gapplication list-apps`" -- "${COMP_WORDS[2]}")) + return 0 + ;; + + *) + COMPREPLY=() + return 0 + ;; + esac + ;; + esac + + # Otherwise, what we will do is based on the command in ${COMP_WORDS[1]} + case "${COMP_WORDS[1]}" in + action) + # Word 3 is the action name. This is the only one we can help with. + if [ "${COMP_CWORD}" == 3 ]; then + COMPREPLY=($(compgen -W "`gapplication list-actions "${COMP_WORDS[2]}"`" -- "${COMP_WORDS[3]}")) + return 0 + else + COMPREPLY=() + return 0 + fi + ;; + launch) + # Filenames... + COMPREPLY=($(compgen -A file "${COMP_WORDS[COMP_CWORD]}")) + return 0 + ;; + *) + # Nothing else should be out this far... + COMPREPLY=() + return 0 + esac +} + +#################################################################################################### + +complete -F __app gapplication diff --git a/gio/gapplication-tool.c b/gio/gapplication-tool.c new file mode 100644 index 000000000..c1529090e --- /dev/null +++ b/gio/gapplication-tool.c @@ -0,0 +1,463 @@ +/* + * Copyright © 2013 Canonical Limited + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the licence, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Author: Ryan Lortie + */ + +#include "config.h" + +#include + +#include +#include + +#include +#include + +struct help_topic +{ + const gchar *command; + const gchar *summary; + const gchar *description; + const gchar *synopsis; +}; + +struct help_substvar +{ + const gchar *var; + const gchar *description; +}; + +static const struct help_topic topics[] = { + { "help", N_("Print help"), + N_("Print help"), + N_("[COMMAND]") + }, + { "version", N_("Print version"), + N_("Print version information and exit") + }, + { "list-apps", N_("List applications"), + N_("List the installed D-Bus activatable applications (by .desktop files)") + }, + { "launch", N_("Launch an application"), + N_("Launch the application (with optional files to open)"), + N_("APPID [FILE...]") + }, + { "action", N_("Activate an action"), + N_("Invoke an action on the application"), + N_("APPID ACTION [PARAMETER]") + }, + { "list-actions", N_("List available actions"), + N_("List static actions for an application (from .desktop file)"), + N_("APPID") + } +}; + +static const struct help_substvar substvars[] = { + { N_("COMMAND"), N_("The command to print detailed help for") }, + { N_("APPID"), N_("Application identifier in D-Bus format (eg: org.example.viewer)") }, + { N_("FILE"), N_("Optional relative or relative filenames, or URIs to open") }, + { N_("ACTION"), N_("The action name to invoke") }, + { N_("PARAMETER"), N_("Optional parameter to the action invocation, in GVariant format") } +}; + +static int +app_help (gboolean requested, + const gchar *command) +{ + const struct help_topic *topic = NULL; + GString *string; + + string = g_string_new (NULL); + + if (command) + { + gint i; + + for (i = 0; i < G_N_ELEMENTS (topics); i++) + if (g_str_equal (topics[i].command, command)) + topic = &topics[i]; + + if (!topic) + { + g_string_printf (string, _("Unknown command %s\n\n"), command); + requested = FALSE; + } + } + + g_string_append (string, _("Usage:\n")); + + if (topic) + { + gint maxwidth; + gint i; + + g_string_append_printf (string, "\n %s %s %s\n\n", "gapplication", + topic->command, topic->synopsis ? _(topic->synopsis) : ""); + g_string_append_printf (string, "%s\n\n", _(topic->description)); + + if (topic->synopsis) + { + g_string_append (string, _("Arguments:\n")); + + maxwidth = 0; + for (i = 0; i < G_N_ELEMENTS (substvars); i++) + if (strstr (topic->synopsis, substvars[i].var)) + maxwidth = MAX(maxwidth, strlen (_(substvars[i].var))); + + for (i = 0; i < G_N_ELEMENTS (substvars); i++) + if (strstr (topic->synopsis, substvars[i].var)) + g_string_append_printf (string, " %-*.*s %s\n", maxwidth, maxwidth, + _(substvars[i].var), _(substvars[i].description)); + g_string_append (string, "\n"); + } + } + else + { + gint maxwidth; + gint i; + + g_string_append_printf (string, "\n %s %s %s\n\n", "gapplication", _("COMMAND"), _("[ARGS...]")); + g_string_append_printf (string, _("Commands:\n")); + + maxwidth = 0; + for (i = 0; i < G_N_ELEMENTS (topics); i++) + maxwidth = MAX(maxwidth, strlen (topics[i].command)); + + for (i = 0; i < G_N_ELEMENTS (topics); i++) + g_string_append_printf (string, " %-*.*s %s\n", maxwidth, maxwidth, + topics[i].command, _(topics[i].summary)); + + g_string_append (string, "\n"); + /* Translators: do not translate 'help', but please translate 'COMMAND'. */ + g_string_append_printf (string, _("Use '%s help COMMAND' to get detailed help.\n\n"), "gapplication"); + } + + if (requested) + g_print ("%s", string->str); + else + g_printerr ("%s\n", string->str); + + g_string_free (string, TRUE); + + return requested ? 0 : 1; +} + +static gboolean +app_check_name (gchar **args, + const gchar *command) +{ + if (args[0] == NULL) + { + g_printerr (_("%s command requires an application id to directly follow\n\n"), command); + return FALSE; + } + + if (!g_dbus_is_name (args[0])) + { + g_printerr (_("invalid application id: '%s'\n"), args[0]); + return FALSE; + } + + return TRUE; +} + +static int +app_no_args (const gchar *command) +{ + /* Translators: %s is replaced with a command name like 'list-actions' */ + g_printerr (_("'%s' takes no arguments\n\n"), command); + return app_help (FALSE, command); +} + +static int +app_version (gchar **args) +{ + if (g_strv_length (args)) + return app_no_args ("version"); + + g_print (PACKAGE_VERSION "\n"); + return 0; +} + +static int +app_list (gchar **args) +{ + GList *apps; + + if (g_strv_length (args)) + return app_no_args ("list"); + + apps = g_app_info_get_all (); + + while (apps) + { + GDesktopAppInfo *info = apps->data; + + if (G_IS_DESKTOP_APP_INFO (info)) + if (g_desktop_app_info_get_boolean (info, "DBusActivatable")) + { + const gchar *filename; + + filename = g_app_info_get_id (G_APP_INFO (info)); + if (g_str_has_suffix (filename, ".desktop")) + { + gchar *id; + + id = g_strndup (filename, strlen (filename) - 8); + g_print ("%s\n", id); + g_free (id); + } + } + + apps = g_list_delete_link (apps, apps); + g_object_unref (info); + } + + return 0; +} + +static gchar * +app_path_for_id (const gchar *app_id) +{ + gchar *path; + gint i; + + path = g_strconcat ("/", app_id, NULL); + for (i = 0; path[i]; i++) + if (path[i] == '.') + path[i] = '/'; + + return path; +} + +static int +app_call (const gchar *app_id, + const gchar *method_name, + GVariant *parameters) +{ + GDBusConnection *session; + GError *error = NULL; + gchar *object_path; + GVariant *result; + + + session = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); + if (!session) + { + g_variant_unref (g_variant_ref_sink (parameters)); + g_printerr (_("unable to connect to D-Bus: %s\n"), error->message); + g_error_free (error); + return 1; + } + + object_path = app_path_for_id (app_id); + + result = g_dbus_connection_call_sync (session, app_id, object_path, "org.freedesktop.Application", + method_name, parameters, G_VARIANT_TYPE_UNIT, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error); + + g_free (object_path); + + if (result) + { + g_variant_unref (result); + return 0; + } + else + { + g_printerr (_("error sending %s message to application: %s\n"), method_name, error->message); + g_error_free (error); + return 1; + } +} + +static GVariant * +app_get_platform_data (void) +{ + GVariantBuilder builder; + const gchar *startup_id; + + g_variant_builder_init (&builder, G_VARIANT_TYPE_VARDICT); + + if ((startup_id = g_getenv ("DESKTOP_STARTUP_iD"))) + g_variant_builder_add (&builder, "{sv}", "desktop-startup-id", g_variant_new_string (startup_id)); + + return g_variant_builder_end (&builder); +} + +static int +app_action (gchar **args) +{ + GVariantBuilder params; + const gchar *name; + + if (!app_check_name (args, "action")) + return 1; + + if (args[1] == NULL) + { + g_printerr (_("action name must be given after application id\n")); + return 1; + } + + name = args[1]; + + if (!g_action_name_is_valid (name)) + { + g_printerr (_("invalid action name: '%s'\n" + "action names must consist of only alphanumerics, '-' and '.'\n"), name); + return 1; + } + + g_variant_builder_init (¶ms, G_VARIANT_TYPE ("av")); + + if (args[2]) + { + GError *error = NULL; + GVariant *parameter; + + parameter = g_variant_parse (NULL, args[2], NULL, NULL, &error); + + if (!parameter) + { + g_printerr (_("error parsing action parameter: %s\n"), error->message); + g_variant_builder_clear (¶ms); + g_error_free (error); + return 1; + } + + g_variant_builder_add (¶ms, "v", parameter); + g_variant_unref (parameter); + + if (args[3]) + { + g_printerr (_("actions accept a maximum of one parameter\n")); + g_variant_builder_clear (¶ms); + return 1; + } + } + + return app_call (args[0], "ActivateAction", g_variant_new ("(sav@a{sv})", name, ¶ms, app_get_platform_data ())); +} + +static int +app_activate (const gchar *app_id) +{ + return app_call (app_id, "Activate", g_variant_new ("(@a{sv})", app_get_platform_data ())); +} + +static int +app_launch (gchar **args) +{ + GVariantBuilder files; + gint i; + + if (!app_check_name (args, "launch")) + return 1; + + if (args[1] == NULL) + return app_activate (args[0]); + + g_variant_builder_init (&files, G_VARIANT_TYPE_STRING_ARRAY); + + for (i = 1; args[i]; i++) + { + GFile *file; + + /* "This operation never fails" */ + file = g_file_new_for_commandline_arg (args[i]); + g_variant_builder_add_value (&files, g_variant_new_take_string (g_file_get_uri (file))); + g_object_unref (file); + } + + return app_call (args[0], "Open", g_variant_new ("(as@a{sv})", &files, app_get_platform_data ())); +} + +static int +app_list_actions (gchar **args) +{ + const gchar * const *actions; + GDesktopAppInfo *app_info; + gchar *filename; + gint i; + + if (!app_check_name (args, "list-actions")) + return 1; + + if (args[1]) + { + g_printerr (_("list-actions command takes only the application id")); + app_help (FALSE, "list-actions"); + } + + filename = g_strconcat (args[0], ".desktop", NULL); + app_info = g_desktop_app_info_new (filename); + g_free (filename); + + if (app_info == NULL) + { + g_printerr (_("unable to find desktop file for application %s\n"), args[0]); + return 1; + } + + actions = g_desktop_app_info_list_actions (app_info); + + for (i = 0; actions[i]; i++) + g_print ("%s\n", actions[i]); + + g_object_unref (app_info); + + return 0; +} + +int +main (int argc, char **argv) +{ + setlocale (LC_ALL, ""); + textdomain (GETTEXT_PACKAGE); + bindtextdomain (GETTEXT_PACKAGE, GLIB_LOCALE_DIR); +#ifdef HAVE_BIND_TEXTDOMAIN_CODESET + bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); +#endif + + if (argc < 2) + return app_help (TRUE, NULL); + + if (g_str_equal (argv[1], "help")) + return app_help (TRUE, argv[2]); + + if (g_str_equal (argv[1], "version")) + return app_version (argv + 2); + + if (g_str_equal (argv[1], "list-apps")) + return app_list (argv + 2); + + if (g_str_equal (argv[1], "launch")) + return app_launch (argv + 2); + + if (g_str_equal (argv[1], "action")) + return app_action (argv + 2); + + if (g_str_equal (argv[1], "list-actions")) + return app_list_actions (argv + 2); + + g_printerr (_("unrecognised command: %s\n\n"), argv[1]); + + return app_help (FALSE, NULL); +}