/* * 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 . */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 *weap_ptr is #nullptr * (and in that * case, set @weap_ptr to be a weak pointer to the new dialog), otherwise just * present *weak_ptr. Note that in this last case, the message will * 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 .") }; 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(); }