summaryrefslogtreecommitdiffstats
path: root/src/terminal-util.cc
diff options
context:
space:
mode:
Diffstat (limited to 'src/terminal-util.cc')
-rw-r--r--src/terminal-util.cc1945
1 files changed, 1945 insertions, 0 deletions
diff --git a/src/terminal-util.cc b/src/terminal-util.cc
new file mode 100644
index 0000000..3ee8f51
--- /dev/null
+++ b/src/terminal-util.cc
@@ -0,0 +1,1945 @@
+/*
+ * Copyright © 2001, 2002 Havoc Pennington
+ * Copyright © 2002 Red Hat, Inc.
+ * Copyright © 2002 Sun Microsystems
+ * Copyright © 2003 Mariano Suarez-Alvarez
+ * Copyright © 2008, 2011 Christian Persch
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <fcntl.h>
+#include <string.h>
+#include <stdlib.h>
+#include <time.h>
+#include <unistd.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <langinfo.h>
+#include <errno.h>
+
+#include <glib.h>
+#include <glib/gi18n.h>
+
+#include <gio/gio.h>
+#include <gtk/gtk.h>
+
+#include <gdesktop-enums.h>
+
+#include "terminal-accels.hh"
+#include "terminal-app.hh"
+#include "terminal-client-utils.hh"
+#include "terminal-debug.hh"
+#include "terminal-defines.hh"
+#include "terminal-intl.hh"
+#include "terminal-util.hh"
+#include "terminal-version.hh"
+#include "terminal-libgsystem.hh"
+
+/**
+ * terminal_util_show_error_dialog:
+ * @transient_parent: parent of the future dialog window;
+ * @weap_ptr: pointer to a #Widget pointer, to control the population.
+ * @error: a #GError, or %nullptr
+ * @message_format: printf() style format string
+ *
+ * Create a #GtkMessageDialog window with the message, and present it, handling its buttons.
+ * If @weap_ptr is not #nullptr, only create the dialog if <literal>*weap_ptr</literal> is #nullptr
+ * (and in that * case, set @weap_ptr to be a weak pointer to the new dialog), otherwise just
+ * present <literal>*weak_ptr</literal>. Note that in this last case, the message <emph>will</emph>
+ * be changed.
+ */
+void
+terminal_util_show_error_dialog (GtkWindow *transient_parent,
+ GtkWidget **weak_ptr,
+ GError *error,
+ const char *message_format,
+ ...)
+{
+ gs_free char *message;
+ va_list args;
+
+ if (message_format)
+ {
+ va_start (args, message_format);
+ message = g_strdup_vprintf (message_format, args);
+ va_end (args);
+ }
+ else message = nullptr;
+
+ if (weak_ptr == nullptr || *weak_ptr == nullptr)
+ {
+ GtkWidget *dialog;
+ dialog = gtk_message_dialog_new (transient_parent,
+ GTK_DIALOG_DESTROY_WITH_PARENT,
+ GTK_MESSAGE_ERROR,
+ GTK_BUTTONS_OK,
+ message ? "%s" : nullptr,
+ message);
+
+ if (error != nullptr)
+ gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
+ "%s", error->message);
+
+ g_signal_connect (G_OBJECT (dialog), "response", G_CALLBACK (gtk_widget_destroy), nullptr);
+
+ if (weak_ptr != nullptr)
+ {
+ *weak_ptr = dialog;
+ g_object_add_weak_pointer (G_OBJECT (dialog), (void**)weak_ptr);
+ }
+
+ gtk_window_set_resizable (GTK_WINDOW (dialog), FALSE);
+
+ gtk_widget_show_all (dialog);
+ }
+ else
+ {
+ g_return_if_fail (GTK_IS_MESSAGE_DIALOG (*weak_ptr));
+
+ /* Sucks that there's no direct accessor for "text" property */
+ g_object_set (G_OBJECT (*weak_ptr), "text", message, nullptr);
+
+ gtk_window_present (GTK_WINDOW (*weak_ptr));
+ }
+}
+
+static gboolean
+open_url (GtkWindow *parent,
+ const char *uri,
+ guint32 user_time,
+ GError **error)
+{
+ GdkScreen *screen;
+ gs_free char *uri_fixed;
+
+ if (parent)
+ screen = gtk_widget_get_screen (GTK_WIDGET (parent));
+ else
+ screen = gdk_screen_get_default ();
+
+ uri_fixed = terminal_util_uri_fixup (uri, error);
+ if (uri_fixed == nullptr)
+ return FALSE;
+
+ return gtk_show_uri (screen, uri_fixed, user_time, error);
+}
+
+void
+terminal_util_show_help (const char *topic)
+{
+ gs_free_error GError *error = nullptr;
+ gs_free char *uri;
+
+ if (topic) {
+ uri = g_strdup_printf ("help:gnome-terminal/%s", topic);
+ } else {
+ uri = g_strdup ("help:gnome-terminal");
+ }
+
+ if (!open_url (nullptr, uri, gtk_get_current_event_time (), &error))
+ {
+ terminal_util_show_error_dialog (nullptr, nullptr, error,
+ _("There was an error displaying help"));
+ }
+}
+
+#define ABOUT_GROUP "About"
+#define ABOUT_URL "https://wiki.gnome.org/Apps/Terminal"
+#define EMAILIFY(string) (g_strdelimit ((string), "%", '@'))
+
+void
+terminal_util_show_about (void)
+{
+ static const char copyright[] =
+ "Copyright © 2002–2004 Havoc Pennington\n"
+ "Copyright © 2003–2004, 2007 Mariano Suárez-Alvarez\n"
+ "Copyright © 2006 Guilherme de S. Pastore\n"
+ "Copyright © 2007–2019 Christian Persch\n"
+ "Copyright © 2013–2019 Egmont Koblinger";
+ char *licence_text;
+ GKeyFile *key_file;
+ GBytes *bytes;
+ const guint8 *data;
+ gsize data_len;
+ GError *error = nullptr;
+ char **authors, **contributors, **artists, **documenters, **array_strv;
+ gsize n_authors = 0, n_contributors = 0, n_artists = 0, n_documenters = 0 , i;
+ GPtrArray *array;
+ gs_free char *comment;
+ gs_free char *version;
+ gs_free char *vte_version;
+ GtkWindow *dialog;
+
+ bytes = g_resources_lookup_data (TERMINAL_RESOURCES_PATH_PREFIX "/ui/terminal.about",
+ G_RESOURCE_LOOKUP_FLAGS_NONE,
+ &error);
+ g_assert_no_error (error);
+
+ data = (guint8 const*)g_bytes_get_data (bytes, &data_len);
+ key_file = g_key_file_new ();
+ g_key_file_load_from_data (key_file, (const char *) data, data_len, GKeyFileFlags(0), &error);
+ g_assert_no_error (error);
+
+ authors = g_key_file_get_string_list (key_file, ABOUT_GROUP, "Authors", &n_authors, nullptr);
+ contributors = g_key_file_get_string_list (key_file, ABOUT_GROUP, "Contributors", &n_contributors, nullptr);
+ artists = g_key_file_get_string_list (key_file, ABOUT_GROUP, "Artists", &n_artists, nullptr);
+ documenters = g_key_file_get_string_list (key_file, ABOUT_GROUP, "Documenters", &n_documenters, nullptr);
+
+ g_key_file_free (key_file);
+ g_bytes_unref (bytes);
+
+ array = g_ptr_array_new ();
+
+ for (i = 0; i < n_authors; ++i)
+ g_ptr_array_add (array, EMAILIFY (authors[i]));
+ g_free (authors); /* strings are now owned by the array */
+
+ if (n_contributors > 0)
+ {
+ g_ptr_array_add (array, g_strdup (""));
+ g_ptr_array_add (array, g_strdup (_("Contributors:")));
+ for (i = 0; i < n_contributors; ++i)
+ g_ptr_array_add (array, EMAILIFY (contributors[i]));
+ }
+ g_free (contributors); /* strings are now owned by the array */
+
+ g_ptr_array_add (array, nullptr);
+ array_strv = (char **) g_ptr_array_free (array, FALSE);
+
+ for (i = 0; i < n_artists; ++i)
+ artists[i] = EMAILIFY (artists[i]);
+ for (i = 0; i < n_documenters; ++i)
+ documenters[i] = EMAILIFY (documenters[i]);
+
+ licence_text = terminal_util_get_licence_text ();
+
+ /* gnome 40 corresponds to g-t 3.40.x. After that, gnome version
+ * increases by 1 while the g-t minor version increases by 2 between
+ * stable releases.
+ */
+ auto const gnome_version = 40 + (TERMINAL_MINOR_VERSION - 40 + 1) / 2;
+ version = g_strdup_printf (_("Version %s for GNOME %d"),
+ VERSION,
+ gnome_version);
+
+ vte_version = g_strdup_printf (_("Using VTE version %u.%u.%u"),
+ vte_get_major_version (),
+ vte_get_minor_version (),
+ vte_get_micro_version ());
+
+ comment = g_strdup_printf("%s\n%s %s",
+ _("A terminal emulator for the GNOME desktop"),
+ vte_version,
+ vte_get_features ());
+
+ dialog = (GtkWindow*)g_object_new (GTK_TYPE_ABOUT_DIALOG,
+ /* Hold the application while the window is shown */
+ "application", terminal_app_get (),
+ "program-name", _("GNOME Terminal"),
+ "copyright", copyright,
+ "comments", comment,
+ "version", version,
+ "authors", array_strv,
+ "artists", artists,
+ "documenters", documenters,
+ "license", licence_text,
+ "wrap-license", TRUE,
+ "website", ABOUT_URL,
+ "translator-credits", _("translator-credits"),
+ "logo-icon-name", GNOME_TERMINAL_ICON_NAME,
+ nullptr);
+
+ g_signal_connect (dialog, "response", G_CALLBACK (gtk_widget_destroy), nullptr);
+ gtk_window_present (dialog);
+
+ g_strfreev (array_strv);
+ g_strfreev (artists);
+ g_strfreev (documenters);
+ g_free (licence_text);
+}
+
+/* sets accessible name and description for the widget */
+
+void
+terminal_util_set_atk_name_description (GtkWidget *widget,
+ const char *name,
+ const char *desc)
+{
+ AtkObject *obj;
+
+ obj = gtk_widget_get_accessible (widget);
+
+ if (obj == nullptr)
+ {
+ g_warning ("%s: for some reason widget has no GtkAccessible",
+ G_STRFUNC);
+ return;
+ }
+
+ if (!GTK_IS_ACCESSIBLE (obj))
+ return; /* This means GAIL is not loaded so we have the NoOp accessible */
+
+ g_return_if_fail (GTK_IS_ACCESSIBLE (obj));
+ if (desc)
+ atk_object_set_description (obj, desc);
+ if (name)
+ atk_object_set_name (obj, name);
+}
+
+void
+terminal_util_open_url (GtkWidget *parent,
+ const char *orig_url,
+ TerminalURLFlavor flavor,
+ guint32 user_time)
+{
+ gs_free_error GError *error = nullptr;
+ gs_free char *uri = nullptr;
+
+ g_return_if_fail (orig_url != nullptr);
+
+ switch (flavor)
+ {
+ case FLAVOR_DEFAULT_TO_HTTP:
+ uri = g_strdup_printf ("http://%s", orig_url);
+ break;
+ case FLAVOR_EMAIL:
+ if (g_ascii_strncasecmp ("mailto:", orig_url, 7) != 0)
+ uri = g_strdup_printf ("mailto:%s", orig_url);
+ else
+ uri = g_strdup (orig_url);
+ break;
+ case FLAVOR_VOIP_CALL:
+ case FLAVOR_AS_IS:
+ uri = g_strdup (orig_url);
+ break;
+ default:
+ uri = nullptr;
+ g_assert_not_reached ();
+ }
+
+ if (!open_url (GTK_WINDOW (parent), uri, user_time, &error))
+ {
+ terminal_util_show_error_dialog (GTK_WINDOW (parent), nullptr, error,
+ _("Could not open the address “%s”"),
+ uri);
+ }
+}
+
+/**
+ * terminal_util_transform_uris_to_quoted_fuse_paths:
+ * @uris:
+ *
+ * Transforms those URIs in @uris to shell-quoted paths that point to
+ * GIO fuse paths.
+ */
+void
+terminal_util_transform_uris_to_quoted_fuse_paths (char **uris)
+{
+ guint i;
+
+ if (!uris)
+ return;
+
+ for (i = 0; uris[i]; ++i)
+ {
+ gs_unref_object GFile *file;
+ gs_free char *path;
+
+ file = g_file_new_for_uri (uris[i]);
+
+ path = g_file_get_path (file);
+ if (path)
+ {
+ char *quoted;
+
+ quoted = g_shell_quote (path);
+ g_free (uris[i]);
+
+ uris[i] = quoted;
+ }
+ }
+}
+
+char *
+terminal_util_concat_uris (char **uris,
+ gsize *length)
+{
+ GString *string;
+ gsize len;
+ guint i;
+
+ len = 0;
+ for (i = 0; uris[i]; ++i)
+ len += strlen (uris[i]) + 1;
+
+ if (length)
+ *length = len;
+
+ string = g_string_sized_new (len + 1);
+ for (i = 0; uris[i]; ++i)
+ {
+ g_string_append (string, uris[i]);
+ g_string_append_c (string, ' ');
+ }
+
+ return g_string_free (string, FALSE);
+}
+
+char *
+terminal_util_get_licence_text (void)
+{
+ const gchar *license[] = {
+ N_("GNOME Terminal is free software: you can redistribute it and/or modify "
+ "it under the terms of the GNU General Public License as published by "
+ "the Free Software Foundation, either version 3 of the License, or "
+ "(at your option) any later version."),
+ N_("GNOME Terminal 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 General Public License for more details."),
+ N_("You should have received a copy of the GNU General Public License "
+ "along with GNOME Terminal. If not, see <http://www.gnu.org/licenses/>.")
+ };
+
+ return g_strjoin ("\n\n", _(license[0]), _(license[1]), _(license[2]), nullptr);
+}
+
+static void
+main_object_destroy_cb (GtkWidget *widget)
+{
+ g_object_set_data (G_OBJECT (widget), "builder", nullptr);
+}
+
+GtkBuilder *
+terminal_util_load_widgets_resource (const char *path,
+ const char *main_object_name,
+ const char *object_name,
+ ...)
+{
+ GtkBuilder *builder;
+ GError *error = nullptr;
+ va_list args;
+
+ builder = gtk_builder_new ();
+ gtk_builder_add_from_resource (builder, path, &error);
+ g_assert_no_error (error);
+
+ va_start (args, object_name);
+
+ while (object_name) {
+ GObject **objectptr;
+
+ objectptr = va_arg (args, GObject**);
+ *objectptr = gtk_builder_get_object (builder, object_name);
+ if (!*objectptr)
+ g_error ("Failed to fetch object \"%s\" from resource \"%s\"\n", object_name, path);
+
+ object_name = va_arg (args, const char*);
+ }
+
+ va_end (args);
+
+ if (main_object_name) {
+ GObject *main_object;
+ GtkWidget *action_area;
+
+ main_object = gtk_builder_get_object (builder, main_object_name);
+ g_object_set_data_full (main_object, "builder", g_object_ref (builder), (GDestroyNotify) g_object_unref);
+ g_signal_connect (main_object, "destroy", G_CALLBACK (main_object_destroy_cb), nullptr);
+
+ /* Fixup dialogue padding, #735242 */
+ if (GTK_IS_DIALOG (main_object) &&
+ (action_area = (GtkWidget *) gtk_builder_get_object (builder, "dialog-action-area"))) {
+ gtk_widget_set_margin_start (action_area, 5);
+ gtk_widget_set_margin_end (action_area, 5);
+ gtk_widget_set_margin_top (action_area, 5);
+ gtk_widget_set_margin_bottom (action_area, 5);
+ }
+ }
+ return builder;
+}
+
+void
+terminal_util_load_objects_resource (const char *path,
+ const char *object_name,
+ ...)
+{
+ gs_unref_object GtkBuilder *builder;
+ GError *error = nullptr;
+ va_list args;
+
+ builder = gtk_builder_new ();
+ gtk_builder_add_from_resource (builder, path, &error);
+ g_assert_no_error (error);
+
+ va_start (args, object_name);
+
+ while (object_name) {
+ GObject **objectptr;
+
+ objectptr = va_arg (args, GObject**);
+ *objectptr = gtk_builder_get_object (builder, object_name);
+ if (*objectptr)
+ g_object_ref (*objectptr);
+ else
+ g_error ("Failed to fetch object \"%s\" from resource \"%s\"\n", object_name, path);
+
+ object_name = va_arg (args, const char*);
+ }
+
+ va_end (args);
+}
+
+gboolean
+terminal_util_dialog_response_on_delete (GtkWindow *widget)
+{
+ gtk_dialog_response (GTK_DIALOG (widget), GTK_RESPONSE_DELETE_EVENT);
+ return TRUE;
+}
+
+void
+terminal_util_dialog_focus_widget (GtkBuilder *builder,
+ const char *widget_name)
+{
+ GtkWidget *widget, *page, *page_parent;
+
+ if (widget_name == nullptr)
+ return;
+
+ widget = GTK_WIDGET (gtk_builder_get_object (builder, widget_name));
+ if (widget == nullptr)
+ return;
+
+ page = widget;
+ while (page != nullptr &&
+ (page_parent = gtk_widget_get_parent (page)) != nullptr &&
+ !GTK_IS_NOTEBOOK (page_parent))
+ page = page_parent;
+
+ page_parent = gtk_widget_get_parent (page);
+ if (page != nullptr && GTK_IS_NOTEBOOK (page_parent)) {
+ GtkNotebook *notebook;
+
+ notebook = GTK_NOTEBOOK (page_parent);
+ gtk_notebook_set_current_page (notebook, gtk_notebook_page_num (notebook, page));
+ }
+
+ if (gtk_widget_is_sensitive (widget))
+ gtk_widget_grab_focus (widget);
+}
+
+/* Proxy stuff */
+
+/*
+ * set_proxy_env:
+ * @env_table: a #GHashTable
+ * @key: the env var name
+ * @value: the env var value
+ *
+ * Adds @value for @key to @env_table, taking care to never overwrite an
+ * existing value for @key. @value is consumed.
+ */
+static void
+set_proxy_env (GHashTable *env_table,
+ const char *key,
+ char *value /* consumed */)
+{
+ char *key1 = nullptr, *key2 = nullptr;
+ char *value1 = nullptr, *value2 = nullptr;
+
+ if (!value)
+ return;
+
+ if (g_hash_table_lookup (env_table, key) == nullptr)
+ key1 = g_strdup (key);
+
+ key2 = g_ascii_strup (key, -1);
+ if (g_hash_table_lookup (env_table, key) != nullptr)
+ {
+ g_free (key2);
+ key2 = nullptr;
+ }
+
+ if (key1 && key2)
+ {
+ value1 = value;
+ value2 = g_strdup (value);
+ }
+ else if (key1)
+ value1 = value;
+ else if (key2)
+ value2 = value;
+ else
+ g_free (value);
+
+ if (key1)
+ g_hash_table_replace (env_table, key1, value1);
+ if (key2)
+ g_hash_table_replace (env_table, key2, value2);
+}
+
+static void
+setup_proxy_env (TerminalApp* app,
+ TerminalProxyProtocol protocol,
+ const char *proxy_scheme,
+ const char *env_name,
+ GHashTable *env_table)
+{
+ GString *buf;
+ gs_free char *host;
+ int port;
+
+ gboolean is_http = (protocol == TERMINAL_PROXY_HTTP);
+
+ GSettings *child_settings = terminal_app_get_proxy_settings_for_protocol(app, protocol);
+
+ host = g_settings_get_string (child_settings, "host");
+ port = g_settings_get_int (child_settings, "port");
+ if (host[0] == '\0' || port == 0)
+ return;
+
+ buf = g_string_sized_new (64);
+
+ g_string_append_printf (buf, "%s://", proxy_scheme);
+
+ if (is_http &&
+ g_settings_get_boolean (child_settings, "use-authentication"))
+ {
+ gs_free char *user;
+
+ user = g_settings_get_string (child_settings, "authentication-user");
+ if (user[0])
+ {
+ gs_free char *password;
+
+ g_string_append_uri_escaped (buf, user, nullptr, TRUE);
+
+ password = g_settings_get_string (child_settings, "authentication-password");
+ if (password[0])
+ {
+ g_string_append_c (buf, ':');
+ g_string_append_uri_escaped (buf, password, nullptr, TRUE);
+ }
+ g_string_append_c (buf, '@');
+ }
+ }
+
+ g_string_append_printf (buf, "%s:%d/", host, port);
+ set_proxy_env (env_table, env_name, g_string_free (buf, FALSE));
+}
+
+static void
+setup_ignore_proxy_env (GSettings *proxy_settings,
+ GHashTable *env_table)
+{
+ GString *buf;
+ gs_strfreev char **ignore;
+ int i;
+
+ g_settings_get (proxy_settings, "ignore-hosts", "^as", &ignore);
+ if (ignore == nullptr)
+ return;
+
+ buf = g_string_sized_new (64);
+ for (i = 0; ignore[i] != nullptr; ++i)
+ {
+ if (buf->len)
+ g_string_append_c (buf, ',');
+ g_string_append (buf, ignore[i]);
+ }
+
+ set_proxy_env (env_table, "no_proxy", g_string_free (buf, FALSE));
+}
+
+/**
+ * terminal_util_add_proxy_env:
+ * @env_table: a #GHashTable
+ *
+ * Adds the proxy env variables to @env_table.
+ */
+void
+terminal_util_add_proxy_env (GHashTable *env_table)
+{
+ auto const app = terminal_app_get();
+ auto const proxy_settings = terminal_app_get_proxy_settings(app);
+ auto const mode = GDesktopProxyMode(g_settings_get_enum (proxy_settings, "mode"));
+
+ if (mode == G_DESKTOP_PROXY_MODE_MANUAL)
+ {
+ setup_proxy_env (app, TERMINAL_PROXY_HTTP, "http", "http_proxy", env_table);
+ /* Even though it's https, the proxy scheme is 'http'. See bug #624440. */
+ setup_proxy_env (app, TERMINAL_PROXY_HTTPS, "http", "https_proxy", env_table);
+ /* Even though it's ftp, the proxy scheme is 'http'. See bug #624440. */
+ setup_proxy_env (app, TERMINAL_PROXY_FTP, "http", "ftp_proxy", env_table);
+ setup_proxy_env (app, TERMINAL_PROXY_SOCKS, "socks", "all_proxy", env_table);
+ setup_ignore_proxy_env (proxy_settings, env_table);
+ }
+ else if (mode == G_DESKTOP_PROXY_MODE_AUTO)
+ {
+ /* Not supported */
+ }
+}
+
+/**
+ * terminal_util_get_etc_shells:
+ *
+ * Returns: (transfer full) the contents of /etc/shells
+ */
+char **
+terminal_util_get_etc_shells (void)
+{
+ GError *err = nullptr;
+ gsize len;
+ gs_free char *contents = nullptr;
+ char *str, *nl, *end;
+ GPtrArray *arr;
+
+ if (!g_file_get_contents ("/etc/shells", &contents, &len, &err) || len == 0) {
+ /* Defaults as per man:getusershell(3) */
+ char *default_shells[3] = {
+ (char*) "/bin/sh",
+ (char*) "/bin/csh",
+ nullptr
+ };
+ return g_strdupv (default_shells);
+ }
+
+ arr = g_ptr_array_new ();
+ str = contents;
+ end = contents + len;
+ while (str < end && (nl = strchr (str, '\n')) != nullptr) {
+ if (str != nl) /* non-empty? */
+ g_ptr_array_add (arr, g_strndup (str, nl - str));
+ str = nl + 1;
+ }
+ /* Anything non-empty left? */
+ if (str < end && str[0])
+ g_ptr_array_add (arr, g_strdup (str));
+
+ g_ptr_array_add (arr, nullptr);
+ return (char **) g_ptr_array_free (arr, FALSE);
+}
+
+/**
+ * terminal_util_get_is_shell:
+ * @command: a string
+ *
+ * Returns wether @command is a valid shell as defined by the contents of /etc/shells.
+ *
+ * Returns: whether @command is a shell
+ */
+gboolean
+terminal_util_get_is_shell (const char *command)
+{
+ gs_strfreev char **shells;
+ guint i;
+
+ shells = terminal_util_get_etc_shells ();
+ if (shells == nullptr)
+ return FALSE;
+
+ for (i = 0; shells[i]; i++)
+ if (g_str_equal (command, shells[i]))
+ return TRUE;
+
+ return FALSE;
+}
+
+static gboolean
+s_to_rgba (GVariant *variant,
+ gpointer *result,
+ gpointer user_data)
+{
+ GdkRGBA *color = (GdkRGBA*)user_data;
+ const char *str;
+
+ if (variant == nullptr) {
+ /* Fallback */
+ *result = nullptr;
+ return TRUE;
+ }
+
+ g_variant_get (variant, "&s", &str);
+ if (!gdk_rgba_parse (color, str))
+ return FALSE;
+
+ color->alpha = 1.0;
+ *result = color;
+ return TRUE;
+}
+
+/**
+ * terminal_g_settings_get_rgba:
+ * @settings: a #GSettings
+ * @key: a valid key in @settings of type "s"
+ * @color: location to store the parsed color
+ *
+ * Gets a color from @key in @settings.
+ *
+ * Returns: @color if parsing succeeded, or %nullptr otherwise
+ */
+const GdkRGBA *
+terminal_g_settings_get_rgba (GSettings *settings,
+ const char *key,
+ GdkRGBA *color)
+{
+ g_return_val_if_fail (color != nullptr, FALSE);
+
+ return (GdkRGBA const*)g_settings_get_mapped (settings, key,
+ s_to_rgba,
+ color);
+}
+
+/**
+ * terminal_g_settings_set_rgba:
+ * @settings: a #GSettings
+ * @key: a valid key in @settings of type "s"
+ * @color: a #GdkRGBA
+ *
+ * Sets a color in @key in @settings.
+ */
+void
+terminal_g_settings_set_rgba (GSettings *settings,
+ const char *key,
+ const GdkRGBA *color)
+{
+ gs_free char *str;
+
+ str = gdk_rgba_to_string (color);
+ g_settings_set_string (settings, key, str);
+}
+
+static gboolean
+as_to_rgba_palette (GVariant *variant,
+ gpointer *result,
+ gpointer user_data)
+{
+ gsize *n_colors = (gsize*)user_data;
+ gs_free GdkRGBA *colors = nullptr;
+ gsize n = 0;
+ GVariantIter iter;
+ const char *str;
+ gsize i;
+
+ /* Fallback */
+ if (variant == nullptr)
+ goto out;
+
+ g_variant_iter_init (&iter, variant);
+ n = g_variant_iter_n_children (&iter);
+ colors = g_new (GdkRGBA, n);
+
+ i = 0;
+ while (g_variant_iter_next (&iter, "&s", &str)) {
+ if (!gdk_rgba_parse (&colors[i++], str)) {
+ return FALSE;
+ }
+ }
+
+ out:
+ gs_transfer_out_value (result, &colors);
+ if (n_colors)
+ *n_colors = n;
+
+ return TRUE;
+}
+
+/**
+ * terminal_g_settings_get_rgba_palette:
+ * @settings: a #GSettings
+ * @key: a valid key in @settings or type "s"
+ * @n_colors: (allow-none): location to store the number of palette entries, or %nullptr
+ *
+ * Returns: (transfer full):
+ */
+GdkRGBA *
+terminal_g_settings_get_rgba_palette (GSettings *settings,
+ const char *key,
+ gsize *n_colors)
+{
+ return (GdkRGBA*)g_settings_get_mapped (settings, key,
+ as_to_rgba_palette,
+ n_colors);
+}
+
+void
+terminal_g_settings_set_rgba_palette (GSettings *settings,
+ const char *key,
+ const GdkRGBA *colors,
+ gsize n_colors)
+{
+ gs_strfreev char **strv;
+ gsize i;
+
+ strv = g_new (char *, n_colors + 1);
+ for (i = 0; i < n_colors; ++i)
+ strv[i] = gdk_rgba_to_string (&colors[i]);
+ strv[n_colors] = nullptr;
+
+ g_settings_set (settings, key, "^as", strv);
+}
+
+static void
+mnemonic_label_set_sensitive_cb (GtkWidget *widget,
+ GParamSpec *pspec,
+ GtkWidget *label)
+{
+ gtk_widget_set_sensitive (label, gtk_widget_get_sensitive (widget));
+}
+
+/**
+ * terminal_util_bind_mnemonic_label_sensitivity:
+ * @container: a #GtkContainer
+ */
+void
+terminal_util_bind_mnemonic_label_sensitivity (GtkWidget *widget)
+{
+ GList *list, *l;
+
+ list = gtk_widget_list_mnemonic_labels (widget);
+ for (l = list; l != nullptr; l = l->next) {
+ GtkWidget *label = (GtkWidget*)l->data;
+
+ if (gtk_widget_is_ancestor (label, widget))
+ continue;
+
+#if 0
+ g_print ("Widget %s has mnemonic label %s\n",
+ gtk_buildable_get_name (GTK_BUILDABLE (widget)),
+ gtk_buildable_get_name (GTK_BUILDABLE (label)));
+#endif
+
+ mnemonic_label_set_sensitive_cb (widget, nullptr, label);
+ g_signal_connect (widget, "notify::sensitive",
+ G_CALLBACK (mnemonic_label_set_sensitive_cb),
+ label);
+ }
+ g_list_free (list);
+
+ if (GTK_IS_CONTAINER (widget))
+ gtk_container_foreach (GTK_CONTAINER (widget),
+ /* See #96 for double casting. */
+ (GtkCallback) (GCallback) terminal_util_bind_mnemonic_label_sensitivity,
+ nullptr);
+}
+
+/*
+ * "1234567", "'", 3 -> "1'234'567"
+ */
+static char *
+add_separators (const char *in, const char *sep, int groupby)
+{
+ int inlen, outlen, seplen, firstgrouplen;
+ char *out, *ret;
+
+ if (in[0] == '\0')
+ return g_strdup("");
+
+ inlen = strlen(in);
+ seplen = strlen(sep);
+ outlen = inlen + (inlen - 1) / groupby * seplen;
+ ret = out = (char*)g_malloc(outlen + 1);
+
+ firstgrouplen = (inlen - 1) % groupby + 1;
+ memcpy(out, in, firstgrouplen);
+ in += firstgrouplen;
+ out += firstgrouplen;
+
+ while (*in != '\0') {
+ memcpy(out, sep, seplen);
+ out += seplen;
+ memcpy(out, in, groupby);
+ in += groupby;
+ out += groupby;
+ }
+
+ g_assert(out - ret == outlen);
+ *out = '\0';
+ return ret;
+}
+
+/**
+ * terminal_util_number_info:
+ * @str: a dec or hex number as string
+ *
+ * Returns: (transfer full): Useful info about @str, or %nullptr if it's too large
+ */
+char *
+terminal_util_number_info (const char *str)
+{
+ gs_free char *decstr = nullptr;
+ gs_free char *hextmp = nullptr;
+ gs_free char *hexstr = nullptr;
+ gs_free char *magnitudestr = nullptr;
+ gboolean exact = TRUE;
+ gboolean hex = FALSE;
+ const char *thousep;
+
+ /* Deliberately not handle octal */
+ if (str[0] == '0' && (str[1] == 'x' || str[1] == 'X')) {
+ str += 2;
+ hex = TRUE;
+ }
+
+ errno = 0;
+ char* end;
+ gint64 num = g_ascii_strtoull(str, &end, hex ? 16 : 10);
+ if (errno || str == end || num == -1)
+ return nullptr;
+
+ /* No use in dec-hex conversion for so small numbers */
+ if (num < 10) {
+ return nullptr;
+ }
+
+ /* Group the decimal digits */
+ thousep = nl_langinfo(THOUSEP);
+ if (thousep[0] != '\0') {
+ /* If thousep is nonempty, use printf's magic which can handle
+ more complex separating logics, e.g. 2+2+2+3 for some locales */
+ decstr = g_strdup_printf("%'" G_GINT64_FORMAT, num);
+ } else {
+ /* If, however, thousep is empty, override it with a space so that we
+ do always group the digits (that's the whole point of this feature;
+ the choice of space guarantees not conflicting with the decimal separator) */
+ gs_free char *tmp = g_strdup_printf("%" G_GINT64_FORMAT, num);
+ thousep = " ";
+ decstr = add_separators(tmp, thousep, 3);
+ }
+
+ /* Group the hex digits by 4 using the same nonempty separator */
+ hextmp = g_strdup_printf("%" G_GINT64_MODIFIER "x", (guint64)(num));
+ hexstr = add_separators(hextmp, thousep, 4);
+
+ /* Find out the human-readable magnitude, e.g. 15.99 Mi */
+ if (num >= 1024) {
+ int power = 0;
+ while (num >= 1024 * 1024) {
+ power++;
+ if (num % 1024 != 0)
+ exact = FALSE;
+ num /= 1024;
+ }
+ /* Show 2 fraction digits, always rounding downwards. Printf rounds floats to the nearest representable value,
+ so do the calculation with integers until we get 100-fold the desired value, and then switch to float. */
+ if (100 * num % 1024 != 0)
+ exact = FALSE;
+ num = 100 * num / 1024;
+ magnitudestr = g_strdup_printf(" %s %.2f %ci", exact ? "=" : "≈", (double) num / 100, "KMGTPE"[power]);
+ } else {
+ magnitudestr = g_strdup("");
+ }
+
+ return g_strdup_printf(hex ? "0x%2$s = %1$s%3$s" : "%s = 0x%s%s", decstr, hexstr, magnitudestr);
+}
+
+/**
+ * terminal_util_timestamp_info:
+ * @str: a dec or hex number as string
+ *
+ * Returns: (transfer full): Formatted localtime if @str is decimal and looks like a timestamp, or %nullptr
+ */
+char *
+terminal_util_timestamp_info (const char *str)
+{
+ /* Bail out on hex numbers */
+ if (str[0] == '0' && (str[1] == 'x' || str[1] == 'X')) {
+ return nullptr;
+ }
+
+ /* Deliberately not handle octal */
+ errno = 0;
+ char* end;
+ gint64 num = g_ascii_strtoull (str, &end, 10);
+ if (errno || end == str || num == -1)
+ return nullptr;
+
+ /* Java uses Unix time in milliseconds. */
+ if (num >= 1000000000000 && num <= 1999999999999)
+ num /= 1000;
+
+ /* Fun: use inclusive interval so you can right-click on these numbers
+ * and check the human-readable time in gnome-terminal.
+ * (They're Sep 9 2001 and May 18 2033 by the way.) */
+ if (num < 1000000000 || num > 1999999999)
+ return nullptr;
+
+ gs_unref_date_time GDateTime* date = g_date_time_new_from_unix_local (num);
+ if (date == nullptr)
+ return nullptr;
+
+ return g_date_time_format(date, "%c");
+}
+
+/**
+ * terminal_util_uri_fixup:
+ * @uri: The URI to verify and maybe fixup
+ * @error: a #GError that is returned in case of errors
+ *
+ * Checks if gnome-terminal should attempt to handle the given URI,
+ * and rewrites if necessary.
+ *
+ * Currently URIs of "file://some-other-host/..." are refused because
+ * GIO (e.g. gtk_show_uri()) silently strips off the remote hostname
+ * and opens the local counterpart which is incorrect and misleading.
+ *
+ * Furthermore, once the hostname is verified, it is stripped off to
+ * avoid potential confusion around short hostname vs. fqdn, and to
+ * work around bug 781800 (LibreOffice bug 107461).
+ *
+ * Returns: The possibly rewritten URI if gnome-terminal should attempt
+ * to handle it, nullptr if it should refuse to handle.
+ */
+char *
+terminal_util_uri_fixup (const char *uri,
+ GError **error)
+{
+ gs_free char *filename;
+ gs_free char *hostname;
+
+ filename = g_filename_from_uri (uri, &hostname, nullptr);
+ if (filename != nullptr &&
+ hostname != nullptr &&
+ hostname[0] != '\0') {
+ /* "file" scheme and nonempty hostname */
+ if (g_ascii_strcasecmp (hostname, "localhost") == 0 ||
+ g_ascii_strcasecmp (hostname, g_get_host_name()) == 0) {
+ /* hostname corresponds to localhost */
+ char const *slash1, *slash2, *slash3;
+
+ /* We shouldn't enter this branch in case of URIs like
+ * "file:/etc/passwd", but just in case we do, or encounter
+ * something else unexpected, leave the URI unchanged. */
+ slash1 = strchr(uri, '/');
+ if (slash1 == nullptr)
+ return g_strdup (uri);
+
+ slash2 = slash1 + 1;
+ if (*slash2 != '/')
+ return g_strdup (uri);
+
+ slash3 = strchr(slash2 + 1, '/');
+ if (slash3 == nullptr)
+ return g_strdup (uri);
+
+ return g_strdup_printf("%.*s%s",
+ (int) (slash2 + 1 - uri),
+ uri,
+ slash3);
+ } else {
+ /* hostname refers to another host (e.g. the OSC 8 escape sequence
+ * was correctly emitted by a utility inside an ssh session) */
+ g_set_error_literal (error,
+ G_IO_ERROR,
+ G_IO_ERROR_NOT_SUPPORTED,
+ _("“file” scheme with remote hostname not supported"));
+ return nullptr;
+ }
+ } else {
+ /* "file" scheme without hostname, or some other scheme */
+ return g_strdup (uri);
+ }
+}
+
+/**
+ * terminal_util_hyperlink_uri_label:
+ * @uri: a URI
+ *
+ * Formats @uri to be displayed in a tooltip.
+ * Performs URI-decoding and converts IDN hostname to UTF-8.
+ *
+ * Returns: (transfer full): The human readable URI as plain text
+ */
+char *terminal_util_hyperlink_uri_label (const char *uri)
+{
+ gs_free char *unesc = nullptr;
+ gboolean replace_hostname;
+
+ if (uri == nullptr)
+ return nullptr;
+
+ unesc = g_uri_unescape_string(uri, nullptr);
+ if (unesc == nullptr)
+ unesc = g_strdup(uri);
+
+ if (g_ascii_strncasecmp(unesc, "ftp://", 6) == 0 ||
+ g_ascii_strncasecmp(unesc, "http://", 7) == 0 ||
+ g_ascii_strncasecmp(unesc, "https://", 8) == 0) {
+ gs_free char *unidn = nullptr;
+ char *hostname = strchr(unesc, '/') + 2;
+ char *hostname_end = strchrnul(hostname, '/');
+ char save = *hostname_end;
+ *hostname_end = '\0';
+ unidn = g_hostname_to_unicode(hostname);
+ replace_hostname = unidn != nullptr && g_ascii_strcasecmp(unidn, hostname) != 0;
+ *hostname_end = save;
+ if (replace_hostname) {
+ char *new_unesc = g_strdup_printf("%.*s%s%s",
+ (int) (hostname - unesc),
+ unesc,
+ unidn,
+ hostname_end);
+ g_free(unesc);
+ unesc = new_unesc;
+ }
+ }
+
+ return g_utf8_make_valid (unesc, -1);
+}
+
+#define TERMINAL_CACHE_DIR "gnome-terminal"
+#define TERMINAL_PRINT_SETTINGS_FILENAME "print-settings.ini"
+#define TERMINAL_PRINT_SETTINGS_GROUP_NAME "Print Settings"
+#define TERMINAL_PAGE_SETUP_GROUP_NAME "Page Setup"
+
+#define KEYFILE_FLAGS_FOR_LOAD GKeyFileFlags(G_KEY_FILE_NONE)
+#define KEYFILE_FLAGS_FOR_SAVE GKeyFileFlags(G_KEY_FILE_KEEP_COMMENTS | G_KEY_FILE_KEEP_TRANSLATIONS)
+
+static char *
+get_cache_dir (void)
+{
+ return g_build_filename (g_get_user_cache_dir (), TERMINAL_CACHE_DIR, nullptr);
+}
+
+static gboolean
+ensure_cache_dir (void)
+{
+ gs_free char *cache_dir;
+ int r;
+
+ cache_dir = get_cache_dir ();
+ errno = 0;
+ r = g_mkdir_with_parents (cache_dir, 0700);
+ if (r == -1 && errno != EEXIST) {
+ auto const errsv = errno;
+ g_printerr ("Failed to create cache dir: %s\n", g_strerror(errsv));
+ }
+ return r == 0;
+}
+
+static char *
+get_cache_filename (const char *filename)
+{
+ gs_free char *cache_dir = get_cache_dir ();
+ return g_build_filename (cache_dir, filename, nullptr);
+}
+
+static GKeyFile *
+load_cache_keyfile (const char *filename,
+ GKeyFileFlags flags,
+ gboolean ignore_error)
+{
+ gs_free char *path;
+ GKeyFile *keyfile;
+
+ path = get_cache_filename (filename);
+ keyfile = g_key_file_new ();
+ if (g_key_file_load_from_file (keyfile, path, flags, nullptr) || ignore_error)
+ return keyfile;
+
+ g_key_file_unref (keyfile);
+ return nullptr;
+}
+
+static void
+save_cache_keyfile (GKeyFile *keyfile,
+ const char *filename)
+{
+ gs_free char *path = nullptr;
+ gs_free char *data = nullptr;
+ gsize len = 0;
+
+ if (!ensure_cache_dir ())
+ return;
+
+ data = g_key_file_to_data (keyfile, &len, nullptr);
+ if (data == nullptr || len == 0)
+ return;
+
+ path = get_cache_filename (filename);
+
+ /* Ignore errors */
+ GError *err = nullptr;
+ if (!g_file_set_contents (path, data, len, &err)) {
+ g_printerr ("Error saving print settings: %s\n", err->message);
+ g_error_free (err);
+ }
+}
+
+static void
+keyfile_remove_keys (GKeyFile *keyfile,
+ const char *group_name,
+ ...)
+{
+ va_list args;
+ const char *key;
+
+ va_start (args, group_name);
+ while ((key = va_arg (args, const char *)) != nullptr) {
+ g_key_file_remove_key (keyfile, group_name, key, nullptr);
+ }
+ va_end (args);
+}
+
+/**
+ * terminal_util_load_print_settings:
+ *
+ * Loads the saved print settings, if any.
+ */
+void
+terminal_util_load_print_settings (GtkPrintSettings **settings,
+ GtkPageSetup **page_setup)
+{
+ gs_unref_key_file GKeyFile *keyfile = load_cache_keyfile (TERMINAL_PRINT_SETTINGS_FILENAME,
+ KEYFILE_FLAGS_FOR_LOAD,
+ FALSE);
+ if (keyfile == nullptr) {
+ *settings = nullptr;
+ *page_setup = nullptr;
+ return;
+ }
+
+ /* Ignore errors */
+ *settings = gtk_print_settings_new_from_key_file (keyfile,
+ TERMINAL_PRINT_SETTINGS_GROUP_NAME,
+ nullptr);
+ *page_setup = gtk_page_setup_new_from_key_file (keyfile,
+ TERMINAL_PAGE_SETUP_GROUP_NAME,
+ nullptr);
+}
+
+/**
+ * terminal_util_save_print_settings:
+ * @settings: (allow-none): a #GtkPrintSettings
+ * @page_setup: (allow-none): a #GtkPageSetup
+ *
+ * Saves the print settings.
+ */
+void
+terminal_util_save_print_settings (GtkPrintSettings *settings,
+ GtkPageSetup *page_setup)
+{
+ gs_unref_key_file GKeyFile *keyfile = nullptr;
+
+ keyfile = load_cache_keyfile (TERMINAL_PRINT_SETTINGS_FILENAME,
+ KEYFILE_FLAGS_FOR_SAVE,
+ TRUE);
+ g_assert (keyfile != nullptr);
+
+ if (settings != nullptr)
+ gtk_print_settings_to_key_file (settings, keyfile,
+ TERMINAL_PRINT_SETTINGS_GROUP_NAME);
+
+ /* Some keys are not desirable to persist; remove these.
+ * This list comes from evince.
+ */
+ keyfile_remove_keys (keyfile,
+ TERMINAL_PRINT_SETTINGS_GROUP_NAME,
+ GTK_PRINT_SETTINGS_COLLATE,
+ GTK_PRINT_SETTINGS_NUMBER_UP,
+ GTK_PRINT_SETTINGS_N_COPIES,
+ GTK_PRINT_SETTINGS_OUTPUT_URI,
+ GTK_PRINT_SETTINGS_PAGE_RANGES,
+ GTK_PRINT_SETTINGS_PAGE_SET,
+ GTK_PRINT_SETTINGS_PRINT_PAGES,
+ GTK_PRINT_SETTINGS_REVERSE,
+ GTK_PRINT_SETTINGS_SCALE,
+ nullptr);
+
+ if (page_setup != nullptr)
+ gtk_page_setup_to_key_file (page_setup, keyfile,
+ TERMINAL_PAGE_SETUP_GROUP_NAME);
+
+ /* Some keys are not desirable to persist; remove these.
+ * This list comes from evince.
+ */
+ keyfile_remove_keys (keyfile,
+ TERMINAL_PAGE_SETUP_GROUP_NAME,
+ "page-setup-orientation",
+ "page-setup-margin-bottom",
+ "page-setup-margin-left",
+ "page-setup-margin-right",
+ "page-setup-margin-top",
+ nullptr);
+
+ save_cache_keyfile (keyfile, TERMINAL_PRINT_SETTINGS_FILENAME);
+}
+
+/*
+ * terminal_util_translate_encoding:
+ * @encoding: the encoding name
+ *
+ * Translates old encoding name to the one supported by ICU, or
+ * to %nullptr if the encoding is not known to ICU.
+ *
+ * Returns: (transfer none): the translated encoding, or %nullptr if
+ * not translation was possible.
+ */
+const char*
+terminal_util_translate_encoding (const char *encoding)
+{
+ G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
+ if (vte_get_encoding_supported (encoding))
+ return encoding;
+ G_GNUC_END_IGNORE_DEPRECATIONS;
+
+ /* ICU knows (or has aliases for) most of the old names, except the following */
+ struct {
+ const char *name;
+ const char *replacement;
+ } translations[] = {
+ { "ARMSCII-8", nullptr }, /* apparently not supported by ICU */
+ { "GEORGIAN-PS", nullptr }, /* no idea which charset this even is */
+ { "ISO-IR-111", nullptr }, /* ISO-IR-111 refers to ECMA-94, but that
+ * standard does not contain cyrillic letters.
+ * ECMA-94 refers to ECMA-113 (ISO-IR-144),
+ * whose assignment differs greatly from ISO-IR-111,
+ * so it cannot be that either.
+ */
+ /* All the MAC_* charsets appear to be unknown to even glib iconv, so
+ * why did we have them in our list in the first place?
+ */
+ { "MAC_DEVANAGARI", nullptr }, /* apparently not supported by ICU */
+ { "MAC_FARSI", nullptr }, /* apparently not supported by ICU */
+ { "MAC_GREEK", "x-MacGreek" },
+ { "MAC_GUJARATI", nullptr }, /* apparently not supported by ICU */
+ { "MAC_GURMUKHI", nullptr }, /* apparently not supported by ICU */
+ { "MAC_ICELANDIC", nullptr }, /* apparently not supported by ICU */
+ { "MAC_ROMANIAN", "x-macroman" }, /* not sure this is the right one */
+ { "MAC_TURKISH", "x-MacTurkish" },
+ { "MAC_UKRAINIAN", "x-MacUkraine" },
+
+ { "TCVN", nullptr }, /* apparently not supported by ICU */
+ { "UHC", "cp949" },
+ { "VISCII", nullptr }, /* apparently not supported by ICU */
+
+ /* ISO-2022-* are known to ICU, but they simply cannot work in vte as
+ * I/O encoding, so don't even try.
+ */
+ { "ISO-2022-JP", nullptr },
+ { "ISO-2022-KR", nullptr },
+ };
+
+ const char *replacement = nullptr;
+ for (guint i = 0; i < G_N_ELEMENTS (translations); ++i) {
+ if (g_str_equal (encoding, translations[i].name)) {
+ replacement = translations[i].replacement;
+ break;
+ }
+ }
+
+ return replacement;
+}
+
+/* BEGIN code copied from glib
+ *
+ * Copyright (C) 1995-1998 Peter Mattis, Spencer Kimball and Josh MacDonald
+ *
+ * Code originally under LGPL2+; used and modified here under GPL3+
+ * Changes:
+ * Remove win32 support.
+ * Make @program nullable.
+ * Use @path instead of getenv("PATH").
+ * Use strchrnul
+ */
+
+/**
+ * terminal_util_find_program_in_path:
+ * @path: (type filename) (nullable): the search path (delimited by G_SEARCHPATH_SEPARATOR)
+ * @program: (type filename) (nullable): the programme to find in @path
+ *
+ * Like g_find_program_in_path(), but uses @path instead of the
+ * PATH environment variable as the search path.
+ *
+ * Returns: (type filename) (transfer full) (nullable): a newly allocated
+ * string containing the full path to @program, or %nullptr if @program
+ * could not be found in @path.
+ */
+char *
+terminal_util_find_program_in_path (const char *path,
+ const char *program)
+{
+ const gchar *p;
+ gchar *name, *freeme;
+ gsize len;
+ gsize pathlen;
+
+ if (program == nullptr)
+ return nullptr;
+
+ /* If it is an absolute path, or a relative path including subdirectories,
+ * don't look in PATH.
+ */
+ if (g_path_is_absolute (program)
+ || strchr (program, G_DIR_SEPARATOR) != nullptr
+ )
+ {
+ if (g_file_test (program, G_FILE_TEST_IS_EXECUTABLE) &&
+ !g_file_test (program, G_FILE_TEST_IS_DIR))
+ return g_strdup (program);
+ else
+ return nullptr;
+ }
+
+ if (path == nullptr)
+ {
+ /* There is no 'PATH' in the environment. The default
+ * search path in GNU libc is the current directory followed by
+ * the path 'confstr' returns for '_CS_PATH'.
+ */
+
+ /* In GLib we put . last, for security, and don't use the
+ * unportable confstr(); UNIX98 does not actually specify
+ * what to search if PATH is unset. POSIX may, dunno.
+ */
+
+ path = "/bin:/usr/bin:.";
+ }
+
+ len = strlen (program) + 1;
+ pathlen = strlen (path);
+ freeme = name = (char*)g_malloc (pathlen + len + 1);
+
+ /* Copy the file name at the top, including '\0' */
+ memcpy (name + pathlen + 1, program, len);
+ name = name + pathlen;
+ /* And add the slash before the filename */
+ *name = G_DIR_SEPARATOR;
+
+ p = path;
+ do
+ {
+ char *startp;
+
+ path = p;
+ p = strchrnul (path, G_SEARCHPATH_SEPARATOR);
+
+ if (p == path)
+ /* Two adjacent colons, or a colon at the beginning or the end
+ * of 'PATH' means to search the current directory.
+ */
+ startp = name + 1;
+ else
+ startp = (char*)memcpy (name - (p - path), path, p - path);
+
+ if (g_file_test (startp, G_FILE_TEST_IS_EXECUTABLE) &&
+ !g_file_test (startp, G_FILE_TEST_IS_DIR))
+ {
+ gchar *ret;
+ ret = g_strdup (startp);
+ g_free (freeme);
+ return ret;
+ }
+ }
+ while (*p++ != '\0');
+
+ g_free (freeme);
+ return nullptr;
+}
+
+/* END code copied from glib */
+
+/*
+ * terminal_util_check_envv:
+ * @strv:
+ *
+ * Validates that each element is of the form 'KEY=VALUE'.
+ */
+gboolean
+terminal_util_check_envv(char const* const* strv)
+{
+ if (!strv)
+ return TRUE;
+
+ for (int i = 0; strv[i]; ++i) {
+ const char *str = strv[i];
+ const char *equal = strchr(str, '=');
+ if (equal == nullptr || equal == str)
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+char**
+terminal_util_get_desktops(void)
+{
+ auto const desktop = g_getenv("XDG_CURRENT_DESKTOP");
+ if (!desktop)
+ return nullptr;
+
+ return g_strsplit(desktop, G_SEARCHPATH_SEPARATOR_S, -1);
+}
+
+#define XTE_CONFIG_DIRNAME "xdg-terminals"
+#define XTE_CONFIG_FILENAME "xdg-terminals.list"
+
+#define NEWLINE '\n'
+#define DOT_DESKTOP ".desktop"
+#define TERMINAL_DESKTOP_FILENAME TERMINAL_APPLICATION_ID DOT_DESKTOP
+
+static bool
+xte_data_check_one(char const* file,
+ bool full)
+{
+ if (!g_file_test(file, G_FILE_TEST_EXISTS)) {
+ _terminal_debug_print(TERMINAL_DEBUG_DEFAULT,
+ "Desktop file \"%s\" does not exist.\n",
+ file);
+ return false;
+ }
+
+ if (!full)
+ return true;
+
+ gs_free_error GError* error = nullptr;
+ gs_unref_key_file auto kf = g_key_file_new();
+ if (!g_key_file_load_from_file(kf,
+ file,
+ GKeyFileFlags(G_KEY_FILE_NONE),
+ &error)) {
+ _terminal_debug_print(TERMINAL_DEBUG_DEFAULT,
+ "Failed to load \"%s\" as keyfile: %s\n",
+ file, error->message);
+
+ return false;
+ }
+
+ if (!g_key_file_has_group(kf, G_KEY_FILE_DESKTOP_GROUP)) {
+ _terminal_debug_print(TERMINAL_DEBUG_DEFAULT,
+ "Keyfile file \"%s\" is not a desktop file.\n",
+ file);
+ return false;
+ }
+
+ // As per the XDG desktop entry spec, the (optional) TryExec key contains
+ // the name of an executable that can be used to determine if the programme
+ // is actually present.
+ gs_free auto try_exec = g_key_file_get_string(kf,
+ G_KEY_FILE_DESKTOP_GROUP,
+ G_KEY_FILE_DESKTOP_KEY_TRY_EXEC,
+ nullptr);
+ if (try_exec && try_exec[0]) {
+ _terminal_debug_print(TERMINAL_DEBUG_DEFAULT,
+ "Desktop file \"%s\" has no TryExec field.\n",
+ file);
+
+ // TryExec may be an abolute path, or be searched in $PATH
+ gs_free char* exec_path = nullptr;
+ if (g_path_is_absolute(try_exec))
+ exec_path = g_strdup(try_exec);
+ else
+ exec_path = g_find_program_in_path(try_exec);
+
+ auto const exists = exec_path != nullptr &&
+ g_file_test(exec_path, GFileTest(G_FILE_TEST_IS_EXECUTABLE));
+
+ _terminal_debug_print(TERMINAL_DEBUG_DEFAULT,
+ "Desktop file \"%s\" is %sinstalled (TryExec).\n",
+ file, exists ? "" : "not ");
+
+ if (!exists)
+ return false;
+ } else {
+ // TryExec is not present. We could fall back to parsing the Exec
+ // key and look if its first argument points to an executable that
+ // exists on the system, but that may also fail if the desktop file
+ // is DBusActivatable=true in which case we would need to find
+ // out if the D-Bus service corresponding to the name of the desktop
+ // file (without the .desktop extension) is activatable.
+ }
+
+ return true;
+}
+
+static bool
+xte_data_check(char const* name,
+ bool full)
+{
+ gs_free auto user_path = g_build_filename(g_get_user_data_dir(),
+ XTE_CONFIG_DIRNAME,
+ name,
+ nullptr);
+ if (xte_data_check_one(user_path, full))
+ return true;
+
+ gs_free auto local_path = g_build_filename(TERM_PREFIX, "local", "share",
+ XTE_CONFIG_DIRNAME,
+ name,
+ nullptr);
+ if (xte_data_check_one(local_path, full))
+ return true;
+
+ gs_free auto sys_path = g_build_filename(TERM_DATADIR,
+ XTE_CONFIG_DIRNAME,
+ name,
+ nullptr);
+ if (xte_data_check_one(sys_path, full))
+ return true;
+
+ return false;
+}
+
+static bool
+xte_data_ensure(void)
+{
+ if (xte_data_check(TERMINAL_DESKTOP_FILENAME, false))
+ return true;
+
+ // If we get here, there wasn't a desktop file in any of the paths. Install
+ // a symlink to the system-installed desktop file into the user path.
+
+ gs_free auto user_dir = g_build_filename(g_get_user_data_dir(),
+ XTE_CONFIG_DIRNAME,
+ nullptr);
+ if (g_mkdir_with_parents(user_dir, 0700) != 0 &&
+ errno != EEXIST) {
+ auto const errsv = errno;
+ _terminal_debug_print(TERMINAL_DEBUG_DEFAULT,
+ "Failed to create directory %s: %s\n",
+ user_dir, g_strerror(errsv));
+ return false;
+ }
+
+ gs_free auto link_path = g_build_filename(user_dir,
+ TERMINAL_DESKTOP_FILENAME,
+ nullptr);
+ gs_free auto target_path = g_build_filename(TERM_DATADIR,
+ "applications",
+ TERMINAL_DESKTOP_FILENAME,
+ nullptr);
+
+ auto const r = symlink(target_path, link_path);
+ if (r != -1) {
+ _terminal_debug_print(TERMINAL_DEBUG_DEFAULT,
+ "Installed symlink %s -> %s\n",
+ link_path, target_path);
+
+ } else {
+ auto const errsv = errno;
+ _terminal_debug_print(TERMINAL_DEBUG_DEFAULT,
+ "Failed to create symlink %s: %s\n",
+ link_path, g_strerror(errsv));
+ }
+
+ return r != -1;
+}
+
+static char**
+xte_config_read(char const* path,
+ GError** error)
+{
+ gs_close_fd auto fd = open(path, O_RDONLY | O_NONBLOCK | O_CLOEXEC);
+ if (fd == -1)
+ return nullptr;
+
+ // This is a small config file, so shouldn't be any bigger than this.
+ // If it is bigger, we'll discard the rest. That's why we're not using
+ // g_file_get_contents() here.
+ char buf[8192];
+ auto r = ssize_t{};
+ do {
+ r = read(fd, buf, sizeof(buf) - 1); // reserve one byte in buf
+ } while (r == -1 && errno == EINTR);
+ if (r < 0)
+ return nullptr;
+
+ buf[r] = '\0'; // NUL terminator; note that r < sizeof(buf)
+
+ auto lines = g_strsplit_set(buf, "\r\n", -1);
+ if (!lines)
+ return nullptr;
+
+ for (auto i = 0; lines[i]; ++i)
+ lines[i] = g_strstrip(lines[i]);
+
+ return lines;
+}
+
+static bool
+xte_config_rewrite(char const* path)
+{
+ gs_free_gstring auto str = g_string_sized_new(1024);
+ g_string_append(str, TERMINAL_DESKTOP_FILENAME);
+ g_string_append_c(str, NEWLINE);
+
+ gs_strfreev auto lines = xte_config_read(path, nullptr);
+ if (lines) {
+ for (auto i = 0; lines[i]; ++i) {
+ if (lines[i][0] == '\0')
+ continue;
+ if (strcmp(lines[i], TERMINAL_DESKTOP_FILENAME) == 0)
+ continue;
+
+ g_string_append(str, lines[i]);
+ g_string_append_c(str, NEWLINE);
+ }
+ }
+
+ gs_free_error GError* error = nullptr;
+ auto const r = g_file_set_contents(path, str->str, str->len, &error);
+ if (!r) {
+ _terminal_debug_print(TERMINAL_DEBUG_DEFAULT,
+ "Failed to rewrite XTE config %s: %s\n",
+ path, error->message);
+ }
+
+ return r;
+}
+
+static void
+xte_config_rewrite(void)
+{
+ auto const user_dir = g_get_user_config_dir();
+ if (g_mkdir_with_parents(user_dir, 0700) != 0 &&
+ errno != EEXIST) {
+ auto const errsv = errno;
+ _terminal_debug_print(TERMINAL_DEBUG_DEFAULT,
+ "Failed to create directory %s: %s\n",
+ user_dir, g_strerror(errsv));
+ // Nothing to do if we can't even create the directory
+ return;
+ }
+
+ // Install as default for all current desktops
+ gs_strfreev auto desktops = terminal_util_get_desktops();
+ if (desktops) {
+ for (auto i = 0; desktops[i]; ++i) {
+ gs_free auto name = g_strdup_printf("%s-" XTE_CONFIG_FILENAME,
+ desktops[i]);
+ gs_free auto path = g_build_filename(user_dir, name, nullptr);
+
+ xte_config_rewrite(path);
+ }
+ }
+
+ // Install as non-desktop specific default too
+ gs_free auto path = g_build_filename(user_dir, XTE_CONFIG_FILENAME, nullptr);
+ xte_config_rewrite(path);
+}
+
+static bool
+xte_config_is_foreign(char const* name)
+{
+ return !g_str_equal(name, TERMINAL_DESKTOP_FILENAME);
+}
+
+static char*
+xte_config_get_default(char const* path)
+{
+ gs_strfreev auto lines = xte_config_read(path, nullptr);
+ if (!lines)
+ return nullptr;
+
+ // A terminal is the default if it's the first non-comment line in the file
+ for (auto i = 0; lines[i]; ++i) {
+ auto const line = lines[i];
+ if (!line[0] || line[0] == '#')
+ continue;
+
+ // If a foreign terminal is default, check whether it is actually installed.
+ // (We always ensure our own desktop file exists.)
+ if (xte_config_is_foreign(line) &&
+ !xte_data_check(line, true)) {
+ _terminal_debug_print(TERMINAL_DEBUG_DEFAULT,
+ "Default entry \"%s\" from config \"%s\" is not installed, skipping.\n",
+ line, path);
+ return nullptr;
+ }
+
+ return g_strdup(line);
+ }
+
+ return nullptr;
+}
+
+static char*
+xte_config_get_default(void)
+{
+ auto const user_dir = g_get_user_config_dir();
+ gs_strfreev auto desktops = terminal_util_get_desktops();
+ if (desktops) {
+ for (auto i = 0; desktops[i]; ++i) {
+ gs_free auto name = g_strdup_printf("%s-" XTE_CONFIG_FILENAME,
+ desktops[i]);
+ gs_free auto path = g_build_filename(user_dir, name, nullptr);
+ if (auto term = xte_config_get_default(path))
+ return term;
+ }
+ }
+
+ gs_free auto user_path = g_build_filename(user_dir, XTE_CONFIG_FILENAME, nullptr);
+ if (auto term = xte_config_get_default(user_path))
+ return term;
+
+ if (desktops) {
+ for (auto i = 0; desktops[i]; ++i) {
+ gs_free auto name = g_strdup_printf("%s-" XTE_CONFIG_FILENAME,
+ desktops[i]);
+ gs_free auto path = g_build_filename("/etc/xdg", name, nullptr);
+ if (auto term = xte_config_get_default(path))
+ return term;
+ }
+ }
+
+ gs_free auto sys_path = g_build_filename("/etc/xdg", XTE_CONFIG_FILENAME, nullptr);
+ if (auto term = xte_config_get_default(sys_path))
+ return term;
+
+ return nullptr;
+}
+
+static bool
+xte_config_is_default(bool* set = nullptr)
+{
+ gs_free auto term = xte_config_get_default();
+
+ auto const is_default = term && g_str_equal(term, TERMINAL_DESKTOP_FILENAME);
+ if (set)
+ *set = term != nullptr;
+ return is_default;
+}
+
+gboolean
+terminal_util_is_default_terminal(void)
+{
+ auto set = false;
+ auto const is_default = xte_config_is_default(&set);
+ if (!set) {
+ // No terminal is default yet, so we claim the default.
+ _terminal_debug_print(TERMINAL_DEBUG_DEFAULT,
+ "No default terminal, claiming default.\n");
+ return terminal_util_make_default_terminal();
+ }
+
+ if (is_default) {
+ // If we're the default terminal, ensure our desktop file is installed
+ // in the right location.
+ xte_data_ensure();
+ }
+
+ return is_default;
+}
+
+gboolean
+terminal_util_make_default_terminal(void)
+{
+ xte_config_rewrite();
+ xte_data_ensure();
+
+ return xte_config_is_default();
+}