diff options
Diffstat (limited to 'src')
95 files changed, 32323 insertions, 0 deletions
diff --git a/src/eggshell.cc b/src/eggshell.cc new file mode 100644 index 0000000..b0926eb --- /dev/null +++ b/src/eggshell.cc @@ -0,0 +1,112 @@ +/* + * Copyright (C) 1997, 1998, 1999, 2000 Free Software Foundation + * Copyright (C) 1999, 2000 Red Hat, Inc. + * All rights reserved. + * + * This file is part of the Gnome Library. + * + * 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/>. + */ +/* + @NOTATION@ + */ + +/* + * + * Gnome utility routines. + * (C) 1997, 1998, 1999 the Free Software Foundation. + * + * Author: Miguel de Icaza, + */ + +#include <config.h> + +#include "eggshell.hh" + +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> +#ifndef G_OS_WIN32 +#include <pwd.h> +#endif + +#include <glib.h> + +/** + * egg_shell: + * @shell: the value of the SHELL env variable + * + * Retrieves the user's preferred shell. + * + * Returns: A newly allocated string that is the path to the shell. + */ +char * +egg_shell (const char *shell) +{ +#ifndef G_OS_WIN32 + struct passwd *pw; + int i; + static const char shells [][14] = { + /* Note that on some systems shells can also + * be installed in /usr/bin */ + "/bin/bash", "/usr/bin/bash", + "/bin/zsh", "/usr/bin/zsh", + "/bin/tcsh", "/usr/bin/tcsh", + "/bin/ksh", "/usr/bin/ksh", + "/bin/csh", "/bin/sh" + }; + + if (geteuid () == getuid () && + getegid () == getgid ()) { + /* only in non-setuid */ + if (shell != nullptr) { + if (access (shell, X_OK) == 0) { + return g_strdup (shell); + } + } + } + pw = getpwuid(getuid()); + if (pw && pw->pw_shell) { + if (access (pw->pw_shell, X_OK) == 0) { + return g_strdup (pw->pw_shell); + } + } + + for (i = 0; i != G_N_ELEMENTS (shells); i++) { + if (access (shells [i], X_OK) == 0) { + return g_strdup (shells[i]); + } + } + + /* If /bin/sh doesn't exist, your system is truly broken. */ + g_assert_not_reached (); + + /* Placate compiler. */ + return nullptr; +#else + /* g_find_program_in_path() always looks also in the Windows + * and System32 directories, so it should always find either cmd.exe + * or command.com. + */ + char *retval = g_find_program_in_path ("cmd.exe"); + + if (retval == nullptr) + retval = g_find_program_in_path ("command.com"); + + g_assert (retval != nullptr); + + return retval; +#endif +} diff --git a/src/eggshell.hh b/src/eggshell.hh new file mode 100644 index 0000000..2312777 --- /dev/null +++ b/src/eggshell.hh @@ -0,0 +1,38 @@ +/* + * Copyright (C) 1997, 1998, 1999, 2000 Free Software Foundation + * Copyright (C) 1999, 2000 Red Hat, Inc. + * All rights reserved. + * + * This file is part of the Gnome Library. + * + * 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/>. + */ +/* + @NOTATION@ + */ + +#ifndef __EGG_USER_SHELL_H__ +#define __EGG_USER_SHELL_H__ + +#include <stdlib.h> +#include <glib.h> + +G_BEGIN_DECLS + +/* Find the name of the user's shell. */ +char *egg_shell (const char *shell); + +G_END_DECLS + +#endif diff --git a/src/external.gschema.xml b/src/external.gschema.xml new file mode 100644 index 0000000..79c3166 --- /dev/null +++ b/src/external.gschema.xml @@ -0,0 +1,143 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright © 2021 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/>. +--> +<schemalist> + + <!-- From gsettings-desktop-schemas --> + + <schema id="org.gnome.desktop.interface" + gettext-domain="gsettings-desktop-schemas" + path="/org/gnome/desktop/interface/"> + <key name="monospace-font-name" + type="s"> + <default>'DejaVu Sans Mono 10'</default> + </key> + </schema> + + <enum id="org.gnome.desktop.GDesktopProxyMode"> + <value nick="none" + value="0"/> + <value nick="manual" + value="1"/> + <value nick="auto" + value="2"/> + </enum> + + <schema id="org.gnome.system.proxy.http" + gettext-domain="gsettings-desktop-schemas" + path="/system/proxy/http/"> + <key name="host" + type="s"> + <default>''</default> + </key> + <key name="port" + type="i"> + <range min="0" + max="65535" /> + <default>8080</default> + </key> + <key name="use-authentication" + type="b"> + <default>false</default> + </key> + <key name="authentication-user" + type="s"> + <default>''</default> + </key> + <key name="authentication-password" + type="s"> + <default>''</default> + </key> + </schema> + + <schema id="org.gnome.system.proxy.https" + gettext-domain="gsettings-desktop-schemas" + path="/system/proxy/https/"> + <key name="host" + type="s"> + <default>''</default> + </key> + <key name="port" + type="i"> + <range min="0" + max="65535" /> + <default>0</default> + </key> + </schema> + + <schema id="org.gnome.system.proxy.ftp" + gettext-domain="gsettings-desktop-schemas" + path="/system/proxy/ftp/"> + <key name="host" + type="s"> + <default>''</default> + </key> + <key name="port" + type="i"> + <range min="0" + max="65535" /> + <default>0</default> + </key> + </schema> + + <schema id="org.gnome.system.proxy.socks" + gettext-domain="gsettings-desktop-schemas" + path="/system/proxy/socks/"> + <key name="host" + type="s"> + <default>''</default> + </key> + <key name="port" + type="i"> + <range min="0" + max="65535" /> + <default>0</default> + </key> + </schema> + + <schema id="org.gnome.system.proxy" + gettext-domain="gsettings-desktop-schemas" + path="/system/proxy/"> + <child name="http" + schema="org.gnome.system.proxy.http" /> + <child name="https" + schema="org.gnome.system.proxy.https" /> + <child name="ftp" + schema="org.gnome.system.proxy.ftp" /> + <child name="socks" + schema="org.gnome.system.proxy.socks" /> + <key name="mode" + enum="org.gnome.desktop.GDesktopProxyMode"> + <default>'none'</default> + </key> + <key name="ignore-hosts" + type="as"> + <default>['localhost', '127.0.0.0/8', '::1']</default> + </key> + </schema> + + <!-- From gtk+ --> + + <schema id="org.gtk.Settings.Debug" + path="/org/gtk/settings/debug/"> + <key name="enable-inspector-keybinding" + type="b"> + <default>false</default> + </key> + </schema> + +</schemalist> diff --git a/src/gnome-terminal-search-provider.ini b/src/gnome-terminal-search-provider.ini new file mode 100644 index 0000000..1b9f81c --- /dev/null +++ b/src/gnome-terminal-search-provider.ini @@ -0,0 +1,20 @@ +# Copyright © 2013 Red Hat, Inc. +# +# 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/>. + +[Shell Search Provider] +DesktopId=org.gnome.Terminal.desktop +BusName=org.gnome.Terminal +ObjectPath=/org/gnome/Terminal/SearchProvider +Version=2 diff --git a/src/gnome-terminal-server.service.in b/src/gnome-terminal-server.service.in new file mode 100644 index 0000000..24a1f4b --- /dev/null +++ b/src/gnome-terminal-server.service.in @@ -0,0 +1,10 @@ +[Unit] +Description=GNOME Terminal Server +PartOf=graphical-session.target +[Service] +Slice=app-@gt_dns_name@.slice +Type=dbus +BusName=@gt_dns_name@ +ExecStart=@libexecdir@/@gt_name@-server +TimeoutStopSec=5s +KillMode=process diff --git a/src/meson.build b/src/meson.build new file mode 100644 index 0000000..8a45547 --- /dev/null +++ b/src/meson.build @@ -0,0 +1,492 @@ +# Copyright © 2019, 2020, 2021, 2022 Christian Persch +# +# This programme 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 programme 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 programme. If not, see <https://www.gnu.org/licenses/>. + +src_inc = include_directories('.') + +# UI files + +ui_xsltproc_command = [ + xsltproc, + '--nonet', + '-o', '@OUTPUT@', + '@INPUT@', +] + +menubar_ui_mnemonics = custom_target( + 'terminal-menubar-with-mnemonics.ui', + command: ui_xsltproc_command, + input: files( + '..' / 'data' / 'ui-filter-mnemonics.xslt', + 'terminal-menubar.ui.in', + ), + install: false, + output: 'terminal-menubar-with-mnemonics.ui', +) + +menubar_ui_nomnemonics = custom_target( + 'terminal-menubar-without-mnemonics.ui', + command: ui_xsltproc_command, + input: files( + '..' / 'data' / 'ui-filter-no-mnemonics.xslt', + 'terminal-menubar.ui.in', + ), + install: false, + output: 'terminal-menubar-without-mnemonics.ui', +) + +# Common sources + +app_sources = files( + 'terminal-accels.cc', + 'terminal-accels.hh', + 'terminal-app.cc', + 'terminal-app.hh', +) + +client_util_sources = files( + 'terminal-client-utils.cc', + 'terminal-client-utils.hh', +) + +debug_sources = files( + 'terminal-debug.cc', + 'terminal-debug.hh', +) + +dbus_sources = gnome.gdbus_codegen( + 'terminal-gdbus-generated', + gt_dns_name + '.xml', + autocleanup: 'none', + install_header: false, + interface_prefix: gt_dns_name, + namespace: 'Terminal', + object_manager: true, +) + +egg_sources = files( + 'eggshell.cc', + 'eggshell.hh', +) + +i18n_sources = files( + 'terminal-i18n.cc', + 'terminal-i18n.hh', + 'terminal-intl.hh', +) + +marshal_sources = gnome.genmarshal( + 'terminal-marshal', + internal: true, + install_header: false, + prefix: '_terminal_marshal', + sources: files( + 'terminal-marshal.list', + ), + stdinc: true, + valist_marshallers: true, +) + +misc_sources = files( + 'terminal-defines.hh', + 'terminal-libgsystem.hh', +) + +profiles_sources = files( + 'terminal-profiles-list.cc', + 'terminal-profiles-list.hh', + 'terminal-schemas.hh', + 'terminal-settings-list.cc', + 'terminal-settings-list.hh', +) + +settings_dbus_sources = gnome.gdbus_codegen( + 'terminal-settings-bridge-generated', + 'org.gnome.Terminal.SettingsBridge.xml', + autocleanup: 'all', + install_header: false, + interface_prefix: gt_dns_name, + namespace: 'Terminal', + object_manager: false, +) + +settings_utils_sources = files( + 'terminal-settings-utils.cc', + 'terminal-settings-utils.hh', +) + +regex_sources = files( + 'terminal-regex.hh', +) + +types_sources = files( + 'terminal-enums.hh', +) + +types_sources += gnome.mkenums( + 'terminal-type-builtins', + c_template: 'terminal-type-builtins.hh.template', + h_template: 'terminal-type-builtins.cc.template', + install_header: false, + sources: types_sources, +) + +version_conf = { + 'TERMINAL_MAJOR_VERSION': gt_major_version.to_string(), + 'TERMINAL_MICRO_VERSION': gt_micro_version.to_string(), + 'TERMINAL_MINOR_VERSION': gt_minor_version.to_string(), +} + +version_sources = [configure_file( + configuration: version_conf, + input: 'terminal-version.hh.in', + install: false, + output: '@BASENAME@', +)] + +util_sources = files( + 'terminal-util.cc', + 'terminal-util.hh', +) + +# Flags + +common_cxxflags = version_cxxflags + [ + '-DTERMINAL_COMPILATION', + '-DTERM_PREFIX="@0@"'.format(gt_prefix), + '-DTERM_BINDIR="@0@"'.format(gt_prefix / gt_bindir), + '-DTERM_DATADIR="@0@"'.format(gt_prefix / gt_datadir), + '-DTERM_LIBEXECDIR="@0@"'.format(gt_prefix / gt_libexecdir), + '-DTERM_LOCALEDIR="@0@"'.format(gt_prefix / gt_localedir), + '-DTERM_PKGDATADIR="@0@"'.format(gt_prefix / gt_pkgdatadir), + '-DTERM_PKGLIBDIR="@0@"'.format(gt_prefix / gt_pkglibdir), + '-DVTE_DISABLE_DEPRECATION_WARNINGS', +] + +# Server + +server_resources_sources = gnome.compile_resources( + 'terminal-server-resources', + 'terminal.gresource.xml', + c_name: 'terminal', + dependencies: [ + menubar_ui_mnemonics, + menubar_ui_nomnemonics, + ], + export: false, +) + +server_sources = app_sources + client_util_sources + debug_sources + dbus_sources + egg_sources + i18n_sources + marshal_sources + misc_sources + profiles_sources + regex_sources + server_resources_sources + settings_dbus_sources + settings_utils_sources + types_sources + util_sources + version_sources + files( + 'server.cc', + 'terminal-enums.hh', + 'terminal-gdbus.cc', + 'terminal-gdbus.hh', + 'terminal-headerbar.cc', + 'terminal-headerbar.hh', + 'terminal-icon-button.cc', + 'terminal-icon-button.hh', + 'terminal-info-bar.cc', + 'terminal-info-bar.hh', + 'terminal-mdi-container.cc', + 'terminal-mdi-container.hh', + 'terminal-menu-button.cc', + 'terminal-menu-button.hh', + 'terminal-notebook.cc', + 'terminal-notebook.hh', + 'terminal-pcre2.hh', + 'terminal-prefs-process.cc', + 'terminal-prefs-process.hh', + 'terminal-screen-container.cc', + 'terminal-screen-container.hh', + 'terminal-screen.cc', + 'terminal-screen.hh', + 'terminal-search-popover.cc', + 'terminal-search-popover.hh', + 'terminal-settings-bridge-impl.cc', + 'terminal-settings-bridge-impl.hh', + 'terminal-tab-label.cc', + 'terminal-tab-label.hh', + 'terminal-window.cc', + 'terminal-window.hh', +) + +if get_option('search_provider') + + server_sources += files( + 'terminal-search-provider.cc', + 'terminal-search-provider.hh', + ) + + server_sources += gnome.gdbus_codegen( + 'terminal-search-provider-gdbus-generated', + gt_prefix / gt_dbusinterfacedir / 'org.gnome.ShellSearchProvider2.xml', + autocleanup: 'none', + install_header: false, + interface_prefix: 'org.gnome.Shell', + namespace: 'Terminal', + ) + +endif + +server_incs = [ + top_inc, + src_inc, +] + +server_cxxflags = common_cxxflags + [ + '-DTERMINAL_SERVER', +] + +server_deps = [ + gio_dep, + gio_unix_dep, + glib_dep, + gtk_dep, + pcre2_dep, + pthreads_dep, + schemas_dep, + uuid_dep, + vte_dep, + x11_dep, +] + +server = executable( + gt_name + '-server', + cpp_args: server_cxxflags, + dependencies: server_deps, + include_directories: server_incs, + install: true, + install_dir: gt_prefix / gt_libexecdir, + sources: server_sources, +) + +# systemd and D-Bus glue + +server_conf = configuration_data() +server_conf.set('gt_name', gt_name) +server_conf.set('gt_dns_name', gt_dns_name) +server_conf.set('libexecdir', gt_prefix / gt_libexecdir) + +configure_file( + input: gt_dns_name + '.service.in', + output: gt_dns_name + '.service', + configuration: server_conf, + install: true, + install_dir: gt_prefix / gt_dbusservicedir, +) + +configure_file( + input: gt_name + '-server.service.in', + output: gt_name + '-server.service', + configuration: server_conf, + install: true, + install_dir: gt_prefix / gt_systemduserdir +) + +# Search provider + +if get_option('search_provider') + + provider_ini = files(gt_name + '-search-provider.ini') + + install_data( + provider_ini, + install_dir: gt_prefix / gt_searchproviderdir, + ) +endif # option 'search_provider' + +# Preferences + +prefs_resources_sources = gnome.compile_resources( + 'terminal-prefs-resources', + 'prefs.gresource.xml', + c_name: 'terminal', + export: false, +) + +prefs_main_sources = app_sources + client_util_sources + debug_sources + i18n_sources + marshal_sources + misc_sources + prefs_resources_sources + profiles_sources + settings_dbus_sources + settings_utils_sources + types_sources + util_sources + version_sources + files( + 'prefs-main.cc', + 'profile-editor.cc', + 'profile-editor.hh', + 'terminal-prefs.cc', + 'terminal-prefs.hh', + 'terminal-settings-bridge-backend.cc', + 'terminal-settings-bridge-backend.hh', +) + +prefs_main_cxxflags = common_cxxflags + [ + '-DTERMINAL_PREFERENCES', +] + +prefs_main_incs = server_incs +prefs_main_deps = server_deps + +prefs_main = executable( + gt_name + '-preferences', + cpp_args: prefs_main_cxxflags, + include_directories: prefs_main_incs, + dependencies: prefs_main_deps, + install: true, + install_dir: gt_prefix / gt_libexecdir, + sources: prefs_main_sources, +) + +# Legacy client + +client_sources = client_util_sources + debug_sources + dbus_sources + i18n_sources + profiles_sources + settings_utils_sources + types_sources + files( + 'terminal-options.cc', + 'terminal-options.hh', + 'terminal.cc', +) + +client_incs = [ + top_inc, + src_inc, +] + +client_incs = [ + top_inc, + src_inc, +] + +client_cxxflags = common_cxxflags + [ + '-DTERMINAL_CLIENT', +] + +client_deps = [ + gio_dep, + gio_unix_dep, + glib_dep, + gtk_dep, + x11_dep, + uuid_dep, + vte_dep, +] + +client = executable( + gt_name, + cpp_args: client_cxxflags, + include_directories: client_incs, + dependencies: client_deps, + install: true, + sources: client_sources, +) + +# Settings schemas + +install_data( + gt_dns_name + '.gschema.xml', + install_dir: gt_prefix / gt_schemadir, +) + +meson.add_install_script( + 'meson_compileschemas.py', + gt_prefix / gt_schemadir, +) + +reference_schemas = custom_target( + 'gschemas.compiled', + command: [ + glib_compile_schemas, + '--targetdir', meson.current_build_dir(), + '--schema-file', files(gt_dns_name + '.gschema.xml'), + '--schema-file', files('external.gschema.xml'), + ], + install: true, + install_dir: gt_prefix / gt_pkglibdir, + output: 'gschemas.compiled', +) + +# Nautilus extension + +if get_option('nautilus_extension') + + nautilus_sources = client_util_sources + dbus_sources + i18n_sources + misc_sources + types_sources + files( + 'terminal-nautilus.cc', + ) + + nautilus_map = meson.current_source_dir() / 'nautilus.map' + + nautilus_incs = [ + top_inc, + src_inc, + ] + + nautilus_cxxflags = glib_version_cxxflags + [ + '-DTERMINAL_NAUTILUS', + '-DTERM_LOCALEDIR="@0@"'.format(gt_prefix / gt_localedir), + ] + + nautilus_deps = [ + gio_dep, + gio_unix_dep, + glib_dep, + libnautilus_extension_dep, + ] + + nautilus_ldflags = [ + '-Wl,--version-script,@0@'.format(nautilus_map), + ] + + nautilus_extension = shared_library( + 'terminal-nautilus', + cpp_args: nautilus_cxxflags, + dependencies: nautilus_deps, + include_directories: nautilus_incs, + install: true, + install_dir: gt_prefix / gt_nautilusextensiondir, + link_args: nautilus_ldflags, + link_depends: nautilus_map, + sources: nautilus_sources, + soversion: '', + ) + +endif # option 'nautilus_extension' + +# Unit tests + +test_regex_sources = regex_sources + files( + 'terminal-regex.cc', +) + +test_regex = executable( + 'test-regex', + cpp_args: [ + '-DTERMINAL_REGEX_MAIN', + ], + dependencies: [ + glib_dep, + pcre2_dep, + ], + include_directories: [top_inc, src_inc,], + sources: test_regex_sources, + install: false, +) + +test_env = [ + 'GNOME_TERMINAL_DEBUG=0', + 'VTE_DEBUG=0', +] + +test_units = [ + ['regex', test_regex], +] + +foreach test: test_units + test( + test[0], + test[1], + env: test_env, + ) +endforeach diff --git a/src/meson_compileschemas.py b/src/meson_compileschemas.py new file mode 100755 index 0000000..3268ddc --- /dev/null +++ b/src/meson_compileschemas.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# Copyright © 2019 Christian Persch +# +# This programme 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 programme 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 programme. If not, see <https://www.gnu.org/licenses/>. + +import os +import subprocess +import sys + +if os.environ.get('DESTDIR'): + sys.exit(0) + +prefix = os.environ['MESON_INSTALL_PREFIX'] +schemasdir = os.path.join(prefix, sys.argv[1]) + +rv = subprocess.call(['glib-compile-schemas', schemasdir]) +sys.exit(rv) diff --git a/src/nautilus.map b/src/nautilus.map new file mode 100644 index 0000000..7cd7993 --- /dev/null +++ b/src/nautilus.map @@ -0,0 +1,9 @@ +{ + global: + nautilus_module_initialize; + nautilus_module_list_types; + nautilus_module_shutdown; + + local: + *; +}; diff --git a/src/org.gnome.Terminal.SettingsBridge.xml b/src/org.gnome.Terminal.SettingsBridge.xml new file mode 100644 index 0000000..247ebbe --- /dev/null +++ b/src/org.gnome.Terminal.SettingsBridge.xml @@ -0,0 +1,83 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Introspection 0.1//EN" + "http://www.freedesktop.org/software/dbus/introspection.dtd"> +<!-- + Copyright © 2022 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, or (at your option) + any later version. + + This program is distributed in the hope conf 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/>. +--> +<node> + <interface name="org.gnome.Terminal.SettingsBridge"> + <annotation name="org.gtk.GDBus.C.Name" value="SettingsBridge" /> + + <method name="get_permission"> + <arg type="s" name="path" direction="in" /> + <arg type="v" name="permission" direction="out" /> + </method> + + <method name="get_writable"> + <arg type="s" name="key" direction="in" /> + <arg type="b" name="writable" direction="out" /> + </method> + + <method name="read"> + <arg type="s" name="key" direction="in" /> + <arg type="s" name="type" direction="in" /> + <arg type="b" name="default" direction="in" /> + <arg type="ay" name="value" direction="out"> + <annotation name="org.gtk.GDBus.C.ForceGVariant" value="true" /> + </arg> + </method> + + <method name="read_user_value"> + <arg type="s" name="key" direction="in" /> + <arg type="s" name="type" direction="in" /> + <arg type="ay" name="value" direction="out"> + <annotation name="org.gtk.GDBus.C.ForceGVariant" value="true" /> + </arg> + </method> + + <method name="reset"> + <arg type="s" name="key" direction="in" /> + </method> + + <method name="subscribe"> + <arg type="s" name="name" direction="in" /> + </method> + + <method name="sync"> + </method> + + <method name="unsubscribe"> + <arg type="s" name="name" direction="in" /> + </method> + + <method name="write"> + <arg type="s" name="key" direction="in" /> + <arg type="ay" name="value" direction="in"> + <annotation name="org.gtk.GDBus.C.ForceGVariant" value="true" /> + </arg> + <arg type="b" name="success" direction="out" /> + </method> + + <method name="write_tree"> + <arg type="s" name="path_prefix" direction="in" /> + <arg type="ay" name="tree" direction="in"> + <annotation name="org.gtk.GDBus.C.ForceGVariant" value="true" /> + </arg> + <arg type="b" name="success" direction="out" /> + </method> + + </interface> +</node> diff --git a/src/org.gnome.Terminal.gschema.xml b/src/org.gnome.Terminal.gschema.xml new file mode 100644 index 0000000..a860b73 --- /dev/null +++ b/src/org.gnome.Terminal.gschema.xml @@ -0,0 +1,736 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright © 2002 Havoc Pennington + Copyright © 2002 Jonathan Blandford + Copyright © 2003, 2004 Mariano Suárez-Alvarez + Copyright © 2005 Kjartan Maraas + Copyright © 2005 Tony Tsui + Copyright © 2006 Guilherme de S. Pastore + Copyright © 2009, 2010 Behdad Esfahbod + Copyright © 2008, 2010 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/>. +--> +<schemalist gettext-domain="gnome-terminal"> + + <enum id='org.gnome.Terminal.NewTerminalMode'> + <value nick='window' value='0'/> + <value nick='tab' value='1'/> + </enum> + + <enum id='org.gnome.Terminal.NewTabPosition'> + <value nick='last' value='0'/> + <value nick='next' value='1'/> + </enum> + + <enum id='org.gnome.Terminal.ScrollbarPolicy'> + <value nick='always' value='0'/> + <!-- <value nick='automatic' value='1'/> --> + <value nick='never' value='2'/> + </enum> + + <enum id='org.gnome.Terminal.TabsbarPolicy'> + <value nick='always' value='0'/> + <value nick='automatic' value='1'/> + <!-- <value nick='never' value='2'/> --> + </enum> + + <enum id='org.gnome.Terminal.ThemeVariant'> + <value nick='system' value='0'/> + <value nick='light' value='1'/> + <value nick='dark' value='2'/> + </enum> + + <enum id='org.gnome.Terminal.ExitAction'> + <value nick='close' value='0'/> + <value nick='restart' value='1'/> + <value nick='hold' value='2'/> + </enum> + + <enum id='org.gnome.Terminal.CJKWidth'> + <value nick='narrow' value='1'/> + <value nick='wide' value='2'/> + </enum> + + <enum id="org.gnome.Terminal.PreserveWorkingDirectory"> + <value nick="never" value='0'/> + <value nick="safe" value='1'/> + <value nick="always" value='2'/> + </enum> + + <!-- From gtk+ --> + <enum id="org.gnome.Terminal.TabPosition"> + <!-- <value nick="left" value="0" /> --> + <!-- <value nick="right" value="1" /> --> + <value nick="top" value="2" /> + <value nick="bottom" value="3" /> + </enum> + + <!-- These really belong into some vte-built enums file, but + using enums from other modules still has some + problems. Just include a copy here for now. + --> + <enum id='org.gnome.Terminal.EraseBinding'> + <value nick='auto' value='0'/> + <value nick='ascii-backspace' value='1'/> + <value nick='ascii-delete' value='2'/> + <value nick='delete-sequence' value='3'/> + <value nick='tty' value='4'/> + </enum> + <enum id='org.gnome.Terminal.Cursor.BlinkMode'> + <value nick='system' value='0'/> + <value nick='on' value='1'/> + <value nick='off' value='2'/> + </enum> + <enum id='org.gnome.Terminal.Cursor.Shape'> + <value nick='block' value='0'/> + <value nick='ibeam' value='1'/> + <value nick='underline' value='2'/> + </enum> + <enum id='org.gnome.Terminal.TextBlinkMode'> + <value nick='never' value='0'/> + <value nick='focused' value='1'/> + <value nick='unfocused' value='2'/> + <value nick='always' value='3'/> + </enum> + + <!-- SettingsList base schema --> + + <schema id="org.gnome.Terminal.SettingsList"> + <key name="list" type="as"> + <default>[]</default> + </key> + <key name="default" type="s"> + <default>''</default> + </key> + </schema> + + <!-- Profiles list schema --> + + <schema id="org.gnome.Terminal.ProfilesList" + extends="org.gnome.Terminal.SettingsList" + path="/org/gnome/terminal/legacy/profiles:/"> + <override name="list">['b1dcc9dd-5262-4d8d-a863-c897e6d979b9']</override> + <override name="default">'b1dcc9dd-5262-4d8d-a863-c897e6d979b9'</override> + </schema> + + <!-- A terminal profile --> + + <schema id="org.gnome.Terminal.Legacy.Profile"> + <key name="visible-name" type="s"> + <!-- Translators: This needs to be parsed as a GVariant string, so keep the regular single quotes around the string as-is, and do not add extra quotes. --> + <default l10n="messages" context="visible-name">'Unnamed'</default> + <summary>Human-readable name of the profile</summary> + <description>Human-readable name of the profile.</description> + </key> + <key name="foreground-color" type="s"> + <default>'#171421'</default> + <summary>Default color of text in the terminal</summary> + <description>Default color of text in the terminal, as a color specification (can be HTML-style hex digits, or a color name such as “red”).</description> + </key> + <key name="background-color" type="s"> + <default>'#ffffff'</default> + <summary>Default color of terminal background</summary> + <description>Default color of terminal background, as a color specification (can be HTML-style hex digits, or a color name such as “red”).</description> + </key> + <key name="bold-color" type="s"> + <default>'#000000'</default> + <summary>Default color of bold text in the terminal</summary> + <description>Default color of bold text in the terminal, as a color specification (can be HTML-style hex digits, or a color name such as “red”). This is ignored if bold-color-same-as-fg is true.</description> + </key> + <key name="bold-color-same-as-fg" type="b"> + <default>true</default> + <summary>Whether bold text should use the same color as normal text</summary> + <description>If true, boldface text will be rendered using the same color as normal text.</description> + </key> + <key name="cell-height-scale" type="d"> + <range min="1.0" max="2.0" /> + <default>1.0</default> + <summary>Scale factor for the cell height to increase line spacing. (Does not increase the font’s height.)</summary> + </key> + <key name="cell-width-scale" type="d"> + <range min="1.0" max="2.0" /> + <default>1.0</default> + <summary>Scale factor for the cell width to increase letter spacing. (Does not increase the font’s width.)</summary> + </key> + <key name="cursor-colors-set" type="b"> + <default>false</default> + <summary>Whether to use custom cursor colors</summary> + <description>If true, use the cursor colors from the profile.</description> + </key> + <key name="cursor-background-color" type="s"> + <default>'#000000'</default> + <summary>Cursor background color</summary> + <description>Custom color of the background of the terminal’s cursor, as a color specification (can be HTML-style hex digits, or a color name such as “red”). This is ignored if cursor-colors-set is false.</description> + </key> + <key name="cursor-foreground-color" type="s"> + <default>'#ffffff'</default> + <summary>Cursor foreground colour</summary> + <description>Custom color for the foreground of the text character at the terminal’s cursor position, as a color specification (can be HTML-style hex digits, or a color name such as “red”). This is ignored if cursor-colors-set is false.</description> + </key> + <key name="highlight-colors-set" type="b"> + <default>false</default> + <summary>Whether to use custom highlight colors</summary> + <description>If true, use the highlight colors from the profile.</description> + </key> + <key name="highlight-background-color" type="s"> + <default>'#000000'</default> + <summary>Highlight background color</summary> + <description>Custom color of the background of the terminal’s highlight, as a color specification (can be HTML-style hex digits, or a color name such as “red”). This is ignored if highlight-colors-set is false.</description> + </key> + <key name="highlight-foreground-color" type="s"> + <default>'#ffffff'</default> + <summary>Highlight foreground colour</summary> + <description>Custom color for the foreground of the text character at the terminal’s highlight position, as a color specification (can be HTML-style hex digits, or a color name such as “red”). This is ignored if highlight-colors-set is false.</description> + </key> + <key name="enable-bidi" type="b"> + <default>true</default> + <summary>Whether to perform bidirectional text rendering</summary> + <description>If true, perform bidirectional text rendering (“BiDi”).</description> + </key> + <key name="enable-shaping" type="b"> + <default>true</default> + <summary>Whether to perform Arabic shaping</summary> + <description>If true, shape Arabic text.</description> + </key> + <key name="enable-sixel" type="b"> + <default>false</default> + <summary>Whether to enable SIXEL images</summary> + <description>If true, SIXEL sequences are parsed and images are rendered.</description> + </key> + <key name="bold-is-bright" type="b"> + <default>false</default> + <summary>Whether bold is also bright</summary> + <description>If true, setting bold on the first 8 colors also switches to their bright variants.</description> + </key> + <key name="audible-bell" type="b"> + <default>true</default> + <summary>Whether to ring the terminal bell</summary> + </key> + <key name="word-char-exceptions" type="ms"> + <default>nothing</default> + <summary>List of ASCII punctuation characters that are not to be treated as part of a word when doing word-wise selection</summary> + </key> + <key name="default-size-columns" type="i"> + <range min="16" max="511" /> + <default>80</default> + <summary>Default number of columns</summary> + <description>Number of columns in newly created terminal windows. Has no effect if use_custom_default_size is not enabled.</description> + </key> + <key name="default-size-rows" type="i"> + <range min="4" max="511" /> + <default>24</default> + <summary>Default number of rows</summary> + <description>Number of rows in newly created terminal windows. Has no effect if use_custom_default_size is not enabled.</description> + </key> + <key name="scrollbar-policy" enum="org.gnome.Terminal.ScrollbarPolicy"> + <default>'always'</default> + <summary>When to show the scrollbar</summary> + </key> + <key name="scrollback-lines" type="i"> + <default>10000</default> + <summary>Number of lines to keep in scrollback</summary> + <description>Number of scrollback lines to keep around. You can scroll back in the terminal by this number of lines; lines that don’t fit in the scrollback are discarded. If scrollback_unlimited is true, this value is ignored.</description> + </key> + <key name="scrollback-unlimited" type="b"> + <default>false</default> + <summary>Whether an unlimited number of lines should be kept in scrollback</summary> + <description>If true, scrollback lines will never be discarded. The scrollback history is stored on disk temporarily, so this may cause the system to run out of disk space if there is a lot of output to the terminal.</description> + </key> + <key name="scroll-on-keystroke" type="b"> + <default>true</default> + <summary>Whether to scroll to the bottom when a key is pressed</summary> + <description>If true, pressing a key jumps the scrollbar to the bottom.</description> + </key> + <key name="scroll-on-output" type="b"> + <default>false</default> + <summary>Whether to scroll to the bottom when there’s new output</summary> + <description>If true, whenever there’s new output the terminal will scroll to the bottom.</description> + </key> + <key name="exit-action" enum="org.gnome.Terminal.ExitAction"> + <default>'close'</default> + <summary>What to do with the terminal when the child command exits</summary> + <description>Possible values are “close” to close the terminal, “restart” to restart the command, and “hold” to keep the terminal open with no command running inside.</description> + </key> + <key name="login-shell" type="b"> + <default>false</default> + <summary>Whether to launch the command in the terminal as a login shell</summary> + <description>If true, the command inside the terminal will be launched as a login shell (argv[0] will have a hyphen in front of it).</description> + </key> + <key name="preserve-working-directory" enum="org.gnome.Terminal.PreserveWorkingDirectory"> + <default>'safe'</default> + <summary>Whether to preserve the working directory when opening a new terminal</summary> + <description> + Controls when opening a new terminal from a previous one carries over the working directory of the opening terminal to the new one. + </description> + </key> + <key name="use-custom-command" type="b"> + <default>false</default> + <summary>Whether to run a custom command instead of the shell</summary> + <description>If true, the value of the custom_command setting will be used in place of running a shell.</description> + </key> + <key name="cursor-blink-mode" enum="org.gnome.Terminal.Cursor.BlinkMode"> + <default>'system'</default> + <summary>Whether to blink the cursor</summary> + <description>The possible values are “system” to use the global cursor blinking settings, or “on” or “off” to set the mode explicitly.</description> + </key> + <key name="cursor-shape" enum="org.gnome.Terminal.Cursor.Shape"> + <default>'block'</default> + <summary>The cursor appearance</summary> + </key> + <key name="text-blink-mode" enum="org.gnome.Terminal.TextBlinkMode"> + <default>'always'</default> + <summary>Possible values are “always” or “never” allow blinking text, or only when the terminal is “focused” or “unfocused”.</summary> + </key> + <key name="custom-command" type="s"> + <default>''</default> + <summary>Custom command to use instead of the shell</summary> + <description>Run this command in place of the shell, if use_custom_command is true.</description> + </key> + <key name="palette" type="as"> + <default>['#171421', + '#c01c28', + '#26a269', + '#a2734c', + '#12488b', + '#a347ba', + '#2aa1b3', + '#d0cfcc', + '#5e5c64', + '#f66151', + '#33da7a', + '#e9ad0c', + '#2a7bde', + '#c061cb', + '#33c7de', + '#ffffff']</default> + <summary>Palette for terminal applications</summary> + </key> + <key name="font" type="s"> + <default>'Monospace 12'</default> + <summary>A Pango font name and size</summary> + </key> + <key name="backspace-binding" enum="org.gnome.Terminal.EraseBinding"> + <default>'ascii-delete'</default> + <summary>The code sequence the Backspace key generates</summary> + </key> + <key name="delete-binding" enum="org.gnome.Terminal.EraseBinding"> + <default>'delete-sequence'</default> + <summary>The code sequence the Delete key generates</summary> + </key> + <key name="use-theme-colors" type="b"> + <default>true</default> + <summary>Whether to use the colors from the theme for the terminal widget</summary> + </key> + <key name="use-system-font" type="b"> + <default>true</default> + <summary>Whether to use the system monospace font</summary> + </key> + <key name="rewrap-on-resize" type="b"> + <default>true</default> + <summary>Whether to rewrap the terminal contents on window resize</summary> + </key> + <key name="encoding" type="s"> + <default>'UTF-8'</default> + <summary>Which encoding to use</summary> + </key> + <key name="cjk-utf8-ambiguous-width" enum="org.gnome.Terminal.CJKWidth"> + <default>'narrow'</default> + <summary>Whether ambiguous-width characters are narrow or wide when using UTF-8 encoding</summary> + </key> + </schema> + + <!-- Keybinding settings --> + + <schema id="org.gnome.Terminal.Legacy.Keybindings"> + <key name="new-tab" type="s"> + <default>'<Control><Shift>t'</default> + <summary>Keyboard shortcut to open a new tab</summary> + </key> + <key name="new-window" type="s"> + <default>'<Control><Shift>n'</default> + <summary>Keyboard shortcut to open a new window</summary> + </key> + <key name="save-contents" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to save the current tab contents to file</summary> + </key> + <key name="export" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to export the current tab contents to file in various formats</summary> + </key> + <key name="print" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to print the current tab contents to printer or file</summary> + </key> + <key name="close-tab" type="s"> + <default>'<Control><Shift>w'</default> + <summary>Keyboard shortcut to close a tab</summary> + </key> + <key name="close-window" type="s"> + <default>'<Control><Shift>q'</default> + <summary>Keyboard shortcut to close a window</summary> + </key> + <key name="copy" type="s"> + <default>'<Control><Shift>c'</default> + <summary>Keyboard shortcut to copy text</summary> + </key> + <key name="copy-html" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to copy text as HTML</summary> + </key> + <key name="paste" type="s"> + <default>'<Control><Shift>v'</default> + <summary>Keyboard shortcut to paste text</summary> + </key> + <key name="select-all" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to select all text</summary> + </key> + <key name="preferences" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to open the Preferences dialog</summary> + </key> + <key name="full-screen" type="s"> + <default>'F11'</default> + <summary>Keyboard shortcut to toggle full screen mode</summary> + </key> + <key name="toggle-menubar" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to toggle the visibility of the menubar</summary> + </key> + <key name="read-only" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to toggle the read-only state</summary> + </key> + <key name="reset" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to reset the terminal</summary> + </key> + <key name="reset-and-clear" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to reset and clear the terminal</summary> + </key> + <key name="find" type="s"> + <default>'<Control><Shift>F'</default> + <summary>Keyboard shortcut to open the search dialog</summary> + </key> + <key name="find-next" type="s"> + <default>'<Control><Shift>G'</default> + <summary>Keyboard shortcut to find the next occurrence of the search term</summary> + </key> + <key name="find-previous" type="s"> + <default>'<Control><Shift>H'</default> + <summary>Keyboard shortcut to find the previous occurrence of the search term</summary> + </key> + <key name="find-clear" type="s"> + <default>'<Control><Shift>J'</default> + <summary>Keyboard shortcut to clear the find highlighting</summary> + </key> + <key name="prev-tab" type="s"> + <default>'<Control>Page_Up'</default> + <summary>Keyboard shortcut to switch to the previous tab</summary> + </key> + <key name="next-tab" type="s"> + <default>'<Control>Page_Down'</default> + <summary>Keyboard shortcut to switch to the next tab</summary> + </key> + <key name="move-tab-left" type="s"> + <default>'<Control><Shift>Page_Up'</default> + <summary>Keyboard shortcut to move the current tab to the left</summary> + </key> + <key name="move-tab-right" type="s"> + <default>'<Control><Shift>Page_Down'</default> + <summary>Keyboard shortcut to move the current tab to the right</summary> + </key> + <key name="detach-tab" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to detach current tab</summary> + </key> + <key name="switch-to-tab-1" type="s"> + <default>'<Alt>1'</default> + <summary>Keyboard shortcut to switch to the numbered tab</summary> + </key> + <key name="switch-to-tab-2" type="s"> + <default>'<Alt>2'</default> + <summary>Keyboard shortcut to switch to the numbered tab</summary> + </key> + <key name="switch-to-tab-3" type="s"> + <default>'<Alt>3'</default> + <summary>Keyboard shortcut to switch to the numbered tab</summary> + </key> + <key name="switch-to-tab-4" type="s"> + <default>'<Alt>4'</default> + <summary>Keyboard shortcut to switch to the numbered tab</summary> + </key> + <key name="switch-to-tab-5" type="s"> + <default>'<Alt>5'</default> + <summary>Keyboard shortcut to switch to the numbered tab</summary> + </key> + <key name="switch-to-tab-6" type="s"> + <default>'<Alt>6'</default> + <summary>Keyboard shortcut to switch to the numbered tab</summary> + </key> + <key name="switch-to-tab-7" type="s"> + <default>'<Alt>7'</default> + <summary>Keyboard shortcut to switch to the numbered tab</summary> + </key> + <key name="switch-to-tab-8" type="s"> + <default>'<Alt>8'</default> + <summary>Keyboard shortcut to switch to the numbered tab</summary> + </key> + <key name="switch-to-tab-9" type="s"> + <default>'<Alt>9'</default> + <summary>Keyboard shortcut to switch to the numbered tab</summary> + </key> + <key name="switch-to-tab-10" type="s"> + <default>'<Alt>0'</default> + <summary>Keyboard shortcut to switch to the numbered tab</summary> + </key> + <key name="switch-to-tab-11" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to switch to the numbered tab</summary> + </key> + <key name="switch-to-tab-12" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to switch to the numbered tab</summary> + </key> + <key name="switch-to-tab-13" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to switch to the numbered tab</summary> + </key> + <key name="switch-to-tab-14" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to switch to the numbered tab</summary> + </key> + <key name="switch-to-tab-15" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to switch to the numbered tab</summary> + </key> + <key name="switch-to-tab-16" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to switch to the numbered tab</summary> + </key> + <key name="switch-to-tab-17" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to switch to the numbered tab</summary> + </key> + <key name="switch-to-tab-18" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to switch to the numbered tab</summary> + </key> + <key name="switch-to-tab-19" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to switch to the numbered tab</summary> + </key> + <key name="switch-to-tab-20" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to switch to the numbered tab</summary> + </key> + <key name="switch-to-tab-21" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to switch to the numbered tab</summary> + </key> + <key name="switch-to-tab-22" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to switch to the numbered tab</summary> + </key> + <key name="switch-to-tab-23" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to switch to the numbered tab</summary> + </key> + <key name="switch-to-tab-24" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to switch to the numbered tab</summary> + </key> + <key name="switch-to-tab-25" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to switch to the numbered tab</summary> + </key> + <key name="switch-to-tab-26" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to switch to the numbered tab</summary> + </key> + <key name="switch-to-tab-27" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to switch to the numbered tab</summary> + </key> + <key name="switch-to-tab-28" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to switch to the numbered tab</summary> + </key> + <key name="switch-to-tab-29" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to switch to the numbered tab</summary> + </key> + <key name="switch-to-tab-30" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to switch to the numbered tab</summary> + </key> + <key name="switch-to-tab-31" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to switch to the numbered tab</summary> + </key> + <key name="switch-to-tab-32" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to switch to the numbered tab</summary> + </key> + <key name="switch-to-tab-33" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to switch to the numbered tab</summary> + </key> + <key name="switch-to-tab-34" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to switch to the numbered tab</summary> + </key> + <key name="switch-to-tab-35" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to switch to the numbered tab</summary> + </key> + <key name="switch-to-tab-last" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to switch to the last tab</summary> + </key> + <key name="help" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to launch help</summary> + </key> + <key name="zoom-in" type="s"> + <default>'<Control>plus'</default> + <summary>Keyboard shortcut to make font larger</summary> + </key> + <key name="zoom-out" type="s"> + <default>'<Control>minus'</default> + <summary>Keyboard shortcut to make font smaller</summary> + </key> + <key name="zoom-normal" type="s"> + <default>'<Control>0'</default> + <summary>Keyboard shortcut to make font normal-size</summary> + </key> + <key name="header-menu" type="s"> + <default>'disabled'</default> + <summary>Keyboard shortcut to show the primary menu</summary> + </key> + </schema> + + <!-- Global settings --> + + <schema id="org.gnome.Terminal.Legacy.Settings" path="/org/gnome/terminal/legacy/"> + + <key name="mnemonics-enabled" type="b"> + <default>false</default> + <summary>Whether the menubar has access keys</summary> + <description> + Whether to have Alt+letter access keys for the menubar. + They may interfere with some applications run inside the terminal + so it’s possible to turn them off. + </description> + </key> + + <key name="shortcuts-enabled" type="b"> + <default>true</default> + <summary>Whether shortcuts are enabled</summary> + <description> + Whether shortcuts are enabled. + They may interfere with some applications run inside the terminal + so it’s possible to turn them off. + </description> + </key> + + <key name="menu-accelerator-enabled" type="b"> + <default>true</default> + <summary>Whether the standard GTK shortcut for menubar access is enabled</summary> + <description> + Normally you can access the menubar with F10. This can also + be customized via gtkrc (gtk-menu-bar-accel = + "whatever"). This option allows the standard menubar + accelerator to be disabled. + </description> + </key> + + <key name="shell-integration-enabled" type="b"> + <default>false</default> + <summary>Whether the shell integration is enabled</summary> + </key> + + <key name="confirm-close" type="b"> + <default>true</default> + <summary>Whether to ask for confirmation before closing a terminal</summary> + </key> + + <key name="context-info" type="as"> + <default>['numbers']</default> + <summary>Additional info section items to appear in the context menu</summary> + </key> + + <key name="default-show-menubar" type="b"> + <default>true</default> + <summary>Whether to show the menubar in new windows</summary> + </key> + + <key name="new-terminal-mode" enum="org.gnome.Terminal.NewTerminalMode"> + <default>'window'</default> + <summary>Whether to open new terminals as windows or tabs</summary> + </key> + + <key name="tab-policy" enum="org.gnome.Terminal.TabsbarPolicy"> + <default>'automatic'</default> + <summary>When to show the tabs bar</summary> + </key> + + <key name="tab-position" enum="org.gnome.Terminal.TabPosition"> + <default>'top'</default> + <summary>The position of the tab bar</summary> + </key> + + <key name="theme-variant" enum="org.gnome.Terminal.ThemeVariant"> + <default>'system'</default> + <summary>Which theme variant to use</summary> + </key> + + <key name="new-tab-position" enum="org.gnome.Terminal.NewTabPosition"> + <default>'last'</default> + <summary>Whether new tabs should open next to the current one or at the last position</summary> + </key> + + <!-- Default terminal --> + + <key name="always-check-default-terminal" type="b"> + <default>true</default> + <summary>Always check whether GNOME Terminal is the default terminal</summary> + </key> + + <!-- Note that changing the following settings will only take effect + when gnome-terminal-server is restarted. + --> + + <key name="headerbar" type="mb"> + <default>nothing</default> + </key> + + <key name="unified-menu" type="b"> + <default>true</default> + </key> + + <!-- <child name="profiles" schema="org.gnome.Terminal.ProfilesList" /> --> + + <child name="keybindings" schema="org.gnome.Terminal.Legacy.Keybindings" /> + + <key name="schema-version" type="u"> + <default>3</default> + </key> + + </schema> + +</schemalist> diff --git a/src/org.gnome.Terminal.service.in b/src/org.gnome.Terminal.service.in new file mode 100644 index 0000000..70c0930 --- /dev/null +++ b/src/org.gnome.Terminal.service.in @@ -0,0 +1,4 @@ +[D-BUS Service] +Name=@gt_dns_name@ +SystemdService=@gt_name@-server.service +Exec=@libexecdir@/gnome-terminal-server diff --git a/src/org.gnome.Terminal.xml b/src/org.gnome.Terminal.xml new file mode 100644 index 0000000..a6ada0e --- /dev/null +++ b/src/org.gnome.Terminal.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Introspection 0.1//EN" + "http://www.freedesktop.org/software/dbus/introspection.dtd"> +<!-- + Copyright © 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, or (at your option) + any later version. + + This program is distributed in the hope conf 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/>. +--> +<node> + <interface name="org.gnome.Terminal.Factory0"> + <annotation name="org.gtk.GDBus.C.Name" value="Factory" /> + <method name="CreateInstance"> + <arg type="a{sv}" name="options" direction="in" /> + <arg type="o" name="receiver" direction="out" /> + </method> + </interface> + + <interface name="org.gnome.Terminal.Terminal0"> + <annotation name="org.gtk.GDBus.C.Name" value="Receiver" /> + <method name="Exec"> + <annotation name="org.gtk.GDBus.C.UnixFD" value="true" /> + <arg type="a{sv}" name="options" direction="in" /> + <arg type="aay" name="arguments" direction="in"> + <annotation name="org.gtk.GDBus.C.ForceGVariant" value="true" /> + </arg> + </method> + + <signal name="ChildExited"> + <arg type="i" name="exit_code" direction="in" /> + </signal> + </interface> +</node> diff --git a/src/preferences.ui b/src/preferences.ui new file mode 100644 index 0000000..ca58c65 --- /dev/null +++ b/src/preferences.ui @@ -0,0 +1,2486 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.19.0 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkListStore" id="new-terminal-mode-liststore"> + <columns> + <!-- column-name label --> + <column type="gchararray"/> + <!-- column-name id --> + <column type="gchararray"/> + </columns> + <data> + <row> + <col id="0" translatable="yes" comments="Open new terminal in new window">Window</col> + <col id="1">window</col> + </row> + <row> + <col id="0" translatable="yes" comments="Open new terminal in new tab">Tab</col> + <col id="1">tab</col> + </row> + </data> + </object> + <object class="GtkListStore" id="new-tab-position-liststore"> + <columns> + <!-- column-name label --> + <column type="gchararray"/> + <!-- column-name id --> + <column type="gchararray"/> + </columns> + <data> + <row> + <col id="0" translatable="yes" comments="New tab opens at the last position">Last</col> + <col id="1">last</col> + </row> + <row> + <col id="0" translatable="yes" comments="New tab opens next to current tab">Next</col> + <col id="1">next</col> + </row> + </data> + </object> + <object class="GtkListStore" id="theme-variant-liststore"> + <columns> + <!-- column-name label --> + <column type="gchararray"/> + <!-- column-name id --> + <column type="gchararray"/> + </columns> + <data> + <row> + <col id="0" translatable="yes" context="theme variant">Default</col> + <col id="1">system</col> + </row> + <row> + <col id="0" translatable="yes" context="theme variant">Light</col> + <col id="1">light</col> + </row> + <row> + <col id="0" translatable="yes" context="theme variant">Dark</col> + <col id="1">dark</col> + </row> + </data> + </object> + <object class="GtkAdjustment" id="adjustment1"> + <property name="lower">16</property> + <property name="upper">511</property> + <property name="value">80</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment2"> + <property name="lower">4</property> + <property name="upper">511</property> + <property name="value">24</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment4"> + <property name="lower">0</property> + <property name="upper">2147483647</property> + <property name="value">10000</property> + <property name="step_increment">1000</property> + <property name="page_increment">10000</property> + </object> + <object class="GtkAdjustment" id="adjustment5"> + <property name="lower">1.0</property> + <property name="upper">2.0</property> + <property name="value">1.0</property> + <property name="step_increment">0.05</property> + <property name="page_increment">0.25</property> + </object> + <object class="GtkAdjustment" id="adjustment6"> + <property name="lower">1.0</property> + <property name="upper">2.0</property> + <property name="value">1.0</property> + <property name="step_increment">0.05</property> + <property name="page_increment">0.25</property> + </object> + <object class="GtkListStore" id="cjk-ambiguous-width-model"> + <columns> + <!-- column-name gchararray --> + <column type="gchararray"/> + <!-- column-name gchararray1 --> + <column type="gchararray"/> + </columns> + <data> + <row> + <col id="0" translatable="yes" comments="ambiguous-width characers are">Narrow</col> + <col id="1">narrow</col> + </row> + <row> + <col id="0" translatable="yes" comments="ambiguous-width characers are">Wide</col> + <col id="1">wide</col> + </row> + </data> + </object> + <object class="GtkListStore" id="model1"> + <columns> + <!-- column-name gchararray --> + <column type="gchararray"/> + </columns> + <data> + <row> + <col id="0" translatable="yes" comments="Cursor shape">Block</col> + </row> + <row> + <col id="0" translatable="yes" comments="Cursor shape">I-Beam</col> + </row> + <row> + <col id="0" translatable="yes" comments="Cursor shape">Underline</col> + </row> + </data> + </object> + <object class="GtkListStore" id="model7"> + <columns> + <!-- column-name gchararray --> + <column type="gchararray"/> + </columns> + <data> + <row> + <col id="0" translatable="yes" comments="Cursor blink mode">Default</col> + </row> + <row> + <col id="0" translatable="yes" comments="Cursor blink mode">Enabled</col> + </row> + <row> + <col id="0" translatable="yes" comments="Cursor blink mode">Disabled</col> + </row> + </data> + </object> + <object class="GtkListStore" id="model5"> + <columns> + <!-- column-name gchararray --> + <column type="gchararray"/> + </columns> + <data> + <row> + <col id="0" translatable="yes" comments="Text blink mode">Never</col> + </row> + <row> + <col id="0" translatable="yes" comments="Text blink mode">When focused</col> + </row> + <row> + <col id="0" translatable="yes" comments="Text blink mode">When unfocused</col> + </row> + <row> + <col id="0" translatable="yes" comments="Text blink mode">Always</col> + </row> + </data> + </object> + <object class="GtkListStore" id="model2"> + <columns> + <!-- column-name gchararray --> + <column type="gchararray"/> + </columns> + <data> + <row> + <col id="0" translatable="yes" comments="When terminal commands set their own titles">Replace initial title</col> + </row> + <row> + <col id="0" translatable="yes" comments="When terminal commands set their own titles">Append initial title</col> + </row> + <row> + <col id="0" translatable="yes" comments="When terminal commands set their own titles">Prepend initial title</col> + </row> + <row> + <col id="0" translatable="yes" comments="When terminal commands set their own titles">Keep initial title</col> + </row> + </data> + </object> + <object class="GtkListStore" id="model3"> + <columns> + <!-- column-name gchararray --> + <column type="gchararray"/> + </columns> + <data> + <row> + <col id="0" translatable="yes" comments="When command exits">Exit the terminal</col> + </row> + <row> + <col id="0" translatable="yes" comments="When command exits">Restart the command</col> + </row> + <row> + <col id="0" translatable="yes" comments="When command exits">Hold the terminal open</col> + </row> + </data> + </object> + <object class="GtkListStore" id="model4"> + <columns> + <!-- column-name gchararray --> + <column type="gchararray"/> + </columns> + <data> + <row> + <col id="0" translatable="yes" comments="This is the name of a colour scheme">GNOME</col> + </row> + <row> + <col id="0" translatable="yes" comments="This is the name of a colour scheme">Tango</col> + </row> + <row> + <col id="0" translatable="yes" comments="This is the name of a colour scheme">Linux console</col> + </row> + <row> + <col id="0" translatable="yes" comments="This is the name of a colour scheme">XTerm</col> + </row> + <row> + <col id="0" translatable="yes" comments="This is the name of a colour scheme">Rxvt</col> + </row> + <row> + <col id="0" translatable="yes" comments="This is the name of a colour scheme">Solarized</col> + </row> + <row> + <col id="0" translatable="yes" comments="This is the name of a colour scheme">Custom</col> + </row> + </data> + </object> + <object class="GtkListStore" id="model6"> + <columns> + <!-- column-name gchararray --> + <column type="gchararray"/> + </columns> + <data> + <row> + <col id="0" translatable="yes" comments="This refers to the Delete keybinding option">Automatic</col> + </row> + <row> + <col id="0" translatable="yes" comments="This refers to the Delete keybinding option">Control-H</col> + </row> + <row> + <col id="0" translatable="yes" comments="This refers to the Delete keybinding option">ASCII DEL</col> + </row> + <row> + <col id="0" translatable="yes" comments="This refers to the Delete keybinding option">Escape sequence</col> + </row> + <row> + <col id="0" translatable="yes" comments="This refers to the Delete keybinding option">TTY Erase</col> + </row> + </data> + </object> + <object class="GtkListStore" id="preserve-working-directory-model"> + <columns> + <!-- column-name label --> + <column type="gchararray"/> + <!-- column-name id --> + <column type="gchararray"/> + </columns> + <data> + <row> + <col id="0" translatable="yes" comments="Preserve working directory">Never</col> + <col id="1">never</col> + </row> + <row> + <col id="0" translatable="yes" comments="Preserve working directory">Shell only</col> + <col id="1">safe</col> + </row> + <row> + <col id="0" translatable="yes" comments="Preserve working directory">Always</col> + <col id="1">always</col> + </row> + </data> + </object> + <object class="GtkApplicationWindow" id="preferences-dialog"> + <property name="can_focus">False</property> + <property name="show_menubar">False</property> + <property name="role">gnome-terminal-preferences</property> + <property name="resizable">False</property> + <child> + <object class="GtkBox" id="dialogue-content-box"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin">12</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <child> + <object class="GtkHBox" id="main-hbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkFrame"> + <property name="visible">True</property> + <child> + <object class="GtkScrolledWindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">never</property> + <child> + <object class="GtkViewport"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkListBox" id="the-listbox"> + <property name="visible">True</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkStack" id="the-stack"> + <property name="visible">True</property> + <child> + <object class="GtkEventBox" id="bug722114-comment25-1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkFrame" id="general-frame"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="border_width">12</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <child> + <object class="GtkCheckButton" id="default-show-menubar-checkbutton"> + <property name="label" translatable="yes">_Show menubar by default in new terminals</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="xalign">0</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="disable-mnemonics-checkbutton"> + <property name="label" translatable="yes">_Enable mnemonics (such as Alt+F to open the File menu)</property> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="xalign">0</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="disable-menu-accel-checkbutton"> + <property name="label" translatable="yes">Enable the _menu accelerator key (F10 by default)</property> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="xalign">0</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkGrid" id="grid1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="row_spacing">6</property> + <property name="column_spacing">12</property> + <child> + <object class="GtkLabel" id="theme-variant-label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Theme _variant:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">theme-variant-combobox</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="top_attach">0</property> + <property name="left_attach">0</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="theme-variant-combobox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="model">theme-variant-liststore</property> + <property name="id_column">1</property> + <child> + <object class="GtkCellRendererText" id="renderer1a"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + <packing> + <property name="top_attach">0</property> + <property name="left_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="new-terminal-mode-label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Open _new terminals in:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">new-terminal-mode-combobox</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="left_attach">0</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="new-terminal-mode-combobox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="model">new-terminal-mode-liststore</property> + <property name="id_column">1</property> + <child> + <object class="GtkCellRendererText" id="renderer2a"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + <packing> + <property name="top_attach">1</property> + <property name="left_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="new-tab-position-label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">New tab _position:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">new-tab-position-combobox</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="top_attach">2</property> + <property name="left_attach">0</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="new-tab-position-combobox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="model">new-tab-position-liststore</property> + <property name="id_column">1</property> + <child> + <object class="GtkCellRendererText" id="renderer3a"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + <packing> + <property name="top_attach">2</property> + <property name="left_attach">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="always-check-default-checkbutton"> + <property name="label" translatable="yes">_Always check if default terminal</property> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="xalign">0</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">4</property> + </packing> + </child> + <child> + <object class="GtkButton" id="make-default-button"> + <property name="label" translatable="yes">_Set as default terminal</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="focus_on_click">False</property> + <property name="halign">start</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">5</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="name">general-prefs</property> + </packing> + </child> + <child> + <object class="GtkEventBox" id="bug722114-comment25-2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkFrame" id="keybindings-frame"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkGrid"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="border_width">12</property> + <property name="column_spacing">12</property> + <property name="row_spacing">6</property> + <child> + <object class="GtkCheckButton" id="disable-shortcuts-checkbutton"> + <property name="label" translatable="yes">_Enable shortcuts</property> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="xalign">0</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="top_attach">0</property> + <property name="left_attach">0</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">never</property> + <property name="shadow_type">in</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <child> + <object class="GtkTreeView" id="accelerators-treeview"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="rules_hint">True</property> + <child internal-child="selection"> + <object class="GtkTreeSelection" id="treeview-selection"/> + </child> + </object> + </child> + </object> + <packing> + <property name="top_attach">1</property> + <property name="left_attach">0</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="name">shortcut-prefs</property> + </packing> + </child> + <child> + <object class="GtkEventBox" id="bug722114-comment25-3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkNotebook" id="profile-editor-notebook"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <object class="GtkGrid" id="general-table"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="border_width">12</property> + <property name="column_spacing">12</property> + <property name="row_spacing">6</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Text Appearance</property> + <property name="xalign">0</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="top_attach">0</property> + <property name="left_attach">0</property> + <property name="width">4</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="default-size-label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_start">12</property> + <property name="label" translatable="yes">Initial terminal si_ze:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">default-size-columns-spinbutton</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="left_attach">0</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="default-size-columns-box"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <child> + <object class="GtkSpinButton" id="default-size-columns-spinbutton"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="adjustment">adjustment1</property> + <property name="numeric">True</property> + <property name="input_hints">GTK_INPUT_HINT_NO_EMOJI</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="default-size-columns-label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">columns</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="top_attach">1</property> + <property name="left_attach">1</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="default-size-rows-box"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <child> + <object class="GtkSpinButton" id="default-size-rows-spinbutton"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="adjustment">adjustment2</property> + <property name="numeric">True</property> + <property name="input_hints">GTK_INPUT_HINT_NO_EMOJI</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="default-size-rows-label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">rows</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="top_attach">1</property> + <property name="left_attach">2</property> + </packing> + </child> + <child> + <object class="GtkButtonBox" id="buttonbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="default-size-reset-button"> + <property name="label" translatable="yes">Rese_t</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <property name="focus_on_click">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="top_attach">1</property> + <property name="left_attach">3</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="custom-font-checkbutton"> + <property name="label" translatable="yes">Custom _font:</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="margin_start">12</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="xalign">0</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="top_attach">2</property> + <property name="left_attach">0</property> + </packing> + </child> + <child> + <object class="GtkFontButton" id="font-selector"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="focus_on_click">False</property> + <property name="font">Sans 12</property> + <property name="title" translatable="yes">Choose A Terminal Font</property> + </object> + <packing> + <property name="top_attach">2</property> + <property name="left_attach">1</property> + <property name="width">3</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_start">12</property> + <property name="label" translatable="yes">Cell spaci_ng:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">cell-width-scale-spinbutton</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="top_attach">3</property> + <property name="left_attach">0</property> + </packing> + </child> + <child> + <object class="GtkHBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <child> + <object class="GtkSpinButton" id="cell-width-scale-spinbutton"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="adjustment">adjustment5</property> + <property name="digits">2</property> + <property name="numeric">True</property> + <property name="input_hints">GTK_INPUT_HINT_NO_EMOJI</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="cell-width-scale-label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="top_attach">3</property> + <property name="left_attach">1</property> + </packing> + </child> + <child> + <object class="GtkHBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <child> + <object class="GtkSpinButton" id="cell-height-scale-spinbutton"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="adjustment">adjustment6</property> + <property name="digits">2</property> + <property name="numeric">True</property> + <property name="input_hints">GTK_INPUT_HINT_NO_EMOJI</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="cell-height-scale-label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="top_attach">3</property> + <property name="left_attach">2</property> + </packing> + </child> + <child> + <object class="GtkButtonBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="cell-scale-reset-button"> + <property name="label" translatable="yes">Rese_t</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <property name="focus_on_click">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="top_attach">3</property> + <property name="left_attach">3</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label481"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_start">12</property> + <property name="label" translatable="yes">Allow b_linking text:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">text-blink-mode-combobox</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="top_attach">4</property> + <property name="left_attach">0</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="text-blink-mode-combobox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="model">model5</property> + <property name="focus_on_click">False</property> + <child> + <object class="GtkCellRendererText" id="renderer2"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + <packing> + <property name="top_attach">4</property> + <property name="left_attach">1</property> + <property name="width">3</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_top">12</property> + <property name="label" translatable="yes">Cursor</property> + <property name="xalign">0</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="top_attach">5</property> + <property name="left_attach">0</property> + <property name="width">4</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label480"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_start">12</property> + <property name="label" translatable="yes">Cursor _shape:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">cursor-shape-combobox</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="top_attach">6</property> + <property name="left_attach">0</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="cursor-shape-combobox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="model">model1</property> + <property name="focus_on_click">False</property> + <child> + <object class="GtkCellRendererText" id="renderer1"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + <packing> + <property name="top_attach">6</property> + <property name="left_attach">1</property> + <property name="width">3</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_start">12</property> + <property name="label" translatable="yes">Cursor blin_king:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">cursor-blink-mode-combobox</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="top_attach">7</property> + <property name="left_attach">0</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="cursor-blink-mode-combobox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="model">model7</property> + <property name="focus_on_click">False</property> + <child> + <object class="GtkCellRendererText" id="renderer5"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + <packing> + <property name="top_attach">7</property> + <property name="left_attach">1</property> + <property name="width">3</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_top">12</property> + <property name="label" translatable="yes">Sound</property> + <property name="xalign">0</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="top_attach">8</property> + <property name="left_attach">0</property> + <property name="width">4</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="bell-checkbutton"> + <property name="label" translatable="yes">Terminal _bell</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="margin_start">12</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="xalign">0.5</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="top_attach">9</property> + <property name="left_attach">0</property> + <property name="width">4</property> + </packing> + </child> + <child> + <object class="GtkHBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <property name="valign">end</property> + <property name="vexpand">True</property> + <child> + <object class="GtkLabel" id="profile-uuid-label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Profile ID:</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="profile-uuid"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="selectable">True</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="top_attach">100</property> + <property name="left_attach">0</property> + <property name="width">4</property> + </packing> + </child> + </object> + <packing> + <property name="position">0</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel" id="label32"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Text</property> + <property name="use_underline">True</property> + <property name="justify">center</property> + </object> + <packing> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="vbox90"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="border_width">12</property> + <property name="spacing">18</property> + <child> + <object class="GtkVBox" id="vbox82"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <child> + <object class="GtkLabel" id="label39"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Text and Background Color</property> + <property name="xalign">0</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment10105"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">12</property> + <child> + <object class="GtkVBox" id="vbox94"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <child> + <object class="GtkCheckButton" id="use-theme-colors-checkbutton"> + <property name="label" translatable="yes">_Use colors from system theme</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="xalign">0.5</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="colors-box"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <child> + <object class="GtkHBox" id="builtin-color-schemes-box"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">12</property> + <child> + <object class="GtkLabel" id="color-scheme-combobox-label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Built-in sche_mes:</property> + <property name="use_underline">True</property> + <property name="justify">center</property> + <property name="mnemonic_widget">color-scheme-combobox</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="color-scheme-combobox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="focus_on_click">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkGrid"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="row_spacing">6</property> + <property name="column_spacing">12</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Text</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="top_attach">0</property> + <property name="left_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Background</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="top_attach">0</property> + <property name="left_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="foreground-colorpicker-label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">_Default color:</property> + <property name="use_underline">True</property> + <property name="justify">center</property> + <property name="mnemonic_widget">foreground-colorpicker</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="left_attach">0</property> + </packing> + </child> + <child> + <object class="GtkColorButton" id="foreground-colorpicker"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="title" translatable="yes">Choose Terminal Text Color</property> + <property name="show-editor">True</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="left_attach">1</property> + </packing> + </child> + <child> + <object class="GtkColorButton" id="background-colorpicker"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="title" translatable="yes">Choose Terminal Background Color</property> + <property name="show-editor">True</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="left_attach">2</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="bold-color-checkbutton"> + <property name="label" translatable="yes">Bo_ld color:</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="use_underline">True</property> + <property name="xalign">0</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="top_attach">2</property> + <property name="left_attach">0</property> + </packing> + </child> + <child> + <object class="GtkColorButton" id="bold-colorpicker"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="title" translatable="yes">Choose Terminal Bold Text Color</property> + <property name="show-editor">True</property> + </object> + <packing> + <property name="top_attach">2</property> + <property name="left_attach">1</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="underline-color-checkbutton"> + <property name="label" translatable="yes">_Underline color:</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="use_underline">True</property> + <property name="xalign">0</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="top_attach">3</property> + <property name="left_attach">0</property> + </packing> + </child> + <child> + <object class="GtkColorButton" id="underline-colorpicker"> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="title" translatable="yes">Choose Terminal Underlined Text Color</property> + </object> + <packing> + <property name="top_attach">3</property> + <property name="left_attach">1</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="cursor-colors-checkbutton"> + <property name="label" translatable="yes">Cu_rsor color:</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="use_underline">True</property> + <property name="xalign">0</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="top_attach">4</property> + <property name="left_attach">0</property> + </packing> + </child> + <child> + <object class="GtkColorButton" id="cursor-foreground-colorpicker"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="halign">start</property> + <property name="title" translatable="yes">Choose Terminal Cursor Foreground Color</property> + <property name="show-editor">True</property> + </object> + <packing> + <property name="top_attach">4</property> + <property name="left_attach">1</property> + </packing> + </child> + <child> + <object class="GtkColorButton" id="cursor-background-colorpicker"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="halign">start</property> + <property name="title" translatable="yes">Choose Terminal Cursor Background Color</property> + <property name="show-editor">True</property> + </object> + <packing> + <property name="top_attach">4</property> + <property name="left_attach">2</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="highlight-colors-checkbutton"> + <property name="label" translatable="yes">_Highlight color:</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="top_attach">5</property> + <property name="left_attach">0</property> + </packing> + </child> + <child> + <object class="GtkColorButton" id="highlight-foreground-colorpicker"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="halign">start</property> + <property name="title" translatable="yes">Choose Terminal Highlight Foreground Color</property> + <property name="show-editor">True</property> + </object> + <packing> + <property name="top_attach">5</property> + <property name="left_attach">1</property> + </packing> + </child> + <child> + <object class="GtkColorButton" id="highlight-background-colorpicker"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="halign">start</property> + <property name="title" translatable="yes">Choose Terminal Highlight Background Color</property> + <property name="show-editor">True</property> + </object> + <packing> + <property name="top_attach">5</property> + <property name="left_attach">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="palette-box"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <child> + <object class="GtkLabel" id="label42"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Palette</property> + <property name="xalign">0</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment10106"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">12</property> + <child> + <object class="GtkVBox" id="vbox8"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <child> + <object class="GtkGrid" id="table25"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="column_spacing">12</property> + <property name="row_spacing">6</property> + <child> + <object class="GtkLabel" id="palette-optionmenu-label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Built-in _schemes:</property> + <property name="use_underline">True</property> + <property name="justify">center</property> + <property name="mnemonic_widget">palette-combobox</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="top_attach">0</property> + <property name="left_attach">0</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="palette-combobox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="model">model4</property> + <property name="focus_on_click">False</property> + <child> + <object class="GtkCellRendererText" id="renderer4"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + <packing> + <property name="top_attach">0</property> + <property name="left_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label43"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Color p_alette:</property> + <property name="use_underline">True</property> + <property name="justify">center</property> + <property name="mnemonic_widget">palette-colorpicker-0</property> + <property name="xalign">0</property> + <property name="yalign">0</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="left_attach">0</property> + </packing> + </child> + <child> + <object class="GtkGrid" id="palette-table"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="column_spacing">6</property> + <property name="row_spacing">6</property> + <child> + <object class="GtkColorButton" id="palette-colorpicker-0"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text">dummy</property> + <property name="show-editor">True</property> + </object> + <packing> + <property name="top_attach">0</property> + <property name="left_attach">0</property> + </packing> + </child> + <child> + <object class="GtkColorButton" id="palette-colorpicker-1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text">dummy</property> + <property name="show-editor">True</property> + </object> + <packing> + <property name="top_attach">0</property> + <property name="left_attach">1</property> + </packing> + </child> + <child> + <object class="GtkColorButton" id="palette-colorpicker-2"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text">dummy</property> + <property name="show-editor">True</property> + </object> + <packing> + <property name="top_attach">0</property> + <property name="left_attach">2</property> + </packing> + </child> + <child> + <object class="GtkColorButton" id="palette-colorpicker-3"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text">dummy</property> + <property name="show-editor">True</property> + </object> + <packing> + <property name="top_attach">0</property> + <property name="left_attach">3</property> + </packing> + </child> + <child> + <object class="GtkColorButton" id="palette-colorpicker-4"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text">dummy</property> + <property name="show-editor">True</property> + </object> + <packing> + <property name="top_attach">0</property> + <property name="left_attach">4</property> + </packing> + </child> + <child> + <object class="GtkColorButton" id="palette-colorpicker-5"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text">dummy</property> + <property name="show-editor">True</property> + </object> + <packing> + <property name="top_attach">0</property> + <property name="left_attach">5</property> + </packing> + </child> + <child> + <object class="GtkColorButton" id="palette-colorpicker-6"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text">dummy</property> + <property name="show-editor">True</property> + </object> + <packing> + <property name="top_attach">0</property> + <property name="left_attach">6</property> + </packing> + </child> + <child> + <object class="GtkColorButton" id="palette-colorpicker-7"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text">dummy</property> + <property name="show-editor">True</property> + </object> + <packing> + <property name="top_attach">0</property> + <property name="left_attach">7</property> + </packing> + </child> + <child> + <object class="GtkColorButton" id="palette-colorpicker-8"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text">dummy</property> + <property name="show-editor">True</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="left_attach">0</property> + </packing> + </child> + <child> + <object class="GtkColorButton" id="palette-colorpicker-9"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text">dummy</property> + <property name="show-editor">True</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="left_attach">1</property> + </packing> + </child> + <child> + <object class="GtkColorButton" id="palette-colorpicker-10"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text">dummy</property> + <property name="show-editor">True</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="left_attach">2</property> + </packing> + </child> + <child> + <object class="GtkColorButton" id="palette-colorpicker-11"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text">dummy</property> + <property name="show-editor">True</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="left_attach">3</property> + </packing> + </child> + <child> + <object class="GtkColorButton" id="palette-colorpicker-12"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text">dummy</property> + <property name="show-editor">True</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="left_attach">4</property> + </packing> + </child> + <child> + <object class="GtkColorButton" id="palette-colorpicker-13"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text">dummy</property> + <property name="show-editor">True</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="left_attach">5</property> + </packing> + </child> + <child> + <object class="GtkColorButton" id="palette-colorpicker-14"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text">dummy</property> + <property name="show-editor">True</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="left_attach">6</property> + </packing> + </child> + <child> + <object class="GtkColorButton" id="palette-colorpicker-15"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text">dummy</property> + <property name="show-editor">True</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="left_attach">7</property> + </packing> + </child> + </object> + <packing> + <property name="top_attach">1</property> + <property name="left_attach">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="bold-is-bright-checkbutton"> + <property name="label" translatable="yes">Show _bold text in bright colors</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="xalign">0.5</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel" id="label45"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Colors</property> + <property name="use_underline">True</property> + <property name="justify">center</property> + </object> + <packing> + <property name="position">1</property> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <object class="GtkGrid" id="table27"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="border_width">12</property> + <property name="column_spacing">12</property> + <property name="row_spacing">6</property> + <child> + <object class="GtkCheckButton" id="scrollbar-checkbutton"> + <property name="label" translatable="yes">_Show scrollbar</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="xalign">0.5</property> + <property name="draw_indicator">True</property> + <property name="hexpand">True</property> + </object> + <packing> + <property name="top_attach">0</property> + <property name="left_attach">0</property> + <property name="width">2</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="scroll-on-output-checkbutton"> + <property name="label" translatable="yes">Scroll on _output</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="xalign">0.5</property> + <property name="draw_indicator">True</property> + <property name="hexpand">True</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="left_attach">0</property> + <property name="width">2</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="scroll-on-keystroke-checkbutton"> + <property name="label" translatable="yes">Scroll on _keystroke</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="xalign">0.5</property> + <property name="draw_indicator">True</property> + <property name="hexpand">True</property> + </object> + <packing> + <property name="top_attach">2</property> + <property name="left_attach">0</property> + <property name="width">2</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="scrollback-limited-checkbutton"> + <property name="label" translatable="yes">_Limit scrollback to:</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="xalign">0.5</property> + <property name="draw_indicator">True</property> + <property name="hexpand">False</property> + </object> + <packing> + <property name="top_attach">3</property> + <property name="left_attach">0</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="scrollback-box"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <property name="hexpand">True</property> + <child> + <object class="GtkSpinButton" id="scrollback-lines-spinbutton"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="adjustment">adjustment4</property> + <property name="numeric">True</property> + <property name="input_hints">GTK_INPUT_HINT_NO_EMOJI</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="scrollback-lines-spinbutton-label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">lines</property> + <property name="justify">center</property> + <property name="mnemonic_widget">scrollback-lines-spinbutton</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="top_attach">3</property> + <property name="left_attach">1</property> + </packing> + </child> + </object> + <packing> + <property name="position">2</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel" id="label60"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Scrolling</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="position">2</property> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <object class="GtkGrid"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="border_width">12</property> + <property name="column_spacing">12</property> + <property name="row_spacing">6</property> + <child> + <object class="GtkCheckButton" id="login-shell-checkbutton"> + <property name="label" translatable="yes">_Run command as a login shell</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="top_attach">0</property> + <property name="left_attach">0</property> + <property name="width">2</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="use-custom-command-checkbutton"> + <property name="label" translatable="yes">Ru_n a custom command instead of my shell</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="left_attach">0</property> + <property name="width">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="custom-command-entry-label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Custom co_mmand:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">custom-command-entry</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="top_attach">2</property> + <property name="left_attach">0</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="custom-command-entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hexpand">True</property> + <property name="input_hints">GTK_INPUT_HINT_NO_EMOJI</property> + </object> + <packing> + <property name="top_attach">2</property> + <property name="left_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="preserve-working-directory-checkbutton-label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Preserve working directory:</property> + <property name="use_underline">True</property> + <property name="justify">center</property> + <property name="mnemonic_widget">preserve-working-directory-combobox</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="top_attach">3</property> + <property name="left_attach">0</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="preserve-working-directory-combobox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="model">preserve-working-directory-model</property> + <property name="focus_on_click">False</property> + <property name="halign">start</property> + <child> + <object class="GtkCellRendererText" id="preserve-working-directory-renderer"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + <packing> + <property name="top_attach">3</property> + <property name="left_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="exit-action-combobox-label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">When command _exits:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">exit-action-combobox</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="top_attach">4</property> + <property name="left_attach">0</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="exit-action-combobox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="model">model3</property> + <property name="focus_on_click">False</property> + <property name="halign">start</property> + <child> + <object class="GtkCellRendererText" id="renderer3"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + <packing> + <property name="top_attach">4</property> + <property name="left_attach">1</property> + </packing> + </child> + </object> + <packing> + <property name="position">3</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel" id="label38"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Command</property> + <property name="use_underline">True</property> + <property name="justify">center</property> + </object> + <packing> + <property name="position">3</property> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <object class="GtkGrid" id="table30"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="border_width">12</property> + <property name="column_spacing">12</property> + <property name="row_spacing">6</property> + <child> + <object class="GtkLabel" id="backspace-binding-combobox-label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Backspace key generates:</property> + <property name="use_underline">True</property> + <property name="justify">center</property> + <property name="mnemonic_widget">backspace-binding-combobox</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="top_attach">0</property> + <property name="left_attach">0</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="backspace-binding-combobox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="model">model6</property> + <property name="focus_on_click">False</property> + <child> + <object class="GtkCellRendererText" id="renderer6"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + <packing> + <property name="top_attach">0</property> + <property name="left_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="delete-binding-combobox-label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Delete key generates:</property> + <property name="use_underline">True</property> + <property name="justify">center</property> + <property name="mnemonic_widget">delete-binding-combobox</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="left_attach">0</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="delete-binding-combobox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="model">model6</property> + <property name="focus_on_click">False</property> + <child> + <object class="GtkCellRendererText" id="renderer7"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + <packing> + <property name="top_attach">1</property> + <property name="left_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="encoding-label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Encoding:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">encoding-combobox</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="top_attach">2</property> + <property name="left_attach">0</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="encoding-combobox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="focus_on_click">False</property> + <property name="id_column">0</property> + <child> + <object class="GtkCellRendererText" id="encoding-combobox-renderer"/> + <attributes> + <attribute name="text">1</attribute> + </attributes> + </child> + </object> + <packing> + <property name="top_attach">2</property> + <property name="left_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="cjk-ambiguous-width-label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Ambiguous-_width characters:</property> + <property name="use_underline">True</property> + <property name="justify">center</property> + <property name="mnemonic_widget">cjk-ambiguous-width-combobox</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="top_attach">3</property> + <property name="left_attach">0</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="cjk-ambiguous-width-combobox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="model">cjk-ambiguous-width-model</property> + <property name="focus_on_click">False</property> + <property name="id_column">1</property> + <child> + <object class="GtkCellRendererText" id="cjk-ambiguous-width-renderer"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + <packing> + <property name="top_attach">3</property> + <property name="left_attach">1</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="enable-sixel-checkbutton"> + <property name="label" translatable="yes">Enable _SIXEL images</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="xalign">0</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="top_attach">4</property> + <property name="left_attach">0</property> + <property name="width">2</property> + </packing> + </child> + <child> + <object class="GtkButton" id="reset-compat-defaults-button"> + <property name="label" translatable="yes">_Reset Compatibility Options to Defaults</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="focus_on_click">False</property> + <property name="halign">start</property> + </object> + <packing> + <property name="top_attach">5</property> + <property name="left_attach">0</property> + <property name="width">2</property> + </packing> + </child> + </object> + <packing> + <property name="position">4</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel" id="label54"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Compatibility</property> + <property name="use_underline">True</property> + <property name="justify">center</property> + </object> + <packing> + <property name="position">4</property> + <property name="tab_fill">False</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="name">profile-prefs</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkButtonBox" id="dialogue-buttonbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">12</property> + <property name="layout_style">start</property> + <child> + <object class="GtkButton" id="help-button"> + <property name="label">gtk-help</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + <property name="focus_on_click">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="close-button"> + <property name="label">gtk-close</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + <property name="focus_on_click">False</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">1</property> + <property name="secondary">True</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <object class="GtkPopoverMenu" id="popover-menu"> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="margin">6</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkModelButton"> + <property name="visible">True</property> + <property name="action-name">win.clone</property> + <property name="text" translatable="yes">Clone…</property> + </object> + </child> + <child> + <object class="GtkModelButton"> + <property name="visible">True</property> + <property name="action-name">win.rename</property> + <property name="text" translatable="yes">Rename…</property> + </object> + </child> + <child> + <object class="GtkModelButton"> + <property name="visible">True</property> + <property name="action-name">win.delete</property> + <property name="text" translatable="yes">Delete…</property> + </object> + </child> + <child> + <object class="GtkSeparator"> + <property name="visible">True</property> + <property name="orientation">horizontal</property> + </object> + </child> + <child> + <object class="GtkModelButton"> + <property name="visible">True</property> + <property name="action-name">win.set-as-default</property> + <property name="text" translatable="yes">Set as default</property> + </object> + </child> + </object> + </child> + </object> + <object class="GtkPopover" id="popover-dialog"> + <property name="can_focus">False</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="border_width">12</property> + <property name="spacing">6</property> + <child> + <object class="GtkLabel" id="popover-dialog-label1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="margin_bottom">6</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="popover-dialog-label2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="popover-dialog-entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="activates_default">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkButtonBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">start</property> + <property name="margin_top">6</property> + <property name="spacing">6</property> + <child> + <object class="GtkButton" id="popover-dialog-cancel"> + <property name="label" translatable="yes">Cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="pack_type">end</property> + <property name="position">0</property> + <property name="secondary">True</property> + </packing> + </child> + <child> + <object class="GtkButton" id="popover-dialog-ok"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">True</property> + <property name="sensitive">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="pack_type">end</property> + <property name="position">1</property> + <property name="secondary">True</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + </object> + </child> + </object> +</interface> diff --git a/src/prefs-main.cc b/src/prefs-main.cc new file mode 100644 index 0000000..568eb3c --- /dev/null +++ b/src/prefs-main.cc @@ -0,0 +1,274 @@ +/* + * Copyright © 2008, 2010, 2011, 2021, 2022 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 <locale.h> + +#include <glib.h> +#include <glib/gi18n.h> +#include <gio/gio.h> + +#include "terminal-app.hh" +#include "terminal-debug.hh" +#include "terminal-i18n.hh" +#include "terminal-defines.hh" +#include "terminal-libgsystem.hh" +#include "terminal-settings-bridge-backend.hh" +#include "terminal-settings-bridge-generated.h" + +// Reduce the default timeout to something that should still always work, +// but not hang the process for long periods of time if something does +// go wrong. See issue #7935. +#define BRIDGE_TIMEOUT 5000 /* ms */ + +static char* arg_profile_uuid = nullptr; +static char* arg_hint = nullptr; +static int arg_bus_fd = -1; +static int arg_timestamp = -1; + +static const GOptionEntry options[] = { + {"profile", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &arg_profile_uuid, "Profile", "UUID"}, + {"hint", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &arg_hint, "Hint", "HINT"}, + {"bus-fd", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_INT, &arg_bus_fd, "Bus FD", "FD"}, + {"timestamp", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_INT, &arg_timestamp, "Timestamp", "VALUE"}, + {nullptr} +}; + +static GSettings* +profile_from_uuid(TerminalApp* app, + char const* uuid_str) noexcept +{ + if (!uuid_str) + return nullptr; + + auto const profiles_list = terminal_app_get_profiles_list(app); + + GSettings* profile = nullptr; + if (g_str_equal(uuid_str, "default")) + profile = terminal_settings_list_ref_default_child(profiles_list); + else if (terminal_settings_list_valid_uuid(uuid_str)) + profile = terminal_settings_list_ref_child(profiles_list, uuid_str); + + return profile; +} + +static void +preferences_cb(GSimpleAction* action, + GVariant* parameter, + void* user_data) +{ + auto const app = terminal_app_get(); + + gs_free char* uuid_str = nullptr; + g_variant_lookup(parameter, "profile", "s", &uuid_str); + + gs_unref_object auto profile = profile_from_uuid(app, uuid_str); + + gs_free char* hint_str = nullptr; + g_variant_lookup(parameter, "hint", "s", &hint_str); + + guint32 ts = 0; + g_variant_lookup(parameter, "timestamp", "u", &ts); + + terminal_app_edit_preferences(app, profile, hint_str, ts); +} + +static void +connection_closed_cb(GDBusConnection* connection, + gboolean peer_vanished, + GError* error, + void* user_data) +{ + auto ptr = reinterpret_cast<void**>(user_data); + + if (error) + g_printerr("D-Bus connection closed: %s\n", error->message); + + // As per glib docs, unref the connection at this point + g_object_unref(g_steal_pointer(ptr)); + + // Exit cleanly + auto const app = g_application_get_default(); + if (app) + g_application_quit(app); +} + +int +main(int argc, + char* argv[]) +{ + // Sanitise environment + g_unsetenv("CHARSET"); + g_unsetenv("DBUS_STARTER_BUS_TYPE"); + // Not interested in silly debug spew polluting the journal, bug #749195 + if (g_getenv("G_ENABLE_DIAGNOSTIC") == nullptr) + g_setenv("G_ENABLE_DIAGNOSTIC", "0", true); + // Maybe: g_setenv("GSETTINGS_BACKEND", "bridge", true); + + if (setlocale(LC_ALL, "") == nullptr) { + g_printerr("Locale not supported.\n"); + return _EXIT_FAILURE_UNSUPPORTED_LOCALE; + } + + terminal_i18n_init(true); + + char const* charset = nullptr; + if (!g_get_charset(&charset)) { + g_printerr("Non UTF-8 locale (%s) is not supported!\n", charset); + return _EXIT_FAILURE_NO_UTF8; + } + + _terminal_debug_init(); + + auto const home_dir = g_get_home_dir(); + if (home_dir == nullptr || chdir(home_dir) < 0) + (void) chdir("/"); + + g_set_prgname("gnome-terminal-preferences"); + g_set_application_name(_("Preferences")); + + gs_free_error GError *error = nullptr; + if (!gtk_init_with_args(&argc, &argv, nullptr, options, nullptr, &error)) { + g_printerr("Failed to parse arguments: %s\n", error->message); + return _EXIT_FAILURE_GTK_INIT; + } + + gs_unref_object GDBusConnection* connection = nullptr; + gs_unref_object GSettingsBackend* backend = nullptr; + gs_unref_object GSimpleActionGroup* action_group = nullptr; + auto export_id = 0u; + if (arg_bus_fd != -1) { + gs_unref_object auto socket = g_socket_new_from_fd(arg_bus_fd, &error); + if (!socket) { + g_printerr("Failed to create bridge socket: %s\n", error->message); + close(arg_bus_fd); + return EXIT_FAILURE; + } + + gs_unref_object auto sockconn = + g_socket_connection_factory_create_connection(socket); + if (!G_IS_IO_STREAM(sockconn)) { + g_printerr("Bridge socket has incorrect type %s\n", G_OBJECT_TYPE_NAME(sockconn)); + return EXIT_FAILURE; + } + + connection = + g_dbus_connection_new_sync(G_IO_STREAM(sockconn), + nullptr, // guid=nullptr for the client + GDBusConnectionFlags(G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT | + G_DBUS_CONNECTION_FLAGS_DELAY_MESSAGE_PROCESSING), + nullptr, // auth observer, + nullptr, // cancellable, + &error); + if (!connection) { + g_printerr("Failed to create bus: %s\n", error->message); + return EXIT_FAILURE; + } + + GActionEntry const action_entries[] = { + { "preferences", preferences_cb, "a{sv}", nullptr, nullptr }, + }; + + action_group = g_simple_action_group_new(); + g_action_map_add_action_entries(G_ACTION_MAP(action_group), + action_entries, G_N_ELEMENTS(action_entries), + nullptr); + + export_id = g_dbus_connection_export_action_group(connection, + TERMINAL_PREFERENCES_OBJECT_PATH, + G_ACTION_GROUP(action_group), + &error); + if (export_id == 0) { + g_printerr("Failed to export actions: %s\n", error->message); + return EXIT_FAILURE; + } + + g_dbus_connection_start_message_processing(connection); + + gs_unref_object auto bridge = + terminal_settings_bridge_proxy_new_sync + (connection, + GDBusProxyFlags( + G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START | + G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START_AT_CONSTRUCTION | + G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS | + G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES), + nullptr, // no name + TERMINAL_SETTINGS_BRIDGE_OBJECT_PATH, + nullptr, // cancellable + &error); + if (!bridge) { + g_printerr("Failed to create settings bridge proxy: %s\n", error->message); + return EXIT_FAILURE; + } + + if (!_terminal_debug_on(TERMINAL_DEBUG_BRIDGE)) { + g_dbus_proxy_set_default_timeout(G_DBUS_PROXY(bridge), BRIDGE_TIMEOUT); + } + + backend = terminal_settings_bridge_backend_new(bridge); + + g_dbus_connection_set_exit_on_close(connection, false); + g_signal_connect(connection, "closed", G_CALLBACK(connection_closed_cb), &connection); + } + + gs_unref_object auto app = + terminal_app_new(TERMINAL_PREFERENCES_APPLICATION_ID, + GApplicationFlags(G_APPLICATION_NON_UNIQUE | + G_APPLICATION_IS_SERVICE), + backend); + + // Need to startup the application now, otherwise we can't use + // gtk_application_add_window() before g_application_run() below. + // This should always succeed. + if (!g_application_register(G_APPLICATION(app), nullptr, &error)) { + g_printerr("Failed to register application: %s\n", error->message); + return EXIT_FAILURE; + } + + // If started from gnome-terminal-server, the "preferences" action + // will be activated to actually show the preferences dialogue. However + // if started directly, need to show the dialogue right now. + if (!connection) { + gs_unref_object GSettings* profile = profile_from_uuid(TERMINAL_APP(app), + arg_profile_uuid); + if (arg_profile_uuid && !profile) { + g_printerr("No profile with UUID \"%s\": %s\n", arg_profile_uuid, error->message); + return EXIT_FAILURE; + } + + terminal_app_edit_preferences(TERMINAL_APP(app), + profile, + arg_hint, + unsigned(arg_timestamp)); + } + + auto const r = g_application_run(app, 0, nullptr); + + if (connection && export_id != 0) { + g_dbus_connection_unexport_action_group(connection, export_id); + export_id = 0; + } + + if (connection && + !g_dbus_connection_flush_sync(connection, nullptr, &error)) { + g_printerr("Failed to flush D-Bus connection: %s\n", error->message); + } + + return r; +} diff --git a/src/prefs.gresource.xml b/src/prefs.gresource.xml new file mode 100644 index 0000000..db48976 --- /dev/null +++ b/src/prefs.gresource.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright © 2012, 2022 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, or (at your option) + any later version. + + This program is distributed in the hope conf 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/>. +--> +<gresources> + <gresource prefix="/org/gnome/terminal"> + <file alias="css/terminal.css" compressed="true">terminal.common.css</file> + <file alias="ui/preferences.ui" compressed="true" preprocess="xml-stripblanks">preferences.ui</file> + </gresource> +</gresources> diff --git a/src/profile-editor.cc b/src/profile-editor.cc new file mode 100644 index 0000000..0cb99b6 --- /dev/null +++ b/src/profile-editor.cc @@ -0,0 +1,1504 @@ +/* + * Copyright © 2002 Havoc Pennington + * Copyright © 2002 Mathias Hasselmann + * Copyright © 2008, 2011, 2017 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 <string.h> +#include <math.h> + +#include <glib.h> +#include <glib/gi18n.h> +#include <gio/gio.h> + +#include "terminal-app.hh" +#include "terminal-enums.hh" +#include "profile-editor.hh" +#include "terminal-prefs.hh" +#include "terminal-schemas.hh" +#include "terminal-type-builtins.hh" +#include "terminal-util.hh" +#include "terminal-profiles-list.hh" +#include "terminal-libgsystem.hh" + + +/* Wrapper around g_signal_connect that maintains a list of the + * handlers installed, and can disconnect them all. */ +typedef struct { + gpointer instance; + gulong handler_id; +} ProfilePrefsSignal; + +static void +profile_prefs_register_signal_handler (gpointer instance, + gulong handler_id) +{ + ProfilePrefsSignal sig; + sig.instance = instance; + sig.handler_id = handler_id; + g_array_append_val (the_pref_data->profile_signals, sig); +} + +static gulong +profile_prefs_signal_connect (gpointer instance, + const gchar *detailed_signal, + GCallback c_handler, + gpointer data) +{ + gulong handler_id = g_signal_connect(instance, detailed_signal, c_handler, data); + profile_prefs_register_signal_handler (instance, handler_id); + return handler_id; +} + +static void +profile_prefs_signal_handlers_disconnect_all (void) +{ + for (guint i = 0; i < the_pref_data->profile_signals->len; i++) { + ProfilePrefsSignal *sig = &g_array_index (the_pref_data->profile_signals, ProfilePrefsSignal, i); + g_signal_handler_disconnect (sig->instance, sig->handler_id); + } + g_array_set_size (the_pref_data->profile_signals, 0); +} + + +/* Wrappers around g_settings_bind and friends that maintain a list of the + * bindings installed, and can unbind them all. */ +typedef struct { + gpointer object; + char *property; +} ProfilePrefsBinding; + +static void +profile_prefs_register_settings_binding (gpointer object, + const char *property) +{ + ProfilePrefsBinding bind; + bind.object = object; + bind.property = g_strdup (property); + g_array_append_val (the_pref_data->profile_bindings, bind); +} + +static void +profile_prefs_settings_bind (GSettings *settings, + const gchar *key, + gpointer object, + const gchar *property, + GSettingsBindFlags flags) +{ + profile_prefs_register_settings_binding (object, property); + g_settings_bind (settings, key, object, property, flags); +} + +static void +profile_prefs_settings_bind_with_mapping (GSettings *settings, + const gchar *key, + gpointer object, + const gchar *property, + GSettingsBindFlags flags, + GSettingsBindGetMapping get_mapping, + GSettingsBindSetMapping set_mapping, + GType (*user_data)(void), + GDestroyNotify destroy) +{ + profile_prefs_register_settings_binding (object, property); + g_settings_bind_with_mapping (settings, key, object, property, flags, get_mapping, set_mapping, (void*)user_data, destroy); +} + +static void +profile_prefs_settings_bind_writable (GSettings *settings, + const gchar *key, + gpointer object, + const gchar *property, + gboolean inverted) +{ + profile_prefs_register_settings_binding (object, property); + g_settings_bind_writable (settings, key, object, property, inverted); +} + +static void +profile_prefs_settings_unbind_all (void) +{ + for (guint i = 0; i < the_pref_data->profile_bindings->len; i++) { + ProfilePrefsBinding *bind = &g_array_index (the_pref_data->profile_bindings, ProfilePrefsBinding, i); + g_settings_unbind (bind->object, bind->property); + g_free (bind->property); + } + g_array_set_size (the_pref_data->profile_bindings, 0); +} + + +typedef struct _TerminalColorScheme TerminalColorScheme; + +struct _TerminalColorScheme +{ + const char *name; + const GdkRGBA foreground; + const GdkRGBA background; +}; + +#define COLOR(r, g, b) { .red = (r) / 255.0, .green = (g) / 255.0, .blue = (b) / 255.0, .alpha = 1.0 } + +static const TerminalColorScheme color_schemes[] = { + { N_("Black on light yellow"), + COLOR (0x00, 0x00, 0x00), + COLOR (0xff, 0xff, 0xdd) + }, + { N_("Black on white"), + COLOR (0x00, 0x00, 0x00), + COLOR (0xff, 0xff, 0xff) + }, + { N_("Gray on black"), + COLOR (0xaa, 0xaa, 0xaa), + COLOR (0x00, 0x00, 0x00) + }, + { N_("Green on black"), + COLOR (0x00, 0xff, 0x00), + COLOR (0x00, 0x00, 0x00) + }, + { N_("White on black"), + COLOR (0xff, 0xff, 0xff), + COLOR (0x00, 0x00, 0x00) + }, + /* Translators: "GNOME" is the name of a colour scheme, "light" can be translated */ + { N_("GNOME light"), + COLOR (0x17, 0x14, 0x21), /* Palette entry 0 */ + COLOR (0xff, 0xff, 0xff) /* Palette entry 15 */ + }, + /* Translators: "GNOME" is the name of a colour scheme, "dark" can be translated */ + { N_("GNOME dark"), + COLOR (0xd0, 0xcf, 0xcc), /* Palette entry 7 */ + COLOR (0x17, 0x14, 0x21) /* Palette entry 0 */ + }, + /* Translators: "Tango" is the name of a colour scheme, "light" can be translated */ + { N_("Tango light"), + COLOR (0x2e, 0x34, 0x36), + COLOR (0xee, 0xee, 0xec) + }, + /* Translators: "Tango" is the name of a colour scheme, "dark" can be translated */ + { N_("Tango dark"), + COLOR (0xd3, 0xd7, 0xcf), + COLOR (0x2e, 0x34, 0x36) + }, + /* Translators: "Solarized" is the name of a colour scheme, "light" can be translated */ + { N_("Solarized light"), + COLOR (0x65, 0x7b, 0x83), /* 11: base00 */ + COLOR (0xfd, 0xf6, 0xe3) /* 15: base3 */ + }, + /* Translators: "Solarized" is the name of a colour scheme, "dark" can be translated */ + { N_("Solarized dark"), + COLOR (0x83, 0x94, 0x96), /* 12: base0 */ + COLOR (0x00, 0x2b, 0x36) /* 8: base03 */ + }, +}; + +#define TERMINAL_PALETTE_SIZE (16) + +enum +{ + TERMINAL_PALETTE_GNOME = 0, + TERMINAL_PALETTE_TANGO = 1, + TERMINAL_PALETTE_LINUX = 2, + TERMINAL_PALETTE_XTERM = 3, + TERMINAL_PALETTE_RXVT = 4, + TERMINAL_PALETTE_SOLARIZED = 5, + TERMINAL_PALETTE_N_BUILTINS +}; + +static const GdkRGBA terminal_palettes[TERMINAL_PALETTE_N_BUILTINS][TERMINAL_PALETTE_SIZE] = +{ + /* Based on GNOME 3.32 palette: https://developer.gnome.org/hig/stable/icon-design.html.en#palette */ + { + COLOR (0x17, 0x14, 0x21), /* Blend of Dark 4 and Black */ + COLOR (0xc0, 0x1c, 0x28), /* Red 4 */ + COLOR (0x26, 0xa2, 0x69), /* Green 5 */ + COLOR (0xa2, 0x73, 0x4c), /* Blend of Brown 2 and Brown 3 */ + COLOR (0x12, 0x48, 0x8b), /* Blend of Blue 5 and Dark 4 */ + COLOR (0xa3, 0x47, 0xba), /* Purple 3 */ + COLOR (0x2a, 0xa1, 0xb3), /* Linear addition Blue 5 + Green 5, darkened slightly */ + COLOR (0xd0, 0xcf, 0xcc), /* Blend of Light 3 and Light 4 */ + COLOR (0x5e, 0x5c, 0x64), /* Dark 2 */ + COLOR (0xf6, 0x61, 0x51), /* Red 1 */ + COLOR (0x33, 0xd1, 0x7a), /* Green 3 */ + COLOR (0xe9, 0xad, 0x0c), /* Blend of Yellow 4 and Yellow 5 */ + COLOR (0x2a, 0x7b, 0xde), /* Blend of Blue 3 and Blue 4 */ + COLOR (0xc0, 0x61, 0xcb), /* Purple 2 */ + COLOR (0x33, 0xc7, 0xde), /* Linear addition Blue 4 + Green 4, darkened slightly */ + COLOR (0xff, 0xff, 0xff) /* Light 1 */ + }, + + /* Tango palette */ + { + COLOR (0x2e, 0x34, 0x36), + COLOR (0xcc, 0x00, 0x00), + COLOR (0x4e, 0x9a, 0x06), + COLOR (0xc4, 0xa0, 0x00), + COLOR (0x34, 0x65, 0xa4), + COLOR (0x75, 0x50, 0x7b), + COLOR (0x06, 0x98, 0x9a), + COLOR (0xd3, 0xd7, 0xcf), + COLOR (0x55, 0x57, 0x53), + COLOR (0xef, 0x29, 0x29), + COLOR (0x8a, 0xe2, 0x34), + COLOR (0xfc, 0xe9, 0x4f), + COLOR (0x72, 0x9f, 0xcf), + COLOR (0xad, 0x7f, 0xa8), + COLOR (0x34, 0xe2, 0xe2), + COLOR (0xee, 0xee, 0xec) + }, + + /* Linux palette */ + { + COLOR (0x00, 0x00, 0x00), + COLOR (0xaa, 0x00, 0x00), + COLOR (0x00, 0xaa, 0x00), + COLOR (0xaa, 0x55, 0x00), + COLOR (0x00, 0x00, 0xaa), + COLOR (0xaa, 0x00, 0xaa), + COLOR (0x00, 0xaa, 0xaa), + COLOR (0xaa, 0xaa, 0xaa), + COLOR (0x55, 0x55, 0x55), + COLOR (0xff, 0x55, 0x55), + COLOR (0x55, 0xff, 0x55), + COLOR (0xff, 0xff, 0x55), + COLOR (0x55, 0x55, 0xff), + COLOR (0xff, 0x55, 0xff), + COLOR (0x55, 0xff, 0xff), + COLOR (0xff, 0xff, 0xff) + }, + + /* XTerm palette */ + { + COLOR (0x00, 0x00, 0x00), + COLOR (0xcd, 0x00, 0x00), + COLOR (0x00, 0xcd, 0x00), + COLOR (0xcd, 0xcd, 0x00), + COLOR (0x00, 0x00, 0xee), + COLOR (0xcd, 0x00, 0xcd), + COLOR (0x00, 0xcd, 0xcd), + COLOR (0xe5, 0xe5, 0xe5), + COLOR (0x7f, 0x7f, 0x7f), + COLOR (0xff, 0x00, 0x00), + COLOR (0x00, 0xff, 0x00), + COLOR (0xff, 0xff, 0x00), + COLOR (0x5c, 0x5c, 0xff), + COLOR (0xff, 0x00, 0xff), + COLOR (0x00, 0xff, 0xff), + COLOR (0xff, 0xff, 0xff) + }, + + /* RXVT palette */ + { + COLOR (0x00, 0x00, 0x00), + COLOR (0xcd, 0x00, 0x00), + COLOR (0x00, 0xcd, 0x00), + COLOR (0xcd, 0xcd, 0x00), + COLOR (0x00, 0x00, 0xcd), + COLOR (0xcd, 0x00, 0xcd), + COLOR (0x00, 0xcd, 0xcd), + COLOR (0xfa, 0xeb, 0xd7), + COLOR (0x40, 0x40, 0x40), + COLOR (0xff, 0x00, 0x00), + COLOR (0x00, 0xff, 0x00), + COLOR (0xff, 0xff, 0x00), + COLOR (0x00, 0x00, 0xff), + COLOR (0xff, 0x00, 0xff), + COLOR (0x00, 0xff, 0xff), + COLOR (0xff, 0xff, 0xff) + }, + + /* Solarized palette (1.0.0beta2): http://ethanschoonover.com/solarized */ + { + COLOR (0x07, 0x36, 0x42), /* 0: base02 */ + COLOR (0xdc, 0x32, 0x2f), /* 1: red */ + COLOR (0x85, 0x99, 0x00), /* 2: green */ + COLOR (0xb5, 0x89, 0x00), /* 3: yellow */ + COLOR (0x26, 0x8b, 0xd2), /* 4: blue */ + COLOR (0xd3, 0x36, 0x82), /* 5: magenta */ + COLOR (0x2a, 0xa1, 0x98), /* 6: cyan */ + COLOR (0xee, 0xe8, 0xd5), /* 7: base2 */ + COLOR (0x00, 0x2b, 0x36), /* 8: base03 */ + COLOR (0xcb, 0x4b, 0x16), /* 9: orange */ + COLOR (0x58, 0x6e, 0x75), /* 10: base01 */ + COLOR (0x65, 0x7b, 0x83), /* 11: base00 */ + COLOR (0x83, 0x94, 0x96), /* 12: base0 */ + COLOR (0x6c, 0x71, 0xc4), /* 13: violet */ + COLOR (0x93, 0xa1, 0xa1), /* 14: base1 */ + COLOR (0xfd, 0xf6, 0xe3) /* 15: base3 */ + }, +}; + +#undef COLOR + +static void profile_colors_notify_scheme_combo_cb (GSettings *profile, + const char *key, + GtkComboBox *combo); + +static void profile_palette_notify_scheme_combo_cb (GSettings *profile, + const char *key, + GtkComboBox *combo); + +static void profile_palette_notify_colorpickers_cb (GSettings *profile, + const char *key, + gpointer user_data); + +static void profile_notify_encoding_combo_cb (GSettings *profile, + const char *key, + GtkComboBox *combo); + +enum { + ENCODINGS_COL_ID, + ENCODINGS_COL_TEXT +}; + +/* gdk_rgba_equal is too strict! */ +static gboolean +rgba_equal (const GdkRGBA *a, + const GdkRGBA *b) +{ + gdouble dr, dg, db; + + dr = a->red - b->red; + dg = a->green - b->green; + db = a->blue - b->blue; + + return (dr * dr + dg * dg + db * db) < 1e-4; +} + +static gboolean +palette_cmp (const GdkRGBA *ca, + const GdkRGBA *cb) +{ + guint i; + + for (i = 0; i < TERMINAL_PALETTE_SIZE; ++i) + if (!rgba_equal (&ca[i], &cb[i])) + return FALSE; + + return TRUE; +} + +static gboolean +palette_is_builtin (const GdkRGBA *colors, + gsize n_colors, + guint *n) +{ + guint i; + + if (n_colors != TERMINAL_PALETTE_SIZE) + return FALSE; + + for (i = 0; i < TERMINAL_PALETTE_N_BUILTINS; ++i) + { + if (palette_cmp (colors, terminal_palettes[i])) + { + *n = i; + return TRUE; + } + } + + return FALSE; +} + +static void +modify_palette_entry (GSettings *profile, + guint i, + const GdkRGBA *color) +{ + gs_free GdkRGBA *colors; + gsize n_colors; + + /* FIXMEchpe: this can be optimised, don't really need to parse the colours! */ + + colors = terminal_g_settings_get_rgba_palette (profile, TERMINAL_PROFILE_PALETTE_KEY, &n_colors); + + if (i < n_colors) + { + colors[i] = *color; + terminal_g_settings_set_rgba_palette (profile, TERMINAL_PROFILE_PALETTE_KEY, + colors, n_colors); + } +} + +static void +color_scheme_combo_changed_cb (GtkWidget *combo, + GParamSpec *pspec, + GSettings *profile) +{ + guint i; + + i = gtk_combo_box_get_active (GTK_COMBO_BOX (combo)); + + if (i < G_N_ELEMENTS (color_schemes)) + { + g_signal_handlers_block_by_func (profile, (void*)profile_colors_notify_scheme_combo_cb, combo); + terminal_g_settings_set_rgba (profile, TERMINAL_PROFILE_FOREGROUND_COLOR_KEY, &color_schemes[i].foreground); + terminal_g_settings_set_rgba (profile, TERMINAL_PROFILE_BACKGROUND_COLOR_KEY, &color_schemes[i].background); + g_signal_handlers_unblock_by_func (profile, (void*)profile_colors_notify_scheme_combo_cb, combo); + } + else + { + /* "custom" selected, no change */ + } +} + +static void +profile_colors_notify_scheme_combo_cb (GSettings *profile, + const char *key, + GtkComboBox *combo) +{ + GdkRGBA fg, bg; + guint i; + + terminal_g_settings_get_rgba (profile, TERMINAL_PROFILE_FOREGROUND_COLOR_KEY, &fg); + terminal_g_settings_get_rgba (profile, TERMINAL_PROFILE_BACKGROUND_COLOR_KEY, &bg); + + for (i = 0; i < G_N_ELEMENTS (color_schemes); ++i) + { + if (rgba_equal (&fg, &color_schemes[i].foreground) && + rgba_equal (&bg, &color_schemes[i].background)) + break; + } + /* If we didn't find a match, then we get the last combo box item which is "custom" */ + + g_signal_handlers_block_by_func (combo, (void*)color_scheme_combo_changed_cb, profile); + gtk_combo_box_set_active (GTK_COMBO_BOX (combo), i); + g_signal_handlers_unblock_by_func (combo, (void*)color_scheme_combo_changed_cb, profile); +} + +static void +palette_scheme_combo_changed_cb (GtkComboBox *combo, + GParamSpec *pspec, + GSettings *profile) +{ + int i; + + i = gtk_combo_box_get_active (GTK_COMBO_BOX (combo)); + + g_signal_handlers_block_by_func (profile, (void*)profile_colors_notify_scheme_combo_cb, combo); + if (i < TERMINAL_PALETTE_N_BUILTINS) + terminal_g_settings_set_rgba_palette (profile, TERMINAL_PROFILE_PALETTE_KEY, + terminal_palettes[i], TERMINAL_PALETTE_SIZE); + else + { + /* "custom" selected, no change */ + } + g_signal_handlers_unblock_by_func (profile, (void*)profile_colors_notify_scheme_combo_cb, combo); +} + +static void +profile_palette_notify_scheme_combo_cb (GSettings *profile, + const char *key, + GtkComboBox *combo) +{ + gs_free GdkRGBA *colors; + gsize n_colors; + guint i; + + colors = terminal_g_settings_get_rgba_palette (profile, TERMINAL_PROFILE_PALETTE_KEY, &n_colors); + if (!palette_is_builtin (colors, n_colors, &i)) + /* If we didn't find a match, then we want the last combo + * box item which is "custom" + */ + i = TERMINAL_PALETTE_N_BUILTINS; + + g_signal_handlers_block_by_func (combo, (void*)palette_scheme_combo_changed_cb, profile); + gtk_combo_box_set_active (combo, i); + g_signal_handlers_unblock_by_func (combo, (void*)palette_scheme_combo_changed_cb, profile); +} + +static void +palette_color_notify_cb (GtkColorButton *button, + GParamSpec *pspec, + GSettings *profile) +{ + GdkRGBA color; + guint i; + + gtk_color_chooser_get_rgba (GTK_COLOR_CHOOSER (button), &color); + i = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (button), "palette-entry-index")); + + g_signal_handlers_block_by_func (profile, (void*)profile_palette_notify_colorpickers_cb, nullptr); + modify_palette_entry (profile, i, &color); + g_signal_handlers_unblock_by_func (profile, (void*)profile_palette_notify_colorpickers_cb, nullptr); +} + +static void +profile_palette_notify_colorpickers_cb (GSettings *profile, + const char *key, + gpointer user_data) +{ + GtkWidget *w; + GtkBuilder *builder = the_pref_data->builder; + gs_free GdkRGBA *colors; + gsize n_colors, i; + + g_assert (strcmp (key, TERMINAL_PROFILE_PALETTE_KEY) == 0); + + colors = terminal_g_settings_get_rgba_palette (profile, TERMINAL_PROFILE_PALETTE_KEY, &n_colors); + + n_colors = MIN (n_colors, TERMINAL_PALETTE_SIZE); + for (i = 0; i < n_colors; i++) + { + char name[32]; + + g_snprintf (name, sizeof (name), "palette-colorpicker-%" G_GSIZE_FORMAT, i); + w = (GtkWidget *) gtk_builder_get_object (builder, name); + + g_signal_handlers_block_by_func (w, (void*)palette_color_notify_cb, profile); + gtk_color_chooser_set_rgba (GTK_COLOR_CHOOSER (w), &colors[i]); + g_signal_handlers_unblock_by_func (w, (void*)palette_color_notify_cb, profile); + } +} + +static void +custom_command_entry_changed_cb (GtkEntry *entry) +{ + const char *command; + gs_free_error GError *error = nullptr; + + command = gtk_entry_get_text (entry); + + if (command[0] == '\0' || + g_shell_parse_argv (command, nullptr, nullptr, &error)) + { + gtk_entry_set_icon_from_icon_name (entry, GTK_ENTRY_ICON_SECONDARY, nullptr); + } + else + { + gs_free char *tooltip; + + gtk_entry_set_icon_from_icon_name (entry, GTK_ENTRY_ICON_SECONDARY, "dialog-warning"); + + tooltip = g_strdup_printf (_("Error parsing command: %s"), error->message); + gtk_entry_set_icon_tooltip_text (entry, GTK_ENTRY_ICON_SECONDARY, tooltip); + } +} + +static void +default_size_reset_cb (GtkWidget *button, + GSettings *profile) +{ + g_settings_reset (profile, TERMINAL_PROFILE_DEFAULT_SIZE_COLUMNS_KEY); + g_settings_reset (profile, TERMINAL_PROFILE_DEFAULT_SIZE_ROWS_KEY); +} + +static void +cell_scale_reset_cb (GtkWidget *button, + GSettings *profile) +{ + g_settings_reset (profile, TERMINAL_PROFILE_CELL_HEIGHT_SCALE_KEY); + g_settings_reset (profile, TERMINAL_PROFILE_CELL_WIDTH_SCALE_KEY); +} + +static void +reset_compat_defaults_cb (GtkWidget *button, + GSettings *profile) +{ + g_settings_reset (profile, TERMINAL_PROFILE_DELETE_BINDING_KEY); + g_settings_reset (profile, TERMINAL_PROFILE_BACKSPACE_BINDING_KEY); + g_settings_reset (profile, TERMINAL_PROFILE_ENCODING_KEY); + g_settings_reset (profile, TERMINAL_PROFILE_CJK_UTF8_AMBIGUOUS_WIDTH_KEY); + g_settings_reset (profile, TERMINAL_PROFILE_ENABLE_SIXEL_KEY); +} + +static gboolean +tree_model_id_to_iter_recurse (GtkTreeModel *model, + int id_column, + const char *active_id, + GtkTreeIter *iter, + GtkTreeIter *result_iter) +{ + do { + /* Descend the tree */ + GtkTreeIter child_iter; + if (gtk_tree_model_iter_children(model, &child_iter, iter) && + tree_model_id_to_iter_recurse (model, id_column, active_id, &child_iter, result_iter)) + return TRUE; + + gs_free char *id = nullptr; + gtk_tree_model_get (model, iter, id_column, &id, -1); + if (g_strcmp0 (id, active_id) == 0) { + *result_iter = *iter; + return TRUE; + } + } while (gtk_tree_model_iter_next (model, iter)); + + return FALSE; +} + +static gboolean +tree_model_id_to_iter (GtkTreeModel *model, + int id_column, + const char *active_id, + GtkTreeIter *iter) +{ + GtkTreeIter first_iter; + + return gtk_tree_model_get_iter_first(model, &first_iter) && + tree_model_id_to_iter_recurse(model, id_column, active_id, &first_iter, iter); +} + +static void +profile_encoding_combo_changed_cb (GtkComboBox *combo, + GSettings *profile) +{ + GtkTreeIter iter; + + if (!gtk_combo_box_get_active_iter(combo, &iter)) + return; + + gs_free char *encoding = nullptr; + gtk_tree_model_get(gtk_combo_box_get_model(combo), + &iter, + ENCODINGS_COL_ID, &encoding, + -1); + if (encoding == nullptr) + return; + + g_signal_handlers_block_by_func (profile, (void*)profile_notify_encoding_combo_cb, combo); + g_settings_set_string(profile, TERMINAL_PROFILE_ENCODING_KEY, encoding); + g_signal_handlers_unblock_by_func (profile, (void*)profile_notify_encoding_combo_cb, combo); +} + +static void +profile_notify_encoding_combo_cb (GSettings *profile, + const char *key, + GtkComboBox *combo) +{ + gs_free char *encoding = nullptr; + g_settings_get(profile, key, "s", &encoding); + + g_signal_handlers_block_by_func (combo, (void*)profile_encoding_combo_changed_cb, profile); + + GtkTreeIter iter; + if (tree_model_id_to_iter(gtk_combo_box_get_model(combo), + ENCODINGS_COL_ID, + encoding, + &iter)) { + gtk_combo_box_set_active_iter(combo, &iter); + } else { + gtk_combo_box_set_active(combo, -1); + } + + g_signal_handlers_unblock_by_func (combo, (void*)profile_encoding_combo_changed_cb, profile); +} + +/* + * initialize widgets + */ + +static void +init_color_scheme_menu (GtkWidget *widget) +{ + GtkCellRenderer *renderer; + GtkTreeIter iter; + gs_unref_object GtkListStore *store; + guint i; + + store = gtk_list_store_new (1, G_TYPE_STRING); + for (i = 0; i < G_N_ELEMENTS (color_schemes); ++i) + gtk_list_store_insert_with_values (store, &iter, -1, + 0, _(color_schemes[i].name), + -1); + gtk_list_store_insert_with_values (store, &iter, -1, + 0, _("Custom"), + -1); + + gtk_combo_box_set_model (GTK_COMBO_BOX (widget), GTK_TREE_MODEL (store)); + + renderer = gtk_cell_renderer_text_new (); + gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (widget), renderer, TRUE); + gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (widget), renderer, "text", 0, nullptr); +} + +typedef enum { + GROUP_UTF8, + GROUP_CJKV, + GROUP_OBSOLETE, + LAST_GROUP +} EncodingGroup; + +typedef struct { + const char *charset; + const char *name; + EncodingGroup group; +} EncodingEntry; + +/* These MUST be sorted by charset so that bsearch can work! */ +static const EncodingEntry encodings[] = { + { "BIG5", N_("Chinese Traditional"), GROUP_CJKV }, + { "BIG5-HKSCS", N_("Chinese Traditional"), GROUP_CJKV }, + { "CP866", N_("Cyrillic/Russian"), GROUP_OBSOLETE }, + { "EUC-JP", N_("Japanese"), GROUP_CJKV }, + { "EUC-KR", N_("Korean"), GROUP_CJKV }, + { "EUC-TW", N_("Chinese Traditional"), GROUP_CJKV }, + { "GB18030", N_("Chinese Simplified"), GROUP_CJKV }, + { "GB2312", N_("Chinese Simplified"), GROUP_CJKV }, + { "GBK", N_("Chinese Simplified"), GROUP_CJKV }, + { "IBM850", N_("Western"), GROUP_OBSOLETE }, + { "IBM852", N_("Central European"), GROUP_OBSOLETE }, + { "IBM855", N_("Cyrillic"), GROUP_OBSOLETE }, + { "IBM857", N_("Turkish"), GROUP_OBSOLETE }, + { "IBM862", N_("Hebrew"), GROUP_OBSOLETE }, + { "IBM864", N_("Arabic"), GROUP_OBSOLETE }, + { "ISO-8859-1", N_("Western"), GROUP_OBSOLETE }, + { "ISO-8859-10", N_("Nordic"), GROUP_OBSOLETE }, + { "ISO-8859-13", N_("Baltic"), GROUP_OBSOLETE }, + { "ISO-8859-14", N_("Celtic"), GROUP_OBSOLETE }, + { "ISO-8859-15", N_("Western"), GROUP_OBSOLETE }, + { "ISO-8859-16", N_("Romanian"), GROUP_OBSOLETE }, + { "ISO-8859-2", N_("Central European"), GROUP_OBSOLETE }, + { "ISO-8859-3", N_("South European"), GROUP_OBSOLETE }, + { "ISO-8859-4", N_("Baltic"), GROUP_OBSOLETE }, + { "ISO-8859-5", N_("Cyrillic"), GROUP_OBSOLETE }, + { "ISO-8859-6", N_("Arabic"), GROUP_OBSOLETE }, + { "ISO-8859-7", N_("Greek"), GROUP_OBSOLETE }, + { "ISO-8859-8", N_("Hebrew Visual"), GROUP_OBSOLETE }, + { "ISO-8859-8-I", N_("Hebrew"), GROUP_OBSOLETE }, + { "ISO-8859-9", N_("Turkish"), GROUP_OBSOLETE }, + { "KOI8-R", N_("Cyrillic"), GROUP_OBSOLETE }, + { "KOI8-U", N_("Cyrillic/Ukrainian"), GROUP_OBSOLETE }, + { "MAC-CYRILLIC", N_("Cyrillic"), GROUP_OBSOLETE }, + { "MAC_ARABIC", N_("Arabic"), GROUP_OBSOLETE }, + { "MAC_CE", N_("Central European"), GROUP_OBSOLETE }, + { "MAC_CROATIAN", N_("Croatian"), GROUP_OBSOLETE }, + { "MAC_GREEK", N_("Greek"), GROUP_OBSOLETE }, + { "MAC_HEBREW", N_("Hebrew"), GROUP_OBSOLETE }, + { "MAC_ROMAN", N_("Western"), GROUP_OBSOLETE }, + { "MAC_ROMANIAN", N_("Romanian"), GROUP_OBSOLETE }, + { "MAC_TURKISH", N_("Turkish"), GROUP_OBSOLETE }, + { "MAC_UKRAINIAN", N_("Cyrillic/Ukrainian"), GROUP_OBSOLETE }, + { "SHIFT_JIS", N_("Japanese"), GROUP_CJKV }, + { "TIS-620", N_("Thai"), GROUP_OBSOLETE }, + { "UHC", N_("Korean"), GROUP_CJKV }, + { "UTF-8", N_("Unicode"), GROUP_UTF8 }, + { "WINDOWS-1250", N_("Central European"), GROUP_OBSOLETE }, + { "WINDOWS-1251", N_("Cyrillic"), GROUP_OBSOLETE }, + { "WINDOWS-1252", N_("Western"), GROUP_OBSOLETE }, + { "WINDOWS-1253", N_("Greek"), GROUP_OBSOLETE }, + { "WINDOWS-1254", N_("Turkish"), GROUP_OBSOLETE }, + { "WINDOWS-1255", N_("Hebrew"), GROUP_OBSOLETE}, + { "WINDOWS-1256", N_("Arabic"), GROUP_OBSOLETE }, + { "WINDOWS-1257", N_("Baltic"), GROUP_OBSOLETE }, + { "WINDOWS-1258", N_("Vietnamese"), GROUP_OBSOLETE }, +}; + +static const struct { + EncodingGroup group; + const char *name; +} encodings_group_names[] = { + { GROUP_UTF8, N_("Unicode") }, + { GROUP_CJKV, N_("Legacy CJK Encodings") }, + { GROUP_OBSOLETE, N_("Obsolete Encodings") }, +}; + +#define EM_DASH "—" + +static void +append_encodings_for_group (GtkTreeStore *store, + EncodingGroup group, + gboolean submenu) +{ + GtkTreeIter parent_iter; + + if (submenu) { + gtk_tree_store_insert_with_values (store, + &parent_iter, + nullptr, + -1, + ENCODINGS_COL_ID, nullptr, + ENCODINGS_COL_TEXT, _(encodings_group_names[group].name), + -1); + } + + for (guint i = 0; i < G_N_ELEMENTS (encodings); i++) { + if (encodings[i].group != group) + continue; + + /* Skip encodings not supported by ICU */ + if (terminal_util_translate_encoding (encodings[i].charset) == nullptr) + continue; + + gs_free char *name = g_strdup_printf ("%s " EM_DASH " %s", + _(encodings[i].name), encodings[i].charset); + + GtkTreeIter iter; + gtk_tree_store_insert_with_values (store, + &iter, + submenu ? &parent_iter : nullptr, + -1, + ENCODINGS_COL_ID, encodings[i].charset, + ENCODINGS_COL_TEXT, name, + -1); + } +} + +static GtkTreeStore * +encodings_tree_store_new (void) +{ + GtkTreeStore *store = gtk_tree_store_new (2, G_TYPE_STRING, G_TYPE_STRING); + + append_encodings_for_group(store, GROUP_UTF8, FALSE); /* UTF-8 in main menu */ + append_encodings_for_group(store, GROUP_CJKV, TRUE); + append_encodings_for_group(store, GROUP_OBSOLETE, TRUE); + + return store; +} + +static void +init_encodings_combo (GtkWidget *widget) +{ + gs_unref_object GtkTreeStore *store = encodings_tree_store_new (); + gtk_combo_box_set_model (GTK_COMBO_BOX (widget), GTK_TREE_MODEL (store)); +} + +static gboolean +s_to_rgba (GValue *value, + GVariant *variant, + gpointer user_data) +{ + const char *s; + GdkRGBA color; + + g_variant_get (variant, "&s", &s); + if (!gdk_rgba_parse (&color, s)) + return FALSE; + + color.alpha = 1.0; + g_value_set_boxed (value, &color); + return TRUE; +} + +static GVariant * +rgba_to_s (const GValue *value, + const GVariantType *expected_type, + gpointer user_data) +{ + GdkRGBA *color; + gs_free char *s = nullptr; + + color = reinterpret_cast<GdkRGBA*>(g_value_get_boxed (value)); + if (color == nullptr) + return nullptr; + + s = gdk_rgba_to_string (color); + return g_variant_new_string (s); +} + +static gboolean +string_to_enum (GValue *value, + GVariant *variant, + gpointer user_data) +{ + GType (* get_type) (void) = (GType (*)(void))user_data; + GEnumClass *klass; + GEnumValue *eval = nullptr; + const char *s; + guint i; + + g_variant_get (variant, "&s", &s); + + klass = reinterpret_cast<GEnumClass*>(g_type_class_ref (get_type ())); + for (i = 0; i < klass->n_values; ++i) { + if (strcmp (klass->values[i].value_nick, s) != 0) + continue; + + eval = &klass->values[i]; + break; + } + + if (eval) + g_value_set_int (value, eval->value); + + g_type_class_unref (klass); + + return eval != nullptr; +} + +static GVariant * +enum_to_string (const GValue *value, + const GVariantType *expected_type, + gpointer user_data) +{ + GType (* get_type) (void) = (GType (*)(void))user_data; + GEnumClass *klass; + GEnumValue *eval = nullptr; + int val; + guint i; + GVariant *variant = nullptr; + + val = g_value_get_int (value); + + klass = reinterpret_cast<GEnumClass*>(g_type_class_ref (get_type ())); + for (i = 0; i < klass->n_values; ++i) { + if (klass->values[i].value != val) + continue; + + eval = &klass->values[i]; + break; + } + + if (eval) + variant = g_variant_new_string (eval->value_nick); + + g_type_class_unref (klass); + + return variant; +} + +static gboolean +scrollbar_policy_to_bool (GValue *value, + GVariant *variant, + gpointer user_data) +{ + const char *str; + + g_variant_get (variant, "&s", &str); + g_value_set_boolean (value, g_str_equal (str, "always")); + + return TRUE; +} + +static GVariant * +bool_to_scrollbar_policy (const GValue *value, + const GVariantType *expected_type, + gpointer user_data) +{ + return g_variant_new_string (g_value_get_boolean (value) ? "always" : "never"); +} + +static gboolean +monospace_filter (const PangoFontFamily *family, + const PangoFontFace *face, + gpointer data) +{ + return pango_font_family_is_monospace ((PangoFontFamily *) family); +} + +/* Called once per Preferences window, to initialize stuff that doesn't depend on the profile being edited */ +void +profile_prefs_init (void) +{ + GtkWidget *w; + GtkBuilder *builder = the_pref_data->builder; + char *text; + + the_pref_data->profile_signals = g_array_new (FALSE, FALSE, sizeof (ProfilePrefsSignal)); + the_pref_data->profile_bindings = g_array_new (FALSE, FALSE, sizeof (ProfilePrefsBinding)); + + w = (GtkWidget *) gtk_builder_get_object (builder, "color-scheme-combobox"); + init_color_scheme_menu (w); + + w = (GtkWidget *) gtk_builder_get_object (builder, "encoding-combobox"); + init_encodings_combo (w); + + /* Translators: Appears as: [numeric entry] × width */ + text = g_strdup_printf ("× %s", _("width")); + gtk_label_set_text ((GtkLabel *) gtk_builder_get_object (builder, "cell-width-scale-label"), + text); + g_free (text); + /* Translators: Appears as: [numeric entry] × height */ + text = g_strdup_printf ("× %s", _("height")); + gtk_label_set_text ((GtkLabel *) gtk_builder_get_object (builder, "cell-height-scale-label"), + text); + g_free (text); +} + +/* Called each time the user switches away from a profile, so it's no longer being edited */ +void +profile_prefs_unload (void) +{ + profile_prefs_signal_handlers_disconnect_all (); + profile_prefs_settings_unbind_all (); +} + +/* Called each time the user selects a new profile to edit */ +void +profile_prefs_load (const char *uuid, GSettings *profile) +{ + GtkWidget *w; + GtkBuilder *builder = the_pref_data->builder; + guint i; + + profile_prefs_unload (); + + gtk_label_set_text (GTK_LABEL (gtk_builder_get_object (builder, "profile-uuid")), + uuid); + + profile_prefs_signal_connect (gtk_builder_get_object (builder, "default-size-reset-button"), + "clicked", + G_CALLBACK (default_size_reset_cb), + profile); + profile_prefs_signal_connect (gtk_builder_get_object (builder, "cell-scale-reset-button"), + "clicked", + G_CALLBACK (cell_scale_reset_cb), + profile); + + /* Hook up the palette colorpickers and combo box */ + + for (i = 0; i < TERMINAL_PALETTE_SIZE; ++i) + { + char name[32]; + char *text; + + g_snprintf (name, sizeof (name), "palette-colorpicker-%u", i); + w = (GtkWidget *) gtk_builder_get_object (builder, name); + + g_object_set_data (G_OBJECT (w), "palette-entry-index", GUINT_TO_POINTER (i)); + + text = g_strdup_printf (_("Choose Palette Color %u"), i); + gtk_color_button_set_title (GTK_COLOR_BUTTON (w), text); + g_free (text); + + text = g_strdup_printf (_("Palette entry %u"), i); + gtk_widget_set_tooltip_text (w, text); + g_free (text); + + profile_prefs_signal_connect (w, "notify::rgba", + G_CALLBACK (palette_color_notify_cb), + profile); + } + + profile_palette_notify_colorpickers_cb (profile, TERMINAL_PROFILE_PALETTE_KEY, nullptr); + profile_prefs_signal_connect (profile, "changed::" TERMINAL_PROFILE_PALETTE_KEY, + G_CALLBACK (profile_palette_notify_colorpickers_cb), + nullptr); + + w = (GtkWidget *) gtk_builder_get_object (builder, "palette-combobox"); + profile_prefs_signal_connect (w, "notify::active", + G_CALLBACK (palette_scheme_combo_changed_cb), + profile); + + profile_palette_notify_scheme_combo_cb (profile, TERMINAL_PROFILE_PALETTE_KEY, GTK_COMBO_BOX (w)); + profile_prefs_signal_connect (profile, "changed::" TERMINAL_PROFILE_PALETTE_KEY, + G_CALLBACK (profile_palette_notify_scheme_combo_cb), + w); + + /* Hook up the color scheme pickers and combo box */ + w = (GtkWidget *) gtk_builder_get_object (builder, "color-scheme-combobox"); + profile_prefs_signal_connect (w, "notify::active", + G_CALLBACK (color_scheme_combo_changed_cb), + profile); + + profile_colors_notify_scheme_combo_cb (profile, nullptr, GTK_COMBO_BOX (w)); + profile_prefs_signal_connect (profile, "changed::" TERMINAL_PROFILE_FOREGROUND_COLOR_KEY, + G_CALLBACK (profile_colors_notify_scheme_combo_cb), + w); + profile_prefs_signal_connect (profile, "changed::" TERMINAL_PROFILE_BACKGROUND_COLOR_KEY, + G_CALLBACK (profile_colors_notify_scheme_combo_cb), + w); + + w = GTK_WIDGET (gtk_builder_get_object (builder, "custom-command-entry")); + custom_command_entry_changed_cb (GTK_ENTRY (w)); + profile_prefs_signal_connect (w, "changed", + G_CALLBACK (custom_command_entry_changed_cb), nullptr); + + profile_prefs_signal_connect (gtk_builder_get_object (builder, "reset-compat-defaults-button"), + "clicked", + G_CALLBACK (reset_compat_defaults_cb), + profile); + + profile_prefs_settings_bind_with_mapping (profile, + TERMINAL_PROFILE_BACKGROUND_COLOR_KEY, + gtk_builder_get_object (builder, + "background-colorpicker"), + "rgba", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_SET), + (GSettingsBindGetMapping) s_to_rgba, + (GSettingsBindSetMapping) rgba_to_s, + nullptr, nullptr); + + profile_prefs_settings_bind_with_mapping (profile, + TERMINAL_PROFILE_BACKSPACE_BINDING_KEY, + gtk_builder_get_object (builder, + "backspace-binding-combobox"), + "active", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_SET), + (GSettingsBindGetMapping) string_to_enum, + (GSettingsBindSetMapping) enum_to_string, + vte_erase_binding_get_type, nullptr); + profile_prefs_settings_bind (profile, + TERMINAL_PROFILE_BOLD_IS_BRIGHT_KEY, + gtk_builder_get_object (builder, "bold-is-bright-checkbutton"), + "active", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_SET)); + profile_prefs_settings_bind (profile, TERMINAL_PROFILE_BOLD_COLOR_SAME_AS_FG_KEY, + gtk_builder_get_object (builder, + "bold-color-checkbutton"), + "active", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_INVERT_BOOLEAN | + G_SETTINGS_BIND_SET)); + + w = GTK_WIDGET (gtk_builder_get_object (builder, "bold-colorpicker")); + profile_prefs_settings_bind (profile, TERMINAL_PROFILE_BOLD_COLOR_SAME_AS_FG_KEY, + w, + "sensitive", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_INVERT_BOOLEAN | + G_SETTINGS_BIND_NO_SENSITIVITY)); + profile_prefs_settings_bind_with_mapping (profile, TERMINAL_PROFILE_BOLD_COLOR_KEY, + w, + "rgba", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_SET | + G_SETTINGS_BIND_NO_SENSITIVITY), + (GSettingsBindGetMapping) s_to_rgba, + (GSettingsBindSetMapping) rgba_to_s, + nullptr, nullptr); + + w = GTK_WIDGET (gtk_builder_get_object (builder, "cell-height-scale-spinbutton")); + profile_prefs_settings_bind (profile, TERMINAL_PROFILE_CELL_HEIGHT_SCALE_KEY, + gtk_spin_button_get_adjustment (GTK_SPIN_BUTTON (w)), + "value", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_SET)); + + w = GTK_WIDGET (gtk_builder_get_object (builder, "cell-width-scale-spinbutton")); + profile_prefs_settings_bind (profile, TERMINAL_PROFILE_CELL_WIDTH_SCALE_KEY, + gtk_spin_button_get_adjustment (GTK_SPIN_BUTTON (w)), + "value", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_SET)); + + profile_prefs_settings_bind (profile, TERMINAL_PROFILE_CURSOR_COLORS_SET_KEY, + gtk_builder_get_object (builder, + "cursor-colors-checkbutton"), + "active", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_SET)); + + w = GTK_WIDGET (gtk_builder_get_object (builder, "cursor-foreground-colorpicker")); + profile_prefs_settings_bind (profile, TERMINAL_PROFILE_CURSOR_COLORS_SET_KEY, + w, + "sensitive", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_NO_SENSITIVITY)); + profile_prefs_settings_bind_with_mapping (profile, TERMINAL_PROFILE_CURSOR_FOREGROUND_COLOR_KEY, + w, + "rgba", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_SET | + G_SETTINGS_BIND_NO_SENSITIVITY), + (GSettingsBindGetMapping) s_to_rgba, + (GSettingsBindSetMapping) rgba_to_s, + nullptr, nullptr); + + w = GTK_WIDGET (gtk_builder_get_object (builder, "cursor-background-colorpicker")); + profile_prefs_settings_bind (profile, TERMINAL_PROFILE_CURSOR_COLORS_SET_KEY, + w, + "sensitive", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_NO_SENSITIVITY)); + profile_prefs_settings_bind_with_mapping (profile, TERMINAL_PROFILE_CURSOR_BACKGROUND_COLOR_KEY, + w, + "rgba", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_SET | + G_SETTINGS_BIND_NO_SENSITIVITY), + (GSettingsBindGetMapping) s_to_rgba, + (GSettingsBindSetMapping) rgba_to_s, + nullptr, nullptr); + + profile_prefs_settings_bind (profile, TERMINAL_PROFILE_HIGHLIGHT_COLORS_SET_KEY, + gtk_builder_get_object (builder, + "highlight-colors-checkbutton"), + "active", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_SET)); + + w = GTK_WIDGET (gtk_builder_get_object (builder, "highlight-foreground-colorpicker")); + profile_prefs_settings_bind (profile, TERMINAL_PROFILE_HIGHLIGHT_COLORS_SET_KEY, + w, + "sensitive", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_NO_SENSITIVITY)); + profile_prefs_settings_bind_with_mapping (profile, TERMINAL_PROFILE_HIGHLIGHT_FOREGROUND_COLOR_KEY, + w, + "rgba", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_SET | + G_SETTINGS_BIND_NO_SENSITIVITY), + (GSettingsBindGetMapping) s_to_rgba, + (GSettingsBindSetMapping) rgba_to_s, + nullptr, nullptr); + + w = GTK_WIDGET (gtk_builder_get_object (builder, "highlight-background-colorpicker")); + profile_prefs_settings_bind (profile, TERMINAL_PROFILE_HIGHLIGHT_COLORS_SET_KEY, + w, + "sensitive", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_NO_SENSITIVITY)); + profile_prefs_settings_bind_with_mapping (profile, TERMINAL_PROFILE_HIGHLIGHT_BACKGROUND_COLOR_KEY, + w, + "rgba", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_SET | + G_SETTINGS_BIND_NO_SENSITIVITY), + (GSettingsBindGetMapping) s_to_rgba, + (GSettingsBindSetMapping) rgba_to_s, + nullptr, nullptr); + + profile_prefs_settings_bind_with_mapping (profile, TERMINAL_PROFILE_CURSOR_SHAPE_KEY, + gtk_builder_get_object (builder, + "cursor-shape-combobox"), + "active", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_SET), + (GSettingsBindGetMapping) string_to_enum, + (GSettingsBindSetMapping) enum_to_string, + vte_cursor_shape_get_type, nullptr); + profile_prefs_settings_bind_with_mapping (profile, TERMINAL_PROFILE_CURSOR_BLINK_MODE_KEY, + gtk_builder_get_object (builder, + "cursor-blink-mode-combobox"), + "active", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_SET), + (GSettingsBindGetMapping) string_to_enum, + (GSettingsBindSetMapping) enum_to_string, + vte_cursor_blink_mode_get_type, nullptr); + profile_prefs_settings_bind_with_mapping (profile, TERMINAL_PROFILE_TEXT_BLINK_MODE_KEY, + gtk_builder_get_object (builder, + "text-blink-mode-combobox"), + "active", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_SET), + (GSettingsBindGetMapping) string_to_enum, + (GSettingsBindSetMapping) enum_to_string, + vte_text_blink_mode_get_type, nullptr); + + profile_prefs_settings_bind (profile, TERMINAL_PROFILE_CUSTOM_COMMAND_KEY, + gtk_builder_get_object (builder, + "custom-command-entry"), + "text", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_SET)); + + w = GTK_WIDGET (gtk_builder_get_object (builder, "default-size-columns-spinbutton")); + profile_prefs_settings_bind (profile, TERMINAL_PROFILE_DEFAULT_SIZE_COLUMNS_KEY, + gtk_spin_button_get_adjustment (GTK_SPIN_BUTTON (w)), + "value", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_SET)); + + w = GTK_WIDGET (gtk_builder_get_object (builder, "default-size-rows-spinbutton")); + profile_prefs_settings_bind (profile, TERMINAL_PROFILE_DEFAULT_SIZE_ROWS_KEY, + gtk_spin_button_get_adjustment (GTK_SPIN_BUTTON (w)), + "value", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_SET)); + + profile_prefs_settings_bind_with_mapping (profile, TERMINAL_PROFILE_DELETE_BINDING_KEY, + gtk_builder_get_object (builder, + "delete-binding-combobox"), + "active", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_SET), + (GSettingsBindGetMapping) string_to_enum, + (GSettingsBindSetMapping) enum_to_string, + vte_erase_binding_get_type, nullptr); + profile_prefs_settings_bind_with_mapping (profile, TERMINAL_PROFILE_EXIT_ACTION_KEY, + gtk_builder_get_object (builder, + "exit-action-combobox"), + "active", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_SET), + (GSettingsBindGetMapping) string_to_enum, + (GSettingsBindSetMapping) enum_to_string, + terminal_exit_action_get_type, nullptr); + w = (GtkWidget*) gtk_builder_get_object (builder, "font-selector"); + gtk_font_chooser_set_filter_func (GTK_FONT_CHOOSER (w), monospace_filter, nullptr, nullptr); +#if GTK_CHECK_VERSION (3, 24, 0) + gtk_font_chooser_set_level (GTK_FONT_CHOOSER (w), + GtkFontChooserLevel(GTK_FONT_CHOOSER_LEVEL_FAMILY | + GTK_FONT_CHOOSER_LEVEL_SIZE)); +#endif + + profile_prefs_settings_bind (profile, TERMINAL_PROFILE_FONT_KEY, + w, + "font-name", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_SET)); + + profile_prefs_settings_bind_with_mapping (profile, + TERMINAL_PROFILE_FOREGROUND_COLOR_KEY, + gtk_builder_get_object (builder, + "foreground-colorpicker"), + "rgba", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_SET), + (GSettingsBindGetMapping) s_to_rgba, + (GSettingsBindSetMapping) rgba_to_s, + nullptr, nullptr); + + profile_prefs_settings_bind (profile, TERMINAL_PROFILE_LOGIN_SHELL_KEY, + gtk_builder_get_object (builder, + "login-shell-checkbutton"), + "active", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_SET)); + + w = GTK_WIDGET (gtk_builder_get_object (builder, "scrollback-lines-spinbutton")); + profile_prefs_settings_bind (profile, TERMINAL_PROFILE_SCROLLBACK_LINES_KEY, + gtk_spin_button_get_adjustment (GTK_SPIN_BUTTON (w)), + "value", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_SET)); + + profile_prefs_settings_bind (profile, TERMINAL_PROFILE_SCROLLBACK_UNLIMITED_KEY, + gtk_builder_get_object (builder, + "scrollback-limited-checkbutton"), + "active", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_SET | + G_SETTINGS_BIND_INVERT_BOOLEAN)); + profile_prefs_settings_bind (profile, TERMINAL_PROFILE_SCROLLBACK_UNLIMITED_KEY, + gtk_builder_get_object (builder, + "scrollback-box"), + "sensitive", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_INVERT_BOOLEAN | + G_SETTINGS_BIND_NO_SENSITIVITY)); + profile_prefs_settings_bind_with_mapping (profile, + TERMINAL_PROFILE_SCROLLBAR_POLICY_KEY, + gtk_builder_get_object (builder, + "scrollbar-checkbutton"), + "active", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_SET), + (GSettingsBindGetMapping) scrollbar_policy_to_bool, + (GSettingsBindSetMapping) bool_to_scrollbar_policy, + nullptr, nullptr); + profile_prefs_settings_bind (profile, TERMINAL_PROFILE_SCROLL_ON_KEYSTROKE_KEY, + gtk_builder_get_object (builder, + "scroll-on-keystroke-checkbutton"), + "active", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_SET)); + profile_prefs_settings_bind (profile, TERMINAL_PROFILE_SCROLL_ON_OUTPUT_KEY, + gtk_builder_get_object (builder, + "scroll-on-output-checkbutton"), + "active", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_SET)); + profile_prefs_settings_bind (profile, TERMINAL_PROFILE_USE_SYSTEM_FONT_KEY, + gtk_builder_get_object (builder, + "custom-font-checkbutton"), + "active", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_SET | + G_SETTINGS_BIND_INVERT_BOOLEAN)); + + w = (GtkWidget *) gtk_builder_get_object (builder, "preserve-working-directory-combobox"); + profile_prefs_settings_bind_with_mapping (profile, TERMINAL_PROFILE_PRESERVE_WORKING_DIRECTORY_KEY, w, + "active", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_SET), + (GSettingsBindGetMapping) string_to_enum, + (GSettingsBindSetMapping) enum_to_string, + terminal_preserve_working_directory_get_type, nullptr); + + profile_prefs_settings_bind (profile, TERMINAL_PROFILE_USE_CUSTOM_COMMAND_KEY, + gtk_builder_get_object (builder, + "use-custom-command-checkbutton"), + "active", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_SET)); + + profile_prefs_settings_bind (profile, TERMINAL_PROFILE_USE_THEME_COLORS_KEY, + gtk_builder_get_object (builder, + "use-theme-colors-checkbutton"), + "active", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_SET)); + profile_prefs_settings_bind (profile, TERMINAL_PROFILE_AUDIBLE_BELL_KEY, + gtk_builder_get_object (builder, "bell-checkbutton"), + "active", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_SET)); + + profile_prefs_settings_bind (profile, + TERMINAL_PROFILE_USE_CUSTOM_COMMAND_KEY, + gtk_builder_get_object (builder, "custom-command-entry-label"), + "sensitive", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_NO_SENSITIVITY)); + profile_prefs_settings_bind (profile, + TERMINAL_PROFILE_USE_CUSTOM_COMMAND_KEY, + gtk_builder_get_object (builder, "custom-command-entry"), + "sensitive", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_NO_SENSITIVITY)); + profile_prefs_settings_bind (profile, + TERMINAL_PROFILE_USE_SYSTEM_FONT_KEY, + gtk_builder_get_object (builder, "font-selector"), + "sensitive", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_INVERT_BOOLEAN | + G_SETTINGS_BIND_NO_SENSITIVITY)); + profile_prefs_settings_bind (profile, + TERMINAL_PROFILE_USE_THEME_COLORS_KEY, + gtk_builder_get_object (builder, "colors-box"), + "sensitive", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_INVERT_BOOLEAN | + G_SETTINGS_BIND_NO_SENSITIVITY)); + profile_prefs_settings_bind_writable (profile, + TERMINAL_PROFILE_PALETTE_KEY, + gtk_builder_get_object (builder, "palette-box"), + "sensitive", + FALSE); + + /* Compatibility options */ + w = (GtkWidget *) gtk_builder_get_object (builder, "encoding-combobox"); + profile_prefs_signal_connect (w, "changed", + G_CALLBACK (profile_encoding_combo_changed_cb), + profile); + + profile_notify_encoding_combo_cb (profile, TERMINAL_PROFILE_ENCODING_KEY, GTK_COMBO_BOX (w)); + profile_prefs_signal_connect (profile, "changed::" TERMINAL_PROFILE_ENCODING_KEY, + G_CALLBACK (profile_notify_encoding_combo_cb), + w); + + w = (GtkWidget *) gtk_builder_get_object (builder, "cjk-ambiguous-width-combobox"); + profile_prefs_settings_bind (profile, TERMINAL_PROFILE_CJK_UTF8_AMBIGUOUS_WIDTH_KEY, + w, + "active-id", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_SET)); + + w = (GtkWidget *) gtk_builder_get_object (builder, "enable-sixel-checkbutton"); + profile_prefs_settings_bind (profile, TERMINAL_PROFILE_ENABLE_SIXEL_KEY, w, + "active", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_SET)); + gtk_widget_set_visible (w, (vte_get_feature_flags() & VTE_FEATURE_FLAG_SIXEL) != 0); +} + +/* Called once per Preferences window, to destroy stuff that doesn't depend on the profile being edited */ +void +profile_prefs_destroy (void) +{ + profile_prefs_unload (); + + g_array_free (the_pref_data->profile_signals, TRUE); + g_array_free (the_pref_data->profile_bindings, TRUE); +} diff --git a/src/profile-editor.hh b/src/profile-editor.hh new file mode 100644 index 0000000..e83d393 --- /dev/null +++ b/src/profile-editor.hh @@ -0,0 +1,37 @@ +/* + * Copyright © 2002 Havoc Pennington + * + * 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/>. + */ + +#ifndef TERMINAL_PROFILE_EDITOR_H +#define TERMINAL_PROFILE_EDITOR_H + +#include <gio/gio.h> +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +void profile_prefs_init (void); + +void profile_prefs_destroy (void); + +void profile_prefs_unload (void); + +void profile_prefs_load (const char *uuid, + GSettings *profile); + +G_END_DECLS + +#endif /* TERMINAL_PROFILE_EDITOR_H */ diff --git a/src/search-popover.ui b/src/search-popover.ui new file mode 100644 index 0000000..4bea44d --- /dev/null +++ b/src/search-popover.ui @@ -0,0 +1,251 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.19.0 --> +<interface> + <requires lib="gtk+" version="3.16"/> + <template class="TerminalSearchPopover" parent="GtkWindow"> + <property name="can_focus">False</property> + <property name="title" translatable="yes">Find</property> + <property name="resizable">False</property> + <property name="skip_pager_hint">True</property> + <child> + <object class="GtkBox" id="box1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_left">12</property> + <property name="margin_right">12</property> + <property name="margin_top">12</property> + <property name="margin_bottom">12</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox" id="box2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">18</property> + <child> + <object class="GtkBox" id="box4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkSearchEntry" id="search_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="activates_default">True</property> + <property name="width_chars">30</property> + <property name="primary_icon_name">edit-find-symbolic</property> + <property name="primary_icon_activatable">False</property> + <property name="primary_icon_sensitive">False</property> + <property name="placeholder_text" translatable="yes">Find</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="search_prev_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">True</property> + <property name="tooltip_text" translatable="yes">Find previous occurrence</property> + <property name="focus_on_click">False</property> + <child> + <object class="GtkImage" id="image2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">go-up-symbolic</property> + <property name="use_fallback">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="search_next_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="tooltip_text" translatable="yes">Find next occurrence</property> + <property name="focus_on_click">False</property> + <child> + <object class="GtkImage" id="image3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">go-down-symbolic</property> + <property name="use_fallback">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <style> + <class name="linked"/> + </style> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="reveal_button"> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="tooltip_text" translatable="yes">Toggle search options</property> + <property name="focus_on_click">False</property> + <property name="active">True</property> + <child> + <object class="GtkImage" id="image1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">view-context-menu-symbolic</property> + <property name="use_fallback">True</property> + </object> + </child> + <accessibility> + <relation type="controller-for" target="revealer"/> + </accessibility> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="close_button"> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="focus_on_click">False</property> + <child> + <object class="GtkImage" id="image4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">window-close-symbolic</property> + <property name="use_fallback">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkRevealer" id="revealer"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="transition_type">none</property> + <property name="reveal_child">True</property> + <child> + <object class="GtkBox" id="box3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_top">18</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <child> + <object class="GtkCheckButton" id="match_case_checkbutton"> + <property name="label" translatable="yes">_Match case</property> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="focus_on_click">False</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="entire_word_checkbutton"> + <property name="label" translatable="yes">Match _entire word only</property> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="focus_on_click">False</property> + <property name="xalign">0</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="regex_checkbutton"> + <property name="label" translatable="yes">Match as _regular expression</property> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="focus_on_click">False</property> + <property name="xalign">0</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="wrap_around_checkbutton"> + <property name="label" translatable="yes">_Wrap around</property> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="focus_on_click">False</property> + <property name="xalign">0</property> + <property name="active">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">3</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </template> +</interface> diff --git a/src/server.cc b/src/server.cc new file mode 100644 index 0000000..48c09f3 --- /dev/null +++ b/src/server.cc @@ -0,0 +1,195 @@ +/* + * Copyright © 2001, 2002 Havoc Pennington + * Copyright © 2002 Red Hat, Inc. + * Copyright © 2002 Sun Microsystems + * Copyright © 2003 Mariano Suarez-Alvarez + * Copyright © 2008, 2010, 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 <errno.h> +#include <locale.h> +#include <pthread.h> +#include <stdlib.h> +#include <time.h> +#include <unistd.h> +#include <sys/resource.h> +#include <sys/types.h> + +#include <glib.h> +#include <glib/gi18n.h> +#include <glib/gstdio.h> +#include <gio/gio.h> + +#include "terminal-app.hh" +#include "terminal-debug.hh" +#include "terminal-gdbus.hh" +#include "terminal-i18n.hh" +#include "terminal-defines.hh" +#include "terminal-libgsystem.hh" + +static char *app_id = nullptr; + +#define INACTIVITY_TIMEOUT (100 /* ms */) + +static gboolean +option_app_id_cb (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + if (!g_application_id_is_valid (value) || + !g_dbus_is_name (value)) { + g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, + "\"%s\" is not a valid application ID", value); + return FALSE; + } + + g_free (app_id); + app_id = g_strdup (value); + + return TRUE; +} + +static const GOptionEntry options[] = { + { "app-id", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_CALLBACK, (void*)option_app_id_cb, "Application ID", "ID" }, + { nullptr } +}; + +/* We use up to 8 FDs per terminal, so let's bump the limit way up. + * However we need to restore the original limit for the child processes. + */ + +static struct rlimit sv_rlimit_nofile; + +static void +atfork_child_restore_rlimit_nofile (void) +{ + if (setrlimit (RLIMIT_NOFILE, &sv_rlimit_nofile) < 0) + _exit (127); +} + +static gboolean +increase_rlimit_nofile (void) +{ + struct rlimit l; + + if (getrlimit (RLIMIT_NOFILE, &sv_rlimit_nofile) < 0) + return FALSE; + + if (pthread_atfork (nullptr, nullptr, atfork_child_restore_rlimit_nofile) != 0) + return FALSE; + + l.rlim_cur = l.rlim_max = sv_rlimit_nofile.rlim_max; + if (setrlimit (RLIMIT_NOFILE, &l) < 0) + return FALSE; + + return TRUE; +} + +static int +init_server (int argc, + char *argv[], + GApplication **application) +{ + if (G_UNLIKELY ((getuid () != geteuid () || + getgid () != getegid ()) && + geteuid () == 0 && + getegid () == 0)) { + g_printerr ("Wrong euid/egid, exiting.\n"); + return _EXIT_FAILURE_WRONG_ID; + } + + if (setlocale (LC_ALL, "") == nullptr) { + g_printerr ("Locale not supported.\n"); + return _EXIT_FAILURE_UNSUPPORTED_LOCALE; + } + + terminal_i18n_init (TRUE); + + g_unsetenv ("CHARSET"); + const char *charset; + if (!g_get_charset (&charset)) { + g_printerr ("Non UTF-8 locale (%s) is not supported!\n", charset); + return _EXIT_FAILURE_NO_UTF8; + } + + /* Sanitise environment */ + g_unsetenv ("DBUS_STARTER_BUS_TYPE"); + + /* Not interested in silly debug spew polluting the journal, bug #749195 */ + if (g_getenv ("G_ENABLE_DIAGNOSTIC") == nullptr) + g_setenv ("G_ENABLE_DIAGNOSTIC", "0", TRUE); + + _terminal_debug_init (); + + /* Change directory to $HOME so we don't prevent unmounting, e.g. if the + * factory is started by nautilus-open-terminal. See bug #565328. + * On failure back to /. + */ + const char *home_dir = g_get_home_dir (); + if (home_dir == nullptr || chdir (home_dir) < 0) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-result" + chdir ("/"); +#pragma GCC diagnostic pop + + g_set_prgname ("gnome-terminal-server"); + g_set_application_name (_("Terminal")); + + GError *error = nullptr; + if (!gtk_init_with_args (&argc, &argv, nullptr, options, nullptr, &error)) { + if (error != nullptr) { + g_printerr ("Failed to parse arguments: %s\n", error->message); + g_error_free (error); + } + + return _EXIT_FAILURE_GTK_INIT; + } + + if (!increase_rlimit_nofile ()) { + auto const errsv = errno; + g_printerr ("Failed to increase RLIMIT_NOFILE: %s\n", g_strerror(errsv)); + } + + /* Now we can create the app */ + auto const app = terminal_app_new (app_id, G_APPLICATION_IS_SERVICE, nullptr); + g_free (app_id); + app_id = nullptr; + + /* We stay around a bit after the last window closed */ + g_application_set_inactivity_timeout (app, INACTIVITY_TIMEOUT); + + *application = app; + return 0; +} + +int +main (int argc, + char *argv[]) +{ + gs_unref_object GApplication *app = nullptr; + int r = init_server (argc, argv, &app); + if (r != 0) + return r; + + /* Note that this flushes the D-Bus connection just before quitting, + * thus ensuring that all pending signal emissions (e.g. child-exited) + * are delivered. + */ + return g_application_run (app, 0, nullptr); +} diff --git a/src/terminal-accels.cc b/src/terminal-accels.cc new file mode 100644 index 0000000..6629810 --- /dev/null +++ b/src/terminal-accels.cc @@ -0,0 +1,615 @@ +/* + * Copyright © 2001, 2002 Havoc Pennington, Red Hat Inc. + * Copyright © 2008, 2011, 2012, 2013 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 <string.h> + +#include <glib.h> +#include <glib/gi18n.h> +#include <gtk/gtk.h> + +#include "terminal-accels.hh" +#include "terminal-app.hh" +#include "terminal-debug.hh" +#include "terminal-schemas.hh" +#include "terminal-intl.hh" +#include "terminal-util.hh" +#include "terminal-libgsystem.hh" + +/* NOTES + * + * There are two sources of keybindings changes, from GSettings and from + * the accel map (happens with in-place menu editing). + * + * When a keybinding gsettings key changes, we propagate that into the + * accel map. + * When the accel map changes, we queue a sync to GSettings. + * + * To avoid infinite loops, we short-circuit in both directions + * if the value is unchanged from last known. + * + * In the keybinding editor, when editing or clearing an accel, we write + * the change directly to GSettings and rely on the callback to + * actually apply the change to the accel map. + */ + +#define KEY_CLOSE_TAB "close-tab" +#define KEY_CLOSE_WINDOW "close-window" +#define KEY_COPY "copy" +#define KEY_COPY_HTML "copy-html" +#define KEY_DETACH_TAB "detach-tab" +#define KEY_EXPORT "export" +#define KEY_FIND "find" +#define KEY_FIND_CLEAR "find-clear" +#define KEY_FIND_PREV "find-previous" +#define KEY_FIND_NEXT "find-next" +#define KEY_FULL_SCREEN "full-screen" +#define KEY_HEADER_MENU "header-menu" +#define KEY_HELP "help" +#define KEY_MOVE_TAB_LEFT "move-tab-left" +#define KEY_MOVE_TAB_RIGHT "move-tab-right" +#define KEY_NEW_TAB "new-tab" +#define KEY_NEW_WINDOW "new-window" +#define KEY_NEXT_TAB "next-tab" +#define KEY_PASTE "paste" +#define KEY_PREFERENCES "preferences" +#define KEY_PREV_TAB "prev-tab" +#define KEY_PRINT "print" +#define KEY_READ_ONLY "read-only" +#define KEY_RESET_AND_CLEAR "reset-and-clear" +#define KEY_RESET "reset" +#define KEY_SAVE_CONTENTS "save-contents" +#define KEY_SELECT_ALL "select-all" +#define KEY_TOGGLE_MENUBAR "toggle-menubar" +#define KEY_ZOOM_IN "zoom-in" +#define KEY_ZOOM_NORMAL "zoom-normal" +#define KEY_ZOOM_OUT "zoom-out" +#define KEY_SWITCH_TAB_PREFIX "switch-to-tab-" + +#if 1 +/* +* We don't want to enable content saving until vte supports it async. +* So we disable this code for stable versions. +*/ +#include "terminal-version.hh" + +#if (TERMINAL_MINOR_VERSION & 1) != 0 +#define ENABLE_SAVE +#else +#undef ENABLE_SAVE +#endif +#endif + +typedef struct +{ + const char *user_visible_name; + const char *settings_key; + const char *action_name; + const GVariantType *action_parameter_type; + const char *action_parameter; + GVariant *parameter; + const char *shadow_action_name; +} KeyEntry; + +typedef struct +{ + KeyEntry *key_entry; + guint n_elements; + const char *user_visible_name; + gboolean headerbar_only; +} KeyEntryList; + +#define ENTRY_FULL(name, key, action, type, parameter, shadow_name) \ + { name, key, "win." action, (const GVariantType *) type, parameter, nullptr, shadow_name } +#define ENTRY(name, key, action, type, parameter) \ + ENTRY_FULL (name, key, action, type, parameter, "win.shadow") +#define ENTRY_MDI(name, key, action, type, parameter) \ + ENTRY_FULL (name, key, action, type, parameter, "win.shadow-mdi") + +static KeyEntry file_entries[] = { + ENTRY (N_("New Tab"), KEY_NEW_TAB, "new-terminal", "(ss)", "('tab','current')" ), + ENTRY (N_("New Window"), KEY_NEW_WINDOW, "new-terminal", "(ss)", "('window','current')"), +#ifdef ENABLE_SAVE + ENTRY (N_("Save Contents"), KEY_SAVE_CONTENTS, "save-contents", nullptr, nullptr ), +#endif +#ifdef ENABLE_EXPORT + ENTRY (N_("Export"), KEY_EXPORT, "export", nullptr, nullptr ), +#endif +#ifdef ENABLE_PRINT + ENTRY (N_("Print"), KEY_PRINT, "print", nullptr, nullptr ), +#endif + ENTRY (N_("Close Tab"), KEY_CLOSE_TAB, "close", "s", "'tab'" ), + ENTRY (N_("Close Window"), KEY_CLOSE_WINDOW, "close", "s", "'window'" ), +}; + +static KeyEntry edit_entries[] = { + ENTRY (N_("Copy"), KEY_COPY, "copy", "s", "'text'" ), + ENTRY (N_("Copy as HTML"), KEY_COPY_HTML, "copy", "s", "'html'" ), + ENTRY (N_("Paste"), KEY_PASTE, "paste-text", nullptr, nullptr ), + ENTRY (N_("Select All"), KEY_SELECT_ALL, "select-all", nullptr, nullptr ), + ENTRY (N_("Preferences"), KEY_PREFERENCES, "edit-preferences", nullptr, nullptr ), +}; + +static KeyEntry search_entries[] = { + ENTRY (N_("Find"), KEY_FIND, "find", nullptr, nullptr), + ENTRY (N_("Find Next"), KEY_FIND_NEXT, "find-forward", nullptr, nullptr), + ENTRY (N_("Find Previous"), KEY_FIND_PREV, "find-backward", nullptr, nullptr), + ENTRY (N_("Clear Highlight"), KEY_FIND_CLEAR, "find-clear", nullptr, nullptr) +}; + +static KeyEntry view_entries[] = { + ENTRY (N_("Hide and Show Menubar"), KEY_TOGGLE_MENUBAR, "menubar-visible", nullptr, nullptr), + ENTRY (N_("Full Screen"), KEY_FULL_SCREEN, "fullscreen", nullptr, nullptr), + ENTRY (N_("Zoom In"), KEY_ZOOM_IN, "zoom-in", nullptr, nullptr), + ENTRY (N_("Zoom Out"), KEY_ZOOM_OUT, "zoom-out", nullptr, nullptr), + ENTRY (N_("Normal Size"), KEY_ZOOM_NORMAL, "zoom-normal", nullptr, nullptr) +}; + +static KeyEntry terminal_entries[] = { + ENTRY (N_("Read-Only"), KEY_READ_ONLY, "read-only", nullptr, nullptr ), + ENTRY (N_("Reset"), KEY_RESET, "reset", "b", "false"), + ENTRY (N_("Reset and Clear"), KEY_RESET_AND_CLEAR, "reset", "b", "true" ), +}; + +static KeyEntry tabs_entries[] = { + ENTRY_MDI (N_("Switch to Previous Tab"), KEY_PREV_TAB, "tab-switch-left", nullptr, nullptr), + ENTRY_MDI (N_("Switch to Next Tab"), KEY_NEXT_TAB, "tab-switch-right", nullptr, nullptr), + ENTRY_MDI (N_("Move Tab to the Left"), KEY_MOVE_TAB_LEFT, "tab-move-left", nullptr, nullptr), + ENTRY_MDI (N_("Move Tab to the Right"), KEY_MOVE_TAB_RIGHT, "tab-move-right", nullptr, nullptr), + ENTRY_MDI (N_("Detach Tab"), KEY_DETACH_TAB, "tab-detach", nullptr, nullptr), + ENTRY_MDI (nullptr, KEY_SWITCH_TAB_PREFIX "1", "active-tab", "i", "0"), + ENTRY_MDI (nullptr, KEY_SWITCH_TAB_PREFIX "2", "active-tab", "i", "1"), + ENTRY_MDI (nullptr, KEY_SWITCH_TAB_PREFIX "3", "active-tab", "i", "2"), + ENTRY_MDI (nullptr, KEY_SWITCH_TAB_PREFIX "4", "active-tab", "i", "3"), + ENTRY_MDI (nullptr, KEY_SWITCH_TAB_PREFIX "5", "active-tab", "i", "4"), + ENTRY_MDI (nullptr, KEY_SWITCH_TAB_PREFIX "6", "active-tab", "i", "5"), + ENTRY_MDI (nullptr, KEY_SWITCH_TAB_PREFIX "7", "active-tab", "i", "6"), + ENTRY_MDI (nullptr, KEY_SWITCH_TAB_PREFIX "8", "active-tab", "i", "7"), + ENTRY_MDI (nullptr, KEY_SWITCH_TAB_PREFIX "9", "active-tab", "i", "8"), + ENTRY_MDI (nullptr, KEY_SWITCH_TAB_PREFIX "10", "active-tab", "i", "9"), + ENTRY_MDI (nullptr, KEY_SWITCH_TAB_PREFIX "11", "active-tab", "i", "10"), + ENTRY_MDI (nullptr, KEY_SWITCH_TAB_PREFIX "12", "active-tab", "i", "11"), + ENTRY_MDI (nullptr, KEY_SWITCH_TAB_PREFIX "13", "active-tab", "i", "12"), + ENTRY_MDI (nullptr, KEY_SWITCH_TAB_PREFIX "14", "active-tab", "i", "13"), + ENTRY_MDI (nullptr, KEY_SWITCH_TAB_PREFIX "15", "active-tab", "i", "14"), + ENTRY_MDI (nullptr, KEY_SWITCH_TAB_PREFIX "16", "active-tab", "i", "15"), + ENTRY_MDI (nullptr, KEY_SWITCH_TAB_PREFIX "17", "active-tab", "i", "16"), + ENTRY_MDI (nullptr, KEY_SWITCH_TAB_PREFIX "18", "active-tab", "i", "17"), + ENTRY_MDI (nullptr, KEY_SWITCH_TAB_PREFIX "19", "active-tab", "i", "18"), + ENTRY_MDI (nullptr, KEY_SWITCH_TAB_PREFIX "20", "active-tab", "i", "19"), + ENTRY_MDI (nullptr, KEY_SWITCH_TAB_PREFIX "21", "active-tab", "i", "20"), + ENTRY_MDI (nullptr, KEY_SWITCH_TAB_PREFIX "22", "active-tab", "i", "21"), + ENTRY_MDI (nullptr, KEY_SWITCH_TAB_PREFIX "23", "active-tab", "i", "22"), + ENTRY_MDI (nullptr, KEY_SWITCH_TAB_PREFIX "24", "active-tab", "i", "23"), + ENTRY_MDI (nullptr, KEY_SWITCH_TAB_PREFIX "25", "active-tab", "i", "24"), + ENTRY_MDI (nullptr, KEY_SWITCH_TAB_PREFIX "26", "active-tab", "i", "25"), + ENTRY_MDI (nullptr, KEY_SWITCH_TAB_PREFIX "27", "active-tab", "i", "26"), + ENTRY_MDI (nullptr, KEY_SWITCH_TAB_PREFIX "28", "active-tab", "i", "27"), + ENTRY_MDI (nullptr, KEY_SWITCH_TAB_PREFIX "29", "active-tab", "i", "28"), + ENTRY_MDI (nullptr, KEY_SWITCH_TAB_PREFIX "30", "active-tab", "i", "29"), + ENTRY_MDI (nullptr, KEY_SWITCH_TAB_PREFIX "31", "active-tab", "i", "30"), + ENTRY_MDI (nullptr, KEY_SWITCH_TAB_PREFIX "32", "active-tab", "i", "31"), + ENTRY_MDI (nullptr, KEY_SWITCH_TAB_PREFIX "33", "active-tab", "i", "32"), + ENTRY_MDI (nullptr, KEY_SWITCH_TAB_PREFIX "34", "active-tab", "i", "33"), + ENTRY_MDI (nullptr, KEY_SWITCH_TAB_PREFIX "35", "active-tab", "i", "34"), + ENTRY_MDI (N_("Switch to Last Tab"), KEY_SWITCH_TAB_PREFIX "last", "active-tab", "i", "-1"), +}; + +static KeyEntry help_entries[] = { + ENTRY (N_("Contents"), KEY_HELP, "help", nullptr, nullptr) +}; + +static KeyEntry global_entries[] = { + ENTRY (N_("Show Primary Menu"), KEY_HEADER_MENU, "header-menu", nullptr, nullptr), +}; + +#undef ENTRY_FULL +#undef ENTRY +#undef ENTRY_MDI + +static KeyEntryList all_entries[] = +{ + { file_entries, G_N_ELEMENTS (file_entries), N_("File"), FALSE }, + { edit_entries, G_N_ELEMENTS (edit_entries), N_("Edit"), FALSE }, + { view_entries, G_N_ELEMENTS (view_entries), N_("View"), FALSE }, + { search_entries, G_N_ELEMENTS (search_entries), N_("Search"), FALSE }, + { terminal_entries, G_N_ELEMENTS (terminal_entries), N_("Terminal"), FALSE }, + { tabs_entries, G_N_ELEMENTS (tabs_entries), N_("Tabs"), FALSE }, + { help_entries, G_N_ELEMENTS (help_entries), N_("Help"), FALSE }, + { global_entries, G_N_ELEMENTS (global_entries), N_("Global"), TRUE }, +}; + +enum +{ + ACTION_COLUMN, + KEYVAL_COLUMN, + N_COLUMNS +}; + +static GHashTable *settings_key_to_entry; +static GSettings *keybinding_settings = nullptr; + +GS_DEFINE_CLEANUP_FUNCTION(GtkTreePath*, _terminal_local_free_tree_path, gtk_tree_path_free) +#define terminal_free_tree_path __attribute__((__cleanup__(_terminal_local_free_tree_path))) + +static char* +binding_name (guint keyval, + GdkModifierType mask) +{ + if (keyval != 0) + return gtk_accelerator_name (keyval, mask); + + return g_strdup ("disabled"); +} + +static void +key_changed_cb (GSettings *settings, + const char *settings_key, + gpointer user_data) +{ + GtkApplication *application = (GtkApplication*)user_data; + const gchar *accels[2] = { nullptr, nullptr }; + + _terminal_debug_print (TERMINAL_DEBUG_ACCELS, + "key %s changed\n", + settings_key); + + KeyEntry *key_entry = reinterpret_cast<KeyEntry*> + (g_hash_table_lookup (settings_key_to_entry, settings_key)); + if (!key_entry) + { + /* shouldn't really happen, but let's be safe */ + _terminal_debug_print (TERMINAL_DEBUG_ACCELS, + " WARNING: KeyEntry for changed key not found, bailing out\n"); + return; + } + + gs_free char *value = g_settings_get_string (settings, settings_key); + + gs_free char *detailed = g_action_print_detailed_name (key_entry->action_name, + key_entry->parameter); + gs_unref_variant GVariant *shadow_parameter = g_variant_new_string (detailed); + gs_free char *shadow_detailed = g_action_print_detailed_name (key_entry->shadow_action_name, + shadow_parameter); + + /* We want to always consume the action's accelerators, even if the corresponding + * action is insensitive, so the corresponding shortcut key escape code isn't sent + * to the terminal. See bug #453193, bug #138609, and bug #559728. + * Since GtkApplication's accelerators don't use GtkAccelGroup, we have no way + * to intercept/chain on its activation. The only way to do this that I found + * was to install an extra action with the same accelerator that shadows + * the real action and gets activated when the shadowed action is disabled. + */ + + if (g_str_equal (value, "disabled")) { + accels[0] = nullptr; + } else { + accels[0] = value; + } + + gtk_application_set_accels_for_action (application, + detailed, + accels); + gtk_application_set_accels_for_action (application, + shadow_detailed, + accels); +} + +static void +add_accel_entries (GApplication*application, + KeyEntry *entries, + guint n_entries) +{ + guint j; + + for (j = 0; j < n_entries; ++j) { + KeyEntry *key_entry = &entries[j]; + if (key_entry->action_parameter) { + GError *err = nullptr; + key_entry->parameter = g_variant_parse (key_entry->action_parameter_type, + key_entry->action_parameter, + nullptr, nullptr, &err); + g_assert_no_error (err); + + g_assert (key_entry->parameter != nullptr); + } + + g_hash_table_insert (settings_key_to_entry, + (gpointer) key_entry->settings_key, + key_entry); + + key_changed_cb (keybinding_settings, key_entry->settings_key, application); + } +} + +void +terminal_accels_init (GApplication *application, + GSettings *settings, + gboolean use_headerbar) +{ + guint i, j; + + keybinding_settings = (GSettings*)g_object_ref (settings); + + settings_key_to_entry = g_hash_table_new (g_str_hash, g_str_equal); + + /* Initialise names of tab_switch_entries */ + j = 1; + for (i = 0; i < G_N_ELEMENTS (tabs_entries); i++) + { + gs_free char *name = nullptr; + + if (tabs_entries[i].user_visible_name != nullptr) + continue; + + name = g_strdup_printf (_("Switch to Tab %u"), j++); + tabs_entries[i].user_visible_name = g_intern_string (name); + } + + for (i = 0; i < G_N_ELEMENTS (all_entries); ++i) { + if (!use_headerbar && all_entries[i].headerbar_only) + continue; + + add_accel_entries (application, all_entries[i].key_entry, all_entries[i].n_elements); + } + + g_signal_connect (keybinding_settings, "changed", G_CALLBACK (key_changed_cb), application); +} + +void +terminal_accels_shutdown (void) +{ + guint i, j; + + for (i = 0; i < G_N_ELEMENTS (all_entries); ++i) { + for (j = 0; j < all_entries[i].n_elements; ++j) { + KeyEntry *key_entry; + + key_entry = &(all_entries[i].key_entry[j]); + if (key_entry->parameter) + g_variant_unref (key_entry->parameter); + } + } + + g_signal_handlers_disconnect_by_func (keybinding_settings, + (void*)key_changed_cb, + g_application_get_default ()); + + g_clear_pointer (&settings_key_to_entry, (GDestroyNotify) g_hash_table_unref); + g_clear_object (&keybinding_settings); +} + +static gboolean +foreach_row_cb (GtkTreeModel *model, + GtkTreePath *path, + GtkTreeIter *iter, + gpointer data) +{ + const char *key = (char const*)data; + KeyEntry *key_entry; + + gtk_tree_model_get (model, iter, + KEYVAL_COLUMN, &key_entry, + -1); + + if (key_entry == nullptr || + !g_str_equal (key_entry->settings_key, key)) + return FALSE; + + gtk_tree_model_row_changed (model, path, iter); + return TRUE; +} + +static void +treeview_key_changed_cb (GSettings *settings, + const char *key, + GtkTreeView *tree_view) +{ + gtk_tree_model_foreach (gtk_tree_view_get_model (tree_view), foreach_row_cb, (gpointer) key); +} + +static void +accel_set_func (GtkTreeViewColumn *tree_column, + GtkCellRenderer *cell, + GtkTreeModel *model, + GtkTreeIter *iter, + gpointer data) +{ + KeyEntry *ke; + + gtk_tree_model_get (model, iter, + KEYVAL_COLUMN, &ke, + -1); + + if (ke == nullptr) { + /* This is a title row */ + g_object_set (cell, + "visible", FALSE, + nullptr); + } else { + gs_free char *value; + guint key; + GdkModifierType mods; + gboolean writable; + GtkWidget *button = (GtkWidget*)data; + + value = g_settings_get_string (keybinding_settings, ke->settings_key); + gtk_accelerator_parse (value, &key, &mods); + + writable = g_settings_is_writable (keybinding_settings, ke->settings_key) && + gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button)); + + g_object_set (cell, + "visible", TRUE, + "sensitive", writable, + "editable", writable, + "accel-key", key, + "accel-mods", mods, + nullptr); + } +} + +static void +accel_update (GtkTreeView *view, + GtkCellRendererAccel *cell, + gchar *path_string, + guint keyval, + GdkModifierType mask) +{ + GtkTreeModel *model; + terminal_free_tree_path GtkTreePath *path = nullptr; + GtkTreeIter iter; + KeyEntry *ke; + gs_free char *str = nullptr; + + model = gtk_tree_view_get_model (view); + + path = gtk_tree_path_new_from_string (path_string); + if (!path) + return; + + if (!gtk_tree_model_get_iter (model, &iter, path)) + return; + + gtk_tree_model_get (model, &iter, KEYVAL_COLUMN, &ke, -1); + + /* sanity check */ + if (ke == nullptr) + return; + + str = binding_name (keyval, mask); + g_settings_set_string (keybinding_settings, ke->settings_key, str); +} + +static void +accel_edited_callback (GtkCellRendererAccel *cell, + gchar *path_string, + guint keyval, + GdkModifierType mask, + guint hardware_keycode, + GtkTreeView *view) +{ + accel_update (view, cell, path_string, keyval, mask); +} + +static void +accel_cleared_callback (GtkCellRendererAccel *cell, + gchar *path_string, + GtkTreeView *view) +{ + accel_update (view, cell, path_string, 0, GdkModifierType(0)); +} + +static void +treeview_destroy_cb (GtkWidget *tree_view, + gpointer user_data) +{ + g_signal_handlers_disconnect_by_func (keybinding_settings, + (void*)treeview_key_changed_cb, + tree_view); +} + +#ifdef ENABLE_DEBUG +static void +row_changed (GtkTreeModel *tree_model, + GtkTreePath *path, + GtkTreeIter *iter, + gpointer user_data) +{ + _terminal_debug_print (TERMINAL_DEBUG_ACCELS, + "ROW-CHANGED [%s]\n", gtk_tree_path_to_string (path) /* leak */); +} +#endif + +void +terminal_accels_fill_treeview (GtkWidget *tree_view, + GtkWidget *disable_shortcuts_button) +{ + GtkTreeViewColumn *column; + GtkCellRenderer *cell_renderer; + gs_unref_object GtkTreeStore *tree = nullptr; + guint i; + + /* Column 1 */ + cell_renderer = gtk_cell_renderer_text_new (); + column = gtk_tree_view_column_new_with_attributes (_("_Action"), + cell_renderer, + "text", ACTION_COLUMN, + nullptr); + gtk_tree_view_append_column (GTK_TREE_VIEW (tree_view), column); + + /* Column 2 */ + cell_renderer = gtk_cell_renderer_accel_new (); + g_object_set (cell_renderer, + "editable", TRUE, + "accel-mode", GTK_CELL_RENDERER_ACCEL_MODE_GTK, + nullptr); + + g_signal_connect (cell_renderer, "accel-edited", + G_CALLBACK (accel_edited_callback), tree_view); + g_signal_connect (cell_renderer, "accel-cleared", + G_CALLBACK (accel_cleared_callback), tree_view); + + column = gtk_tree_view_column_new (); + gtk_tree_view_column_set_title (column, _("Shortcut _Key")); + gtk_tree_view_column_pack_start (column, cell_renderer, TRUE); + gtk_tree_view_column_set_cell_data_func (column, cell_renderer, accel_set_func, + disable_shortcuts_button, nullptr); + gtk_tree_view_append_column (GTK_TREE_VIEW (tree_view), column); + + /* Add the data */ + + tree = gtk_tree_store_new (N_COLUMNS, G_TYPE_STRING, G_TYPE_POINTER); + +#ifdef ENABLE_DEBUG + _TERMINAL_DEBUG_IF (TERMINAL_DEBUG_ACCELS) + g_signal_connect (tree, "row-changed", G_CALLBACK (row_changed), nullptr); +#endif + + for (i = 0; i < G_N_ELEMENTS (all_entries); ++i) + { + GtkTreeIter parent_iter; + guint j; + + gtk_tree_store_insert_with_values (tree, &parent_iter, nullptr, -1, + ACTION_COLUMN, _(all_entries[i].user_visible_name), + KEYVAL_COLUMN, nullptr, + -1); + + for (j = 0; j < all_entries[i].n_elements; ++j) + { + KeyEntry *key_entry = &(all_entries[i].key_entry[j]); + GtkTreeIter iter; + + gtk_tree_store_insert_with_values (tree, &iter, &parent_iter, -1, + ACTION_COLUMN, _(key_entry->user_visible_name), + KEYVAL_COLUMN, key_entry, + -1); + } + } + + gtk_tree_view_set_model (GTK_TREE_VIEW (tree_view), GTK_TREE_MODEL (tree)); + + gtk_tree_view_expand_all (GTK_TREE_VIEW (tree_view)); + + g_signal_connect (keybinding_settings, "changed", + G_CALLBACK (treeview_key_changed_cb), tree_view); + g_signal_connect (tree_view, "destroy", + G_CALLBACK (treeview_destroy_cb), tree); +} diff --git a/src/terminal-accels.hh b/src/terminal-accels.hh new file mode 100644 index 0000000..8c860a3 --- /dev/null +++ b/src/terminal-accels.hh @@ -0,0 +1,37 @@ +/* + * Copyright © 2001 Havoc Pennington + * + * 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/>. + */ + +#ifndef TERMINAL_ACCELS_H +#define TERMINAL_ACCELS_H + +#include <gio/gio.h> +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +void terminal_accels_init (GApplication *application, + GSettings *settings, + gboolean use_headerbar); + +void terminal_accels_shutdown (void); + +void terminal_accels_fill_treeview (GtkWidget *treeview, + GtkWidget *disable_shortcuts_button); + +G_END_DECLS + +#endif /* TERMINAL_ACCELS_H */ diff --git a/src/terminal-app.cc b/src/terminal-app.cc new file mode 100644 index 0000000..b6740e3 --- /dev/null +++ b/src/terminal-app.cc @@ -0,0 +1,1671 @@ +/* + * Copyright © 2001, 2002 Havoc Pennington + * Copyright © 2002 Red Hat, Inc. + * Copyright © 2002 Sun Microsystems + * Copyright © 2003 Mariano Suarez-Alvarez + * Copyright © 2008, 2010, 2011, 2015, 2017, 2022 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 <glib.h> +#include <glib/gi18n.h> +#include <gio/gio.h> + +#define G_SETTINGS_ENABLE_BACKEND +#include <gio/gsettingsbackend.h> + +#include "terminal-intl.hh" +#include "terminal-debug.hh" +#include "terminal-app.hh" +#include "terminal-accels.hh" +#include "terminal-client-utils.hh" +#include "terminal-profiles-list.hh" +#include "terminal-util.hh" +#include "terminal-schemas.hh" +#include "terminal-settings-utils.hh" +#include "terminal-defines.hh" +#include "terminal-libgsystem.hh" + +#ifdef TERMINAL_SERVER +#include "terminal-gdbus.hh" +#include "terminal-prefs-process.hh" +#include "terminal-screen-container.hh" +#include "terminal-screen.hh" +#include "terminal-window.hh" +#endif + +#ifdef TERMINAL_PREFERENCES +#include "terminal-prefs.hh" +#endif + +#ifndef TERMINAL_SERVER +#undef ENABLE_SEARCH_PROVIDER +#endif + +#ifdef ENABLE_SEARCH_PROVIDER +#include "terminal-search-provider.hh" +#endif /* ENABLE_SEARCH_PROVIDER */ + +#include <sys/wait.h> +#include <errno.h> +#include <string.h> +#include <stdlib.h> +#include <time.h> + +#ifdef GDK_WINDOWING_X11 +#include <gdk/gdkx.h> +#endif + +#define GNOME_TERMINAL_PREFERENCES_ICON_NAME "org.gnome.Terminal.Preferences" + +#define DESKTOP_INTERFACE_SETTINGS_SCHEMA "org.gnome.desktop.interface" + +#define SYSTEM_PROXY_SETTINGS_SCHEMA "org.gnome.system.proxy" +#define SYSTEM_HTTP_PROXY_SETTINGS_SCHEMA "org.gnome.system.proxy.http" +#define SYSTEM_HTTPS_PROXY_SETTINGS_SCHEMA "org.gnome.system.proxy.https" +#define SYSTEM_FTP_PROXY_SETTINGS_SCHEMA "org.gnome.system.proxy.ftp" +#define SYSTEM_SOCKS_PROXY_SETTINGS_SCHEMA "org.gnome.system.proxy.socks" + +#define GTK_SETTING_PREFER_DARK_THEME "gtk-application-prefer-dark-theme" + +#define GTK_DEBUG_SETTING_SCHEMA "org.gtk.Settings.Debug" + +#ifdef DISUNIFY_NEW_TERMINAL_SECTION +#error Use a gsettings override instead +#endif + +enum { + PROP_SETTINGS_BACKEND = 1, + PROP_IS_DEFAULT_TERMINAL, + PROP_ASK_DEFAULT_TERMINAL, +}; + +/* + * Session state is stored entirely in the RestartCommand command line. + * + * The number one rule: all stored information is EITHER per-session, + * per-profile, or set from a command line option. THERE CAN BE NO + * OVERLAP. The UI and implementation totally break if you overlap + * these categories. See gnome-terminal 1.x for why. + */ + +struct _TerminalAppClass { + GtkApplicationClass parent_class; + + void (* clipboard_targets_changed) (TerminalApp *app, + GtkClipboard *clipboard); +}; + +struct _TerminalApp +{ + GtkApplication parent_instance; + + TerminalSettingsList *profiles_list; + + GSettingsBackend* settings_backend; + GSettingsSchemaSource* schema_source; + GSettings *global_settings; + GSettings *desktop_interface_settings; + GSettings *system_proxy_settings; + GSettings* system_proxy_protocol_settings[4]; + GSettings *gtk_debug_settings; + +#ifdef TERMINAL_SERVER + GDBusObjectManagerServer *object_manager; + GHashTable *screen_map; + +#ifdef ENABLE_SEARCH_PROVIDER + TerminalSearchProvider *search_provider; +#endif /* ENABLE_SEARCH_PROVIDER */ + + GMenuModel *menubar; + GMenu *menubar_new_terminal_section; + GMenu *menubar_set_profile_section; + + GMenuModel *profilemenu; + GMenuModel *headermenu; + GMenu *headermenu_set_profile_section; + + GMenu *set_profile_menu; + + GtkClipboard *clipboard; + GdkAtom *clipboard_targets; + int n_clipboard_targets; + + GWeakRef prefs_process_ref; + +#endif /* TERMINAL_SERVER */ + + gboolean ask_default; + gboolean xte_is_default; + gboolean unified_menu; + gboolean use_headerbar; +}; + +enum +{ + CLIPBOARD_TARGETS_CHANGED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; + +/* Debugging helper */ + +static void +terminal_app_init_debug (void) +{ +#ifdef ENABLE_DEBUG + const char *env = g_getenv ("GTK_TEXT_DIR"); + if (env != nullptr) { + if (g_str_equal (env, "help")) { + g_printerr ("Usage: GTK_TEXT_DIR=ltr|rtl\n"); + } else { + GtkTextDirection dir; + if (g_str_equal (env, "rtl")) + dir = GTK_TEXT_DIR_RTL; + else + dir = GTK_TEXT_DIR_LTR; + + gtk_widget_set_default_direction (dir); + } + } + + env = g_getenv ("GTK_SETTINGS"); + if (env == nullptr) + return; + + GObject *settings = G_OBJECT (gtk_settings_get_default ()); + GObjectClass *settings_class = G_OBJECT_GET_CLASS (settings); + + if (g_str_equal (env, "help")) { + g_printerr ("Usage: GTK_SETTINGS=setting[,setting…] where 'setting' is one of these:\n"); + + guint n_props; + GParamSpec **props = g_object_class_list_properties (settings_class, &n_props); + for (guint i = 0; i < n_props; i++) { + if (G_PARAM_SPEC_VALUE_TYPE (props[i]) != G_TYPE_BOOLEAN) + continue; + + GValue value = { 0, }; + g_value_init (&value, G_TYPE_BOOLEAN); + g_object_get_property (settings, props[i]->name, &value); + g_printerr (" %s (%s)\n", props[i]->name, g_value_get_boolean (&value) ? "true" : "false"); + g_value_unset (&value); + } + g_printerr (" Use 'setting' to set to true, " + "'~setting' to set to false, " + "and '!setting' to invert.\n"); + } else { + gs_strfreev char **tokens = g_strsplit (env, ",", -1); + for (guint i = 0; tokens[i] != nullptr; i++) { + const char *prop = tokens[i]; + char c = prop[0]; + if (c == '~' || c == '!') + prop++; + + GParamSpec *pspec = g_object_class_find_property (settings_class, prop); + if (pspec == nullptr) { + g_printerr ("Setting \"%s\" does not exist.\n", prop); + } else if (G_PARAM_SPEC_VALUE_TYPE (pspec) != G_TYPE_BOOLEAN) { + g_printerr ("Setting \"%s\" is not boolean.\n", prop); + } else { + GValue value = { 0, }; + g_value_init (&value, G_TYPE_BOOLEAN); + if (c == '!') { + g_object_get_property (settings, pspec->name, &value); + g_value_set_boolean (&value, !g_value_get_boolean (&value)); + } else if (c == '~') { + g_value_set_boolean (&value, FALSE); + } else { + g_value_set_boolean (&value, TRUE); + } + g_object_set_property (settings, pspec->name, &value); + g_value_unset (&value); + } + } + } +#endif +} + +/* Helper functions */ + +static gboolean +strv_contains_gnome (char **strv) +{ + if (strv == nullptr) + return FALSE; + + for (int i = 0; strv[i] != nullptr; i++) { + if (g_ascii_strcasecmp (strv[i], "gnome") == 0 || + g_ascii_strcasecmp (strv[i], "gnome-classic") == 0) + return TRUE; + } + + return FALSE; +} + +/* + * terminal_app_should_use_headerbar: + * + * Determines if the app should use headerbars. This is determined + * * If the pref is set, the pref value is used + * * Otherwise, if XDG_CURRENT_DESKTOP contains GNOME or GNOME-Classic, + * headerbar is used + * * Otherwise, headerbar is not used. + */ +static gboolean +terminal_app_should_use_headerbar (TerminalApp *app) +{ + gboolean set, use; + g_settings_get (app->global_settings, TERMINAL_SETTING_HEADERBAR_KEY, "mb", &set, &use); + if (set) + return use; + + gs_strfreev auto desktops = terminal_util_get_desktops(); + return strv_contains_gnome(desktops); +} + +static gboolean +load_css_from_resource (GApplication *application, + GtkCssProvider *provider, + gboolean theme) +{ + const char *base_path; + gs_free char *uri; + gs_unref_object GFile *file; + gs_free_error GError *error = nullptr; + + base_path = g_application_get_resource_base_path (application); + + if (theme) { + gs_free char *str, *theme_name; + + g_object_get (gtk_settings_get_default (), "gtk-theme-name", &str, nullptr); + theme_name = g_ascii_strdown (str, -1); + uri = g_strdup_printf ("resource://%s/css/%s/terminal.css", base_path, theme_name); + } else { + uri = g_strdup_printf ("resource://%s/css/terminal.css", base_path); + } + + file = g_file_new_for_uri (uri); + if (!g_file_query_exists (file, nullptr /* cancellable */)) + return FALSE; + + if (!gtk_css_provider_load_from_file (provider, file, &error)) + g_assert_no_error (error); + + return TRUE; +} + +static void +add_css_provider (GApplication *application, + gboolean theme) +{ + gs_unref_object GtkCssProvider *provider; + + provider = gtk_css_provider_new (); + if (!load_css_from_resource (application, provider, theme)) + return; + + gtk_style_context_add_provider_for_screen (gdk_screen_get_default (), + GTK_STYLE_PROVIDER (provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); +} + +static void +app_load_css (GApplication *application) +{ + add_css_provider (application, FALSE); + add_css_provider (application, TRUE); +} + +char * +terminal_app_new_profile (TerminalApp *app, + GSettings *base_profile, + const char *name) +{ + char *uuid; + + if (base_profile) { + gs_free char *base_uuid; + + base_uuid = terminal_settings_list_dup_uuid_from_child (app->profiles_list, base_profile); + uuid = terminal_settings_list_clone_child (app->profiles_list, base_uuid, name); + } else { + uuid = terminal_settings_list_add_child (app->profiles_list, name); + } + + return uuid; +} + +void +terminal_app_remove_profile (TerminalApp *app, + GSettings *profile) +{ + g_return_if_fail (TERMINAL_IS_APP (app)); + g_return_if_fail (G_IS_SETTINGS (profile)); + + gs_unref_object GSettings *default_profile = terminal_settings_list_ref_default_child (app->profiles_list); + if (default_profile == profile) + return; + +#ifdef TERMINAL_SERVER + /* First, we need to switch any screen using this profile to the default profile */ + gs_free_list GList *screens = g_hash_table_get_values (app->screen_map); + for (GList *l = screens; l != nullptr; l = l->next) { + TerminalScreen *screen = TERMINAL_SCREEN (l->data); + if (terminal_screen_get_profile (screen) != profile) + continue; + + terminal_screen_set_profile (screen, default_profile); + } +#endif /* TERMINAL_SERVER */ + + /* Now we can safely remove the profile */ + gs_free char *uuid = terminal_settings_list_dup_uuid_from_child (app->profiles_list, profile); + terminal_settings_list_remove_child (app->profiles_list, uuid); +} + +static void +terminal_app_theme_variant_changed_cb (GSettings *settings, + const char *key, + GtkSettings *gtk_settings) +{ + TerminalThemeVariant theme; + + theme = TerminalThemeVariant(g_settings_get_enum (settings, key)); + if (theme == TERMINAL_THEME_VARIANT_SYSTEM) + gtk_settings_reset_property (gtk_settings, GTK_SETTING_PREFER_DARK_THEME); + else + g_object_set (gtk_settings, + GTK_SETTING_PREFER_DARK_THEME, + theme == TERMINAL_THEME_VARIANT_DARK, + nullptr); +} + +/* Submenus for New Terminal per profile, and to change profiles */ + +static void +terminal_app_check_default(TerminalApp* app) +{ +#ifdef TERMINAL_SERVER + // Only do this for the default app ID + gs_free char* app_id = nullptr; + g_object_get(app, "application-id", &app_id, nullptr); + if (!_terminal_debug_on(TERMINAL_DEBUG_DEFAULT) && + !g_str_equal(app_id, TERMINAL_APPLICATION_ID)) + return; +#endif /* TERMINAL_SERVER */ + + // Check whether gnome-terminal is the default terminal + // as per XDG-Terminal-Exec. + app->xte_is_default = terminal_util_is_default_terminal(); + + gboolean ask = false; + g_settings_get(app->global_settings, TERMINAL_SETTING_ALWAYS_CHECK_DEFAULT_KEY, "b", &ask); + app->ask_default = (ask != false) && !app->xte_is_default; +} + +#ifdef TERMINAL_SERVER + +static void terminal_app_update_profile_menus (TerminalApp *app); + +typedef struct { + char *uuid; + char *label; +} ProfileData; + +static void +profile_data_clear (ProfileData *data) +{ + g_free (data->uuid); + g_free (data->label); +} + +typedef struct { + GArray *array; + TerminalApp *app; +} ProfilesForeachData; + +static void +foreach_profile_cb (TerminalSettingsList *list, + const char *uuid, + GSettings *profile, + ProfilesForeachData *user_data) +{ + ProfileData data; + data.uuid = g_strdup (uuid); + data.label = g_settings_get_string (profile, TERMINAL_PROFILE_VISIBLE_NAME_KEY); + + g_array_append_val (user_data->array, data); + + /* only connect if we haven't seen this profile before */ + if (g_signal_handler_find (profile, + GSignalMatchType(G_SIGNAL_MATCH_FUNC | G_SIGNAL_MATCH_DATA), + 0, 0, nullptr, + (void*)terminal_app_update_profile_menus, user_data->app) == 0) + g_signal_connect_swapped (profile, "changed::" TERMINAL_PROFILE_VISIBLE_NAME_KEY, + G_CALLBACK (terminal_app_update_profile_menus), user_data->app); +} + +static int +compare_profile_label_cb (gconstpointer ap, + gconstpointer bp) +{ + const ProfileData *a = (ProfileData const*)ap; + const ProfileData *b = (ProfileData const*)bp; + + return g_utf8_collate (a->label, b->label); +} + +static void +menu_append_numbered (GMenu *menu, + const char *label, + int num, + const char *action_name, + GVariant *target /* floating, consumed */) +{ + gs_free_gstring GString *str; + gs_unref_object GMenuItem *item; + const char *p; + + /* Who'd use more that 4 underscores in a profile name... */ + str = g_string_sized_new (strlen (label) + 4 + 1 + 8); + + if (num < 10) + g_string_append_printf (str, "_%Id. ", num); + else if (num < 36) + g_string_append_printf (str, "_%c. ", (char)('A' + num - 10)); + + /* Append the label with underscores elided */ + for (p = label; *p; p++) { + if (*p == '_') + g_string_append (str, "__"); + else + g_string_append_c (str, *p); + } + + item = g_menu_item_new (str->str, nullptr); + g_menu_item_set_action_and_target_value (item, action_name, target); + g_menu_append_item (menu, item); +} + +static void +append_new_terminal_item (GMenu *section, + const char *label, + const char *target, + ProfileData *data, + guint n_profiles) +{ + gs_unref_object GMenuItem *item = g_menu_item_new (label, nullptr); + + if (n_profiles > 1) { + gs_unref_object GMenu *submenu = g_menu_new (); + + for (guint i = 0; i < n_profiles; i++) { + menu_append_numbered (submenu, data[i].label, i + 1, + "win.new-terminal", + g_variant_new ("(ss)", target, data[i].uuid)); + } + + g_menu_item_set_link (item, G_MENU_LINK_SUBMENU, G_MENU_MODEL (submenu)); + } else { + g_menu_item_set_action_and_target (item, "win.new-terminal", + "(ss)", target, "current"); + } + g_menu_append_item (section, item); +} + +static void +fill_header_new_terminal_menu (GMenuModel *menu, + ProfileData *data, + guint n_profiles) +{ + gs_unref_object GMenu *section = nullptr; + + if (n_profiles <= 1) + return; + + section = g_menu_new (); + + for (guint i = 0; i < n_profiles; i++) { + menu_append_numbered (section, data[i].label, i + 1, + "win.new-terminal", + g_variant_new ("(ss)", "default", data[i].uuid)); + } + + g_menu_append_section (G_MENU (menu), _("New Terminal"), G_MENU_MODEL (section)); +} + +static void +fill_new_terminal_section (TerminalApp *app, + GMenu *section, + ProfileData *profiles, + guint n_profiles) +{ + if (terminal_app_get_menu_unified (app)) { + append_new_terminal_item (section, _("New _Terminal"), "default", profiles, n_profiles); + } else { + append_new_terminal_item (section, _("New _Tab"), "tab", profiles, n_profiles); + append_new_terminal_item (section, _("New _Window"), "window", profiles, n_profiles); + } +} + +static GMenu * +set_profile_submenu_new (ProfileData *data, + guint n_profiles) +{ + /* No submenu if there's only one profile */ + if (n_profiles <= 1) + return nullptr; + + GMenu *menu = g_menu_new (); + for (guint i = 0; i < n_profiles; i++) { + menu_append_numbered (menu, data[i].label, i + 1, + "win.profile", + g_variant_new_string (data[i].uuid)); + } + + return menu; +} + +static void +terminal_app_update_profile_menus (TerminalApp *app) +{ + g_clear_object (&app->set_profile_menu); + + /* Get profiles list and sort by label */ + gs_unref_array GArray *array = g_array_sized_new (FALSE, TRUE, sizeof (ProfileData), + terminal_settings_list_get_n_children (app->profiles_list)); + g_array_set_clear_func (array, (GDestroyNotify) profile_data_clear); + + ProfilesForeachData data = { array, app }; + terminal_settings_list_foreach_child (app->profiles_list, + (TerminalSettingsListForeachFunc) foreach_profile_cb, + &data); + g_array_sort (array, compare_profile_label_cb); + + ProfileData *profiles = (ProfileData*) array->data; + guint n_profiles = array->len; + + app->set_profile_menu = set_profile_submenu_new (profiles, n_profiles); + + if (app->menubar != nullptr) { + g_menu_remove_all (G_MENU (app->menubar_new_terminal_section)); + fill_new_terminal_section (app, app->menubar_new_terminal_section, profiles, n_profiles); + + g_menu_remove_all (G_MENU (app->menubar_set_profile_section)); + if (app->set_profile_menu != nullptr) { + g_menu_append_submenu (app->menubar_set_profile_section, _("Change _Profile"), + G_MENU_MODEL (app->set_profile_menu)); + } + } + + if (app->profilemenu != nullptr) { + g_menu_remove_all (G_MENU (app->profilemenu)); + fill_header_new_terminal_menu (app->profilemenu, profiles, n_profiles); + } + + if (app->headermenu != nullptr) { + g_menu_remove_all (G_MENU (app->headermenu_set_profile_section)); + if (app->set_profile_menu != nullptr) { + g_menu_append_submenu (app->headermenu_set_profile_section, _("_Profile"), + G_MENU_MODEL (app->set_profile_menu)); + } + } +} + +static GMenuModel * +terminal_app_create_menubar (TerminalApp *app, + gboolean shell_shows_menubar) +{ + /* If the menubar is shown by the shell, omit mnemonics for the submenus. This is because Alt+F etc. + * are more important to be usable in the terminal, the menu cannot be replaced runtime (to toggle + * between mnemonic and non-mnemonic versions), gtk-enable-mnemonics or gtk_window_set_mnemonic_modifier() + * don't effect the menubar either, so there wouldn't be a way to disable Alt+F for File etc. otherwise. + * Furthermore, the menu would even grab mnemonics from the File and Preferences windows. + * In Unity, Alt+F10 opens the menubar, this should be good enough for keyboard navigation. + * If the menubar is shown by the app, toggling mnemonics is handled in terminal-window.c using + * gtk_window_set_mnemonic_modifier(). + * See bug 792978 for details. */ + terminal_util_load_objects_resource (shell_shows_menubar ? "/org/gnome/terminal/ui/menubar-without-mnemonics.ui" + : "/org/gnome/terminal/ui/menubar-with-mnemonics.ui", + "menubar", &app->menubar, + "new-terminal-section", &app->menubar_new_terminal_section, + "set-profile-section", &app->menubar_set_profile_section, + nullptr); + + /* Install profile sections */ + terminal_app_update_profile_menus (app); + + return app->menubar; +} + +static void +terminal_app_create_headermenu (TerminalApp *app) +{ + terminal_util_load_objects_resource ("/org/gnome/terminal/ui/headerbar-menu.ui", + "headermenu", &app->headermenu, + "set-profile-section", &app->headermenu_set_profile_section, + nullptr); + + /* Install profile sections */ + terminal_app_update_profile_menus (app); +} + +static void +terminal_app_create_profilemenu (TerminalApp *app) +{ + app->profilemenu = G_MENU_MODEL (g_menu_new ()); + + /* Install profile sections */ + terminal_app_update_profile_menus (app); +} + +/* Clipboard */ + +static void +free_clipboard_targets (TerminalApp *app) +{ + g_free (app->clipboard_targets); + app->clipboard_targets = nullptr; + app->n_clipboard_targets = 0; +} + +static void +update_clipboard_targets (TerminalApp *app, + GdkAtom *targets, + int n_targets) +{ + free_clipboard_targets (app); + + /* Sometimes we receive targets == nullptr but n_targets == -1 */ + if (targets != nullptr && n_targets < 255) { + app->clipboard_targets = reinterpret_cast<GdkAtom*> + (g_memdup (targets, sizeof (targets[0]) * n_targets)); + app->n_clipboard_targets = n_targets; + } +} + +static void +clipboard_targets_received_cb (GtkClipboard *clipboard, + GdkAtom *targets, + int n_targets, + TerminalApp *app) +{ + update_clipboard_targets (app, targets, n_targets); + + _TERMINAL_DEBUG_IF (TERMINAL_DEBUG_CLIPBOARD) { + g_printerr ("Clipboard has %d targets:", app->n_clipboard_targets); + + int i; + for (i = 0; i < app->n_clipboard_targets; i++) { + gs_free char *atom_name = gdk_atom_name (app->clipboard_targets[i]); + g_printerr (" %s", atom_name); + } + g_printerr ("\n"); + } + + g_signal_emit (app, signals[CLIPBOARD_TARGETS_CHANGED], 0, clipboard); +} + +static void +clipboard_owner_change_cb (GtkClipboard *clipboard, + GdkEvent *event G_GNUC_UNUSED, + TerminalApp *app) +{ + _terminal_debug_print (TERMINAL_DEBUG_CLIPBOARD, + "Clipboard owner changed\n"); + + clipboard_targets_received_cb (clipboard, nullptr, 0, app); /* clear */ + + /* We can do this without holding a reference to @app since + * the app lives as long as the process. + */ + gtk_clipboard_request_targets (clipboard, + (GtkClipboardTargetsReceivedFunc) clipboard_targets_received_cb, + app); +} + +/* Preferences */ + +struct PrefsLaunchData { + GWeakRef app_ref; + char* profile_uuid; + char* hint; + unsigned timestamp; +}; + +static auto +prefs_launch_data_new(TerminalApp* app, + char const* profile_uuid, + char const* hint, + unsigned timestamp) +{ + auto data = g_new(PrefsLaunchData, 1); + g_weak_ref_init(&data->app_ref, app); + data->profile_uuid = g_strdup(profile_uuid); + data->hint = g_strdup(hint); + data->timestamp = timestamp; + + return data; +} + +static void +prefs_launch_data_free(PrefsLaunchData* data) +{ + g_weak_ref_clear(&data->app_ref); + g_free(data->profile_uuid); + g_free(data->hint); + g_free(data); +} + +static void +launch_prefs_cb(GObject* source, + GAsyncResult* result, + void* user_data) +{ + auto const data = reinterpret_cast<PrefsLaunchData*>(user_data); + auto const app = reinterpret_cast<TerminalApp*>(g_weak_ref_get(&data->app_ref)); + + // @process holds a ref on itself via the g_subprocess_wait_async() call, + // so we only keep a weak ref that gets cleared when the process exits. + gs_free_error GError* error = nullptr; + gs_unref_object auto process = terminal_prefs_process_new_finish(result, &error); + if (app) + g_weak_ref_init(&app->prefs_process_ref, process); + + if (process) { + _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, + "Preferences process launched successfully.\n"); + + terminal_prefs_process_show(process, + data->profile_uuid, + data->hint, + data->timestamp); + } else { + _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, + "Failed to launch preferences process: %s\n", error->message); + } + + prefs_launch_data_free(data); +} + +/* Callbacks from former app menu. + * The preferences one is still used with the "--preferences" cmdline option. */ + +static void +app_menu_preferences_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + TerminalApp *app = (TerminalApp*)user_data; + + terminal_app_edit_preferences (app, nullptr, nullptr, gtk_get_current_event_time()); +} + +static void +app_menu_help_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + terminal_util_show_help (nullptr); +} + +static void +app_menu_about_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + terminal_util_show_about (); +} + +static void +app_menu_quit_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + GtkApplication *application = (GtkApplication*)user_data; + GtkWindow *window; + + window = gtk_application_get_active_window (application); + if (TERMINAL_IS_WINDOW (window)) + terminal_window_request_close (TERMINAL_WINDOW (window)); + else /* a dialogue */ + gtk_widget_destroy (GTK_WIDGET (window)); +} + +#endif /* TERMINAL_SERVER */ + +/* Class implementation */ + +G_DEFINE_TYPE (TerminalApp, terminal_app, GTK_TYPE_APPLICATION) + +/* GApplicationClass impl */ + +static void +terminal_app_activate (GApplication *application) +{ + /* No-op required because GApplication is stupid */ +} + +static void +terminal_app_startup (GApplication *application) +{ + auto const app = TERMINAL_APP(application); + + g_application_set_resource_base_path (application, TERMINAL_RESOURCES_PATH_PREFIX); + + G_APPLICATION_CLASS (terminal_app_parent_class)->startup (application); + + /* Need to set the WM class (bug #685742) */ +#if defined(TERMINAL_SERVER) + gdk_set_program_class("Gnome-terminal"); +#elif defined(TERMINAL_PREFERENCES) + gdk_set_program_class("Gnome-terminal-preferences"); +#else +#error +#endif + + app_load_css (application); + +#ifdef TERMINAL_SERVER + GActionEntry const action_entries[] = { + { "preferences", app_menu_preferences_cb, nullptr, nullptr, nullptr }, + { "help", app_menu_help_cb, nullptr, nullptr, nullptr }, + { "about", app_menu_about_cb, nullptr, nullptr, nullptr }, + { "quit", app_menu_quit_cb, nullptr, nullptr, nullptr } + }; + + g_action_map_add_action_entries (G_ACTION_MAP (application), + action_entries, G_N_ELEMENTS (action_entries), + application); + + /* Figure out whether the shell shows the menubar */ + gboolean shell_shows_menubar; + g_object_get (gtk_settings_get_default (), + "gtk-shell-shows-menubar", &shell_shows_menubar, + nullptr); + + /* Create menubar */ + terminal_app_create_menubar (app, shell_shows_menubar); + + /* Keep dynamic menus updated */ + g_signal_connect_swapped (app->profiles_list, "children-changed", + G_CALLBACK (terminal_app_update_profile_menus), app); + + /* Show/hide the menubar as appropriate: If the shell wants to show the menubar, make it available. */ + if (shell_shows_menubar) + gtk_application_set_menubar (GTK_APPLICATION (app), + terminal_app_get_menubar (app)); + +#endif /* TERMINAL_SERVER */ + + terminal_app_check_default(app); + + _terminal_debug_print (TERMINAL_DEBUG_SERVER, "Startup complete\n"); +} + +/* GObjectClass impl */ + +static void +terminal_app_init (TerminalApp* app) +{ +#ifdef TERMINAL_SERVER + g_weak_ref_init(&app->prefs_process_ref, nullptr); + + app->screen_map = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, nullptr); +#endif +} + +static void +terminal_app_constructed(GObject *object) +{ + auto app = TERMINAL_APP(object); + + G_OBJECT_CLASS(terminal_app_parent_class)->constructed(object); + + terminal_app_init_debug (); + +#if defined(TERMINAL_SERVER) + gtk_window_set_default_icon_name (GNOME_TERMINAL_ICON_NAME); +#elif defined(TERMINAL_PREFERENCES) + gtk_window_set_default_icon_name(GNOME_TERMINAL_PREFERENCES_ICON_NAME); +#else +#error +#endif + + if (app->settings_backend == nullptr) + app->settings_backend = g_settings_backend_get_default (); + + app->schema_source = terminal_g_settings_schema_source_get_default(); + + /* Desktop proxy settings */ + app->system_proxy_settings = terminal_g_settings_new(app->settings_backend, + app->schema_source, + SYSTEM_PROXY_SETTINGS_SCHEMA); + + /* Since there is no way to get the schema ID of a child schema, we cannot + * verify that the installed schemas are correct. Also, due to a glib bug + * (https://gitlab.gnome.org/GNOME/glib/-/issues/1884) g_settings_get_child() + * doesn't work with non-default schema sources. + * So instead of using g_settings_get_child() on the SYSTEM_PROXY_SETTINGS_SCHEMA, + * we construct the child GSettings directly. + */ + app->system_proxy_protocol_settings[TERMINAL_PROXY_HTTP] = + terminal_g_settings_new(app->settings_backend, + app->schema_source, + SYSTEM_HTTP_PROXY_SETTINGS_SCHEMA); + app->system_proxy_protocol_settings[TERMINAL_PROXY_HTTPS] = + terminal_g_settings_new(app->settings_backend, + app->schema_source, + SYSTEM_HTTPS_PROXY_SETTINGS_SCHEMA); + app->system_proxy_protocol_settings[TERMINAL_PROXY_FTP] = + terminal_g_settings_new(app->settings_backend, + app->schema_source, + SYSTEM_FTP_PROXY_SETTINGS_SCHEMA); + app->system_proxy_protocol_settings[TERMINAL_PROXY_SOCKS] = + terminal_g_settings_new(app->settings_backend, + app->schema_source, + SYSTEM_SOCKS_PROXY_SETTINGS_SCHEMA); + + /* Desktop Interface settings */ + app->desktop_interface_settings = terminal_g_settings_new(app->settings_backend, + app->schema_source, + DESKTOP_INTERFACE_SETTINGS_SCHEMA); + + /* Terminal global settings */ + app->global_settings = terminal_g_settings_new(app->settings_backend, + app->schema_source, + TERMINAL_SETTING_SCHEMA); + + /* Gtk debug settings */ + app->gtk_debug_settings = terminal_g_settings_new(app->settings_backend, + app->schema_source, + GTK_DEBUG_SETTING_SCHEMA); + + /* These are internal settings that exists only for distributions + * to override, so we cache them on startup and don't react to changes. + */ + app->unified_menu = g_settings_get_boolean (app->global_settings, TERMINAL_SETTING_UNIFIED_MENU_KEY); + app->use_headerbar = terminal_app_should_use_headerbar (app); + + GtkSettings *gtk_settings = gtk_settings_get_default (); + terminal_app_theme_variant_changed_cb (app->global_settings, + TERMINAL_SETTING_THEME_VARIANT_KEY, gtk_settings); + g_signal_connect (app->global_settings, + "changed::" TERMINAL_SETTING_THEME_VARIANT_KEY, + G_CALLBACK (terminal_app_theme_variant_changed_cb), + gtk_settings); + +#ifdef TERMINAL_SERVER + /* Clipboard targets */ + GdkDisplay *display = gdk_display_get_default (); + app->clipboard = gtk_clipboard_get_for_display (display, GDK_SELECTION_CLIPBOARD); + clipboard_owner_change_cb (app->clipboard, nullptr, app); + g_signal_connect (app->clipboard, "owner-change", + G_CALLBACK (clipboard_owner_change_cb), app); + +#ifdef GDK_WINDOWING_X11 + if (GDK_IS_X11_DISPLAY(display) && + !gdk_display_supports_selection_notification (display)) + g_printerr ("Display does not support owner-change; copy/paste will be broken!\n"); +#endif +#endif /* TERMINAL_SERVER */ + + /* Get the profiles */ + app->profiles_list = terminal_profiles_list_new(app->settings_backend, + app->schema_source); + + gs_unref_object auto settings = + terminal_g_settings_new_with_path(app->settings_backend, + app->schema_source, + TERMINAL_KEYBINDINGS_SCHEMA, + TERMINAL_KEYBINDINGS_SCHEMA_PATH); + terminal_accels_init (G_APPLICATION (app), settings, app->use_headerbar); +} + +static void +terminal_app_finalize (GObject *object) +{ + auto app = TERMINAL_APP(object); + +#ifdef TERMINAL_SERVER + g_signal_handlers_disconnect_by_func (app->clipboard, + (void*)clipboard_owner_change_cb, + app); + free_clipboard_targets (app); + + g_signal_handlers_disconnect_by_func (app->profiles_list, + (void*)terminal_app_update_profile_menus, + app); + g_hash_table_destroy (app->screen_map); +#endif + + g_object_unref (app->global_settings); + g_object_unref (app->desktop_interface_settings); + g_object_unref (app->system_proxy_settings); + for (int i = 0; i < 4; ++i) + g_object_unref(app->system_proxy_protocol_settings[i]); + g_clear_object (&app->gtk_debug_settings); + g_settings_schema_source_unref(app->schema_source); + g_clear_object (&app->settings_backend); + +#ifdef TERMINAL_SERVER + g_clear_object (&app->menubar); + g_clear_object (&app->menubar_new_terminal_section); + g_clear_object (&app->menubar_set_profile_section); + g_clear_object (&app->profilemenu); + g_clear_object (&app->headermenu); + g_clear_object (&app->headermenu_set_profile_section); + g_clear_object (&app->set_profile_menu); + + { + gs_unref_object auto process = reinterpret_cast<TerminalPrefsProcess*>(g_weak_ref_get(&app->prefs_process_ref)); + if (process) + terminal_prefs_process_abort(process); + } + + g_weak_ref_clear(&app->prefs_process_ref); +#endif /* TERMINAL_SERVER */ + + terminal_accels_shutdown (); + + G_OBJECT_CLASS (terminal_app_parent_class)->finalize (object); +} + +static void +terminal_app_get_property(GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec) +{ + auto app = TERMINAL_APP(object); + + switch (prop_id) { + case PROP_SETTINGS_BACKEND: + g_value_set_object(value, app->settings_backend); + break; + case PROP_IS_DEFAULT_TERMINAL: + g_value_set_boolean(value, app->xte_is_default); + break; + case PROP_ASK_DEFAULT_TERMINAL: + g_value_set_boolean(value, app->ask_default); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +terminal_app_set_property(GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec) +{ + auto app = TERMINAL_APP(object); + + switch (prop_id) { + case PROP_SETTINGS_BACKEND: + app->settings_backend = G_SETTINGS_BACKEND(g_value_dup_object(value)); + break; + case PROP_ASK_DEFAULT_TERMINAL: + app->ask_default = g_value_get_boolean(value); + break; + case PROP_IS_DEFAULT_TERMINAL: // not writable + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +#ifdef TERMINAL_SERVER + +static gboolean +terminal_app_dbus_register (GApplication *application, + GDBusConnection *connection, + const gchar *object_path, + GError **error) +{ + TerminalApp *app = TERMINAL_APP (application); + gs_unref_object TerminalObjectSkeleton *object = nullptr; + gs_unref_object TerminalFactory *factory = nullptr; + + if (!G_APPLICATION_CLASS (terminal_app_parent_class)->dbus_register (application, + connection, + object_path, + error)) + return FALSE; + +#ifdef ENABLE_SEARCH_PROVIDER + if (g_settings_get_boolean (app->global_settings, TERMINAL_SETTING_SHELL_INTEGRATION_KEY)) { + gs_unref_object TerminalSearchProvider *search_provider; + + search_provider = terminal_search_provider_new (); + + if (!terminal_search_provider_dbus_register (search_provider, + connection, + TERMINAL_SEARCH_PROVIDER_PATH, + error)) + return FALSE; + + gs_transfer_out_value (&app->search_provider, &search_provider); + } +#endif /* ENABLE_SEARCH_PROVIDER */ + + object = terminal_object_skeleton_new (TERMINAL_FACTORY_OBJECT_PATH); + factory = terminal_factory_impl_new (); + terminal_object_skeleton_set_factory (object, factory); + + app->object_manager = g_dbus_object_manager_server_new (TERMINAL_OBJECT_PATH_PREFIX); + g_dbus_object_manager_server_export (app->object_manager, G_DBUS_OBJECT_SKELETON (object)); + + /* And export the object */ + g_dbus_object_manager_server_set_connection (app->object_manager, connection); + return TRUE; +} + +static void +terminal_app_dbus_unregister (GApplication *application, + GDBusConnection *connection, + const gchar *object_path) +{ + TerminalApp *app = TERMINAL_APP (application); + + if (app->object_manager) { + g_dbus_object_manager_server_unexport (app->object_manager, TERMINAL_FACTORY_OBJECT_PATH); + g_object_unref (app->object_manager); + app->object_manager = nullptr; + } + +#ifdef ENABLE_SEARCH_PROVIDER + if (app->search_provider) { + terminal_search_provider_dbus_unregister (app->search_provider, connection, TERMINAL_SEARCH_PROVIDER_PATH); + g_object_unref (app->search_provider); + app->search_provider = nullptr; + } +#endif /* ENABLE_SEARCH_PROVIDER */ + + G_APPLICATION_CLASS (terminal_app_parent_class)->dbus_unregister (application, + connection, + object_path); +} + +#endif /* TERMINAL_SERVER */ + +static void +terminal_app_class_init (TerminalAppClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GApplicationClass *g_application_class = G_APPLICATION_CLASS (klass); + + object_class->constructed = terminal_app_constructed; + object_class->finalize = terminal_app_finalize; + object_class->get_property = terminal_app_get_property; + object_class->set_property = terminal_app_set_property; + + g_object_class_install_property + (object_class, + PROP_SETTINGS_BACKEND, + g_param_spec_object("settings-backend", nullptr, nullptr, + G_TYPE_SETTINGS_BACKEND, + GParamFlags(G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS))); + + g_object_class_install_property + (object_class, + PROP_IS_DEFAULT_TERMINAL, + g_param_spec_boolean("is-default-terminal", nullptr, nullptr, + false, + GParamFlags(G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS))); + + g_object_class_install_property + (object_class, + PROP_ASK_DEFAULT_TERMINAL, + g_param_spec_boolean("ask-default-terminal", nullptr, nullptr, + false, + GParamFlags(G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS))); + + g_application_class->activate = terminal_app_activate; + g_application_class->startup = terminal_app_startup; +#ifdef TERMINAL_SERVER + g_application_class->dbus_register = terminal_app_dbus_register; + g_application_class->dbus_unregister = terminal_app_dbus_unregister; +#endif + + signals[CLIPBOARD_TARGETS_CHANGED] = + g_signal_new (I_("clipboard-targets-changed"), + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (TerminalAppClass, clipboard_targets_changed), + nullptr, nullptr, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, 1, G_TYPE_OBJECT); +} + +/* Public API */ + +GApplication* +terminal_app_new(char const* app_id, + GApplicationFlags flags, + GSettingsBackend* backend) +{ + return reinterpret_cast<GApplication*> + (g_object_new (TERMINAL_TYPE_APP, + "application-id", app_id ? app_id : TERMINAL_APPLICATION_ID, + "flags", flags, + "settings-backend", backend, + nullptr)); +} + +#ifdef TERMINAL_SERVER + +TerminalScreen * +terminal_app_get_screen_by_uuid (TerminalApp *app, + const char *uuid) +{ + g_return_val_if_fail (TERMINAL_IS_APP (app), nullptr); + + return reinterpret_cast<TerminalScreen*>(g_hash_table_lookup (app->screen_map, uuid)); +} + +char * +terminal_app_dup_screen_object_path (TerminalApp *app, + TerminalScreen *screen) +{ + char *object_path = g_strdup_printf (TERMINAL_RECEIVER_OBJECT_PATH_FORMAT, + terminal_screen_get_uuid (screen)); + object_path = g_strdelimit (object_path, "-", '_'); + g_assert (g_variant_is_object_path (object_path)); + return object_path; +} + +/** + * terminal_app_get_receiver_impl_by_object_path: + * @app: + * @object_path: + * + * Returns: (transfer full): the #TerminalReceiverImpl for @object_path, or %nullptr + */ +static TerminalReceiverImpl * +terminal_app_get_receiver_impl_by_object_path (TerminalApp *app, + const char *object_path) +{ + gs_unref_object GDBusObject *skeleton = + g_dbus_object_manager_get_object (G_DBUS_OBJECT_MANAGER (app->object_manager), + object_path); + if (skeleton == nullptr || !TERMINAL_IS_OBJECT_SKELETON (skeleton)) + return nullptr; + + TerminalReceiverImpl *impl = nullptr; + g_object_get (skeleton, "receiver", &impl, nullptr); + if (impl == nullptr) + return nullptr; + + g_assert (TERMINAL_IS_RECEIVER_IMPL (impl)); + return impl; +} + +/** + * terminal_app_get_screen_by_object_path: + * @app: + * @object_path: + * + * Returns: (transfer full): the #TerminalScreen for @object_path, or %nullptr + */ +TerminalScreen * +terminal_app_get_screen_by_object_path (TerminalApp *app, + const char *object_path) +{ + gs_unref_object TerminalReceiverImpl *impl = + terminal_app_get_receiver_impl_by_object_path (app, object_path); + if (impl == nullptr) + return nullptr; + + return terminal_receiver_impl_get_screen (impl); +} + +void +terminal_app_register_screen (TerminalApp *app, + TerminalScreen *screen) +{ + const char *uuid = terminal_screen_get_uuid (screen); + g_hash_table_insert (app->screen_map, g_strdup (uuid), screen); + + gs_free char *object_path = terminal_app_dup_screen_object_path (app, screen); + TerminalObjectSkeleton *skeleton = terminal_object_skeleton_new (object_path); + + TerminalReceiverImpl *impl = terminal_receiver_impl_new (screen); + terminal_object_skeleton_set_receiver (skeleton, TERMINAL_RECEIVER (impl)); + g_object_unref (impl); + + g_dbus_object_manager_server_export (app->object_manager, + G_DBUS_OBJECT_SKELETON (skeleton)); +} + +void +terminal_app_unregister_screen (TerminalApp *app, + TerminalScreen *screen) +{ + const char *uuid = terminal_screen_get_uuid (screen); + gboolean found = g_hash_table_remove (app->screen_map, uuid); + g_warn_if_fail (found); + if (!found) + return; /* repeat unregistering */ + + gs_free char *object_path = terminal_app_dup_screen_object_path (app, screen); + gs_unref_object TerminalReceiverImpl *impl = + terminal_app_get_receiver_impl_by_object_path (app, object_path); + g_warn_if_fail (impl != nullptr); + + if (impl != nullptr) + terminal_receiver_impl_unset_screen (impl); + + g_dbus_object_manager_server_unexport (app->object_manager, object_path); +} + +GdkAtom * +terminal_app_get_clipboard_targets (TerminalApp *app, + GtkClipboard *clipboard, + int *n_targets) +{ + g_return_val_if_fail (TERMINAL_IS_APP (app), nullptr); + g_return_val_if_fail (n_targets != nullptr, nullptr); + + if (clipboard != app->clipboard) { + *n_targets = 0; + return nullptr; + } + + *n_targets = app->n_clipboard_targets; + return app->clipboard_targets; +} + +#endif /* TERMINAL_SERVER */ + +void +terminal_app_edit_preferences(TerminalApp* app, + GSettings* profile, + char const* hint, + unsigned timestamp) +{ +#ifdef TERMINAL_SERVER + gs_free char* uuid = nullptr; + if (profile) + uuid = terminal_settings_list_dup_uuid_from_child (app->profiles_list, profile); + + gs_unref_object auto process = reinterpret_cast<TerminalPrefsProcess*>(g_weak_ref_get(&app->prefs_process_ref)); + if (process) { + terminal_prefs_process_show(process, + uuid, + hint, + timestamp); + } else { + terminal_prefs_process_new_async(nullptr, // cancellable, + GAsyncReadyCallback(launch_prefs_cb), + prefs_launch_data_new(app, uuid, hint, timestamp)); + } +#endif /* TERMINAL_SERVER */ +#ifdef TERMINAL_PREFERENCES + terminal_prefs_show_preferences(profile, hint, timestamp); +#endif +} + +/** + * terminal_app_get_profiles_list: + * + * Returns: (transfer none): returns the singleton profiles list #TerminalSettingsList + */ +TerminalSettingsList * +terminal_app_get_profiles_list (TerminalApp *app) +{ + return app->profiles_list; +} + +#ifdef TERMINAL_SERVER + +/** + * terminal_app_get_menubar: + * @app: a #TerminalApp + * + * Returns: (tranfer none): the main window menu bar as a #GMenuModel + */ +GMenuModel * +terminal_app_get_menubar (TerminalApp *app) +{ + return app->menubar; +} + +/** + * terminal_app_get_headermenu: + * @app: a #TerminalApp + * + * Returns: (tranfer none): the main window headerbar menu bar as a #GMenuModel + */ +GMenuModel * +terminal_app_get_headermenu (TerminalApp *app) +{ + if (app->headermenu == nullptr) + terminal_app_create_headermenu (app); + + return app->headermenu; +} + +/** + * terminal_app_get_profilemenu: + * @app: a #TerminalApp + * + * Returns: (tranfer none): the main window headerbar profile menu as a #GMenuModel + */ +GMenuModel * +terminal_app_get_profilemenu (TerminalApp *app) +{ + if (app->profilemenu == nullptr) + terminal_app_create_profilemenu (app); + + return app->profilemenu; +} + +/** + * terminal_app_get_profile_section: + * @app: a #TerminalApp + * + * Returns: (tranfer none): the main window's menubar's profiles section as a #GMenuModel + */ +GMenuModel * +terminal_app_get_profile_section (TerminalApp *app) +{ + return G_MENU_MODEL (app->set_profile_menu); +} + +#endif /* TERMINAL_SERVER */ + +/** + * terminal_app_get_settings_backend: + * @app: a #TerminalApp + * + * Returns: (tranfer none): the #GSettingsBackend to use for all #GSettings instances + */ +GSettingsBackend* +terminal_app_get_settings_backend(TerminalApp *app) +{ + return app->settings_backend; +} + +/** + * terminal_app_get_schema_source: + * @app: a #TerminalApp + * + * Returns: (tranfer none): the #GSettingsSchemaSource to use for all #GSettings instances + */ +GSettingsSchemaSource* +terminal_app_get_schema_source(TerminalApp *app) +{ + return app->schema_source; +} + +/** + * terminal_app_get_global_settings: + * @app: a #TerminalApp + * + * Returns: (tranfer none): the cached #GSettings object for the org.gnome.Terminal.Preferences schema + */ +GSettings * +terminal_app_get_global_settings (TerminalApp *app) +{ + return app->global_settings; +} + +/** + * terminal_app_get_desktop_interface_settings: + * @app: a #TerminalApp + * + * Returns: (tranfer none): the cached #GSettings object for the org.gnome.interface schema + */ +GSettings * +terminal_app_get_desktop_interface_settings (TerminalApp *app) +{ + return app->desktop_interface_settings; +} + +/** + * terminal_app_get_proxy_settings: + * @app: a #TerminalApp + * + * Returns: (tranfer none): the cached #GSettings object for the org.gnome.system.proxy schema + */ +GSettings * +terminal_app_get_proxy_settings (TerminalApp *app) +{ + return app->system_proxy_settings; +} + +/** + * terminal_app_get_proxy_settings_for_protocol: + * @app: a #TerminalApp + * @protocol: a #TerminalProxyProtocol + * + * Returns: (tranfer none): the cached #GSettings object for the org.gnome.system.proxy.@protocol schema + */ +GSettings* +terminal_app_get_proxy_settings_for_protocol(TerminalApp *app, + TerminalProxyProtocol protocol) +{ + return app->system_proxy_protocol_settings[(int)protocol]; +} + +GSettings * +terminal_app_get_gtk_debug_settings (TerminalApp *app) +{ + return app->gtk_debug_settings; +} + +/** + * terminal_app_get_system_font: + * @app: + * + * Creates a #PangoFontDescription for the system monospace font. + * + * Returns: (transfer full): a new #PangoFontDescription + */ +PangoFontDescription * +terminal_app_get_system_font (TerminalApp *app) +{ + gs_free char *font = nullptr; + + g_return_val_if_fail (TERMINAL_IS_APP (app), nullptr); + + font = g_settings_get_string (app->desktop_interface_settings, MONOSPACE_FONT_KEY_NAME); + + return pango_font_description_from_string (font); +} + +#ifdef TERMINAL_SERVER + +GDBusObjectManagerServer * +terminal_app_get_object_manager (TerminalApp *app) +{ + g_warn_if_fail (app->object_manager != nullptr); + return app->object_manager; +} + +#endif /* TERMINAL_SERVER */ + +gboolean +terminal_app_get_menu_unified (TerminalApp *app) +{ + g_return_val_if_fail (TERMINAL_IS_APP (app), TRUE); + + return app->unified_menu; +} + +gboolean +terminal_app_get_use_headerbar (TerminalApp *app) +{ + g_return_val_if_fail (TERMINAL_IS_APP (app), FALSE); + + return app->use_headerbar; +} + +gboolean +terminal_app_get_dialog_use_headerbar (TerminalApp *app) +{ + g_return_val_if_fail (TERMINAL_IS_APP (app), FALSE); + + gboolean dialog_use_header; + g_object_get (gtk_settings_get_default (), + "gtk-dialogs-use-header", &dialog_use_header, + nullptr); + + return dialog_use_header && app->use_headerbar; +} + +gboolean +terminal_app_is_default_terminal(TerminalApp* app) +{ + g_return_val_if_fail(TERMINAL_IS_APP(app), false); + return app->xte_is_default; +} + +gboolean +terminal_app_get_ask_default_terminal(TerminalApp* app) +{ + g_return_val_if_fail(TERMINAL_IS_APP(app), false); + return app->ask_default; +} + +void +terminal_app_unset_ask_default_terminal(TerminalApp* app) +{ + g_return_if_fail(TERMINAL_IS_APP(app)); + app->ask_default = false; + g_object_notify(G_OBJECT(app), "ask-default-terminal"); +} + +void +terminal_app_make_default_terminal(TerminalApp* app) +{ + g_return_if_fail(TERMINAL_IS_APP(app)); + terminal_util_make_default_terminal(); + app->xte_is_default = terminal_util_is_default_terminal(); + g_object_notify(G_OBJECT(app), "is-default-terminal"); +} diff --git a/src/terminal-app.hh b/src/terminal-app.hh new file mode 100644 index 0000000..9409864 --- /dev/null +++ b/src/terminal-app.hh @@ -0,0 +1,142 @@ +/* + * Copyright © 2001 Havoc Pennington + * Copyright © 2008 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/>. + */ + +#ifndef TERMINAL_APP_H +#define TERMINAL_APP_H + +#include <gtk/gtk.h> + +#include "terminal-screen.hh" +#include "terminal-profiles-list.hh" + +G_BEGIN_DECLS + +#define GNOME_TERMINAL_ICON_NAME "org.gnome.Terminal" + +#define TERMINAL_RESOURCES_PATH_PREFIX "/org/gnome/terminal" + +#define MONOSPACE_FONT_KEY_NAME "monospace-font-name" + +/* TerminalApp */ + +#define TERMINAL_TYPE_APP (terminal_app_get_type ()) +#define TERMINAL_APP(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), TERMINAL_TYPE_APP, TerminalApp)) +#define TERMINAL_APP_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), TERMINAL_TYPE_APP, TerminalAppClass)) +#define TERMINAL_IS_APP(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), TERMINAL_TYPE_APP)) +#define TERMINAL_IS_APP_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), TERMINAL_TYPE_APP)) +#define TERMINAL_APP_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), TERMINAL_TYPE_APP, TerminalAppClass)) + +typedef struct _TerminalAppClass TerminalAppClass; +typedef struct _TerminalApp TerminalApp; + +GType terminal_app_get_type (void); + +GApplication *terminal_app_new (const char *app_id, + GApplicationFlags flags, + GSettingsBackend* backend); + +#define terminal_app_get (TerminalApp *) g_application_get_default + +GDBusObjectManagerServer *terminal_app_get_object_manager (TerminalApp *app); + +GdkAtom *terminal_app_get_clipboard_targets (TerminalApp *app, + GtkClipboard *clipboard, + int *n_targets); + +void terminal_app_edit_preferences (TerminalApp *app, + GSettings *profile, + const char *widget_name, + unsigned timestamp); + +char *terminal_app_new_profile (TerminalApp *app, + GSettings *default_base_profile, + const char *name); + +void terminal_app_remove_profile (TerminalApp *app, + GSettings *profile); + +char *terminal_app_dup_screen_object_path (TerminalApp *app, + TerminalScreen *screen); + +TerminalScreen *terminal_app_get_screen_by_uuid (TerminalApp *app, + const char *uuid); + +TerminalScreen *terminal_app_get_screen_by_object_path (TerminalApp *app, + const char *object_path); + +void terminal_app_register_screen (TerminalApp *app, + TerminalScreen *screen); + +void terminal_app_unregister_screen (TerminalApp *app, + TerminalScreen *screen); + +TerminalSettingsList *terminal_app_get_profiles_list (TerminalApp *app); + +/* Menus */ + +GMenuModel *terminal_app_get_menubar (TerminalApp *app); + +GMenuModel *terminal_app_get_headermenu (TerminalApp *app); + +GMenuModel *terminal_app_get_profilemenu (TerminalApp *app); + +GMenuModel *terminal_app_get_profile_section (TerminalApp *app); + +gboolean terminal_app_get_menu_unified (TerminalApp *app); + +gboolean terminal_app_get_use_headerbar (TerminalApp *app); + +gboolean terminal_app_get_dialog_use_headerbar (TerminalApp *app); + +/* GSettings */ + +typedef enum { + TERMINAL_PROXY_HTTP = 0, + TERMINAL_PROXY_HTTPS = 1, + TERMINAL_PROXY_FTP = 2, + TERMINAL_PROXY_SOCKS = 3, +} TerminalProxyProtocol; + +GSettingsBackend* terminal_app_get_settings_backend(TerminalApp* app); + +GSettingsSchemaSource* terminal_app_get_schema_source(TerminalApp* app); + +GSettings *terminal_app_get_global_settings (TerminalApp *app); + +GSettings *terminal_app_get_desktop_interface_settings (TerminalApp *app); + +GSettings *terminal_app_get_proxy_settings (TerminalApp *app); + +GSettings *terminal_app_get_proxy_settings_for_protocol(TerminalApp *app, + TerminalProxyProtocol protocol); + +GSettings *terminal_app_get_gtk_debug_settings (TerminalApp *app); + +PangoFontDescription *terminal_app_get_system_font (TerminalApp *app); + +gboolean terminal_app_is_default_terminal(TerminalApp* app); + +gboolean terminal_app_get_ask_default_terminal(TerminalApp* app); + +void terminal_app_unset_ask_default_terminal(TerminalApp* app); + +void terminal_app_make_default_terminal(TerminalApp* app); + +G_END_DECLS + +#endif /* !TERMINAL_APP_H */ diff --git a/src/terminal-client-utils.cc b/src/terminal-client-utils.cc new file mode 100644 index 0000000..cea473e --- /dev/null +++ b/src/terminal-client-utils.cc @@ -0,0 +1,484 @@ +/* + * Copyright © 2001, 2002 Havoc Pennington + * Copyright © 2002 Red Hat, Inc. + * Copyright © 2002 Sun Microsystems + * Copyright © 2003 Mariano Suarez-Alvarez + * Copyright © 2011, 2013 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 <unistd.h> +#include "terminal-client-utils.hh" +#include "terminal-debug.hh" +#include "terminal-defines.hh" +#include "terminal-libgsystem.hh" + +#ifdef TERMINAL_PREFERENCES +#include "terminal-debug.hh" +#endif + +#include <string.h> + +#include <gio/gio.h> + +#ifndef TERMINAL_NAUTILUS +#include <gdk/gdk.h> +#if defined(TERMINAL_COMPILATION) && defined(GDK_WINDOWING_X11) +#include <gdk/gdkx.h> +#endif +#endif + +#ifdef ENABLE_DEBUG + +static char* +get_binary_path_if_uninstalled(char const* install_dir) noexcept +{ +#ifdef __linux__ + char buf[1024]; + auto const r = readlink("/proc/self/exe", buf, sizeof(buf) - 1); + if (r < 0 || r >= ssize_t(sizeof(buf))) + return nullptr; + + buf[r] = '\0'; // nul terminate + + gs_free auto path = g_path_get_dirname(buf); + if (!path) + return nullptr; + + if (g_str_equal(path, install_dir)) + return nullptr; + + return reinterpret_cast<char*>(g_steal_pointer(&path)); +#else + return nullptr; +#endif /* __linux__ */ +} + +static char* +get_path_if_uninstalled(char const* exe_install_dir, + char const* file_name, + GFileTest tests) +{ + gs_free auto path = get_binary_path_if_uninstalled(exe_install_dir); + if (!path) + return nullptr; + + gs_free auto file = g_build_filename(path, file_name, nullptr); + if (!g_file_test(file, GFileTest(tests | G_FILE_TEST_EXISTS))) + return nullptr; + + return reinterpret_cast<char*>(g_steal_pointer(&path)); +} + +#endif /* ENABLE_DEBUG */ + +/** + * terminal_client_find_file_uninstalled: + * @exe_install_dir: the directory where the current exe is installed + * @file_install_dir: the directory where the file to locate is installed + * @file_name: the name of the file to locate + * @tests: extra tests from #GFileTest + * + * Tries to locate the directory that contains @file_name in a build directory, + * and returns the directory. If @file_name is not found, returns the + * installed location for it. + */ +char* +terminal_client_get_directory_uninstalled(char const* exe_install_dir, + char const *file_install_dir, + char const* file_name, + GFileTest tests) +{ +#ifdef ENABLE_DEBUG + auto path = get_path_if_uninstalled(exe_install_dir, file_name, tests); + if (path) + return path; +#endif /* ENABLE_DEBUG */ + + return g_strdup(file_install_dir); +} + +/** + * terminal_client_find_file_uninstalled: + * @exe_install_dir: the directory where the current exe is installed + * @file_install_dir: the directory where the file to locate is installed + * @file_name: the name of the file to locate + * @tests: extra tests from #GFileTest + * + * Tries to locate the file @file_name in a build directory, and + * returns a full path to it. If @file_name is not found, returns the + * installed location for it. + */ +char* +terminal_client_get_file_uninstalled(char const* exe_install_dir, + char const *file_install_dir, + char const* file_name, + GFileTest tests) +{ +#ifdef ENABLE_DEBUG + gs_free auto path = get_path_if_uninstalled(exe_install_dir, file_name, tests); + if (path) + return g_build_filename(path, file_name, nullptr); +#endif /* ENABLE_DEBUG */ + + return g_build_filename(file_install_dir, file_name, nullptr); +} + +/** + * terminal_client_append_create_instance_options: + * @builder: a #GVariantBuilder of #GVariantType "a{sv}" + * @display: (array element-type=guint8): + * @startup_id: + * @activation_token: + * @geometry: + * @role: + * @profile: + * @title: + * @maximise_window: + * @fullscreen_window: + * + * Appends common options to @builder. + */ +void +terminal_client_append_create_instance_options (GVariantBuilder *builder, + const char *display_name, + const char *startup_id, + const char *activation_token, + const char *geometry, + const char *role, + const char *profile, + const char *encoding, + const char *title, + gboolean active, + gboolean maximise_window, + gboolean fullscreen_window) +{ + /* Bytestring options */ + if (display_name != nullptr) + g_variant_builder_add (builder, "{sv}", + "display", g_variant_new_bytestring (display_name)); + if (startup_id) + g_variant_builder_add (builder, "{sv}", + "desktop-startup-id", g_variant_new_bytestring (startup_id)); + if (activation_token) + g_variant_builder_add (builder, "{sv}", + "activation-token", g_variant_new_string (activation_token)); + + /* String options */ + if (profile) + g_variant_builder_add (builder, "{sv}", + "profile", g_variant_new_string (profile)); + if (encoding) + g_variant_builder_add (builder, "{sv}", + "encoding", g_variant_new_string (encoding)); + if (title) + g_variant_builder_add (builder, "{sv}", + "title", g_variant_new_string (title)); + if (geometry) + g_variant_builder_add (builder, "{sv}", + "geometry", g_variant_new_string (geometry)); + if (role) + g_variant_builder_add (builder, "{sv}", + "role", g_variant_new_string (role)); + + /* Boolean options */ + if (active) + g_variant_builder_add (builder, "{sv}", + "active", g_variant_new_boolean (active)); + + if (maximise_window) + g_variant_builder_add (builder, "{sv}", + "maximize-window", g_variant_new_boolean (TRUE)); + if (fullscreen_window) + g_variant_builder_add (builder, "{sv}", + "fullscreen-window", g_variant_new_boolean (TRUE)); +} + +char const* const* +terminal_client_get_environment_filters (void) +{ + static char const* filters[] = { + "COLORFGBG", + "COLORTERM", + "COLUMNS", + "DEFAULT_COLORS", + "DESKTOP_STARTUP_ID", + "EXIT_CODE", + "EXIT_STATUS", + "GIO_LAUNCHED_DESKTOP_FILE", + "GIO_LAUNCHED_DESKTOP_FILE_PID", + "GJS_DEBUG_OUTPUT", + "GJS_DEBUG_TOPICS", + "GNOME_DESKTOP_ICON", + "INVOCATION_ID", + "JOURNAL_STREAM", + "LINES", + "LISTEN_FDNAMES", + "LISTEN_FDS", + "LISTEN_PID", + "MAINPID", + "MANAGERPID", + "NOTIFY_SOCKET", + "NOTIFY_SOCKET", + "PIDFILE", + "PWD", + "REMOTE_ADDR", + "REMOTE_PORT", + "SERVICE_RESULT", + "SHLVL", + "STY", + "TERM", + "TERMCAP", + "TMUX", + "TMUX_PANE", + "VTE_VERSION", + "WATCHDOG_PID", + "WATCHDOG_USEC", + "WCWIDTH_CJK_LEGACY", + "WINDOWID", + "XDG_ACTIVATION_TOKEN", + nullptr + }; + + return filters; +} + +char const* const* +terminal_client_get_environment_prefix_filters (void) +{ + static char const* filters[] = { + "GNOME_TERMINAL_", + // "VTE_", ? + + /* other terminals */ + "FOOT_", + "ITERM2_", + "MC_", + "MINTTY_", + "PUTTY_", + "RXVT_", + "TERM_", + "URXVT_", + "WEZTERM_", + "XTERM_", + nullptr + }; + + return filters; +} + +static char const* const* +terminal_client_get_environment_prefix_filters_excludes(void) +{ + static char const* filters[] = { + "MC_XDG_OPEN", + + nullptr, + }; + + return filters; +} + +bool +terminal_client_get_environment_prefix_filters_is_excluded(char const* env) +{ + auto const excludes = terminal_client_get_environment_prefix_filters_excludes(); + for (auto j = 0; excludes[j]; ++j) { + if (g_str_equal(excludes[j], env)) + return true; + } + + return false; +} + +static char** +terminal_environ_unsetenv_prefix (char** envv, + char const* prefix) +{ + if (!envv) + return envv; + + for (auto i = 0; envv[i]; ++i) { + if (!g_str_has_prefix (envv[i], prefix)) + continue; + + auto const equal = strchr(envv[i], '='); + g_assert(equal != nullptr); + gs_free char* env = g_strndup(envv[i], equal - envv[i]); + + if (terminal_client_get_environment_prefix_filters_is_excluded(env)) + continue; + + envv = g_environ_unsetenv (envv, env); + } + + return envv; +} + +/** + * terminal_client_filter_environment: + * @envv: (transfer full): the environment + * + * Filters unwanted variables from @envv, and returns it. + * + * Returns: (transfer full): the filtered environment + */ +char** +terminal_client_filter_environment (char** envv) +{ + if (envv == nullptr) + return nullptr; + + auto filters = terminal_client_get_environment_filters (); + for (auto i = 0; filters[i]; ++i) + envv = g_environ_unsetenv (envv, filters[i]); + + filters = terminal_client_get_environment_prefix_filters (); + for (auto i = 0; filters[i]; ++i) + envv = terminal_environ_unsetenv_prefix (envv, filters[i]); + + return envv; +} + +/** + * terminal_client_append_exec_options: + * @builder: a #GVariantBuilder of #GVariantType "a{sv}" + * @pass_environment: whether to pass the current environment + * @working_directory: (allow-none): the cwd, or %nullptr + * @fd_array: (array lenght=fd_array_len): + * @fd_array_len: + * @shell: + * + * Appends the environment and the working directory to @builder. + */ +void +terminal_client_append_exec_options (GVariantBuilder *builder, + gboolean pass_environment, + const char *working_directory, + PassFdElement *fd_array, + gsize fd_array_len, + gboolean shell) +{ + if (pass_environment) { + gs_strfreev char **envv; + + envv = g_get_environ (); + envv = terminal_client_filter_environment (envv); + + envv = g_environ_unsetenv (envv, TERMINAL_ENV_SERVICE_NAME); + envv = g_environ_unsetenv (envv, TERMINAL_ENV_SCREEN); + + g_variant_builder_add (builder, "{sv}", + "environ", + g_variant_new_bytestring_array ((const char * const *) envv, -1)); + } + + if (working_directory) + g_variant_builder_add (builder, "{sv}", + "cwd", g_variant_new_bytestring (working_directory)); + + if (shell) + g_variant_builder_add (builder, "{sv}", + "shell", + g_variant_new_boolean (TRUE)); + + if (fd_array_len) { + gsize i; + + g_variant_builder_open (builder, G_VARIANT_TYPE ("{sv}")); + g_variant_builder_add (builder, "s", "fd-set"); + + g_variant_builder_open (builder, G_VARIANT_TYPE ("v")); + g_variant_builder_open (builder, G_VARIANT_TYPE ("a(ih)")); + for (i = 0; i < fd_array_len; i++) { + g_variant_builder_add (builder, "(ih)", fd_array[i].fd, fd_array[i].index); + } + g_variant_builder_close (builder); /* a(ih) */ + g_variant_builder_close (builder); /* v */ + + g_variant_builder_close (builder); /* {sv} */ + } +} + +#ifndef TERMINAL_NAUTILUS + +/** + * terminal_client_get_fallback_startup_id: + * + * Returns: a fallback startup ID, or %nullptr + */ +char * +terminal_client_get_fallback_startup_id (void) +{ +#if defined(TERMINAL_COMPILATION) && defined(GDK_WINDOWING_X11) + GdkDisplay *display; + Display *xdisplay; + Window xwindow; + XEvent event; + + display = gdk_display_get_default (); + if (display == nullptr || !GDK_IS_X11_DISPLAY (display)) + goto out; + + xdisplay = GDK_DISPLAY_XDISPLAY (display); + + { + XSetWindowAttributes attrs; + Atom atom_name; + Atom atom_type; + const char *name; + + attrs.override_redirect = True; + attrs.event_mask = PropertyChangeMask | StructureNotifyMask; + + xwindow = + XCreateWindow (xdisplay, + RootWindow (xdisplay, 0), + -100, -100, 1, 1, + 0, + CopyFromParent, + CopyFromParent, + (Visual *)CopyFromParent, + CWOverrideRedirect | CWEventMask, + &attrs); + + atom_name = XInternAtom (xdisplay, "WM_NAME", TRUE); + g_assert (atom_name != None); + atom_type = XInternAtom (xdisplay, "STRING", TRUE); + g_assert (atom_type != None); + + name = "Fake Window"; + XChangeProperty (xdisplay, + xwindow, atom_name, + atom_type, + 8, PropModeReplace, (unsigned char *)name, strlen (name)); + } + + XWindowEvent (xdisplay, + xwindow, + PropertyChangeMask, + &event); + + XDestroyWindow(xdisplay, xwindow); + + return g_strdup_printf ("_TIME%lu", event.xproperty.time); +out: +#endif + return nullptr; +} + +#endif /* !TERMINAL_NAUTILUS */ diff --git a/src/terminal-client-utils.hh b/src/terminal-client-utils.hh new file mode 100644 index 0000000..4771acf --- /dev/null +++ b/src/terminal-client-utils.hh @@ -0,0 +1,73 @@ +/* + * Copyright © 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/>. + */ + +#ifndef TERMINAL_CLIENT_UTILS_H +#define TERMINAL_CLIENT_UTILS_H + +#include <gio/gio.h> +#include <gio/gunixfdlist.h> + +G_BEGIN_DECLS + +char* terminal_client_get_directory_uninstalled(char const* exe_install_dir, + char const *file_install_dir, + char const* file_name, + GFileTest tests); + +char* terminal_client_get_file_uninstalled(char const* exe_install_dir, + char const *file_install_dir, + char const* file_name, + GFileTest tests); + +void terminal_client_append_create_instance_options (GVariantBuilder *builder, + const char *display_name, + const char *startup_id, + const char *activation_token, + const char *geometry, + const char *role, + const char *profile, + const char *encoding, + const char *title, + gboolean active, + gboolean maximise_window, + gboolean fullscreen_window); + +typedef struct { + int index; + int fd; +} PassFdElement; + +void terminal_client_append_exec_options (GVariantBuilder *builder, + gboolean pass_environment, + const char *working_directory, + PassFdElement *fd_array, + gsize fd_array_len, + gboolean shell); + +char * terminal_client_get_fallback_startup_id (void) G_GNUC_MALLOC; + +char const* const* terminal_client_get_environment_filters (void); + +char const* const* terminal_client_get_environment_prefix_filters (void); + +bool terminal_client_get_environment_prefix_filters_is_excluded(char const* env); + +char** terminal_client_filter_environment (char** envv) G_GNUC_MALLOC; + +G_END_DECLS + +#endif /* TERMINAL_UTIL_UTILS_H */ diff --git a/src/terminal-debug.cc b/src/terminal-debug.cc new file mode 100644 index 0000000..bf2db7e --- /dev/null +++ b/src/terminal-debug.cc @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2002,2003 Red Hat, Inc. + * + * 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 <glib.h> + +#include "terminal-debug.hh" + +TerminalDebugFlags _terminal_debug_flags; + +void +_terminal_debug_init(void) +{ +#ifdef ENABLE_DEBUG + const GDebugKey keys[] = { + { "accels", TERMINAL_DEBUG_ACCELS }, + { "clipboard", TERMINAL_DEBUG_CLIPBOARD }, + { "encodings", TERMINAL_DEBUG_ENCODINGS }, + { "server", TERMINAL_DEBUG_SERVER }, + { "geometry", TERMINAL_DEBUG_GEOMETRY }, + { "mdi", TERMINAL_DEBUG_MDI }, + { "processes", TERMINAL_DEBUG_PROCESSES }, + { "profile", TERMINAL_DEBUG_PROFILE }, + { "settings-list", TERMINAL_DEBUG_SETTINGS_LIST }, + { "search", TERMINAL_DEBUG_SEARCH }, + { "bridge", TERMINAL_DEBUG_BRIDGE }, + { "default", TERMINAL_DEBUG_DEFAULT }, + }; + + _terminal_debug_flags = TerminalDebugFlags(g_parse_debug_string (g_getenv ("GNOME_TERMINAL_DEBUG"), + keys, G_N_ELEMENTS (keys))); + +#endif /* ENABLE_DEBUG */ +} + diff --git a/src/terminal-debug.hh b/src/terminal-debug.hh new file mode 100644 index 0000000..fedc9e4 --- /dev/null +++ b/src/terminal-debug.hh @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2002 Red Hat, Inc. + * + * 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/>. + */ + +/* The interfaces in this file are subject to change at any time. */ + +#ifndef ENABLE_DEBUG_H +#define ENABLE_DEBUG_H + +#include <glib.h> + +G_BEGIN_DECLS + +typedef enum { + TERMINAL_DEBUG_ACCELS = 1 << 0, + TERMINAL_DEBUG_CLIPBOARD = 1 << 1, + TERMINAL_DEBUG_ENCODINGS = 1 << 2, + TERMINAL_DEBUG_SERVER = 1 << 3, + TERMINAL_DEBUG_GEOMETRY = 1 << 4, + TERMINAL_DEBUG_MDI = 1 << 5, + TERMINAL_DEBUG_PROCESSES = 1 << 6, + TERMINAL_DEBUG_PROFILE = 1 << 7, + TERMINAL_DEBUG_SETTINGS_LIST = 1 << 8, + TERMINAL_DEBUG_SEARCH = 1 << 9, + TERMINAL_DEBUG_BRIDGE = 1 << 10, + TERMINAL_DEBUG_DEFAULT = 1 << 11, +} TerminalDebugFlags; + +void _terminal_debug_init(void); + +extern TerminalDebugFlags _terminal_debug_flags; +static inline gboolean _terminal_debug_on (TerminalDebugFlags flags) G_GNUC_CONST G_GNUC_UNUSED; + +static inline gboolean +_terminal_debug_on (TerminalDebugFlags flags) +{ + return (_terminal_debug_flags & flags) == flags; +} + +#ifdef ENABLE_DEBUG +#define _TERMINAL_DEBUG_IF(flags) if (G_UNLIKELY (_terminal_debug_on (flags))) +#else +#define _TERMINAL_DEBUG_IF(flags) if (0) +#endif + +#if defined(__GNUC__) && G_HAVE_GNUC_VARARGS +#define _terminal_debug_print(flags, fmt, ...) \ + G_STMT_START { _TERMINAL_DEBUG_IF(flags) g_printerr(fmt, ##__VA_ARGS__); } G_STMT_END +#else +#include <stdarg.h> +#include <glib/gstdio.h> +static void _terminal_debug_print (guint flags, const char *fmt, ...) +{ + if (_terminal_debug_on (flags)) { + va_list ap; + va_start (ap, fmt); + g_vfprintf (stderr, fmt, ap); + va_end (ap); + } +} +#endif + +G_END_DECLS + +#endif /* !ENABLE_DEBUG_H */ diff --git a/src/terminal-defines.hh b/src/terminal-defines.hh new file mode 100644 index 0000000..cd85d60 --- /dev/null +++ b/src/terminal-defines.hh @@ -0,0 +1,56 @@ +/* + * Copyright © 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/>. + */ + +#ifndef TERMINAL_DEFINES_H +#define TERMINAL_DEFINES_H + +G_BEGIN_DECLS + +enum { + _EXIT_FAILURE_WRONG_ID = 7, + _EXIT_FAILURE_NO_UTF8 = 8, + _EXIT_FAILURE_UNSUPPORTED_LOCALE = 9, + _EXIT_FAILURE_GTK_INIT = 10 +}; + +#define TERMINAL_APPLICATION_ID "org.gnome.Terminal" + +#define TERMINAL_OBJECT_PATH_PREFIX "/org/gnome/Terminal" +#define TERMINAL_OBJECT_INTERFACE_PREFIX "org.gnome.Terminal" + +#define TERMINAL_FACTORY_OBJECT_PATH TERMINAL_OBJECT_PATH_PREFIX "/Factory0" +#define TERMINAL_FACTORY_INTERFACE_NAME TERMINAL_OBJECT_INTERFACE_PREFIX ".Factory0" + +#define TERMINAL_RECEIVER_OBJECT_PATH_FORMAT TERMINAL_OBJECT_PATH_PREFIX "/screen/%s" +#define TEMRINAL_RECEIVER_INTERFACE_NAME TERMINAL_OBJECT_INTERFACE_PREFIX ".Terminal0" + +#define TERMINAL_SEARCH_PROVIDER_PATH TERMINAL_OBJECT_PATH_PREFIX "/SearchProvider" + +#define TERMINAL_SETTINGS_BRIDGE_INTERFACE_NAME "org.gnome.Terminal.SettingsBridge0" +#define TERMINAL_SETTINGS_BRIDGE_OBJECT_PATH TERMINAL_OBJECT_PATH_PREFIX "/SettingsBridge" + +#define TERMINAL_PREFERENCES_APPLICATION_ID TERMINAL_APPLICATION_ID ".Preferences" +#define TERMINAL_PREFERENCES_OBJECT_PATH TERMINAL_OBJECT_PATH_PREFIX "/Preferences" + +#define TERMINAL_ENV_SERVICE_NAME "GNOME_TERMINAL_SERVICE" +#define TERMINAL_ENV_SCREEN "GNOME_TERMINAL_SCREEN" + +#define TERMINAL_PREFERENCES_BINARY_NAME "gnome-terminal-preferences" + +G_END_DECLS + +#endif /* !TERMINAL_DEFINES_H */ diff --git a/src/terminal-enums.hh b/src/terminal-enums.hh new file mode 100644 index 0000000..93f1460 --- /dev/null +++ b/src/terminal-enums.hh @@ -0,0 +1,69 @@ +/* + * Copyright © 2001 Havoc Pennington + * Copyright © 2002 Mathias Hasselmann + * Copyright © 2008, 2010 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/>. + */ + +#ifndef TERMINAL_ENUMS_H +#define TERMINAL_ENUMS_H + +#include <glib.h> + +G_BEGIN_DECLS + +typedef enum { + TERMINAL_NEW_TERMINAL_MODE_WINDOW, + TERMINAL_NEW_TERMINAL_MODE_TAB +} TerminalNewTerminalMode; + +typedef enum { + TERMINAL_NEW_TAB_POSITION_LAST, + TERMINAL_NEW_TAB_POSITION_NEXT +} TerminalNewTabPosition; + +typedef enum +{ + TERMINAL_EXIT_CLOSE, + TERMINAL_EXIT_RESTART, + TERMINAL_EXIT_HOLD +} TerminalExitAction; + +typedef enum { + TERMINAL_SETTINGS_LIST_FLAG_NONE = 0, + TERMINAL_SETTINGS_LIST_FLAG_HAS_DEFAULT = 1 << 0, + TERMINAL_SETTINGS_LIST_FLAG_ALLOW_EMPTY = 1 << 1 +} TerminalSettingsListFlags; + +typedef enum { + TERMINAL_CJK_WIDTH_NARROW = 1, + TERMINAL_CJK_WIDTH_WIDE = 2 +} TerminalCJKWidth; + +typedef enum { + TERMINAL_THEME_VARIANT_SYSTEM = 0, + TERMINAL_THEME_VARIANT_LIGHT = 1, + TERMINAL_THEME_VARIANT_DARK = 2 +} TerminalThemeVariant; + +typedef enum { + TERMINAL_PRESERVE_WORKING_DIRECTORY_NEVER = 0, + TERMINAL_PRESERVE_WORKING_DIRECTORY_SAFE = 1, + TERMINAL_PRESERVE_WORKING_DIRECTORY_ALWAYS = 2, +} TerminalPreserveWorkingDirectory; + +G_END_DECLS + +#endif /* TERMINAL_ENUMS_H */ diff --git a/src/terminal-gdbus.cc b/src/terminal-gdbus.cc new file mode 100644 index 0000000..55f3391 --- /dev/null +++ b/src/terminal-gdbus.cc @@ -0,0 +1,573 @@ +/* + * Copyright © 2011, 2012 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 "terminal-gdbus.hh" + +#include <gio/gio.h> +#include <gio/gunixfdlist.h> + +#include "terminal-app.hh" +#include "terminal-debug.hh" +#include "terminal-defines.hh" +#include "terminal-mdi-container.hh" +#include "terminal-util.hh" +#include "terminal-window.hh" +#include "terminal-libgsystem.hh" + +/* ------------------------------------------------------------------------- */ + +#define TERMINAL_RECEIVER_IMPL_GET_PRIVATE(impl)(G_TYPE_INSTANCE_GET_PRIVATE ((impl), TERMINAL_TYPE_RECEIVER_IMPL, TerminalReceiverImplPrivate)) + +struct _TerminalReceiverImplPrivate { + TerminalScreen *screen; /* unowned! */ +}; + +enum { + PROP_0, + PROP_SCREEN +}; + +/* helper functions */ + +static void +child_exited_cb (VteTerminal *terminal, + int exit_code, + TerminalReceiver *receiver) +{ + terminal_receiver_emit_child_exited (receiver, exit_code); +} + +static void +terminal_receiver_impl_set_screen (TerminalReceiverImpl *impl, + TerminalScreen *screen) +{ + TerminalReceiverImplPrivate *priv; + + g_return_if_fail (TERMINAL_IS_RECEIVER_IMPL (impl)); + g_return_if_fail (screen == nullptr || TERMINAL_IS_SCREEN (screen)); + + priv = impl->priv; + if (priv->screen == screen) + return; + + if (priv->screen) { + g_signal_handlers_disconnect_matched (priv->screen, + G_SIGNAL_MATCH_DATA, + 0, 0, nullptr, nullptr, impl); + } + + priv->screen = screen; + if (screen) { + g_signal_connect (screen, "child-exited", + G_CALLBACK (child_exited_cb), + impl); + } + + g_object_notify (G_OBJECT (impl), "screen"); +} + +/* Class implementation */ + +namespace { + +typedef struct { + TerminalReceiver *receiver; + GDBusMethodInvocation *invocation; +} ExecData; + +} // anon namespace + +static void +exec_data_free (ExecData *data) +{ + g_object_unref (data->receiver); + g_object_unref (data->invocation); + g_free (data); +} + +static void +exec_cb (TerminalScreen *screen, /* unused, may be %nullptr */ + GError *error, /* set on error, %nullptr on success */ + ExecData *data) +{ + /* Note: these calls transfer the ref */ + g_object_ref (data->invocation); + if (error) { + g_dbus_method_invocation_return_gerror (data->invocation, error); + } else { + terminal_receiver_complete_exec (data->receiver, data->invocation, nullptr /* outfdlist */); + } +} + +static gboolean +terminal_receiver_impl_exec (TerminalReceiver *receiver, + GDBusMethodInvocation *invocation, + GUnixFDList *fd_list, + GVariant *options, + GVariant *arguments) +{ + TerminalReceiverImpl *impl = TERMINAL_RECEIVER_IMPL (receiver); + TerminalReceiverImplPrivate *priv = impl->priv; + const char *working_directory; + gboolean shell; + gsize exec_argc; + gs_free char **exec_argv = nullptr; /* container needs to be freed, strings not owned */ + gs_free char **envv = nullptr; /* container needs to be freed, strings not owned */ + gs_unref_variant GVariant *fd_array = nullptr; + + if (priv->screen == nullptr) { + g_dbus_method_invocation_return_error_literal (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Terminal already closed"); + return TRUE; /* handled */ + } + + if (!g_variant_lookup (options, "cwd", "^&ay", &working_directory)) + working_directory = nullptr; + if (!g_variant_lookup (options, "shell", "b", &shell)) + shell = FALSE; + if (!g_variant_lookup (options, "environ", "^a&ay", &envv)) + envv = nullptr; + if (!g_variant_lookup (options, "fd-set", "@a(ih)", &fd_array)) + fd_array = nullptr; + + /* Check environment */ + if (!terminal_util_check_envv((const char * const*)envv)) { + g_dbus_method_invocation_return_error_literal (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_INVALID_ARGS, + "Malformed environment"); + return TRUE; /* handled */ + } + + /* Check FD passing */ + if ((fd_list != nullptr) ^ (fd_array != nullptr)) { + g_dbus_method_invocation_return_error_literal (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_INVALID_ARGS, + "Must pass both fd-set options and a FD list"); + return TRUE; /* handled */ + } + if (fd_list != nullptr && fd_array != nullptr) { + const int *fd_array_data; + gsize fd_array_data_len, i; + int n_fds; + + fd_array_data = reinterpret_cast<int const*> + (g_variant_get_fixed_array (fd_array, &fd_array_data_len, 2 * sizeof (int))); + n_fds = g_unix_fd_list_get_length (fd_list); + for (i = 0; i < fd_array_data_len; i++) { + const int fd = fd_array_data[2 * i]; + const int idx = fd_array_data[2 * i + 1]; + + if (fd == -1) { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_INVALID_ARGS, + "Passing of invalid FD %d not supported", fd); + return TRUE; /* handled */ + } + if (fd == STDIN_FILENO || + fd == STDOUT_FILENO || + fd == STDERR_FILENO) { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_INVALID_ARGS, + "Passing of std%s not supported", + fd == STDIN_FILENO ? "in" : fd == STDOUT_FILENO ? "out" : "err"); + return TRUE; /* handled */ + } + if (idx < 0 || idx >= n_fds) { + g_dbus_method_invocation_return_error_literal (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_INVALID_ARGS, + "Handle out of range"); + return TRUE; /* handled */ + } + } + } + + if (working_directory != nullptr) + _terminal_debug_print (TERMINAL_DEBUG_SERVER, + "CWD is '%s'\n", working_directory); + + exec_argv = (char **) g_variant_get_bytestring_array (arguments, &exec_argc); + + ExecData *exec_data = g_new (ExecData, 1); + exec_data->receiver = (TerminalReceiver*)g_object_ref (receiver); + /* We want to transfer the ownership of @invocation to ExecData here, but + * we have to temporarily ref it so that in the error case below (where + * terminal_screen_exec() frees the exec data via the supplied callback, + * the g_dbus_method_invocation_take_error() calll still can take ownership + * of the invocation's ref passed to this function (terminal_receiver_impl_exec()). + */ + exec_data->invocation = (GDBusMethodInvocation*)g_object_ref (invocation); + + GError *err = nullptr; + if (!terminal_screen_exec (priv->screen, + exec_argc > 0 ? exec_argv : nullptr, + envv, + shell, + working_directory, + fd_list, fd_array, + (TerminalScreenExecCallback) exec_cb, + exec_data /* adopted */, + (GDestroyNotify) exec_data_free, + nullptr /* cancellable */, + &err)) { + /* Transfers ownership of @invocation */ + g_dbus_method_invocation_take_error (invocation, err); + } + + /* Now we can remove that extra ref again. */ + g_object_unref (invocation); + + return TRUE; /* handled */ +} + +static void +terminal_receiver_impl_iface_init (TerminalReceiverIface *iface) +{ + iface->handle_exec = terminal_receiver_impl_exec; +} + +G_DEFINE_TYPE_WITH_CODE (TerminalReceiverImpl, terminal_receiver_impl, TERMINAL_TYPE_RECEIVER_SKELETON, + G_IMPLEMENT_INTERFACE (TERMINAL_TYPE_RECEIVER, terminal_receiver_impl_iface_init)) + +static void +terminal_receiver_impl_init (TerminalReceiverImpl *impl) +{ + impl->priv = TERMINAL_RECEIVER_IMPL_GET_PRIVATE (impl); +} + +static void +terminal_receiver_impl_dispose (GObject *object) +{ + TerminalReceiverImpl *impl = TERMINAL_RECEIVER_IMPL (object); + + terminal_receiver_impl_set_screen (impl, nullptr); + + G_OBJECT_CLASS (terminal_receiver_impl_parent_class)->dispose (object); +} + +static void +terminal_receiver_impl_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + TerminalReceiverImpl *impl = TERMINAL_RECEIVER_IMPL (object); + + switch (prop_id) { + case PROP_SCREEN: + g_value_set_object (value, terminal_receiver_impl_get_screen (impl)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +terminal_receiver_impl_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + TerminalReceiverImpl *impl = TERMINAL_RECEIVER_IMPL (object); + + switch (prop_id) { + case PROP_SCREEN: + terminal_receiver_impl_set_screen (impl, (TerminalScreen*)g_value_get_object (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +terminal_receiver_impl_class_init (TerminalReceiverImplClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->dispose = terminal_receiver_impl_dispose; + gobject_class->get_property = terminal_receiver_impl_get_property; + gobject_class->set_property = terminal_receiver_impl_set_property; + + g_object_class_install_property + (gobject_class, + PROP_SCREEN, + g_param_spec_object ("screen", nullptr, nullptr, + TERMINAL_TYPE_SCREEN, + GParamFlags(G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS))); + + g_type_class_add_private (gobject_class, sizeof (TerminalReceiverImplPrivate)); +} + +/* public API */ + +/** + * terminal_receiver_impl_new: + * @screen: a #TerminalScreen + * + * Returns: a new #TerminalReceiverImpl for @screen + */ +TerminalReceiverImpl * +terminal_receiver_impl_new (TerminalScreen *screen) +{ + return reinterpret_cast<TerminalReceiverImpl*> + (g_object_new (TERMINAL_TYPE_RECEIVER_IMPL, + "screen", screen, + nullptr)); +} + +/** + * terminal_receiver_impl_get_screen: + * @impl: a #TerminalReceiverImpl + * + * Returns: (transfer none): the impl's #TerminalScreen, or %nullptr + */ +TerminalScreen * +terminal_receiver_impl_get_screen (TerminalReceiverImpl *impl) +{ + g_return_val_if_fail (TERMINAL_IS_RECEIVER_IMPL (impl), nullptr); + + return impl->priv->screen; +} + +/** + * terminal_receiver_impl_unget_screen: + * @impl: a #TerminalReceiverImpl + * + * Unsets the impls #TerminalScreen. + */ +void +terminal_receiver_impl_unset_screen (TerminalReceiverImpl *impl) +{ + g_return_if_fail (TERMINAL_IS_RECEIVER_IMPL (impl)); + + terminal_receiver_impl_set_screen (impl, nullptr); +} + +/* --------------------------------------------------------------------------- + * TerminalFactoryImpl + * --------------------------------------------------------------------------- + */ + +struct _TerminalFactoryImplPrivate { + gpointer dummy; +}; + +static gboolean +terminal_factory_impl_create_instance (TerminalFactory *factory, + GDBusMethodInvocation *invocation, + GVariant *options) +{ + TerminalApp *app = terminal_app_get (); + + /* If a parent screen is specified, use that to fill in missing information */ + TerminalScreen *parent_screen = nullptr; + const char *parent_screen_object_path; + if (g_variant_lookup (options, "parent-screen", "&o", &parent_screen_object_path)) { + parent_screen = terminal_app_get_screen_by_object_path (app, parent_screen_object_path); + if (parent_screen == nullptr) { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, + "Failed to get screen from object path %s", + parent_screen_object_path); + return TRUE; + } + } + + /* Try getting a parent window, first by parent screen then by window ID; + * if that fails, create a new window. + */ + TerminalWindow *window = nullptr; + gboolean have_new_window = FALSE; + const char *window_from_screen_object_path; + if (g_variant_lookup (options, "window-from-screen", "&o", &window_from_screen_object_path)) { + TerminalScreen *window_screen = + terminal_app_get_screen_by_object_path (app, window_from_screen_object_path); + if (window_screen == nullptr) { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, + "Failed to get screen from object path %s", + parent_screen_object_path); + return TRUE; + } + + GtkWidget *win = gtk_widget_get_toplevel (GTK_WIDGET (window_screen)); + if (TERMINAL_IS_WINDOW (win)) + window = TERMINAL_WINDOW (win); + } + + /* Support old client */ + guint window_id; + if (window == nullptr && g_variant_lookup (options, "window-id", "u", &window_id)) { + GtkWindow *win = gtk_application_get_window_by_id (GTK_APPLICATION (app), window_id); + + if (!TERMINAL_IS_WINDOW (win)) { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, + "Nonexisting window %u referenced", + window_id); + return TRUE; + } + + window = TERMINAL_WINDOW (win); + } + + /* Still no parent window? Create a new one */ + if (window == nullptr) { + const char *startup_id, *role, *activation_token; + gboolean start_maximized, start_fullscreen; + + window = terminal_window_new (G_APPLICATION (app)); + have_new_window = TRUE; + + if (g_variant_lookup (options, "activation-token", "&s", &activation_token)) + gtk_window_set_startup_id (GTK_WINDOW (window), activation_token); + else if (g_variant_lookup (options, "desktop-startup-id", "^&ay", &startup_id)) + gtk_window_set_startup_id (GTK_WINDOW (window), startup_id); + + /* Overwrite the default, unique window role set in terminal_window_init */ + if (g_variant_lookup (options, "role", "&s", &role)) + gtk_window_set_role (GTK_WINDOW (window), role); + + gboolean show_menubar; + if (g_variant_lookup (options, "show-menubar", "b", &show_menubar)) + terminal_window_set_menubar_visible (window, show_menubar); + + if (g_variant_lookup (options, "fullscreen-window", "b", &start_fullscreen) && + start_fullscreen) { + gtk_window_fullscreen (GTK_WINDOW (window)); + } + if (g_variant_lookup (options, "maximize-window", "b", &start_maximized) && + start_maximized) { + gtk_window_maximize (GTK_WINDOW (window)); + } + + have_new_window = TRUE; + } + + g_assert_nonnull (window); + + const char *title; + if (!g_variant_lookup (options, "title", "&s", &title)) + title = nullptr; + + double zoom; + if (!g_variant_lookup (options, "zoom", "d", &zoom)) { + if (parent_screen != nullptr) + zoom = vte_terminal_get_font_scale (VTE_TERMINAL (parent_screen)); + else + zoom = 1.0; + } + + /* Look up the profile */ + gs_unref_object GSettings *profile = nullptr; + const char *profile_uuid; + if (!g_variant_lookup (options, "profile", "&s", &profile_uuid)) + profile_uuid = nullptr; + + if (profile_uuid == nullptr && parent_screen != nullptr) { + profile = terminal_screen_ref_profile (parent_screen); + } else { + GError *err = nullptr; + profile = terminal_profiles_list_ref_profile_by_uuid (terminal_app_get_profiles_list (app), + profile_uuid /* default if nullptr */, + &err); + if (profile == nullptr) { + g_dbus_method_invocation_return_gerror (invocation, err); + g_error_free (err); + return TRUE; + } + } + + g_assert_nonnull (profile); + + /* Now we can create the new screen */ + TerminalScreen *screen = terminal_screen_new (profile, title, zoom); + terminal_window_add_screen (window, screen, -1); + + /* Apply window properties */ + gboolean active; + if (g_variant_lookup (options, "active", "b", &active) && + active) { + terminal_window_switch_screen (window, screen); + gtk_widget_grab_focus (GTK_WIDGET (screen)); + } + + if (have_new_window) { + const char *geometry; + + if (g_variant_lookup (options, "geometry", "&s", &geometry) && + !terminal_window_parse_geometry (window, geometry)) + _terminal_debug_print (TERMINAL_DEBUG_GEOMETRY, + "Invalid geometry string \"%s\"", geometry); + } + + gboolean present_window; + gboolean present_window_set = g_variant_lookup (options, "present-window", "b", &present_window); + + if (have_new_window || (present_window_set && present_window)) + gtk_window_present (GTK_WINDOW (window)); + + gs_free char *object_path = terminal_app_dup_screen_object_path (app, screen); + terminal_factory_complete_create_instance (factory, invocation, object_path); + + return TRUE; /* handled */ +} + +static void +terminal_factory_impl_iface_init (TerminalFactoryIface *iface) +{ + iface->handle_create_instance = terminal_factory_impl_create_instance; +} + +G_DEFINE_TYPE_WITH_CODE (TerminalFactoryImpl, terminal_factory_impl, TERMINAL_TYPE_FACTORY_SKELETON, + G_IMPLEMENT_INTERFACE (TERMINAL_TYPE_FACTORY, terminal_factory_impl_iface_init)) + +static void +terminal_factory_impl_init (TerminalFactoryImpl *impl) +{ + impl->priv = G_TYPE_INSTANCE_GET_PRIVATE (impl, TERMINAL_TYPE_FACTORY_IMPL, TerminalFactoryImplPrivate); +} + +static void +terminal_factory_impl_class_init (TerminalFactoryImplClass *klass) +{ + /* g_type_class_add_private (klass, sizeof (TerminalFactoryImplPrivate)); */ +} + +/** + * terminal_factory_impl_new: + * + * Returns: (transfer full): a new #TerminalFactoryImpl + */ +TerminalFactory * +terminal_factory_impl_new (void) +{ + return reinterpret_cast<TerminalFactory*> + (g_object_new (TERMINAL_TYPE_FACTORY_IMPL, nullptr)); +} diff --git a/src/terminal-gdbus.hh b/src/terminal-gdbus.hh new file mode 100644 index 0000000..38c24bc --- /dev/null +++ b/src/terminal-gdbus.hh @@ -0,0 +1,90 @@ +/* + * Copyright © 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/>. + */ + +#ifndef TERMINAL_RECEIVER_IMPL_H +#define TERMINAL_RECEIVER_IMPL_H + +#include <glib-object.h> + +#include "terminal-gdbus-generated.h" +#include "terminal-screen.hh" + +G_BEGIN_DECLS + +#define TERMINAL_TYPE_RECEIVER_IMPL (terminal_receiver_impl_get_type ()) +#define TERMINAL_RECEIVER_IMPL(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), TERMINAL_TYPE_RECEIVER_IMPL, TerminalReceiverImpl)) +#define TERMINAL_RECEIVER_IMPL_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), TERMINAL_TYPE_RECEIVER_IMPL, TerminalReceiverImplClass)) +#define TERMINAL_IS_RECEIVER_IMPL(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), TERMINAL_TYPE_RECEIVER_IMPL)) +#define TERMINAL_IS_RECEIVER_IMPL_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), TERMINAL_TYPE_RECEIVER_IMPL)) +#define TERMINAL_RECEIVER_IMPL_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), TERMINAL_TYPE_RECEIVER_IMPL, TerminalReceiverImplClass)) + +typedef struct _TerminalReceiverImpl TerminalReceiverImpl; +typedef struct _TerminalReceiverImplClass TerminalReceiverImplClass; +typedef struct _TerminalReceiverImplPrivate TerminalReceiverImplPrivate; + +struct _TerminalReceiverImpl +{ + TerminalReceiverSkeleton parent_instance; + + /*< private >*/ + TerminalReceiverImplPrivate *priv; +}; + +struct _TerminalReceiverImplClass +{ + TerminalReceiverSkeletonClass parent_class; +}; + +GType terminal_receiver_impl_get_type (void); + +TerminalReceiverImpl *terminal_receiver_impl_new (TerminalScreen *screen); + +TerminalScreen *terminal_receiver_impl_get_screen (TerminalReceiverImpl *impl); + +void terminal_receiver_impl_unset_screen (TerminalReceiverImpl *impl); + +/* ------------------------------------------------------------------------- */ + +#define TERMINAL_TYPE_FACTORY_IMPL (terminal_factory_impl_get_type ()) +#define TERMINAL_FACTORY_IMPL(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), TERMINAL_TYPE_FACTORY_IMPL, TerminalFactoryImpl)) +#define TERMINAL_FACTORY_IMPL_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), TERMINAL_TYPE_FACTORY_IMPL, TerminalFactoryImplClass)) +#define TERMINAL_IS_FACTORY_IMPL(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), TERMINAL_TYPE_FACTORY_IMPL)) +#define TERMINAL_IS_FACTORY_IMPL_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), TERMINAL_TYPE_FACTORY_IMPL)) +#define TERMINAL_FACTORY_IMPL_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), TERMINAL_TYPE_FACTORY_IMPL, TerminalFactoryImplClass)) + +typedef struct _TerminalFactoryImpl TerminalFactoryImpl; +typedef struct _TerminalFactoryImplPrivate TerminalFactoryImplPrivate; +typedef struct _TerminalFactoryImplClass TerminalFactoryImplClass; + +struct _TerminalFactoryImplClass { + TerminalFactorySkeletonClass parent_class; +}; + +struct _TerminalFactoryImpl +{ + TerminalFactorySkeleton parent_instance; + + TerminalFactoryImplPrivate *priv; +}; + +GType terminal_factory_impl_get_type (void); + +TerminalFactory *terminal_factory_impl_new (void); + +G_END_DECLS + +#endif /* !TERMINAL_RECEIVER_IMPL_H */ diff --git a/src/terminal-headerbar.cc b/src/terminal-headerbar.cc new file mode 100644 index 0000000..38bae5a --- /dev/null +++ b/src/terminal-headerbar.cc @@ -0,0 +1,170 @@ +/* + * Copyright © 2018 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 <glib/gi18n.h> + +#include "terminal-headerbar.hh" +#include "terminal-app.hh" +#include "terminal-libgsystem.hh" + +typedef struct _TerminalHeaderbarPrivate TerminalHeaderbarPrivate; + +struct _TerminalHeaderbar +{ + GtkHeaderBar parent_instance; +}; + +struct _TerminalHeaderbarClass +{ + GtkHeaderBarClass parent_class; +}; + +struct _TerminalHeaderbarPrivate +{ + GtkWidget *profilebutton; + GtkWidget *menubutton; +}; + +enum { + PROP_0, + LAST_PROP +}; + +enum { + LAST_SIGNAL +}; + +/* static guint signals[LAST_SIGNAL]; */ +/* static GParamSpec *pspecs[LAST_PROP]; */ + +G_DEFINE_TYPE_WITH_PRIVATE (TerminalHeaderbar, terminal_headerbar, GTK_TYPE_HEADER_BAR) + +#define PRIV(obj) ((TerminalHeaderbarPrivate *) terminal_headerbar_get_instance_private ((TerminalHeaderbar *)(obj))) + +static void +profilemenu_items_changed_cb (GMenuModel *menu, + int position G_GNUC_UNUSED, + int removed G_GNUC_UNUSED, + int added G_GNUC_UNUSED, + TerminalHeaderbarPrivate *priv) +{ + if (g_menu_model_get_n_items (menu) > 0) + gtk_widget_show (priv->profilebutton); + else + gtk_widget_hide (priv->profilebutton); +} + +/* Class implementation */ + +static void +terminal_headerbar_init (TerminalHeaderbar *headerbar) +{ + + TerminalHeaderbarPrivate *priv = PRIV (headerbar); + GtkWidget *widget = GTK_WIDGET (headerbar); + TerminalApp *app = terminal_app_get (); + GMenuModel *profilemenu; + + gtk_widget_init_template (widget); + + gtk_menu_button_set_menu_model (GTK_MENU_BUTTON (priv->menubutton), + terminal_app_get_headermenu (app)); + + profilemenu = terminal_app_get_profilemenu (app); + gtk_menu_button_set_menu_model (GTK_MENU_BUTTON (priv->profilebutton), + profilemenu); + + g_signal_connect (profilemenu, "items-changed", + G_CALLBACK (profilemenu_items_changed_cb), priv); + profilemenu_items_changed_cb (profilemenu, 0, 0, 0, priv); +} + +static void +terminal_headerbar_dispose (GObject *object) +{ + TerminalHeaderbar *headerbar = TERMINAL_HEADERBAR (object); + TerminalHeaderbarPrivate *priv = PRIV (headerbar); + TerminalApp *app = terminal_app_get (); + + GMenuModel *profilemenu = terminal_app_get_profilemenu (app); + g_signal_handlers_disconnect_by_func (profilemenu, + (void*)profilemenu_items_changed_cb, + priv); + + G_OBJECT_CLASS (terminal_headerbar_parent_class)->dispose (object); +} + +static void +terminal_headerbar_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + // TerminalHeaderbar *headerbar = TERMINAL_HEADERBAR (object); + + switch (prop_id) { + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +terminal_headerbar_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (prop_id) { + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +terminal_headerbar_class_init (TerminalHeaderbarClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + gobject_class->dispose = terminal_headerbar_dispose; + gobject_class->get_property = terminal_headerbar_get_property; + gobject_class->set_property = terminal_headerbar_set_property; + + /* g_object_class_install_properties (gobject_class, G_N_ELEMENTS (pspecs), pspecs); */ + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/terminal/ui/headerbar.ui"); + gtk_widget_class_bind_template_child_private (widget_class, TerminalHeaderbar, menubutton); + gtk_widget_class_bind_template_child_private (widget_class, TerminalHeaderbar, profilebutton); +} + +/* public API */ + +/** + * terminal_headerbar_new: + * + * Returns: a new #TerminalHeaderbar + */ +GtkWidget * +terminal_headerbar_new (void) +{ + return reinterpret_cast<GtkWidget*> + (g_object_new (TERMINAL_TYPE_HEADERBAR, nullptr)); +} diff --git a/src/terminal-headerbar.hh b/src/terminal-headerbar.hh new file mode 100644 index 0000000..c8aabd6 --- /dev/null +++ b/src/terminal-headerbar.hh @@ -0,0 +1,40 @@ +/* + * Copyright © 2018 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/>. + */ + +#pragma once + +#include <gtk/gtk.h> + +#include "terminal-screen.hh" + +G_BEGIN_DECLS + +#define TERMINAL_TYPE_HEADERBAR (terminal_headerbar_get_type ()) +#define TERMINAL_HEADERBAR(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), TERMINAL_TYPE_HEADERBAR, TerminalHeaderbar)) +#define TERMINAL_HEADERBAR_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), TERMINAL_TYPE_HEADERBAR, TerminalHeaderbarClass)) +#define TERMINAL_IS_HEADERBAR(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), TERMINAL_TYPE_HEADERBAR)) +#define TERMINAL_IS_HEADERBAR_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), TERMINAL_TYPE_HEADERBAR)) +#define TERMINAL_HEADERBAR_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), TERMINAL_TYPE_HEADERBAR, TerminalHeaderbarClass)) + +typedef struct _TerminalHeaderbar TerminalHeaderbar; +typedef struct _TerminalHeaderbarClass TerminalHeaderbarClass; + +GType terminal_headerbar_get_type (void); + +GtkWidget *terminal_headerbar_new (void); + +G_END_DECLS diff --git a/src/terminal-headerbar.ui b/src/terminal-headerbar.ui new file mode 100644 index 0000000..047cdfe --- /dev/null +++ b/src/terminal-headerbar.ui @@ -0,0 +1,112 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright © 2018 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, or (at your option) + any later version. + + This program is distributed in the hope conf 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/>. +--> +<interface> + <template class="TerminalHeaderbar" parent="GtkHeaderBar"> + <property name="can-focus">False</property> + <property name="visible">True</property> + <property name="show-close-button">True</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <style> + <class name="linked"/> + </style> + <child> + <object class="GtkButton"> + <property name="visible">True</property> + <property name="focus_on_click">False</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="action-name">win.new-terminal</property> + <property name="action-target">('tab-default','current')</property> + <style> + <class name="image-button"/> + </style> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="icon_name">tab-new-symbolic</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkMenuButton" id="profilebutton"> + <property name="visible">True</property> + <property name="focus_on_click">False</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use-popover">False</property> + <style> + <class name="image-button"/> + <class name="disclosure-button"/> + </style> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="icon_name">pan-down-symbolic</property> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkMenuButton" id="menubutton"> + <property name="visible">True</property> + <property name="focus_on_click">False</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="action-name">win.header-menu</property> + <style> + <class name="image-button"/> + </style> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="icon_name">open-menu-symbolic</property> + </object> + </child> + </object> + <packing> + <property name="pack_type">end</property> + </packing> + </child> + <child> + <object class="GtkButton"> + <property name="visible">True</property> + <property name="focus_on_click">False</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="action-name">win.find</property> + <style> + <class name="image-button"/> + </style> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="icon_name">edit-find-symbolic</property> + </object> + </child> + </object> + <packing> + <property name="pack_type">end</property> + </packing> + </child> + </template> +</interface> diff --git a/src/terminal-headermenu.ui b/src/terminal-headermenu.ui new file mode 100644 index 0000000..42d5296 --- /dev/null +++ b/src/terminal-headermenu.ui @@ -0,0 +1,119 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright © 2012 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/>. +--> +<interface> + <menu id="headermenu"> + <section> + <attribute name="display-hint">horizontal-buttons</attribute> + <item> + <attribute name="label" translatable="yes">Zoom _Out</attribute> + <attribute name="verb-icon">zoom-out-symbolic</attribute> + <attribute name="action">win.zoom-out</attribute> + </item> + <item> + <attribute name="label">100%</attribute> + <attribute name="action">win.zoom-normal</attribute> + </item> + <item> + <attribute name="label" translatable="yes">Zoom _In</attribute> + <attribute name="verb-icon">zoom-in-symbolic</attribute> + <attribute name="action">win.zoom-in</attribute> + </item> + </section> + <section> + <item> + <attribute name="label" translatable="yes">New _Window</attribute> + <attribute name="action">win.new-terminal</attribute> + <attribute name="target" type="(ss)">('window', 'current')</attribute> + </item> + <item> + <attribute name="label" translatable="yes">_Full Screen</attribute> + <attribute name="action">win.enter-fullscreen</attribute> + </item> + </section> + <section> + <item> + <attribute name="label" translatable="yes">Read-_Only</attribute> + <attribute name="action">win.read-only</attribute> + </item> + <item> + <attribute name="label" translatable="yes">Set _Title…</attribute> + <attribute name="action">win.set-title</attribute> + <attribute name="hidden-when">action-missing</attribute> + </item> + <section id="set-profile-section" /> + <submenu> + <attribute name="label" translatable="yes">_Advanced</attribute> + <section> + <item> + <attribute name="label" translatable="yes">_Reset</attribute> + <attribute name="action">win.reset</attribute> + <attribute name="target" type="b">false</attribute> + </item> + <item> + <attribute name="label" translatable="yes">Reset and C_lear</attribute> + <attribute name="action">win.reset</attribute> + <attribute name="target" type="b">true</attribute> + </item> + </section> + <section> + <item> + <attribute name="label" translatable="yes">_1. 80×24</attribute> + <attribute name="action">win.size-to</attribute> + <attribute name="target" type="(uu)">(80, 24)</attribute> + </item> + <item> + <attribute name="label" translatable="yes">_2. 80×43</attribute> + <attribute name="action">win.size-to</attribute> + <attribute name="target" type="(uu)">(80, 43)</attribute> + </item> + <item> + <attribute name="label" translatable="yes">_3. 132×24</attribute> + <attribute name="action">win.size-to</attribute> + <attribute name="target" type="(uu)">(132, 24)</attribute> + </item> + <item> + <attribute name="label" translatable="yes">_4. 132×43</attribute> + <attribute name="action">win.size-to</attribute> + <attribute name="target" type="(uu)">(132, 43)</attribute> + </item> + </section> + <section> + <item> + <attribute name="label" translatable="yes">_Inspector</attribute> + <attribute name="action">win.inspector</attribute> + <attribute name="hidden-when">action-disabled</attribute> + </item> + </section> + </submenu> + </section> + <section> + <item> + <attribute name="label" translatable="yes">_Preferences</attribute> + <attribute name="action">app.preferences</attribute> + </item> + <item> + <attribute name="label" translatable="yes">_Help</attribute> + <attribute name="action">app.help</attribute> + </item> + <item> + <attribute name="label" translatable="yes">_About</attribute> + <attribute name="action">app.about</attribute> + </item> + </section> + </menu> +</interface> diff --git a/src/terminal-i18n.cc b/src/terminal-i18n.cc new file mode 100644 index 0000000..162c49f --- /dev/null +++ b/src/terminal-i18n.cc @@ -0,0 +1,31 @@ +/* + * Copyright © 2002 Havoc Pennington + * + * 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 "terminal-i18n.hh" + +#include <libintl.h> + +void +terminal_i18n_init (gboolean set_default) +{ + bindtextdomain (GETTEXT_PACKAGE, TERM_LOCALEDIR); + bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); + + if (set_default) + textdomain (GETTEXT_PACKAGE); +} diff --git a/src/terminal-i18n.hh b/src/terminal-i18n.hh new file mode 100644 index 0000000..689bb82 --- /dev/null +++ b/src/terminal-i18n.hh @@ -0,0 +1,29 @@ +/* + * Copyright © 2002 Havoc Pennington + * + * 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/>. + */ + +#ifndef TERMINAL_I18N_H +#define TERMINAL_I18N_H + +#include <glib.h> + +G_BEGIN_DECLS + +void terminal_i18n_init (gboolean set_default); + +G_END_DECLS + +#endif /* TERMINAL_I18N_H */ diff --git a/src/terminal-icon-button.cc b/src/terminal-icon-button.cc new file mode 100644 index 0000000..b499824 --- /dev/null +++ b/src/terminal-icon-button.cc @@ -0,0 +1,50 @@ +/* + * terminal-icon-button.c + * + * Copyright © 2010 - Paolo Borelli + * Copyright © 2011 - Ignacio Casal Quinteiro + * + * 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 "terminal-icon-button.hh" +#include "terminal-libgsystem.hh" + +GtkWidget * +terminal_icon_button_new (const char *gicon_name) +{ + GtkWidget *button, *image; + gs_unref_object GIcon *icon; + + button = (GtkWidget *) g_object_new (GTK_TYPE_BUTTON, + "relief", GTK_RELIEF_NONE, + "focus-on-click", FALSE, + nullptr); + + icon = g_themed_icon_new_with_default_fallbacks (gicon_name); + image = gtk_image_new_from_gicon (icon, GTK_ICON_SIZE_MENU); + + gtk_widget_show (image); + gtk_container_add (GTK_CONTAINER (button), image); + + return button; +} + +GtkWidget * +terminal_close_button_new (void) +{ + return terminal_icon_button_new ("window-close-symbolic"); +} diff --git a/src/terminal-icon-button.hh b/src/terminal-icon-button.hh new file mode 100644 index 0000000..ec0a8db --- /dev/null +++ b/src/terminal-icon-button.hh @@ -0,0 +1,33 @@ +/* + * terminal-close-button.h + * + * Copyright © 2010 - Paolo Borelli + * + * 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/>. + */ + +#ifndef __TERMINAL_ICON_BUTTON_H__ +#define __TERMINAL_ICON_BUTTON_H__ + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +GtkWidget *terminal_icon_button_new (const char *gicon_name); + +GtkWidget *terminal_close_button_new (void); + +G_END_DECLS + +#endif /* __TERMINAL_ICON_BUTTON_H__ */ diff --git a/src/terminal-info-bar.cc b/src/terminal-info-bar.cc new file mode 100644 index 0000000..1b451a6 --- /dev/null +++ b/src/terminal-info-bar.cc @@ -0,0 +1,121 @@ +/* + * Copyright © 2010 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 "terminal-info-bar.hh" +#include "terminal-libgsystem.hh" + +#include <gtk/gtk.h> + +#define TERMINAL_INFO_BAR_GET_PRIVATE(info_bar)(G_TYPE_INSTANCE_GET_PRIVATE ((info_bar), TERMINAL_TYPE_INFO_BAR, TerminalInfoBarPrivate)) + +struct _TerminalInfoBarPrivate +{ + GtkWidget *content_box; +}; + +G_DEFINE_TYPE (TerminalInfoBar, terminal_info_bar, GTK_TYPE_INFO_BAR) + +/* helper functions */ + +static void +terminal_info_bar_init (TerminalInfoBar *bar) +{ + GtkInfoBar *info_bar = GTK_INFO_BAR (bar); + TerminalInfoBarPrivate *priv; + + priv = bar->priv = TERMINAL_INFO_BAR_GET_PRIVATE (bar); + + priv->content_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6); + gtk_box_pack_start (GTK_BOX (gtk_info_bar_get_content_area (info_bar)), + priv->content_box, TRUE, TRUE, 0); +} + +static void +terminal_info_bar_class_init (TerminalInfoBarClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + g_type_class_add_private (gobject_class, sizeof (TerminalInfoBarPrivate)); +} + +/* public API */ + +/** + * terminal_info_bar_new: + * @type: a #GtkMessageType + * + * Returns: a new #TerminalInfoBar for @screen + */ +GtkWidget * +terminal_info_bar_new (GtkMessageType type, + const char *first_button_text, + ...) +{ + GtkWidget *info_bar; + va_list args; + + info_bar = reinterpret_cast<GtkWidget*> + (g_object_new (TERMINAL_TYPE_INFO_BAR, + "message-type", type, + "show-close-button", true, + nullptr)); + + va_start (args, first_button_text); + while (first_button_text != nullptr) { + int response_id; + + response_id = va_arg (args, int); + gtk_info_bar_add_button (GTK_INFO_BAR (info_bar), + first_button_text, response_id); + + first_button_text = va_arg (args, const char *); + } + va_end (args); + + return info_bar; +} + +void +terminal_info_bar_format_text (TerminalInfoBar *bar, + const char *format, + ...) +{ + TerminalInfoBarPrivate *priv; + gs_free char *text = nullptr; + GtkWidget *label; + va_list args; + + g_return_if_fail (TERMINAL_IS_INFO_BAR (bar)); + + priv = bar->priv; + + va_start (args, format); + text = g_strdup_vprintf (format, args); + va_end (args); + + label = gtk_label_new (text); + + gtk_label_set_line_wrap (GTK_LABEL (label), TRUE); + gtk_label_set_selectable (GTK_LABEL (label), TRUE); + gtk_label_set_xalign (GTK_LABEL (label), 0.0); + gtk_label_set_yalign (GTK_LABEL (label), 0.0); + + gtk_box_pack_start (GTK_BOX (priv->content_box), label, FALSE, FALSE, 0); + gtk_widget_show_all (priv->content_box); +} diff --git a/src/terminal-info-bar.hh b/src/terminal-info-bar.hh new file mode 100644 index 0000000..87a5234 --- /dev/null +++ b/src/terminal-info-bar.hh @@ -0,0 +1,61 @@ +/* + * Copyright © 2010 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/>. + */ + +#ifndef TERMINAL_INFO_BAR_H +#define TERMINAL_INFO_BAR_H + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define TERMINAL_TYPE_INFO_BAR (terminal_info_bar_get_type ()) +#define TERMINAL_INFO_BAR(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), TERMINAL_TYPE_INFO_BAR, TerminalInfoBar)) +#define TERMINAL_INFO_BAR_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), TERMINAL_TYPE_INFO_BAR, TerminalInfoBarClass)) +#define TERMINAL_IS_INFO_BAR(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), TERMINAL_TYPE_INFO_BAR)) +#define TERMINAL_IS_INFO_BAR_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), TERMINAL_TYPE_INFO_BAR)) +#define TERMINAL_INFO_BAR_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), TERMINAL_TYPE_INFO_BAR, TerminalInfoBarClass)) + +typedef struct _TerminalInfoBar TerminalInfoBar; +typedef struct _TerminalInfoBarClass TerminalInfoBarClass; +typedef struct _TerminalInfoBarPrivate TerminalInfoBarPrivate; + +struct _TerminalInfoBar +{ + GtkInfoBar parent_instance; + + /*< private >*/ + TerminalInfoBarPrivate *priv; +}; + +struct _TerminalInfoBarClass +{ + GtkInfoBarClass parent_class; +}; + +GType terminal_info_bar_get_type (void); + +GtkWidget *terminal_info_bar_new (GtkMessageType type, + const char *first_button_text, + ...) G_GNUC_NULL_TERMINATED; + +void terminal_info_bar_format_text (TerminalInfoBar *bar, + const char *format, + ...) G_GNUC_PRINTF (2, 3); + +G_END_DECLS + +#endif /* !TERMINAL_INFO_BAR_H */ diff --git a/src/terminal-intl.hh b/src/terminal-intl.hh new file mode 100644 index 0000000..daaa963 --- /dev/null +++ b/src/terminal-intl.hh @@ -0,0 +1,29 @@ +/* + * Copyright © 2002 Havoc Pennington + * + * 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/>. + */ + +#ifndef TERMINAL_INTL_H +#define TERMINAL_INTL_H + +#include <glib.h> + +G_BEGIN_DECLS + +#define I_(string) g_intern_static_string (string) + +G_END_DECLS + +#endif /* TERMINAL_INTL_H */ diff --git a/src/terminal-libgsystem.hh b/src/terminal-libgsystem.hh new file mode 100644 index 0000000..370cb28 --- /dev/null +++ b/src/terminal-libgsystem.hh @@ -0,0 +1,317 @@ +/* + * Copyright © 2012 Colin Walters <walters@verbum.org>. + * + * 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/>. + */ + +#ifndef __GSYSTEM_LOCAL_ALLOC_H__ +#define __GSYSTEM_LOCAL_ALLOC_H__ + +#include <gio/gio.h> + +G_BEGIN_DECLS + +#define gs_transfer_out_value(outp, srcp) G_STMT_START { \ + if (outp) \ + { \ + *(outp) = *(srcp); \ + *(srcp) = nullptr; \ + } \ + } G_STMT_END; + +#define GS_DEFINE_CLEANUP_FUNCTION(Type, name, func) \ + static inline void name (void *v) \ + { \ + func (*(Type*)v); \ + } + +#define GS_DEFINE_CLEANUP_FUNCTION0(Type, name, func) \ + static inline void name (void *v) \ + { \ + if (*(Type*)v) \ + func (*(Type*)v); \ + } + +/* These functions shouldn't be invoked directly; + * they are stubs that: + * 1) Take a pointer to the location (typically itself a pointer). + * 2) Provide %nullptr-safety where it doesn't exist already (e.g. g_object_unref) + */ +GS_DEFINE_CLEANUP_FUNCTION0(GArray*, gs_local_array_unref, g_array_unref) +GS_DEFINE_CLEANUP_FUNCTION0(GBytes*, gs_local_bytes_unref, g_bytes_unref) +GS_DEFINE_CLEANUP_FUNCTION0(GChecksum*, gs_local_checksum_free, g_checksum_free) +GS_DEFINE_CLEANUP_FUNCTION0(GDateTime*, gs_local_date_time_unref, g_date_time_unref) +GS_DEFINE_CLEANUP_FUNCTION0(GDir*, gs_local_dir_close, g_dir_close) +GS_DEFINE_CLEANUP_FUNCTION0(GError*, gs_local_free_error, g_error_free) +GS_DEFINE_CLEANUP_FUNCTION0(GHashTable*, gs_local_hashtable_unref, g_hash_table_unref) +GS_DEFINE_CLEANUP_FUNCTION0(GKeyFile*, gs_local_key_file_unref, g_key_file_unref) +GS_DEFINE_CLEANUP_FUNCTION0(GList*, gs_local_list_free, g_list_free) +GS_DEFINE_CLEANUP_FUNCTION0(GMatchInfo*, gs_local_match_info_free, g_match_info_free) +GS_DEFINE_CLEANUP_FUNCTION0(GObject*, gs_local_obj_unref, g_object_unref) +GS_DEFINE_CLEANUP_FUNCTION0(GOptionContext*, gs_local_option_context_free, g_option_context_free) +GS_DEFINE_CLEANUP_FUNCTION0(GPtrArray*, gs_local_ptrarray_unref, g_ptr_array_unref) +GS_DEFINE_CLEANUP_FUNCTION0(GRegex*, gs_local_regex_unref, g_regex_unref) +GS_DEFINE_CLEANUP_FUNCTION0(GSettingsSchema*, gs_local_settings_schema_unref, g_settings_schema_unref) +GS_DEFINE_CLEANUP_FUNCTION0(GSettingsSchemaKey*, gs_local_settings_schema_key_unref, g_settings_schema_key_unref) +GS_DEFINE_CLEANUP_FUNCTION0(GVariant*, gs_local_variant_unref, g_variant_unref) +GS_DEFINE_CLEANUP_FUNCTION0(GVariantBuilder*, gs_local_variant_builder_unref, g_variant_builder_unref) +GS_DEFINE_CLEANUP_FUNCTION0(GVariantIter*, gs_local_variant_iter_free, g_variant_iter_free) +GS_DEFINE_CLEANUP_FUNCTION0(GVariantType*, gs_local_variant_type_free, g_variant_type_free) + +GS_DEFINE_CLEANUP_FUNCTION(char**, gs_local_strfreev, g_strfreev) +GS_DEFINE_CLEANUP_FUNCTION(void*, gs_local_free, g_free) + +/* special */ + +static inline void gs_local_gstring_free (void *v) \ +{ \ + if (*(GString**)v) \ + g_string_free (*(GString**)v, TRUE); \ +} + +/** + * gs_free: + * + * Call g_free() on a variable location when it goes out of scope. + */ +#define gs_free __attribute__ ((cleanup(gs_local_free))) + +/** + * gs_unref_object: + * + * Call g_object_unref() on a variable location when it goes out of + * scope. Note that unlike g_object_unref(), the variable may be + * %nullptr. + */ +#define gs_unref_object __attribute__ ((cleanup(gs_local_obj_unref))) + +/** + * gs_unref_variant: + * + * Call g_variant_unref() on a variable location when it goes out of + * scope. Note that unlike g_variant_unref(), the variable may be + * %nullptr. + */ +#define gs_unref_variant __attribute__ ((cleanup(gs_local_variant_unref))) + +/** + * gs_free_variant_iter: + * + * Call g_variant_iter_free() on a variable location when it goes out of + * scope. + */ +#define gs_free_variant_iter __attribute__ ((cleanup(gs_local_variant_iter_free))) + +/** + * gs_free_variant_builder: + * + * Call g_variant_builder_unref() on a variable location when it goes out of + * scope. + */ +#define gs_unref_variant_builder __attribute__ ((cleanup(gs_local_variant_builder_unref))) + +/** + * gs_free_variant_type: + * + * Call g_variant_type_free() on a variable location when it goes out of + * scope. + */ +#define gs_free_variant_type __attribute__ ((cleanup(gs_local_variant_type_free))) + +/** + * gs_unref_array: + * + * Call g_array_unref() on a variable location when it goes out of + * scope. Note that unlike g_array_unref(), the variable may be + * %nullptr. + + */ +#define gs_unref_array __attribute__ ((cleanup(gs_local_array_unref))) + +/** + * gs_unref_ptrarray: + * + * Call g_ptr_array_unref() on a variable location when it goes out of + * scope. Note that unlike g_ptr_array_unref(), the variable may be + * %nullptr. + + */ +#define gs_unref_ptrarray __attribute__ ((cleanup(gs_local_ptrarray_unref))) + +/** + * gs_unref_hashtable: + * + * Call g_hash_table_unref() on a variable location when it goes out + * of scope. Note that unlike g_hash_table_unref(), the variable may + * be %nullptr. + */ +#define gs_unref_hashtable __attribute__ ((cleanup(gs_local_hashtable_unref))) + +/** + * gs_unref_key_file: + * + * Call g_key_file_unref() on a variable location when it goes out + * of scope. Note that unlike g_key_file_unref(), the variable may + * be %nullptr. + */ +#define gs_unref_key_file __attribute__ ((cleanup(gs_local_key_file_unref))) + +/** + * gs_free_checksum: + * + * Call g_checksum_free() on a variable location when it goes out + * of scope. Note that unlike g_checksum_free(), the variable may + * be %nullptr. + */ +#define gs_free_checksum __attribute__ ((cleanup(gs_local_checksum_free))) + +/** + * gs_unref_date_time: + * + * Call g_date_time_free() on a variable location when it goes out + * of scope. Note that unlike g_date_time_free(), the variable may + * be %nullptr. + */ +#define gs_unref_date_time __attribute__ ((cleanup(gs_local_date_time_unref))) + +/** + * gs_unref_bytes: + * + * Call g_bytes_unref() on a variable location when it goes out + * of scope. Note that unlike g_bytes_unref(), the variable may + * be %nullptr. + */ +#define gs_unref_bytes __attribute__ ((cleanup(gs_local_bytes_unref))) + +/** + * gs_strfreev: + * + * Call g_strfreev() on a variable location when it goes out of scope. + */ +#define gs_strfreev __attribute__ ((cleanup(gs_local_strfreev))) + +/** + * gs_free_error: + * + * Call g_error_free() on a variable location when it goes out of scope. + */ +#define gs_free_error __attribute__ ((cleanup(gs_local_free_error))) + +/** + * gs_free_list: + * + * Call g_list_free() on a variable location when it goes out of scope. + */ +#define gs_free_list __attribute__ ((cleanup(gs_local_list_free))) + +/** + * gs_unref_regex: + * + * Call g_regex_unref() on a variable location when it goes out of + * scope. Note that unlike g_regex_unref(), the variable may be + * %nullptr. + + */ +#define gs_unref_regex __attribute__ ((cleanup(gs_local_regex_unref))) + +/** + * gs_free_match_info: + * + * Call g_regex_unref() on a variable location when it goes out of + * scope. Note that unlike g_regex_unref(), the variable may be + * %nullptr. + + */ +#define gs_free_match_info __attribute__ ((cleanup(gs_local_match_info_free))) + +/** + * gs_unref_settings_schema: + * + * Call g_settings_schema_unref() on a variable location when it goes out of + * scope. Note that unlike g_settings_schema_unref(), the variable may be + * %nullptr. + + */ +#define gs_unref_settings_schema __attribute__ ((cleanup(gs_local_settings_schema_unref))) + +/** + * gs_unref_settings_schema_source: + * + * Call g_settings_schema_source_unref() on a variable location when it goes out of + * scope. Note that unlike g_settings_schema_source_unref(), the variable may be + * %nullptr. + + */ +#define gs_unref_settings_schema_source __attribute__ ((cleanup(gs_local_settings_schema_source_unref))) + +/** + * gs_unref_settings_schema_key: + * + * Call g_settings_schema_key_unref() on a variable location when it goes out of + * scope. Note that unlike g_settings_schema_key_unref(), the variable may be + * %nullptr. + + */ +#define gs_unref_settings_schema_key __attribute__ ((cleanup(gs_local_settings_schema_key_unref))) + +/** + * gs_free_gstring: + * + * Call g_string_free(TRUE) on a variable location when it goes out + * of scope. Note that unlike g_string_free(), the variable may + * be %nullptr. + */ +#define gs_free_gstring __attribute__ ((cleanup(gs_local_gstring_free))) + +/** + * gs_free_option_context: + * + * Call g_regex_unref() on a variable location when it goes out of + * scope. Note that unlike g_option_context_free(), the variable may be + * %NULL. + + */ +#define gs_free_option_context __attribute__ ((cleanup(gs_local_option_context_free))) + +/** + * gs_close_dir: + * + * Call g_dir_close() on a variable location when it goes out of + * scope. + + */ +#define gs_close_dir __attribute__ ((cleanup(gs_local_dir_close))) + +static inline void gs_local_fd_close (void *v) +{ + auto fd = *reinterpret_cast<int*>(v); + if (fd != -1) { + auto const errsv = errno; + close(fd); + errno = errsv; + } +} + +/** + * gs_free_close: + * + * Call close() on a variable location when it goes out of + * scope. + + */ +#define gs_close_fd __attribute__ ((cleanup(gs_local_fd_close))) + +G_END_DECLS + +#endif diff --git a/src/terminal-marshal.list b/src/terminal-marshal.list new file mode 100644 index 0000000..3e91f60 --- /dev/null +++ b/src/terminal-marshal.list @@ -0,0 +1 @@ +BOOLEAN:STRING,INT,UINT diff --git a/src/terminal-mdi-container.cc b/src/terminal-mdi-container.cc new file mode 100644 index 0000000..f3c979d --- /dev/null +++ b/src/terminal-mdi-container.cc @@ -0,0 +1,208 @@ +/* + * Copyright © 2008, 2010, 2011, 2012 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 "terminal-mdi-container.hh" +#include "terminal-debug.hh" +#include "terminal-intl.hh" + +enum { + SCREEN_ADDED, + SCREEN_REMOVED, + SCREEN_SWITCHED, + SCREENS_REORDERED, + SCREEN_CLOSE_REQUEST, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; + +G_DEFINE_INTERFACE (TerminalMdiContainer, terminal_mdi_container, GTK_TYPE_WIDGET) + +static void +terminal_mdi_container_default_init (TerminalMdiContainerInterface *iface) +{ + signals[SCREEN_ADDED] = + g_signal_new (I_("screen-added"), + G_TYPE_FROM_INTERFACE (iface), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (TerminalMdiContainerInterface, screen_added), + nullptr, nullptr, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, + 1, TERMINAL_TYPE_SCREEN); + + signals[SCREEN_ADDED] = + g_signal_new (I_("screen-removed"), + G_TYPE_FROM_INTERFACE (iface), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (TerminalMdiContainerInterface, screen_added), + nullptr, nullptr, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, + 1, TERMINAL_TYPE_SCREEN); + + signals[SCREEN_ADDED] = + g_signal_new (I_("screen-switched"), + G_TYPE_FROM_INTERFACE (iface), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (TerminalMdiContainerInterface, screen_switched), + nullptr, nullptr, + nullptr, + G_TYPE_NONE, + 2, TERMINAL_TYPE_SCREEN, TERMINAL_TYPE_SCREEN); + + signals[SCREENS_REORDERED] = + g_signal_new (I_("screens-reordered"), + G_TYPE_FROM_INTERFACE (iface), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (TerminalMdiContainerInterface, screens_reordered), + nullptr, nullptr, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, + 0); + + signals[SCREEN_CLOSE_REQUEST] = + g_signal_new (I_("screen-close-request"), + G_TYPE_FROM_INTERFACE (iface), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (TerminalMdiContainerInterface, screen_close_request), + nullptr, nullptr, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, + 1, TERMINAL_TYPE_SCREEN); + + g_object_interface_install_property (iface, + g_param_spec_object ("active-screen", nullptr, nullptr, + TERMINAL_TYPE_SCREEN, + GParamFlags(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS))); +} + +/* public API */ + +void +terminal_mdi_container_add_screen (TerminalMdiContainer *container, + TerminalScreen *screen, + int position) +{ + g_return_if_fail (TERMINAL_IS_MDI_CONTAINER (container)); + g_return_if_fail (TERMINAL_IS_SCREEN (screen)); + + TERMINAL_MDI_CONTAINER_GET_IFACE (container)->add_screen (container, screen, position); +} + +void +terminal_mdi_container_remove_screen (TerminalMdiContainer *container, + TerminalScreen *screen) +{ + g_return_if_fail (TERMINAL_IS_MDI_CONTAINER (container)); + g_return_if_fail (TERMINAL_IS_SCREEN (screen)); + + TERMINAL_MDI_CONTAINER_GET_IFACE (container)->remove_screen (container, screen); +} + +TerminalScreen * +terminal_mdi_container_get_active_screen (TerminalMdiContainer *container) +{ + g_return_val_if_fail (TERMINAL_IS_MDI_CONTAINER (container), nullptr); + + return TERMINAL_MDI_CONTAINER_GET_IFACE (container)->get_active_screen (container); +} + +void +terminal_mdi_container_set_active_screen (TerminalMdiContainer *container, + TerminalScreen *screen) +{ + g_return_if_fail (TERMINAL_IS_MDI_CONTAINER (container)); + g_return_if_fail (TERMINAL_IS_SCREEN (screen)); + + TERMINAL_MDI_CONTAINER_GET_IFACE (container)->set_active_screen (container, screen); +} + + +GList * +terminal_mdi_container_list_screens (TerminalMdiContainer *container) +{ + g_return_val_if_fail (TERMINAL_IS_MDI_CONTAINER (container), nullptr); + + return TERMINAL_MDI_CONTAINER_GET_IFACE (container)->list_screens (container); +} + +GList * +terminal_mdi_container_list_screen_containers (TerminalMdiContainer *container) +{ + g_return_val_if_fail (TERMINAL_IS_MDI_CONTAINER (container), nullptr); + + return TERMINAL_MDI_CONTAINER_GET_IFACE (container)->list_screen_containers (container); +} + +int +terminal_mdi_container_get_n_screens (TerminalMdiContainer *container) +{ + g_return_val_if_fail (TERMINAL_IS_MDI_CONTAINER (container), 0); + + return TERMINAL_MDI_CONTAINER_GET_IFACE (container)->get_n_screens (container); +} + +int +terminal_mdi_container_get_active_screen_num (TerminalMdiContainer *container) +{ + g_return_val_if_fail (TERMINAL_IS_MDI_CONTAINER (container), -1); + + return TERMINAL_MDI_CONTAINER_GET_IFACE (container)->get_active_screen_num (container); +} + +void +terminal_mdi_container_set_active_screen_num (TerminalMdiContainer *container, + int position) +{ + g_return_if_fail (TERMINAL_IS_MDI_CONTAINER (container)); + + TERMINAL_MDI_CONTAINER_GET_IFACE (container)->set_active_screen_num (container, position); +} + +void +terminal_mdi_container_reorder_screen (TerminalMdiContainer *container, + TerminalScreen *screen, + int new_position) +{ + g_return_if_fail (TERMINAL_IS_MDI_CONTAINER (container)); + + return TERMINAL_MDI_CONTAINER_GET_IFACE (container)->reorder_screen (container, screen, new_position); +} + +void +terminal_mdi_container_change_screen (TerminalMdiContainer *container, + int change) +{ + int active, n; + + g_return_if_fail (TERMINAL_IS_MDI_CONTAINER (container)); + g_return_if_fail (change == -1 || change == 1); + + n = terminal_mdi_container_get_n_screens (container); + active = terminal_mdi_container_get_active_screen_num (container); + + active += change; + if (active < 0) + active = n - 1; + else if (active >= n) + active = 0; + + terminal_mdi_container_set_active_screen_num (container, active); +} diff --git a/src/terminal-mdi-container.hh b/src/terminal-mdi-container.hh new file mode 100644 index 0000000..6915c8e --- /dev/null +++ b/src/terminal-mdi-container.hh @@ -0,0 +1,104 @@ +/* + * Copyright © 2008, 2010, 2012 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/>. + */ + +#ifndef TERMINAL_MDI_CONTAINER_H +#define TERMINAL_MDI_CONTAINER_H + +#include <gtk/gtk.h> + +#include "terminal-screen.hh" + +G_BEGIN_DECLS + +#define TERMINAL_TYPE_MDI_CONTAINER (terminal_mdi_container_get_type ()) +#define TERMINAL_MDI_CONTAINER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), TERMINAL_TYPE_MDI_CONTAINER, TerminalMdiContainer)) +#define TERMINAL_IS_MDI_CONTAINER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), TERMINAL_TYPE_MDI_CONTAINER)) +#define TERMINAL_MDI_CONTAINER_GET_IFACE(inst) (G_TYPE_INSTANCE_GET_INTERFACE ((inst), TERMINAL_TYPE_MDI_CONTAINER, TerminalMdiContainerInterface)) + +typedef struct _TerminalMdiContainer TerminalMdiContainer; +typedef struct _TerminalMdiContainerInterface TerminalMdiContainerInterface; + +struct _TerminalMdiContainerInterface { + GTypeInterface parent_iface; + + /* vfuncs */ + void (* add_screen) (TerminalMdiContainer *container, + TerminalScreen *screen, + int position); + void (* remove_screen) (TerminalMdiContainer *container, + TerminalScreen *screen); + TerminalScreen * (* get_active_screen) (TerminalMdiContainer *container); + void (* set_active_screen) (TerminalMdiContainer *container, + TerminalScreen *screen); + GList * (* list_screens) (TerminalMdiContainer *container); + GList * (* list_screen_containers) (TerminalMdiContainer *container); + int (* get_n_screens) (TerminalMdiContainer *container); + int (* get_active_screen_num) (TerminalMdiContainer *container); + void (* set_active_screen_num) (TerminalMdiContainer *container, + int position); + void (* reorder_screen) (TerminalMdiContainer *container, + TerminalScreen *screen, + int new_position); + + /* signals */ + void (* screen_added) (TerminalMdiContainer *container, + TerminalScreen *screen); + void (* screen_removed) (TerminalMdiContainer *container, + TerminalScreen *screen); + void (* screen_switched) (TerminalMdiContainer *container, + TerminalScreen *old_active_screen, + TerminalScreen *new_active_screen); + void (* screens_reordered) (TerminalMdiContainer *container); + void (* screen_close_request) (TerminalMdiContainer *container, + TerminalScreen *screen); +}; + +GType terminal_mdi_container_get_type (void); + +void terminal_mdi_container_add_screen (TerminalMdiContainer *container, + TerminalScreen *screen, + int position); + +void terminal_mdi_container_remove_screen (TerminalMdiContainer *container, + TerminalScreen *screen); + +TerminalScreen *terminal_mdi_container_get_active_screen (TerminalMdiContainer *container); + +void terminal_mdi_container_set_active_screen (TerminalMdiContainer *container, + TerminalScreen *screen); + +void terminal_mdi_container_set_active_screen_num (TerminalMdiContainer *container, + int position); + +GList *terminal_mdi_container_list_screens (TerminalMdiContainer *container); + +GList *terminal_mdi_container_list_screen_containers (TerminalMdiContainer *container); + +int terminal_mdi_container_get_n_screens (TerminalMdiContainer *container); + +int terminal_mdi_container_get_active_screen_num (TerminalMdiContainer *container); + +void terminal_mdi_container_reorder_screen (TerminalMdiContainer *container, + TerminalScreen *screen, + int new_position); + +void terminal_mdi_container_change_screen (TerminalMdiContainer *container, + int change); + +G_END_DECLS + +#endif /* TERMINAL_MDI_CONTAINER_H */ diff --git a/src/terminal-menu-button.cc b/src/terminal-menu-button.cc new file mode 100644 index 0000000..30d1c7c --- /dev/null +++ b/src/terminal-menu-button.cc @@ -0,0 +1,149 @@ +/* + * Copyright © 2017 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 + * MERCHANMENUILITY 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 "terminal-menu-button.hh" +#include "terminal-intl.hh" +#include "terminal-libgsystem.hh" + +/* All this just because GtkToggleButton:toggled is RUN_FIRST (and the + * notify::active comes after the toggled signal). :-( + */ + +enum +{ + UPDATE_MENU, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; + +static void popup_menu_selection_done_cb (GtkMenu *menu, + GtkMenuButton *button); + +/* The menu button sets itself insensitive when it has no menu. + * Work around this by using an empty menu. + */ +static void +set_empty_menu (GtkMenuButton *button) +{ + gs_unref_object GMenu *menu = g_menu_new (); + gtk_menu_button_set_menu_model (button, G_MENU_MODEL (menu)); +} + +static void +disconnect_popup_menu (GtkMenuButton *button) +{ + GtkMenu *popup_menu = gtk_menu_button_get_popup (button); + + if (popup_menu) + g_signal_handlers_disconnect_by_func(popup_menu, + (void*)popup_menu_selection_done_cb, + button); +} + +static void +popup_menu_selection_done_cb (GtkMenu *menu, + GtkMenuButton *button) +{ + disconnect_popup_menu (button); + set_empty_menu (button); +} + +/* Class implementation */ + +G_DEFINE_TYPE (TerminalMenuButton, terminal_menu_button, GTK_TYPE_MENU_BUTTON); + +static void +terminal_menu_button_init (TerminalMenuButton *button_) +{ + GtkButton *button = GTK_BUTTON (button_); + GtkMenuButton *menu_button = GTK_MENU_BUTTON (button_); + + gtk_button_set_relief (button, GTK_RELIEF_NONE); + gtk_button_set_focus_on_click (button, FALSE); + gtk_menu_button_set_use_popover (menu_button, FALSE); + set_empty_menu (menu_button); +} + +static void +terminal_menu_button_toggled (GtkToggleButton *button) +{ + gboolean active = gtk_toggle_button_get_active (button); /* this is already the new state */ + + /* On activate, update the menu */ + if (active) + g_signal_emit (button, signals[UPDATE_MENU], 0); + + GTK_TOGGLE_BUTTON_CLASS (terminal_menu_button_parent_class)->toggled (button); +} + +static void +terminal_menu_button_update_menu (TerminalMenuButton *button) +{ + GtkMenuButton *gtk_button = GTK_MENU_BUTTON (button); + GtkMenu *popup_menu = gtk_menu_button_get_popup (gtk_button); + + if (popup_menu) + g_signal_connect (popup_menu, "selection-done", + G_CALLBACK (popup_menu_selection_done_cb), button); +} + +static void +terminal_menu_button_dispose (GObject *object) +{ + disconnect_popup_menu (GTK_MENU_BUTTON (object)); + + G_OBJECT_CLASS (terminal_menu_button_parent_class)->dispose (object); +} + +static void +terminal_menu_button_class_init (TerminalMenuButtonClass *klass) +{ + klass->update_menu = terminal_menu_button_update_menu; + + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->dispose = terminal_menu_button_dispose; + + GtkToggleButtonClass *toggle_button_class = GTK_TOGGLE_BUTTON_CLASS (klass); + toggle_button_class->toggled = terminal_menu_button_toggled; + + signals[UPDATE_MENU] = + g_signal_new (I_("update-menu"), + G_OBJECT_CLASS_TYPE (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (TerminalMenuButtonClass, update_menu), + nullptr, nullptr, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, + 0); +} + +/* public API */ + +/** + * terminal_menu_button_new: + * + * Returns: a new #TerminalMenuButton + */ +GtkWidget * +terminal_menu_button_new (void) +{ + return reinterpret_cast<GtkWidget*> + (g_object_new (TERMINAL_TYPE_MENU_BUTTON, nullptr)); +} diff --git a/src/terminal-menu-button.hh b/src/terminal-menu-button.hh new file mode 100644 index 0000000..86197ab --- /dev/null +++ b/src/terminal-menu-button.hh @@ -0,0 +1,57 @@ +/* + * Copyright © 2008, 2017 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 + * MERCHANMENUILITY 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/>. + */ + +#ifndef TERMINAL_MENU_BUTTON_H +#define TERMINAL_MENU_BUTTON_H + +#include <gtk/gtk.h> + +#include "terminal-screen.hh" + +G_BEGIN_DECLS + +#define TERMINAL_TYPE_MENU_BUTTON (terminal_menu_button_get_type ()) +#define TERMINAL_MENU_BUTTON(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), TERMINAL_TYPE_MENU_BUTTON, TerminalMenuButton)) +#define TERMINAL_MENU_BUTTON_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), TERMINAL_TYPE_MENU_BUTTON, TerminalMenuButtonClass)) +#define TERMINAL_IS_MENU_BUTTON(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), TERMINAL_TYPE_MENU_BUTTON)) +#define TERMINAL_IS_MENU_BUTTON_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), TERMINAL_TYPE_MENU_BUTTON)) +#define TERMINAL_MENU_BUTTON_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), TERMINAL_TYPE_MENU_BUTTON, TerminalMenuButtonClass)) + +typedef struct _TerminalMenuButton TerminalMenuButton; +typedef struct _TerminalMenuButtonClass TerminalMenuButtonClass; +typedef struct _TerminalMenuButtonPrivate TerminalMenuButtonPrivate; + +struct _TerminalMenuButton +{ + GtkMenuButton parent_instance; +}; + +struct _TerminalMenuButtonClass +{ + GtkMenuButtonClass parent_class; + + /* Signals */ + void (* update_menu) (TerminalMenuButton *menu_button); +}; + +GType terminal_menu_button_get_type (void); + +GtkWidget *terminal_menu_button_new (void); + +G_END_DECLS + +#endif /* !TERMINAL_MENU_BUTTON_H */ diff --git a/src/terminal-menubar.ui.in b/src/terminal-menubar.ui.in new file mode 100644 index 0000000..794d92d --- /dev/null +++ b/src/terminal-menubar.ui.in @@ -0,0 +1,248 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright © 2012, 2017 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/>. +--> +<interface> + <menu id="menubar"> + <submenu> + <WITH_MNEMONIC><attribute name="label" translatable="yes">_File</attribute></WITH_MNEMONIC> + <WITHOUT_MNEMONIC><attribute name="label" translatable="yes">File</attribute></WITHOUT_MNEMONIC> + <section id="new-terminal-section" /> + <section> + <item> + <attribute name="label" translatable="yes">_Save Contents…</attribute> + <attribute name="action">win.save-contents</attribute> + <attribute name="hidden-when">action-missing</attribute> + </item> + <item> + <attribute name="label" translatable="yes">_Export…</attribute> + <attribute name="action">win.export</attribute> + <attribute name="hidden-when">action-missing</attribute> + </item> + <item> + <attribute name="label" translatable="yes">_Print…</attribute> + <attribute name="action">win.print</attribute> + <attribute name="hidden-when">action-missing</attribute> + </item> + </section> + <section> + <item> + <attribute name="label" translatable="yes">C_lose Tab</attribute> + <attribute name="action">win.close</attribute> + <attribute name="target">tab</attribute> + </item> + <item> + <attribute name="label" translatable="yes">_Close Window</attribute> + <attribute name="action">win.close</attribute> + <attribute name="target">window</attribute> + </item> + </section> + </submenu> + <submenu> + <WITH_MNEMONIC><attribute name="label" translatable="yes">_Edit</attribute></WITH_MNEMONIC> + <WITHOUT_MNEMONIC><attribute name="label" translatable="yes">Edit</attribute></WITHOUT_MNEMONIC> + <section> + <item> + <attribute name="label" translatable="yes">_Copy</attribute> + <attribute name="action">win.copy</attribute> + <attribute name="target">text</attribute> + </item> + <item> + <attribute name="label" translatable="yes">Copy as _HTML</attribute> + <attribute name="action">win.copy</attribute> + <attribute name="target">html</attribute> + </item> + <item> + <attribute name="label" translatable="yes">_Paste</attribute> + <attribute name="action">win.paste-text</attribute> + </item> + <item> + <attribute name="label" translatable="yes">Paste as _Filenames</attribute> + <attribute name="action">win.paste-uris</attribute> + <attribute name="hidden-when">action-disabled</attribute> + </item> + </section> + <section> + <item> + <attribute name="label" translatable="yes">Select _All</attribute> + <attribute name="action">win.select-all</attribute> + </item> + </section> + <section> + <item> + <attribute name="label" translatable="yes">P_references</attribute> + <attribute name="action">win.edit-preferences</attribute> + </item> + </section> + </submenu> + <submenu> + <WITH_MNEMONIC><attribute name="label" translatable="yes">_View</attribute></WITH_MNEMONIC> + <WITHOUT_MNEMONIC><attribute name="label" translatable="yes">View</attribute></WITHOUT_MNEMONIC> + <section> + <item> + <attribute name="label" translatable="yes">Show _Menubar</attribute> + <attribute name="action">win.menubar-visible</attribute> + <attribute name="hidden-when">action-disabled</attribute> + </item> + <item> + <attribute name="label" translatable="yes">_Full Screen</attribute> + <attribute name="action">win.fullscreen</attribute> + </item> + </section> + <section> + <item> + <attribute name="label" translatable="yes">Zoom _In</attribute> + <attribute name="action">win.zoom-in</attribute> + </item> + <item> + <attribute name="label" translatable="yes">_Normal Size</attribute> + <attribute name="action">win.zoom-normal</attribute> + </item> + <item> + <attribute name="label" translatable="yes">Zoom _Out</attribute> + <attribute name="action">win.zoom-out</attribute> + </item> + </section> + </submenu> + <submenu> + <WITH_MNEMONIC><attribute name="label" translatable="yes">_Search</attribute></WITH_MNEMONIC> + <WITHOUT_MNEMONIC><attribute name="label" translatable="yes">Search</attribute></WITHOUT_MNEMONIC> + <section> + <item> + <attribute name="label" translatable="yes">_Find…</attribute> + <attribute name="action">win.find</attribute> + </item> + <item> + <attribute name="label" translatable="yes">Find _Next</attribute> + <attribute name="action">win.find-forward</attribute> + </item> + <item> + <attribute name="label" translatable="yes">Find _Previous</attribute> + <attribute name="action">win.find-backward</attribute> + </item> + <item> + <attribute name="label" translatable="yes">_Clear Highlight</attribute> + <attribute name="action">win.find-clear</attribute> + </item> + </section> + </submenu> + <submenu> + <WITH_MNEMONIC><attribute name="label" translatable="yes">_Terminal</attribute></WITH_MNEMONIC> + <WITHOUT_MNEMONIC><attribute name="label" translatable="yes">Terminal</attribute></WITHOUT_MNEMONIC> + <section> + <section id="set-profile-section" /> + <item> + <attribute name="label" translatable="yes">Set _Title…</attribute> + <attribute name="action">win.set-title</attribute> + <attribute name="hidden-when">action-missing</attribute> + </item> + </section> + <section> + <item> + <attribute name="label" translatable="yes">Read-_Only</attribute> + <attribute name="action">win.read-only</attribute> + </item> + </section> + <section> + <item> + <attribute name="label" translatable="yes">_Reset</attribute> + <attribute name="action">win.reset</attribute> + <attribute name="target" type="b">false</attribute> + </item> + <item> + <attribute name="label" translatable="yes">Reset and C_lear</attribute> + <attribute name="action">win.reset</attribute> + <attribute name="target" type="b">true</attribute> + </item> + </section> + <section> + <item> + <attribute name="label" translatable="yes">_1. 80×24</attribute> + <attribute name="action">win.size-to</attribute> + <attribute name="target" type="(uu)">(80, 24)</attribute> + </item> + <item> + <attribute name="label" translatable="yes">_2. 80×43</attribute> + <attribute name="action">win.size-to</attribute> + <attribute name="target" type="(uu)">(80, 43)</attribute> + </item> + <item> + <attribute name="label" translatable="yes">_3. 132×24</attribute> + <attribute name="action">win.size-to</attribute> + <attribute name="target" type="(uu)">(132, 24)</attribute> + </item> + <item> + <attribute name="label" translatable="yes">_4. 132×43</attribute> + <attribute name="action">win.size-to</attribute> + <attribute name="target" type="(uu)">(132, 43)</attribute> + </item> + </section> + </submenu> + <submenu> + <WITH_MNEMONIC><attribute name="label" translatable="yes">Ta_bs</attribute></WITH_MNEMONIC> + <WITHOUT_MNEMONIC><attribute name="label" translatable="yes">Tabs</attribute></WITHOUT_MNEMONIC> + <attribute name="action">win.tabs-menu</attribute> + <attribute name="hidden-when">action-disabled</attribute> + <section> + <item> + <attribute name="label" translatable="yes">_Previous Tab</attribute> + <attribute name="action">win.tab-switch-left</attribute> + </item> + <item> + <attribute name="label" translatable="yes">_Next Tab</attribute> + <attribute name="action">win.tab-switch-right</attribute> + </item> + </section> + <section> + <item> + <attribute name="label" translatable="yes">Move Terminal _Left</attribute> + <attribute name="action">win.tab-move-left</attribute> + </item> + <item> + <attribute name="label" translatable="yes">Move Terminal _Right</attribute> + <attribute name="action">win.tab-move-right</attribute> + </item> + </section> + <section> + <item> + <attribute name="label" translatable="yes">_Detach Terminal</attribute> + <attribute name="action">win.tab-detach</attribute> + </item> + </section> + </submenu> + <submenu> + <WITH_MNEMONIC><attribute name="label" translatable="yes">_Help</attribute></WITH_MNEMONIC> + <WITHOUT_MNEMONIC><attribute name="label" translatable="yes">Help</attribute></WITHOUT_MNEMONIC> + <section> + <item> + <attribute name="label" translatable="yes">_Contents</attribute> + <attribute name="action">win.help</attribute> + </item> + <item> + <attribute name="label" translatable="yes">_About</attribute> + <attribute name="action">win.about</attribute> + </item> + </section> + <section> + <item> + <attribute name="label" translatable="yes">_Inspector</attribute> + <attribute name="action">win.inspector</attribute> + <attribute name="hidden-when">action-disabled</attribute> + </item> + </section> + </submenu> + </menu> +</interface> diff --git a/src/terminal-nautilus.cc b/src/terminal-nautilus.cc new file mode 100644 index 0000000..34968e5 --- /dev/null +++ b/src/terminal-nautilus.cc @@ -0,0 +1,776 @@ +/* + * Copyright (C) 2004, 2005 Free Software Foundation, Inc. + * Copyright © 2011 Christian Persch + * Author: Christian Neumair <chris@gnome-de.org> + * + * 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 <glib.h> +#include <glib/gi18n-lib.h> +#include <gio/gio.h> + +/* Work around https://gitlab.gnome.org/GNOME/nautilus/-/issues/1884 */ +extern "C" { +#include <nautilus-extension.h> +} + +#include <errno.h> +#include <fcntl.h> +#include <stdlib.h> +#include <string.h> + +#include "terminal-i18n.hh" +#include "terminal-client-utils.hh" +#include "terminal-defines.hh" +#include "terminal-gdbus-generated.h" + +/* Nautilus extension class */ + +#undef TERMINAL_NAUTILUS + +#define TERMINAL_TYPE_NAUTILUS (terminal_nautilus_get_type ()) +#define TERMINAL_NAUTILUS(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), TERMINAL_TYPE_NAUTILUS, TerminalNautilus)) +#define TERMINAL_NAUTILUS_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), TERMINAL_TYPE_NAUTILUS, TerminalNautilusClass)) +#define TERMINAL_IS_NAUTILUS(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), TERMINAL_TYPE_NAUTILUS)) +#define TERMINAL_IS_NAUTILUS_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), TERMINAL_TYPE_NAUTILUS)) +#define TERMINAL_NAUTILUS_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), TERMINAL_TYPE_NAUTILUS, TerminalNautilusClass)) + +typedef struct _TerminalNautilus TerminalNautilus; +typedef struct _TerminalNautilusClass TerminalNautilusClass; + +struct _TerminalNautilus { + GObject parent_instance; + + GSettings *lockdown_prefs; +}; + +struct _TerminalNautilusClass { + GObjectClass parent_class; +}; + +static GType terminal_nautilus_get_type (void); + +/* Nautilus menu item class */ + +#define TERMINAL_TYPE_NAUTILUS_MENU_ITEM (terminal_nautilus_menu_item_get_type ()) +#define TERMINAL_NAUTILUS_MENU_ITEM(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), TERMINAL_TYPE_NAUTILUS_MENU_ITEM, TerminalNautilusMenuItem)) +#define TERMINAL_NAUTILUS_MENU_ITEM_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), TERMINAL_TYPE_NAUTILUS_MENU_ITEM, TerminalNautilusMenuItemClass)) +#define TERMINAL_IS_NAUTILUS_MENU_ITEM(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), TERMINAL_TYPE_NAUTILUS_MENU_ITEM)) +#define TERMINAL_IS_NAUTILUS_MENU_ITEM_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), TERMINAL_TYPE_NAUTILUS_MENU_ITEM)) +#define TERMINAL_NAUTILUS_MENU_ITEM_GET_CLASS(o)(G_TYPE_INSTANCE_GET_CLASS ((o), TERMINAL_TYPE_NAUTILUS_MENU_ITEM, TerminalNautilusMenuItemClass)) + +typedef struct _TerminalNautilusMenuItem TerminalNautilusMenuItem; +typedef struct _TerminalNautilusMenuItemClass TerminalNautilusMenuItemClass; + +struct _TerminalNautilusMenuItem { + NautilusMenuItem parent_instance; + + TerminalNautilus *nautilus; + NautilusFileInfo *file_info; + gboolean remote_terminal; +}; + +struct _TerminalNautilusMenuItemClass { + NautilusMenuItemClass parent_class; +}; + +static GType terminal_nautilus_menu_item_get_type (void); + +/* --- */ + +#define TERMINAL_ICON_NAME "org.gnome.Terminal" + +typedef enum { + /* local files. Always open "conventionally", i.e. cd and spawn. */ + FILE_INFO_LOCAL, + FILE_INFO_DESKTOP, + /* SFTP: Shell terminals are opened "remote" (i.e. with ssh client), + * commands are executed like OTHER. + */ + FILE_INFO_SFTP, + /* OTHER: Terminals and commands are opened by mapping the URI back + * to ~/.gvfs, i.e. to the GVFS FUSE bridge. + */ + FILE_INFO_OTHER +} TerminalFileInfo; + +static TerminalFileInfo +get_terminal_file_info_from_uri (const char *uri) +{ + TerminalFileInfo ret; + char *uri_scheme; + + uri_scheme = g_uri_parse_scheme (uri); + + if (uri_scheme == nullptr) { + ret = FILE_INFO_OTHER; + } else if (strcmp (uri_scheme, "file") == 0) { + ret = FILE_INFO_LOCAL; + } else if (strcmp (uri_scheme, "x-nautilus-desktop") == 0) { + ret = FILE_INFO_DESKTOP; + } else if (strcmp (uri_scheme, "sftp") == 0 || + strcmp (uri_scheme, "ssh") == 0) { + ret = FILE_INFO_SFTP; + } else { + ret = FILE_INFO_OTHER; + } + + g_free (uri_scheme); + + return ret; +} + +/* Helpers */ + +#define NAUTILUS_SETTINGS_SCHEMA "org.gnome.Nautilus" +#define GNOME_DESKTOP_LOCKDOWN_SETTINGS_SCHEMA "org.gnome.desktop.lockdown" + +/* a very simple URI parsing routine from Launchpad #333462, until GLib supports URI parsing (GNOME #489862) */ +#define SFTP_PREFIX "sftp://" +static void +parse_sftp_uri (GFile *file, + char **user, + char **host, + unsigned int *port, + char **path) +{ + char *tmp, *save; + char *uri; + + uri = g_file_get_uri (file); + g_assert (uri != nullptr); + save = uri; + + *path = nullptr; + *user = nullptr; + *host = nullptr; + *port = 0; + + /* skip intial 'sftp:// prefix */ + g_assert (!strncmp (uri, SFTP_PREFIX, strlen (SFTP_PREFIX))); + uri += strlen (SFTP_PREFIX); + + /* cut out the path */ + tmp = strchr (uri, '/'); + if (tmp != nullptr) { + *path = g_uri_unescape_string (tmp, "/"); + *tmp = '\0'; + } + + /* read the username - it ends with @ */ + tmp = strchr (uri, '@'); + if (tmp != nullptr) { + *tmp++ = '\0'; + + *user = strdup (uri); + if (strchr (*user, ':') != nullptr) { + /* chop the password */ + *(strchr (*user, ':')) = '\0'; + } + + uri = tmp; + } + + /* now read the port, starts with : */ + tmp = strchr (uri, ':'); + if (tmp != nullptr) { + *tmp++ = '\0'; + *port = atoi (tmp); /*FIXME: getservbyname*/ + } + + /* what is left is the host */ + *host = strdup (uri); + g_free (save); +} + +static char ** +ssh_argv (const char *uri, + int *argcp) +{ + GFile *file; + char **argv; + int argc; + char *host_name, *path, *user_name, *quoted_path; + guint host_port; + + g_assert (uri != nullptr); + + argv = g_new0 (char *, 9); + argc = 0; + argv[argc++] = g_strdup ("ssh"); + argv[argc++] = g_strdup ("-t"); + + file = g_file_new_for_uri (uri); + parse_sftp_uri (file, &user_name, &host_name, &host_port, &path); + g_object_unref (file); + + if (user_name != nullptr) { + argv[argc++ ]= g_strdup_printf ("%s@%s", user_name, host_name); + g_free (host_name); + g_free (user_name); + } else { + argv[argc++] = host_name; + } + + if (host_port != 0) { + argv[argc++] = g_strdup ("-p"); + argv[argc++] = g_strdup_printf ("%u", host_port); + } + + /* FIXME to we have to consider the remote file encoding? */ + quoted_path = g_shell_quote (path); + + /* login shell */ + argv[argc++] = g_strdup_printf ("cd %s && exec $SHELL -l", quoted_path); + + g_free (path); + g_free (quoted_path); + + *argcp = argc; + return argv; +} + +static gboolean +terminal_locked_down (TerminalNautilus *nautilus) +{ + return g_settings_get_boolean (nautilus->lockdown_prefs, + "disable-command-line"); +} + +/* used to determine for remote URIs whether GVFS is capable of mapping them to ~/.gvfs */ +static gboolean +uri_has_local_path (const char *uri) +{ + GFile *file; + char *path; + gboolean ret; + + file = g_file_new_for_uri (uri); + path = g_file_get_path (file); + + ret = (path != nullptr); + + g_free (path); + g_object_unref (file); + + return ret; +} + +/* Nautilus menu item class */ + +namespace { + +typedef struct { + TerminalNautilus *nautilus; + guint32 timestamp; + char *path; + char *uri; + TerminalFileInfo info; + gboolean remote; +} ExecData; + +} // anon namespace + +static void +exec_data_free (ExecData *data) +{ + g_object_unref (data->nautilus); + g_free (data->path); + g_free (data->uri); + + g_free (data); +} + +/* FIXME: make this async */ +static gboolean +create_terminal (ExecData *data /* transfer full */) +{ + TerminalFactory *factory; + TerminalReceiver *receiver; + GError *error = nullptr; + GVariantBuilder builder; + char *object_path; + char startup_id[32]; + char **argv; + int argc; + + factory = terminal_factory_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION, + GDBusProxyFlags(G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES | + G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS), + TERMINAL_APPLICATION_ID, + TERMINAL_FACTORY_OBJECT_PATH, + nullptr /* cancellable */, + &error); + if (factory == nullptr) { + g_dbus_error_strip_remote_error (error); + g_printerr ("Error constructing proxy for %s:%s: %s\n", + TERMINAL_APPLICATION_ID, TERMINAL_FACTORY_OBJECT_PATH, + error->message); + g_error_free (error); + exec_data_free (data); + return FALSE; + } + + g_snprintf (startup_id, sizeof (startup_id), "_TIME%u", data->timestamp); + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{sv}")); + + terminal_client_append_create_instance_options (&builder, + nullptr, // display name. FIXMEgtk4? + startup_id, + nullptr, /* activation_token */ + nullptr /* geometry */, + nullptr /* role */, + nullptr /* use default profile */, + nullptr /* use profile encoding */, + nullptr /* title */, + TRUE, /* active */ + FALSE /* maximised */, + FALSE /* fullscreen */); + + if (!terminal_factory_call_create_instance_sync + (factory, + g_variant_builder_end (&builder), + &object_path, + nullptr /* cancellable */, + &error)) { + g_dbus_error_strip_remote_error (error); + g_printerr ("Error creating terminal: %s\n", error->message); + g_error_free (error); + g_object_unref (factory); + exec_data_free (data); + return FALSE; + } + + g_object_unref (factory); + + receiver = terminal_receiver_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION, + GDBusProxyFlags(G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES | + G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS), + TERMINAL_APPLICATION_ID, + object_path, + nullptr /* cancellable */, + &error); + if (receiver == nullptr) { + g_dbus_error_strip_remote_error (error); + g_printerr ("Failed to create proxy for terminal: %s\n", error->message); + g_error_free (error); + g_free (object_path); + exec_data_free (data); + return FALSE; + } + + g_free (object_path); + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{sv}")); + + terminal_client_append_exec_options (&builder, + TRUE, /* pass environment */ + data->path, + nullptr, 0, /* FD array */ + TRUE /* shell */); + + if (data->info == FILE_INFO_SFTP && + data->remote) { + argv = ssh_argv (data->uri, &argc); + } else { + argv = nullptr; argc = 0; + } + + if (!terminal_receiver_call_exec_sync (receiver, + g_variant_builder_end (&builder), + g_variant_new_bytestring_array ((const char * const *) argv, argc), + nullptr /* in FD list */, + nullptr /* out FD list */, + nullptr /* cancellable */, + &error)) { + g_dbus_error_strip_remote_error (error); + g_printerr ("Error: %s\n", error->message); + g_error_free (error); + g_strfreev (argv); + g_object_unref (receiver); + exec_data_free (data); + return FALSE; + } + + g_strfreev (argv); + + exec_data_free (data); + + g_object_unref (receiver); + + return TRUE; +} + +static void +terminal_nautilus_menu_item_activate (NautilusMenuItem *item) +{ + TerminalNautilusMenuItem *menu_item = TERMINAL_NAUTILUS_MENU_ITEM (item); + TerminalNautilus *nautilus = menu_item->nautilus; + char *uri, *path; + TerminalFileInfo info; + ExecData *data; + + uri = nautilus_file_info_get_activation_uri (menu_item->file_info); + if (uri == nullptr) + return; + + path = nullptr; + info = get_terminal_file_info_from_uri (uri); + + switch (info) { + case FILE_INFO_LOCAL: + path = g_filename_from_uri (uri, nullptr, nullptr); + break; + + case FILE_INFO_DESKTOP: + path = g_strdup (g_get_home_dir ()); + break; + + case FILE_INFO_SFTP: + if (menu_item->remote_terminal) + break; + + [[fallthrough]]; + case FILE_INFO_OTHER: { + GFile *file; + + /* map back remote URI to local path */ + file = g_file_new_for_uri (uri); + path = g_file_get_path (file); + g_object_unref (file); + break; + } + + default: + g_assert_not_reached (); + } + + if (path == nullptr && (info != FILE_INFO_SFTP || !menu_item->remote_terminal)) { + g_free (uri); + return; + } + + data = g_new (ExecData, 1); + data->nautilus = (TerminalNautilus*)g_object_ref (nautilus); + data->timestamp = 0; // GDK_CURRENT_TIME + data->path = path; + data->uri = uri; + data->info = info; + data->remote = menu_item->remote_terminal; + + create_terminal (data); +} + +G_DEFINE_DYNAMIC_TYPE (TerminalNautilusMenuItem, terminal_nautilus_menu_item, NAUTILUS_TYPE_MENU_ITEM) + +static void +terminal_nautilus_menu_item_init (TerminalNautilusMenuItem *nautilus_menu_item) +{ +} + +static void +terminal_nautilus_menu_item_dispose (GObject *object) +{ + TerminalNautilusMenuItem *menu_item = TERMINAL_NAUTILUS_MENU_ITEM (object); + + if (menu_item->file_info != nullptr) { + g_object_unref (menu_item->file_info); + menu_item->file_info = nullptr; + } + if (menu_item->nautilus != nullptr) { + g_object_unref (menu_item->nautilus); + menu_item->nautilus = nullptr; + } + + G_OBJECT_CLASS (terminal_nautilus_menu_item_parent_class)->dispose (object); +} + +static void +terminal_nautilus_menu_item_class_init (TerminalNautilusMenuItemClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + NautilusMenuItemClass *menu_item_class = NAUTILUS_MENU_ITEM_CLASS (klass); + + gobject_class->dispose = terminal_nautilus_menu_item_dispose; + + menu_item_class->activate = terminal_nautilus_menu_item_activate; +} + +static void +terminal_nautilus_menu_item_class_finalize (TerminalNautilusMenuItemClass *klass) +{ +} + +static NautilusMenuItem * +terminal_nautilus_menu_item_new (TerminalNautilus *nautilus, + NautilusFileInfo *file_info, + TerminalFileInfo terminal_file_info, + gboolean remote_terminal, + gboolean is_file_item) +{ + TerminalNautilusMenuItem *item; + const char *action_name; + const char *name; + const char *tooltip; + + if (is_file_item) { + action_name = remote_terminal ? "TerminalNautilus:OpenRemote" + : "TerminalNautilus:OpenLocal"; + } else { + action_name = remote_terminal ? "TerminalNautilus:OpenFolderRemote" + : "TerminalNautilus:OpenFolderLocal"; + } + + switch (terminal_file_info) { + case FILE_INFO_SFTP: + if (remote_terminal) { + name = _("Open in _Remote Terminal"); + } else { + name = _("Open in _Local Terminal"); + } + + if (is_file_item) { + tooltip = _("Open the currently selected folder in a terminal"); + } else { + tooltip = _("Open the currently open folder in a terminal"); + } + break; + + case FILE_INFO_LOCAL: + case FILE_INFO_OTHER: + name = _("Open in T_erminal"); + + if (is_file_item) { + tooltip = _("Open the currently selected folder in a terminal"); + } else { + tooltip = _("Open the currently open folder in a terminal"); + } + break; + + case FILE_INFO_DESKTOP: + name = _("Open T_erminal"); + tooltip = _("Open a terminal"); + break; + + default: + g_assert_not_reached (); + } + + item = (TerminalNautilusMenuItem*)g_object_new (TERMINAL_TYPE_NAUTILUS_MENU_ITEM, + "name", action_name, + "label", name, + "tip", tooltip, + "icon", TERMINAL_ICON_NAME, + nullptr); + + item->nautilus = (TerminalNautilus*)g_object_ref (nautilus); + item->file_info = (NautilusFileInfo*)g_object_ref (file_info); + item->remote_terminal = remote_terminal; + + return (NautilusMenuItem *) item; +} + +/* Nautilus extension class implementation */ + +static GList * +terminal_nautilus_get_background_items (NautilusMenuProvider *provider, + NautilusFileInfo *file_info) +{ + TerminalNautilus *nautilus = TERMINAL_NAUTILUS (provider); + gchar *uri; + GList *items; + NautilusMenuItem *item; + TerminalFileInfo terminal_file_info; + + if (terminal_locked_down (nautilus)) + return nullptr; + + uri = nautilus_file_info_get_activation_uri (file_info); + if (uri == nullptr) + return nullptr; + + items = nullptr; + + terminal_file_info = get_terminal_file_info_from_uri (uri); + + + if (terminal_file_info == FILE_INFO_SFTP) { + /* remote SSH location */ + item = terminal_nautilus_menu_item_new (nautilus, + file_info, + terminal_file_info, + TRUE, + FALSE); + items = g_list_append (items, item); + } + + if (terminal_file_info == FILE_INFO_DESKTOP || + uri_has_local_path (uri)) { + /* local locations and remote locations that offer local back-mapping */ + item = terminal_nautilus_menu_item_new (nautilus, + file_info, + terminal_file_info, + FALSE, + FALSE); + items = g_list_append (items, item); + } + + g_free (uri); + + return items; +} + +static GList * +terminal_nautilus_get_file_items (NautilusMenuProvider *provider, + GList *files) +{ + TerminalNautilus *nautilus = TERMINAL_NAUTILUS (provider); + gchar *uri; + GList *items; + NautilusMenuItem *item; + NautilusFileInfo *file_info; + GFileType file_type; + TerminalFileInfo terminal_file_info; + + if (terminal_locked_down (nautilus)) + return nullptr; + + /* Only add items when passed exactly one file */ + if (files == nullptr || files->next != nullptr) + return nullptr; + + file_info = (NautilusFileInfo *) files->data; + file_type = nautilus_file_info_get_file_type (file_info); + if (!nautilus_file_info_is_directory (file_info) && + file_type != G_FILE_TYPE_SHORTCUT && + file_type != G_FILE_TYPE_MOUNTABLE) + return nullptr; + + uri = nautilus_file_info_get_activation_uri (file_info); + if (uri == nullptr) + return nullptr; + + items = nullptr; + + terminal_file_info = get_terminal_file_info_from_uri (uri); + + switch (terminal_file_info) { + case FILE_INFO_LOCAL: + case FILE_INFO_SFTP: + case FILE_INFO_OTHER: + if (terminal_file_info == FILE_INFO_SFTP || + uri_has_local_path (uri)) { + item = terminal_nautilus_menu_item_new (nautilus, + file_info, + terminal_file_info, + terminal_file_info == FILE_INFO_SFTP, + TRUE); + items = g_list_append (items, item); + } + + if (terminal_file_info == FILE_INFO_SFTP && + uri_has_local_path (uri)) { + item = terminal_nautilus_menu_item_new (nautilus, + file_info, + terminal_file_info, + FALSE, + TRUE); + items = g_list_append (items, item); + } + + case FILE_INFO_DESKTOP: + break; + + default: + g_assert_not_reached (); + } + + g_free (uri); + + return items; +} + +static void +terminal_nautilus_menu_provider_interface_init (NautilusMenuProviderInterface *interface) +{ + interface->get_background_items = terminal_nautilus_get_background_items; + interface->get_file_items = terminal_nautilus_get_file_items; +} + +G_DEFINE_DYNAMIC_TYPE_EXTENDED (TerminalNautilus, terminal_nautilus, G_TYPE_OBJECT, 0, + G_IMPLEMENT_INTERFACE_DYNAMIC (NAUTILUS_TYPE_MENU_PROVIDER, + terminal_nautilus_menu_provider_interface_init)) + +static void +terminal_nautilus_init (TerminalNautilus *nautilus) +{ + nautilus->lockdown_prefs = g_settings_new (GNOME_DESKTOP_LOCKDOWN_SETTINGS_SCHEMA); +} + +static void +terminal_nautilus_dispose (GObject *object) +{ + TerminalNautilus *nautilus = TERMINAL_NAUTILUS (object); + + g_clear_object (&nautilus->lockdown_prefs); + + G_OBJECT_CLASS (terminal_nautilus_parent_class)->dispose (object); +} + +static void +terminal_nautilus_class_init (TerminalNautilusClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->dispose = terminal_nautilus_dispose; + + terminal_i18n_init (FALSE); +} + +static void +terminal_nautilus_class_finalize (TerminalNautilusClass *klass) +{ +} + +/* Nautilus extension */ + +static GType type_list[1]; + +#define EXPORT extern "C" __attribute__((__visibility__("default"))) + +EXPORT void +nautilus_module_initialize (GTypeModule *module) +{ + terminal_nautilus_register_type (module); + terminal_nautilus_menu_item_register_type (module); + + type_list[0] = TERMINAL_TYPE_NAUTILUS; +} + +EXPORT void +nautilus_module_shutdown (void) +{ +} + +EXPORT void +nautilus_module_list_types (const GType **types, + int *num_types) +{ + *types = type_list; + *num_types = G_N_ELEMENTS (type_list); +} diff --git a/src/terminal-notebook-menu.ui b/src/terminal-notebook-menu.ui new file mode 100644 index 0000000..d5c7f41 --- /dev/null +++ b/src/terminal-notebook-menu.ui @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright © 2012 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/>. +--> +<interface> + <menu id="notebook-popup"> + <section> + <item> + <attribute name="label" translatable="yes">Move Terminal _Left</attribute> + <attribute name="action">win.tab-move-left</attribute> + </item> + <item> + <attribute name="label" translatable="yes">Move Terminal _Right</attribute> + <attribute name="action">win.tab-move-right</attribute> + </item> + </section> + <section> + <item> + <attribute name="label" translatable="yes">_Detach Terminal</attribute> + <attribute name="action">win.tab-detach</attribute> + </item> + <item> + <attribute name="label" translatable="yes">Set _Title…</attribute> + <attribute name="action">win.set-title</attribute> + <attribute name="hidden-when">action-missing</attribute> + </item> + </section> + <section> + <item> + <attribute name="label" translatable="yes">C_lose Terminal</attribute> + <attribute name="action">win.close</attribute> + <attribute name="target">tab</attribute> + </item> + </section> + </menu> +</interface> diff --git a/src/terminal-notebook.cc b/src/terminal-notebook.cc new file mode 100644 index 0000000..6686d34 --- /dev/null +++ b/src/terminal-notebook.cc @@ -0,0 +1,596 @@ +/* + * Copyright © 2001 Havoc Pennington + * Copyright © 2002 Red Hat, Inc. + * Copyright © 2008, 2010, 2011, 2012 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 "terminal-notebook.hh" + +#include <gtk/gtk.h> + +#include "terminal-debug.hh" +#include "terminal-app.hh" +#include "terminal-intl.hh" +#include "terminal-mdi-container.hh" +#include "terminal-screen-container.hh" +#include "terminal-tab-label.hh" +#include "terminal-schemas.hh" +#include "terminal-libgsystem.hh" + +#define TERMINAL_NOTEBOOK_GET_PRIVATE(notebook)(G_TYPE_INSTANCE_GET_PRIVATE ((notebook), TERMINAL_TYPE_NOTEBOOK, TerminalNotebookPrivate)) + +struct _TerminalNotebookPrivate +{ + TerminalScreen *active_screen; + GtkPolicyType policy; +}; + +enum +{ + PROP_0, + PROP_ACTIVE_SCREEN, + PROP_TAB_POLICY +}; + +#define ACTION_AREA_BORDER_WIDTH (2) +#define ACTION_BUTTON_SPACING (6) + +/* helper functions */ + +static void +update_tab_visibility (TerminalNotebook *notebook, + int change) +{ + TerminalNotebookPrivate *priv = notebook->priv; + GtkNotebook *gtk_notebook = GTK_NOTEBOOK (notebook); + int new_n_pages; + gboolean show_tabs; + + if (gtk_widget_in_destruction (GTK_WIDGET (notebook))) + return; + + new_n_pages = gtk_notebook_get_n_pages (gtk_notebook) + change; + /* Don't do anything if we're going to have zero pages (and thus close the window) */ + if (new_n_pages == 0) + return; + + switch (priv->policy) { + case GTK_POLICY_ALWAYS: + show_tabs = TRUE; + break; + case GTK_POLICY_AUTOMATIC: + show_tabs = new_n_pages > 1; + break; + case GTK_POLICY_NEVER: + case GTK_POLICY_EXTERNAL: + default: + show_tabs = FALSE; + break; + } + + gtk_notebook_set_show_tabs (gtk_notebook, show_tabs); +} + +static void +close_button_clicked_cb (TerminalTabLabel *tab_label, + gpointer user_data) +{ + TerminalScreen *screen; + TerminalNotebook *notebook; + + screen = terminal_tab_label_get_screen (tab_label); + + /* notebook is not passed as user_data because it can change during DND + * and the close button is not notified about that, see bug 731998. + */ + notebook = TERMINAL_NOTEBOOK (gtk_widget_get_ancestor (GTK_WIDGET (screen), + TERMINAL_TYPE_NOTEBOOK)); + + if (notebook != nullptr) + g_signal_emit_by_name (notebook, "screen-close-request", screen); +} + + +static void +remove_reorder_bindings (GtkBindingSet *binding_set, + guint keysym) +{ + guint keypad_keysym = keysym - GDK_KEY_Left + GDK_KEY_KP_Left; + gtk_binding_entry_skip (binding_set, keysym, GDK_MOD1_MASK); + gtk_binding_entry_skip (binding_set, keypad_keysym, GDK_MOD1_MASK); +} + +/* TerminalMdiContainer impl */ + +static void +terminal_notebook_add_screen (TerminalMdiContainer *container, + TerminalScreen *screen, + int position) +{ + TerminalNotebook *notebook = TERMINAL_NOTEBOOK (container); + GtkNotebook *gtk_notebook = GTK_NOTEBOOK (notebook); + GtkWidget *screen_container, *tab_label; + + g_warn_if_fail (gtk_widget_get_parent (GTK_WIDGET (screen)) == nullptr); + + screen_container = terminal_screen_container_new (screen); + gtk_widget_show (screen_container); + + update_tab_visibility (notebook, +1); + + tab_label = terminal_tab_label_new (screen); + g_signal_connect (tab_label, "close-button-clicked", + G_CALLBACK (close_button_clicked_cb), nullptr); + + gtk_notebook_insert_page (gtk_notebook, + screen_container, + tab_label, + position); + gtk_container_child_set (GTK_CONTAINER (notebook), + screen_container, + "tab-expand", TRUE, + "tab-fill", TRUE, + nullptr); + gtk_notebook_set_tab_reorderable (gtk_notebook, screen_container, TRUE); +#if 0 + gtk_notebook_set_tab_detachable (gtk_notebook, screen_container, TRUE); +#endif +} + +static void +terminal_notebook_remove_screen (TerminalMdiContainer *container, + TerminalScreen *screen) +{ + TerminalNotebook *notebook = TERMINAL_NOTEBOOK (container); + TerminalScreenContainer *screen_container; + + g_warn_if_fail (gtk_widget_is_ancestor (GTK_WIDGET (screen), GTK_WIDGET (notebook))); + + update_tab_visibility (notebook, -1); + + screen_container = terminal_screen_container_get_from_screen (screen); + gtk_container_remove (GTK_CONTAINER (notebook), + GTK_WIDGET (screen_container)); +} + +static TerminalScreen * +terminal_notebook_get_active_screen (TerminalMdiContainer *container) +{ + TerminalNotebook *notebook = TERMINAL_NOTEBOOK (container); + GtkNotebook *gtk_notebook = GTK_NOTEBOOK (notebook); + GtkWidget *widget; + + widget = gtk_notebook_get_nth_page (gtk_notebook, gtk_notebook_get_current_page (gtk_notebook)); + return terminal_screen_container_get_screen (TERMINAL_SCREEN_CONTAINER (widget)); +} + +static void +terminal_notebook_set_active_screen (TerminalMdiContainer *container, + TerminalScreen *screen) +{ + TerminalNotebook *notebook = TERMINAL_NOTEBOOK (container); + GtkNotebook *gtk_notebook = GTK_NOTEBOOK (notebook); + TerminalScreenContainer *screen_container; + GtkWidget *widget; + + screen_container = terminal_screen_container_get_from_screen (screen); + widget = GTK_WIDGET (screen_container); + + gtk_notebook_set_current_page (gtk_notebook, + gtk_notebook_page_num (gtk_notebook, widget)); +} + +static GList * +terminal_notebook_list_screen_containers (TerminalMdiContainer *container) +{ + /* We are trusting that GtkNotebook will return pages in order */ + return gtk_container_get_children (GTK_CONTAINER (container)); +} + +static GList * +terminal_notebook_list_screens (TerminalMdiContainer *container) +{ + GList *list, *l; + + list = terminal_notebook_list_screen_containers (container); + for (l = list; l != nullptr; l = l->next) + l->data = terminal_screen_container_get_screen ((TerminalScreenContainer *) l->data); + + return list; +} + +static int +terminal_notebook_get_n_screens (TerminalMdiContainer *container) +{ + return gtk_notebook_get_n_pages (GTK_NOTEBOOK (container)); +} + +static int +terminal_notebook_get_active_screen_num (TerminalMdiContainer *container) +{ + return gtk_notebook_get_current_page (GTK_NOTEBOOK (container)); +} + +static void +terminal_notebook_set_active_screen_num (TerminalMdiContainer *container, + int position) +{ + GtkNotebook *gtk_notebook = GTK_NOTEBOOK (container); + + gtk_notebook_set_current_page (gtk_notebook, position); +} + +static void +terminal_notebook_reorder_screen (TerminalMdiContainer *container, + TerminalScreen *screen, + int new_position) +{ + GtkNotebook *notebook = GTK_NOTEBOOK (container); + GtkWidget *child; + int n, pos; + + g_return_if_fail (new_position == 1 || new_position == -1); + + child = GTK_WIDGET (terminal_screen_container_get_from_screen (screen)); + n = gtk_notebook_get_n_pages (notebook); + pos = gtk_notebook_page_num (notebook, child); + + pos += new_position; + gtk_notebook_reorder_child (notebook, child, + pos < 0 ? n - 1 : pos < n ? pos : 0); +} + +static void +terminal_notebook_mdi_iface_init (TerminalMdiContainerInterface *iface) +{ + iface->add_screen = terminal_notebook_add_screen; + iface->remove_screen = terminal_notebook_remove_screen; + iface->get_active_screen = terminal_notebook_get_active_screen; + iface->set_active_screen = terminal_notebook_set_active_screen; + iface->list_screens = terminal_notebook_list_screens; + iface->list_screen_containers = terminal_notebook_list_screen_containers; + iface->get_n_screens = terminal_notebook_get_n_screens; + iface->get_active_screen_num = terminal_notebook_get_active_screen_num; + iface->set_active_screen_num = terminal_notebook_set_active_screen_num; + iface->reorder_screen = terminal_notebook_reorder_screen; +} + +G_DEFINE_TYPE_WITH_CODE (TerminalNotebook, terminal_notebook, GTK_TYPE_NOTEBOOK, + G_IMPLEMENT_INTERFACE (TERMINAL_TYPE_MDI_CONTAINER, terminal_notebook_mdi_iface_init)) + +/* GtkNotebookClass impl */ + +static void +terminal_notebook_switch_page (GtkNotebook *gtk_notebook, + GtkWidget *child, + guint page_num) +{ + TerminalNotebook *notebook = TERMINAL_NOTEBOOK (gtk_notebook); + TerminalNotebookPrivate *priv = notebook->priv; + TerminalScreen *screen, *old_active_screen; + + GTK_NOTEBOOK_CLASS (terminal_notebook_parent_class)->switch_page (gtk_notebook, child, page_num); + + screen = terminal_screen_container_get_screen (TERMINAL_SCREEN_CONTAINER (child)); + + old_active_screen = priv->active_screen; + if (screen == old_active_screen) + return; + + /* Workaround to remove gtknotebook's feature of computing its size based on + * all pages. When the widget is hidden, its size will not be taken into + * account. + * FIXME! + */ +// if (old_active_screen) +// gtk_widget_hide (GTK_WIDGET (terminal_screen_container_get_from_screen (old_active_screen))); + /* Make sure that the widget is no longer hidden due to the workaround */ +// if (child) +// gtk_widget_show (child); + if (old_active_screen) + gtk_widget_hide (GTK_WIDGET (old_active_screen)); + if (screen) + gtk_widget_show (GTK_WIDGET (screen)); + + priv->active_screen = screen; + + g_signal_emit_by_name (notebook, "screen-switched", old_active_screen, screen); + g_object_notify (G_OBJECT (notebook), "active-screen"); +} + +static void +terminal_notebook_page_added (GtkNotebook *gtk_notebook, + GtkWidget *child, + guint page_num) +{ + TerminalNotebook *notebook = TERMINAL_NOTEBOOK (gtk_notebook); + void (* page_added) (GtkNotebook *, GtkWidget *, guint) = + GTK_NOTEBOOK_CLASS (terminal_notebook_parent_class)->page_added; + + if (page_added) + page_added (gtk_notebook, child, page_num); + + update_tab_visibility (notebook, 0); + g_signal_emit_by_name (gtk_notebook, "screen-added", + terminal_screen_container_get_screen (TERMINAL_SCREEN_CONTAINER (child))); +} + +static void +terminal_notebook_page_removed (GtkNotebook *gtk_notebook, + GtkWidget *child, + guint page_num) +{ + TerminalNotebook *notebook = TERMINAL_NOTEBOOK (gtk_notebook); + void (* page_removed) (GtkNotebook *, GtkWidget *, guint) = + GTK_NOTEBOOK_CLASS (terminal_notebook_parent_class)->page_removed; + + if (page_removed) + page_removed (gtk_notebook, child, page_num); + + update_tab_visibility (notebook, 0); + g_signal_emit_by_name (gtk_notebook, "screen-removed", + terminal_screen_container_get_screen (TERMINAL_SCREEN_CONTAINER (child))); +} + +static void +terminal_notebook_page_reordered (GtkNotebook *notebook, + GtkWidget *child, + guint page_num) +{ + void (* page_reordered) (GtkNotebook *, GtkWidget *, guint) = + GTK_NOTEBOOK_CLASS (terminal_notebook_parent_class)->page_reordered; + + if (page_reordered) + page_reordered (notebook, child, page_num); + + g_signal_emit_by_name (notebook, "screens-reordered"); +} + +static GtkNotebook * +terminal_notebook_create_window (GtkNotebook *notebook, + GtkWidget *page, + gint x, + gint y) +{ + return GTK_NOTEBOOK_CLASS (terminal_notebook_parent_class)->create_window (notebook, page, x, y); +} + +/* GtkWidgetClass impl */ + +static void +terminal_notebook_grab_focus (GtkWidget *widget) +{ + TerminalScreen *screen; + + screen = terminal_mdi_container_get_active_screen (TERMINAL_MDI_CONTAINER (widget)); + gtk_widget_grab_focus (GTK_WIDGET (screen)); +} + +/* GObjectClass impl */ + +static void +terminal_notebook_init (TerminalNotebook *notebook) +{ + TerminalNotebookPrivate *priv; + + priv = notebook->priv = TERMINAL_NOTEBOOK_GET_PRIVATE (notebook); + + priv->active_screen = nullptr; + priv->policy = GTK_POLICY_AUTOMATIC; +} + +static void +terminal_notebook_constructed (GObject *object) +{ + GSettings *settings; + GtkWidget *widget = GTK_WIDGET (object); + GtkNotebook *notebook = GTK_NOTEBOOK (object); + + G_OBJECT_CLASS (terminal_notebook_parent_class)->constructed (object); + + settings = terminal_app_get_global_settings (terminal_app_get ()); + + update_tab_visibility (TERMINAL_NOTEBOOK (notebook), 0); + g_settings_bind (settings, + TERMINAL_SETTING_TAB_POLICY_KEY, + object, + "tab-policy", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_NO_SENSITIVITY)); + + g_settings_bind (settings, + TERMINAL_SETTING_TAB_POSITION_KEY, + object, + "tab-pos", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_NO_SENSITIVITY)); + + gtk_notebook_set_scrollable (notebook, TRUE); + gtk_notebook_set_show_border (notebook, FALSE); + gtk_notebook_set_group_name (notebook, I_("gnome-terminal-window")); + + /* Necessary for scroll events */ + gtk_widget_add_events (widget, GDK_SCROLL_MASK); +} + +static void +terminal_notebook_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + TerminalMdiContainer *mdi_container = TERMINAL_MDI_CONTAINER (object); + + switch (prop_id) { + case PROP_ACTIVE_SCREEN: + g_value_set_object (value, terminal_notebook_get_active_screen (mdi_container)); + break; + case PROP_TAB_POLICY: + g_value_set_enum (value, terminal_notebook_get_tab_policy (TERMINAL_NOTEBOOK (mdi_container))); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +terminal_notebook_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + TerminalMdiContainer *mdi_container = TERMINAL_MDI_CONTAINER (object); + + switch (prop_id) { + case PROP_ACTIVE_SCREEN: + terminal_notebook_set_active_screen (mdi_container, (TerminalScreen*)g_value_get_object (value)); + break; + case PROP_TAB_POLICY: + terminal_notebook_set_tab_policy (TERMINAL_NOTEBOOK (mdi_container), GtkPolicyType(g_value_get_enum (value))); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +terminal_notebook_class_init (TerminalNotebookClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkNotebookClass *notebook_class = GTK_NOTEBOOK_CLASS (klass); + + g_type_class_add_private (gobject_class, sizeof (TerminalNotebookPrivate)); + + gobject_class->constructed = terminal_notebook_constructed; + gobject_class->get_property = terminal_notebook_get_property; + gobject_class->set_property = terminal_notebook_set_property; + + g_object_class_override_property (gobject_class, PROP_ACTIVE_SCREEN, "active-screen"); + + widget_class->grab_focus = terminal_notebook_grab_focus; + + notebook_class->switch_page = terminal_notebook_switch_page; + notebook_class->create_window = terminal_notebook_create_window; + notebook_class->page_added = terminal_notebook_page_added; + notebook_class->page_removed = terminal_notebook_page_removed; + notebook_class->page_reordered = terminal_notebook_page_reordered; + + g_object_class_install_property + (gobject_class, + PROP_TAB_POLICY, + g_param_spec_enum ("tab-policy", nullptr, nullptr, + GTK_TYPE_POLICY_TYPE, + GTK_POLICY_AUTOMATIC, + GParamFlags(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS))); + + /* Remove unwanted and interfering keybindings */ + GtkBindingSet *binding_set = gtk_binding_set_by_class (terminal_notebook_parent_class); + gtk_binding_entry_skip (binding_set, GDK_KEY_Page_Up, GdkModifierType(GDK_CONTROL_MASK)); + gtk_binding_entry_skip (binding_set, GDK_KEY_Page_Up, GdkModifierType(GDK_CONTROL_MASK | GDK_MOD1_MASK)); + gtk_binding_entry_skip (binding_set, GDK_KEY_Page_Down, GdkModifierType(GDK_CONTROL_MASK)); + gtk_binding_entry_skip (binding_set, GDK_KEY_Page_Down, GdkModifierType(GDK_CONTROL_MASK | GDK_MOD1_MASK)); + remove_reorder_bindings (binding_set, GDK_KEY_Up); + remove_reorder_bindings (binding_set, GDK_KEY_Down); + remove_reorder_bindings (binding_set, GDK_KEY_Left); + remove_reorder_bindings (binding_set, GDK_KEY_Right); + remove_reorder_bindings (binding_set, GDK_KEY_Home); + remove_reorder_bindings (binding_set, GDK_KEY_Home); + remove_reorder_bindings (binding_set, GDK_KEY_End); + remove_reorder_bindings (binding_set, GDK_KEY_End); +} + +/* public API */ + +/** + * terminal_notebook_new: + * + * Returns: (transfer full): a new #TerminalNotebook + */ +GtkWidget * +terminal_notebook_new (void) +{ + return reinterpret_cast<GtkWidget*> + (g_object_new (TERMINAL_TYPE_NOTEBOOK, nullptr)); +} + +void +terminal_notebook_set_tab_policy (TerminalNotebook *notebook, + GtkPolicyType policy) +{ + TerminalNotebookPrivate *priv = notebook->priv; + + if (priv->policy == policy) + return; + + priv->policy = policy; + update_tab_visibility (notebook, 0); + + g_object_notify (G_OBJECT (notebook), "tab-policy"); +} + +GtkPolicyType +terminal_notebook_get_tab_policy (TerminalNotebook *notebook) +{ + return notebook->priv->policy; +} + +GtkWidget * +terminal_notebook_get_action_box (TerminalNotebook *notebook, + GtkPackType pack_type) +{ + GtkNotebook *gtk_notebook; + GtkWidget *box, *inner_box; + + g_return_val_if_fail (TERMINAL_IS_NOTEBOOK (notebook), nullptr); + + gtk_notebook = GTK_NOTEBOOK (notebook); + box = gtk_notebook_get_action_widget (gtk_notebook, pack_type); + if (box != nullptr) { + gs_free_list GList *list; + + list = gtk_container_get_children (GTK_CONTAINER (box)); + g_assert (list->data != nullptr); + return (GtkWidget*)list->data; + } + + /* Create container for the buttons */ + box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); + gtk_container_set_border_width (GTK_CONTAINER (box), ACTION_AREA_BORDER_WIDTH); + + inner_box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, ACTION_BUTTON_SPACING); + gtk_box_pack_start (GTK_BOX (box), inner_box, TRUE, FALSE, 0); + gtk_widget_show (inner_box); + + gtk_notebook_set_action_widget (gtk_notebook, box, pack_type); + gtk_widget_show (box); + + /* FIXME: this appears to be necessary to make the icon buttons contained + * in the action area render the same way as buttons in the tab labels (e.g. + * the close button). gtk+ bug? + */ + G_GNUC_BEGIN_IGNORE_DEPRECATIONS + gtk_style_context_add_region (gtk_widget_get_style_context (box), + GTK_STYLE_REGION_TAB, + pack_type == GTK_PACK_START ? GTK_REGION_FIRST : GTK_REGION_LAST); + G_GNUC_END_IGNORE_DEPRECATIONS + + return inner_box; +} diff --git a/src/terminal-notebook.hh b/src/terminal-notebook.hh new file mode 100644 index 0000000..68cd51f --- /dev/null +++ b/src/terminal-notebook.hh @@ -0,0 +1,62 @@ +/* + * Copyright © 2008, 2010, 2012 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/>. + */ + +#ifndef TERMINAL_NOTEBOOK_H +#define TERMINAL_NOTEBOOK_H + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define TERMINAL_TYPE_NOTEBOOK (terminal_notebook_get_type ()) +#define TERMINAL_NOTEBOOK(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), TERMINAL_TYPE_NOTEBOOK, TerminalNotebook)) +#define TERMINAL_NOTEBOOK_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), TERMINAL_TYPE_NOTEBOOK, TerminalNotebookClass)) +#define TERMINAL_IS_NOTEBOOK(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), TERMINAL_TYPE_NOTEBOOK)) +#define TERMINAL_IS_NOTEBOOK_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), TERMINAL_TYPE_NOTEBOOK)) +#define TERMINAL_NOTEBOOK_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), TERMINAL_TYPE_NOTEBOOK, TerminalNotebookClass)) + +typedef struct _TerminalNotebook TerminalNotebook; +typedef struct _TerminalNotebookClass TerminalNotebookClass; +typedef struct _TerminalNotebookPrivate TerminalNotebookPrivate; + +struct _TerminalNotebook +{ + GtkNotebook parent_instance; + + /*< private >*/ + TerminalNotebookPrivate *priv; +}; + +struct _TerminalNotebookClass +{ + GtkNotebookClass parent_class; +}; + +GType terminal_notebook_get_type (void); + +GtkWidget *terminal_notebook_new (void); + +void terminal_notebook_set_tab_policy (TerminalNotebook *notebook, + GtkPolicyType policy); +GtkPolicyType terminal_notebook_get_tab_policy (TerminalNotebook *notebook); + +GtkWidget *terminal_notebook_get_action_box (TerminalNotebook *notebook, + GtkPackType pack_type); + +G_END_DECLS + +#endif /* TERMINAL_NOTEBOOK_H */ diff --git a/src/terminal-options.cc b/src/terminal-options.cc new file mode 100644 index 0000000..6c13955 --- /dev/null +++ b/src/terminal-options.cc @@ -0,0 +1,1743 @@ +/* + * Copyright © 2001, 2002 Havoc Pennington + * Copyright © 2002 Red Hat, Inc. + * Copyright © 2002 Sun Microsystems + * Copyright © 2003 Mariano Suarez-Alvarez + * Copyright © 2008, 2017 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 <errno.h> +#include <string.h> +#include <stdlib.h> + +#include <glib.h> +#include <glib/gi18n.h> +#include <glib/gprintf.h> + +#include "terminal-options.hh" +#include "terminal-client-utils.hh" +#include "terminal-defines.hh" +#include "terminal-schemas.hh" +#include "terminal-screen.hh" +#include "terminal-app.hh" +#include "terminal-util.hh" +#include "terminal-version.hh" +#include "terminal-libgsystem.hh" +#include "terminal-settings-utils.hh" + +static int verbosity = 1; + +void +terminal_fprintf (FILE* fp, + int verbosity_level, + const char* format, + ...) +{ + if (verbosity < verbosity_level) + return; + + va_list args; + va_start(args, format); + gs_free char *str = g_strdup_vprintf(format, args); + va_end(args); + + gs_strfreev char **lines = g_strsplit_set(str, "\n\r", -1); + for (gsize i = 0; lines[i]; ++i) { + if (lines[i][0] != '\0') + g_fprintf(fp, "# %s\n", lines[i]); + } +} + +static TerminalVerbosity +verbosity_from_log_level (GLogLevelFlags log_level) +{ + guint level = log_level & G_LOG_LEVEL_MASK; + TerminalVerbosity res; + level = level & ~(level - 1); /* extract the highest bit */ + switch (level) { + case G_LOG_LEVEL_DEBUG: + res = TERMINAL_VERBOSITY_DEBUG; + break; + case G_LOG_LEVEL_INFO: + res = TERMINAL_VERBOSITY_DETAIL; + break; + default: + /* better display than lose important messages */ + res = TERMINAL_VERBOSITY_NORMAL; + } + return res; +} + +/* Need to install a special log writer so we never output + * anything without the '# ' prepended, in case --print-environment + * is used. + * + * FIXME: Until issue glib#2087 is fixed, apply a simple log level filter + * to prevent spamming dconf (and other) debug messages to stderr, + * see issue gnome-terminal#42. + */ +GLogWriterOutput +terminal_log_writer (GLogLevelFlags log_level, + const GLogField *fields, + gsize n_fields, + gpointer user_data) +{ +#if GLIB_CHECK_VERSION(2, 68, 0) + char const* domain = nullptr; + for (auto i = gsize{0}; i < n_fields; i++) { + if (g_str_equal(fields[i].key, "GLIB_DOMAIN")) { + domain = (char const*)fields[i].value; + break; + } + } + if (g_log_writer_default_would_drop(log_level, domain)) + return G_LOG_WRITER_HANDLED; +#endif /* glib 2.68 */ + + TerminalVerbosity level = verbosity_from_log_level(log_level); + for (gsize i = 0; i < n_fields; i++) { + if (g_str_equal (fields[i].key, "MESSAGE")) + terminal_fprintf (stderr, level, "%s\n", (const char*)fields[i].value); + } + + return G_LOG_WRITER_HANDLED; +} + +static GOptionContext *get_goption_context (TerminalOptions *options); + +static void +terminal_options_ensure_schema_source(TerminalOptions* options) +{ + if (options->schema_source) + return; + + options->schema_source = terminal_g_settings_schema_source_get_default(); +} + +static TerminalSettingsList * +terminal_options_ensure_profiles_list (TerminalOptions *options) +{ + if (options->profiles_list == nullptr) { + terminal_options_ensure_schema_source(options); + options->profiles_list = terminal_profiles_list_new(nullptr /* default backend */, + options->schema_source); + } + + return options->profiles_list; +} + +static char * +terminal_util_key_file_get_string_unescape (GKeyFile *key_file, + const char *group, + const char *key, + GError **error) +{ + char *escaped, *unescaped; + + escaped = g_key_file_get_string (key_file, group, key, error); + if (!escaped) + return nullptr; + + unescaped = g_strcompress (escaped); + g_free (escaped); + + return unescaped; +} + +static char ** +terminal_util_key_file_get_argv (GKeyFile *key_file, + const char *group, + const char *key, + int *argc, + GError **error) +{ + char **argv; + char *flat; + gboolean retval; + + flat = terminal_util_key_file_get_string_unescape (key_file, group, key, error); + if (!flat) + return nullptr; + + retval = g_shell_parse_argv (flat, argc, &argv, error); + g_free (flat); + + if (retval) + return argv; + + return nullptr; +} + +static InitialTab* +initial_tab_new (char *profile /* adopts */) +{ + InitialTab *it; + + it = g_slice_new (InitialTab); + + it->profile = profile; + it->exec_argv = nullptr; + it->title = nullptr; + it->working_dir = nullptr; + it->zoom = 1.0; + it->zoom_set = FALSE; + it->active = FALSE; + it->fd_list = nullptr; + it->fd_array = nullptr; + + return it; +} + +static void +initial_tab_free (InitialTab *it) +{ + g_free (it->profile); + g_strfreev (it->exec_argv); + g_free (it->title); + g_free (it->working_dir); + g_clear_object (&it->fd_list); + if (it->fd_array) + g_array_unref (it->fd_array); + g_slice_free (InitialTab, it); +} + +static InitialWindow* +initial_window_new (guint source_tag) +{ + InitialWindow *iw; + + iw = g_slice_new0 (InitialWindow); + iw->source_tag = source_tag; + + return iw; +} + +static void +initial_window_free (InitialWindow *iw) +{ + g_list_free_full (iw->tabs, (GDestroyNotify) initial_tab_free); + g_free (iw->geometry); + g_free (iw->role); + g_slice_free (InitialWindow, iw); +} + +static void +apply_window_defaults (TerminalOptions *options, + InitialWindow *iw) +{ + if (options->default_role) + { + iw->role = options->default_role; + options->default_role = nullptr; + } + + if (iw->geometry == nullptr) + iw->geometry = g_strdup (options->default_geometry); + + if (options->default_window_menubar_forced) + { + iw->force_menubar_state = TRUE; + iw->menubar_state = options->default_window_menubar_state; + + options->default_window_menubar_forced = FALSE; + } + + iw->start_fullscreen |= options->default_fullscreen; + iw->start_maximized |= options->default_maximize; +} + +static void +apply_tab_defaults (TerminalOptions *options, + InitialTab *it) +{ + it->wait = options->default_wait; +} + +static InitialWindow* +add_new_window (TerminalOptions *options, + char *profile /* adopts */, + gboolean implicit_if_first_window) +{ + InitialWindow *iw; + InitialTab *it; + + iw = initial_window_new (0); + iw->implicit_first_window = (options->initial_windows == nullptr) && implicit_if_first_window; + apply_window_defaults (options, iw); + + it = initial_tab_new (profile); + + /* If this is an implicit first window, the new tab should be active */ + if (iw->implicit_first_window) + it->active = TRUE; + + iw->tabs = g_list_prepend (nullptr, it); + apply_tab_defaults (options, it); + + options->initial_windows = g_list_append (options->initial_windows, iw); + return iw; +} + +static InitialWindow* +ensure_top_window (TerminalOptions *options, + gboolean implicit_if_first_window) +{ + InitialWindow *iw; + + if (options->initial_windows == nullptr) + iw = add_new_window (options, nullptr /* profile */, implicit_if_first_window); + else + iw = (InitialWindow*)g_list_last (options->initial_windows)->data; + + g_assert_nonnull (iw->tabs); + + return iw; +} + +static InitialTab* +ensure_top_tab (TerminalOptions *options) +{ + InitialWindow *iw; + InitialTab *it; + + iw = ensure_top_window (options, TRUE); + + g_assert_nonnull (iw->tabs); + + it = (InitialTab*)g_list_last (iw->tabs)->data; + + return it; +} + +/* handle deprecated command line options */ + +static void +deprecated_option_warning (const gchar *option_name) +{ + terminal_printerr (_("Option “%s” is deprecated and might be removed in a later version of gnome-terminal."), + option_name); + terminal_printerr ("\n"); +} + +static void +deprecated_command_option_warning (const char *option_name) +{ + deprecated_option_warning (option_name); + + /* %s is being replaced with "-- " (without quotes), which must be used literally, not translatable */ + terminal_printerr (_("Use “%s” to terminate the options and put the command line to execute after it."), "-- "); + terminal_printerr ("\n"); +} + +static gboolean +unsupported_option_callback (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + terminal_printerr (_("Option “%s” is no longer supported in this version of gnome-terminal."), + option_name); + terminal_printerr ("\n"); + return TRUE; /* we do not want to bail out here but continue */ +} + +static gboolean +unsupported_option_fatal_callback (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_UNKNOWN_OPTION, + _("Option “%s” is no longer supported in this version of gnome-terminal."), + option_name); + return FALSE; +} + + +static gboolean G_GNUC_NORETURN +option_version_cb (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + terminal_print ("GNOME Terminal %s using VTE %u.%u.%u %s\n", + VERSION, + vte_get_major_version (), + vte_get_minor_version (), + vte_get_micro_version (), + vte_get_features ()); + exit (EXIT_SUCCESS); +} + +static gboolean +option_verbosity_cb (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + if (g_str_equal (option_name, "--quiet") || g_str_equal (option_name, "-q")) + verbosity = 0; + else + verbosity++; + + return TRUE; +} + +static gboolean +option_app_id_callback (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + TerminalOptions *options = (TerminalOptions*)data; + + if (!g_application_id_is_valid (value)) { + g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, + "\"%s\" is not a valid application ID", value); + return FALSE; + } + + g_free (options->server_app_id); + options->server_app_id = g_strdup (value); + + return TRUE; +} + +static gboolean +option_command_callback (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + TerminalOptions *options = (TerminalOptions*)data; + GError *err = nullptr; + char **exec_argv; + + deprecated_command_option_warning (option_name); + + if (!g_shell_parse_argv (value, nullptr, &exec_argv, &err)) + { + g_set_error(error, + G_OPTION_ERROR, + G_OPTION_ERROR_BAD_VALUE, + _("Argument to “%s” is not a valid command: %s"), + "--command/-e", + err->message); + g_error_free (err); + return FALSE; + } + + if (options->initial_windows) + { + InitialTab *it = ensure_top_tab (options); + + g_strfreev (it->exec_argv); + it->exec_argv = exec_argv; + } + else + { + g_strfreev (options->exec_argv); + options->exec_argv = exec_argv; + } + + return TRUE; +} + +static gboolean +option_profile_cb (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + TerminalOptions *options = (TerminalOptions*)data; + char *profile; + + profile = terminal_profiles_list_dup_uuid_or_name (terminal_options_ensure_profiles_list (options), + value, error); + if (profile == nullptr) + { + terminal_printerr ("Profile '%s' specified but not found. Attempting to fall back " + "to the default profile.\n", value); + g_clear_error (error); + profile = terminal_profiles_list_dup_uuid_or_name (terminal_options_ensure_profiles_list (options), + nullptr, error); + } + + if (profile == nullptr) + return FALSE; + + if (options->initial_windows) + { + InitialTab *it = ensure_top_tab (options); + + g_free (it->profile); + it->profile = profile; + } + else + { + g_free (options->default_profile); + options->default_profile = profile; + } + + return TRUE; +} + +static gboolean +option_profile_id_cb (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + TerminalOptions *options = (TerminalOptions*)data; + char *profile; + + profile = terminal_profiles_list_dup_uuid (terminal_options_ensure_profiles_list (options), + value, error); + if (profile == nullptr) + return FALSE; + + if (options->initial_windows) + { + InitialTab *it = ensure_top_tab (options); + + g_free (it->profile); + it->profile = profile; + } + else + { + g_free (options->default_profile); + options->default_profile = profile; + } + + return TRUE; +} + + +static gboolean +option_window_callback (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + TerminalOptions *options = (TerminalOptions*)data; + char *profile; + + if (value != nullptr) { + profile = terminal_profiles_list_dup_uuid_or_name (terminal_options_ensure_profiles_list (options), + value, error); + + if (value && profile == nullptr) { + terminal_printerr ("Profile '%s' specified but not found. Attempting to fall back " + "to the default profile.\n", value); + g_clear_error (error); + profile = terminal_profiles_list_dup_uuid_or_name (terminal_options_ensure_profiles_list (options), + nullptr, error); + } + + if (profile == nullptr) + return FALSE; + } else + profile = nullptr; + + add_new_window (options, profile /* adopts */, FALSE); + + return TRUE; +} + +static gboolean +option_tab_callback (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + TerminalOptions *options = (TerminalOptions*)data; + char *profile; + + if (value != nullptr) { + profile = terminal_profiles_list_dup_uuid_or_name (terminal_options_ensure_profiles_list (options), + value, error); + if (profile == nullptr) + return FALSE; + } else + profile = nullptr; + + if (options->initial_windows) + { + InitialWindow *iw; + + iw = (InitialWindow*)g_list_last (options->initial_windows)->data; + iw->tabs = g_list_append (iw->tabs, initial_tab_new (profile /* adopts */)); + } + else + add_new_window (options, profile /* adopts */, TRUE); + + return TRUE; +} + +static gboolean +option_role_callback (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + TerminalOptions *options = (TerminalOptions*)data; + InitialWindow *iw; + + if (options->initial_windows) + { + iw = (InitialWindow*)g_list_last (options->initial_windows)->data; + iw->role = g_strdup (value); + } + else if (!options->default_role) + options->default_role = g_strdup (value); + else + { + g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, + "%s", _("Two roles given for one window")); + return FALSE; + } + + return TRUE; +} + +static gboolean +option_show_menubar_callback (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + TerminalOptions *options = (TerminalOptions*)data; + InitialWindow *iw; + + if (options->initial_windows) + { + iw = (InitialWindow*)g_list_last (options->initial_windows)->data; + if (iw->force_menubar_state && iw->menubar_state == TRUE) + { + terminal_printerr_detail (_("“%s” option given twice for the same window\n"), + "--show-menubar"); + + return TRUE; + } + + iw->force_menubar_state = TRUE; + iw->menubar_state = TRUE; + } + else + { + options->default_window_menubar_forced = TRUE; + options->default_window_menubar_state = TRUE; + } + + return TRUE; +} + +static gboolean +option_hide_menubar_callback (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + TerminalOptions *options = (TerminalOptions*)data; + InitialWindow *iw; + + if (options->initial_windows) + { + iw = (InitialWindow*)g_list_last (options->initial_windows)->data; + + if (iw->force_menubar_state && iw->menubar_state == FALSE) + { + terminal_printerr_detail (_("“%s” option given twice for the same window\n"), + "--hide-menubar"); + return TRUE; + } + + iw->force_menubar_state = TRUE; + iw->menubar_state = FALSE; + } + else + { + options->default_window_menubar_forced = TRUE; + options->default_window_menubar_state = FALSE; + } + + return TRUE; +} + +static gboolean +option_maximize_callback (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + TerminalOptions *options = (TerminalOptions*)data; + InitialWindow *iw; + + if (options->initial_windows) + { + iw = (InitialWindow*)g_list_last (options->initial_windows)->data; + iw->start_maximized = TRUE; + } + else + options->default_maximize = TRUE; + + return TRUE; +} + +static gboolean +option_fullscreen_callback (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + TerminalOptions *options = (TerminalOptions*)data; + + if (options->initial_windows) + { + InitialWindow *iw; + + iw = (InitialWindow*)g_list_last (options->initial_windows)->data; + iw->start_fullscreen = TRUE; + } + else + options->default_fullscreen = TRUE; + + return TRUE; +} + +static gboolean +option_geometry_callback (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + TerminalOptions *options = (TerminalOptions*)data; + + if (options->initial_windows) + { + InitialWindow *iw; + + iw = (InitialWindow*)g_list_last (options->initial_windows)->data; + iw->geometry = g_strdup (value); + } + else + options->default_geometry = g_strdup (value); + + return TRUE; +} + +static gboolean +option_load_config_cb (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + TerminalOptions *options = (TerminalOptions*)data; + GFile *file; + char *config_file; + GKeyFile *key_file; + gboolean result; + + file = g_file_new_for_commandline_arg (value); + config_file = g_file_get_path (file); + g_object_unref (file); + + key_file = g_key_file_new (); + result = g_key_file_load_from_file (key_file, config_file, GKeyFileFlags(0), error) && + terminal_options_merge_config (options, key_file, + strcmp (option_name, "load-config") == 0 ? SOURCE_DEFAULT : SOURCE_SESSION, + error); + g_key_file_free (key_file); + g_free (config_file); + + return result; +} + +static gboolean +option_title_callback (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + TerminalOptions *options = (TerminalOptions*)data; + + if (options->initial_windows) + { + InitialTab *it = ensure_top_tab (options); + + g_free (it->title); + it->title = g_strdup (value); + } + else + { + g_free (options->default_title); + options->default_title = g_strdup (value); + } + + return TRUE; +} + +static gboolean +option_working_directory_callback (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + TerminalOptions *options = (TerminalOptions*)data; + + if (options->initial_windows) + { + InitialTab *it = ensure_top_tab (options); + + g_free (it->working_dir); + it->working_dir = g_strdup (value); + } + else + { + g_free (options->default_working_dir); + options->default_working_dir = g_strdup (value); + } + + return TRUE; +} + +static gboolean +option_wait_cb (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + TerminalOptions *options = (TerminalOptions*)data; + + if (options->initial_windows) + { + InitialTab *it = ensure_top_tab (options); + + g_free (it->working_dir); + it->wait = TRUE; + } + else + { + options->default_wait = TRUE; + } + + return TRUE; +} + +static gboolean +option_pass_fd_cb (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + TerminalOptions *options = (TerminalOptions*)data; + + errno = 0; + char *end; + gint64 v = g_ascii_strtoll (value, &end, 10); + if (errno || end == value || v == -1 || v < G_MININT || v > G_MAXINT) { + g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, + "Failed to parse \"%s\" as file descriptor number", + value); + return FALSE; + } + + int fd = v; + if (fd == STDIN_FILENO || + fd == STDOUT_FILENO || + fd == STDERR_FILENO) { + g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, + "FD passing of %s is not supported", + fd == STDIN_FILENO ? "stdin" : fd == STDOUT_FILENO ? "stdout" : "stderr"); + return FALSE; + } + + InitialTab *it = ensure_top_tab (options); + if (it->fd_list == nullptr) + it->fd_list = g_unix_fd_list_new (); + if (it->fd_array == nullptr) + it->fd_array = g_array_sized_new (FALSE /* zero terminate */, + TRUE /* clear */, + sizeof (PassFdElement), + 8 /* that should be plenty */); + + + for (guint i = 0; i < it->fd_array->len; i++) { + PassFdElement *e = &g_array_index (it->fd_array, PassFdElement, i); + if (e->fd == fd) { + g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, + _("Cannot pass FD %d twice"), fd); + return FALSE; + } + } + + int idx = g_unix_fd_list_append (it->fd_list, fd, error); + if (idx == -1) { + g_prefix_error (error, "%d: ", fd); + return FALSE; + } + + PassFdElement e = { idx, fd }; + g_array_append_val (it->fd_array, e); + +#if 0 + if (fd == STDOUT_FILENO || + fd == STDERR_FILENO) + verbosity = 0; + if (fd == STDIN_FILENO) + it->wait = TRUE; +#endif + + return TRUE; +} + +static gboolean +option_active_callback (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + TerminalOptions *options = (TerminalOptions*)data; + InitialTab *it; + + it = ensure_top_tab (options); + it->active = TRUE; + + return TRUE; +} + +static gboolean +option_zoom_callback (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + TerminalOptions *options = (TerminalOptions*)data; + double zoom; + char *end; + + /* Try reading a locale-style double first, in case it was + * typed by a person, then fall back to ascii_strtod (we + * always save session in C locale format) + */ + end = nullptr; + errno = 0; + zoom = g_strtod (value, &end); + if (end == nullptr || *end != '\0') + { + g_set_error (error, + G_OPTION_ERROR, + G_OPTION_ERROR_BAD_VALUE, + _("“%s” is not a valid zoom factor"), + value); + return FALSE; + } + + if (zoom < (TERMINAL_SCALE_MINIMUM + 1e-6)) + { + terminal_printerr (_("Zoom factor “%g” is too small, using %g\n"), + zoom, + TERMINAL_SCALE_MINIMUM); + zoom = TERMINAL_SCALE_MINIMUM; + } + + if (zoom > (TERMINAL_SCALE_MAXIMUM - 1e-6)) + { + terminal_printerr (_("Zoom factor “%g” is too large, using %g\n"), + zoom, + TERMINAL_SCALE_MAXIMUM); + zoom = TERMINAL_SCALE_MAXIMUM; + } + + if (options->initial_windows) + { + InitialTab *it = ensure_top_tab (options); + it->zoom = zoom; + it->zoom_set = TRUE; + } + else + { + options->zoom = zoom; + options->zoom_set = TRUE; + } + + return TRUE; +} + +/* Evaluation of the arguments given to the command line options */ +static gboolean +digest_options_callback (GOptionContext *context, + GOptionGroup *group, + gpointer data, + GError **error) +{ + TerminalOptions *options = (TerminalOptions*)data; + InitialTab *it; + + if (options->execute) + { + if (options->exec_argv == nullptr) + { + g_set_error (error, + G_OPTION_ERROR, + G_OPTION_ERROR_BAD_VALUE, + _("Option “%s” requires specifying the command to run" + " on the rest of the command line"), + "--execute/-x"); + return FALSE; + } + + /* Apply -x/--execute command only to the first tab */ + it = ensure_top_tab (options); + it->exec_argv = options->exec_argv; + options->exec_argv = nullptr; + } + + return TRUE; +} + +static char* +getenv_utf8(char const* env) +{ + auto const value = g_getenv(env); + if (!value || + !value[0] || + !g_utf8_validate(value, -1, nullptr)) + return nullptr; + + return g_strdup(value); +} + +/** + * terminal_options_parse: + * @argcp: (inout) address of the argument count. Changed if any arguments were handled + * @argvp: (inout) address of the argument vector. Any parameters understood by + * the terminal #GOptionContext are removed + * @error: a #GError to fill in + * + * Parses the argument vector *@argvp. + * + * Returns: a new #TerminalOptions containing the windows and tabs to open, + * or %nullptr on error. + */ +TerminalOptions * +terminal_options_parse (int *argcp, + char ***argvp, + GError **error) +{ + TerminalOptions *options; + GOptionContext *context; + gboolean retval; + int i; + char **argv = *argvp; + + options = g_new0 (TerminalOptions, 1); + + options->print_environment = FALSE; + options->default_window_menubar_forced = FALSE; + options->default_window_menubar_state = TRUE; + options->default_fullscreen = FALSE; + options->default_maximize = FALSE; + options->execute = FALSE; + + options->startup_id = getenv_utf8("DESKTOP_STARTUP_ID"); + options->activation_token = getenv_utf8("XDG_ACTIVATION_TOKEN"); + options->display_name = nullptr; + options->initial_windows = nullptr; + options->default_role = nullptr; + options->default_geometry = nullptr; + options->default_title = nullptr; + options->zoom = 1.0; + options->zoom_set = FALSE; + + options->default_working_dir = g_get_current_dir (); + + /* Collect info from gnome-terminal private env vars */ + const char *server_unique_name = g_getenv (TERMINAL_ENV_SERVICE_NAME); + if (server_unique_name != nullptr) { + if (g_dbus_is_unique_name (server_unique_name)) + options->server_unique_name = g_strdup (server_unique_name); + else + terminal_printerr ("Warning: %s set but \"%s\" is not a unique D-Bus name.\n", + TERMINAL_ENV_SERVICE_NAME, + server_unique_name); + } + + const char *parent_screen_object_path = g_getenv (TERMINAL_ENV_SCREEN); + if (parent_screen_object_path != nullptr) { + if (g_variant_is_object_path (parent_screen_object_path)) + options->parent_screen_object_path = g_strdup (parent_screen_object_path); + else + terminal_printerr ("Warning: %s set but \"%s\" is not a valid D-Bus object path.\n", + TERMINAL_ENV_SCREEN, + parent_screen_object_path); + } + + /* The old -x/--execute option is broken, so we need to pre-scan for it. */ + /* We now also support passing the command after the -- switch. */ + options->exec_argv = nullptr; + for (i = 1 ; i < *argcp; ++i) + { + gboolean is_execute; + gboolean is_dashdash; + int j, last; + + is_execute = strcmp (argv[i], "-x") == 0 || strcmp (argv[i], "--execute") == 0; + is_dashdash = strcmp (argv[i], "--") == 0; + + if (!is_execute && !is_dashdash) + continue; + + if (is_execute) + deprecated_command_option_warning (argv[i]); + + options->execute = is_execute; + + /* Skip the switch */ + last = i; + ++i; + if (i == *argcp) + break; /* we'll complain about this later for -x/--execute; it's fine for -- */ + + /* Collect the args, and remove them from argv */ + options->exec_argv = g_new0 (char*, *argcp - i + 1); + for (j = 0; i < *argcp; ++i, ++j) + options->exec_argv[j] = g_strdup (argv[i]); + options->exec_argv[j] = nullptr; + + *argcp = last; + break; + } + + context = get_goption_context (options); + retval = g_option_context_parse (context, argcp, argvp, error); + g_option_context_free (context); + + if (!retval) { + terminal_options_free (options); + return nullptr; + } + +#ifdef GDK_WINDOWING_X11 + /* Do this here so that gdk_display is initialized */ + if (options->startup_id == nullptr) { + options->startup_id = terminal_client_get_fallback_startup_id (); + } +#endif /* X11 */ + + GdkDisplay *display = gdk_display_get_default (); + if (display != nullptr) + options->display_name = g_strdup (gdk_display_get_name (display)); + + /* Sanity check */ + guint wait = 0; + for (GList *lw = options->initial_windows; lw != nullptr; lw = lw->next) { + InitialWindow *iw = (InitialWindow*)lw->data; + for (GList *lt = iw->tabs; lt != nullptr; lt = lt->next) { + InitialTab *it = (InitialTab*)lt->data; + if (it->wait) + wait++; + } + } + + if (wait > 1) { + g_set_error_literal (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, + _("Can only use --wait once")); + return FALSE; + } + + options->wait = wait != 0; + return options; +} + +/** + * terminal_options_merge_config: + * @options: + * @key_file: a #GKeyFile containing to merge the options from + * @source_tag: a source_tag to use in new #InitialWindow<!-- -->s + * @error: a #GError to fill in + * + * Merges the saved options from @key_file into @options. + * + * Returns: %TRUE if @key_file was a valid key file containing a stored + * terminal configuration, or %FALSE on error + */ +gboolean +terminal_options_merge_config (TerminalOptions *options, + GKeyFile *key_file, + guint source_tag, + GError **error) +{ + int version, compat_version; + char **groups; + guint i; + gboolean have_error = FALSE; + GList *initial_windows = nullptr; + + if (!g_key_file_has_group (key_file, TERMINAL_CONFIG_GROUP)) + { + g_set_error_literal (error, TERMINAL_OPTION_ERROR, + TERMINAL_OPTION_ERROR_INVALID_CONFIG_FILE, + _("Not a valid terminal config file.")); + return FALSE; + } + + version = g_key_file_get_integer (key_file, TERMINAL_CONFIG_GROUP, TERMINAL_CONFIG_PROP_VERSION, nullptr); + compat_version = g_key_file_get_integer (key_file, TERMINAL_CONFIG_GROUP, TERMINAL_CONFIG_PROP_COMPAT_VERSION, nullptr); + + if (version <= 0 || + compat_version <= 0 || + compat_version > TERMINAL_CONFIG_COMPAT_VERSION) + { + g_set_error_literal (error, TERMINAL_OPTION_ERROR, + TERMINAL_OPTION_ERROR_INCOMPATIBLE_CONFIG_FILE, + _("Incompatible terminal config file version.")); + return FALSE; + } + + groups = g_key_file_get_string_list (key_file, TERMINAL_CONFIG_GROUP, TERMINAL_CONFIG_PROP_WINDOWS, nullptr, error); + if (!groups) + return FALSE; + + for (i = 0; groups[i]; ++i) + { + const char *window_group = groups[i]; + char *active_terminal; + char **tab_groups; + InitialWindow *iw; + guint j; + + tab_groups = g_key_file_get_string_list (key_file, window_group, TERMINAL_CONFIG_WINDOW_PROP_TABS, nullptr, error); + if (!tab_groups) + continue; /* no tabs in this window, skip it */ + + iw = initial_window_new (source_tag); + initial_windows = g_list_append (initial_windows, iw); + apply_window_defaults (options, iw); + + active_terminal = g_key_file_get_string (key_file, window_group, TERMINAL_CONFIG_WINDOW_PROP_ACTIVE_TAB, nullptr); + iw->role = g_key_file_get_string (key_file, window_group, TERMINAL_CONFIG_WINDOW_PROP_ROLE, nullptr); + iw->geometry = g_key_file_get_string (key_file, window_group, TERMINAL_CONFIG_WINDOW_PROP_GEOMETRY, nullptr); + iw->start_fullscreen = g_key_file_get_boolean (key_file, window_group, TERMINAL_CONFIG_WINDOW_PROP_FULLSCREEN, nullptr); + iw->start_maximized = g_key_file_get_boolean (key_file, window_group, TERMINAL_CONFIG_WINDOW_PROP_MAXIMIZED, nullptr); + if (g_key_file_has_key (key_file, window_group, TERMINAL_CONFIG_WINDOW_PROP_MENUBAR_VISIBLE, nullptr)) + { + iw->force_menubar_state = TRUE; + iw->menubar_state = g_key_file_get_boolean (key_file, window_group, TERMINAL_CONFIG_WINDOW_PROP_MENUBAR_VISIBLE, nullptr); + } + + for (j = 0; tab_groups[j]; ++j) + { + const char *tab_group = tab_groups[j]; + InitialTab *it; + char *profile; + + profile = g_key_file_get_string (key_file, tab_group, TERMINAL_CONFIG_TERMINAL_PROP_PROFILE_ID, nullptr); + it = initial_tab_new (profile /* adopts */); + + iw->tabs = g_list_append (iw->tabs, it); + + if (g_strcmp0 (active_terminal, tab_group) == 0) + it->active = TRUE; + +/* it->width = g_key_file_get_integer (key_file, tab_group, TERMINAL_CONFIG_TERMINAL_PROP_WIDTH, nullptr); + it->height = g_key_file_get_integer (key_file, tab_group, TERMINAL_CONFIG_TERMINAL_PROP_HEIGHT, nullptr);*/ + it->working_dir = terminal_util_key_file_get_string_unescape (key_file, tab_group, TERMINAL_CONFIG_TERMINAL_PROP_WORKING_DIRECTORY, nullptr); + it->title = g_key_file_get_string (key_file, tab_group, TERMINAL_CONFIG_TERMINAL_PROP_TITLE, nullptr); + + if (g_key_file_has_key (key_file, tab_group, TERMINAL_CONFIG_TERMINAL_PROP_COMMAND, nullptr) && + !(it->exec_argv = terminal_util_key_file_get_argv (key_file, tab_group, TERMINAL_CONFIG_TERMINAL_PROP_COMMAND, nullptr, error))) + { + have_error = TRUE; + break; + } + } + + g_free (active_terminal); + g_strfreev (tab_groups); + + if (have_error) + break; + } + + g_strfreev (groups); + + if (have_error) + { + g_list_free_full (initial_windows, (GDestroyNotify) initial_window_free); + return FALSE; + } + + options->initial_windows = g_list_concat (options->initial_windows, initial_windows); + + return TRUE; +} + +/** + * terminal_options_ensure_window: + * @options: + * + * Ensure that @options will contain at least one window to open. + */ +void +terminal_options_ensure_window (TerminalOptions *options) +{ + terminal_options_ensure_schema_source(options); + gs_unref_object auto global_settings = + terminal_g_settings_new(nullptr, // default backend + options->schema_source, + TERMINAL_SETTING_SCHEMA); + + gs_free char *mode_str = g_settings_get_string (global_settings, + TERMINAL_SETTING_NEW_TERMINAL_MODE_KEY); + + gboolean implicit_if_first_window = g_str_equal (mode_str, "tab"); + ensure_top_window (options, implicit_if_first_window); +} + +/** + * terminal_options_free: + * @options: + * + * Frees @options. + */ +void +terminal_options_free (TerminalOptions *options) +{ + g_list_free_full (options->initial_windows, (GDestroyNotify) initial_window_free); + + g_free (options->default_role); + g_free (options->default_geometry); + g_free (options->default_working_dir); + g_free (options->default_title); + g_free (options->default_profile); + + g_strfreev (options->exec_argv); + + g_free (options->server_unique_name); + g_free (options->parent_screen_object_path); + + g_free (options->display_name); + g_free (options->startup_id); + g_free (options->activation_token); + g_free (options->server_app_id); + + g_free (options->sm_client_id); + g_free (options->sm_config_prefix); + + g_clear_object (&options->profiles_list); + g_clear_pointer (&options->schema_source, g_settings_schema_source_unref); + + g_free (options); +} + +static GOptionContext * +get_goption_context (TerminalOptions *options) +{ + const GOptionEntry global_unique_goptions[] = { + { + "app-id", + 0, + G_OPTION_FLAG_HIDDEN, + G_OPTION_ARG_CALLBACK, + (void*)option_app_id_callback, + "Server application ID", + "ID" + }, + { + "disable-factory", + 0, + G_OPTION_FLAG_NO_ARG | G_OPTION_FLAG_HIDDEN, + G_OPTION_ARG_CALLBACK, + (void*)unsupported_option_fatal_callback, + N_("Do not register with the activation nameserver, do not re-use an active terminal"), + nullptr + }, + { + "load-config", + 0, + G_OPTION_FLAG_FILENAME, + G_OPTION_ARG_CALLBACK, + (void*)option_load_config_cb, + N_("Load a terminal configuration file"), + N_("FILE") + }, + { + "save-config", + 0, + G_OPTION_FLAG_FILENAME | G_OPTION_FLAG_HIDDEN, + G_OPTION_ARG_CALLBACK, + (void*)unsupported_option_callback, + nullptr, nullptr + }, + { + "no-environment", + 0, + 0, + G_OPTION_ARG_NONE, + &options->no_environment, + N_("Do not pass the environment"), + nullptr + }, + { + "preferences", + 0, + 0, + G_OPTION_ARG_NONE, + &options->show_preferences, + N_("Show preferences window"), + nullptr + }, + { + "print-environment", + 'p', + 0, + G_OPTION_ARG_NONE, + &options->print_environment, + N_("Print environment variables to interact with the terminal"), + nullptr + }, + { + "version", + 0, + G_OPTION_FLAG_NO_ARG | G_OPTION_FLAG_HIDDEN, + G_OPTION_ARG_CALLBACK, + (void*)option_version_cb, + nullptr, + nullptr + }, + { + "verbose", + 'v', + G_OPTION_FLAG_NO_ARG, + G_OPTION_ARG_CALLBACK, + (void*)option_verbosity_cb, + N_("Increase diagnostic verbosity"), + nullptr + }, + { + "quiet", + 'q', + G_OPTION_FLAG_NO_ARG, + G_OPTION_ARG_CALLBACK, + (void*)option_verbosity_cb, + N_("Suppress output"), + nullptr + }, + { nullptr, 0, 0, G_OPTION_ARG_NONE, nullptr, nullptr, nullptr } + }; + + const GOptionEntry global_multiple_goptions[] = { + { + "window", + 0, + G_OPTION_FLAG_NO_ARG, + G_OPTION_ARG_CALLBACK, + (void*)option_window_callback, + N_("Open a new window containing a tab with the default profile"), + nullptr + }, + { + "tab", + 0, + G_OPTION_FLAG_NO_ARG, + G_OPTION_ARG_CALLBACK, + (void*)option_tab_callback, + N_("Open a new tab in the last-opened window with the default profile"), + nullptr + }, + { nullptr, 0, 0, G_OPTION_ARG_NONE, nullptr, nullptr, nullptr } + }; + + const GOptionEntry window_goptions[] = { + { + "show-menubar", + 0, + G_OPTION_FLAG_NO_ARG, + G_OPTION_ARG_CALLBACK, + (void*)option_show_menubar_callback, + N_("Turn on the menubar"), + nullptr + }, + { + "hide-menubar", + 0, + G_OPTION_FLAG_NO_ARG, + G_OPTION_ARG_CALLBACK, + (void*)option_hide_menubar_callback, + N_("Turn off the menubar"), + nullptr + }, + { + "maximize", + 0, + G_OPTION_FLAG_NO_ARG, + G_OPTION_ARG_CALLBACK, + (void*)option_maximize_callback, + N_("Maximize the window"), + nullptr + }, + { + "full-screen", + 0, + G_OPTION_FLAG_NO_ARG, + G_OPTION_ARG_CALLBACK, + (void*)option_fullscreen_callback, + N_("Full-screen the window"), + nullptr + }, + { + "geometry", + 0, + 0, + G_OPTION_ARG_CALLBACK, + (void*)option_geometry_callback, + N_("Set the window size; for example: 80x24, or 80x24+200+200 (COLSxROWS+X+Y)"), + N_("GEOMETRY") + }, + { + "role", + 0, + 0, + G_OPTION_ARG_CALLBACK, + (void*)option_role_callback, + N_("Set the window role"), + N_("ROLE") + }, + { + "active", + 0, + G_OPTION_FLAG_NO_ARG, + G_OPTION_ARG_CALLBACK, + (void*)option_active_callback, + N_("Set the last specified tab as the active one in its window"), + nullptr + }, + { nullptr, 0, 0, G_OPTION_ARG_NONE, nullptr, nullptr, nullptr } + }; + + const GOptionEntry terminal_goptions[] = { + { + "command", + 'e', + G_OPTION_FLAG_FILENAME, + G_OPTION_ARG_CALLBACK, + (void*)option_command_callback, + N_("Execute the argument to this option inside the terminal"), + nullptr + }, + { + "profile", + 0, + 0, + G_OPTION_ARG_CALLBACK, + (void*)option_profile_cb, + N_("Use the given profile instead of the default profile"), + N_("PROFILE-NAME") + }, + { + "title", + 't', + 0, + G_OPTION_ARG_CALLBACK, + (void*)option_title_callback, + N_("Set the initial terminal title"), + N_("TITLE") + }, + { + "working-directory", + 0, + G_OPTION_FLAG_FILENAME, + G_OPTION_ARG_CALLBACK, + (void*)option_working_directory_callback, + N_("Set the working directory"), + N_("DIRNAME") + }, + { + "wait", + 0, + G_OPTION_FLAG_NO_ARG, + G_OPTION_ARG_CALLBACK, + (void*)option_wait_cb, + N_("Wait until the child exits"), + nullptr + }, + { + "fd", + 0, + 0, + G_OPTION_ARG_CALLBACK, + (void*)option_pass_fd_cb, + N_("Forward file descriptor"), + /* FD = file descriptor */ + N_("FD") + }, + { + "zoom", + 0, + 0, + G_OPTION_ARG_CALLBACK, + (void*)option_zoom_callback, + N_("Set the terminal’s zoom factor (1.0 = normal size)"), + N_("ZOOM") + }, + { nullptr, 0, 0, G_OPTION_ARG_NONE, nullptr, nullptr, nullptr } + }; + + const GOptionEntry internal_goptions[] = { + { + "profile-id", + 0, + G_OPTION_FLAG_HIDDEN, + G_OPTION_ARG_CALLBACK, + (void*)option_profile_id_cb, + nullptr, nullptr + }, + { + "window-with-profile", + 0, + G_OPTION_FLAG_HIDDEN, + G_OPTION_ARG_CALLBACK, + (void*)option_window_callback, + nullptr, nullptr + }, + { + "tab-with-profile", + 0, + G_OPTION_FLAG_HIDDEN, + G_OPTION_ARG_CALLBACK, + (void*)option_tab_callback, + nullptr, nullptr + }, + { + "window-with-profile-internal-id", + 0, + G_OPTION_FLAG_HIDDEN, + G_OPTION_ARG_CALLBACK, + (void*)option_window_callback, + nullptr, nullptr + }, + { + "tab-with-profile-internal-id", + 0, + G_OPTION_FLAG_HIDDEN, + G_OPTION_ARG_CALLBACK, + (void*)option_tab_callback, + nullptr, nullptr + }, + { + "default-working-directory", + 0, + G_OPTION_FLAG_HIDDEN, + G_OPTION_ARG_FILENAME, + &options->default_working_dir, + nullptr, nullptr, + }, + { + "use-factory", + 0, + G_OPTION_FLAG_NO_ARG | G_OPTION_FLAG_HIDDEN, + G_OPTION_ARG_CALLBACK, + (void*)unsupported_option_callback, + nullptr, nullptr + }, + { + "startup-id", + 0, + G_OPTION_FLAG_HIDDEN, + G_OPTION_ARG_STRING, + &options->startup_id, + nullptr, + nullptr + }, + { + "activation-token", + 0, + G_OPTION_FLAG_HIDDEN, + G_OPTION_ARG_STRING, + &options->activation_token, + nullptr, + nullptr + }, + { nullptr, 0, 0, G_OPTION_ARG_NONE, nullptr, nullptr, nullptr } + }; + + const GOptionEntry smclient_goptions[] = { + { "sm-client-disable", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &options->sm_client_disable, nullptr, nullptr }, + { "sm-client-state-file", 0, G_OPTION_FLAG_HIDDEN | G_OPTION_FLAG_FILENAME, G_OPTION_ARG_CALLBACK, (void*)option_load_config_cb, nullptr, nullptr }, + { "sm-client-id", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &options->sm_client_id, nullptr, nullptr }, + { "sm-disable", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &options->sm_client_disable, nullptr, nullptr }, + { "sm-config-prefix", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &options->sm_config_prefix, nullptr, nullptr }, + { nullptr } + }; + + GOptionContext *context; + GOptionGroup *group; + gs_free char *parameter; + + parameter = g_strdup_printf ("[-- %s …]", _("COMMAND")); + context = g_option_context_new (parameter); + g_option_context_set_translation_domain (context, GETTEXT_PACKAGE); + g_option_context_set_ignore_unknown_options (context, FALSE); + + g_option_context_add_group (context, gtk_get_option_group (TRUE)); + + group = g_option_group_new ("gnome-terminal", + N_("GNOME Terminal Emulator"), + N_("Show GNOME Terminal options"), + options, + nullptr); + g_option_group_set_translation_domain (group, GETTEXT_PACKAGE); + g_option_group_add_entries (group, global_unique_goptions); + g_option_group_add_entries (group, internal_goptions); + g_option_group_set_parse_hooks (group, nullptr, digest_options_callback); + g_option_context_set_main_group (context, group); + + group = g_option_group_new ("terminal", + N_("Options to open new windows or terminal tabs; more than one of these may be specified:"), + N_("Show terminal options"), + options, + nullptr); + g_option_group_set_translation_domain (group, GETTEXT_PACKAGE); + g_option_group_add_entries (group, global_multiple_goptions); + g_option_context_add_group (context, group); + + group = g_option_group_new ("window-options", + N_("Window options; if used before the first --window or --tab argument, sets the default for all windows:"), + N_("Show per-window options"), + options, + nullptr); + g_option_group_set_translation_domain (group, GETTEXT_PACKAGE); + g_option_group_add_entries (group, window_goptions); + g_option_context_add_group (context, group); + + group = g_option_group_new ("terminal-options", + N_("Terminal options; if used before the first --window or --tab argument, sets the default for all terminals:"), + N_("Show per-terminal options"), + options, + nullptr); + g_option_group_set_translation_domain (group, GETTEXT_PACKAGE); + g_option_group_add_entries (group, terminal_goptions); + g_option_context_add_group (context, group); + + group = g_option_group_new ("sm-client", "", "", options, nullptr); + g_option_group_add_entries (group, smclient_goptions); + g_option_context_add_group (context, group); + + return context; +} diff --git a/src/terminal-options.hh b/src/terminal-options.hh new file mode 100644 index 0000000..48edb32 --- /dev/null +++ b/src/terminal-options.hh @@ -0,0 +1,196 @@ +/* + * Copyright © 2001, 2002 Havoc Pennington + * Copyright © 2002 Red Hat, Inc. + * Copyright © 2002 Sun Microsystems + * Copyright © 2003 Mariano Suarez-Alvarez + * Copyright © 2008 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/>. + */ + +#ifndef TERMINAL_OPTIONS_H +#define TERMINAL_OPTIONS_H + +#include <glib.h> +#include <stdio.h> + +#include <gio/gio.h> +#include <gio/gunixfdlist.h> + +#include "terminal-profiles-list.hh" + +G_BEGIN_DECLS + +#define TERMINAL_CONFIG_VERSION (1) /* Bump this for any changes */ +#define TERMINAL_CONFIG_COMPAT_VERSION (1) /* Bump this for incompatible changes */ + +#define TERMINAL_CONFIG_GROUP "GNOME Terminal Configuration" +#define TERMINAL_CONFIG_PROP_VERSION "Version" +#define TERMINAL_CONFIG_PROP_COMPAT_VERSION "CompatVersion" +#define TERMINAL_CONFIG_PROP_WINDOWS "Windows" + +#define TERMINAL_CONFIG_WINDOW_PROP_ACTIVE_TAB "ActiveTerminal" +#define TERMINAL_CONFIG_WINDOW_PROP_FULLSCREEN "Fullscreen" +#define TERMINAL_CONFIG_WINDOW_PROP_GEOMETRY "Geometry" +#define TERMINAL_CONFIG_WINDOW_PROP_MAXIMIZED "Maximized" +#define TERMINAL_CONFIG_WINDOW_PROP_MENUBAR_VISIBLE "MenubarVisible" +#define TERMINAL_CONFIG_WINDOW_PROP_ROLE "Role" +#define TERMINAL_CONFIG_WINDOW_PROP_TABS "Terminals" + +#define TERMINAL_CONFIG_TERMINAL_PROP_HEIGHT "Height" +#define TERMINAL_CONFIG_TERMINAL_PROP_COMMAND "Command" +#define TERMINAL_CONFIG_TERMINAL_PROP_PROFILE_ID "ProfileID" +#define TERMINAL_CONFIG_TERMINAL_PROP_TITLE "Title" +#define TERMINAL_CONFIG_TERMINAL_PROP_WIDTH "Width" +#define TERMINAL_CONFIG_TERMINAL_PROP_WORKING_DIRECTORY "WorkingDirectory" +#define TERMINAL_CONFIG_TERMINAL_PROP_ZOOM "Zoom" + +enum +{ + SOURCE_DEFAULT = 0, + SOURCE_SESSION = 1 +}; + +typedef struct +{ + GSettingsSchemaSource* schema_source; /* may be nullptr */ + TerminalSettingsList *profiles_list; /* may be nullptr */ + + gboolean print_environment; + + char *server_unique_name; + char *parent_screen_object_path; + + char *server_app_id; + char *startup_id; + char *activation_token; + char *display_name; + gboolean show_preferences; + GList *initial_windows; + gboolean default_window_menubar_forced; + gboolean default_window_menubar_state; + gboolean default_fullscreen; + gboolean default_maximize; + gboolean default_wait; + char *default_role; + char *default_geometry; + char *default_working_dir; + char *default_title; + char **exec_argv; + char *default_profile; + gboolean default_profile_is_id; + gboolean no_environment; + + gboolean execute; + double zoom; + + gboolean sm_client_disable; + char *sm_client_id; + char *sm_config_prefix; + + guint zoom_set : 1; + guint wait : 1; +} TerminalOptions; + +typedef struct +{ + char *profile; + gboolean profile_is_id; + char **exec_argv; + char *title; + char *working_dir; + double zoom; + GUnixFDList *fd_list; + GArray *fd_array; + guint zoom_set : 1; + guint active : 1; + guint wait : 1; +} InitialTab; + +typedef struct +{ + guint source_tag; + gboolean implicit_first_window; + + GList *tabs; /* list of InitialTab */ + + gboolean force_menubar_state; + gboolean menubar_state; + + gboolean start_fullscreen; + gboolean start_maximized; + + char *geometry; + char *role; + +} InitialWindow; + +#define TERMINAL_OPTION_ERROR (g_quark_from_static_string ("terminal-option-error")) + +typedef enum { + TERMINAL_OPTION_ERROR_NOT_SUPPORTED, + TERMINAL_OPTION_ERROR_NOT_IN_FACTORY, + TERMINAL_OPTION_ERROR_EXCLUSIVE_OPTIONS, + TERMINAL_OPTION_ERROR_INVALID_CONFIG_FILE, + TERMINAL_OPTION_ERROR_INCOMPATIBLE_CONFIG_FILE +} TerminalOptionError; + +TerminalOptions *terminal_options_parse (int *argcp, + char ***argvp, + GError **error); + +gboolean terminal_options_merge_config (TerminalOptions *options, + GKeyFile *key_file, + guint source_tag, + GError **error); + +void terminal_options_ensure_window (TerminalOptions *options); + +const char *terminal_options_get_service_name (TerminalOptions *options); + +const char *terminal_options_get_parent_screen_object_path (TerminalOptions *options); + +void terminal_options_free (TerminalOptions *options); + +typedef enum { + TERMINAL_VERBOSITY_QUIET = 0, + TERMINAL_VERBOSITY_NORMAL = 1, + TERMINAL_VERBOSITY_DETAIL = 2, + TERMINAL_VERBOSITY_DEBUG = 3 +} TerminalVerbosity; + +void terminal_fprintf (FILE* fp, + int verbosity_level, + const char* format, + ...) G_GNUC_PRINTF(3, 4); + +#define terminal_print_level(level,...) terminal_fprintf(stdout, TERMINAL_VERBOSITY_ ## level, __VA_ARGS__) +#define terminal_printerr_level(level,...) terminal_fprintf(stderr, TERMINAL_VERBOSITY_ ## level, __VA_ARGS__) + +#define terminal_print(...) terminal_print_level(NORMAL, __VA_ARGS__) +#define terminal_print_detail(...) terminal_print_level(DETAIL, __VA_ARGS__) +#define terminal_print_debug(...) terminal_print_level(DEBUG, __VA_ARGS__) + +#define terminal_printerr_detail(...) terminal_printerr_level(DETAIL, __VA_ARGS__) +#define terminal_printerr(...) terminal_printerr_level(NORMAL, __VA_ARGS__) +#define terminal_printerr_debug(...) terminal_printerr_level(DEBUG, __VA_ARGS__) + +GLogWriterOutput terminal_log_writer (GLogLevelFlags log_level, + const GLogField *fields, + gsize n_fields, + gpointer user_data); + +G_END_DECLS + +#endif /* !TERMINAL_OPTIONS_H */ diff --git a/src/terminal-pcre2.hh b/src/terminal-pcre2.hh new file mode 100644 index 0000000..af7e8e4 --- /dev/null +++ b/src/terminal-pcre2.hh @@ -0,0 +1,25 @@ +/* + * Copyright © 2015 Christian Persch + * + * This library 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 programme 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/>. + */ + +#pragma once + +#ifdef __VTE_VTE_H__ +#error "Must include terminal-pcre2.h before vte/vte.h" +#endif + +#define PCRE2_CODE_UNIT_WIDTH 0 +#include <pcre2.h> diff --git a/src/terminal-prefs-process.cc b/src/terminal-prefs-process.cc new file mode 100644 index 0000000..a19386f --- /dev/null +++ b/src/terminal-prefs-process.cc @@ -0,0 +1,513 @@ +/* + * Copyright © 2020, 2022 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 <unistd.h> +#include <fcntl.h> +#include <sys/socket.h> + +#include <glib.h> + +#include "terminal-settings-bridge-impl.hh" + +#include "terminal-app.hh" +#include "terminal-client-utils.hh" +#include "terminal-debug.hh" +#include "terminal-defines.hh" +#include "terminal-intl.hh" +#include "terminal-libgsystem.hh" +#include "terminal-prefs-process.hh" + +#include "terminal-settings-bridge-generated.h" + +struct _TerminalPrefsProcess { + GObject parent_instance; + + GSubprocess* subprocess; + GCancellable* cancellable; + GDBusConnection *connection; + TerminalSettingsBridgeImpl* bridge_impl; +}; + +struct _TerminalPrefsProcessClass { + GObjectClass parent_class; + + // Signals + void (*exited)(TerminalPrefsProcess* process, + int status); +}; + +enum { + SIGNAL_EXITED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; + +// helper functions + +template<typename T> +inline constexpr auto +IMPL(T* that) noexcept +{ + return reinterpret_cast<TerminalPrefsProcess*>(that); +} + +// BEGIN copied from vte/src/libc-glue.hh + +static inline int +fd_get_descriptor_flags(int fd) noexcept +{ + auto flags = int{}; + do { + flags = fcntl(fd, F_GETFD); + } while (flags == -1 && errno == EINTR); + + return flags; +} + +static inline int +fd_set_descriptor_flags(int fd, + int flags) noexcept +{ + auto r = int{}; + do { + r = fcntl(fd, F_SETFD, flags); + } while (r == -1 && errno == EINTR); + + return r; +} + +static inline int +fd_change_descriptor_flags(int fd, + int set_flags, + int unset_flags) noexcept +{ + auto const flags = fd_get_descriptor_flags(fd); + if (flags == -1) + return -1; + + auto const new_flags = (flags | set_flags) & ~unset_flags; + if (new_flags == flags) + return 0; + + return fd_set_descriptor_flags(fd, new_flags); +} + +static inline int +fd_get_status_flags(int fd) noexcept +{ + auto flags = int{}; + do { + flags = fcntl(fd, F_GETFL, 0); + } while (flags == -1 && errno == EINTR); + + return flags; +} + +static inline int +fd_set_status_flags(int fd, + int flags) noexcept +{ + auto r = int{}; + do { + r = fcntl(fd, F_SETFL, flags); + } while (r == -1 && errno == EINTR); + + return r; +} + +static inline int +fd_change_status_flags(int fd, + int set_flags, + int unset_flags) noexcept +{ + auto const flags = fd_get_status_flags(fd); + if (flags == -1) + return -1; + + auto const new_flags = (flags | set_flags) & ~unset_flags; + if (new_flags == flags) + return 0; + + return fd_set_status_flags(fd, new_flags); +} + +static inline int +fd_set_cloexec(int fd) noexcept +{ + return fd_change_descriptor_flags(fd, FD_CLOEXEC, 0); +} + +static inline int +fd_set_nonblock(int fd) noexcept +{ + return fd_change_status_flags(fd, O_NONBLOCK, 0); +} + +// END copied from vte + +static int +socketpair_cloexec_nonblock(int domain, + int type, + int protocol, + int sv[2]) noexcept +{ + auto r = int{}; +#if defined(SOCK_CLOEXEC) && defined(SOCK_NONBLOCK) + r = socketpair(domain, + type | SOCK_CLOEXEC | SOCK_NONBLOCK, + protocol, + sv); + if (r != -1) + return r; + + // Maybe cloexec and/or nonblock aren't supported by the kernel + if (errno != EINVAL && errno != EPROTOTYPE) + return r; + + // If so, fall back to applying the flags after the socketpair() call +#endif /* SOCK_CLOEXEC && SOCK_NONBLOCK */ + + r = socketpair(domain, type, protocol, sv); + if (r == -1) + return r; + + if (fd_set_cloexec(sv[0]) == -1 || + fd_set_nonblock(sv[0]) == -1 || + fd_set_cloexec(sv[1]) == -1 || + fd_set_nonblock(sv[1]) == -1) { + close(sv[0]); + close(sv[1]); + return -1; + } + + return r; +} + +static void +subprocess_wait_cb(GObject* source, + GAsyncResult* result, + void* user_data) +{ + gs_unref_object auto impl = IMPL(user_data); // ref added on g_subprocess_wait_async + + gs_free_error GError* error = nullptr; + if (g_subprocess_wait_finish(impl->subprocess, result, &error)) { + } // else: @cancellable was cancelled + + auto const status = g_subprocess_get_status(impl->subprocess); + + g_signal_emit(impl, signals[SIGNAL_EXITED], 0, status); + + g_clear_object(&impl->subprocess); +} + +// GInitable implementation + +static gboolean +terminal_prefs_process_initable_init(GInitable* initable, + GCancellable* cancellable, + GError** error) noexcept +{ + auto const impl = IMPL(initable); + + // Create a private D-Bus connection between the server and the preferences + // process, over which we proxy the settings (since otherwise there would + // be no way to modify the server's settings on backends other than dconf). + + int socket_fds[2]{-1, -1}; + auto r = socketpair_cloexec_nonblock(AF_UNIX, SOCK_STREAM, 0, socket_fds); + if (r != 0) { + auto const errsv = errno; + g_set_error(error, G_IO_ERROR, g_io_error_from_errno(errsv), + "Failed to create bridge socketpair: %s", + g_strerror(errsv)); + return false; + } + + // Launch process + auto const launcher = g_subprocess_launcher_new(GSubprocessFlags(0)); + // or use G_SUBPROCESS_FLAGS_STDOUT_SILENCE | G_SUBPROCESS_FLAGS_STDERR_SILENCE ? + + // Note that g_subprocess_launcher_set_cwd() is not necessary since + // the server's cwd is already $HOME. + g_subprocess_launcher_set_environ(launcher, nullptr); // inherit server's environment + g_subprocess_launcher_unsetenv(launcher, "DBUS_SESSION_BUS_ADDRESS"); + g_subprocess_launcher_unsetenv(launcher, "DBUS_STARTER_BUS_TYPE"); + // ? g_subprocess_launcher_setenv(launcher, "GSETTINGS_BACKEND", "bridge", true); + g_subprocess_launcher_take_fd(launcher, socket_fds[1], 3); + socket_fds[1] = -1; + + gs_free auto exe = terminal_client_get_file_uninstalled(TERM_LIBEXECDIR, + TERM_LIBEXECDIR, + TERMINAL_PREFERENCES_BINARY_NAME, + G_FILE_TEST_IS_EXECUTABLE); + + char *argv[16]; + auto argc = 0; + _TERMINAL_DEBUG_IF(TERMINAL_DEBUG_BRIDGE) { + argv[argc++] = (char*)"vte-2.91"; + argv[argc++] = (char*)"--fd=3"; + argv[argc++] = (char*)"--"; + argv[argc++] = (char*)"gdb"; + argv[argc++] = (char*)"--args"; + } + argv[argc++] = exe; + argv[argc++] = (char*)"--bus-fd=3"; + argv[argc++] = nullptr; + g_assert(argc <= int(G_N_ELEMENTS(argv))); + + impl->subprocess = g_subprocess_launcher_spawnv(launcher, // consumed + argv, + error); + if (!impl->subprocess) { + close(socket_fds[0]); + return false; + } + + g_subprocess_wait_async(impl->subprocess, + impl->cancellable, + GAsyncReadyCallback(subprocess_wait_cb), + g_object_ref(initable)); + + // Create server end of the D-Bus connection + gs_unref_object auto socket = g_socket_new_from_fd(socket_fds[0], error); + socket_fds[0] = -1; + + if (!socket) { + g_prefix_error(error, "Failed to create bridge GSocket: "); + g_subprocess_force_exit(impl->subprocess); + return false; + } + + gs_unref_object auto sockconn = + g_socket_connection_factory_create_connection(socket); + if (!G_IS_IO_STREAM(sockconn)) { + g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Bridge socket has incorrect type %s", G_OBJECT_TYPE_NAME(sockconn)); + g_subprocess_force_exit(impl->subprocess); + return false; + } + + gs_free auto guid = g_dbus_generate_guid(); + + impl->connection = + g_dbus_connection_new_sync(G_IO_STREAM(sockconn), + guid, + GDBusConnectionFlags(G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_SERVER | + G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_ALLOW_ANONYMOUS), + nullptr, // auth observer, + cancellable, + error); + if (!impl->connection) { + g_prefix_error(error, "Failed to create bridge D-Bus connection: "); + g_subprocess_force_exit(impl->subprocess); + return false; + } + + g_dbus_connection_set_exit_on_close(impl->connection, false); + + auto const app = terminal_app_get(); + impl->bridge_impl = + terminal_settings_bridge_impl_new(terminal_app_get_settings_backend(app)); + if (!g_dbus_interface_skeleton_export(G_DBUS_INTERFACE_SKELETON(impl->bridge_impl), + impl->connection, + TERMINAL_SETTINGS_BRIDGE_OBJECT_PATH, + error)) { + g_prefix_error(error, "Failed to export D-Bus skeleton: "); + g_subprocess_force_exit(impl->subprocess); + return false; + } + + return true; +} + +static void +terminal_prefs_process_initable_iface_init(GInitableIface* iface) noexcept +{ + iface->init = terminal_prefs_process_initable_init; +} + +static void +terminal_prefs_process_async_initable_iface_init(GAsyncInitableIface* iface) noexcept +{ + // Use the default implementation which runs the GInitiable in a thread. +} + +// Class Implementation + +G_DEFINE_TYPE_WITH_CODE(TerminalPrefsProcess, + terminal_prefs_process, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE(G_TYPE_INITABLE, + terminal_prefs_process_initable_iface_init) + G_IMPLEMENT_INTERFACE(G_TYPE_ASYNC_INITABLE, + terminal_prefs_process_async_initable_iface_init)) + +static void +terminal_prefs_process_init(TerminalPrefsProcess* process) /* noexcept */ +{ + g_application_hold(g_application_get_default()); +} + +static void +terminal_prefs_process_finalize(GObject* object) noexcept +{ + auto const impl = IMPL(object); + g_clear_object(&impl->bridge_impl); + g_clear_object(&impl->connection); + g_clear_object(&impl->cancellable); + g_clear_object(&impl->subprocess); + + G_OBJECT_CLASS(terminal_prefs_process_parent_class)->finalize(object); + + g_application_release(g_application_get_default()); +} + +static void +terminal_prefs_process_class_init(TerminalPrefsProcessClass* klass) /* noexcept */ +{ + auto const gobject_class = G_OBJECT_CLASS(klass); + gobject_class->finalize = terminal_prefs_process_finalize; + + signals[SIGNAL_EXITED] = + g_signal_new(I_("exited"), + G_OBJECT_CLASS_TYPE(gobject_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET(TerminalPrefsProcessClass, exited), + nullptr, nullptr, + g_cclosure_marshal_VOID__INT, + G_TYPE_NONE, + 1, + G_TYPE_INT); + g_signal_set_va_marshaller(signals[SIGNAL_EXITED], + G_OBJECT_CLASS_TYPE(klass), + g_cclosure_marshal_VOID__INTv); +} + +// public API + +TerminalPrefsProcess* +terminal_prefs_process_new_finish(GAsyncResult* result, + GError** error) +{ + gs_unref_object auto source = G_ASYNC_INITABLE(g_async_result_get_source_object(result)); + auto const o = g_async_initable_new_finish(source, result, error); + return reinterpret_cast<TerminalPrefsProcess*>(o); +} + +void +terminal_prefs_process_new_async(GCancellable* cancellable, + GAsyncReadyCallback callback, + void* user_data) +{ + g_async_initable_new_async(TERMINAL_TYPE_PREFS_PROCESS, + G_PRIORITY_DEFAULT, + cancellable, + callback, + user_data, + // properties, + nullptr); +} + +TerminalPrefsProcess* +terminal_prefs_process_new_sync(GCancellable* cancellable, + GError** error) +{ + return reinterpret_cast<TerminalPrefsProcess*> + (g_initable_new(TERMINAL_TYPE_PREFS_PROCESS, + cancellable, + error, + // properties, + nullptr)); +} + +void +terminal_prefs_process_abort(TerminalPrefsProcess* process) +{ + g_return_if_fail(TERMINAL_IS_PREFS_PROCESS(process)); + + auto const impl = IMPL(process); + if (impl->subprocess) + g_subprocess_force_exit(impl->subprocess); +} + +#ifdef ENABLE_DEBUG + +static void +show_cb(GObject* source, + GAsyncResult* result, + void* user_data) +{ + gs_unref_object auto process = IMPL(user_data); // added on g_dbus_connection_call + + gs_free_error GError *error = nullptr; + gs_unref_variant auto rv = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), + result, + &error); + + if (error) + _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, "terminal_prefs_process_show failed: %s\n", error->message); +} + +#endif /* ENABLE_DEBUG */ + +void +terminal_prefs_process_show(TerminalPrefsProcess* process, + char const* profile_uuid, + char const* hint, + unsigned timestamp) +{ + auto const impl = IMPL(process); + + auto builder = GVariantBuilder{}; + g_variant_builder_init(&builder, G_VARIANT_TYPE("(sava{sv})")); + g_variant_builder_add(&builder, "s", "preferences"); + g_variant_builder_open(&builder, G_VARIANT_TYPE("av")); // parameter + g_variant_builder_open(&builder, G_VARIANT_TYPE("v")); + g_variant_builder_open(&builder, G_VARIANT_TYPE("a{sv}")); + if (profile_uuid) + g_variant_builder_add(&builder, "{sv}", "profile", g_variant_new_string(profile_uuid)); + if (hint) + g_variant_builder_add(&builder, "{sv}", "hint", g_variant_new_string(hint)); + g_variant_builder_add(&builder, "{sv}", "timestamp", g_variant_new_uint32(timestamp)); + g_variant_builder_close(&builder); // a{sv} + g_variant_builder_close(&builder); // v + g_variant_builder_close(&builder); // av + g_variant_builder_open(&builder, G_VARIANT_TYPE("a{sv}")); // platform data + g_variant_builder_close(&builder); // a{sv} + + g_dbus_connection_call(impl->connection, + nullptr, // since not on a message bus + TERMINAL_PREFERENCES_OBJECT_PATH, + "org.gtk.Actions", + "Activate", + g_variant_builder_end(&builder), + G_VARIANT_TYPE("()"), + G_DBUS_CALL_FLAGS_NO_AUTO_START, + 30 * 1000, // ms timeout + impl->cancellable, // cancelleable +#ifdef ENABLE_DEBUG + show_cb, // callback + g_object_ref(process) // callback data +#else + nullptr, nullptr +#endif + ); +} diff --git a/src/terminal-prefs-process.hh b/src/terminal-prefs-process.hh new file mode 100644 index 0000000..a93a532 --- /dev/null +++ b/src/terminal-prefs-process.hh @@ -0,0 +1,53 @@ +/* + * Copyright © 2022 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/>. + */ + +#pragma once + +#include <gio/gio.h> + +G_BEGIN_DECLS + +#define TERMINAL_TYPE_PREFS_PROCESS (terminal_prefs_process_get_type ()) +#define TERMINAL_PREFS_PROCESS(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), TERMINAL_TYPE_PREFS_PROCESS, TerminalPrefsProcess)) +#define TERMINAL_PREFS_PROCESS_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), TERMINAL_TYPE_PREFS_PROCESS, TerminalPrefsProcessClass)) +#define TERMINAL_IS_PREFS_PROCESS(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), TERMINAL_TYPE_PREFS_PROCESS)) +#define TERMINAL_IS_PREFS_PROCESS_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), TERMINAL_TYPE_PREFS_PROCESS)) +#define TERMINAL_PREFS_PROCESS_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), TERMINAL_TYPE_PREFS_PROCESS, TerminalPrefsProcessClass)) + +typedef struct _TerminalPrefsProcess TerminalPrefsProcess; +typedef struct _TerminalPrefsProcessClass TerminalPrefsProcessClass; + +GType terminal_prefs_process_get_type(void); + +void terminal_prefs_process_new_async(GCancellable* cancellable, + GAsyncReadyCallback callback, + void* user_data); + +TerminalPrefsProcess* terminal_prefs_process_new_finish(GAsyncResult* result, + GError** error); + +TerminalPrefsProcess* terminal_prefs_process_new_sync(GCancellable* cancellable, + GError** error); + +void terminal_prefs_process_abort(TerminalPrefsProcess* process); + +void terminal_prefs_process_show(TerminalPrefsProcess* process, + char const* profile_uuid, + char const* hint, + unsigned timestamp); + +G_END_DECLS diff --git a/src/terminal-prefs.cc b/src/terminal-prefs.cc new file mode 100644 index 0000000..437c58a --- /dev/null +++ b/src/terminal-prefs.cc @@ -0,0 +1,950 @@ +/* + * Copyright © 2001, 2002 Havoc Pennington, Red Hat Inc. + * Copyright © 2008, 2011, 2012, 2013 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 <string.h> + +#include <uuid.h> + +#include <glib.h> +#include <glib/gi18n.h> +#include <gtk/gtk.h> + +#include "profile-editor.hh" +#include "terminal-prefs.hh" +#include "terminal-accels.hh" +#include "terminal-app.hh" +#include "terminal-debug.hh" +#include "terminal-schemas.hh" +#include "terminal-util.hh" +#include "terminal-profiles-list.hh" +#include "terminal-libgsystem.hh" + +PrefData *the_pref_data = nullptr; /* global */ + +/* Bottom */ + +static void +prefs_dialog_help_button_clicked_cb (GtkWidget *button, + PrefData *data) +{ + terminal_util_show_help ("pref"); +} + +static void +prefs_dialog_close_button_clicked_cb (GtkWidget *button, + PrefData *data) +{ + gtk_widget_destroy (data->dialog); +} + +/* Sidebar */ + +static inline GSimpleAction * +lookup_action (GtkWindow *window, + const char *name) +{ + GAction *action; + + action = g_action_map_lookup_action (G_ACTION_MAP (window), name); + g_return_val_if_fail (action != nullptr, nullptr); + + return G_SIMPLE_ACTION (action); +} + +/* Update the sidebar (visibility of icons, sensitivity of menu entries) to reflect the default and the selected profiles. */ +static void +listbox_update (GtkListBox *box) +{ + int i; + GtkListBoxRow *row; + GSettings *profile; + gs_unref_object GSettings *default_profile; + GtkStack *stack; + GtkMenuButton *button; + + default_profile = terminal_settings_list_ref_default_child (the_pref_data->profiles_list); + + /* GTK+ doesn't seem to like if a popover is assigned to multiple buttons at once + * (not even temporarily), so make sure to remove it from the previous button first. */ + for (i = 0; (row = gtk_list_box_get_row_at_index (box, i)) != nullptr; i++) { + button = (GtkMenuButton*)g_object_get_data (G_OBJECT (row), "popover-button"); + gtk_menu_button_set_popover (button, nullptr); + } + + for (i = 0; (row = gtk_list_box_get_row_at_index (box, i)) != nullptr; i++) { + profile = (GSettings*)g_object_get_data (G_OBJECT (row), "gsettings"); + + gboolean is_selected_profile = (profile != nullptr && profile == the_pref_data->selected_profile); + gboolean is_default_profile = (profile != nullptr && profile == default_profile); + + stack = (GtkStack*)g_object_get_data (G_OBJECT (row), "home-stack"); + gtk_stack_set_visible_child_name (stack, is_default_profile ? "home" : "placeholder"); + + stack = (GtkStack*)g_object_get_data (G_OBJECT (row), "popover-stack"); + gtk_stack_set_visible_child_name (stack, is_selected_profile ? "button" : "placeholder"); + if (is_selected_profile) { + g_simple_action_set_enabled (lookup_action (GTK_WINDOW (the_pref_data->dialog), "delete"), !is_default_profile); + g_simple_action_set_enabled (lookup_action (GTK_WINDOW (the_pref_data->dialog), "set-as-default"), !is_default_profile); + + GtkPopover *popover_menu = GTK_POPOVER (gtk_builder_get_object (the_pref_data->builder, "popover-menu")); + button = (GtkMenuButton*)g_object_get_data (G_OBJECT (row), "popover-button"); + gtk_menu_button_set_popover (button, GTK_WIDGET (popover_menu)); + gtk_popover_set_relative_to (popover_menu, GTK_WIDGET (button)); + } + } +} + +static void +update_window_title (void) +{ + GtkListBoxRow *row = the_pref_data->selected_list_box_row; + if (row == nullptr) + return; + + GSettings *profile = (GSettings*)g_object_get_data (G_OBJECT (row), "gsettings"); + GtkLabel *label = (GtkLabel*)g_object_get_data (G_OBJECT (row), "label"); + const char *text = gtk_label_get_text (label); + gs_free char *subtitle; + gs_free char *title; + + if (profile == nullptr) { + subtitle = g_strdup (text); + } else { + subtitle = g_strdup_printf (_("Profile “%s”"), text); + } + + title = g_strdup_printf (_("Preferences – %s"), subtitle); + gtk_window_set_title (GTK_WINDOW (the_pref_data->dialog), title); +} + +/* A new entry is selected in the sidebar */ +static void +listbox_row_selected_cb (GtkListBox *box, + GtkListBoxRow *row, + GtkStack *stack) +{ + profile_prefs_unload (); + + /* row can be nullptr intermittently during a profile meta operations */ + g_free (the_pref_data->selected_profile_uuid); + if (row != nullptr) { + the_pref_data->selected_profile = (GSettings*)g_object_get_data (G_OBJECT (row), "gsettings"); + the_pref_data->selected_profile_uuid = g_strdup ((char const*)g_object_get_data (G_OBJECT (row), "uuid")); + } else { + the_pref_data->selected_profile = nullptr; + the_pref_data->selected_profile_uuid = nullptr; + } + the_pref_data->selected_list_box_row = row; + + listbox_update (box); + + if (row != nullptr) { + if (the_pref_data->selected_profile != nullptr) { + profile_prefs_load (the_pref_data->selected_profile_uuid, the_pref_data->selected_profile); + } + + char const* stack_child_name = (char const*)g_object_get_data (G_OBJECT (row), "stack_child_name"); + gtk_stack_set_visible_child_name (stack, stack_child_name); + } + + update_window_title (); +} + +/* A profile's name changed, perhaps externally */ +static void +profile_name_changed_cb (GtkLabel *label, + GParamSpec *pspec, + GtkListBoxRow *row) +{ + gtk_list_box_row_changed (row); /* trigger re-sorting */ + + if (row == the_pref_data->selected_list_box_row) + update_window_title (); +} + +/* Select a profile in the sidebar by UUID */ +static gboolean +listbox_select_profile (const char *uuid) +{ + GtkListBoxRow *row; + for (int i = 0; (row = gtk_list_box_get_row_at_index (the_pref_data->listbox, i)) != nullptr; i++) { + const char *rowuuid = (char const*) g_object_get_data (G_OBJECT (row), "uuid"); + if (g_strcmp0 (rowuuid, uuid) == 0) { + g_signal_emit_by_name (row, "activate"); + return TRUE; + } + } + return FALSE; +} + +/* Create a new profile now, select it, update the UI. */ +static void +profile_new_now (const char *name) +{ + gs_free char *uuid = terminal_app_new_profile (terminal_app_get (), nullptr, name); + + listbox_select_profile (uuid); +} + +/* Clone the selected profile now, select it, update the UI. */ +static void +profile_clone_now (const char *name) +{ + if (the_pref_data->selected_profile == nullptr) + return; + + gs_free char *uuid = terminal_app_new_profile (terminal_app_get (), the_pref_data->selected_profile, name); + + listbox_select_profile (uuid); +} + +/* Rename the selected profile now, update the UI. */ +static void +profile_rename_now (const char *name) +{ + if (the_pref_data->selected_profile == nullptr) + return; + + /* This will automatically trigger a call to profile_name_changed_cb(). */ + g_settings_set_string (the_pref_data->selected_profile, TERMINAL_PROFILE_VISIBLE_NAME_KEY, name); +} + +/* Delete the selected profile now, update the UI. */ +static void +profile_delete_now (const char *dummy) +{ + if (the_pref_data->selected_profile == nullptr) + return; + + /* Prepare to select the next one, or if there's no such then the previous one. */ + int index = gtk_list_box_row_get_index (the_pref_data->selected_list_box_row); + GtkListBoxRow *new_selected_row = gtk_list_box_get_row_at_index (the_pref_data->listbox, index + 1); + if (new_selected_row == nullptr) + new_selected_row = gtk_list_box_get_row_at_index (the_pref_data->listbox, index - 1); + GSettings *new_selected_profile = (GSettings*)g_object_get_data (G_OBJECT (new_selected_row), "gsettings"); + gs_free char *uuid = nullptr; + if (new_selected_profile != nullptr) + uuid = terminal_settings_list_dup_uuid_from_child (the_pref_data->profiles_list, new_selected_profile); + + terminal_app_remove_profile (terminal_app_get (), the_pref_data->selected_profile); + + listbox_select_profile (uuid); +} + +/* "Set as default" selected. Do it now without asking for confirmation. */ +static void +profile_set_as_default_cb (GSimpleAction *simple, + GVariant *parameter, + gpointer user_data) +{ + if (the_pref_data->selected_profile_uuid == nullptr) + return; + + /* This will automatically trigger a call to listbox_update() via "default-changed". */ + terminal_settings_list_set_default_child (the_pref_data->profiles_list, the_pref_data->selected_profile_uuid); +} + + +static void +popover_dialog_cancel_clicked_cb (GtkButton *button, + gpointer user_data) +{ + GtkPopover *popover_dialog = GTK_POPOVER (gtk_builder_get_object (the_pref_data->builder, "popover-dialog")); + + gtk_popover_popdown (popover_dialog); +} + +static void +popover_dialog_ok_clicked_cb (GtkButton *button, + void (*fn) (const char *)) +{ + GtkEntry *entry = GTK_ENTRY (gtk_builder_get_object (the_pref_data->builder, "popover-dialog-entry")); + const char *name = gtk_entry_get_text (entry); + + /* Perform what we came for */ + (*fn) (name); + + /* Hide/popdown the popover */ + popover_dialog_cancel_clicked_cb (button, nullptr); +} + +static void +popover_dialog_closed_cb (GtkPopover *popover, + gpointer user_data) +{ + + GtkEntry *entry = GTK_ENTRY (gtk_builder_get_object (the_pref_data->builder, "popover-dialog-entry")); + gtk_entry_set_text (entry, ""); + + GtkButton *ok = GTK_BUTTON (gtk_builder_get_object (the_pref_data->builder, "popover-dialog-ok")); + GtkButton *cancel = GTK_BUTTON (gtk_builder_get_object (the_pref_data->builder, "popover-dialog-cancel")); + + g_signal_handlers_disconnect_matched (ok, G_SIGNAL_MATCH_FUNC, 0, 0, nullptr, + (void*)popover_dialog_ok_clicked_cb, nullptr); + g_signal_handlers_disconnect_matched (cancel, G_SIGNAL_MATCH_FUNC, 0, 0, nullptr, + (void*)popover_dialog_cancel_clicked_cb, nullptr); + g_signal_handlers_disconnect_matched (popover, G_SIGNAL_MATCH_FUNC, 0, 0, nullptr, + (void*)popover_dialog_closed_cb, nullptr); +} + + +/* Updates the OK button's sensitivity (insensitive if entry field is empty or whitespace only). + * The entry's initial value and OK's initial sensitivity have to match in the .ui file. */ +static void +popover_dialog_notify_text_cb (GtkEntry *entry, + GParamSpec *pspec, + GtkWidget *ok) +{ + gs_free char *text = g_strchomp (g_strdup (gtk_entry_get_text (entry))); + gtk_widget_set_sensitive (ok, text[0] != '\0'); +} + + +/* Common dialog for entering new profile name, or confirming deletion */ +static void +profile_popup_dialog (GtkWidget *relative_to, + const char *header, + const char *body, + const char *entry_text, + const char *ok_text, + void (*fn) (const char *)) +{ + GtkLabel *label1 = GTK_LABEL (gtk_builder_get_object (the_pref_data->builder, "popover-dialog-label1")); + gtk_label_set_text (label1, header); + + GtkLabel *label2 = GTK_LABEL (gtk_builder_get_object (the_pref_data->builder, "popover-dialog-label2")); + gtk_label_set_text (label2, body); + + GtkEntry *entry = GTK_ENTRY (gtk_builder_get_object (the_pref_data->builder, "popover-dialog-entry")); + if (entry_text != nullptr) { + gtk_entry_set_text (entry, entry_text); + gtk_widget_show (GTK_WIDGET (entry)); + } else { + gtk_entry_set_text (entry, "."); /* to make the OK button sensitive */ + gtk_widget_hide (GTK_WIDGET (entry)); + } + + GtkButton *ok = GTK_BUTTON (gtk_builder_get_object (the_pref_data->builder, "popover-dialog-ok")); + gtk_button_set_label (ok, ok_text); + GtkButton *cancel = GTK_BUTTON (gtk_builder_get_object (the_pref_data->builder, "popover-dialog-cancel")); + GtkPopover *popover_dialog = GTK_POPOVER (gtk_builder_get_object (the_pref_data->builder, "popover-dialog")); + + g_signal_connect (ok, "clicked", G_CALLBACK (popover_dialog_ok_clicked_cb), (void*)fn); + g_signal_connect (cancel, "clicked", G_CALLBACK (popover_dialog_cancel_clicked_cb), nullptr); + g_signal_connect (popover_dialog, "closed", G_CALLBACK (popover_dialog_closed_cb), nullptr); + + gtk_popover_set_relative_to (popover_dialog, relative_to); + gtk_popover_set_position (popover_dialog, GTK_POS_BOTTOM); + gtk_popover_set_default_widget (popover_dialog, GTK_WIDGET (ok)); + + gtk_popover_popup (popover_dialog); + + gtk_widget_grab_focus (entry_text != nullptr ? GTK_WIDGET (entry) : GTK_WIDGET (cancel)); +} + +/* "New" selected, ask for profile name */ +static void +profile_new_cb (GtkButton *button, + gpointer user_data) +{ + profile_popup_dialog (GTK_WIDGET (the_pref_data->new_profile_button), + _("New Profile"), + _("Enter name for new profile with default settings:"), + "", + _("Create"), + profile_new_now); +} + +/* "Clone" selected, ask for profile name */ +static void +profile_clone_cb (GSimpleAction *simple, + GVariant *parameter, + gpointer user_data) +{ + gs_free char *name = g_settings_get_string (the_pref_data->selected_profile, TERMINAL_PROFILE_VISIBLE_NAME_KEY); + + gs_free char *label = g_strdup_printf (_("Enter name for new profile based on “%s”:"), name); + gs_free char *clone_name = g_strdup_printf (_("%s (Copy)"), name); + + profile_popup_dialog (GTK_WIDGET (the_pref_data->selected_list_box_row), + _("Clone Profile"), + label, + clone_name, + _("Clone"), + profile_clone_now); +} + +/* "Rename" selected, ask for new name */ +static void +profile_rename_cb (GSimpleAction *simple, + GVariant *parameter, + gpointer user_data) +{ + if (the_pref_data->selected_profile == nullptr) + return; + + gs_free char *name = g_settings_get_string (the_pref_data->selected_profile, TERMINAL_PROFILE_VISIBLE_NAME_KEY); + + gs_free char *label = g_strdup_printf (_("Enter new name for profile “%s”:"), name); + + profile_popup_dialog (GTK_WIDGET (the_pref_data->selected_list_box_row), + _("Rename Profile"), + label, + name, + _("Rename"), + profile_rename_now); +} + +/* "Delete" selected, ask for confirmation */ +static void +profile_delete_cb (GSimpleAction *simple, + GVariant *parameter, + gpointer user_data) +{ + if (the_pref_data->selected_profile == nullptr) + return; + + gs_free char *name = g_settings_get_string (the_pref_data->selected_profile, TERMINAL_PROFILE_VISIBLE_NAME_KEY); + + gs_free char *label = g_strdup_printf (_("Really delete profile “%s”?"), name); + + profile_popup_dialog (GTK_WIDGET (the_pref_data->selected_list_box_row), + _("Delete Profile"), + label, + nullptr, + _("Delete"), + profile_delete_now); +} + +/* Create a (non-header) row of the sidebar, either a global or a profile entry. */ +static GtkListBoxRow * +listbox_create_row (const char *name, + const char *stack_child_name, + const char *uuid, + GSettings *gsettings /* adopted */, + gpointer sort_order) +{ + GtkListBoxRow *row = GTK_LIST_BOX_ROW (gtk_list_box_row_new ()); + + g_object_set_data_full (G_OBJECT (row), "stack_child_name", g_strdup (stack_child_name), g_free); + g_object_set_data_full (G_OBJECT (row), "uuid", g_strdup (uuid), g_free); + if (gsettings != nullptr) + g_object_set_data_full (G_OBJECT (row), "gsettings", gsettings, (GDestroyNotify)g_object_unref); + g_object_set_data (G_OBJECT (row), "sort_order", sort_order); + + GtkBox *hbox = GTK_BOX (gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0)); + gtk_widget_set_margin_start (GTK_WIDGET (hbox), 6); + gtk_widget_set_margin_end (GTK_WIDGET (hbox), 6); + gtk_widget_set_margin_top (GTK_WIDGET (hbox), 6); + gtk_widget_set_margin_bottom (GTK_WIDGET (hbox), 6); + + GtkLabel *label = GTK_LABEL (gtk_label_new (name)); + if (gsettings != nullptr) { + g_signal_connect (label, "notify::label", G_CALLBACK (profile_name_changed_cb), row); + g_settings_bind (gsettings, + TERMINAL_PROFILE_VISIBLE_NAME_KEY, + label, + "label", + G_SETTINGS_BIND_GET); + } + gtk_label_set_xalign (label, 0); + gtk_box_pack_start (hbox, GTK_WIDGET (label), TRUE, TRUE, 0); + g_object_set_data (G_OBJECT (row), "label", label); + + /* Always add the "default" symbol and the "menu" button, even on rows of global prefs. + * Use GtkStack to possible achieve visibility:hidden on it. + * This is so that all listbox rows have the same dimensions, and the width doesn't change + * as you switch the default profile. */ + + GtkStack *popover_stack = GTK_STACK (gtk_stack_new ()); + gtk_widget_set_margin_start (GTK_WIDGET (popover_stack), 6); + GtkMenuButton *popover_button = GTK_MENU_BUTTON (gtk_menu_button_new ()); + gtk_button_set_relief (GTK_BUTTON (popover_button), GTK_RELIEF_NONE); + gtk_stack_add_named (popover_stack, GTK_WIDGET (popover_button), "button"); + GtkLabel *popover_label = GTK_LABEL (gtk_label_new ("")); + gtk_stack_add_named (popover_stack, GTK_WIDGET (popover_label), "placeholder"); + g_object_set_data (G_OBJECT (row), "popover-stack", popover_stack); + g_object_set_data (G_OBJECT (row), "popover-button", popover_button); + + gtk_box_pack_end (hbox, GTK_WIDGET (popover_stack), FALSE, FALSE, 0); + + GtkStack *home_stack = GTK_STACK (gtk_stack_new ()); + gtk_widget_set_margin_start (GTK_WIDGET (home_stack), 12); + GtkImage *home_image = GTK_IMAGE (gtk_image_new_from_icon_name ("emblem-default-symbolic", GTK_ICON_SIZE_BUTTON)); + gtk_widget_set_tooltip_text (GTK_WIDGET (home_image), _("This is the default profile")); + gtk_stack_add_named (home_stack, GTK_WIDGET (home_image), "home"); + GtkLabel *home_label = GTK_LABEL (gtk_label_new ("")); + gtk_stack_add_named (home_stack, GTK_WIDGET (home_label), "placeholder"); + g_object_set_data (G_OBJECT (row), "home-stack", home_stack); + + gtk_box_pack_end (hbox, GTK_WIDGET (home_stack), FALSE, FALSE, 0); + + gtk_container_add (GTK_CONTAINER (row), GTK_WIDGET (hbox)); + + gtk_widget_show_all (GTK_WIDGET (row)); + + gtk_stack_set_visible_child_name (popover_stack, "placeholder"); + gtk_stack_set_visible_child_name (home_stack, "placeholder"); + + return row; +} + +/* Add all the non-profile rows to the sidebar */ +static void +listbox_add_all_globals (PrefData *data) +{ + GtkListBoxRow *row; + + row = listbox_create_row (_("General"), + "general-prefs", + nullptr, nullptr, (gpointer) 0); + gtk_list_box_insert (data->listbox, GTK_WIDGET (row), -1); + + row = listbox_create_row (_("Shortcuts"), + "shortcut-prefs", + nullptr, nullptr, (gpointer) 1); + gtk_list_box_insert (data->listbox, GTK_WIDGET (row), -1); +} + +/* Remove all the profile rows from the sidebar */ +static void +listbox_remove_all_profiles (PrefData *data) +{ + int i = 0; + + data->selected_profile = nullptr; + g_free (data->selected_profile_uuid); + data->selected_profile_uuid = nullptr; + profile_prefs_unload (); + + GtkListBoxRow *row = gtk_list_box_get_row_at_index (GTK_LIST_BOX (the_pref_data->listbox), 0); + g_signal_emit_by_name (row, "activate"); + + while ((row = gtk_list_box_get_row_at_index (data->listbox, i)) != nullptr) { + if (g_object_get_data (G_OBJECT (row), "gsettings") != nullptr) { + gtk_widget_destroy (GTK_WIDGET (row)); + } else { + i++; + } + } +} + +/* Add all the profiles to the sidebar */ +static void +listbox_add_all_profiles (PrefData *data) +{ + GList *list, *l; + GtkListBoxRow *row; + + list = terminal_settings_list_ref_children (data->profiles_list); + + for (l = list; l != nullptr; l = l->next) { + GSettings *profile = (GSettings *) l->data; + gs_free gchar *uuid = terminal_settings_list_dup_uuid_from_child (data->profiles_list, profile); + + row = listbox_create_row (nullptr, + "profile-prefs", + uuid, + profile /* adopts */, + (gpointer) 42); + gtk_list_box_insert (data->listbox, GTK_WIDGET (row), -1); + } + + g_list_free(list); /* the items themselves were adopted into the model above */ + + listbox_update (data->listbox); /* FIXME: This is not needed but I don't know why :-) */ +} + +/* Re-add all the profiles to the sidebar. + * This is called when a profile is added or removed, and also when the list of profiles is + * modified externally. + * Try to keep the selected profile, whenever possible. + * When the list is modified externally, the terminal_settings_list_*() methods seem to preserve + * the GSettings object for every profile that remains in the list. There's no guarantee however + * that a newly created GSettings can't receive the same address that a ceased one used to have. + * So don't rely on GSettings* to keep track of the selected profile, use the UUID instead. */ +static void +listbox_readd_profiles (PrefData *data) +{ + gs_free char *uuid = g_strdup (data->selected_profile_uuid); + + listbox_remove_all_profiles (data); + listbox_add_all_profiles (data); + + if (uuid != nullptr) + listbox_select_profile (uuid); +} + +/* Create a header row ("Global" or "Profiles +") */ +static GtkWidget * +listboxrow_create_header (const char *text, + gboolean visible_button) +{ + GtkBox *hbox = GTK_BOX (gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0)); + gtk_widget_set_margin_start (GTK_WIDGET (hbox), 6); + gtk_widget_set_margin_end (GTK_WIDGET (hbox), 6); + gtk_widget_set_margin_top (GTK_WIDGET (hbox), 6); + gtk_widget_set_margin_bottom (GTK_WIDGET (hbox), 6); + + GtkLabel *label = GTK_LABEL (gtk_label_new (nullptr)); + gs_free char *markup = g_markup_printf_escaped ("<b>%s</b>", text); + gtk_label_set_markup (label, markup); + gtk_label_set_xalign (label, 0); + gtk_box_pack_start (hbox, GTK_WIDGET (label), TRUE, TRUE, 0); + + /* Always add a "new profile" button. Use GtkStack to possible achieve visibility:hidden on it. + * This is so that both header rows have the same dimensions. */ + + GtkStack *stack = GTK_STACK (gtk_stack_new ()); + GtkButton *button = GTK_BUTTON (gtk_button_new_from_icon_name ("list-add-symbolic", GTK_ICON_SIZE_BUTTON)); + gtk_button_set_relief (button, GTK_RELIEF_NONE); + gtk_stack_add_named (stack, GTK_WIDGET (button), "button"); + GtkLabel *labelx = GTK_LABEL (gtk_label_new ("")); + gtk_stack_add_named (stack, GTK_WIDGET (labelx), "placeholder"); + + gtk_box_pack_end (hbox, GTK_WIDGET (stack), FALSE, FALSE, 0); + + gtk_widget_show_all (GTK_WIDGET (hbox)); + + if (visible_button) { + gtk_stack_set_visible_child_name (stack, "button"); + g_signal_connect (button, "clicked", G_CALLBACK (profile_new_cb), nullptr); + the_pref_data->new_profile_button = GTK_WIDGET (button); + } else { + gtk_stack_set_visible_child_name (stack, "placeholder"); + } + + return GTK_WIDGET (hbox); +} + +/* Manage the creation or removal of the header row ("Global" or "Profiles +") */ +static void +listboxrow_update_header (GtkListBoxRow *row, + GtkListBoxRow *before, + gpointer user_data) +{ + if (before == nullptr) { + if (gtk_list_box_row_get_header (row) == nullptr) { + gtk_list_box_row_set_header (row, listboxrow_create_header (_("Global"), FALSE)); + } + return; + } + + GSettings *profile = (GSettings*)g_object_get_data (G_OBJECT (row), "gsettings"); + if (profile != nullptr) { + GSettings *profile_before = (GSettings*)g_object_get_data (G_OBJECT (before), "gsettings"); + if (profile_before != nullptr) { + gtk_list_box_row_set_header (row, nullptr); + } else { + if (gtk_list_box_row_get_header (row) == nullptr) { + gtk_list_box_row_set_header (row, listboxrow_create_header (_("Profiles"), TRUE)); + } + } + } +} + +/* Sort callback for rows of the sidebar (global and profile ones). + * Global ones are kept at the top in fixed order. This is implemented via sort_order + * which is an integer disguised as a pointer for ease of implementation. + * Profile ones are sorted lexicographically. */ +static gint +listboxrow_compare_cb (GtkListBoxRow *row1, + GtkListBoxRow *row2, + gpointer user_data) +{ + gpointer sort_order_1 = g_object_get_data (G_OBJECT (row1), "sort_order"); + gpointer sort_order_2 = g_object_get_data (G_OBJECT (row2), "sort_order"); + + if (sort_order_1 != sort_order_2) + return sort_order_1 < sort_order_2 ? -1 : 1; + + GtkLabel *label1 = (GtkLabel*)g_object_get_data (G_OBJECT (row1), "label"); + const char *text1 = gtk_label_get_text (label1); + GtkLabel *label2 = (GtkLabel*)g_object_get_data (G_OBJECT (row2), "label"); + const char *text2 = gtk_label_get_text (label2); + + return g_utf8_collate (text1, text2); +} + +/* Keybindings tab */ + +/* Make sure the treeview is repainted with the correct text color, see bug 792139. */ +static void +shortcuts_button_toggled_cb (GtkWidget *widget, + GtkTreeView *tree_view) +{ + gtk_widget_queue_draw (GTK_WIDGET (tree_view)); +} + +/* misc */ + +static void +prefs_dialog_destroy_cb (GtkWidget *widget, + PrefData *data) +{ + /* Don't run this handler again */ + g_signal_handlers_disconnect_by_func (widget, (void*)prefs_dialog_destroy_cb, data); + + g_signal_handlers_disconnect_by_func (data->profiles_list, + (void*)listbox_readd_profiles, data); + g_signal_handlers_disconnect_by_func (data->profiles_list, + (void*)listbox_update, data->listbox); + + profile_prefs_destroy (); + + g_object_unref (data->builder); + g_free (data->selected_profile_uuid); + g_free (data); +} + +static void +make_default_button_clicked_cb(GtkWidget* button, + PrefData* data) +{ + terminal_app_make_default_terminal(terminal_app_get()); +} + +void +terminal_prefs_show_preferences(GSettings* profile, + char const* widget_name, + unsigned timestamp) +{ + TerminalApp *app = terminal_app_get (); + PrefData *data; + GtkWidget *dialog, *tree_view; + GtkWidget *show_menubar_button, *disable_mnemonics_button, *disable_menu_accel_button; + GtkWidget *disable_shortcuts_button; + GtkWidget *theme_variant_label, *theme_variant_combo; + GtkWidget *new_terminal_mode_label, *new_terminal_mode_combo; + GtkWidget *new_tab_position_combo; + GtkWidget *close_button, *help_button; + GtkWidget *content_box, *general_frame, *keybindings_frame; + GtkWidget *always_check_default_button, *make_default_button; + GSettings *settings; + + const GActionEntry action_entries[] = { + { "clone", profile_clone_cb, nullptr, nullptr, nullptr }, + { "rename", profile_rename_cb, nullptr, nullptr, nullptr }, + { "delete", profile_delete_cb, nullptr, nullptr, nullptr }, + { "set-as-default", profile_set_as_default_cb, nullptr, nullptr, nullptr }, + }; + + if (the_pref_data != nullptr) + goto done; + + { + the_pref_data = g_new0 (PrefData, 1); + data = the_pref_data; + data->profiles_list = terminal_app_get_profiles_list (app); + + /* FIXME this method is only used from here. Inline it here instead. */ + data->builder = terminal_util_load_widgets_resource ("/org/gnome/terminal/ui/preferences.ui", + "preferences-dialog", + "preferences-dialog", &dialog, + "dialogue-content-box", &content_box, + "general-frame", &general_frame, + "keybindings-frame", &keybindings_frame, + "close-button", &close_button, + "help-button", &help_button, + "default-show-menubar-checkbutton", &show_menubar_button, + "theme-variant-label", &theme_variant_label, + "theme-variant-combobox", &theme_variant_combo, + "new-terminal-mode-label", &new_terminal_mode_label, + "new-terminal-mode-combobox", &new_terminal_mode_combo, + "disable-mnemonics-checkbutton", &disable_mnemonics_button, + "disable-shortcuts-checkbutton", &disable_shortcuts_button, + "disable-menu-accel-checkbutton", &disable_menu_accel_button, + "new-tab-position-combobox", &new_tab_position_combo, + "always-check-default-checkbutton", &always_check_default_button, + "make-default-button", &make_default_button, + "accelerators-treeview", &tree_view, + "the-stack", &data->stack, + "the-listbox", &data->listbox, + nullptr); + + data->dialog = dialog; + + gtk_window_set_application (GTK_WINDOW (data->dialog), GTK_APPLICATION (terminal_app_get ())); + + terminal_util_bind_mnemonic_label_sensitivity (dialog); + + settings = terminal_app_get_global_settings (app); + + g_action_map_add_action_entries (G_ACTION_MAP (dialog), + action_entries, G_N_ELEMENTS (action_entries), + data); + + /* Sidebar */ + + gtk_list_box_set_header_func (GTK_LIST_BOX (data->listbox), + listboxrow_update_header, + nullptr, + nullptr); + g_signal_connect (data->listbox, "row-selected", G_CALLBACK (listbox_row_selected_cb), data->stack); + gtk_list_box_set_sort_func (data->listbox, listboxrow_compare_cb, nullptr, nullptr); + + listbox_add_all_globals (data); + listbox_add_all_profiles (data); + g_signal_connect_swapped (data->profiles_list, "children-changed", + G_CALLBACK (listbox_readd_profiles), data); + g_signal_connect_swapped (data->profiles_list, "default-changed", + G_CALLBACK (listbox_update), data->listbox); + + GtkEntry *entry = GTK_ENTRY (gtk_builder_get_object (the_pref_data->builder, "popover-dialog-entry")); + GtkButton *ok = GTK_BUTTON (gtk_builder_get_object (the_pref_data->builder, "popover-dialog-ok")); + g_signal_connect (entry, "notify::text", G_CALLBACK (popover_dialog_notify_text_cb), ok); + + /* General page */ + + gboolean shell_shows_menubar; + g_object_get (gtk_settings_get_default (), + "gtk-shell-shows-menubar", &shell_shows_menubar, + nullptr); + if (shell_shows_menubar || terminal_app_get_use_headerbar (app)) { + gtk_widget_set_visible (show_menubar_button, FALSE); + } else { + g_settings_bind (settings, + TERMINAL_SETTING_DEFAULT_SHOW_MENUBAR_KEY, + show_menubar_button, + "active", + GSettingsBindFlags(G_SETTINGS_BIND_GET | G_SETTINGS_BIND_SET)); + } + + g_settings_bind (settings, + TERMINAL_SETTING_THEME_VARIANT_KEY, + theme_variant_combo, + "active-id", + GSettingsBindFlags(G_SETTINGS_BIND_GET | G_SETTINGS_BIND_SET)); + + if (terminal_app_get_menu_unified (app) || + terminal_app_get_use_headerbar (app)) { + g_settings_bind (settings, + TERMINAL_SETTING_NEW_TERMINAL_MODE_KEY, + new_terminal_mode_combo, + "active-id", + GSettingsBindFlags(G_SETTINGS_BIND_GET | G_SETTINGS_BIND_SET)); + } else { + gtk_widget_set_visible (new_terminal_mode_label, FALSE); + gtk_widget_set_visible (new_terminal_mode_combo, FALSE); + } + + g_settings_bind (settings, + TERMINAL_SETTING_NEW_TAB_POSITION_KEY, + new_tab_position_combo, + "active-id", + GSettingsBindFlags(G_SETTINGS_BIND_GET | G_SETTINGS_BIND_SET)); + + if (shell_shows_menubar) { + gtk_widget_set_visible (disable_mnemonics_button, FALSE); + } else { + g_settings_bind (settings, + TERMINAL_SETTING_ENABLE_MNEMONICS_KEY, + disable_mnemonics_button, + "active", + GSettingsBindFlags(G_SETTINGS_BIND_GET | G_SETTINGS_BIND_SET)); + } + g_settings_bind (settings, + TERMINAL_SETTING_ENABLE_MENU_BAR_ACCEL_KEY, + disable_menu_accel_button, + "active", + GSettingsBindFlags(G_SETTINGS_BIND_GET | G_SETTINGS_BIND_SET)); + + g_settings_bind(settings, + TERMINAL_SETTING_ALWAYS_CHECK_DEFAULT_KEY, + always_check_default_button, + "active", + GSettingsBindFlags(G_SETTINGS_BIND_GET | G_SETTINGS_BIND_SET)); + + g_signal_connect(make_default_button, "clicked", + G_CALLBACK(make_default_button_clicked_cb), data); + + g_object_bind_property(app, "is-default-terminal", + make_default_button, "sensitive", + GBindingFlags(G_BINDING_DEFAULT | + G_BINDING_SYNC_CREATE | + G_BINDING_INVERT_BOOLEAN)); + + /* Shortcuts page */ + + g_settings_bind (settings, + TERMINAL_SETTING_ENABLE_SHORTCUTS_KEY, + disable_shortcuts_button, + "active", + GSettingsBindFlags(G_SETTINGS_BIND_GET | G_SETTINGS_BIND_SET)); + + g_signal_connect (disable_shortcuts_button, "toggled", + G_CALLBACK (shortcuts_button_toggled_cb), tree_view); + + terminal_accels_fill_treeview (tree_view, disable_shortcuts_button); + + /* Profile page */ + + profile_prefs_init (); + + /* Move action widgets to titlebar when headerbar is used */ + if (terminal_app_get_dialog_use_headerbar (app)) { + GtkWidget *headerbar; + GtkWidget *bbox; + + headerbar = (GtkWidget*)g_object_new (GTK_TYPE_HEADER_BAR, + "show-close-button", TRUE, + nullptr); + bbox = gtk_widget_get_parent (help_button); + + gtk_container_remove (GTK_CONTAINER (bbox), (GtkWidget*)g_object_ref (help_button)); + gtk_header_bar_pack_start (GTK_HEADER_BAR (headerbar), help_button); + g_object_unref (help_button); + + gtk_style_context_add_class (gtk_widget_get_style_context (help_button), + "text-button"); + + gtk_widget_show (headerbar); + gtk_widget_hide (bbox); + + gtk_window_set_titlebar (GTK_WINDOW (dialog), headerbar); + + /* Remove extra spacing around the content, and extra frames */ + g_object_set (G_OBJECT (content_box), "margin", 0, nullptr); + gtk_frame_set_shadow_type (GTK_FRAME (general_frame), GTK_SHADOW_NONE); + gtk_frame_set_shadow_type (GTK_FRAME (keybindings_frame), GTK_SHADOW_NONE); + } + + /* misc */ + + g_signal_connect (close_button, "clicked", G_CALLBACK (prefs_dialog_close_button_clicked_cb), data); + g_signal_connect (help_button, "clicked", G_CALLBACK (prefs_dialog_help_button_clicked_cb), data); + g_signal_connect (dialog, "destroy", G_CALLBACK (prefs_dialog_destroy_cb), data); + + g_object_add_weak_pointer (G_OBJECT (dialog), (gpointer *) &the_pref_data); + } + +done: + if (profile != nullptr) { + gs_free char *uuid = terminal_settings_list_dup_uuid_from_child (the_pref_data->profiles_list, profile); + listbox_select_profile (uuid); + } else { + GtkListBoxRow *row = gtk_list_box_get_row_at_index (GTK_LIST_BOX (the_pref_data->listbox), 0); + g_signal_emit_by_name (row, "activate"); + } + + terminal_util_dialog_focus_widget (the_pref_data->builder, widget_name); + + gtk_window_present_with_time(GTK_WINDOW(the_pref_data->dialog), timestamp); +} diff --git a/src/terminal-prefs.hh b/src/terminal-prefs.hh new file mode 100644 index 0000000..e1ce784 --- /dev/null +++ b/src/terminal-prefs.hh @@ -0,0 +1,57 @@ +/* + * Copyright © 2013 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/>. + */ + +#ifndef TERMINAL_PREFS_H +#define TERMINAL_PREFS_H + +#include <gtk/gtk.h> + +#include "terminal-profiles-list.hh" + +G_BEGIN_DECLS + +/* FIXME move back to the .c file if profile-editor.c is also merged there, + * also remove the terminal-profiles-list.h incude above. */ +/* FIXME PrefData is a very bad name, rename to PrefsDialog maybe? */ + +/* Everything about a preferences dialog */ +typedef struct { + TerminalSettingsList *profiles_list; + + GSettings *selected_profile; + GtkListBoxRow *selected_list_box_row; + char *selected_profile_uuid; /* a copy thereof, to survive changes to profiles_list */ + + GtkBuilder *builder; + GtkWidget *dialog; + GtkListBox *listbox; + GtkWidget *new_profile_button; + GtkWidget *stack; + + GArray *profile_signals; + GArray *profile_bindings; +} PrefData; + +extern PrefData *the_pref_data; /* global */ + +void terminal_prefs_show_preferences(GSettings* profile, + char const* widget_name, + unsigned timestamp); + +G_END_DECLS + +#endif /* TERMINAL_PREFS_H */ diff --git a/src/terminal-profiles-list.cc b/src/terminal-profiles-list.cc new file mode 100644 index 0000000..e791087 --- /dev/null +++ b/src/terminal-profiles-list.cc @@ -0,0 +1,272 @@ +/* + * Copyright © 2001, 2002 Havoc Pennington + * Copyright © 2002 Red Hat, Inc. + * Copyright © 2002 Sun Microsystems + * Copyright © 2003 Mariano Suarez-Alvarez + * Copyright © 2011, 2013 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 "terminal-profiles-list.hh" +#include "terminal-schemas.hh" +#include "terminal-libgsystem.hh" + +#include <string.h> +#include <uuid.h> + +/* Counts occurrences of @str in @strv */ +static guint +strv_contains (char **strv, + const char *str, + guint *idx) +{ + guint n, i; + + if (strv == nullptr) + return 0; + + n = 0; + for (i = 0; strv[i]; i++) { + if (strcmp (strv[i], str) == 0) { + n++; + if (idx) + *idx = i; + } + } + + return n; +} + +static gboolean +valid_uuid (const char *str, + GError **error) +{ + if (terminal_settings_list_valid_uuid (str)) + return TRUE; + + g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, + "\"%s\" is not a valid UUID", str); + return FALSE; +} + +/** + * terminal_profiles_list_new: + * @backend: a #GSettingsBackend + * @schema_source: a #GSettingsSchemaSource + * + * Returns: (transfer full): a new #TerminalSettingsList for the profiles list + */ +TerminalSettingsList * +terminal_profiles_list_new(GSettingsBackend* backend, + GSettingsSchemaSource* schema_source) +{ + return terminal_settings_list_new (backend, + schema_source, + TERMINAL_PROFILES_PATH_PREFIX, + TERMINAL_PROFILES_LIST_SCHEMA, + TERMINAL_PROFILE_SCHEMA, + TERMINAL_SETTINGS_LIST_FLAG_HAS_DEFAULT); +} + +static void +get_profile_names (TerminalSettingsList *list, + char ***profilesp, + char ***namesp) +{ + char **profiles, **names; + guint i, n; + + *profilesp = profiles = terminal_settings_list_dupv_children (list); + + n = g_strv_length (profiles); + *namesp = names = g_new0 (char *, n + 1); + for (i = 0; i < n; i++) { + gs_unref_object GSettings *profile; + + profile = terminal_settings_list_ref_child (list, profiles[i]); + names[i] = g_settings_get_string (profile, TERMINAL_PROFILE_VISIBLE_NAME_KEY); + } + + names[n] = nullptr; +} + +/** + * terminal_profiles_list_ref_children_sorted: + * @list: + * + * Returns: (transfer full): + */ +GList * +terminal_profiles_list_ref_children_sorted (TerminalSettingsList *list) +{ + return g_list_sort (terminal_settings_list_ref_children (list), + terminal_profiles_compare); +} + +/** + * terminal_profiles_list_dup_uuid: + * @list: + * @uuid: (allow-none): + * @error: + * + * Returns: (transfer full): the UUID of the profile specified by @uuid, or %nullptr + */ +char * +terminal_profiles_list_dup_uuid (TerminalSettingsList *list, + const char *uuid, + GError **error) +{ + char *rv; + + if (uuid == nullptr) { + rv = terminal_settings_list_dup_default_child (list); + if (rv == nullptr) + goto err; + return rv; + } else if (!valid_uuid (uuid, error)) + return nullptr; + + if (terminal_settings_list_has_child (list, uuid)) + return g_strdup (uuid); + + err: + g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, + "No profile with UUID \"%s\" exists", uuid); + return nullptr; +} + +/** + * terminal_profiles_list_ref_profile_by_uuid_or_name: + * @list: + * @uuid: + * @error: + * + * Returns: (transfer full): the profile #GSettings specified by @uuid, or %nullptr + */ +GSettings * +terminal_profiles_list_ref_profile_by_uuid (TerminalSettingsList *list, + const char *uuid, + GError **error) +{ + gs_free char *profile_uuid; + GSettings *profile; + + profile_uuid = terminal_profiles_list_dup_uuid (list, uuid, error); + if (profile_uuid == nullptr) + return nullptr; + + profile = terminal_settings_list_ref_child (list, profile_uuid); + return profile; +} + +/** + * terminal_profiles_list_get_profile_by_uuid: + * @list: + * @uuid: (allow-none): + * @error: + * + * Returns: (transfer full): the UUID of the profile specified by @uuid, or %nullptr + */ +char * +terminal_profiles_list_dup_uuid_or_name (TerminalSettingsList *list, + const char *uuid_or_name, + GError **error) +{ + char **profiles, **profile_names; + char *rv; + guint n, i; + + rv = terminal_profiles_list_dup_uuid (list, uuid_or_name, nullptr); + if (rv != nullptr) + return rv; + + /* Not found as UUID; try finding a profile with this string as 'visible-name' */ + get_profile_names (list, &profiles, &profile_names); + n = strv_contains (profile_names, uuid_or_name, &i); + + if (n == 0) { + g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, + "No profile with UUID or name \"%s\" exists", uuid_or_name); + rv = nullptr; + } else if (n != 1) { + g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, + "No profile with UUID \"%s\" found and name is ambiguous", uuid_or_name); + rv = nullptr; + } else { + rv = g_strdup (profiles[i]); + } + + g_strfreev (profiles); + g_strfreev (profile_names); + + return rv; +} + +/** + * terminal_profiles_list_ref_profile_by_uuid_or_name: + * @list: + * @uuid: + * @error: + * + * Returns: (transfer full): the profile #GSettings specified by @uuid, or %nullptr + */ +GSettings * +terminal_profiles_list_ref_profile_by_uuid_or_name (TerminalSettingsList *list, + const char *uuid_or_name, + GError **error) +{ + gs_free char *uuid; + GSettings *profile; + + uuid = terminal_profiles_list_dup_uuid_or_name (list, uuid_or_name, error); + if (uuid == nullptr) + return nullptr; + + profile = terminal_settings_list_ref_child (list, uuid); + g_assert (profile != nullptr); + return profile; +} + +int +terminal_profiles_compare (gconstpointer pa, + gconstpointer pb) +{ + GSettings *a = (GSettings *) pa; + GSettings *b = (GSettings *) pb; + gs_free char *na = nullptr; + gs_free char *nb = nullptr; + gs_free char *patha = nullptr; + gs_free char *pathb = nullptr; + int result; + + if (pa == pb) + return 0; + if (pa == nullptr) + return 1; + if (pb == nullptr) + return -1; + + na = g_settings_get_string (a, TERMINAL_PROFILE_VISIBLE_NAME_KEY); + nb = g_settings_get_string (b, TERMINAL_PROFILE_VISIBLE_NAME_KEY); + result = g_utf8_collate (na, nb); + if (result != 0) + return result; + + g_object_get (a, "path", &patha, nullptr); + g_object_get (b, "path", &pathb, nullptr); + return strcmp (patha, pathb); +} diff --git a/src/terminal-profiles-list.hh b/src/terminal-profiles-list.hh new file mode 100644 index 0000000..afd8da5 --- /dev/null +++ b/src/terminal-profiles-list.hh @@ -0,0 +1,54 @@ +/* + * Copyright © 2013 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/>. + */ + +#ifndef TERMINAL_PROFILES_LIST_H +#define TERMINAL_PROFILES_LIST_H + +#include <glib.h> +#include <gio/gio.h> + +#include "terminal-settings-list.hh" + +G_BEGIN_DECLS + +TerminalSettingsList *terminal_profiles_list_new(GSettingsBackend* backend, + GSettingsSchemaSource* schema_source); + +GList *terminal_profiles_list_ref_children_sorted (TerminalSettingsList *list); + +char *terminal_profiles_list_dup_uuid (TerminalSettingsList *list, + const char *uuid, + GError **error); + +GSettings *terminal_profiles_list_ref_profile_by_uuid (TerminalSettingsList *list, + const char *uuid, + GError **error); + +char *terminal_profiles_list_dup_uuid_or_name (TerminalSettingsList *list, + const char *uuid_or_name, + GError **error); + +GSettings *terminal_profiles_list_ref_profile_by_uuid_or_name (TerminalSettingsList *list, + const char *uuid_or_name, + GError **error); + +int terminal_profiles_compare (gconstpointer pa, + gconstpointer pb); + +G_END_DECLS + +#endif /* TERMINAL_PROFILES_LIST_H */ diff --git a/src/terminal-regex.cc b/src/terminal-regex.cc new file mode 100644 index 0000000..3856c64 --- /dev/null +++ b/src/terminal-regex.cc @@ -0,0 +1,390 @@ +/* + * Copyright © 2015 Egmont Koblinger + * + * 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 <glib.h> +#include <stdio.h> + +#include "terminal-regex.hh" + +#ifdef TERMINAL_REGEX_MAIN + +/* Shorthand for expecting the pattern to match the entire input string */ +#define ENTIRE ((char *) 1) + +static char* +get_match (const char *pattern, const char *string, GRegexMatchFlags match_flags) +{ + GRegex *regex; + GMatchInfo *match_info; + gchar *match; + + regex = g_regex_new (pattern, GRegexCompileFlags(0), GRegexMatchFlags(0), nullptr); + g_regex_match (regex, string, match_flags, &match_info); + match = g_match_info_fetch (match_info, 0); + + g_free (regex); + g_free (match_info); + return match; +} + +/* Macros rather than functions to report useful line numbers on failure. */ +#define assert_match(__pattern, __string, __expected) do { \ + gchar *__actual_match = get_match(__pattern, __string, GRegexMatchFlags(0)); \ + const gchar *__expected_match = __expected; \ + if (__expected_match == ENTIRE) __expected_match = __string; \ + g_assert_cmpstr(__actual_match, ==, __expected_match); \ + g_free (__actual_match); \ +} while (0) + +#define assert_match_anchored(__pattern, __string, __expected) do { \ + gchar *__actual_match = get_match(__pattern, __string, G_REGEX_MATCH_ANCHORED); \ + const gchar *__expected_match = __expected; \ + if (__expected_match == ENTIRE) __expected_match = __string; \ + g_assert_cmpstr(__actual_match, ==, __expected_match); \ + g_free (__actual_match); \ +} while (0) + +int +main (int argc, char **argv) +{ + /* SCHEME is case insensitive */ + assert_match_anchored (SCHEME, "http", ENTIRE); + assert_match_anchored (SCHEME, "HTTPS", ENTIRE); + + /* USER is nonempty, alphanumeric, dot, plus and dash */ + assert_match_anchored (USER, "", nullptr); + assert_match_anchored (USER, "dr.john-smith", ENTIRE); + assert_match_anchored (USER, "abc+def@ghi", "abc+def"); + + /* PASS is optional colon-prefixed value, allowing quite some characters, but definitely not @ */ + assert_match_anchored (PASS, "", ENTIRE); + assert_match_anchored (PASS, "nocolon", ""); + assert_match_anchored (PASS, ":s3cr3T", ENTIRE); + assert_match_anchored (PASS, ":$?#@host", ":$?#"); + + /* Hostname of at least 1 component, containing at least one non-digit in at least one of the segments */ + assert_match_anchored (HOSTNAME1, "example.com", ENTIRE); + assert_match_anchored (HOSTNAME1, "a-b.c-d", ENTIRE); + assert_match_anchored (HOSTNAME1, "a_b", "a"); /* TODO: can/should we totally abort here? */ + assert_match_anchored (HOSTNAME1, "déjà-vu.com", ENTIRE); + assert_match_anchored (HOSTNAME1, "➡.ws", ENTIRE); + assert_match_anchored (HOSTNAME1, "cömbining-áccents", ENTIRE); + assert_match_anchored (HOSTNAME1, "12", nullptr); + assert_match_anchored (HOSTNAME1, "12.34", nullptr); + assert_match_anchored (HOSTNAME1, "12.ab", ENTIRE); +// assert_match_anchored (HOSTNAME1, "ab.12", nullptr); /* errr... could we fail here?? */ + + /* Hostname of at least 2 components, containing at least one non-digit in at least one of the segments */ + assert_match_anchored (HOSTNAME2, "example.com", ENTIRE); + assert_match_anchored (HOSTNAME2, "example", nullptr); + assert_match_anchored (HOSTNAME2, "12", nullptr); + assert_match_anchored (HOSTNAME2, "12.34", nullptr); + assert_match_anchored (HOSTNAME2, "12.ab", ENTIRE); + assert_match_anchored (HOSTNAME2, "ab.12", nullptr); +// assert_match_anchored (HOSTNAME2, "ab.cd.12", nullptr); /* errr... could we fail here?? */ + + /* IPv4 segment (number between 0 and 255) */ + assert_match_anchored (DEFS "(?&S4)", "0", ENTIRE); + assert_match_anchored (DEFS "(?&S4)", "1", ENTIRE); + assert_match_anchored (DEFS "(?&S4)", "9", ENTIRE); + assert_match_anchored (DEFS "(?&S4)", "10", ENTIRE); + assert_match_anchored (DEFS "(?&S4)", "99", ENTIRE); + assert_match_anchored (DEFS "(?&S4)", "100", ENTIRE); + assert_match_anchored (DEFS "(?&S4)", "200", ENTIRE); + assert_match_anchored (DEFS "(?&S4)", "250", ENTIRE); + assert_match_anchored (DEFS "(?&S4)", "255", ENTIRE); + assert_match_anchored (DEFS "(?&S4)", "256", nullptr); + assert_match_anchored (DEFS "(?&S4)", "260", nullptr); + assert_match_anchored (DEFS "(?&S4)", "300", nullptr); + assert_match_anchored (DEFS "(?&S4)", "1000", nullptr); + assert_match_anchored (DEFS "(?&S4)", "", nullptr); + assert_match_anchored (DEFS "(?&S4)", "a1b", nullptr); + + /* IPv4 addresses */ + assert_match_anchored (DEFS "(?&IPV4)", "11.22.33.44", ENTIRE); + assert_match_anchored (DEFS "(?&IPV4)", "0.1.254.255", ENTIRE); + assert_match_anchored (DEFS "(?&IPV4)", "75.150.225.300", nullptr); + assert_match_anchored (DEFS "(?&IPV4)", "1.2.3.4.5", "1.2.3.4"); /* we could also bail out and not match at all */ + + /* IPv6 addresses */ + assert_match_anchored (DEFS "(?&IPV6)", "11:::22", nullptr); + assert_match_anchored (DEFS "(?&IPV6)", "11:22::33:44::55:66", nullptr); + assert_match_anchored (DEFS "(?&IPV6)", "dead::beef", ENTIRE); + assert_match_anchored (DEFS "(?&IPV6)", "faded::bee", nullptr); + assert_match_anchored (DEFS "(?&IPV6)", "live::pork", nullptr); + assert_match_anchored (DEFS "(?&IPV6)", "::1", ENTIRE); + assert_match_anchored (DEFS "(?&IPV6)", "11::22:33::44", nullptr); + assert_match_anchored (DEFS "(?&IPV6)", "11:22:::33", nullptr); + assert_match_anchored (DEFS "(?&IPV6)", "dead:beef::192.168.1.1", ENTIRE); + assert_match_anchored (DEFS "(?&IPV6)", "192.168.1.1", nullptr); + assert_match_anchored (DEFS "(?&IPV6)", "11:22:33:44:55:66:77:87654", nullptr); + assert_match_anchored (DEFS "(?&IPV6)", "11:22::33:45678", nullptr); + assert_match_anchored (DEFS "(?&IPV6)", "11:22:33:44:55:66:192.168.1.12345", nullptr); + + assert_match_anchored (DEFS "(?&IPV6)", "11:22:33:44:55:66:77", nullptr); /* no :: */ + assert_match_anchored (DEFS "(?&IPV6)", "11:22:33:44:55:66:77:88", ENTIRE); + assert_match_anchored (DEFS "(?&IPV6)", "11:22:33:44:55:66:77:88:99", nullptr); + assert_match_anchored (DEFS "(?&IPV6)", "::11:22:33:44:55:66:77", ENTIRE); /* :: at the start */ + assert_match_anchored (DEFS "(?&IPV6)", "::11:22:33:44:55:66:77:88", nullptr); + assert_match_anchored (DEFS "(?&IPV6)", "11:22:33::44:55:66:77", ENTIRE); /* :: in the middle */ + assert_match_anchored (DEFS "(?&IPV6)", "11:22:33::44:55:66:77:88", nullptr); + assert_match_anchored (DEFS "(?&IPV6)", "11:22:33:44:55:66:77::", ENTIRE); /* :: at the end */ + assert_match_anchored (DEFS "(?&IPV6)", "11:22:33:44:55:66:77:88::", nullptr); + assert_match_anchored (DEFS "(?&IPV6)", "::", ENTIRE); /* :: only */ + + assert_match_anchored (DEFS "(?&IPV6)", "11:22:33:44:55:192.168.1.1", nullptr); /* no :: */ + assert_match_anchored (DEFS "(?&IPV6)", "11:22:33:44:55:66:192.168.1.1", ENTIRE); + assert_match_anchored (DEFS "(?&IPV6)", "11:22:33:44:55:66:77:192.168.1.1", nullptr); + assert_match_anchored (DEFS "(?&IPV6)", "::11:22:33:44:55:192.168.1.1", ENTIRE); /* :: at the start */ + assert_match_anchored (DEFS "(?&IPV6)", "::11:22:33:44:55:66:192.168.1.1", nullptr); + assert_match_anchored (DEFS "(?&IPV6)", "11:22:33::44:55:192.168.1.1", ENTIRE); /* :: in the imddle */ + assert_match_anchored (DEFS "(?&IPV6)", "11:22:33::44:55:66:192.168.1.1", nullptr); + assert_match_anchored (DEFS "(?&IPV6)", "11:22:33:44:55::192.168.1.1", ENTIRE); /* :: at the end(ish) */ + assert_match_anchored (DEFS "(?&IPV6)", "11:22:33:44:55:66::192.168.1.1", nullptr); + assert_match_anchored (DEFS "(?&IPV6)", "::192.168.1.1", ENTIRE); /* :: only(ish) */ + + /* URL_HOST is either a hostname, or an IPv4 address, or a bracket-enclosed IPv6 address */ + assert_match_anchored (DEFS URL_HOST, "example", ENTIRE); + assert_match_anchored (DEFS URL_HOST, "example.com", ENTIRE); + assert_match_anchored (DEFS URL_HOST, "11.22.33.44", ENTIRE); + assert_match_anchored (DEFS URL_HOST, "[11.22.33.44]", nullptr); + assert_match_anchored (DEFS URL_HOST, "dead::be:ef", "dead"); /* TODO: can/should we totally abort here? */ + assert_match_anchored (DEFS URL_HOST, "[dead::be:ef]", ENTIRE); + + /* EMAIL_HOST is either an at least two-component hostname, or a bracket-enclosed IPv[46] address */ + assert_match_anchored (DEFS EMAIL_HOST, "example", nullptr); + assert_match_anchored (DEFS EMAIL_HOST, "example.com", ENTIRE); + assert_match_anchored (DEFS EMAIL_HOST, "11.22.33.44", nullptr); + assert_match_anchored (DEFS EMAIL_HOST, "[11.22.33.44]", ENTIRE); + assert_match_anchored (DEFS EMAIL_HOST, "[11.22.33.456]", nullptr); + assert_match_anchored (DEFS EMAIL_HOST, "dead::be:ef", nullptr); + assert_match_anchored (DEFS EMAIL_HOST, "[dead::be:ef]", ENTIRE); + + /* Number between 1 and 65535 (helper for port) */ + assert_match_anchored (N_1_65535, "0", nullptr); + assert_match_anchored (N_1_65535, "1", ENTIRE); + assert_match_anchored (N_1_65535, "10", ENTIRE); + assert_match_anchored (N_1_65535, "100", ENTIRE); + assert_match_anchored (N_1_65535, "1000", ENTIRE); + assert_match_anchored (N_1_65535, "10000", ENTIRE); + assert_match_anchored (N_1_65535, "60000", ENTIRE); + assert_match_anchored (N_1_65535, "65000", ENTIRE); + assert_match_anchored (N_1_65535, "65500", ENTIRE); + assert_match_anchored (N_1_65535, "65530", ENTIRE); + assert_match_anchored (N_1_65535, "65535", ENTIRE); + assert_match_anchored (N_1_65535, "65536", nullptr); + assert_match_anchored (N_1_65535, "65540", nullptr); + assert_match_anchored (N_1_65535, "65600", nullptr); + assert_match_anchored (N_1_65535, "66000", nullptr); + assert_match_anchored (N_1_65535, "70000", nullptr); + assert_match_anchored (N_1_65535, "100000", nullptr); + assert_match_anchored (N_1_65535, "", nullptr); + assert_match_anchored (N_1_65535, "a1b", nullptr); + + /* PORT is an optional colon-prefixed value */ + assert_match_anchored (PORT, "", ENTIRE); + assert_match_anchored (PORT, ":1", ENTIRE); + assert_match_anchored (PORT, ":65535", ENTIRE); + assert_match_anchored (PORT, ":65536", ""); /* TODO: can/should we totally abort here? */ + + /* Parentheses are only allowed in matching pairs, see bug 763980. */ + /* TODO: add tests for PATHCHARS and PATHNONTERM; and/or URLPATH */ + assert_match_anchored (DEFS URLPATH, "/ab/cd", ENTIRE); + assert_match_anchored (DEFS URLPATH, "/ab/cd.html.", "/ab/cd.html"); + assert_match_anchored (DEFS URLPATH, "/The_Offspring_(album)", ENTIRE); + assert_match_anchored (DEFS URLPATH, "/The_Offspring)", "/The_Offspring"); + assert_match_anchored (DEFS URLPATH, "/a((b(c)d)e(f))", ENTIRE); + assert_match_anchored (DEFS URLPATH, "/a((b(c)d)e(f)))", "/a((b(c)d)e(f))"); + assert_match_anchored (DEFS URLPATH, "/a(b).(c).", "/a(b).(c)"); + assert_match_anchored (DEFS URLPATH, "/a.(b.(c.).).(d.(e.).).)", "/a.(b.(c.).).(d.(e.).)"); + assert_match_anchored (DEFS URLPATH, "/a)b(c", "/a"); + assert_match_anchored (DEFS URLPATH, "/.", "/"); + assert_match_anchored (DEFS URLPATH, "/(.", "/"); + assert_match_anchored (DEFS URLPATH, "/).", "/"); + assert_match_anchored (DEFS URLPATH, "/().", "/()"); + assert_match_anchored (DEFS URLPATH, "/", ENTIRE); + assert_match_anchored (DEFS URLPATH, "", ENTIRE); + assert_match_anchored (DEFS URLPATH, "?", ENTIRE); + assert_match_anchored (DEFS URLPATH, "?param=value", ENTIRE); + assert_match_anchored (DEFS URLPATH, "#", ENTIRE); + assert_match_anchored (DEFS URLPATH, "#anchor", ENTIRE); + assert_match_anchored (DEFS URLPATH, "/php?param[]=value1¶m[]=value2", ENTIRE); + assert_match_anchored (DEFS URLPATH, "/foo?param1[index1]=value1¶m2[index2]=value2", ENTIRE); + assert_match_anchored (DEFS URLPATH, "/[[[]][]]", ENTIRE); + assert_match_anchored (DEFS URLPATH, "/[([])]([()])", ENTIRE); + assert_match_anchored (DEFS URLPATH, "/([()])[([])]", ENTIRE); + assert_match_anchored (DEFS URLPATH, "/[(])", "/"); + assert_match_anchored (DEFS URLPATH, "/([)]", "/"); + + + /* Put the components together and test the big picture */ + + assert_match (REGEX_URL_AS_IS, "There's no URL here http:/foo", nullptr); + assert_match (REGEX_URL_AS_IS, "Visit http://example.com for details", "http://example.com"); + assert_match (REGEX_URL_AS_IS, "Trailing dot http://foo/bar.html.", "http://foo/bar.html"); + assert_match (REGEX_URL_AS_IS, "Trailing ellipsis http://foo/bar.html...", "http://foo/bar.html"); + assert_match (REGEX_URL_AS_IS, "Trailing comma http://foo/bar,baz,", "http://foo/bar,baz"); + assert_match (REGEX_URL_AS_IS, "Trailing semicolon http://foo/bar;baz;", "http://foo/bar;baz"); + assert_match (REGEX_URL_AS_IS, "See <http://foo/bar>", "http://foo/bar"); + assert_match (REGEX_URL_AS_IS, "<http://foo.bar/asdf.qwer.html>", "http://foo.bar/asdf.qwer.html"); + assert_match (REGEX_URL_AS_IS, "Go to http://192.168.1.1.", "http://192.168.1.1"); + assert_match (REGEX_URL_AS_IS, "If not, see <http://www.gnu.org/licenses/>.", "http://www.gnu.org/licenses/"); + assert_match (REGEX_URL_AS_IS, "<a href=\"http://foo/bar\">foo</a>", "http://foo/bar"); + assert_match (REGEX_URL_AS_IS, "<a href='http://foo/bar'>foo</a>", "http://foo/bar"); + assert_match (REGEX_URL_AS_IS, "<url>http://foo/bar</url>", "http://foo/bar"); + + assert_match (REGEX_URL_AS_IS, "http://", nullptr); + assert_match (REGEX_URL_AS_IS, "http://a", ENTIRE); + assert_match (REGEX_URL_AS_IS, "http://aa.", "http://aa"); + assert_match (REGEX_URL_AS_IS, "http://aa.b", ENTIRE); + assert_match (REGEX_URL_AS_IS, "http://aa.bb", ENTIRE); + assert_match (REGEX_URL_AS_IS, "http://aa.bb/c", ENTIRE); + assert_match (REGEX_URL_AS_IS, "http://aa.bb/cc", ENTIRE); + assert_match (REGEX_URL_AS_IS, "http://aa.bb/cc/", ENTIRE); + + assert_match (REGEX_URL_AS_IS, "HtTp://déjà-vu.com:10000/déjà/vu", ENTIRE); + assert_match (REGEX_URL_AS_IS, "HTTP://joe:sEcReT@➡.ws:1080", ENTIRE); + assert_match (REGEX_URL_AS_IS, "https://cömbining-áccents", ENTIRE); + + assert_match (REGEX_URL_AS_IS, "http://111.222.33.44", ENTIRE); + assert_match (REGEX_URL_AS_IS, "http://111.222.33.44/", ENTIRE); + assert_match (REGEX_URL_AS_IS, "http://111.222.33.44/foo", ENTIRE); + assert_match (REGEX_URL_AS_IS, "http://1.2.3.4:5555/xyz", ENTIRE); + assert_match (REGEX_URL_AS_IS, "https://[dead::beef]:12345/ipv6", ENTIRE); + assert_match (REGEX_URL_AS_IS, "https://[dead::beef:11.22.33.44]", ENTIRE); + assert_match (REGEX_URL_AS_IS, "http://1.2.3.4:", "http://1.2.3.4"); /* TODO: can/should we totally abort here? */ + assert_match (REGEX_URL_AS_IS, "https://dead::beef/no-brackets-ipv6", "https://dead"); /* ditto */ + assert_match (REGEX_URL_AS_IS, "http://111.222.333.444/", nullptr); + assert_match (REGEX_URL_AS_IS, "http://1.2.3.4:70000", "http://1.2.3.4"); /* TODO: can/should we totally abort here? */ + assert_match (REGEX_URL_AS_IS, "http://[dead::beef:111.222.333.444]", nullptr); + + /* '?' or '#' without '/', #7888 */ + assert_match (REGEX_URL_AS_IS, "http://foo.bar?", ENTIRE); + assert_match (REGEX_URL_AS_IS, "http://foo.bar?param=value", ENTIRE); + assert_match (REGEX_URL_AS_IS, "http://foo.bar:12345?param=value", ENTIRE); + assert_match (REGEX_URL_AS_IS, "http://1.2.3.4?param=value", ENTIRE); + assert_match (REGEX_URL_AS_IS, "http://[dead::beef]?param=value", ENTIRE); + assert_match (REGEX_URL_AS_IS, "http://foo.bar#", ENTIRE); + assert_match (REGEX_URL_AS_IS, "http://foo.bar#anchor", ENTIRE); + assert_match (REGEX_URL_AS_IS, "http://foo.bar:12345#anchor", ENTIRE); + assert_match (REGEX_URL_AS_IS, "http://1.2.3.4#anchor", ENTIRE); + assert_match (REGEX_URL_AS_IS, "http://[dead::beef]#anchor", ENTIRE); + + /* Username, password */ + assert_match (REGEX_URL_AS_IS, "http://joe@example.com", ENTIRE); + assert_match (REGEX_URL_AS_IS, "http://user.name:sec.ret@host.name", ENTIRE); + assert_match (REGEX_URL_AS_IS, "http://joe:secret@[::1]", ENTIRE); + assert_match (REGEX_URL_AS_IS, "http://dudewithnopassword:@example.com", ENTIRE); + assert_match (REGEX_URL_AS_IS, "http://safeguy:!#$%^&*@host", ENTIRE); + assert_match (REGEX_URL_AS_IS, "http://invalidusername!@host", "http://invalidusername"); + + assert_match (REGEX_URL_AS_IS, "http://ab.cd/ef?g=h&i=j|k=l#m=n:o=p", ENTIRE); + assert_match (REGEX_URL_AS_IS, "http:///foo", nullptr); + + /* Parentheses are only allowed in matching pairs, see bug 763980. */ + assert_match (REGEX_URL_AS_IS, "https://en.wikipedia.org/wiki/The_Offspring_(album)", ENTIRE); + assert_match (REGEX_URL_AS_IS, "[markdown](https://en.wikipedia.org/wiki/The_Offspring)", "https://en.wikipedia.org/wiki/The_Offspring"); + assert_match (REGEX_URL_AS_IS, "[markdown](https://en.wikipedia.org/wiki/The_Offspring_(album))", "https://en.wikipedia.org/wiki/The_Offspring_(album)"); + assert_match (REGEX_URL_AS_IS, "[markdown](http://foo.bar/(a(b)c)d)e)f", "http://foo.bar/(a(b)c)d"); + assert_match (REGEX_URL_AS_IS, "[markdown](http://foo.bar/a)b(c", "http://foo.bar/a"); + + /* Apostrophes are allowed, except at trailing position if the URL is preceded by an apostrophe, see bug 448044. */ + assert_match (REGEX_URL_AS_IS, "https://en.wikipedia.org/wiki/Moore's_law", ENTIRE); + assert_match (REGEX_URL_AS_IS, "<a href=\"https://en.wikipedia.org/wiki/Moore's_law\">", "https://en.wikipedia.org/wiki/Moore's_law"); + assert_match (REGEX_URL_AS_IS, "https://en.wikipedia.org/wiki/Cryin'", ENTIRE); + assert_match (REGEX_URL_AS_IS, "<a href=\"https://en.wikipedia.org/wiki/Cryin'\">", "https://en.wikipedia.org/wiki/Cryin'"); + assert_match (REGEX_URL_AS_IS, "<a href='https://en.wikipedia.org/wiki/Aerosmith'>", "https://en.wikipedia.org/wiki/Aerosmith"); + + /* No scheme */ + assert_match (REGEX_URL_HTTP, "www.foo.bar/baz", ENTIRE); + assert_match (REGEX_URL_HTTP, "WWW3.foo.bar/baz", ENTIRE); + assert_match (REGEX_URL_HTTP, "FTP.FOO.BAR/BAZ", ENTIRE); /* FIXME if no scheme is given and url starts with ftp, can we make the protocol ftp instead of http? */ + assert_match (REGEX_URL_HTTP, "ftpxy.foo.bar/baz", ENTIRE); +// assert_match (REGEX_URL_HTTP, "ftp.123/baz", nullptr); /* errr... could we fail here?? */ + assert_match (REGEX_URL_HTTP, "foo.bar/baz", nullptr); + assert_match (REGEX_URL_HTTP, "abc.www.foo.bar/baz", nullptr); + assert_match (REGEX_URL_HTTP, "uvwww.foo.bar/baz", nullptr); + assert_match (REGEX_URL_HTTP, "xftp.foo.bar/baz", nullptr); + + /* file:/ or file://(hostname)?/ */ + assert_match (REGEX_URL_FILE, "file:", nullptr); + assert_match (REGEX_URL_FILE, "file:/", ENTIRE); + assert_match (REGEX_URL_FILE, "file://", nullptr); + assert_match (REGEX_URL_FILE, "file:///", ENTIRE); + assert_match (REGEX_URL_FILE, "file:////", nullptr); + assert_match (REGEX_URL_FILE, "file:etc/passwd", nullptr); + assert_match (REGEX_URL_FILE, "File:/etc/passwd", ENTIRE); + assert_match (REGEX_URL_FILE, "FILE:///etc/passwd", ENTIRE); + assert_match (REGEX_URL_FILE, "file:////etc/passwd", nullptr); + assert_match (REGEX_URL_FILE, "file://host.name", nullptr); + assert_match (REGEX_URL_FILE, "file://host.name/", ENTIRE); + assert_match (REGEX_URL_FILE, "file://host.name/etc", ENTIRE); + + assert_match (REGEX_URL_FILE, "See file:/.", "file:/"); + assert_match (REGEX_URL_FILE, "See file:///.", "file:///"); + assert_match (REGEX_URL_FILE, "See file:/lost+found.", "file:/lost+found"); + assert_match (REGEX_URL_FILE, "See file:///lost+found.", "file:///lost+found"); + + /* Email */ + assert_match (REGEX_EMAIL, "Write to foo@bar.com.", "foo@bar.com"); + assert_match (REGEX_EMAIL, "Write to <foo@bar.com>", "foo@bar.com"); + assert_match (REGEX_EMAIL, "Write to mailto:foo@bar.com.", "mailto:foo@bar.com"); + assert_match (REGEX_EMAIL, "Write to MAILTO:FOO@BAR.COM.", "MAILTO:FOO@BAR.COM"); + assert_match (REGEX_EMAIL, "Write to foo@[1.2.3.4]", "foo@[1.2.3.4]"); + assert_match (REGEX_EMAIL, "Write to foo@[1.2.3.456]", nullptr); + assert_match (REGEX_EMAIL, "Write to foo@[1::2345]", "foo@[1::2345]"); + assert_match (REGEX_EMAIL, "Write to foo@[dead::beef]", "foo@[dead::beef]"); + assert_match (REGEX_EMAIL, "Write to foo@1.2.3.4", nullptr); + assert_match (REGEX_EMAIL, "Write to foo@1.2.3.456", nullptr); + assert_match (REGEX_EMAIL, "Write to foo@1::2345", nullptr); + assert_match (REGEX_EMAIL, "Write to foo@dead::beef", nullptr); + assert_match (REGEX_EMAIL, "<baz email=\"foo@bar.com\"/>", "foo@bar.com"); + assert_match (REGEX_EMAIL, "<baz email='foo@bar.com'/>", "foo@bar.com"); + assert_match (REGEX_EMAIL, "<email>foo@bar.com</email>", "foo@bar.com"); + + /* Sip, examples from rfc 3261 */ + assert_match (REGEX_URL_VOIP, "sip:alice@atlanta.com;maddr=239.255.255.1;ttl=15", ENTIRE); + assert_match (REGEX_URL_VOIP, "sip:alice@atlanta.com", ENTIRE); + assert_match (REGEX_URL_VOIP, "sip:alice:secretword@atlanta.com;transport=tcp", ENTIRE); + assert_match (REGEX_URL_VOIP, "sips:alice@atlanta.com?subject=project%20x&priority=urgent", ENTIRE); + assert_match (REGEX_URL_VOIP, "sip:+1-212-555-1212:1234@gateway.com;user=phone", ENTIRE); + assert_match (REGEX_URL_VOIP, "sips:1212@gateway.com", ENTIRE); + assert_match (REGEX_URL_VOIP, "sip:alice@192.0.2.4", ENTIRE); + assert_match (REGEX_URL_VOIP, "sip:atlanta.com;method=REGISTER?to=alice%40atlanta.com", ENTIRE); + assert_match (REGEX_URL_VOIP, "SIP:alice;day=tuesday@atlanta.com", ENTIRE); + assert_match (REGEX_URL_VOIP, "Dial sip:alice@192.0.2.4.", "sip:alice@192.0.2.4"); + + /* Extremely long match, bug 770147 */ + assert_match (REGEX_URL_AS_IS, "http://www.example.com/ThisPathConsistsOfMoreThan1024Characters" + "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890", ENTIRE); + + printf("terminal-regex tests passed :)\n"); + return 0; +} + +#endif /* TERMINAL_REGEX_MAIN */ diff --git a/src/terminal-regex.hh b/src/terminal-regex.hh new file mode 100644 index 0000000..6fcc0f9 --- /dev/null +++ b/src/terminal-regex.hh @@ -0,0 +1,158 @@ +/* + * Copyright © 2015 Egmont Koblinger + * + * 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/>. + */ + +/* + * Mini style-guide: + * + * #define'd fragments should preferably have an outermost group, for the + * exact same reason as why usually in C/C++ #define's the values are enclosed + * in parentheses: that is, so that you don't get surprised when you use the + * macro and append a quantifier. + * + * For repeated fragments prefer regex-style (?(DEFINE)(?<NAME>(...))) and use + * as (?&NAME), so that the regex string and the compiled regex object is + * smaller. + * + * Build small blocks, comment and unittest them heavily. + * + * Use free-spacing mode for improved readability. The hardest to read is + * which additional characters belong to a "(?" prefix. To improve + * readability, place a space after this, and for symmetry, before the closing + * parenthesis. Also place a space around "|" characters. No space before + * quantifiers. Try to be consistent with the existing style (yes I know the + * existing style is not consistent either, but please do your best). + * + * See http://www.rexegg.com/regex-disambiguation.html for all the "(?" + * syntaxes. + */ + +#ifndef TERMINAL_REGEX_H +#define TERMINAL_REGEX_H + +/* Lookbehind to see if there's a preceding apostrophe. + * Unlike the other *_DEF macros which define regex subroutines, + * this one is a named capture that defines APOS_START to either + * an apostrophe or the empty string, depending on the character + * preceding this APOS_START_DEF construct. */ +#define APOS_START_DEF "(?<APOS_START>(?<='))?" + +#define SCHEME "(?ix: news | telnet | nntp | https? | ftps? | sftp | webcal )" + +#define USERCHARS "-+.[:alnum:]" +/* Nonempty username, e.g. "john.smith" */ +#define USER "[" USERCHARS "]+" + +#define PASSCHARS_CLASS "[-[:alnum:]\\Q,?;.:/!%$^*&~\"#'\\E]" +/* Optional colon-prefixed password. I guess empty password should be allowed, right? E.g. ":secret", ":", "" */ +#define PASS "(?x: :" PASSCHARS_CLASS "* )?" + +/* Optional at-terminated username (with perhaps a password too), e.g. "joe@", "pete:secret@", "" */ +#define USERPASS "(?:" USER PASS "@)?" + +/* S4: IPv4 segment (number between 0 and 255) with lookahead at the end so that we don't match "25" in the string "256". + The lookahead could go to the last segment of IPv4 only but this construct allows nicer unittesting. */ +#define S4_DEF "(?(DEFINE)(?<S4>(?x: (?: [0-9] | [1-9][0-9] | 1[0-9]{2} | 2[0-4][0-9] | 25[0-5] ) (?! [0-9] ) )))" + +/* IPV4: Decimal IPv4, e.g. "1.2.3.4", with lookahead (implemented in S4) at the end so that we don't match "192.168.1.123" in the string "192.168.1.1234". */ +#define IPV4_DEF S4_DEF "(?(DEFINE)(?<IPV4>(?x: (?: (?&S4) \\. ){3} (?&S4) )))" + +/* IPv6, including embedded IPv4, e.g. "::1", "dead:beef::1.2.3.4". + * Lookahead for the next char not being a dot or digit, so it doesn't get stuck matching "dead:beef::1" in "dead:beef::1.2.3.4". + * This is not required since the surrounding brackets would trigger backtracking, but it allows nicer unittesting. + * TODO: more strict check (right number of colons, etc.) + * TODO: add zone_id: RFC 4007 section 11, RFC 6874 */ + +/* S6: IPv6 segment, S6C: IPv6 segment followed by a comma, CS6: comma followed by an IPv6 segment */ +#define S6_DEF "(?(DEFINE)(?<S6>[[:xdigit:]]{1,4})(?<CS6>:(?&S6))(?<S6C>(?&S6):))" + +/* No :: shorthand */ +#define IPV6_FULL "(?x: (?&S6C){7} (?&S6) )" +/* Begins with :: */ +#define IPV6_LEFT "(?x: : (?&CS6){1,7} )" +/* :: somewhere in the middle - use negative lookahead to make sure there aren't too many colons in total */ +#define IPV6_MID "(?x: (?! (?: [[:xdigit:]]*: ){8} ) (?&S6C){1,6} (?&CS6){1,6} )" +/* Ends with :: */ +#define IPV6_RIGHT "(?x: (?&S6C){1,7} : )" +/* Is "::" and nothing more */ +#define IPV6_NULL "(?x: :: )" + +/* The same ones for IPv4-embedded notation, without the actual IPv4 part */ +#define IPV6V4_FULL "(?x: (?&S6C){6} )" +#define IPV6V4_LEFT "(?x: :: (?&S6C){0,5} )" /* includes "::<ipv4>" */ +#define IPV6V4_MID "(?x: (?! (?: [[:xdigit:]]*: ){7} ) (?&S6C){1,4} (?&CS6){1,4} ) :" +#define IPV6V4_RIGHT "(?x: (?&S6C){1,5} : )" + +/* IPV6: An IPv6 address (possibly with an embedded IPv4). + * This macro defines both IPV4 and IPV6, since the latter one requires the former. */ +#define IP_DEF IPV4_DEF S6_DEF "(?(DEFINE)(?<IPV6>(?x: (?: " IPV6_NULL " | " IPV6_LEFT " | " IPV6_MID " | " IPV6_RIGHT " | " IPV6_FULL " | (?: " IPV6V4_FULL " | " IPV6V4_LEFT " | " IPV6V4_MID " | " IPV6V4_RIGHT " ) (?&IPV4) ) (?! [.:[:xdigit:]] ) )))" + +/* Either an alphanumeric character or dash; or if [negative lookahead] not ASCII + * then any graphical Unicode character. + * A segment can consist entirely of numbers. + * (Note: PCRE doesn't support character class subtraction/intersection.) */ +#define HOSTNAMESEGMENTCHARS_CLASS "(?x: [-[:alnum:]] | (?! [[:ascii:]] ) [[:graph:]] )" + +/* A hostname of at least 1 component. The last component cannot be entirely numbers. + * E.g. "foo", "example.com", "1234.com", but not "foo.123" */ +#define HOSTNAME1 "(?x: (?: " HOSTNAMESEGMENTCHARS_CLASS "+ \\. )* " HOSTNAMESEGMENTCHARS_CLASS "* (?! [0-9] ) " HOSTNAMESEGMENTCHARS_CLASS "+ )" + +/* A hostname of at least 2 components. The last component cannot be entirely numbers. + * E.g. "example.com", "1234.com", but not "1234.56" */ +#define HOSTNAME2 "(?x: (?: " HOSTNAMESEGMENTCHARS_CLASS "+ \\.)+ " HOSTNAME1 " )" + +/* For URL: Hostname, IPv4, or bracket-enclosed IPv6, e.g. "example.com", "1.2.3.4", "[::1]" */ +#define URL_HOST "(?x: " HOSTNAME1 " | (?&IPV4) | \\[ (?&IPV6) \\] )" + +/* For e-mail: Hostname of at least two segments, or bracket-enclosed IPv4 or IPv6, e.g. "example.com", "[1.2.3.4]", "[::1]". + * Technically an e-mail with a single-component hostname might be valid on a local network, but let's avoid tons of false positives (e.g. in a typical shell prompt). */ +#define EMAIL_HOST "(?x: " HOSTNAME2 " | \\[ (?: (?&IPV4) | (?&IPV6) ) \\] )" + +/* Number between 1 and 65535, with lookahead at the end so that we don't match "6789" in the string "67890", + and in turn we don't eventually match "http://host:6789" in "http://host:67890". */ +#define N_1_65535 "(?x: (?: [1-9][0-9]{0,3} | [1-5][0-9]{4} | 6[0-4][0-9]{3} | 65[0-4][0-9]{2} | 655[0-2][0-9] | 6553[0-5] ) (?! [0-9] ) )" + +/* Optional colon-prefixed port, e.g. ":1080", "" */ +#define PORT "(?x: \\:" N_1_65535 " )?" + +/* Omit the parentheses, see below */ +#define PATHCHARS_CLASS "[-[:alnum:]\\Q_$.+!*,:;@&=?/~#|%'\\E]" +/* Chars to end a URL. Apostrophe only allowed if there wasn't one in front of the URL, see bug 448044 */ +#define PATHTERM_CLASS "[-[:alnum:]\\Q_$+*:@&=/~#|%'\\E]" +#define PATHTERM_NOAPOS_CLASS "[-[:alnum:]\\Q_$+*:@&=/~#|%\\E]" + +/* Recursive definition of PATH that allows parentheses and square brackets only if balanced, see bug 763980. */ +#define PATH_INNER_DEF "(?(DEFINE)(?<PATH_INNER>(?x: (?: " PATHCHARS_CLASS "* (?: \\( (?&PATH_INNER) \\) | \\[ (?&PATH_INNER) \\] ) )* " PATHCHARS_CLASS "* )))" +/* Same as above, but the last character (if exists and is not a parenthesis) must be from PATHTERM_CLASS. */ +#define PATH_DEF "(?(DEFINE)(?<PATH>(?x: (?: " PATHCHARS_CLASS "* (?: \\( (?&PATH_INNER) \\) | \\[ (?&PATH_INNER) \\] ) )* (?: " PATHCHARS_CLASS "* (?(<APOS_START>)" PATHTERM_NOAPOS_CLASS "|" PATHTERM_CLASS ") )? )))" + +#define URLPATH "(?x: [/?#](?&PATH) )?" +#define VOIP_PATH "(?x: [;?](?&PATH) )?" + +/* Now let's put these fragments together */ + +#define DEFS APOS_START_DEF IP_DEF PATH_INNER_DEF PATH_DEF + +#define REGEX_URL_AS_IS DEFS SCHEME "://" USERPASS URL_HOST PORT URLPATH +/* TODO: also support file:/etc/passwd */ +#define REGEX_URL_FILE DEFS "(?ix: file:/ (?: / (?: " HOSTNAME1 " )? / )? (?! / ) )(?&PATH)" +/* Lookbehind so that we don't catch "abc.www.foo.bar", bug 739757. Lookahead for www/ftp for convenience (so that we can reuse HOSTNAME1). */ +#define REGEX_URL_HTTP DEFS "(?<!(?:" HOSTNAMESEGMENTCHARS_CLASS "|[.]))(?=(?i:www|ftp))" HOSTNAME1 PORT URLPATH +#define REGEX_URL_VOIP DEFS "(?i:h323:|sips?:)" USERPASS URL_HOST PORT VOIP_PATH +#define REGEX_EMAIL DEFS "(?i:mailto:)?" USER "@" EMAIL_HOST +#define REGEX_NEWS_MAN "(?i:news:|man:|info:)[-[:alnum:]\\Q^_{|}~!\"#$%&'()*+,./;:=?`\\E]+" + +#endif /* !TERMINAL_REGEX_H */ diff --git a/src/terminal-schemas.hh b/src/terminal-schemas.hh new file mode 100644 index 0000000..61f1305 --- /dev/null +++ b/src/terminal-schemas.hh @@ -0,0 +1,105 @@ +/* + * Copyright © 2008, 2010 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/>. + */ + +#ifndef TERMINAL_SCHEMAS_H +#define TERMINAL_SCHEMAS_H + +#include <glib.h> + +G_BEGIN_DECLS + +#define TERMINAL_SCHEMA_VERSION (3u) + +#define TERMINAL_KEYBINDINGS_SCHEMA "org.gnome.Terminal.Legacy.Keybindings" +#define TERMINAL_PROFILE_SCHEMA "org.gnome.Terminal.Legacy.Profile" +#define TERMINAL_SETTING_SCHEMA "org.gnome.Terminal.Legacy.Settings" +#define TERMINAL_SETTINGS_LIST_SCHEMA "org.gnome.Terminal.SettingsList" +#define TERMINAL_PROFILES_LIST_SCHEMA "org.gnome.Terminal.ProfilesList" + +#define TERMINAL_KEYBINDINGS_SCHEMA_PATH "/org/gnome/terminal/legacy/keybindings/" + +#define TERMINAL_PROFILE_AUDIBLE_BELL_KEY "audible-bell" +#define TERMINAL_PROFILE_BOLD_IS_BRIGHT_KEY "bold-is-bright" +#define TERMINAL_PROFILE_BACKGROUND_COLOR_KEY "background-color" +#define TERMINAL_PROFILE_BACKSPACE_BINDING_KEY "backspace-binding" +#define TERMINAL_PROFILE_BOLD_COLOR_KEY "bold-color" +#define TERMINAL_PROFILE_BOLD_COLOR_SAME_AS_FG_KEY "bold-color-same-as-fg" +#define TERMINAL_PROFILE_CELL_HEIGHT_SCALE_KEY "cell-height-scale" +#define TERMINAL_PROFILE_CELL_WIDTH_SCALE_KEY "cell-width-scale" +#define TERMINAL_PROFILE_CURSOR_COLORS_SET_KEY "cursor-colors-set" +#define TERMINAL_PROFILE_CURSOR_BACKGROUND_COLOR_KEY "cursor-background-color" +#define TERMINAL_PROFILE_CURSOR_FOREGROUND_COLOR_KEY "cursor-foreground-color" +#define TERMINAL_PROFILE_CJK_UTF8_AMBIGUOUS_WIDTH_KEY "cjk-utf8-ambiguous-width" +#define TERMINAL_PROFILE_CURSOR_BLINK_MODE_KEY "cursor-blink-mode" +#define TERMINAL_PROFILE_CURSOR_SHAPE_KEY "cursor-shape" +#define TERMINAL_PROFILE_CUSTOM_COMMAND_KEY "custom-command" +#define TERMINAL_PROFILE_DEFAULT_SIZE_COLUMNS_KEY "default-size-columns" +#define TERMINAL_PROFILE_DEFAULT_SIZE_ROWS_KEY "default-size-rows" +#define TERMINAL_PROFILE_DELETE_BINDING_KEY "delete-binding" +#define TERMINAL_PROFILE_ENABLE_BIDI_KEY "enable-bidi" +#define TERMINAL_PROFILE_ENABLE_SHAPING_KEY "enable-shaping" +#define TERMINAL_PROFILE_ENABLE_SIXEL_KEY "enable-sixel" +#define TERMINAL_PROFILE_ENCODING_KEY "encoding" +#define TERMINAL_PROFILE_EXIT_ACTION_KEY "exit-action" +#define TERMINAL_PROFILE_FONT_KEY "font" +#define TERMINAL_PROFILE_FOREGROUND_COLOR_KEY "foreground-color" +#define TERMINAL_PROFILE_HIGHLIGHT_COLORS_SET_KEY "highlight-colors-set" +#define TERMINAL_PROFILE_HIGHLIGHT_BACKGROUND_COLOR_KEY "highlight-background-color" +#define TERMINAL_PROFILE_HIGHLIGHT_FOREGROUND_COLOR_KEY "highlight-foreground-color" +#define TERMINAL_PROFILE_LOGIN_SHELL_KEY "login-shell" +#define TERMINAL_PROFILE_NAME_KEY "name" +#define TERMINAL_PROFILE_PALETTE_KEY "palette" +#define TERMINAL_PROFILE_PRESERVE_WORKING_DIRECTORY_KEY "preserve-working-directory" +#define TERMINAL_PROFILE_REWRAP_ON_RESIZE_KEY "rewrap-on-resize" +#define TERMINAL_PROFILE_SCROLLBACK_LINES_KEY "scrollback-lines" +#define TERMINAL_PROFILE_SCROLLBACK_UNLIMITED_KEY "scrollback-unlimited" +#define TERMINAL_PROFILE_SCROLLBAR_POLICY_KEY "scrollbar-policy" +#define TERMINAL_PROFILE_SCROLL_ON_KEYSTROKE_KEY "scroll-on-keystroke" +#define TERMINAL_PROFILE_SCROLL_ON_OUTPUT_KEY "scroll-on-output" +#define TERMINAL_PROFILE_TEXT_BLINK_MODE_KEY "text-blink-mode" +#define TERMINAL_PROFILE_USE_CUSTOM_COMMAND_KEY "use-custom-command" +#define TERMINAL_PROFILE_USE_SKEY_KEY "use-skey" +#define TERMINAL_PROFILE_USE_SYSTEM_FONT_KEY "use-system-font" +#define TERMINAL_PROFILE_USE_THEME_COLORS_KEY "use-theme-colors" +#define TERMINAL_PROFILE_VISIBLE_NAME_KEY "visible-name" +#define TERMINAL_PROFILE_WORD_CHAR_EXCEPTIONS_KEY "word-char-exceptions" + +#define TERMINAL_SETTING_CONFIRM_CLOSE_KEY "confirm-close" +#define TERMINAL_SETTING_CONTEXT_INFO_KEY "context-info" +#define TERMINAL_SETTING_DEFAULT_SHOW_MENUBAR_KEY "default-show-menubar" +#define TERMINAL_SETTING_ENABLE_MENU_BAR_ACCEL_KEY "menu-accelerator-enabled" +#define TERMINAL_SETTING_ENABLE_MNEMONICS_KEY "mnemonics-enabled" +#define TERMINAL_SETTING_ENABLE_SHORTCUTS_KEY "shortcuts-enabled" +#define TERMINAL_SETTING_HEADERBAR_KEY "headerbar" +#define TERMINAL_SETTING_NEW_TERMINAL_MODE_KEY "new-terminal-mode" +#define TERMINAL_SETTING_NEW_TAB_POSITION_KEY "new-tab-position" +#define TERMINAL_SETTING_SCHEMA_VERSION "schema-version" +#define TERMINAL_SETTING_SHELL_INTEGRATION_KEY "shell-integration-enabled" +#define TERMINAL_SETTING_TAB_POLICY_KEY "tab-policy" +#define TERMINAL_SETTING_TAB_POSITION_KEY "tab-position" +#define TERMINAL_SETTING_THEME_VARIANT_KEY "theme-variant" +#define TERMINAL_SETTING_UNIFIED_MENU_KEY "unified-menu" +#define TERMINAL_SETTING_ALWAYS_CHECK_DEFAULT_KEY "always-check-default-terminal" + +#define TERMINAL_SETTINGS_LIST_LIST_KEY "list" +#define TERMINAL_SETTINGS_LIST_DEFAULT_KEY "default" + +#define TERMINAL_PROFILES_PATH_PREFIX "/org/gnome/terminal/legacy/profiles:/" + +G_END_DECLS + +#endif /* TERMINAL_SCHEMAS_H */ diff --git a/src/terminal-screen-container.cc b/src/terminal-screen-container.cc new file mode 100644 index 0000000..6d85190 --- /dev/null +++ b/src/terminal-screen-container.cc @@ -0,0 +1,392 @@ +/* + * Copyright © 2008, 2010, 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 "terminal-screen-container.hh" +#include "terminal-debug.hh" + +#if 0 +#define USE_SCROLLED_WINDOW +#endif + +#include <gtk/gtk.h> + +#define TERMINAL_SCREEN_CONTAINER_GET_PRIVATE(screen_container)(G_TYPE_INSTANCE_GET_PRIVATE ((screen_container), TERMINAL_TYPE_SCREEN_CONTAINER, TerminalScreenContainerPrivate)) + +struct _TerminalScreenContainerPrivate +{ + TerminalScreen *screen; +#ifdef USE_SCROLLED_WINDOW + GtkWidget *scrolled_window; +#else + GtkWidget *hbox; + GtkWidget *vscrollbar; +#endif + GtkPolicyType hscrollbar_policy; + GtkPolicyType vscrollbar_policy; +}; + +enum +{ + PROP_0, + PROP_SCREEN, + PROP_HSCROLLBAR_POLICY, + PROP_VSCROLLBAR_POLICY +}; + +G_DEFINE_TYPE (TerminalScreenContainer, terminal_screen_container, GTK_TYPE_OVERLAY) + +#define TERMINAL_SCREEN_CONTAINER_CSS_NAME "terminal-screen-container" + +/* helper functions */ + +/* Widget class implementation */ + +static void +terminal_screen_container_realize (GtkWidget *widget) +{ + + GTK_WIDGET_CLASS (terminal_screen_container_parent_class)->realize (widget); + + /* We need to realize the screen itself too, see issue #203 */ + TerminalScreenContainer *container = TERMINAL_SCREEN_CONTAINER (widget); + TerminalScreenContainerPrivate *priv = container->priv; + gtk_widget_realize (GTK_WIDGET (priv->screen)); +} + +#ifndef USE_SCROLLED_WINDOW + +static void +terminal_screen_container_style_updated (GtkWidget *widget) +{ + TerminalScreenContainer *container = TERMINAL_SCREEN_CONTAINER (widget); + TerminalScreenContainerPrivate *priv = container->priv; + GtkCornerType corner; + gboolean set; + + GTK_WIDGET_CLASS (terminal_screen_container_parent_class)->style_updated (widget); + + gtk_widget_style_get (widget, + "window-placement", &corner, + "window-placement-set", &set, + nullptr); + + if (!set) { + g_object_get (gtk_widget_get_settings (widget), + "gtk-scrolled-window-placement", &corner, + nullptr); + } + + switch (corner) { + case GTK_CORNER_TOP_LEFT: + case GTK_CORNER_BOTTOM_LEFT: + gtk_box_reorder_child (GTK_BOX (priv->hbox), priv->vscrollbar, -1); + break; + case GTK_CORNER_TOP_RIGHT: + case GTK_CORNER_BOTTOM_RIGHT: + gtk_box_reorder_child (GTK_BOX (priv->hbox), priv->vscrollbar, 0); + break; + default: + g_assert_not_reached (); + } +} + +#endif /* !USE_SCROLLED_WINDOW */ + +/* Class implementation */ + +static void +terminal_screen_container_init (TerminalScreenContainer *container) +{ + TerminalScreenContainerPrivate *priv; + + priv = container->priv = TERMINAL_SCREEN_CONTAINER_GET_PRIVATE (container); + + priv->hscrollbar_policy = GTK_POLICY_AUTOMATIC; + priv->vscrollbar_policy = GTK_POLICY_AUTOMATIC; +} + +static void +terminal_screen_container_constructed (GObject *object) +{ + TerminalScreenContainer *container = TERMINAL_SCREEN_CONTAINER (object); + TerminalScreenContainerPrivate *priv = container->priv; + + G_OBJECT_CLASS (terminal_screen_container_parent_class)->constructed (object); + + g_assert (priv->screen != nullptr); + +#ifdef USE_SCROLLED_WINDOW +{ + GtkAdjustment *hadjustment; + GtkAdjustment *vadjustment; + + hadjustment = gtk_scrollable_get_hadjustment (GTK_SCROLLABLE (priv->screen)); + vadjustment = gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (priv->screen)); + + priv->scrolled_window = gtk_scrolled_window_new (hadjustment, vadjustment); + gtk_scrolled_window_set_overlay_scrolling (GTK_SCROLLED_WINDOW (priv->scrolled_window), FALSE); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (priv->scrolled_window), + priv->hscrollbar_policy, + priv->vscrollbar_policy); + gtk_container_add (GTK_CONTAINER (priv->scrolled_window), GTK_WIDGET (priv->screen)); + + gtk_container_add (GTK_CONTAINER (container), priv->scrolled_window); + gtk_widget_show_all (priv->scrolled_window); +} +#else + priv->hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); + + priv->vscrollbar = gtk_scrollbar_new (GTK_ORIENTATION_VERTICAL, + gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (priv->screen))); + + gtk_box_pack_start (GTK_BOX (priv->hbox), GTK_WIDGET (priv->screen), TRUE, TRUE, 0); + gtk_box_pack_start (GTK_BOX (priv->hbox), priv->vscrollbar, FALSE, FALSE, 0); + + gtk_container_add (GTK_CONTAINER (container), priv->hbox); + gtk_widget_show_all (priv->hbox); +#endif + + _terminal_screen_update_scrollbar (priv->screen); +} + +static void +terminal_screen_container_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + TerminalScreenContainer *container = TERMINAL_SCREEN_CONTAINER (object); + TerminalScreenContainerPrivate *priv = container->priv; + + switch (prop_id) { + case PROP_SCREEN: + break; + case PROP_HSCROLLBAR_POLICY: + g_value_set_enum (value, priv->hscrollbar_policy); + break; + case PROP_VSCROLLBAR_POLICY: + g_value_set_enum (value, priv->vscrollbar_policy); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +terminal_screen_container_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + TerminalScreenContainer *container = TERMINAL_SCREEN_CONTAINER (object); + TerminalScreenContainerPrivate *priv = container->priv; + + switch (prop_id) { + case PROP_SCREEN: + priv->screen = (TerminalScreen*)g_value_get_object (value); + break; + case PROP_HSCROLLBAR_POLICY: + terminal_screen_container_set_policy (container, + GtkPolicyType(g_value_get_enum (value)), + priv->vscrollbar_policy); + break; + case PROP_VSCROLLBAR_POLICY: + terminal_screen_container_set_policy (container, + priv->hscrollbar_policy, + GtkPolicyType(g_value_get_enum (value))); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +terminal_screen_container_class_init (TerminalScreenContainerClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + g_type_class_add_private (gobject_class, sizeof (TerminalScreenContainerPrivate)); + + gobject_class->constructed = terminal_screen_container_constructed; + gobject_class->get_property = terminal_screen_container_get_property; + gobject_class->set_property = terminal_screen_container_set_property; + + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + widget_class->realize = terminal_screen_container_realize; + +#ifndef USE_SCROLLED_WINDOW + widget_class->style_updated = terminal_screen_container_style_updated; + + gtk_widget_class_install_style_property (widget_class, + g_param_spec_enum ("window-placement", nullptr, nullptr, + GTK_TYPE_CORNER_TYPE, + GTK_CORNER_BOTTOM_RIGHT, + GParamFlags(G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS))); + gtk_widget_class_install_style_property (widget_class, + g_param_spec_boolean ("window-placement-set", nullptr, nullptr, + FALSE, + GParamFlags(G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS))); +#endif + + gtk_widget_class_set_css_name(widget_class, TERMINAL_SCREEN_CONTAINER_CSS_NAME); + + g_object_class_install_property + (gobject_class, + PROP_SCREEN, + g_param_spec_object ("screen", nullptr, nullptr, + TERMINAL_TYPE_SCREEN, + GParamFlags(G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS))); + + g_object_class_install_property + (gobject_class, + PROP_HSCROLLBAR_POLICY, + g_param_spec_enum ("hscrollbar-policy", nullptr, nullptr, + GTK_TYPE_POLICY_TYPE, + GTK_POLICY_AUTOMATIC, + GParamFlags(G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS))); + g_object_class_install_property + (gobject_class, + PROP_VSCROLLBAR_POLICY, + g_param_spec_enum ("vscrollbar-policy", nullptr, nullptr, + GTK_TYPE_POLICY_TYPE, + GTK_POLICY_AUTOMATIC, + GParamFlags(G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS))); +} + +/* public API */ + +/** + * terminal_screen_container_new: + * @screen: a #TerminalScreen + * + * Returns: a new #TerminalScreenContainer for @screen + */ +GtkWidget * +terminal_screen_container_new (TerminalScreen *screen) +{ + return reinterpret_cast<GtkWidget*> + (g_object_new (TERMINAL_TYPE_SCREEN_CONTAINER, + "screen", screen, + nullptr)); +} + +/** + * terminal_screen_container_get_screen: + * @container: a #TerminalScreenContainer + * + * Returns: @container's #TerminalScreen + */ +TerminalScreen * +terminal_screen_container_get_screen (TerminalScreenContainer *container) +{ + if (container == nullptr) + return nullptr; + + g_return_val_if_fail (TERMINAL_IS_SCREEN_CONTAINER (container), nullptr); + + return container->priv->screen; +} + +/** + * terminal_screen_container_get_from_screen: + * @screen: a #TerminalScreenContainerPrivate + * + * Returns the #TerminalScreenContainer containing @screen. + */ +TerminalScreenContainer * +terminal_screen_container_get_from_screen (TerminalScreen *screen) +{ + if (screen == nullptr) + return nullptr; + + g_return_val_if_fail (TERMINAL_IS_SCREEN (screen), nullptr); + + return TERMINAL_SCREEN_CONTAINER (gtk_widget_get_ancestor (GTK_WIDGET (screen), TERMINAL_TYPE_SCREEN_CONTAINER)); +} + +/** + * terminal_screen_container_set_policy: + * @container: a #TerminalScreenContainer + * @hpolicy: a #GtkPolicyType + * @vpolicy: a #GtkPolicyType + * + * Sets @container's scrollbar policy. + */ +void +terminal_screen_container_set_policy (TerminalScreenContainer *container, + GtkPolicyType hpolicy, + GtkPolicyType vpolicy) +{ + TerminalScreenContainerPrivate *priv; + GObject *object; + + g_return_if_fail (TERMINAL_IS_SCREEN_CONTAINER (container)); + + object = G_OBJECT (container); + priv = container->priv; + + g_object_freeze_notify (object); + + if (priv->hscrollbar_policy != hpolicy) { + priv->hscrollbar_policy = hpolicy; + g_object_notify (object, "hscrollbar-policy"); + } + if (priv->vscrollbar_policy != vpolicy) { + priv->vscrollbar_policy = vpolicy; + g_object_notify (object, "vscrollbar-policy"); + } + +#ifdef USE_SCROLLED_WINDOW + switch (vpolicy) { + case GTK_POLICY_NEVER: + vpolicy = GTK_POLICY_EXTERNAL; + break; + case GTK_POLICY_AUTOMATIC: + case GTK_POLICY_ALWAYS: + vpolicy = GTK_POLICY_ALWAYS; + break; + default: + g_assert_not_reached (); + } + + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (priv->scrolled_window), hpolicy, vpolicy); +#else + switch (vpolicy) { + case GTK_POLICY_NEVER: + gtk_widget_hide (priv->vscrollbar); + break; + case GTK_POLICY_AUTOMATIC: + case GTK_POLICY_ALWAYS: + gtk_widget_show (priv->vscrollbar); + break; + default: + g_assert_not_reached (); + } +#endif /* USE_SCROLLED_WINDOW */ + + g_object_thaw_notify (object); +} diff --git a/src/terminal-screen-container.hh b/src/terminal-screen-container.hh new file mode 100644 index 0000000..6d30e5c --- /dev/null +++ b/src/terminal-screen-container.hh @@ -0,0 +1,64 @@ +/* + * Copyright © 2008, 2010 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/>. + */ + +#ifndef TERMINAL_SCREEN_CONTAINER_H +#define TERMINAL_SCREEN_CONTAINER_H + +#include <gtk/gtk.h> +#include "terminal-screen.hh" + +G_BEGIN_DECLS + +#define TERMINAL_TYPE_SCREEN_CONTAINER (terminal_screen_container_get_type ()) +#define TERMINAL_SCREEN_CONTAINER(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), TERMINAL_TYPE_SCREEN_CONTAINER, TerminalScreenContainer)) +#define TERMINAL_SCREEN_CONTAINER_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), TERMINAL_TYPE_SCREEN_CONTAINER, TerminalScreenContainerClass)) +#define TERMINAL_IS_SCREEN_CONTAINER(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), TERMINAL_TYPE_SCREEN_CONTAINER)) +#define TERMINAL_IS_SCREEN_CONTAINER_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), TERMINAL_TYPE_SCREEN_CONTAINER)) +#define TERMINAL_SCREEN_CONTAINER_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), TERMINAL_TYPE_SCREEN_CONTAINER, TerminalScreenContainerClass)) + +typedef struct _TerminalScreenContainer TerminalScreenContainer; +typedef struct _TerminalScreenContainerClass TerminalScreenContainerClass; +typedef struct _TerminalScreenContainerPrivate TerminalScreenContainerPrivate; + +struct _TerminalScreenContainer +{ + GtkOverlay parent_instance; + + /*< private >*/ + TerminalScreenContainerPrivate *priv; +}; + +struct _TerminalScreenContainerClass +{ + GtkOverlayClass parent_class; +}; + +GType terminal_screen_container_get_type (void); + +GtkWidget *terminal_screen_container_new (TerminalScreen *screen); + +TerminalScreen *terminal_screen_container_get_screen (TerminalScreenContainer *container); + +TerminalScreenContainer *terminal_screen_container_get_from_screen (TerminalScreen *screen); + +void terminal_screen_container_set_policy (TerminalScreenContainer *container, + GtkPolicyType hpolicy, + GtkPolicyType vpolicy); + +G_END_DECLS + +#endif /* TERMINAL_SCREEN_CONTAINER_H */ diff --git a/src/terminal-screen.cc b/src/terminal-screen.cc new file mode 100644 index 0000000..a559f7f --- /dev/null +++ b/src/terminal-screen.cc @@ -0,0 +1,2358 @@ +/* + * Copyright © 2001 Havoc Pennington + * Copyright © 2007, 2008, 2010, 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 "terminal-pcre2.hh" +#include "terminal-regex.hh" +#include "terminal-screen.hh" +#include "terminal-client-utils.hh" + +#include <errno.h> +#include <string.h> +#include <stdlib.h> +#include <unistd.h> +#include <sys/wait.h> +#include <fcntl.h> +#include <uuid.h> + +#if defined(__FreeBSD__) || defined(__DragonFly__) || defined(__OpenBSD__) +#include <sys/sysctl.h> +#endif + +#include <glib.h> +#include <glib/gi18n.h> +#include <gio/gio.h> +#include <gio/gunixfdlist.h> + +#include <gtk/gtk.h> + +#ifdef GDK_WINDOWING_X11 +#include <gdk/gdkx.h> +#endif + +#include "terminal-accels.hh" +#include "terminal-app.hh" +#include "terminal-debug.hh" +#include "terminal-defines.hh" +#include "terminal-enums.hh" +#include "terminal-intl.hh" +#include "terminal-marshal.h" +#include "terminal-schemas.hh" +#include "terminal-screen-container.hh" +#include "terminal-util.hh" +#include "terminal-window.hh" +#include "terminal-info-bar.hh" +#include "terminal-libgsystem.hh" + +#include "eggshell.hh" + +#define URL_MATCH_CURSOR_NAME "pointer" + +namespace { + +typedef struct { + volatile int refcount; + char **argv; /* as passed */ + char **exec_argv; /* as processed */ + char **envv; + char *cwd; + gboolean as_shell; + + VtePtyFlags pty_flags; + GSpawnFlags spawn_flags; + + /* FD passing */ + GUnixFDList *fd_list; + int n_fd_map; + int* fd_map; + + /* async exec callback */ + TerminalScreenExecCallback callback; + gpointer callback_data; + GDestroyNotify callback_data_destroy_notify; + + /* Cancellable */ + GCancellable *cancellable; +} ExecData; + +} // anon namespace + +typedef struct +{ + int tag; + TerminalURLFlavor flavor; +} TagData; + +struct _TerminalScreenPrivate +{ + char *uuid; + gboolean registered; /* D-Bus interface is registered */ + + GSettings *profile; /* never nullptr */ + guint profile_changed_id; + guint profile_forgotten_id; + int child_pid; + GSList *match_tags; + gboolean exec_on_realize; + guint idle_exec_source; + ExecData *exec_data; +}; + +enum +{ + PROFILE_SET, + SHOW_POPUP_MENU, + MATCH_CLICKED, + CLOSE_SCREEN, + LAST_SIGNAL +}; + +enum { + PROP_0, + PROP_PROFILE, + PROP_TITLE, +}; + +enum +{ + TARGET_COLOR, + TARGET_BGIMAGE, + TARGET_RESET_BG, + TARGET_MOZ_URL, + TARGET_NETSCAPE_URL, + TARGET_TAB +}; + +static void terminal_screen_constructed (GObject *object); +static void terminal_screen_dispose (GObject *object); +static void terminal_screen_finalize (GObject *object); +static void terminal_screen_drag_data_received (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + GtkSelectionData *selection_data, + guint info, + guint time); +static void terminal_screen_set_font (TerminalScreen *screen); +static void terminal_screen_system_font_changed_cb (GSettings *, + const char*, + TerminalScreen *screen); +static gboolean terminal_screen_popup_menu (GtkWidget *widget); +static gboolean terminal_screen_button_press (GtkWidget *widget, + GdkEventButton *event); +static void terminal_screen_child_exited (VteTerminal *terminal, + int status); + +static void terminal_screen_window_title_changed (VteTerminal *vte_terminal, + TerminalScreen *screen); + +static void update_color_scheme (TerminalScreen *screen); + +static char* terminal_screen_check_hyperlink (TerminalScreen *screen, + GdkEvent *event); +static void terminal_screen_check_extra (TerminalScreen *screen, + GdkEvent *event, + char **number_info, + char **timestamp_info); +static char* terminal_screen_check_match (TerminalScreen *screen, + GdkEvent *event, + int *flavor); + +static void terminal_screen_show_info_bar (TerminalScreen *screen, + GError *error, + gboolean show_relaunch); + + +static char**terminal_screen_get_child_environment (TerminalScreen *screen, + char **initial_envv, + char **path, + char **shell); + +static gboolean terminal_screen_get_child_command (TerminalScreen *screen, + char **exec_argv, + const char *path_env, + const char *shell_env, + gboolean shell, + gboolean *preserve_cwd_p, + GSpawnFlags *spawn_flags_p, + char ***argv_p, + GError **err); + +static void terminal_screen_queue_idle_exec (TerminalScreen *screen); + +static guint signals[LAST_SIGNAL]; + +typedef struct { + const char *pattern; + TerminalURLFlavor flavor; +} TerminalRegexPattern; + +static const TerminalRegexPattern url_regex_patterns[] = { + { REGEX_URL_AS_IS, FLAVOR_AS_IS }, + { REGEX_URL_HTTP, FLAVOR_DEFAULT_TO_HTTP }, + { REGEX_URL_FILE, FLAVOR_AS_IS }, + { REGEX_URL_VOIP, FLAVOR_VOIP_CALL }, + { REGEX_EMAIL, FLAVOR_EMAIL }, + { REGEX_NEWS_MAN, FLAVOR_AS_IS }, +}; + +static const TerminalRegexPattern extra_regex_patterns[] = { + { "(0[Xx][[:xdigit:]]+|[[:digit:]]+)", FLAVOR_NUMBER }, +}; + +static VteRegex **url_regexes; +static VteRegex **extra_regexes; +static TerminalURLFlavor *url_regex_flavors; +static TerminalURLFlavor *extra_regex_flavors; +static guint n_url_regexes; +static guint n_extra_regexes; + +/* See bug #697024 */ +#ifndef __linux__ + +#undef dup3 +#define dup3 fake_dup3 + +static int +fake_dup3 (int fd, int fd2, int flags) +{ + if (dup2 (fd, fd2) == -1) + return -1; + + return fcntl (fd2, F_SETFD, flags); +} +#endif /* !__linux__ */ + +static char* +strv_to_string (char **strv) +{ + return strv ? g_strjoinv (" ", strv) : g_strdup ("(null)"); +} + +static char* +exec_data_to_string (ExecData *data) +{ + gs_free char *str1 = nullptr; + gs_free char *str2 = nullptr; + return data ? g_strdup_printf ("data %p argv:[%s] exec-argv:[%s] envv:%p(%u) as-shell:%s cwd:%s", + data, + (str1 = strv_to_string (data->argv)), + (str2 = strv_to_string (data->exec_argv)), + data->envv, data->envv ? g_strv_length (data->envv) : 0, + data->as_shell ? "true" : "false", + data->cwd) + : g_strdup ("(null)"); +} + +static ExecData* +exec_data_new (void) +{ + ExecData *data = g_new0 (ExecData, 1); + data->refcount = 1; + + return data; +} + +static ExecData * +exec_data_clone (ExecData *data, + gboolean preserve_argv) +{ + if (data == nullptr) + return nullptr; + + ExecData *clone = exec_data_new (); + clone->envv = g_strdupv (data->envv); + clone->cwd = g_strdup (data->cwd); + + /* If FDs were passed, cannot repeat argv. Return data only for env and cwd */ + if (!preserve_argv || + data->fd_list != nullptr) { + clone->as_shell = TRUE; + return clone; + } + + clone->argv = g_strdupv (data->argv); + clone->as_shell = data->as_shell; + + return clone; +} + +static void +exec_data_callback (ExecData *data, + GError *error, + TerminalScreen *screen) +{ + if (data->callback) + data->callback (screen, error, data->callback_data); +} + +static ExecData* +exec_data_ref (ExecData *data) +{ + data->refcount++; + return data; +} + +static void +exec_data_unref (ExecData *data) +{ + if (data == nullptr) + return; + + if (--data->refcount > 0) + return; + + g_strfreev (data->argv); + g_strfreev (data->exec_argv); + g_strfreev (data->envv); + g_free (data->cwd); + g_clear_object (&data->fd_list); + g_free (data->fd_map); + + if (data->callback_data_destroy_notify && data->callback_data) + data->callback_data_destroy_notify (data->callback_data); + + g_clear_object (&data->cancellable); + + g_free (data); +} + +GS_DEFINE_CLEANUP_FUNCTION0(ExecData*, _terminal_local_unref_exec_data, exec_data_unref) +#define terminal_unref_exec_data __attribute__((__cleanup__(_terminal_local_unref_exec_data))) + +static void +terminal_screen_clear_exec_data (TerminalScreen *screen, + gboolean cancelled) +{ + TerminalScreenPrivate *priv = screen->priv; + + if (priv->exec_data == nullptr) + return; + + if (cancelled) { + gs_free_error GError *err = nullptr; + g_set_error_literal (&err, G_IO_ERROR, G_IO_ERROR_CANCELLED, + "Spawning was cancelled"); + exec_data_callback (priv->exec_data, err, screen); + } + + exec_data_unref (priv->exec_data); + priv->exec_data = nullptr; +} + +G_DEFINE_TYPE (TerminalScreen, terminal_screen, VTE_TYPE_TERMINAL) + +static void +free_tag_data (TagData *tagdata) +{ + g_slice_free (TagData, tagdata); +} + +static void +precompile_regexes (const TerminalRegexPattern *regex_patterns, + guint n_regexes, + VteRegex ***regexes, + TerminalURLFlavor **regex_flavors) +{ + guint i; + + *regexes = g_new0 (VteRegex*, n_regexes); + *regex_flavors = g_new0 (TerminalURLFlavor, n_regexes); + + for (i = 0; i < n_regexes; ++i) + { + GError *error = nullptr; + + (*regexes)[i] = vte_regex_new_for_match (regex_patterns[i].pattern, -1, + PCRE2_UTF | PCRE2_NO_UTF_CHECK | PCRE2_UCP | PCRE2_MULTILINE, + &error); + g_assert_no_error (error); + + if (!vte_regex_jit ((*regexes)[i], PCRE2_JIT_COMPLETE, &error) || + !vte_regex_jit ((*regexes)[i], PCRE2_JIT_PARTIAL_SOFT, &error)) { + g_printerr ("Failed to JIT regex '%s': %s\n", regex_patterns[i].pattern, error->message); + g_clear_error (&error); + } + + (*regex_flavors)[i] = regex_patterns[i].flavor; + } +} + +static void +terminal_screen_class_enable_menu_bar_accel_notify_cb (GSettings *settings, + const char *key, + TerminalScreenClass *klass) +{ + static gboolean is_enabled = TRUE; /* the binding is enabled by default since GtkWidgetClass installs it */ + gboolean enable; + GtkBindingSet *binding_set; + + enable = g_settings_get_boolean (settings, key); + + /* Only remove the 'skip' entry when we have added it previously! */ + if (enable == is_enabled) + return; + + is_enabled = enable; + + binding_set = gtk_binding_set_by_class (klass); + if (enable) + gtk_binding_entry_remove (binding_set, GDK_KEY_F10, GDK_SHIFT_MASK); + else + gtk_binding_entry_skip (binding_set, GDK_KEY_F10, GDK_SHIFT_MASK); +} + +static TerminalWindow * +terminal_screen_get_window (TerminalScreen *screen) +{ + GtkWidget *widget = GTK_WIDGET (screen); + GtkWidget *toplevel; + + toplevel = gtk_widget_get_toplevel (widget); + if (!gtk_widget_is_toplevel (toplevel)) + return nullptr; + + return TERMINAL_WINDOW (toplevel); +} + +static void +terminal_screen_realize (GtkWidget *widget) +{ + TerminalScreen *screen = TERMINAL_SCREEN (widget); + + GTK_WIDGET_CLASS (terminal_screen_parent_class)->realize (widget); + + terminal_screen_set_font (screen); + + TerminalScreenPrivate *priv = screen->priv; + if (priv->exec_on_realize) + terminal_screen_queue_idle_exec (screen); + + priv->exec_on_realize = FALSE; + +} + +static void +terminal_screen_update_style (TerminalScreen *screen) +{ + update_color_scheme (screen); + terminal_screen_set_font (screen); +} + +static void +terminal_screen_style_updated (GtkWidget *widget) +{ + TerminalScreen *screen = TERMINAL_SCREEN (widget); + + GTK_WIDGET_CLASS (terminal_screen_parent_class)->style_updated (widget); + + terminal_screen_update_style (screen); +} + +static void +terminal_screen_init (TerminalScreen *screen) +{ + const GtkTargetEntry target_table[] = { + { (char *) "GTK_NOTEBOOK_TAB", GTK_TARGET_SAME_APP, TARGET_TAB }, + { (char *) "application/x-color", 0, TARGET_COLOR }, + { (char *) "x-special/gnome-reset-background", 0, TARGET_RESET_BG }, + { (char *) "text/x-moz-url", 0, TARGET_MOZ_URL }, + { (char *) "_NETSCAPE_URL", 0, TARGET_NETSCAPE_URL } + }; + VteTerminal *terminal = VTE_TERMINAL (screen); + TerminalScreenPrivate *priv; + TerminalApp *app; + GtkTargetList *target_list; + GtkTargetEntry *targets; + int n_targets; + guint i; + uuid_t u; + char uuidstr[37]; + + priv = screen->priv = G_TYPE_INSTANCE_GET_PRIVATE (screen, TERMINAL_TYPE_SCREEN, TerminalScreenPrivate); + + uuid_generate (u); + uuid_unparse (u, uuidstr); + priv->uuid = g_strdup (uuidstr); + + vte_terminal_set_mouse_autohide (terminal, TRUE); + + priv->child_pid = -1; + + vte_terminal_set_allow_hyperlink (terminal, TRUE); + + for (i = 0; i < n_url_regexes; ++i) + { + TagData *tag_data; + + tag_data = g_slice_new (TagData); + tag_data->flavor = url_regex_flavors[i]; + tag_data->tag = vte_terminal_match_add_regex (terminal, url_regexes[i], 0); + vte_terminal_match_set_cursor_name (terminal, tag_data->tag, URL_MATCH_CURSOR_NAME); + + priv->match_tags = g_slist_prepend (priv->match_tags, tag_data); + } + + /* Setup DND */ + target_list = gtk_target_list_new (nullptr, 0); + gtk_target_list_add_uri_targets (target_list, 0); + gtk_target_list_add_text_targets (target_list, 0); + gtk_target_list_add_table (target_list, target_table, G_N_ELEMENTS (target_table)); + + targets = gtk_target_table_new_from_list (target_list, &n_targets); + + gtk_drag_dest_set (GTK_WIDGET (screen), + GtkDestDefaults(GTK_DEST_DEFAULT_MOTION | + GTK_DEST_DEFAULT_HIGHLIGHT | + GTK_DEST_DEFAULT_DROP), + targets, n_targets, + GdkDragAction(GDK_ACTION_COPY | GDK_ACTION_MOVE)); + + gtk_target_table_free (targets, n_targets); + gtk_target_list_unref (target_list); + + g_signal_connect (screen, "window-title-changed", + G_CALLBACK (terminal_screen_window_title_changed), + screen); + + app = terminal_app_get (); + g_signal_connect (terminal_app_get_desktop_interface_settings (app), "changed::" MONOSPACE_FONT_KEY_NAME, + G_CALLBACK (terminal_screen_system_font_changed_cb), screen); + +} + +static void +terminal_screen_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + TerminalScreen *screen = TERMINAL_SCREEN (object); + + switch (prop_id) + { + case PROP_PROFILE: + g_value_set_object (value, terminal_screen_get_profile (screen)); + break; + case PROP_TITLE: + g_value_set_string (value, terminal_screen_get_title (screen)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +terminal_screen_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + TerminalScreen *screen = TERMINAL_SCREEN (object); + + switch (prop_id) + { + case PROP_PROFILE: + terminal_screen_set_profile (screen, (GSettings*)g_value_get_object (value)); + break; + case PROP_TITLE: + /* not writable */ + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +terminal_screen_class_init (TerminalScreenClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass); + VteTerminalClass *terminal_class = VTE_TERMINAL_CLASS (klass); + GSettings *settings; + + object_class->constructed = terminal_screen_constructed; + object_class->dispose = terminal_screen_dispose; + object_class->finalize = terminal_screen_finalize; + object_class->get_property = terminal_screen_get_property; + object_class->set_property = terminal_screen_set_property; + + widget_class->realize = terminal_screen_realize; + widget_class->style_updated = terminal_screen_style_updated; + widget_class->drag_data_received = terminal_screen_drag_data_received; + widget_class->button_press_event = terminal_screen_button_press; + widget_class->popup_menu = terminal_screen_popup_menu; + + terminal_class->child_exited = terminal_screen_child_exited; + + signals[PROFILE_SET] = + g_signal_new (I_("profile-set"), + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (TerminalScreenClass, profile_set), + nullptr, nullptr, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, + 1, G_TYPE_SETTINGS); + + signals[SHOW_POPUP_MENU] = + g_signal_new (I_("show-popup-menu"), + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (TerminalScreenClass, show_popup_menu), + nullptr, nullptr, + g_cclosure_marshal_VOID__POINTER, + G_TYPE_NONE, + 1, + G_TYPE_POINTER); + + signals[MATCH_CLICKED] = + g_signal_new (I_("match-clicked"), + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (TerminalScreenClass, match_clicked), + g_signal_accumulator_true_handled, nullptr, + _terminal_marshal_BOOLEAN__STRING_INT_UINT, + G_TYPE_BOOLEAN, + 3, G_TYPE_STRING, G_TYPE_INT, G_TYPE_UINT); + + signals[CLOSE_SCREEN] = + g_signal_new (I_("close-screen"), + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (TerminalScreenClass, close_screen), + nullptr, nullptr, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, + 0); + + g_object_class_install_property + (object_class, + PROP_PROFILE, + g_param_spec_object ("profile", nullptr, nullptr, + G_TYPE_SETTINGS, + GParamFlags(G_PARAM_READWRITE | + G_PARAM_STATIC_NAME | + G_PARAM_STATIC_NICK | + G_PARAM_STATIC_BLURB))); + + g_object_class_install_property + (object_class, + PROP_TITLE, + g_param_spec_string ("title", nullptr, nullptr, + nullptr, + GParamFlags(G_PARAM_READABLE | + G_PARAM_STATIC_NAME | + G_PARAM_STATIC_NICK | + G_PARAM_STATIC_BLURB))); + + g_type_class_add_private (object_class, sizeof (TerminalScreenPrivate)); + + n_url_regexes = G_N_ELEMENTS (url_regex_patterns); + precompile_regexes (url_regex_patterns, n_url_regexes, &url_regexes, &url_regex_flavors); + n_extra_regexes = G_N_ELEMENTS (extra_regex_patterns); + precompile_regexes (extra_regex_patterns, n_extra_regexes, &extra_regexes, &extra_regex_flavors); + + /* This fixes bug #329827 */ + settings = terminal_app_get_global_settings (terminal_app_get ()); + terminal_screen_class_enable_menu_bar_accel_notify_cb (settings, TERMINAL_SETTING_ENABLE_MENU_BAR_ACCEL_KEY, klass); + g_signal_connect (settings, "changed::" TERMINAL_SETTING_ENABLE_MENU_BAR_ACCEL_KEY, + G_CALLBACK (terminal_screen_class_enable_menu_bar_accel_notify_cb), klass); +} + +static void +terminal_screen_constructed (GObject *object) +{ + TerminalScreen *screen = TERMINAL_SCREEN (object); + TerminalScreenPrivate *priv = screen->priv; + + G_OBJECT_CLASS (terminal_screen_parent_class)->constructed (object); + + terminal_app_register_screen (terminal_app_get (), screen); + priv->registered = TRUE; +} + +static void +terminal_screen_dispose (GObject *object) +{ + TerminalScreen *screen = TERMINAL_SCREEN (object); + TerminalScreenPrivate *priv = screen->priv; + GtkSettings *settings; + + /* Unset child PID so that when an eventual child-exited signal arrives, + * we don't emit "close". + */ + priv->child_pid = -1; + + settings = gtk_widget_get_settings (GTK_WIDGET (screen)); + g_signal_handlers_disconnect_matched (settings, G_SIGNAL_MATCH_DATA, + 0, 0, nullptr, nullptr, + screen); + + if (priv->idle_exec_source != 0) + { + g_source_remove (priv->idle_exec_source); + priv->idle_exec_source = 0; + } + + terminal_screen_clear_exec_data (screen, TRUE); + + G_OBJECT_CLASS (terminal_screen_parent_class)->dispose (object); + + /* Unregister *after* chaining up to the parent's dispose, + * since that will terminate the child process if there still + * is any, and we need to get the dbus signal out + * from the TerminalReceiver. + */ + if (priv->registered) { + terminal_app_unregister_screen (terminal_app_get (), screen); + priv->registered = FALSE; + } +} + +static void +terminal_screen_finalize (GObject *object) +{ + TerminalScreen *screen = TERMINAL_SCREEN (object); + TerminalScreenPrivate *priv = screen->priv; + + g_signal_handlers_disconnect_by_func (terminal_app_get_desktop_interface_settings (terminal_app_get ()), + (void*)terminal_screen_system_font_changed_cb, + screen); + + terminal_screen_set_profile (screen, nullptr); + + g_slist_free_full (priv->match_tags, (GDestroyNotify) free_tag_data); + + g_free (priv->uuid); + + G_OBJECT_CLASS (terminal_screen_parent_class)->finalize (object); +} + +TerminalScreen * +terminal_screen_new (GSettings *profile, + const char *title, + double zoom) +{ + g_return_val_if_fail (G_IS_SETTINGS (profile), nullptr); + + TerminalScreen *screen = (TerminalScreen*)g_object_new (TERMINAL_TYPE_SCREEN, nullptr); + + terminal_screen_set_profile (screen, profile); + + vte_terminal_set_size (VTE_TERMINAL (screen), + g_settings_get_int (profile, TERMINAL_PROFILE_DEFAULT_SIZE_COLUMNS_KEY), + g_settings_get_int (profile, TERMINAL_PROFILE_DEFAULT_SIZE_ROWS_KEY)); + + /* If given an initial title, strip it of control characters and + * feed it to the terminal. + */ + if (title != nullptr) { + GString *seq; + const char *p; + + seq = g_string_new ("\033]0;"); + for (p = title; *p; p = g_utf8_next_char (p)) { + gunichar c = g_utf8_get_char (p); + if (c < 0x20 || (c >= 0x7f && c <= 0x9f)) + continue; + else if (c == ';') + break; + + g_string_append_unichar (seq, c); + } + g_string_append (seq, "\033\\"); + + vte_terminal_feed (VTE_TERMINAL (screen), seq->str, seq->len); + g_string_free (seq, TRUE); + } + + vte_terminal_set_font_scale (VTE_TERMINAL (screen), zoom); + terminal_screen_set_font (screen); + + return screen; +} + +static gboolean +terminal_screen_reexec_from_exec_data (TerminalScreen *screen, + ExecData *data, + char **envv, + const char *cwd, + GCancellable *cancellable, + GError **error) +{ + _TERMINAL_DEBUG_IF (TERMINAL_DEBUG_PROCESSES) { + gs_free char *str = exec_data_to_string (data); + _terminal_debug_print (TERMINAL_DEBUG_PROCESSES, + "[screen %p] reexec_from_data: envv:%p(%u) cwd:%s data:[%s]\n", + screen, + envv, envv ? g_strv_length (envv) : 0, + cwd, + str); + } + + return terminal_screen_exec (screen, + data ? data->argv : nullptr, + envv ? envv : data ? data->envv : nullptr, + data ? data->as_shell : TRUE, + /* If we have command line args, must always pass the cwd from the command line, too */ + data && data->argv ? data->cwd : cwd ? cwd : data ? data->cwd : nullptr, + nullptr /* fd list */, nullptr /* fd array */, + nullptr, nullptr, nullptr, /* callback + data + destroy notify */ + cancellable, + error); +} + +gboolean +terminal_screen_reexec_from_screen (TerminalScreen *screen, + TerminalScreen *parent_screen, + GCancellable *cancellable, + GError **error) +{ + g_return_val_if_fail (TERMINAL_IS_SCREEN (screen), FALSE); + + if (parent_screen == nullptr) + return TRUE; + + g_return_val_if_fail (TERMINAL_IS_SCREEN (parent_screen), FALSE); + + terminal_unref_exec_data ExecData* data = exec_data_clone (parent_screen->priv->exec_data, FALSE); + gs_free char* cwd = terminal_screen_get_current_dir (parent_screen); + + _terminal_debug_print (TERMINAL_DEBUG_PROCESSES, + "[screen %p] reexec_from_screen: parent:%p cwd:%s\n", + screen, + parent_screen, + cwd); + + return terminal_screen_reexec_from_exec_data (screen, + data, + nullptr /* envv */, + cwd, + cancellable, + error); +} + +gboolean +terminal_screen_reexec (TerminalScreen *screen, + char **envv, + const char *cwd, + GCancellable *cancellable, + GError **error) +{ + g_return_val_if_fail (TERMINAL_IS_SCREEN (screen), FALSE); + + + _terminal_debug_print (TERMINAL_DEBUG_PROCESSES, + "[screen %p] reexec: envv:%p(%u) cwd:%s\n", + screen, + envv, envv ? g_strv_length (envv) : 0, + cwd); + + return terminal_screen_reexec_from_exec_data (screen, + screen->priv->exec_data, + envv, + cwd, + cancellable, + error); +} + +gboolean +terminal_screen_exec (TerminalScreen *screen, + char **argv, + char **initial_envv, + gboolean as_shell, + const char *cwd, + GUnixFDList *fd_list, + GVariant *fd_array, + TerminalScreenExecCallback callback, + gpointer user_data, + GDestroyNotify destroy_notify, + GCancellable *cancellable, + GError **error) +{ + g_return_val_if_fail (TERMINAL_IS_SCREEN (screen), FALSE); + g_return_val_if_fail (cancellable == nullptr || G_IS_CANCELLABLE (cancellable), FALSE); + g_return_val_if_fail (error == nullptr || *error == nullptr, FALSE); + g_return_val_if_fail (gtk_widget_get_parent (GTK_WIDGET (screen)) != nullptr, FALSE); + + _TERMINAL_DEBUG_IF (TERMINAL_DEBUG_PROCESSES) { + gs_free char *argv_str = nullptr; + _terminal_debug_print (TERMINAL_DEBUG_PROCESSES, + "[screen %p] exec: argv:[%s] envv:%p(%u) as-shell:%s cwd:%s\n", + screen, + (argv_str = strv_to_string(argv)), + initial_envv, initial_envv ? g_strv_length (initial_envv) : 0, + as_shell ? "true":"false", + cwd); + } + + TerminalScreenPrivate *priv = screen->priv; + + ExecData *data = exec_data_new (); + data->callback = callback; + data->callback_data = user_data; + data->callback_data_destroy_notify = destroy_notify; + + GError *err = nullptr; + if (priv->child_pid != -1) { + g_set_error_literal (&err, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, + "Cannot launch a new child process while the terminal is still running another child process"); + + terminal_screen_show_info_bar (screen, err, FALSE); + g_propagate_error (error, err); + exec_data_unref (data); /* frees the callback data */ + return FALSE; + } + + gs_free char *path = nullptr; + gs_free char *shell = nullptr; + gs_strfreev char **envv = terminal_screen_get_child_environment (screen, + initial_envv, + &path, + &shell); + + gboolean preserve_cwd = FALSE; + GSpawnFlags spawn_flags = GSpawnFlags(G_SPAWN_SEARCH_PATH_FROM_ENVP | + VTE_SPAWN_NO_PARENT_ENVV); + gs_strfreev char **exec_argv = nullptr; + if (!terminal_screen_get_child_command (screen, + argv, + path, + shell, + as_shell, + &preserve_cwd, + &spawn_flags, + &exec_argv, + &err)) { + terminal_screen_show_info_bar (screen, err, FALSE); + g_propagate_error (error, err); + exec_data_unref (data); /* frees the callback data */ + return FALSE; + } + + if (!preserve_cwd) { + cwd = g_get_home_dir (); + envv = g_environ_unsetenv (envv, "PWD"); + } + + data->fd_list = (GUnixFDList*)(fd_list ? g_object_ref(fd_list) : nullptr); + + if (fd_array) { + g_assert_nonnull(fd_list); + int n_fds = g_unix_fd_list_get_length(fd_list); + + gsize fd_array_data_len; + const int *fd_array_data = (int const*)g_variant_get_fixed_array (fd_array, &fd_array_data_len, 2 * sizeof (int)); + + data->n_fd_map = fd_array_data_len; + data->fd_map = g_new (int, data->n_fd_map); + for (gsize i = 0; i < fd_array_data_len; i++) { + const int fd = fd_array_data[2 * i]; + const int idx = fd_array_data[2 * i + 1]; + g_assert_cmpint(idx, >=, 0); + g_assert_cmpuint(idx, <, n_fds); + + data->fd_map[idx] = fd; + } + } else { + data->n_fd_map = 0; + data->fd_map = nullptr; + } + + data->argv = g_strdupv (argv); + data->exec_argv = g_strdupv (exec_argv); + data->cwd = g_strdup (cwd); + data->envv = g_strdupv (envv); + data->as_shell = as_shell; + data->pty_flags = VTE_PTY_DEFAULT; + data->spawn_flags = spawn_flags; + data->cancellable = (GCancellable*)(cancellable ? g_object_ref (cancellable) : nullptr); + + terminal_screen_clear_exec_data (screen, TRUE); + priv->exec_data = data; + + terminal_screen_queue_idle_exec (screen); + + return TRUE; +} + +const char* +terminal_screen_get_title (TerminalScreen *screen) +{ + return vte_terminal_get_window_title (VTE_TERMINAL (screen)); +} + +static void +terminal_screen_profile_changed_cb (GSettings *profile, + const char *prop_name, + TerminalScreen *screen) +{ + TerminalScreenPrivate *priv = screen->priv; + GObject *object = G_OBJECT (screen); + VteTerminal *vte_terminal = VTE_TERMINAL (screen); + TerminalWindow *window; + + g_object_freeze_notify (object); + + if ((window = terminal_screen_get_window (screen))) + { + /* We need these in line for the set_size in + * update_on_realize + */ + terminal_window_update_geometry (window); + } + + if (!prop_name || prop_name == I_(TERMINAL_PROFILE_SCROLLBAR_POLICY_KEY)) + _terminal_screen_update_scrollbar (screen); + + if (!prop_name || prop_name == I_(TERMINAL_PROFILE_ENCODING_KEY)) + { + gs_free char *charset = g_settings_get_string (profile, TERMINAL_PROFILE_ENCODING_KEY); + const char *encoding = terminal_util_translate_encoding (charset); + if (encoding != nullptr) + vte_terminal_set_encoding (vte_terminal, encoding, nullptr); + } + + if (!prop_name || prop_name == I_(TERMINAL_PROFILE_CJK_UTF8_AMBIGUOUS_WIDTH_KEY)) + { + int width; + + width = g_settings_get_enum (profile, TERMINAL_PROFILE_CJK_UTF8_AMBIGUOUS_WIDTH_KEY); + vte_terminal_set_cjk_ambiguous_width (vte_terminal, width); + } + + if (gtk_widget_get_realized (GTK_WIDGET (screen)) && + (!prop_name || + prop_name == I_(TERMINAL_PROFILE_USE_SYSTEM_FONT_KEY) || + prop_name == I_(TERMINAL_PROFILE_FONT_KEY) || + prop_name == I_(TERMINAL_PROFILE_CELL_WIDTH_SCALE_KEY) || + prop_name == I_(TERMINAL_PROFILE_CELL_HEIGHT_SCALE_KEY))) + terminal_screen_set_font (screen); + + if (!prop_name || + prop_name == I_(TERMINAL_PROFILE_USE_THEME_COLORS_KEY) || + prop_name == I_(TERMINAL_PROFILE_FOREGROUND_COLOR_KEY) || + prop_name == I_(TERMINAL_PROFILE_BACKGROUND_COLOR_KEY) || + prop_name == I_(TERMINAL_PROFILE_BOLD_COLOR_SAME_AS_FG_KEY) || + prop_name == I_(TERMINAL_PROFILE_BOLD_COLOR_KEY) || + prop_name == I_(TERMINAL_PROFILE_CURSOR_COLORS_SET_KEY) || + prop_name == I_(TERMINAL_PROFILE_CURSOR_BACKGROUND_COLOR_KEY) || + prop_name == I_(TERMINAL_PROFILE_CURSOR_FOREGROUND_COLOR_KEY) || + prop_name == I_(TERMINAL_PROFILE_HIGHLIGHT_COLORS_SET_KEY) || + prop_name == I_(TERMINAL_PROFILE_HIGHLIGHT_BACKGROUND_COLOR_KEY) || + prop_name == I_(TERMINAL_PROFILE_HIGHLIGHT_FOREGROUND_COLOR_KEY) || + prop_name == I_(TERMINAL_PROFILE_PALETTE_KEY)) + update_color_scheme (screen); + + if (!prop_name || prop_name == I_(TERMINAL_PROFILE_AUDIBLE_BELL_KEY)) + vte_terminal_set_audible_bell (vte_terminal, g_settings_get_boolean (profile, TERMINAL_PROFILE_AUDIBLE_BELL_KEY)); + + if (!prop_name || prop_name == I_(TERMINAL_PROFILE_SCROLL_ON_KEYSTROKE_KEY)) + vte_terminal_set_scroll_on_keystroke (vte_terminal, + g_settings_get_boolean (profile, TERMINAL_PROFILE_SCROLL_ON_KEYSTROKE_KEY)); + if (!prop_name || prop_name == I_(TERMINAL_PROFILE_SCROLL_ON_OUTPUT_KEY)) + vte_terminal_set_scroll_on_output (vte_terminal, + g_settings_get_boolean (profile, TERMINAL_PROFILE_SCROLL_ON_OUTPUT_KEY)); + if (!prop_name || + prop_name == I_(TERMINAL_PROFILE_SCROLLBACK_LINES_KEY) || + prop_name == I_(TERMINAL_PROFILE_SCROLLBACK_UNLIMITED_KEY)) + { + glong lines = g_settings_get_boolean (profile, TERMINAL_PROFILE_SCROLLBACK_UNLIMITED_KEY) ? + -1 : g_settings_get_int (profile, TERMINAL_PROFILE_SCROLLBACK_LINES_KEY); + vte_terminal_set_scrollback_lines (vte_terminal, lines); + } + + if (!prop_name || prop_name == I_(TERMINAL_PROFILE_BACKSPACE_BINDING_KEY)) + vte_terminal_set_backspace_binding (vte_terminal, + VteEraseBinding(g_settings_get_enum (profile, TERMINAL_PROFILE_BACKSPACE_BINDING_KEY))); + + if (!prop_name || prop_name == I_(TERMINAL_PROFILE_DELETE_BINDING_KEY)) + vte_terminal_set_delete_binding (vte_terminal, + VteEraseBinding(g_settings_get_enum (profile, TERMINAL_PROFILE_DELETE_BINDING_KEY))); + + if (!prop_name || prop_name == I_(TERMINAL_PROFILE_ENABLE_BIDI_KEY)) + vte_terminal_set_enable_bidi (vte_terminal, + g_settings_get_boolean (profile, TERMINAL_PROFILE_ENABLE_BIDI_KEY)); + if (!prop_name || prop_name == I_(TERMINAL_PROFILE_ENABLE_SHAPING_KEY)) + vte_terminal_set_enable_shaping (vte_terminal, + g_settings_get_boolean (profile, TERMINAL_PROFILE_ENABLE_SHAPING_KEY)); + + if (!prop_name || prop_name == I_(TERMINAL_PROFILE_ENABLE_SIXEL_KEY)) + vte_terminal_set_enable_sixel (vte_terminal, + g_settings_get_boolean (profile, TERMINAL_PROFILE_ENABLE_SIXEL_KEY)); + + if (!prop_name || prop_name == I_(TERMINAL_PROFILE_BOLD_IS_BRIGHT_KEY)) + vte_terminal_set_bold_is_bright (vte_terminal, + g_settings_get_boolean (profile, TERMINAL_PROFILE_BOLD_IS_BRIGHT_KEY)); + + if (!prop_name || prop_name == I_(TERMINAL_PROFILE_CURSOR_BLINK_MODE_KEY)) + vte_terminal_set_cursor_blink_mode (vte_terminal, + VteCursorBlinkMode(g_settings_get_enum (priv->profile, TERMINAL_PROFILE_CURSOR_BLINK_MODE_KEY))); + + if (!prop_name || prop_name == I_(TERMINAL_PROFILE_CURSOR_SHAPE_KEY)) + vte_terminal_set_cursor_shape (vte_terminal, + VteCursorShape(g_settings_get_enum (priv->profile, TERMINAL_PROFILE_CURSOR_SHAPE_KEY))); + + if (!prop_name || prop_name == I_(TERMINAL_PROFILE_REWRAP_ON_RESIZE_KEY)) + vte_terminal_set_rewrap_on_resize (vte_terminal, + g_settings_get_boolean (profile, TERMINAL_PROFILE_REWRAP_ON_RESIZE_KEY)); + + if (!prop_name || prop_name == I_(TERMINAL_PROFILE_TEXT_BLINK_MODE_KEY)) + vte_terminal_set_text_blink_mode (vte_terminal, + VteTextBlinkMode(g_settings_get_enum (profile, TERMINAL_PROFILE_TEXT_BLINK_MODE_KEY))); + + if (!prop_name || prop_name == I_(TERMINAL_PROFILE_WORD_CHAR_EXCEPTIONS_KEY)) + { + gs_free char *word_char_exceptions; + g_settings_get (profile, TERMINAL_PROFILE_WORD_CHAR_EXCEPTIONS_KEY, "ms", &word_char_exceptions); + vte_terminal_set_word_char_exceptions (vte_terminal, word_char_exceptions); + } + + g_object_thaw_notify (object); +} + +static void +update_color_scheme (TerminalScreen *screen) +{ + GtkWidget *widget = GTK_WIDGET (screen); + TerminalScreenPrivate *priv = screen->priv; + GSettings *profile = priv->profile; + gs_free GdkRGBA *colors; + gsize n_colors; + GdkRGBA fg, bg, bold, theme_fg, theme_bg; + GdkRGBA cursor_bg, cursor_fg; + GdkRGBA highlight_bg, highlight_fg; + GdkRGBA *boldp; + GdkRGBA *cursor_bgp = nullptr, *cursor_fgp = nullptr; + GdkRGBA *highlight_bgp = nullptr, *highlight_fgp = nullptr; + GtkStyleContext *context; + gboolean use_theme_colors; + + context = gtk_widget_get_style_context (widget); + gtk_style_context_get_color (context, gtk_style_context_get_state (context), &theme_fg); + G_GNUC_BEGIN_IGNORE_DEPRECATIONS + gtk_style_context_get_background_color (context, gtk_style_context_get_state (context), &theme_bg); + G_GNUC_END_IGNORE_DEPRECATIONS + + use_theme_colors = g_settings_get_boolean (profile, TERMINAL_PROFILE_USE_THEME_COLORS_KEY); + if (use_theme_colors || + (!terminal_g_settings_get_rgba (profile, TERMINAL_PROFILE_FOREGROUND_COLOR_KEY, &fg) || + !terminal_g_settings_get_rgba (profile, TERMINAL_PROFILE_BACKGROUND_COLOR_KEY, &bg))) + { + fg = theme_fg; + bg = theme_bg; + } + + if (!g_settings_get_boolean (profile, TERMINAL_PROFILE_BOLD_COLOR_SAME_AS_FG_KEY) && + !use_theme_colors && + terminal_g_settings_get_rgba (profile, TERMINAL_PROFILE_BOLD_COLOR_KEY, &bold)) + boldp = &bold; + else + boldp = nullptr; + + if (g_settings_get_boolean (profile, TERMINAL_PROFILE_CURSOR_COLORS_SET_KEY) && + !use_theme_colors) + { + if (terminal_g_settings_get_rgba (profile, TERMINAL_PROFILE_CURSOR_BACKGROUND_COLOR_KEY, &cursor_bg)) + cursor_bgp = &cursor_bg; + if (terminal_g_settings_get_rgba (profile, TERMINAL_PROFILE_CURSOR_FOREGROUND_COLOR_KEY, &cursor_fg)) + cursor_fgp = &cursor_fg; + } + + if (g_settings_get_boolean (profile, TERMINAL_PROFILE_HIGHLIGHT_COLORS_SET_KEY) && + !use_theme_colors) + { + if (terminal_g_settings_get_rgba (profile, TERMINAL_PROFILE_HIGHLIGHT_BACKGROUND_COLOR_KEY, &highlight_bg)) + highlight_bgp = &highlight_bg; + if (terminal_g_settings_get_rgba (profile, TERMINAL_PROFILE_HIGHLIGHT_FOREGROUND_COLOR_KEY, &highlight_fg)) + highlight_fgp = &highlight_fg; + } + + colors = terminal_g_settings_get_rgba_palette (priv->profile, TERMINAL_PROFILE_PALETTE_KEY, &n_colors); + vte_terminal_set_colors (VTE_TERMINAL (screen), &fg, &bg, + colors, n_colors); + vte_terminal_set_color_bold (VTE_TERMINAL (screen), boldp); + vte_terminal_set_color_cursor (VTE_TERMINAL (screen), cursor_bgp); + vte_terminal_set_color_cursor_foreground (VTE_TERMINAL (screen), cursor_fgp); + vte_terminal_set_color_highlight (VTE_TERMINAL (screen), highlight_bgp); + vte_terminal_set_color_highlight_foreground (VTE_TERMINAL (screen), highlight_fgp); +} + +static void +terminal_screen_set_font (TerminalScreen *screen) +{ + TerminalScreenPrivate *priv = screen->priv; + GSettings *profile = priv->profile; + PangoFontDescription *desc; + int size; + + if (g_settings_get_boolean (profile, TERMINAL_PROFILE_USE_SYSTEM_FONT_KEY)) + { + desc = terminal_app_get_system_font (terminal_app_get ()); + } + else + { + gs_free char *font; + font = g_settings_get_string (profile, TERMINAL_PROFILE_FONT_KEY); + desc = pango_font_description_from_string (font); + } + + size = pango_font_description_get_size (desc); + /* Sanity check */ + if (size == 0) { + if (pango_font_description_get_size_is_absolute (desc)) + pango_font_description_set_absolute_size (desc, 10); + else + pango_font_description_set_size (desc, 10); + } + + vte_terminal_set_font (VTE_TERMINAL (screen), desc); + + pango_font_description_free (desc); + + vte_terminal_set_cell_width_scale (VTE_TERMINAL (screen), + g_settings_get_double (profile, TERMINAL_PROFILE_CELL_WIDTH_SCALE_KEY)); + vte_terminal_set_cell_height_scale (VTE_TERMINAL (screen), + g_settings_get_double (profile, TERMINAL_PROFILE_CELL_HEIGHT_SCALE_KEY)); +} + +static void +terminal_screen_system_font_changed_cb (GSettings *settings, + const char *key, + TerminalScreen *screen) +{ + TerminalScreenPrivate *priv = screen->priv; + + if (!gtk_widget_get_realized (GTK_WIDGET (screen))) + return; + + if (!g_settings_get_boolean (priv->profile, TERMINAL_PROFILE_USE_SYSTEM_FONT_KEY)) + return; + + terminal_screen_set_font (screen); +} + +void +terminal_screen_set_profile (TerminalScreen *screen, + GSettings *profile) +{ + TerminalScreenPrivate *priv = screen->priv; + GSettings*old_profile; + + old_profile = priv->profile; + if (profile == old_profile) + return; + + if (priv->profile_changed_id) + { + g_signal_handler_disconnect (G_OBJECT (priv->profile), + priv->profile_changed_id); + priv->profile_changed_id = 0; + } + + priv->profile = profile; + if (profile) + { + g_object_ref (profile); + priv->profile_changed_id = + g_signal_connect (profile, "changed", + G_CALLBACK (terminal_screen_profile_changed_cb), + screen); + terminal_screen_profile_changed_cb (profile, nullptr, screen); + + g_signal_emit (G_OBJECT (screen), signals[PROFILE_SET], 0, old_profile); + } + + if (old_profile) + g_object_unref (old_profile); + + g_object_notify (G_OBJECT (screen), "profile"); +} + +GSettings* +terminal_screen_get_profile (TerminalScreen *screen) +{ + TerminalScreenPrivate *priv = screen->priv; + + return priv->profile; +} + +GSettings* +terminal_screen_ref_profile (TerminalScreen *screen) +{ + TerminalScreenPrivate *priv = screen->priv; + + if (priv->profile != nullptr) + return (GSettings*)g_object_ref (priv->profile); + return nullptr; +} + +static gboolean +should_preserve_cwd (TerminalPreserveWorkingDirectory preserve_cwd, + const char *path, + const char *arg0) +{ + switch (preserve_cwd) { + case TERMINAL_PRESERVE_WORKING_DIRECTORY_SAFE: { + gs_free char *resolved_arg0 = terminal_util_find_program_in_path (path, arg0); + return resolved_arg0 != nullptr && + terminal_util_get_is_shell (resolved_arg0); + } + + case TERMINAL_PRESERVE_WORKING_DIRECTORY_ALWAYS: + return TRUE; + + case TERMINAL_PRESERVE_WORKING_DIRECTORY_NEVER: + default: + return FALSE; + } +} + +static gboolean +terminal_screen_get_child_command (TerminalScreen *screen, + char **argv, + const char *path_env, + const char *shell_env, + gboolean as_shell, + gboolean *preserve_cwd_p, + GSpawnFlags *spawn_flags_p, + char ***exec_argv_p, + GError **err) +{ + TerminalScreenPrivate *priv = screen->priv; + GSettings *profile = priv->profile; + TerminalPreserveWorkingDirectory preserve_cwd; + char **exec_argv; + + g_assert (spawn_flags_p != nullptr && exec_argv_p != nullptr && preserve_cwd_p != nullptr); + + *exec_argv_p = exec_argv = nullptr; + + preserve_cwd = TerminalPreserveWorkingDirectory + (g_settings_get_enum (profile, TERMINAL_PROFILE_PRESERVE_WORKING_DIRECTORY_KEY)); + + if (argv) + { + exec_argv = g_strdupv (argv); + + /* argv and cwd come from the command line client, so it must always be used */ + *preserve_cwd_p = TRUE; + *spawn_flags_p = GSpawnFlags(*spawn_flags_p | G_SPAWN_SEARCH_PATH_FROM_ENVP); + } + else if (g_settings_get_boolean (profile, TERMINAL_PROFILE_USE_CUSTOM_COMMAND_KEY)) + { + gs_free char *exec_argv_str; + + exec_argv_str = g_settings_get_string (profile, TERMINAL_PROFILE_CUSTOM_COMMAND_KEY); + if (!g_shell_parse_argv (exec_argv_str, nullptr, &exec_argv, err)) + return FALSE; + + *preserve_cwd_p = should_preserve_cwd (preserve_cwd, path_env, exec_argv[0]); + *spawn_flags_p = GSpawnFlags(*spawn_flags_p | G_SPAWN_SEARCH_PATH_FROM_ENVP); + } + else if (as_shell) + { + const char *only_name; + char *shell; + int argc = 0; + + shell = egg_shell (shell_env); + + only_name = strrchr (shell, '/'); + if (only_name != nullptr) + only_name++; + else { + only_name = shell; + *spawn_flags_p = GSpawnFlags(*spawn_flags_p | G_SPAWN_SEARCH_PATH_FROM_ENVP); + } + + exec_argv = g_new (char*, 3); + + exec_argv[argc++] = shell; + + if (g_settings_get_boolean (profile, TERMINAL_PROFILE_LOGIN_SHELL_KEY)) + exec_argv[argc++] = g_strconcat ("-", only_name, nullptr); + else + exec_argv[argc++] = g_strdup (only_name); + + exec_argv[argc++] = nullptr; + + *preserve_cwd_p = should_preserve_cwd (preserve_cwd, path_env, shell); + *spawn_flags_p = GSpawnFlags(*spawn_flags_p | G_SPAWN_FILE_AND_ARGV_ZERO); + } + + else + { + g_set_error_literal (err, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, + _("No command supplied nor shell requested")); + return FALSE; + } + + *exec_argv_p = exec_argv; + + return TRUE; +} + +static gboolean +remove_prefixed_cb(void* key, + void* value, + void* user_data) +{ + auto const env = reinterpret_cast<char const*>(key); + auto const prefix = reinterpret_cast<char const*>(user_data); + + if (terminal_client_get_environment_prefix_filters_is_excluded(env)) + return false; + + return g_str_has_prefix(env, prefix); +} + +static char** +terminal_screen_get_child_environment (TerminalScreen *screen, + char **initial_envv, + char **path, + char **shell) +{ + TerminalApp *app = terminal_app_get (); + char **env; + gs_strfreev char** current_environ = nullptr; + char *e, *v; + GHashTable *env_table; + GHashTableIter iter; + GPtrArray *retval; + guint i; + + env_table = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + + if (initial_envv) + env = initial_envv; + else { + env = current_environ = g_get_environ (); + /* Remove this variable which we set in server.c:main() */ + env = g_environ_unsetenv (env, "G_ENABLE_DIAGNOSTIC"); + } + + for (i = 0; env[i]; ++i) + { + v = strchr (env[i], '='); + if (v) + g_hash_table_replace (env_table, g_strndup (env[i], v - env[i]), g_strdup (v + 1)); + else + g_hash_table_replace (env_table, g_strdup (env[i]), nullptr); + } + + /* Remove unwanted env variables */ + auto const filters = terminal_client_get_environment_filters (); + for (i = 0; filters[i]; ++i) + g_hash_table_remove (env_table, filters[i]); + + auto const pfilters = terminal_client_get_environment_prefix_filters (); + for (i = 0; pfilters[i]; ++i) { + g_hash_table_foreach_remove (env_table, + GHRFunc(remove_prefixed_cb), + (void*)pfilters[i]); + } + + terminal_util_add_proxy_env (env_table); + + /* Add gnome-terminal private env vars used to communicate back to g-t-server */ + GDBusConnection *connection = g_application_get_dbus_connection (G_APPLICATION (app)); + g_hash_table_replace (env_table, g_strdup (TERMINAL_ENV_SERVICE_NAME), + g_strdup (g_dbus_connection_get_unique_name (connection))); + + g_hash_table_replace (env_table, g_strdup (TERMINAL_ENV_SCREEN), + terminal_app_dup_screen_object_path (app, screen)); + + /* Convert to strv */ + retval = g_ptr_array_sized_new (g_hash_table_size (env_table)); + g_hash_table_iter_init (&iter, env_table); + while (g_hash_table_iter_next (&iter, (gpointer *) &e, (gpointer *) &v)) + g_ptr_array_add (retval, g_strdup_printf ("%s=%s", e, v ? v : "")); + g_ptr_array_add (retval, nullptr); + + *path = g_strdup ((char const*)g_hash_table_lookup (env_table, "PATH")); + *shell = g_strdup ((char const*)g_hash_table_lookup (env_table, "SHELL")); + + g_hash_table_destroy (env_table); + return (char **) g_ptr_array_free (retval, FALSE); +} + +enum { + RESPONSE_RELAUNCH, + RESPONSE_EDIT_PREFERENCES +}; + +static void +info_bar_response_cb (GtkWidget *info_bar, + int response, + TerminalScreen *screen) +{ + gtk_widget_grab_focus (GTK_WIDGET (screen)); + + switch (response) { + case GTK_RESPONSE_CANCEL: + gtk_widget_destroy (info_bar); + g_signal_emit (screen, signals[CLOSE_SCREEN], 0); + break; + case RESPONSE_RELAUNCH: + gtk_widget_destroy (info_bar); + terminal_screen_reexec (screen, nullptr, nullptr, nullptr, nullptr); + break; + case RESPONSE_EDIT_PREFERENCES: + terminal_app_edit_preferences (terminal_app_get (), + terminal_screen_get_profile (screen), + "custom-command-entry", + gtk_get_current_event_time()); + break; + default: + gtk_widget_destroy (info_bar); + break; + } +} + +static void +terminal_screen_show_info_bar (TerminalScreen *screen, + GError *error, + gboolean show_relaunch) +{ + GtkWidget *info_bar; + + if (!gtk_widget_get_parent (GTK_WIDGET (screen))) + return; + + info_bar = terminal_info_bar_new (GTK_MESSAGE_ERROR, + _("_Preferences"), RESPONSE_EDIT_PREFERENCES, + !show_relaunch ? nullptr : _("_Relaunch"), RESPONSE_RELAUNCH, + nullptr); + terminal_info_bar_format_text (TERMINAL_INFO_BAR (info_bar), + _("There was an error creating the child process for this terminal")); + terminal_info_bar_format_text (TERMINAL_INFO_BAR (info_bar), + "%s", error->message); + g_signal_connect (info_bar, "response", + G_CALLBACK (info_bar_response_cb), screen); + + gtk_widget_set_halign (info_bar, GTK_ALIGN_FILL); + gtk_widget_set_valign (info_bar, GTK_ALIGN_START); + gtk_overlay_add_overlay (GTK_OVERLAY (terminal_screen_container_get_from_screen (screen)), + info_bar); + gtk_info_bar_set_default_response (GTK_INFO_BAR (info_bar), GTK_RESPONSE_CANCEL); + gtk_widget_show (info_bar); +} + +static void +spawn_result_cb (VteTerminal *terminal, + GPid pid, + GError *error, + gpointer user_data) +{ + TerminalScreen *screen = TERMINAL_SCREEN (terminal); + ExecData *exec_data = (ExecData*)user_data; + + /* Terminal was destroyed while the spawn operation was in progress; nothing to do. */ + if (terminal == nullptr) + goto out; + + { + TerminalScreenPrivate *priv = screen->priv; + + priv->child_pid = pid; + + if (error) { + // FIXMEchpe should be unnecessary, vte already does this internally + vte_terminal_set_pty (terminal, nullptr); + + gboolean can_reexec = TRUE; /* FIXME */ + terminal_screen_show_info_bar (screen, error, can_reexec); + } + + /* Retain info for reexec, if possible */ + ExecData *new_exec_data = exec_data_clone (exec_data, TRUE); + terminal_screen_clear_exec_data (screen, FALSE); + priv->exec_data = new_exec_data; + } + +out: + + /* Must do this even if the terminal was destroyed */ + exec_data_callback (exec_data, error, screen); + + exec_data_unref (exec_data); +} + +static gboolean +idle_exec_cb (TerminalScreen *screen) +{ + TerminalScreenPrivate *priv = screen->priv; + + priv->idle_exec_source = 0; + + ExecData *data = priv->exec_data; + _TERMINAL_DEBUG_IF (TERMINAL_DEBUG_PROCESSES) { + gs_free char *str = exec_data_to_string (data); + _terminal_debug_print (TERMINAL_DEBUG_PROCESSES, + "[screen %p] now launching the child process: %s\n", + screen, str); + } + + int n_fds; + int *fds; + if (data->fd_list) { + fds = g_unix_fd_list_steal_fds(data->fd_list, &n_fds); + } else { + fds = nullptr; + n_fds = 0; + } + + VteTerminal *terminal = VTE_TERMINAL (screen); + vte_terminal_spawn_with_fds_async (terminal, + data->pty_flags, + data->cwd, + (char const* const*)data->exec_argv, + (char const* const*)data->envv, + fds, n_fds, + data->fd_map, data->n_fd_map, + data->spawn_flags, + nullptr, nullptr, nullptr, /* child setup, data, destroy */ + -1, + data->cancellable, + spawn_result_cb, + exec_data_ref (data)); + + return FALSE; /* don't run again */ +} + +static void +terminal_screen_queue_idle_exec (TerminalScreen *screen) +{ + TerminalScreenPrivate *priv = screen->priv; + + if (priv->idle_exec_source != 0) + return; + + if (!gtk_widget_get_realized (GTK_WIDGET (screen))) { + priv->exec_on_realize = TRUE; + return; + } + + _terminal_debug_print (TERMINAL_DEBUG_PROCESSES, + "[screen %p] scheduling launching the child process on idle\n", + screen); + + priv->idle_exec_source = g_idle_add ((GSourceFunc) idle_exec_cb, screen); +} + +static TerminalScreenPopupInfo * +terminal_screen_popup_info_new (TerminalScreen *screen) +{ + TerminalScreenPopupInfo *info; + + info = g_slice_new0 (TerminalScreenPopupInfo); + info->ref_count = 1; + + return info; +} + +TerminalScreenPopupInfo * +terminal_screen_popup_info_ref (TerminalScreenPopupInfo *info) +{ + g_return_val_if_fail (info != nullptr, nullptr); + + info->ref_count++; + return info; +} + +void +terminal_screen_popup_info_unref (TerminalScreenPopupInfo *info) +{ + g_return_if_fail (info != nullptr); + + if (--info->ref_count > 0) + return; + + g_free (info->hyperlink); + g_free (info->url); + g_free (info->number_info); + g_free (info->timestamp_info); + g_slice_free (TerminalScreenPopupInfo, info); +} + +static gboolean +terminal_screen_popup_menu (GtkWidget *widget) +{ + TerminalScreen *screen = TERMINAL_SCREEN (widget); + TerminalScreenPopupInfo *info; + + info = terminal_screen_popup_info_new (screen); + info->button = 0; + info->timestamp = gtk_get_current_event_time (); + + g_signal_emit (screen, signals[SHOW_POPUP_MENU], 0, info); + terminal_screen_popup_info_unref (info); + + return TRUE; +} + +static void +terminal_screen_do_popup (TerminalScreen *screen, + GdkEventButton *event, + char *hyperlink, + char *url, + int url_flavor, + char *number_info, + char *timestamp_info) +{ + TerminalScreenPopupInfo *info; + + info = terminal_screen_popup_info_new (screen); + info->button = event->button; + info->state = event->state & gtk_accelerator_get_default_mod_mask (); + info->timestamp = event->time; + info->hyperlink = hyperlink; /* adopted */ + info->url = url; /* adopted */ + info->url_flavor = TerminalURLFlavor(url_flavor); + info->number_info = number_info; /* adopted */ + info->timestamp_info = timestamp_info; /* adopted */ + + g_signal_emit (screen, signals[SHOW_POPUP_MENU], 0, info); + terminal_screen_popup_info_unref (info); +} + +static gboolean +terminal_screen_button_press (GtkWidget *widget, + GdkEventButton *event) +{ + TerminalScreen *screen = TERMINAL_SCREEN (widget); + gboolean (* button_press_event) (GtkWidget*, GdkEventButton*) = + GTK_WIDGET_CLASS (terminal_screen_parent_class)->button_press_event; + gs_free char *hyperlink = nullptr; + gs_free char *url = nullptr; + int url_flavor = 0; + gs_free char *number_info = nullptr; + gs_free char *timestamp_info = nullptr; + guint state; + + state = event->state & gtk_accelerator_get_default_mod_mask (); + + hyperlink = terminal_screen_check_hyperlink (screen, (GdkEvent*)event); + url = terminal_screen_check_match (screen, (GdkEvent*)event, &url_flavor); + terminal_screen_check_extra (screen, (GdkEvent*)event, &number_info, ×tamp_info); + + if (hyperlink != nullptr && + (event->button == 1 || event->button == 2) && + (state & GDK_CONTROL_MASK)) + { + gboolean handled = FALSE; + + g_signal_emit (screen, signals[MATCH_CLICKED], 0, + hyperlink, + FLAVOR_AS_IS, + state, + &handled); + if (handled) + return TRUE; /* don't do anything else such as select with the click */ + } + + if (url != nullptr && + (event->button == 1 || event->button == 2) && + (state & GDK_CONTROL_MASK)) + { + gboolean handled = FALSE; + + g_signal_emit (screen, signals[MATCH_CLICKED], 0, + url, + url_flavor, + state, + &handled); + if (handled) + return TRUE; /* don't do anything else such as select with the click */ + } + + if (event->type == GDK_BUTTON_PRESS && event->button == 3) + { + if (!(event->state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK | GDK_MOD1_MASK))) + { + /* on right-click, we should first try to send the mouse event to + * the client, and popup only if that's not handled. */ + if (button_press_event && button_press_event (widget, event)) + return TRUE; + + terminal_screen_do_popup (screen, event, hyperlink, url, url_flavor, number_info, timestamp_info); + hyperlink = nullptr; /* adopted to the popup info */ + url = nullptr; /* ditto */ + number_info = nullptr; /* ditto */ + timestamp_info = nullptr; /* ditto */ + return TRUE; + } + else if (!(event->state & (GDK_CONTROL_MASK | GDK_MOD1_MASK))) + { + /* do popup on shift+right-click */ + terminal_screen_do_popup (screen, event, hyperlink, url, url_flavor, number_info, timestamp_info); + hyperlink = nullptr; /* adopted to the popup info */ + url = nullptr; /* ditto */ + number_info = nullptr; /* ditto */ + timestamp_info = nullptr; /* ditto */ + return TRUE; + } + } + + /* default behavior is to let the terminal widget deal with it */ + if (button_press_event) + return button_press_event (widget, event); + + return FALSE; +} + +/** + * terminal_screen_get_current_dir: + * @screen: + * + * Tries to determine the current working directory of the foreground process + * in @screen's PTY. + * + * Returns: a newly allocated string containing the current working directory, + * or %nullptr on failure + */ +char * +terminal_screen_get_current_dir (TerminalScreen *screen) +{ + const char *uri; + + uri = vte_terminal_get_current_directory_uri (VTE_TERMINAL (screen)); + if (uri != nullptr) + return g_filename_from_uri (uri, nullptr, nullptr); + + ExecData *data = screen->priv->exec_data; + if (data && data->cwd) + return g_strdup (data->cwd); + + return nullptr; +} + +static void +terminal_screen_window_title_changed (VteTerminal *vte_terminal, + TerminalScreen *screen) +{ + g_object_notify (G_OBJECT (screen), "title"); +} + +static void +terminal_screen_child_exited (VteTerminal *terminal, + int status) +{ + TerminalScreen *screen = TERMINAL_SCREEN (terminal); + TerminalScreenPrivate *priv = screen->priv; + TerminalExitAction action; + + /* Don't do anything if we don't have a child */ + if (priv->child_pid == -1) + return; + + /* No need to chain up to VteTerminalClass::child_exited since it's nullptr */ + + _terminal_debug_print (TERMINAL_DEBUG_PROCESSES, + "[screen %p] child process exited\n", + screen); + + priv->child_pid = -1; + + action = TerminalExitAction(g_settings_get_enum (priv->profile, TERMINAL_PROFILE_EXIT_ACTION_KEY)); + + switch (action) + { + case TERMINAL_EXIT_CLOSE: + g_signal_emit (screen, signals[CLOSE_SCREEN], 0); + break; + case TERMINAL_EXIT_RESTART: + terminal_screen_reexec (screen, nullptr, nullptr, nullptr, nullptr); + break; + case TERMINAL_EXIT_HOLD: { + GtkWidget *info_bar; + + info_bar = terminal_info_bar_new (GTK_MESSAGE_INFO, + _("_Relaunch"), RESPONSE_RELAUNCH, + nullptr); + if (WIFEXITED (status)) { + terminal_info_bar_format_text (TERMINAL_INFO_BAR (info_bar), + _("The child process exited normally with status %d."), WEXITSTATUS (status)); + } else if (WIFSIGNALED (status)) { + terminal_info_bar_format_text (TERMINAL_INFO_BAR (info_bar), + _("The child process was aborted by signal %d."), WTERMSIG (status)); + } else { + terminal_info_bar_format_text (TERMINAL_INFO_BAR (info_bar), + _("The child process was aborted.")); + } + g_signal_connect (info_bar, "response", + G_CALLBACK (info_bar_response_cb), screen); + + gtk_widget_set_halign (info_bar, GTK_ALIGN_FILL); + gtk_widget_set_valign (info_bar, GTK_ALIGN_START); + gtk_overlay_add_overlay (GTK_OVERLAY (terminal_screen_container_get_from_screen (screen)), + info_bar); + gtk_info_bar_set_default_response (GTK_INFO_BAR (info_bar), RESPONSE_RELAUNCH); + gtk_widget_show (info_bar); + break; + } + + default: + break; + } +} + +static void +terminal_screen_drag_data_received (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + GtkSelectionData *selection_data, + guint info, + guint timestamp) +{ + TerminalScreen *screen = TERMINAL_SCREEN (widget); + TerminalScreenPrivate *priv = screen->priv; + const guchar *selection_data_data; + GdkAtom selection_data_target; + gint selection_data_length, selection_data_format; + + selection_data_data = gtk_selection_data_get_data (selection_data); + selection_data_target = gtk_selection_data_get_target (selection_data); + selection_data_length = gtk_selection_data_get_length (selection_data); + selection_data_format = gtk_selection_data_get_format (selection_data); + +#if 0 + { + GList *tmp; + + g_print ("info: %d\n", info); + tmp = context->targets; + while (tmp != nullptr) + { + GdkAtom atom = GDK_POINTER_TO_ATOM (tmp->data); + + g_print ("Target: %s\n", gdk_atom_name (atom)); + + tmp = tmp->next; + } + + g_print ("Chosen target: %s\n", gdk_atom_name (selection_data->target)); + } +#endif + + if (gtk_targets_include_uri (&selection_data_target, 1)) + { + gs_strfreev char **uris; + gs_free char *text = nullptr; + gsize len; + + uris = gtk_selection_data_get_uris (selection_data); + if (!uris) + return; + + terminal_util_transform_uris_to_quoted_fuse_paths (uris); + + text = terminal_util_concat_uris (uris, &len); + terminal_screen_paste_text (screen, text, len); + } + else if (gtk_targets_include_text (&selection_data_target, 1)) + { + gs_free char *text; + + text = (char *) gtk_selection_data_get_text (selection_data); + if (text && text[0]) + terminal_screen_paste_text (screen, text, -1); + } + else switch (info) + { + case TARGET_COLOR: + { + guint16 *data = (guint16 *)selection_data_data; + GdkRGBA color; + + /* We accept drops with the wrong format, since the KDE color + * chooser incorrectly drops application/x-color with format 8. + * So just check for the data length. + */ + if (selection_data_length != 8) + return; + + color.red = (double) data[0] / 65535.; + color.green = (double) data[1] / 65535.; + color.blue = (double) data[2] / 65535.; + color.alpha = 1.; + /* FIXME: use opacity from data[3] */ + + terminal_g_settings_set_rgba (priv->profile, + TERMINAL_PROFILE_BACKGROUND_COLOR_KEY, + &color); + g_settings_set_boolean (priv->profile, TERMINAL_PROFILE_USE_THEME_COLORS_KEY, FALSE); + } + break; + + case TARGET_MOZ_URL: + { + char *utf8_data, *text; + char *uris[2]; + gsize len; + + /* MOZ_URL is in UCS-2 but in format 8. BROKEN! + * + * The data contains the URL, a \n, then the + * title of the web page. + * + * Note that some producers (e.g. dolphin) delimit with a \r\n + * (see issue#293), so we need to handle that, too. + */ + if (selection_data_format != 8 || + selection_data_length == 0 || + (selection_data_length % 2) != 0) + return; + + utf8_data = g_utf16_to_utf8 ((const gunichar2*) selection_data_data, + selection_data_length / 2, + nullptr, nullptr, nullptr); + if (!utf8_data) + return; + + uris[0] = g_strdelimit(utf8_data, "\r\n", 0); + uris[1] = nullptr; + terminal_util_transform_uris_to_quoted_fuse_paths (uris); /* This may replace uris[0] */ + + text = terminal_util_concat_uris (uris, &len); + terminal_screen_paste_text (screen, text, len); + g_free (text); + g_free (uris[0]); + } + break; + + case TARGET_NETSCAPE_URL: + { + char *utf8_data, *newline, *text; + char *uris[2]; + gsize len; + + /* The data contains the URL, a \n, then the + * title of the web page. + */ + if (selection_data_length < 0 || selection_data_format != 8) + return; + + utf8_data = g_strndup ((char *) selection_data_data, selection_data_length); + newline = strchr (utf8_data, '\n'); + if (newline) + *newline = '\0'; + + uris[0] = utf8_data; + uris[1] = nullptr; + terminal_util_transform_uris_to_quoted_fuse_paths (uris); /* This may replace uris[0] */ + + text = terminal_util_concat_uris (uris, &len); + terminal_screen_paste_text (screen, text, len); + g_free (text); + g_free (uris[0]); + } + break; + + case TARGET_RESET_BG: + g_settings_reset (priv->profile, TERMINAL_PROFILE_BACKGROUND_COLOR_KEY); + break; + + case TARGET_TAB: + { + GtkWidget *container; + TerminalScreen *moving_screen; + TerminalWindow *source_window; + TerminalWindow *dest_window; + + container = *(GtkWidget**) selection_data_data; + if (!GTK_IS_WIDGET (container)) + return; + + moving_screen = terminal_screen_container_get_screen (TERMINAL_SCREEN_CONTAINER (container)); + g_warn_if_fail (TERMINAL_IS_SCREEN (moving_screen)); + if (!TERMINAL_IS_SCREEN (moving_screen)) + return; + + source_window = terminal_screen_get_window (moving_screen); + dest_window = terminal_screen_get_window (screen); + terminal_window_move_screen (source_window, dest_window, moving_screen, -1); + + gtk_drag_finish (context, TRUE, TRUE, timestamp); + } + break; + + default: + g_assert_not_reached (); + } +} + +void +_terminal_screen_update_scrollbar (TerminalScreen *screen) +{ + TerminalScreenPrivate *priv = screen->priv; + TerminalScreenContainer *container; + GtkPolicyType vpolicy; + + container = terminal_screen_container_get_from_screen (screen); + if (container == nullptr) + return; + + vpolicy = GtkPolicyType(g_settings_get_enum (priv->profile, TERMINAL_PROFILE_SCROLLBAR_POLICY_KEY)); + + terminal_screen_container_set_policy (container, GTK_POLICY_NEVER, vpolicy); +} + +void +terminal_screen_get_size (TerminalScreen *screen, + int *width_chars, + int *height_chars) +{ + VteTerminal *terminal = VTE_TERMINAL (screen); + + *width_chars = vte_terminal_get_column_count (terminal); + *height_chars = vte_terminal_get_row_count (terminal); +} + +void +terminal_screen_get_cell_size (TerminalScreen *screen, + int *cell_width_pixels, + int *cell_height_pixels) +{ + VteTerminal *terminal = VTE_TERMINAL (screen); + + *cell_width_pixels = vte_terminal_get_char_width (terminal); + *cell_height_pixels = vte_terminal_get_char_height (terminal); +} + +static char* +terminal_screen_check_hyperlink (TerminalScreen *screen, + GdkEvent *event) +{ + return vte_terminal_hyperlink_check_event (VTE_TERMINAL (screen), event); +} + +static char* +terminal_screen_check_match (TerminalScreen *screen, + GdkEvent *event, + int *flavor) +{ + TerminalScreenPrivate *priv = screen->priv; + GSList *tags; + int tag; + char *match; + + match = vte_terminal_match_check_event (VTE_TERMINAL (screen), event, &tag); + for (tags = priv->match_tags; tags != nullptr; tags = tags->next) + { + TagData *tag_data = (TagData*) tags->data; + if (tag_data->tag == tag) + { + if (flavor) + *flavor = tag_data->flavor; + return match; + } + } + + g_free (match); + return nullptr; +} + +static void +terminal_screen_check_extra (TerminalScreen *screen, + GdkEvent *event, + char **number_info, + char **timestamp_info) +{ + guint i; + char **matches; + gboolean flavor_number_found = FALSE; + + matches = g_newa (char *, n_extra_regexes); + memset(matches, 0, sizeof(char*) * n_extra_regexes); + + if ( + vte_terminal_event_check_regex_simple (VTE_TERMINAL (screen), + event, + extra_regexes, + n_extra_regexes, + 0, + matches)) + { + for (i = 0; i < n_extra_regexes; i++) + { + if (matches[i] != nullptr) + { + /* Store the first match for each flavor, free all the others */ + switch (extra_regex_flavors[i]) + { + case FLAVOR_NUMBER: + if (!flavor_number_found) + { + *number_info = terminal_util_number_info (matches[i]); + *timestamp_info = terminal_util_timestamp_info (matches[i]); + flavor_number_found = TRUE; + } + g_free (matches[i]); + break; + default: + g_free (matches[i]); + } + } + } + } +} + +/** + * terminal_screen_has_foreground_process: + * @screen: + * @process_name: (out) (allow-none): the basename of the program, or %nullptr + * @cmdline: (out) (allow-none): the full command line, or %nullptr + * + * Checks whether there's a foreground process running in + * this terminal. + * + * Returns: %TRUE iff there's a foreground process running in @screen + */ +gboolean +terminal_screen_has_foreground_process (TerminalScreen *screen, + char **process_name, + char **cmdline) +{ + TerminalScreenPrivate *priv = screen->priv; + gs_free char *command = nullptr; + gs_free char *data_buf = nullptr; + gs_free char *basename = nullptr; + gs_free char *name = nullptr; + VtePty *pty; + int fd; +#if defined(__FreeBSD__) || defined(__DragonFly__) || defined(__OpenBSD__) + int mib[4]; +#else + char filename[64]; +#endif + char *data; + gsize i; + gsize len; + int fgpid; + + if (priv->child_pid == -1) + return FALSE; + + pty = vte_terminal_get_pty (VTE_TERMINAL (screen)); + if (pty == nullptr) + return FALSE; + + fd = vte_pty_get_fd (pty); + if (fd == -1) + return FALSE; + + fgpid = tcgetpgrp (fd); + if (fgpid == -1 || fgpid == priv->child_pid) + return FALSE; + +#if defined(__FreeBSD__) || defined(__DragonFly__) + mib[0] = CTL_KERN; + mib[1] = KERN_PROC; + mib[2] = KERN_PROC_ARGS; + mib[3] = fgpid; + if (sysctl (mib, G_N_ELEMENTS (mib), nullptr, &len, nullptr, 0) == -1) + return TRUE; + + data_buf = (char*)g_malloc0 (len); + if (sysctl (mib, G_N_ELEMENTS (mib), data_buf, &len, nullptr, 0) == -1) + return TRUE; + data = data_buf; +#elif defined(__OpenBSD__) + mib[0] = CTL_KERN; + mib[1] = KERN_PROC_ARGS; + mib[2] = fgpid; + mib[3] = KERN_PROC_ARGV; + if (sysctl (mib, G_N_ELEMENTS (mib), nullptr, &len, nullptr, 0) == -1) + return TRUE; + + data_buf = (char*)g_malloc0 (len); + if (sysctl (mib, G_N_ELEMENTS (mib), data_buf, &len, nullptr, 0) == -1) + return TRUE; + data = ((char**)data_buf)[0]; +#else + g_snprintf (filename, sizeof (filename), "/proc/%d/cmdline", fgpid); + if (!g_file_get_contents (filename, &data_buf, &len, nullptr)) + return TRUE; + data = data_buf; +#endif + + basename = g_path_get_basename (data); + if (!basename) + return TRUE; + + name = g_filename_to_utf8 (basename, -1, nullptr, nullptr, nullptr); + if (!name) + return TRUE; + + if (!process_name && !cmdline) + return TRUE; + + gs_transfer_out_value (process_name, &name); + + if (len > 0 && data[len - 1] == '\0') + len--; + for (i = 0; i < len; i++) + { + if (data[i] == '\0') + data[i] = ' '; + } + + command = g_filename_to_utf8 (data, -1, nullptr, nullptr, nullptr); + if (!command) + return TRUE; + + gs_transfer_out_value (cmdline, &command); + + return TRUE; +} + +const char * +terminal_screen_get_uuid (TerminalScreen *screen) +{ + g_return_val_if_fail (TERMINAL_IS_SCREEN (screen), nullptr); + + return screen->priv->uuid; +} + +/** + * terminal_screen_paste_text: + * @screen: + * @text: a NUL-terminated string + * @len: length of @text, or -1 + * + * Inserts @text to @terminal as if pasted. + */ +void +terminal_screen_paste_text (TerminalScreen* screen, + char const* text, + gssize len) +{ + g_return_if_fail (text != nullptr); + g_return_if_fail (len >= -1); + + /* This is just an API hack until vte 0.69 adds vte_terminal_paste_text_len() */ + /* Note that @text MUST be NUL-terminated */ + + vte_terminal_paste_text (VTE_TERMINAL (screen), text); +} diff --git a/src/terminal-screen.hh b/src/terminal-screen.hh new file mode 100644 index 0000000..ded6b8c --- /dev/null +++ b/src/terminal-screen.hh @@ -0,0 +1,171 @@ +/* + * Copyright © 2001 Havoc Pennington + * Copyright © 2008, 2010 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/>. + */ + +#ifndef TERMINAL_SCREEN_H +#define TERMINAL_SCREEN_H + +#include <glib-object.h> +#include <gio/gio.h> + +#include <vte/vte.h> + +G_BEGIN_DECLS + +typedef enum { + FLAVOR_AS_IS, + FLAVOR_DEFAULT_TO_HTTP, + FLAVOR_VOIP_CALL, + FLAVOR_EMAIL, + FLAVOR_NUMBER, +} TerminalURLFlavor; + +/* Forward decls */ +typedef struct _TerminalScreenPopupInfo TerminalScreenPopupInfo; +typedef struct _TerminalWindow TerminalWindow; + +#define TERMINAL_TYPE_SCREEN (terminal_screen_get_type ()) +#define TERMINAL_SCREEN(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), TERMINAL_TYPE_SCREEN, TerminalScreen)) +#define TERMINAL_SCREEN_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), TERMINAL_TYPE_SCREEN, TerminalScreenClass)) +#define TERMINAL_IS_SCREEN(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), TERMINAL_TYPE_SCREEN)) +#define TERMINAL_IS_SCREEN_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), TERMINAL_TYPE_SCREEN)) +#define TERMINAL_SCREEN_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), TERMINAL_TYPE_SCREEN, TerminalScreenClass)) + +typedef struct _TerminalScreen TerminalScreen; +typedef struct _TerminalScreenClass TerminalScreenClass; +typedef struct _TerminalScreenPrivate TerminalScreenPrivate; + +struct _TerminalScreen +{ + VteTerminal parent_instance; + + TerminalScreenPrivate *priv; +}; + +struct _TerminalScreenClass +{ + VteTerminalClass parent_class; + + void (* profile_set) (TerminalScreen *screen, + GSettings *old_profile); + void (* show_popup_menu) (TerminalScreen *screen, + TerminalScreenPopupInfo *info); + gboolean (* match_clicked) (TerminalScreen *screen, + const char *url, + int flavor, + guint state); + void (* close_screen) (TerminalScreen *screen); +}; + +GType terminal_screen_get_type (void) G_GNUC_CONST; + +const char *terminal_screen_get_uuid (TerminalScreen *screen); + +TerminalScreen *terminal_screen_new (GSettings *profile, + const char *title, + double zoom); + +typedef void (* TerminalScreenExecCallback) (TerminalScreen *screen, + GError *error, + gpointer user_data); + +gboolean terminal_screen_exec (TerminalScreen *screen, + char **argv, + char **envv, + gboolean as_shell, + const char *cwd, + GUnixFDList *fd_list, + GVariant *fd_array, + TerminalScreenExecCallback callback, + gpointer user_data, + GDestroyNotify destroy_notify, + GCancellable *cancellable, + GError **error); + + +gboolean terminal_screen_reexec (TerminalScreen *screen, + char **envv, + const char *cwd, + GCancellable *cancellable, + GError **error); + +gboolean terminal_screen_reexec_from_screen (TerminalScreen *screen, + TerminalScreen *parent_screen, + GCancellable *cancellable, + GError **error); + +void terminal_screen_set_profile (TerminalScreen *screen, + GSettings *profile); +GSettings* terminal_screen_get_profile (TerminalScreen *screen); +GSettings* terminal_screen_ref_profile (TerminalScreen *screen); + +const char* terminal_screen_get_title (TerminalScreen *screen); + +char *terminal_screen_get_current_dir (TerminalScreen *screen); + +void terminal_screen_get_size (TerminalScreen *screen, + int *width_chars, + int *height_chars); +void terminal_screen_get_cell_size (TerminalScreen *screen, + int *width_chars, + int *height_chars); + +void _terminal_screen_update_scrollbar (TerminalScreen *screen); + +void terminal_screen_save_config (TerminalScreen *screen, + GKeyFile *key_file, + const char *group); + +gboolean terminal_screen_has_foreground_process (TerminalScreen *screen, + char **process_name, + char **cmdline); + +/* Allow scales a bit smaller and a bit larger than the usual pango ranges */ +#define TERMINAL_SCALE_XXX_SMALL (PANGO_SCALE_XX_SMALL/1.2) +#define TERMINAL_SCALE_XXXX_SMALL (TERMINAL_SCALE_XXX_SMALL/1.2) +#define TERMINAL_SCALE_XXXXX_SMALL (TERMINAL_SCALE_XXXX_SMALL/1.2) +#define TERMINAL_SCALE_XXX_LARGE (PANGO_SCALE_XX_LARGE*1.2) +#define TERMINAL_SCALE_XXXX_LARGE (TERMINAL_SCALE_XXX_LARGE*1.2) +#define TERMINAL_SCALE_XXXXX_LARGE (TERMINAL_SCALE_XXXX_LARGE*1.2) +#define TERMINAL_SCALE_MINIMUM (TERMINAL_SCALE_XXXXX_SMALL/1.2) +#define TERMINAL_SCALE_MAXIMUM (TERMINAL_SCALE_XXXXX_LARGE*1.2) + +struct _TerminalScreenPopupInfo { + int ref_count; + char *url; + TerminalURLFlavor url_flavor; + char *hyperlink; + char *number_info; + char *timestamp_info; + guint button; + guint state; + guint32 timestamp; +}; + +TerminalScreenPopupInfo *terminal_screen_popup_info_ref (TerminalScreenPopupInfo *info); + +void terminal_screen_popup_info_unref (TerminalScreenPopupInfo *info); + +/* API hack */ + +void terminal_screen_paste_text (TerminalScreen* screen, + char const* text, + gssize len); + +G_END_DECLS + +#endif /* TERMINAL_SCREEN_H */ diff --git a/src/terminal-search-popover.cc b/src/terminal-search-popover.cc new file mode 100644 index 0000000..73d682f --- /dev/null +++ b/src/terminal-search-popover.cc @@ -0,0 +1,587 @@ +/* + * Copyright © 2015 Christian Persch + * Copyright © 2005 Paolo Maggi + * Copyright © 2010 Red Hat (Red Hat author: Behdad Esfahbod) + * + * 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 <glib.h> +#include <glib/gi18n.h> +#include <gtk/gtk.h> + +#include "terminal-pcre2.hh" +#include "terminal-search-popover.hh" +#include "terminal-intl.hh" +#include "terminal-window.hh" +#include "terminal-app.hh" +#include "terminal-libgsystem.hh" + +typedef struct _TerminalSearchPopoverPrivate TerminalSearchPopoverPrivate; + +struct _TerminalSearchPopover +{ + GtkWindow parent_instance; +}; + +struct _TerminalSearchPopoverClass +{ + GtkWindowClass parent_class; + + /* Signals */ + void (* search) (TerminalSearchPopover *popover, + gboolean backward); +}; + +struct _TerminalSearchPopoverPrivate +{ + GtkWidget *search_entry; + GtkWidget *search_prev_button; + GtkWidget *search_next_button; + GtkWidget *reveal_button; + GtkWidget *close_button; + GtkWidget *revealer; + GtkWidget *match_case_checkbutton; + GtkWidget *entire_word_checkbutton; + GtkWidget *regex_checkbutton; + GtkWidget *wrap_around_checkbutton; + + gboolean search_text_changed; + + /* Cached regex */ + gboolean regex_caseless; + char *regex_pattern; + VteRegex *regex; +}; + +enum { + PROP_0, + PROP_REGEX, + PROP_WRAP_AROUND, + LAST_PROP +}; + +enum { + SEARCH, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; +static GParamSpec *pspecs[LAST_PROP]; +static GtkListStore *history_store; + +G_DEFINE_TYPE_WITH_PRIVATE (TerminalSearchPopover, terminal_search_popover, GTK_TYPE_WINDOW) + +#define PRIV(obj) ((TerminalSearchPopoverPrivate *) terminal_search_popover_get_instance_private ((TerminalSearchPopover *)(obj))) + +/* history */ + +#define HISTORY_MIN_ITEM_LEN (3) +#define HISTORY_LENGTH (10) + +static gboolean +history_enabled (void) +{ + gboolean enabled; + + /* not quite an exact setting for this, but close enough… */ + g_object_get (gtk_settings_get_default (), "gtk-recent-files-enabled", &enabled, nullptr); + if (!enabled) + return FALSE; + + if (history_store == nullptr) { + history_store = gtk_list_store_new (1, G_TYPE_STRING); + g_object_set_data_full (G_OBJECT (terminal_app_get ()), "search-history-store", + history_store, (GDestroyNotify) g_object_unref); + } + + return TRUE; +} + +static gboolean +history_remove_item (const char *text) +{ + GtkTreeModel *model = GTK_TREE_MODEL (history_store); + GtkTreeIter iter; + + if (!gtk_tree_model_get_iter_first (model, &iter)) + return FALSE; + + do { + gs_free gchar *item_text; + + gtk_tree_model_get (model, &iter, 0, &item_text, -1); + + if (item_text != nullptr && strcmp (item_text, text) == 0) { + gtk_list_store_remove (history_store, &iter); + return TRUE; + } + } while (gtk_tree_model_iter_next (model, &iter)); + + return FALSE; +} + +static void +history_clamp (int max) +{ + GtkTreePath *path; + GtkTreeIter iter; + + /* -1 because TreePath counts from 0 */ + path = gtk_tree_path_new_from_indices (max - 1, -1); + + if (gtk_tree_model_get_iter (GTK_TREE_MODEL (history_store), &iter, path)) + while (1) + if (!gtk_list_store_remove (history_store, &iter)) + break; + + gtk_tree_path_free (path); +} + +static void +history_insert_item (const char *text) +{ + GtkTreeIter iter; + + if (!history_enabled () || text == nullptr) + return; + + if (g_utf8_strlen (text, -1) <= HISTORY_MIN_ITEM_LEN) + return; + + /* remove the text from the store if it was already + * present. If it wasn't, clamp to max history - 1 + * before inserting the new row, otherwise appending + * would not work */ + if (!history_remove_item (text)) + history_clamp (HISTORY_LENGTH - 1); + + gtk_list_store_insert_with_values (history_store, &iter, 0, + 0, text, + -1); +} + +/* helper functions */ + +static void +update_sensitivity (TerminalSearchPopover *popover) +{ + TerminalSearchPopoverPrivate *priv = PRIV (popover); + gboolean can_search; + + can_search = priv->regex != nullptr; + + gtk_widget_set_sensitive (priv->search_prev_button, can_search); + gtk_widget_set_sensitive (priv->search_next_button, can_search); +} + +static void +perform_search (TerminalSearchPopover *popover, + gboolean backward) +{ + TerminalSearchPopoverPrivate *priv = PRIV (popover); + + if (priv->regex == nullptr) + return; + + /* Add to search history */ + if (priv->search_text_changed) { + const char *search_text; + + search_text = gtk_entry_get_text (GTK_ENTRY (priv->search_entry)); + history_insert_item (search_text); + + priv->search_text_changed = FALSE; + } + + g_signal_emit (popover, signals[SEARCH], 0, backward); +} + +static void +previous_match_cb (GtkWidget *widget, + TerminalSearchPopover *popover) +{ + perform_search (popover, TRUE); +} + +static void +next_match_cb (GtkWidget *widget, + TerminalSearchPopover *popover) +{ + perform_search (popover, FALSE); +} + +static void +close_clicked_cb (GtkWidget *widget, + GtkWidget *popover) +{ + gtk_widget_hide (popover); +} + +static void +search_button_clicked_cb (GtkWidget *button, + TerminalSearchPopover *popover) +{ + TerminalSearchPopoverPrivate *priv = PRIV (popover); + + perform_search (popover, button == priv->search_prev_button); +} + +static gboolean +key_press_cb (GtkWidget *popover, + GdkEventKey *event, + gpointer user_data G_GNUC_UNUSED) +{ + if (event->keyval == GDK_KEY_Escape) { + gtk_widget_hide (popover); + return TRUE; + } + return FALSE; +} + +static void +update_regex (TerminalSearchPopover *popover) +{ + TerminalSearchPopoverPrivate *priv = PRIV (popover); + const char *search_text; + gboolean caseless; + gs_free char *pattern; + gs_free_error GError *error = nullptr; + + search_text = gtk_entry_get_text (GTK_ENTRY (priv->search_entry)); + + caseless = !gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (priv->match_case_checkbutton)); + + if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (priv->regex_checkbutton))) { + pattern = g_strdup (search_text); + } else { + pattern = g_regex_escape_string (search_text, -1); + } + + if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (priv->entire_word_checkbutton))) { + char *new_pattern; + new_pattern = g_strdup_printf ("\\b%s\\b", pattern); + g_free (pattern); + pattern = new_pattern; + } + + if (priv->regex_caseless == caseless && + g_strcmp0 (priv->regex_pattern, pattern) == 0) + return; + + if (priv->regex) { + vte_regex_unref (priv->regex); + } + + g_clear_pointer (&priv->regex_pattern, g_free); + + /* FIXME: if comping the regex fails, show the error message somewhere */ + if (search_text[0] != '\0') { + guint32 compile_flags; + + compile_flags = PCRE2_UTF | PCRE2_NO_UTF_CHECK | PCRE2_UCP | PCRE2_MULTILINE; + if (caseless) + compile_flags |= PCRE2_CASELESS; + + priv->regex = vte_regex_new_for_search (pattern, -1, compile_flags, &error); + if (priv->regex != nullptr && + (!vte_regex_jit (priv->regex, PCRE2_JIT_COMPLETE, nullptr) || + !vte_regex_jit (priv->regex, PCRE2_JIT_PARTIAL_SOFT, nullptr))) { + } + + if (priv->regex != nullptr) + gs_transfer_out_value (&priv->regex_pattern, &pattern); + } else { + priv->regex = nullptr; + } + + priv->regex_caseless = caseless; + + update_sensitivity (popover); + + g_object_notify_by_pspec (G_OBJECT (popover), pspecs[PROP_REGEX]); +} + +static void +search_text_changed_cb (GtkToggleButton *button, + TerminalSearchPopover *popover) +{ + TerminalSearchPopoverPrivate *priv = PRIV (popover); + + update_regex (popover); + priv->search_text_changed = TRUE; +} + +static void +search_parameters_changed_cb (GtkToggleButton *button, + TerminalSearchPopover *popover) +{ + update_regex (popover); +} + +static void +wrap_around_toggled_cb (GtkToggleButton *button, + TerminalSearchPopover *popover) +{ + g_object_notify_by_pspec (G_OBJECT (popover), pspecs[PROP_WRAP_AROUND]); +} + +/* public functions */ + +/* Class implementation */ + +static void +terminal_search_popover_grab_focus (GtkWidget *widget) +{ + TerminalSearchPopover *popover = TERMINAL_SEARCH_POPOVER (widget); + TerminalSearchPopoverPrivate *priv = PRIV (popover); + + gtk_widget_grab_focus (priv->search_entry); +} + +static void +terminal_search_popover_init (TerminalSearchPopover *popover) +{ + TerminalSearchPopoverPrivate *priv = PRIV (popover); + GtkWidget *widget = GTK_WIDGET (popover); + + priv->regex_pattern = 0; + priv->regex_caseless = TRUE; + + gtk_widget_init_template (widget); + + /* Make the search entry reasonably wide */ + gtk_widget_set_size_request (priv->search_entry, 300, -1); + + /* Add entry completion with history */ +#if 0 + g_object_set (G_OBJECT (priv->search_entry), + "model", history_store, + "entry-text-column", 0, + nullptr); +#endif + + if (history_enabled ()) { + gs_unref_object GtkEntryCompletion *completion; + + completion = gtk_entry_completion_new (); + gtk_entry_completion_set_model (completion, GTK_TREE_MODEL (history_store)); + gtk_entry_completion_set_text_column (completion, 0); + gtk_entry_completion_set_minimum_key_length (completion, HISTORY_MIN_ITEM_LEN); + gtk_entry_completion_set_popup_completion (completion, FALSE); + gtk_entry_completion_set_inline_completion (completion, TRUE); + gtk_entry_set_completion (GTK_ENTRY (priv->search_entry), completion); + } + +#if 0 + gtk_popover_set_default_widget (GTK_POPOVER (popover), priv->search_prev_button); +#else + GtkWindow *window = GTK_WINDOW (popover); + gtk_window_set_default (window, priv->search_prev_button); +#endif + + g_signal_connect (priv->search_entry, "previous-match", G_CALLBACK (previous_match_cb), popover); + g_signal_connect (priv->search_entry, "next-match", G_CALLBACK (next_match_cb), popover); + + g_signal_connect (priv->search_prev_button, "clicked", G_CALLBACK (search_button_clicked_cb), popover); + g_signal_connect (priv->search_next_button, "clicked", G_CALLBACK (search_button_clicked_cb), popover); + + g_signal_connect (priv->close_button, "clicked", G_CALLBACK (close_clicked_cb), popover); + + g_object_bind_property (priv->reveal_button, "active", + priv->revealer, "reveal-child", + G_BINDING_DEFAULT); + + update_sensitivity (popover); + + g_signal_connect (priv->search_entry, "search-changed", G_CALLBACK (search_text_changed_cb), popover); + g_signal_connect (priv->match_case_checkbutton, "toggled", G_CALLBACK (search_parameters_changed_cb), popover); + g_signal_connect (priv->entire_word_checkbutton, "toggled", G_CALLBACK (search_parameters_changed_cb), popover); + g_signal_connect (priv->regex_checkbutton, "toggled", G_CALLBACK (search_parameters_changed_cb), popover); + + g_signal_connect (priv->wrap_around_checkbutton, "toggled", G_CALLBACK (wrap_around_toggled_cb), popover); + + g_signal_connect (popover, "key-press-event", G_CALLBACK (key_press_cb), nullptr); + + if (terminal_app_get_dialog_use_headerbar (terminal_app_get ())) { + GtkWidget *headerbar; + + headerbar = (GtkWidget*)g_object_new (GTK_TYPE_HEADER_BAR, + "title", gtk_window_get_title (window), + "has-subtitle", FALSE, + "show-close-button", TRUE, + "visible", TRUE, + nullptr); + gtk_style_context_add_class (gtk_widget_get_style_context (headerbar), + "default-decoration"); + gtk_window_set_titlebar (window, headerbar); + } +} + +static void +terminal_search_popover_finalize (GObject *object) +{ + TerminalSearchPopover *popover = TERMINAL_SEARCH_POPOVER (object); + TerminalSearchPopoverPrivate *priv = PRIV (popover); + + if (priv->regex) { + vte_regex_unref (priv->regex); + } + + g_free (priv->regex_pattern); + + G_OBJECT_CLASS (terminal_search_popover_parent_class)->finalize (object); +} + +static void +terminal_search_popover_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + TerminalSearchPopover *popover = TERMINAL_SEARCH_POPOVER (object); + + switch (prop_id) { + case PROP_REGEX: + g_value_set_boxed (value, terminal_search_popover_get_regex (popover)); + break; + case PROP_WRAP_AROUND: + g_value_set_boolean (value, terminal_search_popover_get_wrap_around (popover)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +terminal_search_popover_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (prop_id) { + case PROP_REGEX: + case PROP_WRAP_AROUND: + /* not writable */ + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +terminal_search_popover_class_init (TerminalSearchPopoverClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + gobject_class->finalize = terminal_search_popover_finalize; + gobject_class->get_property = terminal_search_popover_get_property; + gobject_class->set_property = terminal_search_popover_set_property; + + widget_class->grab_focus = terminal_search_popover_grab_focus; + + signals[SEARCH] = + g_signal_new (I_("search"), + G_OBJECT_CLASS_TYPE (gobject_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (TerminalSearchPopoverClass, search), + nullptr, nullptr, + g_cclosure_marshal_VOID__BOOLEAN, + G_TYPE_NONE, + 1, + G_TYPE_BOOLEAN); + + pspecs[PROP_REGEX] = + g_param_spec_boxed ("regex", nullptr, nullptr, + VTE_TYPE_REGEX, + GParamFlags(G_PARAM_READABLE | + G_PARAM_STATIC_NAME | + G_PARAM_STATIC_NICK | + G_PARAM_STATIC_BLURB)); + + pspecs[PROP_WRAP_AROUND] = + g_param_spec_boolean ("wrap-around", nullptr, nullptr, + FALSE, + GParamFlags(G_PARAM_READABLE | + G_PARAM_STATIC_NAME | + G_PARAM_STATIC_NICK | + G_PARAM_STATIC_BLURB)); + + g_object_class_install_properties (gobject_class, G_N_ELEMENTS (pspecs), pspecs); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/terminal/ui/search-popover.ui"); + gtk_widget_class_bind_template_child_private (widget_class, TerminalSearchPopover, search_entry); + gtk_widget_class_bind_template_child_private (widget_class, TerminalSearchPopover, search_prev_button); + gtk_widget_class_bind_template_child_private (widget_class, TerminalSearchPopover, search_next_button); + gtk_widget_class_bind_template_child_private (widget_class, TerminalSearchPopover, reveal_button); + gtk_widget_class_bind_template_child_private (widget_class, TerminalSearchPopover, close_button); + gtk_widget_class_bind_template_child_private (widget_class, TerminalSearchPopover, revealer); + gtk_widget_class_bind_template_child_private (widget_class, TerminalSearchPopover, match_case_checkbutton); + gtk_widget_class_bind_template_child_private (widget_class, TerminalSearchPopover, entire_word_checkbutton); + gtk_widget_class_bind_template_child_private (widget_class, TerminalSearchPopover, regex_checkbutton); + gtk_widget_class_bind_template_child_private (widget_class, TerminalSearchPopover, wrap_around_checkbutton); +} + +/* public API */ + +/** + * terminal_search_popover_new: + * + * Returns: a new #TerminalSearchPopover + */ +TerminalSearchPopover * +terminal_search_popover_new (GtkWidget *relative_to_widget) +{ + return reinterpret_cast<TerminalSearchPopover*> + (g_object_new (TERMINAL_TYPE_SEARCH_POPOVER, +#if 0 + "relative-to", relative_to_widget, +#else + "transient-for", gtk_widget_get_toplevel (relative_to_widget), +#endif + nullptr)); +} + +/** + * terminal_search_popover_get_regex: + * @popover: a #TerminalSearchPopover + * + * Returns: (transfer none): the search regex, or %nullptr + */ +VteRegex * +terminal_search_popover_get_regex (TerminalSearchPopover *popover) +{ + g_return_val_if_fail (TERMINAL_IS_SEARCH_POPOVER (popover), nullptr); + + return PRIV (popover)->regex; +} + +/** + * terminal_search_popover_get_wrap_around: + * @popover: a #TerminalSearchPopover + * + * Returns: (transfer none): whether search should wrap around + */ +gboolean +terminal_search_popover_get_wrap_around (TerminalSearchPopover *popover) +{ + g_return_val_if_fail (TERMINAL_IS_SEARCH_POPOVER (popover), FALSE); + + return gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (PRIV (popover)->wrap_around_checkbutton)); +} diff --git a/src/terminal-search-popover.hh b/src/terminal-search-popover.hh new file mode 100644 index 0000000..b7af128 --- /dev/null +++ b/src/terminal-search-popover.hh @@ -0,0 +1,49 @@ +/* + * Copyright © 2008 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/>. + */ + +#ifndef TERMINAL_SEARCH_POPOVER_H +#define TERMINAL_SEARCH_POPOVER_H + +#include <gtk/gtk.h> + +#include "terminal-screen.hh" + +G_BEGIN_DECLS + +#define TERMINAL_TYPE_SEARCH_POPOVER (terminal_search_popover_get_type ()) +#define TERMINAL_SEARCH_POPOVER(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), TERMINAL_TYPE_SEARCH_POPOVER, TerminalSearchPopover)) +#define TERMINAL_SEARCH_POPOVER_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), TERMINAL_TYPE_SEARCH_POPOVER, TerminalSearchPopoverClass)) +#define TERMINAL_IS_SEARCH_POPOVER(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), TERMINAL_TYPE_SEARCH_POPOVER)) +#define TERMINAL_IS_SEARCH_POPOVER_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), TERMINAL_TYPE_SEARCH_POPOVER)) +#define TERMINAL_SEARCH_POPOVER_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), TERMINAL_TYPE_SEARCH_POPOVER, TerminalSearchPopoverClass)) + +typedef struct _TerminalSearchPopover TerminalSearchPopover; +typedef struct _TerminalSearchPopoverClass TerminalSearchPopoverClass; + +GType terminal_search_popover_get_type (void); + +TerminalSearchPopover *terminal_search_popover_new (GtkWidget *relative_to_widget); + +VteRegex * + terminal_search_popover_get_regex (TerminalSearchPopover *popover); + +gboolean terminal_search_popover_get_wrap_around (TerminalSearchPopover *popover); + +G_END_DECLS + +#endif /* !TERMINAL_SEARCH_POPOVER_H */ diff --git a/src/terminal-search-provider.cc b/src/terminal-search-provider.cc new file mode 100644 index 0000000..deb5dc1 --- /dev/null +++ b/src/terminal-search-provider.cc @@ -0,0 +1,380 @@ +/* + * Copyright © 2013, 2014 Red Hat, Inc. + * + * 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 <string.h> + +#include "terminal-app.hh" +#include "terminal-debug.hh" +#include "terminal-libgsystem.hh" +#include "terminal-screen-container.hh" +#include "terminal-search-provider.hh" +#include "terminal-search-provider-gdbus-generated.h" +#include "terminal-window.hh" + +struct _TerminalSearchProvider +{ + GObject parent; + + TerminalSearchProvider2 *skeleton; +}; + +struct _TerminalSearchProviderClass +{ + GObjectClass parent_class; +}; + +G_DEFINE_TYPE (TerminalSearchProvider, terminal_search_provider, G_TYPE_OBJECT) + +static char * +normalize_casefold_and_unaccent (const char *str) +{ + gs_free char *casefolded = nullptr, *normalized = nullptr; + char *retval = nullptr; + + if (str == nullptr) + goto out; + + normalized = g_utf8_normalize (str, -1, G_NORMALIZE_ALL_COMPOSE); + casefolded = g_utf8_casefold (normalized, -1); + retval = g_str_to_ascii (casefolded, nullptr); + + out: + return retval; +} + +static char ** +normalize_casefold_and_unaccent_terms (const char* const *terms) +{ + char **casefolded_terms; + guint i, n; + + n = g_strv_length ((char **) terms); + casefolded_terms = g_new (char *, n + 1); + + for (i = 0; i < n; i++) + casefolded_terms[i] = normalize_casefold_and_unaccent (terms[i]); + casefolded_terms[n] = nullptr; + + return casefolded_terms; +} + +static gboolean +match_terms (const char *str, + const char* const *terms) +{ + gs_free char *casefolded_str = nullptr; + gboolean matches = TRUE; + guint i; + + if (str == nullptr) + { + matches = FALSE; + goto out; + } + + casefolded_str = normalize_casefold_and_unaccent (str); + for (i = 0; terms[i] != nullptr; i++) + { + if (strstr (casefolded_str, terms[i]) == nullptr) + { + matches = FALSE; + break; + } + } + + out: + return matches; +} + +static gboolean +handle_get_initial_result_set_cb (TerminalSearchProvider2 *skeleton, + GDBusMethodInvocation *invocation, + const char *const *terms, + gpointer user_data) +{ + GList *l, *screens = nullptr, *windows; + gs_unref_ptrarray GPtrArray *results; + TerminalApp *app; + gs_strfreev char **casefolded_terms = nullptr; + + _terminal_debug_print (TERMINAL_DEBUG_SEARCH, "GetInitialResultSet started\n"); + + app = terminal_app_get (); + windows = gtk_application_get_windows (GTK_APPLICATION (app)); + for (l = windows; l != nullptr; l = l->next) + { + TerminalWindow *window = (TerminalWindow*)(l->data); + GList *c, *containers; + + if (!TERMINAL_IS_WINDOW (l->data)) + continue; + + containers = terminal_window_list_screen_containers (window); + for (c = containers; c != nullptr; c = c->next) + { + TerminalScreenContainer *container = TERMINAL_SCREEN_CONTAINER (c->data); + TerminalScreen *screen; + + screen = terminal_screen_container_get_screen (container); + screens = g_list_prepend (screens, screen); + } + } + + casefolded_terms = normalize_casefold_and_unaccent_terms (terms); + results = g_ptr_array_new_with_free_func (g_free); + + for (l = screens; l != nullptr; l = l->next) + { + TerminalScreen *screen = TERMINAL_SCREEN (l->data); + gs_free char *cmdline = nullptr, *process = nullptr; + const char *cwd, *title; + + cwd = vte_terminal_get_current_directory_uri (VTE_TERMINAL (screen)); + title = terminal_screen_get_title (screen); + terminal_screen_has_foreground_process (screen, &process, &cmdline); + if (match_terms (cwd, (const char *const *) casefolded_terms) || + match_terms (title, (const char *const *) casefolded_terms) || + match_terms (process, (const char *const *) casefolded_terms) || + match_terms (cmdline, (const char *const *) casefolded_terms)) + { + const char *uuid; + + uuid = terminal_screen_get_uuid (screen); + g_ptr_array_add (results, g_strdup (uuid)); + + _terminal_debug_print (TERMINAL_DEBUG_SEARCH, "Search hit: %s\n", uuid); + } + } + + g_ptr_array_add (results, nullptr); + terminal_search_provider2_complete_get_initial_result_set (skeleton, + invocation, + (const char *const *) results->pdata); + + _terminal_debug_print (TERMINAL_DEBUG_SEARCH, "GetInitialResultSet completed\n"); + return TRUE; +} + +static gboolean +handle_get_subsearch_result_set_cb (TerminalSearchProvider2 *skeleton, + GDBusMethodInvocation *invocation, + const char *const *previous_results, + const char *const *terms, + gpointer user_data) +{ + gs_unref_ptrarray GPtrArray *results; + TerminalApp *app; + gs_strfreev char **casefolded_terms = nullptr; + guint i; + + _terminal_debug_print (TERMINAL_DEBUG_SEARCH, "GetSubsearchResultSet started\n"); + + app = terminal_app_get (); + casefolded_terms = normalize_casefold_and_unaccent_terms (terms); + results = g_ptr_array_new_with_free_func (g_free); + + for (i = 0; previous_results[i] != nullptr; i++) + { + TerminalScreen *screen; + gs_free char *cmdline = nullptr, *process = nullptr; + const char *cwd, *title; + + screen = terminal_app_get_screen_by_uuid (app, previous_results[i]); + if (screen == nullptr) + { + _terminal_debug_print (TERMINAL_DEBUG_SEARCH, "Not a screen: %s\n", previous_results[i]); + continue; + } + + cwd = vte_terminal_get_current_directory_uri (VTE_TERMINAL (screen)); + title = terminal_screen_get_title (screen); + terminal_screen_has_foreground_process (screen, &process, &cmdline); + if (match_terms (cwd, (const char *const *) casefolded_terms) || + match_terms (title, (const char *const *) casefolded_terms) || + match_terms (process, (const char *const *) casefolded_terms) || + match_terms (cmdline, (const char *const *) casefolded_terms)) + { + g_ptr_array_add (results, g_strdup (previous_results[i])); + _terminal_debug_print (TERMINAL_DEBUG_SEARCH, "Search hit: %s\n", previous_results[i]); + } + } + + g_ptr_array_add (results, nullptr); + terminal_search_provider2_complete_get_subsearch_result_set (skeleton, + invocation, + (const char *const *) results->pdata); + + _terminal_debug_print (TERMINAL_DEBUG_SEARCH, "GetSubsearchResultSet completed\n"); + return TRUE; +} + +static gboolean +handle_get_result_metas_cb (TerminalSearchProvider2 *skeleton, + GDBusMethodInvocation *invocation, + const char *const *results, + gpointer user_data) +{ + GVariantBuilder builder; + TerminalApp *app; + guint i; + + _terminal_debug_print (TERMINAL_DEBUG_SEARCH, "GetResultMetas started\n"); + + app = terminal_app_get (); + g_variant_builder_init (&builder, G_VARIANT_TYPE ("aa{sv}")); + + for (i = 0; results[i] != nullptr; i++) + { + TerminalScreen *screen; + const char *title; + gs_free char *escaped_text = nullptr; + gs_free char *text = nullptr; + + screen = terminal_app_get_screen_by_uuid (app, results[i]); + if (screen == nullptr) + { + _terminal_debug_print (TERMINAL_DEBUG_SEARCH, "Not a screen: %s\n", results[i]); + continue; + } + + title = terminal_screen_get_title (screen); + if (terminal_screen_has_foreground_process (screen, nullptr, nullptr)) { + VteTerminal *terminal = VTE_TERMINAL (screen); + long cursor_row; + + vte_terminal_get_cursor_position (terminal, nullptr, &cursor_row); + text = vte_terminal_get_text_range (terminal, + MAX(0, cursor_row - 1), + 0, + cursor_row + 1, + vte_terminal_get_column_count (terminal) - 1, + nullptr, nullptr, nullptr); + } + + g_variant_builder_open (&builder, G_VARIANT_TYPE ("a{sv}")); + g_variant_builder_add (&builder, "{sv}", "id", g_variant_new_string (results[i])); + g_variant_builder_add (&builder, "{sv}", "name", g_variant_new_string (title)); + if (text != nullptr) + { + escaped_text = g_markup_escape_text (text, -1); + g_variant_builder_add (&builder, "{sv}", "description", g_variant_new_string (escaped_text)); + } + g_variant_builder_close (&builder); + + _terminal_debug_print (TERMINAL_DEBUG_SEARCH, "Meta for %s: %s\n", results[i], title); + } + + terminal_search_provider2_complete_get_result_metas (skeleton, invocation, g_variant_builder_end (&builder)); + + _terminal_debug_print (TERMINAL_DEBUG_SEARCH, "GetResultMetas completed\n"); + + return TRUE; +} + +static gboolean +handle_activate_result_cb (TerminalSearchProvider2 *skeleton, + GDBusMethodInvocation *invocation, + const char *identifier, + const char* const *terms, + guint timestamp, + gpointer user_data) +{ + GtkWidget *toplevel; + TerminalApp *app; + TerminalScreen *screen; + + app = terminal_app_get (); + screen = terminal_app_get_screen_by_uuid (app, identifier); + if (screen == nullptr) + goto out; + + toplevel = gtk_widget_get_toplevel (GTK_WIDGET (screen)); + if (!gtk_widget_is_toplevel (toplevel)) + goto out; + + terminal_window_switch_screen (TERMINAL_WINDOW (toplevel), screen); + gtk_window_present_with_time (GTK_WINDOW (toplevel), timestamp); + _terminal_debug_print (TERMINAL_DEBUG_SEARCH, "ActivateResult: %s\n", identifier); + + out: + terminal_search_provider2_complete_activate_result (skeleton, invocation); + return TRUE; +} + +static void +terminal_search_provider_init (TerminalSearchProvider *provider) +{ + provider->skeleton = terminal_search_provider2_skeleton_new (); + + g_signal_connect (provider->skeleton, "handle-get-initial-result-set", + G_CALLBACK (handle_get_initial_result_set_cb), provider); + g_signal_connect (provider->skeleton, "handle-get-subsearch-result-set", + G_CALLBACK (handle_get_subsearch_result_set_cb), provider); + g_signal_connect (provider->skeleton, "handle-get-result-metas", + G_CALLBACK (handle_get_result_metas_cb), provider); + g_signal_connect (provider->skeleton, "handle-activate-result", + G_CALLBACK (handle_activate_result_cb), provider); +} + +static void +terminal_search_provider_dispose (GObject *object) +{ + TerminalSearchProvider *provider = TERMINAL_SEARCH_PROVIDER (object); + + g_clear_object (&provider->skeleton); + + G_OBJECT_CLASS (terminal_search_provider_parent_class)->dispose (object); +} + +static void +terminal_search_provider_class_init (TerminalSearchProviderClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->dispose = terminal_search_provider_dispose; +} + +TerminalSearchProvider * +terminal_search_provider_new (void) +{ + return reinterpret_cast<TerminalSearchProvider*> + (g_object_new (TERMINAL_TYPE_SEARCH_PROVIDER, nullptr)); +} + +gboolean +terminal_search_provider_dbus_register (TerminalSearchProvider *provider, + GDBusConnection *connection, + const char *object_path, + GError **error) +{ + return g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (provider->skeleton), + connection, + object_path, + error); +} + +void +terminal_search_provider_dbus_unregister (TerminalSearchProvider *provider, + GDBusConnection *connection, + const char *object_path) +{ + if (g_dbus_interface_skeleton_has_connection (G_DBUS_INTERFACE_SKELETON (provider->skeleton), connection)) + g_dbus_interface_skeleton_unexport_from_connection (G_DBUS_INTERFACE_SKELETON (provider->skeleton), + connection); +} diff --git a/src/terminal-search-provider.hh b/src/terminal-search-provider.hh new file mode 100644 index 0000000..719bd3e --- /dev/null +++ b/src/terminal-search-provider.hh @@ -0,0 +1,51 @@ +/* + * Copyright © 2013 Red Hat, Inc. + * + * 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/>. + */ + +#ifndef TERMINAL_SEARCH_PROVIDER_H +#define TERMINAL_SEARCH_PROVIDER_H + +#include <glib-object.h> +#include <gio/gio.h> + +G_BEGIN_DECLS + +#define TERMINAL_TYPE_SEARCH_PROVIDER (terminal_search_provider_get_type ()) +#define TERMINAL_SEARCH_PROVIDER(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), TERMINAL_TYPE_SEARCH_PROVIDER, TerminalSearchProvider)) +#define TERMINAL_SEARCH_PROVIDER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), TERMINAL_TYPE_SEARCH_PROVIDER, TerminalSearchProviderClass)) +#define TERMINAL_IS_SEARCH_PROVIDER(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), TERMINAL_TYPE_SEARCH_PROVIDER)) +#define TERMINAL_IS_SEARCH_PROVIDER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), TERMINAL_TYPE_SEARCH_PROVIDER)) +#define TERMINAL_SEARCH_PROVIDER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), TERMINAL_TYPE_SEARCH_PROVIDER, TerminalSearchProviderClass)) + +typedef struct _TerminalSearchProvider TerminalSearchProvider; +typedef struct _TerminalSearchProviderClass TerminalSearchProviderClass; + +GType terminal_search_provider_get_type (void); + +TerminalSearchProvider *terminal_search_provider_new (void); + +gboolean terminal_search_provider_dbus_register (TerminalSearchProvider *provider, + GDBusConnection *connection, + const char *object_path, + GError **error); + +void terminal_search_provider_dbus_unregister (TerminalSearchProvider *provider, + GDBusConnection *connection, + const char *object_path); + +G_END_DECLS + +#endif /* !TERMINAL_SEARCH_PROVIDER_H */ diff --git a/src/terminal-settings-bridge-backend.cc b/src/terminal-settings-bridge-backend.cc new file mode 100644 index 0000000..4e950de --- /dev/null +++ b/src/terminal-settings-bridge-backend.cc @@ -0,0 +1,563 @@ +/* + * Copyright © 2008, 2010, 2011, 2022 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" +#define G_SETTINGS_ENABLE_BACKEND + +#include <cassert> + +#include "terminal-debug.hh" +#include "terminal-libgsystem.hh" +#include "terminal-settings-bridge-backend.hh" +#include "terminal-settings-utils.hh" +#include "terminal-settings-bridge-generated.h" + +#include <gio/gio.h> +#include <gio/gsettingsbackend.h> + +struct _TerminalSettingsBridgeBackend { + GSettingsBackend parent_instance; + + TerminalSettingsBridge* bridge; + GCancellable* cancellable; + + GHashTable* cache; +}; + +struct _TerminalSettingsBridgeBackendClass { + GSettingsBackendClass parent_class; +}; + +enum { + PROP_SETTINGS_BRIDGE = 1, +}; + +#define PRIORITY (10000) + +// _g_io_modules_ensure_extension_points_registered() is not public, +// so just ensure a type that does this call on registration, which +// all of the glib-internal settings backends do. However as an added +// complication, none of their get_type() functions are public. So +// instead we need to create an object using the public function and +// immediately delete it again. +// However, this *still* does not work; the +// g_null_settings_backend_new() call prints the warning about a non- +// registered extension point even though its get_type() function calls +// _g_io_modules_ensure_extension_points_registered() immediately before. +// Therefore we can only use this backend when creating a GSettings +// ourself, by explicitly passing it at that time. + +G_DEFINE_TYPE_WITH_CODE(TerminalSettingsBridgeBackend, + terminal_settings_bridge_backend, + G_TYPE_SETTINGS_BACKEND, + // _g_io_modules_ensure_extension_points_registered(); + // { gs_unref_object auto dummy = g_null_settings_backend_new(); } + // + // g_io_extension_point_implement(G_SETTINGS_BACKEND_EXTENSION_POINT_NAME, + // g_define_type_id, "bridge", PRIORITY) +); + +// Note that since D-Bus doesn't support maybe values, we use arrays +// with either zero or one item to send/receive a maybe. +// If we get more than one item, just use the first one. + +/* helper functions */ + +template<typename T> +inline constexpr auto +IMPL(T* that) noexcept +{ + return reinterpret_cast<TerminalSettingsBridgeBackend*>(that); +} + +typedef struct { + GVariant* value; + bool value_set; + bool writable; + bool writable_set; +} CacheEntry; + +static auto +cache_entry_new(void) +{ + return g_new0(CacheEntry, 1); +} + +static void +cache_entry_free(CacheEntry* e) noexcept +{ + if (e->value) + g_variant_unref(e->value); + g_free(e); +} + +static auto +cache_lookup_entry(TerminalSettingsBridgeBackend* impl, + char const* key) noexcept +{ + return reinterpret_cast<CacheEntry*>(g_hash_table_lookup(impl->cache, key)); +} + +static auto +cache_ensure_entry(TerminalSettingsBridgeBackend* impl, + char const* key) noexcept +{ + g_hash_table_insert(impl->cache, g_strdup(key), cache_entry_new()); + return cache_lookup_entry(impl, key); +} + +static void +cache_insert_value(TerminalSettingsBridgeBackend* impl, + char const* key, + GVariant* value) noexcept +{ + auto const ce = cache_ensure_entry(impl, key); + g_clear_pointer(&ce->value, g_variant_unref); + ce->value = value ? g_variant_ref(value) : nullptr; + ce->value_set = true; +} + +static void +cache_insert_writable(TerminalSettingsBridgeBackend* impl, + char const* key, + bool writable) noexcept +{ + auto const ce = cache_ensure_entry(impl, key); + ce->writable = writable; + ce->writable_set = true; +} + +static void +cache_remove_path(TerminalSettingsBridgeBackend* impl, + char const* path) noexcept +{ + auto iter = GHashTableIter{}; + g_hash_table_iter_init(&iter, impl->cache); + void* keyp = nullptr; + void* valuep = nullptr; + while (g_hash_table_iter_next(&iter, &keyp, &valuep)) { + auto const key = reinterpret_cast<char const*>(keyp); + if (g_str_has_prefix(key, path)) { + auto ce = reinterpret_cast<CacheEntry*>(valuep); + g_clear_pointer(&ce->value, g_variant_unref); + ce->value_set = false; + } + } +} + +static void +cache_remove_value(TerminalSettingsBridgeBackend* impl, + char const* key) noexcept +{ + auto const ce = cache_ensure_entry(impl, key); + g_clear_pointer(&ce->value, g_variant_unref); + ce->value_set = false; +} + +static void +cache_remove_writable(TerminalSettingsBridgeBackend* impl, + char const* key) noexcept +{ + auto const ce = cache_ensure_entry(impl, key); + ce->writable_set = false; +} + +/* GSettingsBackend class implementation */ + +static GPermission* +terminal_settings_bridge_backend_get_permission(GSettingsBackend* backend, + char const* path) noexcept +{ + _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, + "Bridge backend ::get_permission\n"); + + return g_simple_permission_new(true); +} + +static gboolean +terminal_settings_bridge_backend_get_writable(GSettingsBackend* backend, + char const* key) noexcept +{ + auto const impl = IMPL(backend); + + auto const ce = cache_lookup_entry(impl, key); + if (ce && ce->writable_set) + return ce->writable; + + auto writable = gboolean{false}; + auto const r = + terminal_settings_bridge_call_get_writable_sync(impl->bridge, + key, + &writable, + impl->cancellable, + nullptr); + + if (r) + cache_insert_writable(impl, key, writable); + else + cache_remove_writable(impl, key); + + _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, + "Bridge backend ::get_writable key %s success %d writable %d\n", + key, r, writable); + + return writable; +} + +static GVariant* +terminal_settings_bridge_backend_read(GSettingsBackend* backend, + char const* key, + GVariantType const* type, + gboolean default_value) noexcept +{ + if (default_value) + return nullptr; + + auto const impl = IMPL(backend); + auto const ce = cache_lookup_entry(impl, key); + if (ce && ce->value_set) + return ce->value ? g_variant_ref(ce->value) : nullptr; + + gs_unref_variant GVariant* rv = nullptr; + auto r = + terminal_settings_bridge_call_read_sync(impl->bridge, + key, + g_variant_type_peek_string(type), + default_value, + &rv, + impl->cancellable, + nullptr); + + auto const value = r ? terminal_g_variant_unwrap(rv) : nullptr; + + if (r && value && !g_variant_is_of_type(value, type)) { + _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, + "Bridge backend ::read key %s got type %s expected type %s\n", + key, + g_variant_get_type_string(value), + g_variant_type_peek_string(type)); + + g_clear_pointer(&value, g_variant_unref); + r = false; + } + + if (r) + cache_insert_value(impl, key, value); + else + cache_remove_value(impl, key); + + _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, + "Bridge backend ::read key %s success %d value %s\n", + key, r, value ? g_variant_print(value, true) : "(null)"); + + return value; +} + +static GVariant* +terminal_settings_bridge_backend_read_user_value(GSettingsBackend* backend, + char const* key, + GVariantType const* type) noexcept +{ + auto const impl = IMPL(backend); + + auto const ce = cache_lookup_entry(impl, key); + if (ce && ce->value_set) + return ce->value ? g_variant_ref(ce->value) : nullptr; + + gs_unref_variant GVariant* rv = nullptr; + auto r = + terminal_settings_bridge_call_read_user_value_sync(impl->bridge, + key, + g_variant_type_peek_string(type), + &rv, + impl->cancellable, + nullptr); + + auto const value = r ? terminal_g_variant_unwrap(rv) : nullptr; + + if (r && value && !g_variant_is_of_type(value, type)) { + _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, + "Bridge backend ::read_user_value key %s got type %s expected type %s\n", + key, + g_variant_get_type_string(value), + g_variant_type_peek_string(type)); + + g_clear_pointer(&value, g_variant_unref); + r = false; + } + + if (r) + cache_insert_value(impl, key, value); + else + cache_remove_value(impl, key); + + _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, + "Bridge backend ::read_user_value key %s success %d value %s\n", + key, r, value ? g_variant_print(value, true) : "(null)"); + + return value; +} + +static void +terminal_settings_bridge_backend_reset(GSettingsBackend* backend, + char const* key, + void* tag) noexcept +{ + auto const impl = IMPL(backend); + auto const r = + terminal_settings_bridge_call_reset_sync(impl->bridge, + key, + impl->cancellable, + nullptr); + + cache_remove_value(impl, key); + + g_settings_backend_changed(backend, key, tag); + + _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, + "Bridge backend ::reset key %s success %d\n", + key, r); +} + +static void +terminal_settings_bridge_backend_sync(GSettingsBackend* backend) noexcept +{ + auto const impl = IMPL(backend); + terminal_settings_bridge_call_sync_sync(impl->bridge, + impl->cancellable, + nullptr); + + _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, + "Bridge backend ::sync\n"); +} + +static void +terminal_settings_bridge_backend_subscribe(GSettingsBackend* backend, + char const* name) noexcept +{ + auto const impl = IMPL(backend); + terminal_settings_bridge_call_subscribe_sync(impl->bridge, + name, + impl->cancellable, + nullptr); + + _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, + "Bridge backend ::subscribe name %s\n", name); +} + +static void +terminal_settings_bridge_backend_unsubscribe(GSettingsBackend* backend, + char const* name) noexcept +{ + auto const impl = IMPL(backend); + terminal_settings_bridge_call_unsubscribe_sync(impl->bridge, + name, + impl->cancellable, + nullptr); + + _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, + "Bridge backend ::unsubscribe name %s\n", name); +} + +static gboolean +terminal_settings_bridge_backend_write(GSettingsBackend* backend, + char const* key, + GVariant* value, + void* tag) noexcept +{ + auto const impl = IMPL(backend); + + gs_unref_variant auto holder = g_variant_ref_sink(value); + + auto success = gboolean{false}; + auto const r = + terminal_settings_bridge_call_write_sync(impl->bridge, + key, + terminal_g_variant_wrap(value), + &success, + impl->cancellable, + nullptr); + + cache_insert_value(impl, key, value); + + g_settings_backend_changed(backend, key, tag); + + _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, + "Bridge backend ::write key %s value %s success %d\n", + key, value ? g_variant_print(value, true) : "(null)", r); + + return r && success; +} + +static gboolean +terminal_settings_bridge_backend_write_tree(GSettingsBackend* backend, + GTree* tree, + void* tag) noexcept +{ + auto const impl = IMPL(backend); + + gs_free char* path_prefix = nullptr; + gs_free char const** keys = nullptr; + gs_free GVariant** values = nullptr; + g_settings_backend_flatten_tree(tree, + &path_prefix, + &keys, + &values); + + auto builder = GVariantBuilder{}; + g_variant_builder_init(&builder, G_VARIANT_TYPE("a(smv)")); + for (auto i = 0; keys[i]; ++i) { + gs_unref_variant auto value = values[i] ? g_variant_ref_sink(values[i]) : nullptr; + + g_variant_builder_add(&builder, + "(smv)", + keys[i], + value ? g_variant_new_variant(value) : nullptr); + + gs_free auto wkey = g_strconcat(path_prefix, keys[i], nullptr); + // Directory reset? + if (g_str_has_suffix(wkey, "/")) { + g_warn_if_fail(!value); + cache_remove_path(impl, wkey); + } else { + cache_insert_value(impl, wkey, value); + } + } + + auto const tree_value = terminal_g_variant_wrap(g_variant_builder_end(&builder)); + + auto success = gboolean{false}; + auto const r = + terminal_settings_bridge_call_write_tree_sync(impl->bridge, + path_prefix, + tree_value, // consumed + &success, + impl->cancellable, + nullptr); + + g_settings_backend_changed_tree(backend, tree, tag); + + _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, + "Bridge backend ::write_tree success %d\n", + r); + + return r && success; +} + +/* GObject class implementation */ + +static void +terminal_settings_bridge_backend_init(TerminalSettingsBridgeBackend* backend) /* noexcept */ +{ + auto const impl = IMPL(backend); + + // Note that unfortunately it appears to be impossible to receive all + // change notifications from a GSettingsBackend directly, so we cannot + // get forwarded change notifications from the bridge. Instead, we have + // to cache written values (since the actual write happens delayed in + // the remote backend and the next read may still return the old value + // otherwise). + impl->cache = g_hash_table_new_full(g_str_hash, + g_str_equal, + g_free, + GDestroyNotify(cache_entry_free)); +} + +static void +terminal_settings_bridge_backend_constructed(GObject* object) noexcept +{ + G_OBJECT_CLASS(terminal_settings_bridge_backend_parent_class)->constructed(object); + + auto const impl = IMPL(object); + assert(impl->bridge); +} + +static void +terminal_settings_bridge_backend_finalize(GObject* object) noexcept +{ + auto const impl = IMPL(object); + g_clear_pointer(&impl->cache, g_hash_table_unref); + g_clear_object(&impl->cancellable); + g_clear_object(&impl->bridge); + + G_OBJECT_CLASS(terminal_settings_bridge_backend_parent_class)->finalize(object); +} + +static void +terminal_settings_bridge_backend_set_property(GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec) noexcept +{ + auto const impl = IMPL(object); + + switch (prop_id) { + case PROP_SETTINGS_BRIDGE: + impl->bridge = TERMINAL_SETTINGS_BRIDGE(g_value_dup_object(value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void +terminal_settings_bridge_backend_class_init(TerminalSettingsBridgeBackendClass* klass) /* noexcept */ +{ + auto const gobject_class = G_OBJECT_CLASS(klass); + gobject_class->constructed = terminal_settings_bridge_backend_constructed; + gobject_class->finalize = terminal_settings_bridge_backend_finalize; + gobject_class->set_property = terminal_settings_bridge_backend_set_property; + + g_object_class_install_property + (gobject_class, + PROP_SETTINGS_BRIDGE, + g_param_spec_object("settings-bridge", nullptr, nullptr, + TERMINAL_TYPE_SETTINGS_BRIDGE, + GParamFlags(G_PARAM_WRITABLE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS))); + + auto const backend_class = G_SETTINGS_BACKEND_CLASS(klass); + backend_class->get_permission = terminal_settings_bridge_backend_get_permission; + backend_class->get_writable = terminal_settings_bridge_backend_get_writable; + backend_class->read = terminal_settings_bridge_backend_read; + backend_class->read_user_value = terminal_settings_bridge_backend_read_user_value; + backend_class->reset = terminal_settings_bridge_backend_reset; + backend_class->subscribe = terminal_settings_bridge_backend_subscribe; + backend_class->sync = terminal_settings_bridge_backend_sync; + backend_class->unsubscribe = terminal_settings_bridge_backend_unsubscribe; + backend_class->write = terminal_settings_bridge_backend_write; + backend_class->write_tree = terminal_settings_bridge_backend_write_tree; +} + +/* public API */ + +/** + * terminal_settings_bridge_backend_new: + * @bridge: a #TerminalSettingsBridge + * + * Returns: (transfer full): a new #TerminalSettingsBridgeBackend for @bridge + */ +GSettingsBackend* +terminal_settings_bridge_backend_new(TerminalSettingsBridge* bridge) +{ + return reinterpret_cast<GSettingsBackend*> + (g_object_new (TERMINAL_TYPE_SETTINGS_BRIDGE_BACKEND, + "settings-bridge", bridge, + nullptr)); +} diff --git a/src/terminal-settings-bridge-backend.hh b/src/terminal-settings-bridge-backend.hh new file mode 100644 index 0000000..2b7c188 --- /dev/null +++ b/src/terminal-settings-bridge-backend.hh @@ -0,0 +1,38 @@ +/* + * Copyright © 2008, 2010, 2022 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/>. + */ + +#pragma once + +#include "terminal-settings-bridge-generated.h" + +G_BEGIN_DECLS + +#define TERMINAL_TYPE_SETTINGS_BRIDGE_BACKEND (terminal_settings_bridge_backend_get_type ()) +#define TERMINAL_SETTINGS_BRIDGE_BACKEND(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), TERMINAL_TYPE_SETTINGS_BRIDGE_BACKEND, TerminalSettingsBridgeBackend)) +#define TERMINAL_SETTINGS_BRIDGE_BACKEND_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), TERMINAL_TYPE_SETTINGS_BRIDGE_BACKEND, TerminalSettingsBridgeBackendClass)) +#define TERMINAL_IS_SETTINGS_BRIDGE_BACKEND(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), TERMINAL_TYPE_SETTINGS_BRIDGE_BACKEND)) +#define TERMINAL_IS_SETTINGS_BRIDGE_BACKEND_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), TERMINAL_TYPE_SETTINGS_BRIDGE_BACKEND)) +#define TERMINAL_SETTINGS_BRIDGE_BACKEND_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), TERMINAL_TYPE_SETTINGS_BRIDGE_BACKEND, TerminalSettingsBridgeBackendClass)) + +typedef struct _TerminalSettingsBridgeBackend TerminalSettingsBridgeBackend; +typedef struct _TerminalSettingsBridgeBackendClass TerminalSettingsBridgeBackendClass; + +GType terminal_settings_bridge_backend_get_type(void); + +GSettingsBackend* terminal_settings_bridge_backend_new(TerminalSettingsBridge* bridge); + +G_END_DECLS diff --git a/src/terminal-settings-bridge-impl.cc b/src/terminal-settings-bridge-impl.cc new file mode 100644 index 0000000..da26e38 --- /dev/null +++ b/src/terminal-settings-bridge-impl.cc @@ -0,0 +1,402 @@ +/* + * Copyright © 2008, 2010, 2011, 2022 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" +#define G_SETTINGS_ENABLE_BACKEND + +#include <cassert> + +#include "terminal-settings-bridge-impl.hh" + +#include "terminal-app.hh" +#include "terminal-debug.hh" +#include "terminal-libgsystem.hh" +#include "terminal-settings-utils.hh" +#include "terminal-settings-bridge-generated.h" + +#include <gio/gio.h> +#include <gio/gsettingsbackend.h> + +enum { + PROP_SETTINGS_BACKEND = 1, +}; + +struct _TerminalSettingsBridgeImpl { + TerminalSettingsBridgeSkeleton parent_instance; + + GSettingsBackend* backend; + void* tag; +}; + +struct _TerminalSettingsBridgeImplClass { + TerminalSettingsBridgeSkeletonClass parent_class; +}; + +/* helper functions */ + +template<typename T> +static inline constexpr auto +IMPL(T* that) noexcept +{ + return reinterpret_cast<TerminalSettingsBridgeImpl*>(that); +} + +static GVariantType* +type_from_string(GDBusMethodInvocation* invocation, + char const* type) noexcept +{ + if (!g_variant_type_string_is_valid(type)) { + g_dbus_method_invocation_return_error(invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_INVALID_ARGS, + "Invalid type: %s", + type); + return nullptr; + } + + return g_variant_type_new(type); +} + +static auto +value(GDBusMethodInvocation* invocation, + char const* format, + ...) noexcept +{ + va_list args; + va_start(args, format); + auto const v = g_variant_new_va(format, nullptr, &args); + va_end(args); + g_dbus_method_invocation_return_value(invocation, v); + return true; +} + +static auto +nothing(GDBusMethodInvocation* invocation) noexcept +{ + return value(invocation, "()"); +} + +static auto +novalue(GDBusMethodInvocation* invocation) noexcept +{ + g_dbus_method_invocation_return_error_literal(invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "No value"); + return true; +} + +static auto +success(GDBusMethodInvocation* invocation, + bool v = true) noexcept +{ + return value(invocation, "(b)", v); +} + +/* TerminalSettingsBridge interface implementation */ + +static gboolean +terminal_settings_bridge_impl_get_permission(TerminalSettingsBridge* object, + GDBusMethodInvocation* invocation, + char const* path) noexcept +{ + _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, + "Bridge impl ::get_permission path %s\n", + path); + + return novalue(invocation); +} + +static gboolean +terminal_settings_bridge_impl_get_writable(TerminalSettingsBridge* object, + GDBusMethodInvocation* invocation, + char const* key) noexcept +{ + _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, + "Bridge impl ::get_writable key %s\n", + key); + + auto const impl = IMPL(object); + auto const v = terminal_g_settings_backend_get_writable(impl->backend, key); + return success(invocation, v); +} + +static gboolean +terminal_settings_bridge_impl_read(TerminalSettingsBridge* object, + GDBusMethodInvocation* invocation, + char const* key, + char const* type, + gboolean default_value) noexcept +{ + _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, + "Bridge impl ::read key %s type %s default %d\n", + key, type, default_value); + + gs_free_variant_type auto vtype = type_from_string(invocation, type); + if (!vtype) + return true; + + auto const impl = IMPL(object); + gs_unref_variant auto v = terminal_g_settings_backend_read(impl->backend, + key, + vtype, + default_value); + return value(invocation, "(@ay)", terminal_g_variant_wrap(v)); +} + +static gboolean +terminal_settings_bridge_impl_read_user_value(TerminalSettingsBridge* object, + GDBusMethodInvocation* invocation, + char const* key, + char const* type) noexcept +{ + _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, + "Bridge impl ::read_user_value key %s type %s\n", + key, type); + + gs_free_variant_type auto vtype = type_from_string(invocation, type); + if (!vtype) + return true; + + auto const impl = IMPL(object); + gs_unref_variant auto v = terminal_g_settings_backend_read_user_value(impl->backend, + key, + vtype); + return value(invocation, "(@ay)", terminal_g_variant_wrap(v)); +} + +static gboolean +terminal_settings_bridge_impl_reset(TerminalSettingsBridge* object, + GDBusMethodInvocation* invocation, + char const* key) noexcept +{ + _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, + "Bridge impl ::reset key %s\n", + key); + + auto const impl = IMPL(object); + terminal_g_settings_backend_reset(impl->backend, key, impl->tag); + return nothing(invocation); +} + +static gboolean +terminal_settings_bridge_impl_subscribe(TerminalSettingsBridge* object, + GDBusMethodInvocation* invocation, + char const* name) noexcept +{ + _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, + "Bridge impl ::subscribe name %s\n", + name); + + auto const impl = IMPL(object); + terminal_g_settings_backend_subscribe(impl->backend, name); + return nothing(invocation); +} + +static gboolean +terminal_settings_bridge_impl_sync(TerminalSettingsBridge* object, + GDBusMethodInvocation* invocation) noexcept +{ + _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, + "Bridge impl ::sync\n"); + + auto const impl = IMPL(object); + terminal_g_settings_backend_sync(impl->backend); + return nothing(invocation); +} + +static gboolean +terminal_settings_bridge_impl_unsubscribe(TerminalSettingsBridge* object, + GDBusMethodInvocation* invocation, + char const* name) noexcept +{ + _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, + "Bridge impl ::unsubscribe name %s\n", + name); + + auto const impl = IMPL(object); + terminal_g_settings_backend_unsubscribe(impl->backend, name); + return nothing(invocation); +} + +static gboolean +terminal_settings_bridge_impl_write(TerminalSettingsBridge* object, + GDBusMethodInvocation* invocation, + char const* key, + GVariant* value) noexcept +{ + auto const impl = IMPL(object); + gs_unref_variant auto v = terminal_g_variant_unwrap(value); + + _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, + "Bridge impl ::write key %s value %s\n", + key, v ? g_variant_print(v, true): "(null)"); + + auto const r = terminal_g_settings_backend_write(impl->backend, + key, + v, + impl->tag); + return success(invocation, r); +} + +static gboolean +terminal_settings_bridge_impl_write_tree(TerminalSettingsBridge* object, + GDBusMethodInvocation* invocation, + char const* path_prefix, + GVariant* variant) noexcept +{ + _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, + "Bridge impl ::write_tree path-prefix %s\n", + path_prefix); + + gs_unref_variant auto tree_value = terminal_g_variant_unwrap(variant); + if (!tree_value || + !g_variant_is_of_type(tree_value, G_VARIANT_TYPE("a(smv)"))) { + _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, + "Bridge impl ::write_tree got type %s expected type a(smv)\n", + tree_value ? g_variant_get_type_string(tree_value) : "(null)"); + + g_dbus_method_invocation_return_error + (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_INVALID_ARGS, + "Invalid type: got type \"%s\" expected type \"a(smv)\"", + tree_value ? g_variant_get_type_string(tree_value) : "(null)"); + return true; + } + + auto const tree = terminal_g_settings_backend_create_tree(); + + auto iter = GVariantIter{}; + g_variant_iter_init(&iter, tree_value); + + char const* key = nullptr; + GVariant* value = nullptr; + while (g_variant_iter_loop(&iter, "(&smv)", &key, &value)) { + g_tree_insert(tree, + g_strconcat(path_prefix, key, nullptr), // adopts + value ? g_variant_get_variant(value) : nullptr); + } + + auto const impl = IMPL(object); + auto const v = terminal_g_settings_backend_write_tree(impl->backend, + tree, + impl->tag); + + g_tree_unref(tree); + return success(invocation, v); +} + +static void +terminal_settings_bridge_impl_iface_init(TerminalSettingsBridgeIface* iface) noexcept +{ + iface->handle_get_permission = terminal_settings_bridge_impl_get_permission; + iface->handle_get_writable = terminal_settings_bridge_impl_get_writable; + iface->handle_read = terminal_settings_bridge_impl_read; + iface->handle_read_user_value = terminal_settings_bridge_impl_read_user_value; + iface->handle_reset = terminal_settings_bridge_impl_reset; + iface->handle_subscribe = terminal_settings_bridge_impl_subscribe; + iface->handle_sync= terminal_settings_bridge_impl_sync; + iface->handle_unsubscribe = terminal_settings_bridge_impl_unsubscribe; + iface->handle_write = terminal_settings_bridge_impl_write; + iface->handle_write_tree = terminal_settings_bridge_impl_write_tree; +} + +/* GObject class implementation */ + +G_DEFINE_TYPE_WITH_CODE(TerminalSettingsBridgeImpl, + terminal_settings_bridge_impl, + TERMINAL_TYPE_SETTINGS_BRIDGE_SKELETON, + G_IMPLEMENT_INTERFACE(TERMINAL_TYPE_SETTINGS_BRIDGE, + terminal_settings_bridge_impl_iface_init)); + +static void +terminal_settings_bridge_impl_init(TerminalSettingsBridgeImpl* impl) /* noexcept */ +{ + impl->tag = &impl->tag; +} + +static void +terminal_settings_bridge_impl_constructed(GObject* object) noexcept +{ + G_OBJECT_CLASS(terminal_settings_bridge_impl_parent_class)->constructed(object); + + auto const impl = IMPL(object); + assert(impl->backend); +} + +static void +terminal_settings_bridge_impl_finalize(GObject* object) noexcept +{ + auto const impl = IMPL(object); + g_clear_object(&impl->backend); + + G_OBJECT_CLASS(terminal_settings_bridge_impl_parent_class)->finalize(object); +} + +static void +terminal_settings_bridge_impl_set_property(GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec) noexcept +{ + auto const impl = IMPL(object); + + switch (prop_id) { + case PROP_SETTINGS_BACKEND: + impl->backend = G_SETTINGS_BACKEND(g_value_dup_object(value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void +terminal_settings_bridge_impl_class_init(TerminalSettingsBridgeImplClass* klass) /* noexcept */ +{ + auto const gobject_class = G_OBJECT_CLASS(klass); + gobject_class->constructed = terminal_settings_bridge_impl_constructed; + gobject_class->finalize = terminal_settings_bridge_impl_finalize; + gobject_class->set_property = terminal_settings_bridge_impl_set_property; + + g_object_class_install_property + (gobject_class, + PROP_SETTINGS_BACKEND, + g_param_spec_object("settings-backend", nullptr, nullptr, + G_TYPE_SETTINGS_BACKEND, + GParamFlags(G_PARAM_WRITABLE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS))); +} + +/* public API */ + +/** +* terminal_settings_bridge_impl_new: +* @backend: a #GSettingsBackend +* +* Returns: (transfer full): a new #TerminalSettingsBridgeImpl for @backend + */ +TerminalSettingsBridgeImpl* +terminal_settings_bridge_impl_new(GSettingsBackend* backend) +{ + return reinterpret_cast<TerminalSettingsBridgeImpl*> + (g_object_new (TERMINAL_TYPE_SETTINGS_BRIDGE_IMPL, + "settings-backend", backend, + nullptr)); +} diff --git a/src/terminal-settings-bridge-impl.hh b/src/terminal-settings-bridge-impl.hh new file mode 100644 index 0000000..ab8881f --- /dev/null +++ b/src/terminal-settings-bridge-impl.hh @@ -0,0 +1,38 @@ +/* + * Copyright © 2008, 2010, 2022 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/>. + */ + +#pragma once + +#include <gio/gio.h> + +G_BEGIN_DECLS + +#define TERMINAL_TYPE_SETTINGS_BRIDGE_IMPL (terminal_settings_bridge_impl_get_type ()) +#define TERMINAL_SETTINGS_BRIDGE_IMPL(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), TERMINAL_TYPE_SETTINGS_BRIDGE_IMPL, TerminalSettingsBridgeImpl)) +#define TERMINAL_SETTINGS_BRIDGE_IMPL_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), TERMINAL_TYPE_SETTINGS_BRIDGE_IMPL, TerminalSettingsBridgeImplClass)) +#define TERMINAL_IS_SETTINGS_BRIDGE_IMPL(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), TERMINAL_TYPE_SETTINGS_BRIDGE_IMPL)) +#define TERMINAL_IS_SETTINGS_BRIDGE_IMPL_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), TERMINAL_TYPE_SETTINGS_BRIDGE_IMPL)) +#define TERMINAL_SETTINGS_BRIDGE_IMPL_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), TERMINAL_TYPE_SETTINGS_BRIDGE_IMPL, TerminalSettingsBridgeImplClass)) + +typedef struct _TerminalSettingsBridgeImpl TerminalSettingsBridgeImpl; +typedef struct _TerminalSettingsBridgeImplClass TerminalSettingsBridgeImplClass; + +GType terminal_settings_bridge_impl_get_type(void); + +TerminalSettingsBridgeImpl* terminal_settings_bridge_impl_new(GSettingsBackend* backend); + +G_END_DECLS diff --git a/src/terminal-settings-list.cc b/src/terminal-settings-list.cc new file mode 100644 index 0000000..8907643 --- /dev/null +++ b/src/terminal-settings-list.cc @@ -0,0 +1,930 @@ +/* + * Copyright © 2013 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 "terminal-settings-list.hh" +#include "terminal-client-utils.hh" + +#include <string.h> +#include <uuid.h> + +#define G_SETTINGS_ENABLE_BACKEND +#include <gio/gsettingsbackend.h> + +#include "terminal-type-builtins.hh" +#include "terminal-settings-utils.hh" +#include "terminal-schemas.hh" +#include "terminal-debug.hh" +#include "terminal-libgsystem.hh" + +struct _TerminalSettingsList { + GSettings parent; + + GSettingsBackend* settings_backend; + GSettingsSchemaSource* schema_source; + char *path; + char *child_schema_id; + + char **uuids; + char *default_uuid; + + GHashTable *children; + + TerminalSettingsListFlags flags; +}; + +struct _TerminalSettingsListClass { + GSettingsClass parent; + + void (* children_changed) (TerminalSettingsList *list); + void (* default_changed) (TerminalSettingsList *list); +}; + +enum { + PROP_SCHEMA_SOURCE = 1, + PROP_CHILD_SCHEMA_ID, + PROP_FLAGS +}; + +enum { + SIGNAL_CHILDREN_CHANGED, + SIGNAL_DEFAULT_CHANGED, + N_SIGNALS +}; + +static guint signals[N_SIGNALS]; + +static void +strv_printerr (char **strv) +{ + char **p; + + if (strv == nullptr) { + g_printerr ("(null)"); + return; + } + + for (p = strv; *p; p++) + g_printerr ("%s'%s'", p != strv ? ", " : "", *p); +} + +static char ** +strv_sort (char **strv) +{ + // FIXMEchpe + return strv; +} + +static gboolean +strv_equal (char **a, + char **b) +{ + char **e, **f; + + if (a == nullptr || b == nullptr) + return a == b; + + for (e = a, f = b; *e && *f; e++, f++) { + if (!g_str_equal (*e, *f)) + return FALSE; + } + + return *e == *f; +} + +static int +strv_find (char **strv, + const char *str) +{ + int i; + + if (strv == nullptr || str == nullptr) + return -1; + + for (i = 0; strv[i]; i++) { + if (!g_str_equal (strv[i], str)) + continue; + + return i; + } + + return -1; +} + +#if defined(TERMINAL_SERVER) || defined(TERMINAL_PREFERENCES) + +static char ** +strv_dupv_insert (char **strv, + const char *str) +{ + char **nstrv, **p, **q; + + if (strv == nullptr) { + char *s[2] = { (char *) str, nullptr }; + return g_strdupv (s); + } + + /* Is it already in the list? */ + for (p = strv; *p; p++) + if (g_str_equal (*p, str)) + return g_strdupv (strv); + + /* Not found; append */ + nstrv = g_new (char *, (p - strv) + 2); + for (p = strv, q = nstrv; *p; p++, q++) + *q = g_strdup (*p); + *q++ = g_strdup (str); + *q = nullptr; + + return strv_sort (nstrv); +} + +static char ** +strv_dupv_remove (char **strv, + const char *str) +{ + char **nstrv, **p, **q; + + if (strv == nullptr) + return nullptr; + + nstrv = g_strdupv (strv); + for (p = q = nstrv; *p; p++) { + if (!g_str_equal (*p, str)) + *q++ = *p; + else + g_free (*p); + } + *q = nullptr; + + return nstrv; +} + +#endif /* TERMINAL_SERVER || TERMINAL_PREFERENCES */ + +gboolean +terminal_settings_list_valid_uuid (const char *str) +{ + uuid_t u; + + if (str == nullptr) + return FALSE; + + return uuid_parse ((char *) str, u) == 0; +} + +#if defined(TERMINAL_SERVER) || defined(TERMINAL_PREFERENCES) + +static char * +new_list_entry (void) +{ + uuid_t u; + char name[37]; + + uuid_generate (u); + uuid_unparse (u, name); + + return g_strdup (name); +} + +#endif /* TERMINAL_SERVER || TERMINAL_PREFERENCES */ + +static gboolean +validate_list (TerminalSettingsList *list, + char **entries) +{ + gboolean allow_empty = (list->flags & TERMINAL_SETTINGS_LIST_FLAG_ALLOW_EMPTY) != 0; + guint i; + + if (entries == nullptr) + return allow_empty; + + for (i = 0; entries[i]; i++) { + if (!terminal_settings_list_valid_uuid (entries[i])) + return FALSE; + } + + return (i > 0) || allow_empty; +} + +static gboolean +list_map_func (GVariant *value, + gpointer *result, + gpointer user_data) +{ + TerminalSettingsList *list = (TerminalSettingsList*)user_data; + gs_strfreev char **entries; + + entries = strv_sort (g_variant_dup_strv (value, nullptr)); + + if (validate_list (list, entries)) { + gs_transfer_out_value(result, &entries); + return TRUE; + } + + return FALSE; +} + +static char* +path_new (TerminalSettingsList *list, + char const* uuid) +{ + return g_strdup_printf ("%s:%s/", list->path, uuid); +} + +static GSettings * +terminal_settings_list_ref_child_internal (TerminalSettingsList *list, + const char *uuid) +{ + GSettings *child; + gs_free char *path = nullptr; + + if (strv_find (list->uuids, uuid) == -1) + return nullptr; + + _terminal_debug_print (TERMINAL_DEBUG_SETTINGS_LIST, + "%s UUID %s\n", G_STRFUNC, uuid); + + child = (GSettings*)g_hash_table_lookup (list->children, uuid); + if (child) + goto done; + + path = path_new (list, uuid); + child = terminal_g_settings_new_with_path(list->settings_backend, + list->schema_source, + list->child_schema_id, + path); + g_hash_table_insert (list->children, g_strdup (uuid), child /* adopted */); + + done: + return (GSettings*)g_object_ref(child); +} + +#if defined(TERMINAL_SERVER) || defined(TERMINAL_PREFERENCES) + +static char * +terminal_settings_list_add_child_internal (TerminalSettingsList *list, + const char *uuid, + const char *name) +{ + + auto const new_uuid = new_list_entry(); + _terminal_debug_print (TERMINAL_DEBUG_SETTINGS_LIST, + "%s NEW UUID %s\n", G_STRFUNC, new_uuid); + + gs_free auto path = path_new(list, uuid); + gs_free auto new_path = path_new(list, new_uuid); + + auto tree = terminal_g_settings_backend_create_tree(); + terminal_g_settings_backend_clone_schema(list->settings_backend, + list->schema_source, + list->child_schema_id, + path, + new_path, + tree); + if (name) { + g_tree_insert(tree, + g_strconcat(new_path, TERMINAL_PROFILE_VISIBLE_NAME_KEY, nullptr), // transfer + g_variant_take_ref(g_variant_new_string(name))); // transfer + } + +#ifdef ENABLE_DEBUG + _TERMINAL_DEBUG_IF(TERMINAL_DEBUG_SETTINGS_LIST) { + g_printerr("Cloning schema %s from %s -> %s\n", list->child_schema_id, path, new_path); + terminal_g_settings_backend_print_tree(tree); + } +#endif + + auto const tag = &list; + (void)terminal_g_settings_backend_write_tree(list->settings_backend, tree, tag); + g_tree_unref(tree); + + gs_strfreev auto new_uuids = strv_dupv_insert(list->uuids, new_uuid); + g_settings_set_strv(&list->parent, TERMINAL_SETTINGS_LIST_LIST_KEY, + (char const* const*)new_uuids); + + return new_uuid; +} + +static void +terminal_settings_list_remove_child_internal (TerminalSettingsList *list, + const char *uuid) +{ + gs_strfreev char **new_uuids; + + _terminal_debug_print (TERMINAL_DEBUG_SETTINGS_LIST, + "%s UUID %s\n", G_STRFUNC, uuid); + + new_uuids = strv_dupv_remove (list->uuids, uuid); + + if ((new_uuids == nullptr || new_uuids[0] == nullptr) && + (list->flags & TERMINAL_SETTINGS_LIST_FLAG_ALLOW_EMPTY) == 0) + return; + + g_settings_set_strv (&list->parent, TERMINAL_SETTINGS_LIST_LIST_KEY, (const char * const *) new_uuids); + + if (list->default_uuid != nullptr && + g_str_equal (list->default_uuid, uuid)) + g_settings_set_string (&list->parent, TERMINAL_SETTINGS_LIST_DEFAULT_KEY, ""); + + /* Now we unset all keys under the child */ + gs_free auto path = path_new(list, uuid); + terminal_g_settings_backend_erase_path(list->settings_backend, + list->schema_source, + list->child_schema_id, + path); +} + +#endif /* TERMINAL_SERVER || TERMINAL_PREFERENCES */ + +static void +terminal_settings_list_update_list (TerminalSettingsList *list) +{ + char **uuids, *uuid; + GSettings *child; + GHashTable *new_children; + guint i; + gboolean changed; + + uuids = (char**)g_settings_get_mapped (&list->parent, + TERMINAL_SETTINGS_LIST_LIST_KEY, + list_map_func, list); + + _TERMINAL_DEBUG_IF (TERMINAL_DEBUG_SETTINGS_LIST) { + g_printerr ("%s: current UUIDs [", G_STRFUNC); + strv_printerr (list->uuids); + g_printerr ("]\n new UUIDs ["); + strv_printerr (uuids); + g_printerr ("]\n"); + } + + if (strv_equal (uuids, list->uuids) && + ((list->flags & TERMINAL_SETTINGS_LIST_FLAG_HAS_DEFAULT) == 0 || + strv_find (list->uuids, list->default_uuid) != -1)) { + g_strfreev (uuids); + return; + } + + new_children = g_hash_table_new_full (g_str_hash, g_str_equal, + (GDestroyNotify) g_free, + (GDestroyNotify) g_object_unref); + + if (uuids) { + for (i = 0; uuids[i] != nullptr; i++) { + uuid = uuids[i]; + + child = (GSettings*)g_hash_table_lookup (list->children, uuid); + + if (child) { + g_object_ref (child); + g_hash_table_remove (list->children, uuid); + g_hash_table_insert (new_children, g_strdup (uuid), child /* adopted */); + } + } + + changed = !strv_equal (uuids, list->uuids); + } else { + changed = g_strv_length (list->uuids) != 0; + } + + g_hash_table_unref (list->children); + list->children = new_children; + + g_strfreev (list->uuids); + list->uuids = uuids; /* adopts */ + + if (changed) + g_signal_emit (list, signals[SIGNAL_CHILDREN_CHANGED], 0); +} + +static void +terminal_settings_list_update_default (TerminalSettingsList *list) +{ + if ((list->flags & TERMINAL_SETTINGS_LIST_FLAG_HAS_DEFAULT) == 0) + return; + + g_free (list->default_uuid); + list->default_uuid = g_settings_get_string (&list->parent, + TERMINAL_SETTINGS_LIST_DEFAULT_KEY); + + _terminal_debug_print (TERMINAL_DEBUG_SETTINGS_LIST, + "%s new default UUID %s\n", G_STRFUNC, list->default_uuid); + + g_signal_emit (list, signals[SIGNAL_DEFAULT_CHANGED], 0); +} + +G_DEFINE_TYPE (TerminalSettingsList, terminal_settings_list, G_TYPE_SETTINGS); + +static void +terminal_settings_list_changed (GSettings *list_settings, + const char *key) +{ + TerminalSettingsList *list = TERMINAL_SETTINGS_LIST (list_settings); + + _terminal_debug_print (TERMINAL_DEBUG_SETTINGS_LIST, + "%s key %s", G_STRFUNC, key ? key : "(null)"); + + if (key == nullptr || + g_str_equal (key, TERMINAL_SETTINGS_LIST_LIST_KEY)) { + terminal_settings_list_update_list (list); + terminal_settings_list_update_default (list); + } + + if (key == nullptr) + return; + + if (g_str_equal (key, TERMINAL_SETTINGS_LIST_DEFAULT_KEY)) { + terminal_settings_list_update_default (list); + } +} + +static void +terminal_settings_list_init (TerminalSettingsList *list) +{ + list->flags = TERMINAL_SETTINGS_LIST_FLAG_NONE; +} + +static void +terminal_settings_list_constructed (GObject *object) +{ + TerminalSettingsList *list = TERMINAL_SETTINGS_LIST (object); + + G_OBJECT_CLASS (terminal_settings_list_parent_class)->constructed (object); + + g_object_get(object, "backend", &list->settings_backend, nullptr); + g_assert(list->settings_backend); + g_assert(list->schema_source); + + g_assert (list->schema_source != nullptr); + g_assert (list->child_schema_id != nullptr); + + g_object_get (object, "path", &list->path, nullptr); + + list->children = g_hash_table_new_full (g_str_hash, g_str_equal, + (GDestroyNotify) g_free, + (GDestroyNotify) g_object_unref); + + terminal_settings_list_changed (&list->parent, nullptr); +} + +static void +terminal_settings_list_finalize (GObject *object) +{ + TerminalSettingsList *list = TERMINAL_SETTINGS_LIST (object); + + g_free (list->path); + g_free (list->child_schema_id); + g_strfreev (list->uuids); + g_free (list->default_uuid); + g_hash_table_unref (list->children); + g_settings_schema_source_unref(list->schema_source); + g_clear_object(&list->settings_backend); + + G_OBJECT_CLASS (terminal_settings_list_parent_class)->finalize (object); +} + +static void +terminal_settings_list_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + TerminalSettingsList *list = TERMINAL_SETTINGS_LIST (object); + + switch (prop_id) { + case PROP_SCHEMA_SOURCE: { + auto const schema_source = reinterpret_cast<GSettingsSchemaSource*>(g_value_get_boxed(value)); + list->schema_source = g_settings_schema_source_ref(schema_source); + break; + } + case PROP_CHILD_SCHEMA_ID: + list->child_schema_id = g_value_dup_string (value); + break; + case PROP_FLAGS: + list->flags = TerminalSettingsListFlags(g_value_get_flags (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +terminal_settings_list_class_init (TerminalSettingsListClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GSettingsClass *settings_class = G_SETTINGS_CLASS (klass); + + object_class->set_property = terminal_settings_list_set_property; + object_class->constructed = terminal_settings_list_constructed; + object_class->finalize = terminal_settings_list_finalize; + + /** + * TerminalSettingsList:child-schema-id: + * + * The name of the schema of the children of this list. + */ + g_object_class_install_property (object_class, PROP_SCHEMA_SOURCE, + g_param_spec_boxed("schema-source", nullptr, nullptr, + G_TYPE_SETTINGS_SCHEMA_SOURCE, + GParamFlags(G_PARAM_CONSTRUCT_ONLY | + G_PARAM_WRITABLE | + G_PARAM_STATIC_STRINGS))); + + /** + * TerminalSettingsList:child-schema-id: + * + * The name of the schema of the children of this list. + */ + g_object_class_install_property (object_class, PROP_CHILD_SCHEMA_ID, + g_param_spec_string ("child-schema-id", nullptr, nullptr, + nullptr, + GParamFlags(G_PARAM_CONSTRUCT_ONLY | + G_PARAM_WRITABLE | + G_PARAM_STATIC_STRINGS))); + + /** + * TerminalSettingsList:flags: + * + * Flags from #TerminalSettingsListFlags. + */ + g_object_class_install_property (object_class, PROP_FLAGS, + g_param_spec_flags ("flags", nullptr,nullptr, + TERMINAL_TYPE_SETTINGS_LIST_FLAGS, + TERMINAL_SETTINGS_LIST_FLAG_NONE, + GParamFlags(G_PARAM_CONSTRUCT_ONLY | + G_PARAM_WRITABLE | + G_PARAM_STATIC_STRINGS))); + + /** + * TerminalSettingsList::children-changed: + * @list: the object on which the signal was emitted + * + * The "children-changed" signal is emitted when the list of children + * has potentially changed. + */ + signals[SIGNAL_CHILDREN_CHANGED] = + g_signal_new ("children-changed", TERMINAL_TYPE_SETTINGS_LIST, + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (TerminalSettingsListClass, children_changed), + nullptr, nullptr, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, + 0); + + /** + * TerminalSettingsList::default-changed: + * @list: the object on which the signal was emitted + * + * The "default-changed" signal is emitted when the default child + * has potentially changed. + */ + signals[SIGNAL_DEFAULT_CHANGED] = + g_signal_new ("default-changed", TERMINAL_TYPE_SETTINGS_LIST, + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (TerminalSettingsListClass, default_changed), + nullptr, nullptr, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, + 0); + + settings_class->changed = terminal_settings_list_changed; +} + + +/** + * terminal_settings_list_new: + * @backend: (nullable): a #GSettingsBackend, or %NULL + * @schema_source: a #GSettingsSchemaSource + * @path: the settings path for the list + * @schema_id: the schema of the list, equal to or derived from "org.gnome.Terminal.SettingsList" + * @child_schema_id: the schema of the list children + * @flags: list flags + * + * Returns: (transfer full): the newly created #TerminalSettingsList + */ +TerminalSettingsList * +terminal_settings_list_new (GSettingsBackend* backend, + GSettingsSchemaSource* schema_source, + const char *path, + const char *schema_id, + const char *child_schema_id, + TerminalSettingsListFlags flags) +{ + g_return_val_if_fail (backend == nullptr || G_IS_SETTINGS_BACKEND (backend), nullptr); + g_return_val_if_fail (schema_source != nullptr, nullptr); + g_return_val_if_fail (path != nullptr, nullptr); + g_return_val_if_fail (schema_id != nullptr, nullptr); + g_return_val_if_fail (child_schema_id != nullptr, nullptr); + g_return_val_if_fail (g_str_has_suffix (path, ":/"), nullptr); + + gs_unref_settings_schema auto schema = + g_settings_schema_source_lookup(schema_source, schema_id, true); + g_assert_nonnull(schema); + + return reinterpret_cast<TerminalSettingsList*>(g_object_new (TERMINAL_TYPE_SETTINGS_LIST, + "backend", backend, + "schema-source", schema_source, + "settings-schema", schema, + "child-schema-id", child_schema_id, + "path", path, + "flags", flags, + nullptr)); +} + +/** + * terminal_settings_list_dupv_children: + * @list: a #TerminalSettingsList + * + * Returns: (transfer full): the UUIDs of the children in the settings list, or %nullptr + * if the list is empty + */ +char ** +terminal_settings_list_dupv_children (TerminalSettingsList *list) +{ + g_return_val_if_fail (TERMINAL_IS_SETTINGS_LIST (list), nullptr); + + return g_strdupv (list->uuids); +} + +/** + * terminal_settings_list_dup_default_child: + * @list: a #TerminalSettingsList + * + * Returns: (transfer full): the UUID of the default child in the settings list + */ +char * +terminal_settings_list_dup_default_child (TerminalSettingsList *list) +{ + g_return_val_if_fail (TERMINAL_IS_SETTINGS_LIST (list), nullptr); + + if ((list->flags & TERMINAL_SETTINGS_LIST_FLAG_HAS_DEFAULT) == 0) + return nullptr; + + if ((strv_find (list->uuids, list->default_uuid)) != -1) + return g_strdup (list->default_uuid); + + /* Just randomly designate the first child as default, but don't write that + * to the settings. + */ + if (list->uuids == nullptr || list->uuids[0] == nullptr) { + g_warn_if_fail ((list->flags & TERMINAL_SETTINGS_LIST_FLAG_ALLOW_EMPTY)); + return nullptr; + } + + return g_strdup (list->uuids[0]); +} + +/** + * terminal_settings_list_has_child: + * @list: a #TerminalSettingsList + * @uuid: the UUID of a list child + * + * Returns: %TRUE iff the child with @uuid exists + */ +gboolean +terminal_settings_list_has_child (TerminalSettingsList *list, + const char *uuid) +{ + g_return_val_if_fail (TERMINAL_IS_SETTINGS_LIST (list), FALSE); + g_return_val_if_fail (terminal_settings_list_valid_uuid (uuid), FALSE); + + return strv_find (list->uuids, uuid) != -1; +} + +/** + * terminal_settings_list_ref_child: + * @list: a #TerminalSettingsList + * @uuid: the UUID of a list child + * + * Returns the child #GSettings for the list child with UUID @uuid, or %nullptr + * if @list has no such child. + * + * Returns: (transfer full): a reference to the #GSettings for the child, or %nullptr + */ +GSettings * +terminal_settings_list_ref_child (TerminalSettingsList *list, + const char *uuid) +{ + g_return_val_if_fail (TERMINAL_IS_SETTINGS_LIST (list), nullptr); + g_return_val_if_fail (terminal_settings_list_valid_uuid (uuid), nullptr); + + return terminal_settings_list_ref_child_internal (list, uuid); +} + +/** + * terminal_settings_list_ref_children: + * @list: a #TerminalSettingsList + * + * Returns the list of children #GSettings or @list. + * + * Returns: (transfer full): a list of child #GSettings of @list + */ +GList * +terminal_settings_list_ref_children (TerminalSettingsList *list) +{ + GList *l; + guint i; + + g_return_val_if_fail (TERMINAL_IS_SETTINGS_LIST (list), nullptr); + + if (list->uuids == nullptr) + return nullptr; + + l = nullptr; + for (i = 0; list->uuids[i]; i++) + l = g_list_prepend (l, terminal_settings_list_ref_child (list, list->uuids[i])); + + return g_list_reverse (l); +} + +/** + * terminal_settings_list_ref_default_child: + * @list: a #TerminalSettingsList + * + * Returns the default child #GSettings for the list, or %nullptr if @list has no + * children. + * + * Returns: (transfer full): a reference to the #GSettings for the default child, or %nullptr + */ +GSettings * +terminal_settings_list_ref_default_child (TerminalSettingsList *list) +{ + gs_free char *uuid = nullptr; + + g_return_val_if_fail (TERMINAL_IS_SETTINGS_LIST (list), nullptr); + + uuid = terminal_settings_list_dup_default_child (list); + if (uuid == nullptr) + return nullptr; + + return terminal_settings_list_ref_child_internal (list, uuid); +} + +#if defined(TERMINAL_SERVER) || defined(TERMINAL_PREFERENCES) + +/** + * terminal_settings_list_add_child: + * @list: a #TerminalSettingsList + * @name: the name of the new profile + * + * Adds a new child to the list, and returns a reference to its #GSettings. + * + * Returns: (transfer full): the UUID of new child + */ +char * +terminal_settings_list_add_child (TerminalSettingsList *list, + const char *name) +{ + g_return_val_if_fail (TERMINAL_IS_SETTINGS_LIST (list), nullptr); + + return terminal_settings_list_add_child_internal (list, nullptr, name); +} + +/** + * terminal_settings_list_clone_child: + * @list: a #TerminalSettingsList + * @uuid: the UUID of the child to clone + * @name: the name of the new child + * + * Adds a new child to the list, and returns a reference to its #GSettings. + * All keys of the new child will have the same value as @uuid's. + * + * Returns: (transfer full): the UUID of new child + */ +char * +terminal_settings_list_clone_child (TerminalSettingsList *list, + const char *uuid, + const char *name) +{ + g_return_val_if_fail (TERMINAL_IS_SETTINGS_LIST (list), nullptr); + g_return_val_if_fail (terminal_settings_list_valid_uuid (uuid), nullptr); + + return terminal_settings_list_add_child_internal (list, uuid, name); +} + +/** + * terminal_settings_list_remove_child: + * @list: a #TerminalSettingsList + * @uuid: the UUID of a list child + * + * Removes the child with UUID @uuid from the list. + */ +void +terminal_settings_list_remove_child (TerminalSettingsList *list, + const char *uuid) +{ + g_return_if_fail (TERMINAL_IS_SETTINGS_LIST (list)); + g_return_if_fail (terminal_settings_list_valid_uuid (uuid)); + + terminal_settings_list_remove_child_internal (list, uuid); +} + +#endif /* TERMINAL_SERVER || TERMINAL_PREFERENCES */ + +/** + * terminal_settings_list_dup_uuid_from_child: + * @list: a #TerminalSettingsList + * @child: a #GSettings of a child in the list + * + * Returns the UUID of @child in the list, or %nullptr if @child is not in the list. + * + * Returns: (transfer full): the UUID of the child in the settings list, or %nullptr + */ +char * +terminal_settings_list_dup_uuid_from_child (TerminalSettingsList *list, + GSettings *child) +{ + gs_free char *path; + char *p; + + g_return_val_if_fail (TERMINAL_IS_SETTINGS_LIST (list), nullptr); + + g_object_get (child, "path", &path, nullptr); + g_return_val_if_fail (g_str_has_prefix (path, list->path), nullptr); + + p = path + strlen (list->path); + g_return_val_if_fail (p[0] == ':', nullptr); + p++; + g_return_val_if_fail (strlen (p) == 37, nullptr); + g_return_val_if_fail (p[36] == '/', nullptr); + p[36] = '\0'; + g_assert (terminal_settings_list_valid_uuid (p)); + + return g_strdup (p); +} + +/** + * terminal_settings_list_get_set_default_child: + * @list: a #TerminalSettingsList + * @uuid: the UUID of a child in the list + * + * Sets @uuid as the default child. + */ +void +terminal_settings_list_set_default_child (TerminalSettingsList *list, + const char *uuid) +{ + g_return_if_fail (TERMINAL_IS_SETTINGS_LIST (list)); + g_return_if_fail (terminal_settings_list_valid_uuid (uuid)); + + if (!terminal_settings_list_has_child (list, uuid)) + return; + + g_settings_set_string (&list->parent, TERMINAL_SETTINGS_LIST_DEFAULT_KEY, uuid); +} + +/** + * terminal_settings_list_foreach_child: + * @list: a #TerminalSettingsList + * @callback: a #TerminalSettingsListForeachFunc + * @user_data: user data for @callback + * + * Calls @callback for each child of @list. + * + * NOTE: No changes to @list must be made from @callback. + */ +void +terminal_settings_list_foreach_child (TerminalSettingsList *list, + TerminalSettingsListForeachFunc callback, + gpointer user_data) +{ + g_return_if_fail (TERMINAL_IS_SETTINGS_LIST (list)); + g_return_if_fail (callback); + + for (char **p = list->uuids; *p; p++) { + const char *uuid = *p; + gs_unref_object GSettings *child = terminal_settings_list_ref_child_internal (list, uuid); + if (child != nullptr) + callback (list, uuid, child, user_data); + } +} + +/** + * terminal_settings_list_foreach_child: + * @list: a #TerminalSettingsList + * + * Returns: the number of children of @list. + */ +guint +terminal_settings_list_get_n_children (TerminalSettingsList *list) +{ + g_return_val_if_fail (TERMINAL_IS_SETTINGS_LIST (list), 0); + + return g_hash_table_size (list->children); +} diff --git a/src/terminal-settings-list.hh b/src/terminal-settings-list.hh new file mode 100644 index 0000000..de99766 --- /dev/null +++ b/src/terminal-settings-list.hh @@ -0,0 +1,91 @@ +/* + * Copyright © 2013 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/>. + */ + +#ifndef TERMINAL_SETTINGS_LIST_H +#define TERMINAL_SETTINGS_LIST_H + +#include <gio/gio.h> + +#include "terminal-enums.hh" + +G_BEGIN_DECLS + +#define TERMINAL_TYPE_SETTINGS_LIST (terminal_settings_list_get_type ()) +#define TERMINAL_SETTINGS_LIST(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), TERMINAL_TYPE_SETTINGS_LIST, TerminalSettingsList)) +#define TERMINAL_SETTINGS_LIST_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), TERMINAL_TYPE_SETTINGS_LIST, TerminalSettingsListClass)) +#define TERMINAL_IS_SETTINGS_LIST(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), TERMINAL_TYPE_SETTINGS_LIST)) +#define TERMINAL_IS_SETTINGS_LIST_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), TERMINAL_TYPE_SETTINGS_LIST)) +#define TERMINAL_SETTINGS_LIST_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), TERMINAL_TYPE_SETTINGS_LIST, TerminalSettingsListClass)) + +typedef struct _TerminalSettingsList TerminalSettingsList; +typedef struct _TerminalSettingsListClass TerminalSettingsListClass; + +GType terminal_settings_list_get_type (void); + +TerminalSettingsList *terminal_settings_list_new (GSettingsBackend* backend, + GSettingsSchemaSource* schema_source, + const char *path, + const char *schema_id, + const char *child_schema_id, + TerminalSettingsListFlags flags); + +char **terminal_settings_list_dupv_children (TerminalSettingsList *list); + +GList *terminal_settings_list_ref_children (TerminalSettingsList *list); + +gboolean terminal_settings_list_has_child (TerminalSettingsList *list, + const char *uuid); + +GSettings *terminal_settings_list_ref_child (TerminalSettingsList *list, + const char *uuid); + +char *terminal_settings_list_add_child (TerminalSettingsList *list, + const char *name); + +char *terminal_settings_list_clone_child (TerminalSettingsList *list, + const char *uuid, + const char *name); + +void terminal_settings_list_remove_child (TerminalSettingsList *list, + const char *uuid); + +char *terminal_settings_list_dup_uuid_from_child (TerminalSettingsList *list, + GSettings *child); + +GSettings *terminal_settings_list_ref_default_child (TerminalSettingsList *list); + +char *terminal_settings_list_dup_default_child (TerminalSettingsList *list); + +void terminal_settings_list_set_default_child (TerminalSettingsList *list, + const char *uuid); + +typedef void (* TerminalSettingsListForeachFunc) (TerminalSettingsList *list, + const char *uuid, + GSettings *child, + gpointer user_data); + +void terminal_settings_list_foreach_child (TerminalSettingsList *list, + TerminalSettingsListForeachFunc callback, + gpointer user_data); + +guint terminal_settings_list_get_n_children (TerminalSettingsList *list); + +gboolean terminal_settings_list_valid_uuid (const char *str); + +G_END_DECLS + +#endif /* TERMINAL_SETTINGS_LIST_H */ diff --git a/src/terminal-settings-utils.cc b/src/terminal-settings-utils.cc new file mode 100644 index 0000000..a40ab5e --- /dev/null +++ b/src/terminal-settings-utils.cc @@ -0,0 +1,992 @@ +/* + * Copyright © 2008, 2010, 2011, 2022 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" +#define G_SETTINGS_ENABLE_BACKEND + +#include <gio/gsettingsbackend.h> + +#include "terminal-settings-utils.hh" +#include "terminal-client-utils.hh" +#include "terminal-debug.hh" +#include "terminal-libgsystem.hh" + +#ifdef ENABLE_DEBUG + +static gboolean +settings_change_event_cb(GSettings* settings, + void* keys, + int n_keys, + void* data) +{ + gs_free char* schema_id = nullptr; + gs_free char* path = nullptr; + g_object_get(settings, + "schema-id", &schema_id, + "path", &path, + nullptr); + + auto const qkeys = reinterpret_cast<GQuark*>(keys); + for (auto i = 0; i < n_keys; ++i) { + auto key = g_quark_to_string(qkeys[i]); + _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, + "Bridge backend ::change-event schema %s path %s key %s\n", + schema_id, path, key); + } + + return false; // propagate +} + +static gboolean +settings_writable_change_event_cb(GSettings* settings, + char const* key, + void* data) +{ + gs_free char* schema_id = nullptr; + gs_free char* path = nullptr; + g_object_get(settings, + "schema-id", &schema_id, + "path", &path, + nullptr); + + _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, + "Bridge backend ::writeable-change-event schema %s path %s key %s\n", + schema_id, path, key); + + return false; // propagate +} + +#endif /* ENABLE_DEBUG */ + +GSettings* +terminal_g_settings_new_with_path (GSettingsBackend* backend, + GSettingsSchemaSource* source, + char const* schema_id, + char const* path) +{ + gs_unref_settings_schema GSettingsSchema* schema = + g_settings_schema_source_lookup(source, + schema_id, + TRUE /* recursive */); + g_assert_nonnull(schema); + + auto const settings = g_settings_new_full(schema, + backend, + path); + +#ifdef ENABLE_DEBUG + _TERMINAL_DEBUG_IF(TERMINAL_DEBUG_BRIDGE) { + + _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, + "Creating GSettings for schema %s at %s with backend %s\n", + schema_id, path, + backend ? G_OBJECT_TYPE_NAME(backend) : "(default)"); + + if (backend != nullptr && + g_str_equal(G_OBJECT_TYPE_NAME(backend), "TerminalSettingsBridgeBackend")) { + g_signal_connect(settings, + "change-event", + G_CALLBACK(settings_change_event_cb), + nullptr); + g_signal_connect(settings, + "writable-change-event", + G_CALLBACK(settings_writable_change_event_cb), + nullptr); + } + } +#endif /* ENABLE_DEBUG */ + + return settings; +} + +GSettings* +terminal_g_settings_new(GSettingsBackend* backend, + GSettingsSchemaSource* source, + char const* schema_id) +{ + return terminal_g_settings_new_with_path(backend, source, schema_id, nullptr); +} + +#if defined(TERMINAL_SERVER) || defined(TERMINAL_PREFERENCES) + +void +terminal_g_settings_backend_clone_schema(GSettingsBackend* backend, + GSettingsSchemaSource* schema_source, + char const* schema_id, + char const* path, + char const* new_path, + GTree* tree) +{ + gs_unref_settings_schema auto schema = + g_settings_schema_source_lookup(schema_source, schema_id, true); + if (schema == nullptr) [[unlikely]] // This shouldn't really happen ever + return; + + gs_strfreev auto keys = g_settings_schema_list_keys(schema); + + for (auto i = 0; keys[i]; ++i) { + gs_unref_settings_schema_key auto schema_key = + g_settings_schema_get_key(schema, keys[i]); + + gs_free auto rkey = g_strconcat(path, keys[i], nullptr); + auto const value = + terminal_g_settings_backend_read(backend, + rkey, + g_settings_schema_key_get_value_type(schema_key), + false); + + if (value) { + g_tree_insert(tree, + g_strconcat(new_path, keys[i], nullptr), // transfer + value); // transfer + } + } +} + +gboolean +terminal_g_settings_backend_erase_path(GSettingsBackend* backend, + GSettingsSchemaSource* schema_source, + char const* schema_id, + char const* path) + +{ + // We want to erase all keys below @path, not just keys we wrote ourself + // or that are (currently) in a known schema. DConf supports this kind of + // 'directory reset' by writing a NULL value for the non-key @path (i.e. + // which ends in a slash). However, neither g_settings_backend_reset() nor + // g_settings_backend_write() accept a non-key path, and the latter + // doesn't accept NULL values anyway. g_settings_backend_write_tree() + // does allow NULL values, and the DConf backend works fine with this and + // performs the directory reset, however it also (as is a documented + // requirement) calls g_settings_backend_changed_tree() which chokes on + // such a tree containing a non-key path. + // + // We could: + // 1. Just do nothing, i.e. leave the deleted settings lying around. + // 2. Fix glib. However, getting any improvements to gsettings into glib + // seems almost impossible at this point. + // 3. Interpose a fixed g_settings_backend_changed_tree() that works + // with these non-key paths. This will work with out-of-tree + // settings backends like DConf. However, this will *not* work with + // the settings backends inside libgio, like the memory and keyfile + // backends, due to -Bsymbolic_functions. + // 4. At least reset those keys we know might exists, i.e. those in + // the schema. + // + // Since I don't like 1, 2 is impossible, and 3 is too hacky, let's at least + // do 4. + +#if 0 + // This is how this function would ideally work if glib was fixed (option 2 above) + auto tree = terminal_g_settings_backend_create_tree(); + g_tree_insert(tree, g_strdup(path), nullptr); + auto const tag = &backend; + auto const r = terminal_g_settings_backend_write_tree(backend, tree, tag); + g_tree_unref(tree); +#endif + + gs_unref_settings_schema auto schema = + g_settings_schema_source_lookup(schema_source, schema_id, true); + if (schema == nullptr) [[unlikely]] // This shouldn't really happen ever + return false; + + auto tree = terminal_g_settings_backend_create_tree(); + gs_strfreev auto keys = g_settings_schema_list_keys(schema); + + for (auto i = 0; keys[i]; ++i) { + g_tree_insert(tree, + g_strconcat(path, keys[i], nullptr), // transfer + nullptr); // reset key + } + + auto const tag = &backend; + auto const r = terminal_g_settings_backend_write_tree(backend, tree, tag); + g_tree_unref(tree); + return r; +} + +#endif /* TERMINAL_SERVER || TERMINAL_PREFERENCES */ + +#define TERMINAL_SCHEMA_VERIFIER_ERROR (g_quark_from_static_string("TerminalSchemaVerifier")) + +typedef enum { + TERMINAL_SCHEMA_VERIFIER_SCHEMA_MISSING, + TERMINAL_SCHEMA_VERIFIER_SCHEMA_PATH, + TERMINAL_SCHEMA_VERIFIER_KEY_MISSING, + TERMINAL_SCHEMA_VERIFIER_KEY_TYPE, + TERMINAL_SCHEMA_VERIFIER_KEY_DEFAULT, + TERMINAL_SCHEMA_VERIFIER_KEY_RANGE_TYPE, + TERMINAL_SCHEMA_VERIFIER_KEY_RANGE, + TERMINAL_SCHEMA_VERIFIER_KEY_RANGE_TYPE_UNKNOWN, + TERMINAL_SCHEMA_VERIFIER_KEY_RANGE_TYPE_MISMATCH, + TERMINAL_SCHEMA_VERIFIER_KEY_RANGE_ENUM_VALUE, + TERMINAL_SCHEMA_VERIFIER_KEY_RANGE_INTERVAL, + TERMINAL_SCHEMA_VERIFIER_CHILD_MISSING, +} TerminalSchemaVerifierError; + +static gboolean +strv_contains(char const* const* strv, + char const* str) +{ + if (strv == nullptr) + return FALSE; + + for (size_t i = 0; strv[i]; i++) { + if (g_str_equal (strv[i], str)) + return TRUE; + } + + return FALSE; +} + +static gboolean +schema_key_range_compatible(GSettingsSchema* source_schema, + GSettingsSchemaKey* source_key, + char const* key, + GSettingsSchemaKey* reference_key, + GError** error) +{ + gs_unref_variant GVariant* source_range = + g_settings_schema_key_get_range(source_key); + gs_unref_variant GVariant* reference_range = + g_settings_schema_key_get_range(reference_key); + + char const* source_type = nullptr; + gs_unref_variant GVariant* source_data = nullptr; + g_variant_get(source_range, "(&sv)", &source_type, &source_data); + + char const* reference_type = nullptr; + gs_unref_variant GVariant* reference_data = nullptr; + g_variant_get(reference_range, "(&sv)", &reference_type, &reference_data); + + if (!g_str_equal(source_type, reference_type)) { + g_set_error(error, TERMINAL_SCHEMA_VERIFIER_ERROR, + TERMINAL_SCHEMA_VERIFIER_KEY_RANGE_TYPE, + "Schema \"%s\" key \"%s\" has range type \"%s\" but reference range type is \"%s\"", + g_settings_schema_get_id(source_schema), + key, source_type, reference_type); + return FALSE; + } + + if (g_str_equal(reference_type, "type")) + ; /* no constraints; this is fine */ + else if (g_str_equal(reference_type, "enum")) { + size_t source_values_len = 0; + gs_free char const** source_values = g_variant_get_strv(source_data, &source_values_len); + + size_t reference_values_len = 0; + gs_free char const** reference_values = g_variant_get_strv(reference_data, &reference_values_len); + + /* Check that every enum value in source is valid according to the reference */ + for (size_t i = 0; i < source_values_len; ++i) { + if (!strv_contains(reference_values, source_values[i])) { + g_set_error(error, TERMINAL_SCHEMA_VERIFIER_ERROR, + TERMINAL_SCHEMA_VERIFIER_KEY_RANGE_ENUM_VALUE, + "Schema \"%s\" key \"%s\" has enum value \"%s\" not in reference schema", + g_settings_schema_get_id(source_schema), + key, source_values[i]); + return FALSE; + } + } + } else if (g_str_equal(reference_type, "flags")) { + /* Our schemas don't use flags. If that changes, need to implement this! */ + g_assert_not_reached(); + } else if (g_str_equal(reference_type, "range")) { + if (!g_variant_is_of_type(source_data, + g_variant_get_type(reference_data))) { + char const* source_type_str = g_variant_get_type_string(source_data); + char const* reference_type_str = g_variant_get_type_string(reference_data); + g_set_error(error, TERMINAL_SCHEMA_VERIFIER_ERROR, + TERMINAL_SCHEMA_VERIFIER_KEY_RANGE_TYPE_MISMATCH, + "Schema \"%s\" key \"%s\" has range type \"%s\" but reference range type is \"%s\"", + g_settings_schema_get_id(source_schema), + key, source_type_str, reference_type_str); + return FALSE; + } + + gs_unref_variant GVariant* reference_min = nullptr; + gs_unref_variant GVariant* reference_max = nullptr; + g_variant_get(reference_data, "(**)", &reference_min, &reference_max); + + gs_unref_variant GVariant* source_min = nullptr; + gs_unref_variant GVariant* source_max = nullptr; + g_variant_get(source_data, "(**)", &source_min, &source_max); + + /* The source interval must be contained within the reference interval */ + if (g_variant_compare(source_min, reference_min) < 0 || + g_variant_compare(source_max, reference_max) > 0) { + g_set_error(error, TERMINAL_SCHEMA_VERIFIER_ERROR, + TERMINAL_SCHEMA_VERIFIER_KEY_RANGE_INTERVAL, + "Schema \"%s\" key \"%s\" has range interval not contained in reference range interval", + g_settings_schema_get_id(source_schema), key); + return FALSE; + } + } else { + g_set_error(error, TERMINAL_SCHEMA_VERIFIER_ERROR, + TERMINAL_SCHEMA_VERIFIER_KEY_RANGE_TYPE_UNKNOWN, + "Schema \"%s\" key \"%s\" has unknown range type \"%s\"", + g_settings_schema_get_id(source_schema), + key, reference_type); + return FALSE; + } + + return TRUE; +} + +static gboolean +schema_verify_key(GSettingsSchema* source_schema, + char const* key, + GSettingsSchema* reference_schema, + GError** error) +{ + if (!g_settings_schema_has_key(source_schema, key)) { + g_set_error(error, TERMINAL_SCHEMA_VERIFIER_ERROR, + TERMINAL_SCHEMA_VERIFIER_KEY_MISSING, + "Schema \"%s\" has missing key \"%s\"", + g_settings_schema_get_id(source_schema), key); + return FALSE; + } + + gs_unref_settings_schema_key GSettingsSchemaKey* source_key = + g_settings_schema_get_key(source_schema, key); + g_assert_nonnull(source_key); + + gs_unref_settings_schema_key GSettingsSchemaKey* reference_key = + g_settings_schema_get_key(reference_schema, key); + g_assert_nonnull(reference_key); + + GVariantType const* source_type = g_settings_schema_key_get_value_type(source_key); + GVariantType const* reference_type = g_settings_schema_key_get_value_type(reference_key); + if (!g_variant_type_equal(source_type, reference_type)) { + gs_free char* source_type_str = g_variant_type_dup_string(source_type); + gs_free char* reference_type_str = g_variant_type_dup_string(reference_type); + g_set_error(error, TERMINAL_SCHEMA_VERIFIER_ERROR, + TERMINAL_SCHEMA_VERIFIER_KEY_TYPE, + "Schema \"%s\" has type \"%s\" but reference type is \"%s\"", + g_settings_schema_get_id(source_schema), + source_type_str, reference_type_str); + return FALSE; + } + + gs_unref_variant GVariant* source_default = g_settings_schema_key_get_default_value(source_key); + if (!g_settings_schema_key_range_check(reference_key, source_default)) { + gs_free char* source_value_str = g_variant_print(source_default, TRUE); + g_set_error(error, TERMINAL_SCHEMA_VERIFIER_ERROR, + TERMINAL_SCHEMA_VERIFIER_KEY_DEFAULT, + "Schema \"%s\" default value \"%s\" does not conform to reference schema", + g_settings_schema_get_id(source_schema), source_value_str); + return FALSE; + } + + if (!schema_key_range_compatible(source_schema, + source_key, + key, + reference_key, + error)) + return FALSE; + + return TRUE; +} + +static gboolean +schema_verify_child(GSettingsSchema* source_schema, + char const* child_name, + GSettingsSchema* reference_schema, + GError** error) +{ + /* Should verify the child's schema ID is as expected and exists in + * the source, but there appears to be no API to get the schema ID of + * the child. + * + * We work around this missing verification by never calling + * g_settings_get_child() and instead always constructing the child + * GSettings directly; and the existence and correctness of that + * schema is verified by the per-schema checks. + */ + + return TRUE; +} + +static gboolean +schema_verify(GSettingsSchema* source_schema, + GSettingsSchema* reference_schema, + GError** error) +{ + /* Verify path */ + char const* source_path = g_settings_schema_get_path(source_schema); + char const* reference_path = g_settings_schema_get_path(reference_schema); + if (g_strcmp0(source_path, reference_path) != 0) { + g_set_error(error, TERMINAL_SCHEMA_VERIFIER_ERROR, + TERMINAL_SCHEMA_VERIFIER_SCHEMA_PATH, + "Schema \"%s\" has path \"%s\" but reference path is \"%s\"", + g_settings_schema_get_id(source_schema), + source_path ? source_path : "(null)", + reference_path ? reference_path : "(null)"); + return FALSE; + } + + /* Verify keys */ + gs_strfreev char** keys = g_settings_schema_list_keys(reference_schema); + if (keys) { + for (int i = 0; keys[i]; ++i) { + if (!schema_verify_key(source_schema, + keys[i], + reference_schema, + error)) + return FALSE; + } + } + + /* Verify child schemas */ + gs_strfreev char** source_children = g_settings_schema_list_children(source_schema); + gs_strfreev char** reference_children = g_settings_schema_list_children(reference_schema); + if (reference_children) { + for (size_t i = 0; reference_children[i]; ++i) { + if (!strv_contains((char const* const*)source_children, reference_children[i])) { + g_set_error(error, TERMINAL_SCHEMA_VERIFIER_ERROR, + TERMINAL_SCHEMA_VERIFIER_CHILD_MISSING, + "Schema \"%s\" has missing child \"%s\"", + g_settings_schema_get_id(source_schema), + reference_children[i]); + return FALSE; + } + + if (!schema_verify_child(source_schema, + reference_children[i], + reference_schema, + error)) + return FALSE; + } + } + + return TRUE; +} + +static gboolean +schemas_source_verify_schema_by_name(GSettingsSchemaSource* source, + char const* schema_name, + GSettingsSchemaSource* reference_source, + GError** error) +{ + gs_unref_settings_schema GSettingsSchema* source_schema = + g_settings_schema_source_lookup(source, schema_name, TRUE /* recursive */); + + if (!source_schema) { + g_set_error(error, TERMINAL_SCHEMA_VERIFIER_ERROR, + TERMINAL_SCHEMA_VERIFIER_SCHEMA_MISSING, + "Schema \"%s\" is missing", schema_name); + return FALSE; + } + + gs_unref_settings_schema GSettingsSchema* reference_schema = + g_settings_schema_source_lookup(reference_source, + schema_name, + FALSE /* recursive */); + g_assert_nonnull(reference_schema); + + return schema_verify(source_schema, + reference_schema, + error); +} + +static gboolean +schemas_source_verify_schemas(GSettingsSchemaSource* source, + char const* const* schemas, + GSettingsSchemaSource* reference_source, + GError** error) +{ + if (!schemas) + return TRUE; + + for (int i = 0; schemas[i]; ++i) { + if (!schemas_source_verify_schema_by_name(source, + schemas[i], + reference_source, + error)) + return FALSE; + } + + return TRUE; +} + +static gboolean +schemas_source_verify(GSettingsSchemaSource* source, + GSettingsSchemaSource* reference_source, + GError** error) +{ + gs_strfreev char** reloc_schemas = nullptr; + gs_strfreev char** nonreloc_schemas = nullptr; + + g_settings_schema_source_list_schemas(reference_source, + FALSE /* recursive */, + &reloc_schemas, + &nonreloc_schemas); + + if (!schemas_source_verify_schemas(source, + (char const* const*)reloc_schemas, + reference_source, + error)) + return FALSE; + + if (!schemas_source_verify_schemas(source, + (char const* const*)nonreloc_schemas, + reference_source, + error)) + return FALSE; + + return TRUE; +} + +GSettingsSchemaSource* +terminal_g_settings_schema_source_get_default(void) +{ + GSettingsSchemaSource* default_source = g_settings_schema_source_get_default(); + + gs_free auto schema_dir = + terminal_client_get_directory_uninstalled( +#if defined(TERMINAL_SERVER) + TERM_LIBEXECDIR, +#elif defined(TERMINAL_PREFERENCES) + TERM_LIBEXECDIR, +#elif defined(TERMINAL_CLIENT) + TERM_BINDIR, +#else +#error Need to define installed location +#endif + TERM_PKGLIBDIR, + "gschemas.compiled", + GFileTest(0)); + + gs_free_error GError* error = nullptr; + GSettingsSchemaSource* reference_source = + g_settings_schema_source_new_from_directory(schema_dir, + nullptr /* parent source */, + FALSE /* trusted */, + &error); + if (!reference_source) { + /* Can only use the installed schemas, or abort here. */ + g_printerr("Failed to load reference schemas: %s\n" + "Using unverified installed schemas.\n", + error->message); + + return g_settings_schema_source_ref(default_source); + } + + if (!schemas_source_verify(default_source, reference_source, &error)) { + g_printerr("Installed schemas failed verification: %s\n" + "Falling back to built-in reference schemas.\n", + error->message); + + return reference_source; /* transfer */ + } + + /* Installed schemas verified; use them. */ + g_settings_schema_source_unref(reference_source); + return g_settings_schema_source_ref(default_source); +} + +// BEGIN copied from glib/gio/gsettingsbackend.c + +/* + * Copyright © 2009, 2010 Codethink Limited + * Copyright © 2010 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see <http://www.gnu.org/licenses/>. + * + * Authors: Ryan Lortie <desrt@desrt.ca> + * Matthias Clasen <mclasen@redhat.com> + */ + +static void +variant_unref0(void* data) +{ + if (data) + g_variant_unref(reinterpret_cast<GVariant*>(data)); +} + +static int +compare_string(void const* a, + void const* b, + void* closure) +{ + return strcmp(reinterpret_cast<char const*>(a), + reinterpret_cast<char const*>(b)); +} + +/* + * terminal_g_settings_backend_create_tree: + * + * This is a convenience function for creating a tree that is compatible + * with terminal_g_settings_backend_write(). It merely calls g_tree_new_full() + * with strcmp(), g_free() and g_variant_unref(). + * + * Returns: (transfer full): a new #GTree + */ +GTree* +terminal_g_settings_backend_create_tree(void) +{ + return g_tree_new_full(compare_string, nullptr, + g_free, + variant_unref0); +} + +#ifdef ENABLE_DEBUG + +static gboolean +print_tree(void* key, + void* value, + void* closure) +{ + g_printerr(" %s => %s\n", + reinterpret_cast<char const*>(key), + value ? g_variant_print(reinterpret_cast<GVariant*>(value), true): "(null)"); + + return false; // continue +} + +void +terminal_g_settings_backend_print_tree(GTree* tree) +{ + g_printerr("Settings tree: [\n"); + g_tree_foreach(tree, print_tree, nullptr); + g_printerr("]\n"); +} + +#endif /* ENABLE_DEBUG */ + +#if defined(TERMINAL_SERVER) || defined(TERMINAL_PREFERENCES) + +/* + * g_settings_backend_read: + * @backend: a #GSettingsBackend implementation + * @key: the key to read + * @expected_type: a #GVariantType + * @default_value: if the default value should be returned + * + * Reads a key. This call will never block. + * + * If the key exists, the value associated with it will be returned. + * If the key does not exist, %nullptr will be returned. + * + * The returned value will be of the type given in @expected_type. If + * the backend stored a value of a different type then %nullptr will be + * returned. + * + * If @default_value is %TRUE then this gets the default value from the + * backend (ie: the one that the backend would contain if + * g_settings_reset() were called). + * + * Returns: (nullable) (transfer full): the value that was read, or %nullptr + */ +GVariant* +terminal_g_settings_backend_read(GSettingsBackend* backend, + char const* key, + GVariantType const* expected_type, + gboolean default_value) +{ + auto value = G_SETTINGS_BACKEND_GET_CLASS(backend)->read (backend, + key, + expected_type, + default_value); + + if (value) + value = g_variant_take_ref(value); + + if (value && !g_variant_is_of_type(value, expected_type)) [[unlikely]] { + g_clear_pointer(&value, g_variant_unref); + } + + return value; +} + +/* + * terminal_g_settings_backend_read_user_value: + * @backend: a #GSettingsBackend implementation + * @key: the key to read + * @expected_type: a #GVariantType + * + * Reads the 'user value' of a key. + * + * This is the value of the key that the user has control over and has + * set for themselves. Put another way: if the user did not set the + * value for themselves, then this will return %nullptr(even if the + * sysadmin has provided a default value). + * + * Returns:(nullable)(transfer full): the value that was read, or %nullptr + */ +GVariant* +terminal_g_settings_backend_read_user_value(GSettingsBackend* backend, + char const*key, + GVariantType const* expected_type) +{ + auto value = G_SETTINGS_BACKEND_GET_CLASS(backend)->read_user_value(backend, + key, + expected_type); + + if (value) + value = g_variant_take_ref(value); + + if (value && !g_variant_is_of_type(value, expected_type)) [[unlikely]] { + g_clear_pointer(&value, g_variant_unref); + } + + return value; +} + +/* + * terminal_g_settings_backend_write: + * @backend: a #GSettingsBackend implementation + * @key: the name of the key + * @value: a #GVariant value to write to this key + * @origin_tag: the origin tag + * + * Writes exactly one key. + * + * This call does not fail. During this call a + * #GSettingsBackend::changed signal will be emitted if the value of the + * key has changed. The updated key value will be visible to any signal + * callbacks. + * + * One possible method that an implementation might deal with failures is + * to emit a second "changed" signal(either during this call, or later) + * to indicate that the affected keys have suddenly "changed back" to their + * old values. + * + * If @value has a floating reference, it will be sunk. + * + * Returns: %TRUE if the write succeeded, %FALSE if the key was not writable + */ +gboolean +terminal_g_settings_backend_write(GSettingsBackend* backend, + char const* key, + GVariant* value, + void* origin_tag) +{ + g_variant_ref_sink(value); + auto const success = G_SETTINGS_BACKEND_GET_CLASS(backend)->write(backend, + key, + value, + origin_tag); + g_variant_unref(value); + + return success; +} + +/* + * terminal_g_settings_backend_write_tree: + * @backend: a #GSettingsBackend implementation + * @tree: a #GTree containing key-value pairs to write + * @origin_tag: the origin tag + * + * Writes one or more keys. This call will never block. + * + * The key of each item in the tree is the key name to write to and the + * value is a #GVariant to write. The proper type of #GTree for this + * call can be created with terminal_g_settings_backend_create_tree(). This call + * might take a reference to the tree; you must not modified the #GTree + * after passing it to this call. + * + * This call does not fail. During this call a #GSettingsBackend::changed + * signal will be emitted if any keys have been changed. The new values of + * all updated keys will be visible to any signal callbacks. + * + * One possible method that an implementation might deal with failures is + * to emit a second "changed" signal(either during this call, or later) + * to indicate that the affected keys have suddenly "changed back" to their + * old values. + */ +gboolean +terminal_g_settings_backend_write_tree(GSettingsBackend* backend, + GTree* tree, + void* origin_tag) +{ + return G_SETTINGS_BACKEND_GET_CLASS(backend)->write_tree(backend, + tree, + origin_tag); +} + +/* + * terminal_g_settings_backend_reset: + * @backend: a #GSettingsBackend implementation + * @key: the name of a key + * @origin_tag: the origin tag + * + * "Resets" the named key to its "default" value(ie: after system-wide + * defaults, mandatory keys, etc. have been taken into account) or possibly + * unsets it. + */ +void +terminal_g_settings_backend_reset(GSettingsBackend*backend, + char const* key, + void* origin_tag) +{ + G_SETTINGS_BACKEND_GET_CLASS(backend)->reset(backend, key, origin_tag); +} + +/* + * terminal_g_settings_backend_get_writable: + * @backend: a #GSettingsBackend implementation + * @key: the name of a key + * + * Finds out if a key is available for writing to. This is the + * interface through which 'lockdown' is implemented. Locked down + * keys will have %FALSE returned by this call. + * + * You should not write to locked-down keys, but if you do, the + * implementation will deal with it. + * + * Returns: %TRUE if the key is writable + */ +gboolean +terminal_g_settings_backend_get_writable(GSettingsBackend* backend, + char const* key) +{ + return G_SETTINGS_BACKEND_GET_CLASS(backend)->get_writable(backend, key); +} + +/* + * terminal_g_settings_backend_unsubscribe: + * @backend: a #GSettingsBackend + * @name: a key or path to subscribe to + * + * Reverses the effect of a previous call to + * terminal_g_settings_backend_subscribe(). + */ +void +terminal_g_settings_backend_unsubscribe(GSettingsBackend* backend, + const char* name) +{ + G_SETTINGS_BACKEND_GET_CLASS(backend)->unsubscribe(backend, name); +} + +/* + * terminal_g_settings_backend_subscribe: + * @backend: a #GSettingsBackend + * @name: a key or path to subscribe to + * + * Requests that change signals be emitted for events on @name. + */ +void +terminal_g_settings_backend_subscribe(GSettingsBackend* backend, + char const* name) +{ + G_SETTINGS_BACKEND_GET_CLASS(backend)->subscribe(backend, name); +} + +/* + * terminal_g_settings_backend_get_permission: + * @backend: a #GSettingsBackend + * @path: a path + * + * Gets the permission object associated with writing to keys below + * @path on @backend. + * + * If this is not implemented in the backend, then a %TRUE + * #GSimplePermission is returned. + * + * Returns:(not nullable)(transfer full): a non-%nullptr #GPermission. + * Free with g_object_unref() + */ +GPermission* +terminal_g_settings_backend_get_permission(GSettingsBackend* backend, + char const* path) +{ + auto const klass = G_SETTINGS_BACKEND_GET_CLASS(backend); + if (klass->get_permission) + return klass->get_permission(backend, path); + + return g_simple_permission_new(TRUE); +} + +/* + * terminal_g_settings_backend_sync_default: + * + * Syncs. + */ +void +terminal_g_settings_backend_sync(GSettingsBackend* backend) +{ + auto const klass = G_SETTINGS_BACKEND_GET_CLASS(backend); + if (klass->sync) + klass->sync(backend); +} + +// END copied from glib + +// Note: Since D-Bus/GDBus does not support GVariant maybe types (not even +// on private peer-to-peer connections), we need to wrap the variants +// for transport over the bus. The format is a "mv" variant whose inner +// value is the variant to transport, or Nothing for a nullptr GVariant. +// We then transport that variant in serialised form as a byte array over +// the bus. + +/* + * terminal_g_variant_wrap: + * @variant: (nullable): a #GVariant, or %NULL + * + * Wraps @variant for transport over D-Bus. + * if @variant is floating, it is consumed. + * + * Returns: (transfer full): a floating variant wrapping @variant + */ +GVariant* +terminal_g_variant_wrap(GVariant* variant) +{ + auto const value = variant ? g_variant_new_variant(variant) : nullptr; + auto const maybe = g_variant_new_maybe(G_VARIANT_TYPE_VARIANT, value); + + return g_variant_new_from_data(G_VARIANT_TYPE("ay"), + g_variant_get_data(maybe), + g_variant_get_size(maybe), + false, // trusted + GDestroyNotify(g_variant_unref), + g_variant_ref_sink(maybe)); // adopts +} + +/* + * terminal_g_variant_unwrap: + * @variant: a "ay" #GVariant + * + * Unwraps a variant transported over D-Bus. + * If @variant is floating, it is NOT consumed. + * + * Returns: (transfer full): a non-floating variant unwrapping @variant + */ +GVariant* +terminal_g_variant_unwrap(GVariant* variant) +{ + g_return_val_if_fail(g_variant_is_of_type(variant, G_VARIANT_TYPE("ay")), nullptr); + + gs_unref_bytes auto bytes = g_variant_get_data_as_bytes(variant); + gs_unref_variant auto maybe = g_variant_take_ref(g_variant_new_from_bytes(G_VARIANT_TYPE("mv"), bytes, false)); + gs_unref_variant auto value = g_variant_get_maybe(maybe); + return value ? g_variant_get_variant(value) : nullptr; +} + + +#endif /* TERMINAL_SERVER || TERMINAL_PREFERENCES */ diff --git a/src/terminal-settings-utils.hh b/src/terminal-settings-utils.hh new file mode 100644 index 0000000..b34afb3 --- /dev/null +++ b/src/terminal-settings-utils.hh @@ -0,0 +1,114 @@ +/* + * Copyright © 2008, 2010, 2022 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/>. + */ + +#pragma once + +#include <gio/gio.h> + +GSettings* terminal_g_settings_new (GSettingsBackend* backend, + GSettingsSchemaSource* source, + char const* schema_id); + +GSettings* terminal_g_settings_new_with_path (GSettingsBackend* backend, + GSettingsSchemaSource* source, + char const* schema_id, + char const* path); + +void terminal_g_settings_backend_clone_schema(GSettingsBackend* backend, + GSettingsSchemaSource*schema_source, + char const* schema_id, + char const* path, + char const* new_path, + GTree* tree); + +gboolean terminal_g_settings_backend_erase_path(GSettingsBackend* backend, + GSettingsSchemaSource* schema_source, + char const* schema_id, + char const* path); + +GTree* terminal_g_settings_backend_create_tree(void); + +void terminal_g_settings_backend_print_tree(GTree* tree); + +GSettingsSchemaSource* terminal_g_settings_schema_source_get_default(void); + +GTree* terminal_g_settings_backend_create_tree(void); + +// BEGIN copied from glib/gio/gsettingsbackendinternal.h + +/* + * Copyright © 2009, 2010 Codethink Limited + * Copyright © 2010 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see <http://www.gnu.org/licenses/>. + * + * Authors: Ryan Lortie <desrt@desrt.ca> + * Matthias Clasen <mclasen@redhat.com> + */ + +GPermission* terminal_g_settings_backend_get_permission(GSettingsBackend* backend, + char const*path); + +gboolean terminal_g_settings_backend_get_writable(GSettingsBackend* backend, + const char* key); + +GVariant* terminal_g_settings_backend_read(GSettingsBackend* backend, + char const* key, + GVariantType const* expected_type, + gboolean default_value); + +GVariant* terminal_g_settings_backend_read_user_value(GSettingsBackend* backend, + char const* key, + GVariantType const* expected_type); + +void terminal_g_settings_backend_reset(GSettingsBackend* backend, + char const* key, + void* origin_tag); + +void terminal_g_settings_backend_subscribe(GSettingsBackend* backend, + const char* name); + +void terminal_g_settings_backend_sync(GSettingsBackend* backend); + +void terminal_g_settings_backend_unsubscribe(GSettingsBackend* backend, + const char* name); + +gboolean terminal_g_settings_backend_write(GSettingsBackend* backend, + char const* key, + GVariant* value, + void* origin_tag); + +gboolean terminal_g_settings_backend_write_tree(GSettingsBackend* backend, + GTree* tree, + void* origin_tag); + +// END copied from glib + +GVariant* terminal_g_variant_wrap(GVariant* variant); + +GVariant* terminal_g_variant_unwrap(GVariant* variant); diff --git a/src/terminal-tab-label.cc b/src/terminal-tab-label.cc new file mode 100644 index 0000000..88af5d4 --- /dev/null +++ b/src/terminal-tab-label.cc @@ -0,0 +1,395 @@ +/* + * Copyright © 2001 Havoc Pennington + * Copyright © 2007, 2008 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 <glib.h> +#include <glib/gi18n.h> +#include <gtk/gtk.h> + +#include "terminal-intl.hh" +#include "terminal-tab-label.hh" +#include "terminal-icon-button.hh" +#include "terminal-window.hh" + +#define TERMINAL_TAB_LABEL_GET_PRIVATE(tab_label)(G_TYPE_INSTANCE_GET_PRIVATE ((tab_label), TERMINAL_TYPE_TAB_LABEL, TerminalTabLabelPrivate)) + +#define SPACING (4) + +struct _TerminalTabLabelPrivate +{ + TerminalScreen *screen; + GtkWidget *label; + GtkWidget *close_button; + gboolean bold; + GtkPositionType tab_pos; +}; + +enum +{ + PROP_0, + PROP_SCREEN +}; + +enum +{ + CLOSE_BUTTON_CLICKED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; + +G_DEFINE_TYPE (TerminalTabLabel, terminal_tab_label, GTK_TYPE_BOX); + +/* helper functions */ + +static void +close_button_clicked_cb (GtkWidget *widget, + TerminalTabLabel *tab_label) +{ + g_signal_emit (tab_label, signals[CLOSE_BUTTON_CLICKED], 0); +} + +static void +sync_tab_label (TerminalScreen *screen, + GParamSpec *pspec, + GtkWidget *label) +{ + GtkWidget *hbox; + const char *title; + TerminalWindow *window; + + title = terminal_screen_get_title (screen); + hbox = gtk_widget_get_parent (label); + + gtk_label_set_text (GTK_LABEL (label), + title && title[0] ? title : _("Terminal")); + + gtk_widget_set_tooltip_text (hbox, title); + + /* This call updates the window size: bug 732588. + * FIXMEchpe: This is probably a GTK+ bug, should get them fix it. + */ + window = TERMINAL_WINDOW (gtk_widget_get_ancestor (GTK_WIDGET (label), + TERMINAL_TYPE_WINDOW)); + if (window != nullptr) + terminal_window_update_size (window); +} + +static void +notify_tab_pos_cb (GtkNotebook *notebook, + GParamSpec *pspec G_GNUC_UNUSED, + TerminalTabLabel *label) +{ + TerminalTabLabelPrivate *priv = label->priv; + GtkPositionType pos; + + pos = gtk_notebook_get_tab_pos (notebook); + if (pos == priv->tab_pos) + return; + + priv->tab_pos = pos; + + switch (pos) { + case GTK_POS_LEFT: + case GTK_POS_RIGHT: + gtk_widget_hide (priv->close_button); + break; + case GTK_POS_TOP: + case GTK_POS_BOTTOM: + gtk_widget_show (priv->close_button); + break; + } +} + +/* public functions */ + +/* Class implementation */ + +static void +terminal_tab_label_parent_set (GtkWidget *widget, + GtkWidget *old_parent) +{ + GtkWidget *parent; + void (* parent_set) (GtkWidget *, GtkWidget *) = GTK_WIDGET_CLASS (terminal_tab_label_parent_class)->parent_set; + + if (GTK_IS_NOTEBOOK (old_parent)) { + g_signal_handlers_disconnect_by_func (old_parent, + (void*)notify_tab_pos_cb, + widget); + } + + if (parent_set) + parent_set (widget, old_parent); + + parent = gtk_widget_get_parent (widget); + if (GTK_IS_NOTEBOOK (parent)) { + notify_tab_pos_cb (GTK_NOTEBOOK (parent), nullptr, TERMINAL_TAB_LABEL (widget)); + g_signal_connect (parent, "notify::tab-pos", + G_CALLBACK (notify_tab_pos_cb), widget); + } +} + +static void +terminal_tab_label_get_preferred_width (GtkWidget *widget, + int *minimum_width, + int *natural_width) +{ + TerminalTabLabel *tab_label = TERMINAL_TAB_LABEL (widget); + TerminalTabLabelPrivate *priv = tab_label->priv; + + if (priv->tab_pos == GTK_POS_LEFT || + priv->tab_pos == GTK_POS_RIGHT) { + if (natural_width) + *natural_width = 160; + if (minimum_width) + *minimum_width = 160; + } + else + GTK_WIDGET_CLASS (terminal_tab_label_parent_class)->get_preferred_width (widget, minimum_width, natural_width); +} + +static void +terminal_tab_label_init (TerminalTabLabel *tab_label) +{ + TerminalTabLabelPrivate *priv; + + priv = tab_label->priv = TERMINAL_TAB_LABEL_GET_PRIVATE (tab_label); + + priv->tab_pos = (GtkPositionType) -1; /* invalid */ +} + +static void +terminal_tab_label_constructed (GObject *object) +{ + TerminalTabLabel *tab_label = TERMINAL_TAB_LABEL (object); + TerminalTabLabelPrivate *priv = tab_label->priv; + GtkWidget *hbox, *label, *close_button; + + G_OBJECT_CLASS (terminal_tab_label_parent_class)->constructed (object); + + hbox = GTK_WIDGET (tab_label); + + g_assert (priv->screen != nullptr); + + gtk_box_set_spacing (GTK_BOX (hbox), SPACING); + + priv->label = label = gtk_label_new (nullptr); + gtk_widget_set_halign (label, GTK_ALIGN_CENTER); + gtk_widget_set_valign (label, GTK_ALIGN_BASELINE); + gtk_widget_set_margin_start (label, 0); + gtk_widget_set_margin_end (label, 0); + gtk_widget_set_margin_top (label, 0); + gtk_widget_set_margin_bottom (label, 0); + + gtk_label_set_ellipsize (GTK_LABEL (label), PANGO_ELLIPSIZE_END); + gtk_label_set_single_line_mode (GTK_LABEL (label), TRUE); + + gtk_box_set_center_widget (GTK_BOX (hbox), label); + + priv->close_button = close_button = terminal_close_button_new (); + gtk_widget_set_tooltip_text (close_button, _("Close tab")); + gtk_box_pack_end (GTK_BOX (hbox), close_button, FALSE, FALSE, 0); + + sync_tab_label (priv->screen, nullptr, label); + g_signal_connect (priv->screen, "notify::title", + G_CALLBACK (sync_tab_label), label); + + g_signal_connect (close_button, "clicked", + G_CALLBACK (close_button_clicked_cb), tab_label); + + gtk_widget_show_all (hbox); +} + +static void +terminal_tab_label_dispose (GObject *object) +{ + TerminalTabLabel *tab_label = TERMINAL_TAB_LABEL (object); + TerminalTabLabelPrivate *priv = tab_label->priv; + + if (priv->screen != nullptr) { + g_signal_handlers_disconnect_by_func (priv->screen, + (void*)sync_tab_label, + priv->label); + g_object_unref (priv->screen); + priv->screen = nullptr; + } + + G_OBJECT_CLASS (terminal_tab_label_parent_class)->dispose (object); +} + +static void +terminal_tab_label_finalize (GObject *object) +{ +// TerminalTabLabel *tab_label = TERMINAL_TAB_LABEL (object); + + G_OBJECT_CLASS (terminal_tab_label_parent_class)->finalize (object); +} + +static void +terminal_tab_label_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + TerminalTabLabel *tab_label = TERMINAL_TAB_LABEL (object); + + switch (prop_id) { + case PROP_SCREEN: + g_value_set_object (value, terminal_tab_label_get_screen (tab_label)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +terminal_tab_label_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + TerminalTabLabel *tab_label = TERMINAL_TAB_LABEL (object); + TerminalTabLabelPrivate *priv = tab_label->priv; + + switch (prop_id) { + case PROP_SCREEN: + priv->screen = (TerminalScreen*)g_value_dup_object (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +terminal_tab_label_class_init (TerminalTabLabelClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + gobject_class->constructed = terminal_tab_label_constructed; + gobject_class->dispose = terminal_tab_label_dispose; + gobject_class->finalize = terminal_tab_label_finalize; + gobject_class->get_property = terminal_tab_label_get_property; + gobject_class->set_property = terminal_tab_label_set_property; + + widget_class->parent_set = terminal_tab_label_parent_set; + widget_class->get_preferred_width = terminal_tab_label_get_preferred_width; + + signals[CLOSE_BUTTON_CLICKED] = + g_signal_new (I_("close-button-clicked"), + G_OBJECT_CLASS_TYPE (gobject_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (TerminalTabLabelClass, close_button_clicked), + nullptr, nullptr, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, + 0); + + g_object_class_install_property + (gobject_class, + PROP_SCREEN, + g_param_spec_object ("screen", nullptr, nullptr, + TERMINAL_TYPE_SCREEN, + GParamFlags(G_PARAM_READWRITE | + G_PARAM_STATIC_NAME | + G_PARAM_STATIC_NICK | + G_PARAM_STATIC_BLURB | + G_PARAM_CONSTRUCT_ONLY))); + + g_type_class_add_private (gobject_class, sizeof (TerminalTabLabelPrivate)); +} + +/* public API */ + +/** + * terminal_tab_label_new: + * @screen: a #TerminalScreen + * + * Returns: a new #TerminalTabLabel for @screen + */ +GtkWidget * +terminal_tab_label_new (TerminalScreen *screen) +{ + return reinterpret_cast<GtkWidget*> + (g_object_new (TERMINAL_TYPE_TAB_LABEL, + "orientation", GTK_ORIENTATION_HORIZONTAL, + "screen", screen, + nullptr)); +} + +/** + * terminal_tab_label_set_bold: + * @tab_label: a #TerminalTabLabel + * @bold: whether to enable label bolding + * + * Sets the tab label text bold, or unbolds it. + */ +void +terminal_tab_label_set_bold (TerminalTabLabel *tab_label, + gboolean bold) +{ + TerminalTabLabelPrivate *priv = tab_label->priv; + PangoAttrList *attr_list; + PangoAttribute *weight_attr; + gboolean free_list = FALSE; + + bold = bold != FALSE; + if (priv->bold == bold) + return; + + priv->bold = bold; + + attr_list = gtk_label_get_attributes (GTK_LABEL (priv->label)); + if (!attr_list) { + attr_list = pango_attr_list_new (); + free_list = TRUE; + } + + if (bold) + weight_attr = pango_attr_weight_new (PANGO_WEIGHT_BOLD); + else + weight_attr = pango_attr_weight_new (PANGO_WEIGHT_NORMAL); + + /* gtk_label_get_attributes() returns the label's internal list, + * which we're probably not supposed to modify directly. + * It seems to work ok however. + */ + pango_attr_list_change (attr_list, weight_attr); + + gtk_label_set_attributes (GTK_LABEL (priv->label), attr_list); + + if (free_list) + pango_attr_list_unref (attr_list); +} + +/** + * terminal_tab_label_get_screen: + * @tab_label: a #TerminalTabLabel + * + * Returns: (transfer none): the #TerminalScreen for @tab_label + */ +TerminalScreen * +terminal_tab_label_get_screen (TerminalTabLabel *tab_label) +{ + g_return_val_if_fail (TERMINAL_IS_TAB_LABEL (tab_label), nullptr); + + return tab_label->priv->screen; +} diff --git a/src/terminal-tab-label.hh b/src/terminal-tab-label.hh new file mode 100644 index 0000000..91b7e61 --- /dev/null +++ b/src/terminal-tab-label.hh @@ -0,0 +1,66 @@ +/* + * Copyright © 2008 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/>. + */ + +#ifndef TERMINAL_TAB_LABEL_H +#define TERMINAL_TAB_LABEL_H + +#include <gtk/gtk.h> + +#include "terminal-screen.hh" + +G_BEGIN_DECLS + +#define TERMINAL_TYPE_TAB_LABEL (terminal_tab_label_get_type ()) +#define TERMINAL_TAB_LABEL(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), TERMINAL_TYPE_TAB_LABEL, TerminalTabLabel)) +#define TERMINAL_TAB_LABEL_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), TERMINAL_TYPE_TAB_LABEL, TerminalTabLabelClass)) +#define TERMINAL_IS_TAB_LABEL(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), TERMINAL_TYPE_TAB_LABEL)) +#define TERMINAL_IS_TAB_LABEL_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), TERMINAL_TYPE_TAB_LABEL)) +#define TERMINAL_TAB_LABEL_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), TERMINAL_TYPE_TAB_LABEL, TerminalTabLabelClass)) + +typedef struct _TerminalTabLabel TerminalTabLabel; +typedef struct _TerminalTabLabelClass TerminalTabLabelClass; +typedef struct _TerminalTabLabelPrivate TerminalTabLabelPrivate; + +struct _TerminalTabLabel +{ + GtkBox parent_instance; + + /*< private >*/ + TerminalTabLabelPrivate *priv; +}; + +struct _TerminalTabLabelClass +{ + GtkBoxClass parent_class; + + /* Signals */ + void (* close_button_clicked) (TerminalTabLabel *tab_label); +}; + +GType terminal_tab_label_get_type (void); + +GtkWidget * terminal_tab_label_new (TerminalScreen *screen); + +void terminal_tab_label_set_bold (TerminalTabLabel *tab_label, + gboolean bold); + +TerminalScreen *terminal_tab_label_get_screen (TerminalTabLabel *tab_label); + +G_END_DECLS + +#endif /* !TERMINAL_TAB_LABEL_H */ diff --git a/src/terminal-type-builtins.cc.template b/src/terminal-type-builtins.cc.template new file mode 100644 index 0000000..00c0276 --- /dev/null +++ b/src/terminal-type-builtins.cc.template @@ -0,0 +1,45 @@ +/*** BEGIN file-header ***/ +#include <config.h> + +#include "terminal-type-builtins.hh" + +/*** END file-header ***/ + +/*** BEGIN file-production ***/ +/* enumerations from "@filename@" */ + +#include "@filename@" + +/*** END file-production ***/ + +/*** BEGIN value-header ***/ +GType +@enum_name@_get_type (void) +{ + static gsize g_define_type_id__ = 0; + + if (g_once_init_enter (&g_define_type_id__)) { + static const G@Type@Value values[] = { +/*** END value-header ***/ + +/*** BEGIN value-production ***/ + { @VALUENAME@, "@VALUENAME@", "@valuenick@" }, +/*** END value-production ***/ + +/*** BEGIN value-tail ***/ + { 0, nullptr, nullptr } + }; + GType g_define_type_id = \ + g_@type@_register_static (/* g_intern_static_string */ ("@EnumName@"), values); + + g_once_init_leave (&g_define_type_id__, g_define_type_id); + } + + return g_define_type_id__; +} + +/*** END value-tail ***/ + +/*** BEGIN file-tail ***/ + +/*** END file-tail ***/ diff --git a/src/terminal-type-builtins.hh.template b/src/terminal-type-builtins.hh.template new file mode 100644 index 0000000..c454d05 --- /dev/null +++ b/src/terminal-type-builtins.hh.template @@ -0,0 +1,25 @@ +/*** BEGIN file-header ***/ +#ifndef TERMINAL_TYPE_BUILTINS_H +#define TERMINAL_TYPE_BUILTINS_H + +#include <glib-object.h> + +G_BEGIN_DECLS +/*** END file-header ***/ + +/*** BEGIN file-production ***/ + +/* enumerations from "@filename@" */ +/*** END file-production ***/ + +/*** BEGIN value-header ***/ +GType @enum_name@_get_type (void); +#define TERMINAL_TYPE_@ENUMSHORT@ (@enum_name@_get_type ()) +/*** END value-header ***/ + +/*** BEGIN file-tail ***/ + +G_END_DECLS + +#endif /* !TERMINAL_TYPE_BUILTINS_H */ +/*** END file-tail ***/ 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(); +} diff --git a/src/terminal-util.hh b/src/terminal-util.hh new file mode 100644 index 0000000..3770c85 --- /dev/null +++ b/src/terminal-util.hh @@ -0,0 +1,120 @@ +/* + * Copyright © 2001 Havoc Pennington + * Copyright © 2008, 2010 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/>. + */ + +#ifndef TERMINAL_UTIL_H +#define TERMINAL_UTIL_H + +#include <gio/gio.h> +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +void terminal_util_show_error_dialog (GtkWindow *transient_parent, + GtkWidget **weap_ptr, + GError *error, + const char *message_format, ...) G_GNUC_PRINTF(4, 5); + +void terminal_util_show_help (const char *topic); + +void terminal_util_show_about (void); + +void terminal_util_set_labelled_by (GtkWidget *widget, + GtkLabel *label); +void terminal_util_set_atk_name_description (GtkWidget *widget, + const char *name, + const char *desc); + +void terminal_util_open_url (GtkWidget *parent, + const char *orig_url, + TerminalURLFlavor flavor, + guint32 user_time); + +void terminal_util_transform_uris_to_quoted_fuse_paths (char **uris); + +char *terminal_util_concat_uris (char **uris, + gsize *length); + +char *terminal_util_get_licence_text (void); + +GtkBuilder *terminal_util_load_widgets_resource (const char *path, + const char *main_object_name, + const char *object_name, + ...); + +void terminal_util_load_objects_resource (const char *path, + const char *object_name, + ...); + +void terminal_util_dialog_focus_widget (GtkBuilder *builder, + const char *widget_name); + +gboolean terminal_util_dialog_response_on_delete (GtkWindow *widget); + +void terminal_util_add_proxy_env (GHashTable *env_table); + +char **terminal_util_get_etc_shells (void); + +gboolean terminal_util_get_is_shell (const char *command); + +const GdkRGBA *terminal_g_settings_get_rgba (GSettings *settings, + const char *key, + GdkRGBA *rgba); +void terminal_g_settings_set_rgba (GSettings *settings, + const char *key, + const GdkRGBA *rgba); + +GdkRGBA *terminal_g_settings_get_rgba_palette (GSettings *settings, + const char *key, + gsize *n_colors); +void terminal_g_settings_set_rgba_palette (GSettings *settings, + const char *key, + const GdkRGBA *colors, + gsize n_colors); + +void terminal_util_bind_mnemonic_label_sensitivity (GtkWidget *widget); + +char *terminal_util_number_info (const char *str); +char *terminal_util_timestamp_info (const char *str); + +char *terminal_util_uri_fixup (const char *uri, + GError **error); + +char *terminal_util_hyperlink_uri_label (const char *str); + +void terminal_util_load_print_settings (GtkPrintSettings **settings, + GtkPageSetup **page_setup); + +void terminal_util_save_print_settings (GtkPrintSettings *settings, + GtkPageSetup *page_setup); + +const char *terminal_util_translate_encoding (const char *encoding); + +char *terminal_util_find_program_in_path (const char *path, + const char *program); + +gboolean terminal_util_check_envv(char const* const* strv); + +char** terminal_util_get_desktops(void); + +gboolean terminal_util_is_default_terminal(void); + +gboolean terminal_util_make_default_terminal(void); + +G_END_DECLS + +#endif /* TERMINAL_UTIL_H */ diff --git a/src/terminal-version.hh.in b/src/terminal-version.hh.in new file mode 100644 index 0000000..154a3a1 --- /dev/null +++ b/src/terminal-version.hh.in @@ -0,0 +1,34 @@ +/* + * Copyright © 2009 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/>. + */ + +#if !defined (__TERMINAL_TERMINAL_H_INSIDE__) && !defined (TERMINAL_COMPILATION) +#error "Only <terminal/terminal.h> can be included directly." +#endif + +#ifndef TERMINAL_VERSION_H +#define TERMINAL_VERSION_H + +#define TERMINAL_MAJOR_VERSION (@TERMINAL_MAJOR_VERSION@) +#define TERMINAL_MINOR_VERSION (@TERMINAL_MINOR_VERSION@) +#define TERMINAL_MICRO_VERSION (@TERMINAL_MICRO_VERSION@) + +#define TERMINAL_CHECK_VERSION(major,minor,micro) \ + (TERMINAL_MAJOR_VERSION > (major) || \ + (TERMINAL_MAJOR_VERSION == (major) && TERMINAL_MINOR_VERSION > (minor)) || \ + (TERMINAL_MAJOR_VERSION == (major) && TERMINAL_MINOR_VERSION == (minor) && TERMINAL_MICRO_VERSION >= (micro))) + +#endif /* !TERMINAL_VERSION_H */ diff --git a/src/terminal-window.cc b/src/terminal-window.cc new file mode 100644 index 0000000..36eb7ce --- /dev/null +++ b/src/terminal-window.cc @@ -0,0 +1,3369 @@ +/* + * Copyright © 2001 Havoc Pennington + * Copyright © 2002 Red Hat, Inc. + * Copyright © 2007, 2008, 2009, 2011, 2017 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 <string.h> +#include <stdlib.h> + +#include <glib.h> +#include <glib/gi18n.h> + +#include <gtk/gtk.h> +#include <uuid.h> + +#include "terminal-app.hh" +#include "terminal-debug.hh" +#include "terminal-enums.hh" +#include "terminal-headerbar.hh" +#include "terminal-icon-button.hh" +#include "terminal-intl.hh" +#include "terminal-mdi-container.hh" +#include "terminal-menu-button.hh" +#include "terminal-notebook.hh" +#include "terminal-schemas.hh" +#include "terminal-screen-container.hh" +#include "terminal-search-popover.hh" +#include "terminal-tab-label.hh" +#include "terminal-util.hh" +#include "terminal-window.hh" +#include "terminal-libgsystem.hh" + +struct _TerminalWindowPrivate +{ + char *uuid; + + GtkClipboard *clipboard; + + TerminalScreenPopupInfo *popup_info; + + GtkWidget *menubar; + TerminalMdiContainer *mdi_container; + GtkWidget *main_vbox; + GtkWidget* ask_default_infobar; + TerminalScreen *active_screen; + + /* Size of a character cell in pixels */ + int old_char_width; + int old_char_height; + + /* Width and height added to the actual terminal grid by "chrome" inside + * what was traditionally the X11 window: menu bar, title bar, + * style-provided padding. This must be included when resizing the window + * and also included in geometry hints. */ + int old_chrome_width; + int old_chrome_height; + + /* Width and height added to the window by client-side decorations. + * This must be included in geometry hints but must not be included when + * resizing the window. */ + int old_csd_width; + int old_csd_height; + + /* Width and height of the padding around the geometry widget. */ + int old_padding_width; + int old_padding_height; + + void *old_geometry_widget; /* only used for pointer value as it may be freed */ + + /* For restoring hints after unmaximizing etc */ + GdkGeometry hints; + GdkWindowState window_state; + + GtkWidget *confirm_close_dialog; + TerminalSearchPopover *search_popover; + + guint use_default_menubar_visibility : 1; + + guint disposed : 1; + guint present_on_insert : 1; + + guint realized : 1; +}; + +#define TERMINAL_WINDOW_CSS_NAME "terminal-window" + +#define MIN_WIDTH_CHARS 4 +#define MIN_HEIGHT_CHARS 1 + +#if 1 +/* + * We don't want to enable content saving until vte supports it async. + * So we disable this code for stable versions. + */ +#include "terminal-version.hh" + +#if (TERMINAL_MINOR_VERSION & 1) != 0 +#define ENABLE_SAVE +#else +#undef ENABLE_SAVE +#endif +#endif + +/* See bug #789356 and issue gnome-terminal#129*/ +static inline constexpr auto +window_state_is_snapped(GdkWindowState state) noexcept +{ + return (state & (GDK_WINDOW_STATE_FULLSCREEN | + GDK_WINDOW_STATE_MAXIMIZED | + GDK_WINDOW_STATE_BOTTOM_TILED | + GDK_WINDOW_STATE_LEFT_TILED | + GDK_WINDOW_STATE_RIGHT_TILED | + GDK_WINDOW_STATE_TOP_TILED | + GDK_WINDOW_STATE_TILED)) != 0; +} + +static void terminal_window_dispose (GObject *object); +static void terminal_window_finalize (GObject *object); +static gboolean terminal_window_state_event (GtkWidget *widget, + GdkEventWindowState *event); + +static gboolean terminal_window_delete_event (GtkWidget *widget, + GdkEvent *event, + gpointer data); + +static gboolean notebook_button_press_cb (GtkWidget *notebook, + GdkEventButton *event, + TerminalWindow *window); +static gboolean notebook_popup_menu_cb (GtkWidget *notebook, + TerminalWindow *window); +static void mdi_screen_switched_cb (TerminalMdiContainer *container, + TerminalScreen *old_active_screen, + TerminalScreen *screen, + TerminalWindow *window); +static void mdi_screen_added_cb (TerminalMdiContainer *container, + TerminalScreen *screen, + TerminalWindow *window); +static void mdi_screen_removed_cb (TerminalMdiContainer *container, + TerminalScreen *screen, + TerminalWindow *window); +static void mdi_screens_reordered_cb (TerminalMdiContainer *container, + TerminalWindow *window); +static void screen_close_request_cb (TerminalMdiContainer *container, + TerminalScreen *screen, + TerminalWindow *window); + +/* Menu action callbacks */ +static gboolean find_larger_zoom_factor (double *zoom); +static gboolean find_smaller_zoom_factor (double *zoom); +static void terminal_window_update_zoom_sensitivity (TerminalWindow *window); +static void terminal_window_update_search_sensitivity (TerminalScreen *screen, + TerminalWindow *window); +static void terminal_window_update_paste_sensitivity (TerminalWindow *window); + +static void terminal_window_show (GtkWidget *widget); + +static gboolean confirm_close_window_or_tab (TerminalWindow *window, + TerminalScreen *screen); + +G_DEFINE_TYPE (TerminalWindow, terminal_window, GTK_TYPE_APPLICATION_WINDOW) + +/* Zoom helpers */ + +static const double zoom_factors[] = { + TERMINAL_SCALE_MINIMUM, + TERMINAL_SCALE_XXXXX_SMALL, + TERMINAL_SCALE_XXXX_SMALL, + TERMINAL_SCALE_XXX_SMALL, + PANGO_SCALE_XX_SMALL, + PANGO_SCALE_X_SMALL, + PANGO_SCALE_SMALL, + PANGO_SCALE_MEDIUM, + PANGO_SCALE_LARGE, + PANGO_SCALE_X_LARGE, + PANGO_SCALE_XX_LARGE, + TERMINAL_SCALE_XXX_LARGE, + TERMINAL_SCALE_XXXX_LARGE, + TERMINAL_SCALE_XXXXX_LARGE, + TERMINAL_SCALE_MAXIMUM +}; + +static gboolean +find_larger_zoom_factor (double *zoom) +{ + double current = *zoom; + guint i; + + for (i = 0; i < G_N_ELEMENTS (zoom_factors); ++i) + { + /* Find a font that's larger than this one */ + if ((zoom_factors[i] - current) > 1e-6) + { + *zoom = zoom_factors[i]; + return TRUE; + } + } + + return FALSE; +} + +static gboolean +find_smaller_zoom_factor (double *zoom) +{ + double current = *zoom; + int i; + + i = (int) G_N_ELEMENTS (zoom_factors) - 1; + while (i >= 0) + { + /* Find a font that's smaller than this one */ + if ((current - zoom_factors[i]) > 1e-6) + { + *zoom = zoom_factors[i]; + return TRUE; + } + + --i; + } + + return FALSE; +} + +static inline GSimpleAction * +lookup_action (TerminalWindow *window, + const char *name) +{ + GAction *action; + + action = g_action_map_lookup_action (G_ACTION_MAP (window), name); + g_return_val_if_fail (action != nullptr, nullptr); + + return G_SIMPLE_ACTION (action); +} + +/* Context menu helpers */ + +/* We don't want context menus to show accelerators. + * Setting the menu's accel group and/or accel path to nullptr + * unfortunately doesn't hide accelerators; we need to walk + * the menu items and remove the accelerators on each, + * manually. + */ +static void +popup_menu_remove_accelerators (GtkWidget *menu) +{ + gs_free_list GList *menu_items; + GList *l; + + menu_items = gtk_container_get_children (GTK_CONTAINER (menu)); + for (l = menu_items; l != nullptr; l = l ->next) { + GtkMenuItem *item = (GtkMenuItem*) (l->data); + GtkWidget *label, *submenu; + + if (!GTK_IS_MENU_ITEM (item)) + continue; + + if (GTK_IS_ACCEL_LABEL ((label = gtk_bin_get_child (GTK_BIN (item))))) + gtk_accel_label_set_accel (GTK_ACCEL_LABEL (label), 0, GdkModifierType(0)); + + /* Recurse into submenus */ + if ((submenu = gtk_menu_item_get_submenu (item))) + popup_menu_remove_accelerators (submenu); + } +} + +/* Because we're using gtk_menu_attach_to_widget(), the attach + * widget holds a strong reference to the menu, causing it not to + * be automatically destroyed once popped down. So we need to + * detach the menu from the attach widget manually, which will + * cause the menu to be destroyed. We cannot do so in the + * "deactivate" handler however, since that causes the menu + * item activation to be lost. The "selection-done" signal + * appears to be the right place. + */ + +static void +popup_menu_destroy_cb (GtkWidget *menu, + gpointer user_data) +{ + /* g_printerr ("Menu %p destroyed!\n", menu); */ +} + +static void +popup_menu_selection_done_cb (GtkMenu *menu, + gpointer user_data) +{ + g_signal_handlers_disconnect_by_func(menu, + (void*)popup_menu_selection_done_cb, + user_data); + + /* g_printerr ("selection-done %p\n", menu); */ + + /* This will remove the ref from the attach widget widget, and destroy the menu */ + if (gtk_menu_get_attach_widget (menu) != nullptr) + gtk_menu_detach (menu); +} + +static void +popup_menu_detach_cb (GtkWidget *attach_widget, + GtkMenu *menu) +{ + gtk_menu_shell_deactivate (GTK_MENU_SHELL (menu)); +} + +static GtkWidget * +context_menu_new (GMenuModel *menu, + GtkWidget *widget) +{ + GtkWidget *popup_menu; + + popup_menu = gtk_menu_new_from_model (menu); + gtk_style_context_add_class (gtk_widget_get_style_context (popup_menu), + GTK_STYLE_CLASS_CONTEXT_MENU); + gtk_menu_attach_to_widget (GTK_MENU (popup_menu), widget, + (GtkMenuDetachFunc)popup_menu_detach_cb); + + popup_menu_remove_accelerators (popup_menu); + + /* Staggered destruction */ + g_signal_connect (popup_menu, "selection-done", + G_CALLBACK (popup_menu_selection_done_cb), widget); + g_signal_connect (popup_menu, "destroy", + G_CALLBACK (popup_menu_destroy_cb), widget); + + return popup_menu; +} + +/* GAction callbacks */ + +static void +action_new_terminal_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + TerminalWindow *window = (TerminalWindow*)user_data; + TerminalWindowPrivate *priv = window->priv; + TerminalApp *app; + TerminalSettingsList *profiles_list; + gs_unref_object GSettings *profile = nullptr; + gboolean can_toggle = FALSE; + + g_assert (TERMINAL_IS_WINDOW (window)); + + app = terminal_app_get (); + + const char *mode_str, *uuid_str; + g_variant_get (parameter, "(&s&s)", &mode_str, &uuid_str); + + TerminalNewTerminalMode mode; + if (g_str_equal (mode_str, "tab")) + mode = TERMINAL_NEW_TERMINAL_MODE_TAB; + else if (g_str_equal (mode_str, "window")) + mode = TERMINAL_NEW_TERMINAL_MODE_WINDOW; + else if (g_str_equal (mode_str, "tab-default")) { + mode = TERMINAL_NEW_TERMINAL_MODE_TAB; + can_toggle = TRUE; + } else { + mode = TerminalNewTerminalMode(g_settings_get_enum (terminal_app_get_global_settings (app), + TERMINAL_SETTING_NEW_TERMINAL_MODE_KEY)); + can_toggle = TRUE; + } + + if (can_toggle) { + GdkEvent *event = gtk_get_current_event (); + if (event != nullptr) { + GdkModifierType modifiers; + + if ((gdk_event_get_state (event, &modifiers) && + (modifiers & gtk_accelerator_get_default_mod_mask () & GDK_CONTROL_MASK))) { + /* Invert */ + if (mode == TERMINAL_NEW_TERMINAL_MODE_WINDOW) + mode = TERMINAL_NEW_TERMINAL_MODE_TAB; + else + mode = TERMINAL_NEW_TERMINAL_MODE_WINDOW; + } + gdk_event_free (event); + } + } + + TerminalScreen *parent_screen = priv->active_screen; + + profiles_list = terminal_app_get_profiles_list (app); + if (g_str_equal (uuid_str, "current")) + profile = terminal_screen_ref_profile (parent_screen); + else if (g_str_equal (uuid_str, "default")) + profile = terminal_settings_list_ref_default_child (profiles_list); + else + profile = terminal_settings_list_ref_child (profiles_list, uuid_str); + + if (profile == nullptr) + return; + + if (mode == TERMINAL_NEW_TERMINAL_MODE_WINDOW) + window = terminal_window_new (G_APPLICATION (app)); + + TerminalScreen *screen = terminal_screen_new (profile, + nullptr /* title */, + 1.0); + + /* Now add the new screen to the window */ + terminal_window_add_screen (window, screen, -1); + terminal_window_switch_screen (window, screen); + gtk_widget_grab_focus (GTK_WIDGET (screen)); + + /* Start child process, if possible by using the same args as the parent screen */ + terminal_screen_reexec_from_screen (screen, parent_screen, nullptr, nullptr); + + if (mode == TERMINAL_NEW_TERMINAL_MODE_WINDOW) + gtk_window_present (GTK_WINDOW (window)); +} + +#ifdef ENABLE_SAVE + +static void +save_contents_dialog_on_response (GtkDialog *dialog, + int response_id, + gpointer user_data) +{ + VteTerminal *terminal = (VteTerminal*)user_data; + GtkWindow *parent; + gs_free gchar *filename_uri = nullptr; + gs_unref_object GFile *file = nullptr; + GOutputStream *stream; + gs_free_error GError *error = nullptr; + + if (response_id != GTK_RESPONSE_ACCEPT) + { + gtk_widget_destroy (GTK_WIDGET (dialog)); + return; + } + + parent = (GtkWindow*) gtk_widget_get_ancestor (GTK_WIDGET (terminal), GTK_TYPE_WINDOW); + filename_uri = gtk_file_chooser_get_uri (GTK_FILE_CHOOSER (dialog)); + + gtk_widget_destroy (GTK_WIDGET (dialog)); + + if (filename_uri == nullptr) + return; + + file = g_file_new_for_uri (filename_uri); + stream = G_OUTPUT_STREAM (g_file_replace (file, nullptr, FALSE, G_FILE_CREATE_NONE, nullptr, &error)); + + if (stream) + { + /* XXX + * FIXME + * This is a sync operation. + * Should be replaced with the async version when vte implements that. + */ + vte_terminal_write_contents_sync (terminal, stream, + VTE_WRITE_DEFAULT, + nullptr, &error); + g_object_unref (stream); + } + + if (error) + { + terminal_util_show_error_dialog (parent, nullptr, error, + "%s", _("Could not save contents")); + } +} + +static void +action_save_contents_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + TerminalWindow *window = (TerminalWindow*)user_data; + GtkWidget *dialog = nullptr; + TerminalWindowPrivate *priv = window->priv; + VteTerminal *terminal; + + if (priv->active_screen == nullptr) + return; + + terminal = VTE_TERMINAL (priv->active_screen); + g_return_if_fail (VTE_IS_TERMINAL (terminal)); + + dialog = gtk_file_chooser_dialog_new (_("Save as…"), + GTK_WINDOW(window), + GTK_FILE_CHOOSER_ACTION_SAVE, + _("_Cancel"), GTK_RESPONSE_CANCEL, + _("_Save"), GTK_RESPONSE_ACCEPT, + nullptr); + + gtk_file_chooser_set_do_overwrite_confirmation (GTK_FILE_CHOOSER (dialog), TRUE); + gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER (dialog), g_get_user_special_dir (G_USER_DIRECTORY_DOCUMENTS)); + + gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (window)); + gtk_window_set_modal (GTK_WINDOW (dialog), TRUE); + gtk_window_set_destroy_with_parent (GTK_WINDOW (dialog), TRUE); + + g_signal_connect (dialog, "response", G_CALLBACK (save_contents_dialog_on_response), terminal); + g_signal_connect (dialog, "delete_event", G_CALLBACK (terminal_util_dialog_response_on_delete), nullptr); + + gtk_window_present (GTK_WINDOW (dialog)); +} + +#endif /* ENABLE_SAVE */ + +#ifdef ENABLE_PRINT + +static void +print_begin_cb (GtkPrintOperation *op, + GtkPrintContext *context, + TerminalApp *app) +{ + GtkPrintSettings *settings; + GtkPageSetup *page_setup; + + /* Don't save if the print dialogue was cancelled */ + if (gtk_print_operation_get_status(op) == GTK_PRINT_STATUS_FINISHED_ABORTED) + return; + + settings = gtk_print_operation_get_print_settings (op); + page_setup = gtk_print_operation_get_default_page_setup (op); + terminal_util_save_print_settings (settings, page_setup); +} + +static void +print_done_cb (GtkPrintOperation *op, + GtkPrintOperationResult result, + TerminalWindow *window) +{ + if (result != GTK_PRINT_OPERATION_RESULT_ERROR) + return; + + /* FIXME: show error */ +} + +static void +action_print_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + TerminalWindow *window = user_data; + TerminalWindowPrivate *priv = window->priv; + gs_unref_object GtkPrintSettings *settings = nullptr; + gs_unref_object GtkPageSetup *page_setup = nullptr; + gs_unref_object GtkPrintOperation *op = nullptr; + gs_free_error GError *error = nullptr; + GtkPrintOperationResult result; + + if (priv->active_screen == nullptr) + return; + + op = vte_print_operation_new (VTE_TERMINAL (priv->active_screen), + VTE_PRINT_OPERATION_DEFAULT /* flags */); + if (op == nullptr) + return; + + terminal_util_load_print_settings (&settings, &page_setup); + if (settings != nullptr) + gtk_print_operation_set_print_settings (op, settings); + if (page_setup != nullptr) + gtk_print_operation_set_default_page_setup (op, page_setup); + + g_signal_connect (op, "begin-print", G_CALLBACK (print_begin_cb), window); + g_signal_connect (op, "done", G_CALLBACK (print_done_cb), window); + + /* FIXME: show progress better */ + + result = gtk_print_operation_run (op, + /* this is the only supported one: */ + GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG, + GTK_WINDOW (window), + &error); + /* VtePrintOperation always runs async */ + g_assert_cmpint (result, ==, GTK_PRINT_OPERATION_RESULT_IN_PROGRESS); +} + +#endif /* ENABLE_PRINT */ + +#ifdef ENABLE_EXPORT + +static void +action_export_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + TerminalWindow *window = user_data; + TerminalWindowPrivate *priv = window->priv; + gs_unref_object VteExportOperation *op = nullptr; + gs_free_error GError *error = nullptr; + + if (priv->active_screen == nullptr) + return; + + op = vte_export_operation_new (VTE_TERMINAL (priv->active_screen), + TRUE /* interactive */, + VTE_EXPORT_FORMAT_ASK /* allow user to choose export format */, + nullptr, nullptr /* GSettings & key to load/store default directory from, FIXME */, + nullptr, nullptr /* progress callback & user data, FIXME */); + if (op == nullptr) + return; + + /* FIXME: show progress better */ + + vte_export_operation_run_async (op, GTK_WINDOW (window), nullptr /* cancellable */); +} + +#endif /* ENABLE_EXPORT */ + +static void +action_close_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + TerminalWindow *window = (TerminalWindow*)user_data; + TerminalWindowPrivate *priv = window->priv; + TerminalScreen *screen; + const char *mode_str; + + g_assert_nonnull (parameter); + g_variant_get (parameter, "&s", &mode_str); + + if (g_str_equal (mode_str, "tab")) + screen = priv->active_screen; + else if (g_str_equal (mode_str, "window")) + screen = nullptr; + else + return; + + if (confirm_close_window_or_tab (window, screen)) + return; + + if (screen) + terminal_window_remove_screen (window, screen); + else + gtk_widget_destroy (GTK_WIDGET (window)); +} + +static void +action_copy_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + TerminalWindow *window = (TerminalWindow*)user_data; + TerminalWindowPrivate *priv = window->priv; + const char *format_str; + VteFormat format; + + if (priv->active_screen == nullptr) + return; + + g_assert_nonnull (parameter); + g_variant_get (parameter, "&s", &format_str); + + if (g_str_equal (format_str, "text")) + format = VTE_FORMAT_TEXT; + else if (g_str_equal (format_str, "html")) + format = VTE_FORMAT_HTML; + else + return; + + vte_terminal_copy_clipboard_format (VTE_TERMINAL (priv->active_screen), format); +} + +/* Clipboard helpers */ + +typedef struct { + GWeakRef screen_weak_ref; +} PasteData; + +static void +clipboard_uris_received_cb (GtkClipboard *clipboard, + /* const */ char **uris, + PasteData *data) +{ + gs_unref_object TerminalScreen *screen = nullptr; + + if (uris != nullptr && uris[0] != nullptr && + (screen = (TerminalScreen*)g_weak_ref_get (&data->screen_weak_ref))) { + gs_free char *text; + gsize len; + + /* This potentially modifies the strings in |uris| but that's ok */ + terminal_util_transform_uris_to_quoted_fuse_paths (uris); + text = terminal_util_concat_uris (uris, &len); + + terminal_screen_paste_text (screen, text, len); + } + + g_weak_ref_clear (&data->screen_weak_ref); + g_slice_free (PasteData, data); +} + +static void +request_clipboard_contents_for_paste (TerminalWindow *window, + gboolean paste_as_uris) +{ + TerminalWindowPrivate *priv = window->priv; + GdkAtom *targets; + int n_targets; + + if (priv->active_screen == nullptr) + return; + + targets = terminal_app_get_clipboard_targets (terminal_app_get (), + priv->clipboard, + &n_targets); + if (targets == nullptr) + return; + + if (paste_as_uris && gtk_targets_include_uri (targets, n_targets)) { + PasteData *data = g_slice_new (PasteData); + g_weak_ref_init (&data->screen_weak_ref, priv->active_screen); + + gtk_clipboard_request_uris (priv->clipboard, + (GtkClipboardURIReceivedFunc) clipboard_uris_received_cb, + data); + return; + } else if (gtk_targets_include_text (targets, n_targets)) { + vte_terminal_paste_clipboard (VTE_TERMINAL (priv->active_screen)); + } +} + +static void +action_paste_text_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + TerminalWindow *window = (TerminalWindow*)user_data; + + request_clipboard_contents_for_paste (window, FALSE); +} + +static void +action_paste_uris_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + TerminalWindow *window = (TerminalWindow*)user_data; + + request_clipboard_contents_for_paste (window, TRUE); +} + +static void +action_select_all_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + TerminalWindow *window = (TerminalWindow*)user_data; + TerminalWindowPrivate *priv = window->priv; + + if (priv->active_screen == nullptr) + return; + + vte_terminal_select_all (VTE_TERMINAL (priv->active_screen)); +} + +static void +action_reset_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + TerminalWindow *window = (TerminalWindow*)user_data; + TerminalWindowPrivate *priv = window->priv; + + g_assert_nonnull (parameter); + + if (priv->active_screen == nullptr) + return; + + vte_terminal_reset (VTE_TERMINAL (priv->active_screen), + TRUE, + g_variant_get_boolean (parameter)); +} + +static void +tab_switch_relative (TerminalWindow *window, + int change) +{ + TerminalWindowPrivate *priv = window->priv; + int n_screens, value; + + n_screens = terminal_mdi_container_get_n_screens (priv->mdi_container); + value = terminal_mdi_container_get_active_screen_num (priv->mdi_container) + change; + + gboolean keynav_wrap_around; + g_object_get (gtk_widget_get_settings (GTK_WIDGET (window)), + "gtk-keynav-wrap-around", &keynav_wrap_around, + nullptr); + if (keynav_wrap_around) { + if (value < 0) + value += n_screens; + else if (value >= n_screens) + value -= n_screens; + } + + if (value < 0 || value >= n_screens) + return; + + g_action_change_state (G_ACTION (lookup_action (window, "active-tab")), + g_variant_new_int32 (value)); +} + +static void +action_tab_switch_left_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + TerminalWindow *window = (TerminalWindow*)user_data; + + tab_switch_relative (window, -1); +} + +static void +action_tab_switch_right_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + TerminalWindow *window = (TerminalWindow*)user_data; + + tab_switch_relative (window, 1); +} + +static void +action_tab_move_left_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + TerminalWindow *window = (TerminalWindow*)user_data; + TerminalWindowPrivate *priv = window->priv; + int change; + + if (priv->active_screen == nullptr) + return; + + change = gtk_widget_get_direction (GTK_WIDGET (window)) == GTK_TEXT_DIR_RTL ? 1 : -1; + terminal_mdi_container_reorder_screen (priv->mdi_container, + terminal_mdi_container_get_active_screen (priv->mdi_container), + change); +} + +static void +action_tab_move_right_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + TerminalWindow *window = (TerminalWindow*)user_data; + TerminalWindowPrivate *priv = window->priv; + int change; + + if (priv->active_screen == nullptr) + return; + + change = gtk_widget_get_direction (GTK_WIDGET (window)) == GTK_TEXT_DIR_RTL ? -1 : 1; + terminal_mdi_container_reorder_screen (priv->mdi_container, + terminal_mdi_container_get_active_screen (priv->mdi_container), + change); +} + +static void +action_zoom_in_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + TerminalWindow *window = (TerminalWindow*)user_data; + TerminalWindowPrivate *priv = window->priv; + double zoom; + + if (priv->active_screen == nullptr) + return; + + zoom = vte_terminal_get_font_scale (VTE_TERMINAL (priv->active_screen)); + if (!find_larger_zoom_factor (&zoom)) + return; + + vte_terminal_set_font_scale (VTE_TERMINAL (priv->active_screen), zoom); + terminal_window_update_zoom_sensitivity (window); +} + +static void +action_zoom_out_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + TerminalWindow *window = (TerminalWindow*)user_data; + TerminalWindowPrivate *priv = window->priv; + double zoom; + + if (priv->active_screen == nullptr) + return; + + zoom = vte_terminal_get_font_scale (VTE_TERMINAL (priv->active_screen)); + if (!find_smaller_zoom_factor (&zoom)) + return; + + vte_terminal_set_font_scale (VTE_TERMINAL (priv->active_screen), zoom); + terminal_window_update_zoom_sensitivity (window); +} + +static void +action_zoom_normal_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + TerminalWindow *window = (TerminalWindow*)user_data; + TerminalWindowPrivate *priv = window->priv; + + if (priv->active_screen == nullptr) + return; + + vte_terminal_set_font_scale (VTE_TERMINAL (priv->active_screen), PANGO_SCALE_MEDIUM); + terminal_window_update_zoom_sensitivity (window); +} + +static void +action_tab_detach_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + TerminalWindow *window = (TerminalWindow*)user_data; + TerminalWindowPrivate *priv = window->priv; + TerminalApp *app; + TerminalWindow *new_window; + TerminalScreen *screen; + char geometry[32]; + int width, height; + + app = terminal_app_get (); + + screen = priv->active_screen; + + terminal_screen_get_size (screen, &width, &height); + g_snprintf (geometry, sizeof (geometry), "%dx%d", width, height); + + new_window = terminal_window_new (G_APPLICATION (app)); + + terminal_window_move_screen (window, new_window, screen, -1); + + terminal_window_parse_geometry (new_window, geometry); + + gtk_window_present_with_time (GTK_WINDOW (new_window), gtk_get_current_event_time ()); +} + +static void +action_help_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + terminal_util_show_help (nullptr); +} + +static void +action_about_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + terminal_util_show_about (); +} + +static void +action_edit_preferences_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + TerminalWindow *window = (TerminalWindow*)user_data; + TerminalWindowPrivate *priv = window->priv; + + terminal_app_edit_preferences (terminal_app_get (), + terminal_screen_get_profile (priv->active_screen), + nullptr, + gtk_get_current_event_time()); +} + +static void +action_size_to_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + TerminalWindow *window = (TerminalWindow*)user_data; + TerminalWindowPrivate *priv = window->priv; + guint width, height; + + g_assert_nonnull (parameter); + + if (priv->active_screen == nullptr) + return; + + g_variant_get (parameter, "(uu)", &width, &height); + if (width < MIN_WIDTH_CHARS || height < MIN_HEIGHT_CHARS || + width > 256 || height > 256) + return; + + vte_terminal_set_size (VTE_TERMINAL (priv->active_screen), width, height); + terminal_window_update_size (window); +} + +static void +action_open_match_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + TerminalWindow *window = (TerminalWindow*)user_data; + TerminalWindowPrivate *priv = window->priv; + TerminalScreenPopupInfo *info = priv->popup_info; + + if (info == nullptr) + return; + if (info->url == nullptr) + return; + + terminal_util_open_url (GTK_WIDGET (window), info->url, info->url_flavor, + gtk_get_current_event_time ()); +} + +static void +action_copy_match_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + TerminalWindow *window = (TerminalWindow*)user_data; + TerminalWindowPrivate *priv = window->priv; + TerminalScreenPopupInfo *info = priv->popup_info; + + if (info == nullptr) + return; + if (info->url == nullptr) + return; + + gtk_clipboard_set_text (priv->clipboard, info->url, -1); +} + +static void +action_open_hyperlink_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + TerminalWindow *window = (TerminalWindow*)user_data; + TerminalWindowPrivate *priv = window->priv; + TerminalScreenPopupInfo *info = priv->popup_info; + + if (info == nullptr) + return; + if (info->hyperlink == nullptr) + return; + + terminal_util_open_url (GTK_WIDGET (window), info->hyperlink, FLAVOR_AS_IS, + gtk_get_current_event_time ()); +} + +static void +action_copy_hyperlink_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + TerminalWindow *window = (TerminalWindow*)user_data; + TerminalWindowPrivate *priv = window->priv; + TerminalScreenPopupInfo *info = priv->popup_info; + + if (info == nullptr) + return; + if (info->hyperlink == nullptr) + return; + + gtk_clipboard_set_text (priv->clipboard, info->hyperlink, -1); +} + +static void +action_enter_fullscreen_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + TerminalWindow *window = (TerminalWindow*)user_data; + + g_action_group_change_action_state (G_ACTION_GROUP (window), "fullscreen", + g_variant_new_boolean (TRUE)); +} + +static void +action_leave_fullscreen_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + TerminalWindow *window = (TerminalWindow*)user_data; + + g_action_group_change_action_state (G_ACTION_GROUP (window), "fullscreen", + g_variant_new_boolean (FALSE)); +} + +static void +action_inspector_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + gtk_window_set_interactive_debugging (TRUE); +} + +static void +search_popover_search_cb (TerminalSearchPopover *popover, + gboolean backward, + TerminalWindow *window) +{ + TerminalWindowPrivate *priv = window->priv; + + if (G_UNLIKELY (priv->active_screen == nullptr)) + return; + + if (backward) + vte_terminal_search_find_previous (VTE_TERMINAL (priv->active_screen)); + else + vte_terminal_search_find_next (VTE_TERMINAL (priv->active_screen)); +} + +static void +search_popover_notify_regex_cb (TerminalSearchPopover *popover, + GParamSpec *pspec G_GNUC_UNUSED, + TerminalWindow *window) +{ + TerminalWindowPrivate *priv = window->priv; + VteRegex *regex; + + if (G_UNLIKELY (priv->active_screen == nullptr)) + return; + + regex = terminal_search_popover_get_regex (popover); + vte_terminal_search_set_regex (VTE_TERMINAL (priv->active_screen), regex, 0); + + terminal_window_update_search_sensitivity (priv->active_screen, window); +} + +static void +search_popover_notify_wrap_around_cb (TerminalSearchPopover *popover, + GParamSpec *pspec G_GNUC_UNUSED, + TerminalWindow *window) +{ + TerminalWindowPrivate *priv = window->priv; + gboolean wrap; + + if (G_UNLIKELY (priv->active_screen == nullptr)) + return; + + wrap = terminal_search_popover_get_wrap_around (popover); + vte_terminal_search_set_wrap_around (VTE_TERMINAL (priv->active_screen), wrap); +} + +static void +action_find_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + TerminalWindow *window = (TerminalWindow*)user_data; + TerminalWindowPrivate *priv = window->priv; + + if (G_UNLIKELY(priv->active_screen == nullptr)) + return; + + if (priv->search_popover != nullptr) { + search_popover_notify_regex_cb (priv->search_popover, nullptr, window); + search_popover_notify_wrap_around_cb (priv->search_popover, nullptr, window); + + gtk_window_present_with_time (GTK_WINDOW (priv->search_popover), + gtk_get_current_event_time ()); + gtk_widget_grab_focus (GTK_WIDGET (priv->search_popover)); + return; + } + + if (priv->active_screen == nullptr) + return; + + priv->search_popover = terminal_search_popover_new (GTK_WIDGET (window)); + + g_signal_connect (priv->search_popover, "search", G_CALLBACK (search_popover_search_cb), window); + + search_popover_notify_regex_cb (priv->search_popover, nullptr, window); + g_signal_connect (priv->search_popover, "notify::regex", G_CALLBACK (search_popover_notify_regex_cb), window); + + search_popover_notify_wrap_around_cb (priv->search_popover, nullptr, window); + g_signal_connect (priv->search_popover, "notify::wrap-around", G_CALLBACK (search_popover_notify_wrap_around_cb), window); + + g_signal_connect (priv->search_popover, "destroy", G_CALLBACK (gtk_widget_destroyed), &priv->search_popover); + + gtk_window_present_with_time (GTK_WINDOW (priv->search_popover), gtk_get_current_event_time ()); + gtk_widget_grab_focus (GTK_WIDGET (priv->search_popover)); +} + +static void +action_find_forward_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + TerminalWindow *window = (TerminalWindow*)user_data; + TerminalWindowPrivate *priv = window->priv; + + if (priv->active_screen == nullptr) + return; + + vte_terminal_search_find_next (VTE_TERMINAL (priv->active_screen)); +} + +static void +action_find_backward_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + TerminalWindow *window = (TerminalWindow*)user_data; + TerminalWindowPrivate *priv = window->priv; + + if (priv->active_screen == nullptr) + return; + + vte_terminal_search_find_previous (VTE_TERMINAL (priv->active_screen)); +} + +static void +action_find_clear_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + TerminalWindow *window = (TerminalWindow*)user_data; + TerminalWindowPrivate *priv = window->priv; + + if (priv->active_screen == nullptr) + return; + + vte_terminal_search_set_regex (VTE_TERMINAL (priv->active_screen), nullptr, 0); + vte_terminal_unselect_all (VTE_TERMINAL (priv->active_screen)); +} + +static void +action_shadow_activate_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + TerminalWindow *window = (TerminalWindow*)user_data; + gs_free char *param = g_variant_print(parameter, TRUE); + + _terminal_debug_print (TERMINAL_DEBUG_ACCELS, + "Window %p shadow action activated for %s\n", + window, param); + + /* We make sure in terminal-accels to always install the keybinding + * for the real action first, so that it's first in line for activation. + * That means we can make this here a NOP, instead of forwarding the + * activation to the shadowed action. + */ +} + +static void +action_menubar_visible_state_cb (GSimpleAction *action, + GVariant *state, + gpointer user_data) +{ + TerminalWindow *window = (TerminalWindow*)user_data; + gboolean active; + + active = g_variant_get_boolean (state); + terminal_window_set_menubar_visible (window, active); /* this also sets the action state */ +} + +static void +action_fullscreen_state_cb (GSimpleAction *action, + GVariant *state, + gpointer user_data) +{ + TerminalWindow *window = (TerminalWindow*)user_data; + + if (!gtk_widget_get_realized (GTK_WIDGET (window))) + return; + + if (g_variant_get_boolean (state)) + gtk_window_fullscreen (GTK_WINDOW (window)); + else + gtk_window_unfullscreen (GTK_WINDOW (window)); + + /* The window-state-changed callback will update the action's actual state */ +} + +static void +action_read_only_state_cb (GSimpleAction *action, + GVariant *state, + gpointer user_data) +{ + TerminalWindow *window = (TerminalWindow*)user_data; + TerminalWindowPrivate *priv = window->priv; + + g_assert_nonnull (state); + + g_simple_action_set_state (action, state); + + terminal_window_update_paste_sensitivity (window); + + if (priv->active_screen == nullptr) + return; + + vte_terminal_set_input_enabled (VTE_TERMINAL (priv->active_screen), + !g_variant_get_boolean (state)); +} + +static void +action_profile_state_cb (GSimpleAction *action, + GVariant *state, + gpointer user_data) +{ + TerminalWindow *window = (TerminalWindow*)user_data; + TerminalWindowPrivate *priv = window->priv; + TerminalSettingsList *profiles_list; + const gchar *uuid; + gs_unref_object GSettings *profile; + + g_assert_nonnull (state); + + uuid = g_variant_get_string (state, nullptr); + profiles_list = terminal_app_get_profiles_list (terminal_app_get ()); + profile = terminal_settings_list_ref_child (profiles_list, uuid); + if (profile == nullptr) + return; + + g_simple_action_set_state (action, state); + + terminal_screen_set_profile (priv->active_screen, profile); +} + +static void +action_active_tab_set_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + TerminalWindow *window = (TerminalWindow*)user_data; + TerminalWindowPrivate *priv = window->priv; + int value, n_screens; + + g_assert_nonnull (parameter); + + n_screens = terminal_mdi_container_get_n_screens (priv->mdi_container); + + value = g_variant_get_int32 (parameter); + if (value < 0) + value += n_screens; + if (value < 0 || value >= n_screens) + return; + + g_action_change_state (G_ACTION (action), g_variant_new_int32 (value)); +} + +static void +action_active_tab_state_cb (GSimpleAction *action, + GVariant *state, + gpointer user_data) +{ + TerminalWindow *window = (TerminalWindow*)user_data; + TerminalWindowPrivate *priv = window->priv; + + g_assert_nonnull (state); + + g_simple_action_set_state (action, state); + + terminal_mdi_container_set_active_screen_num (priv->mdi_container, g_variant_get_int32 (state)); +} + +/* Menubar mnemonics & accel settings handling */ + +static void +enable_menubar_accel_changed_cb (GSettings *settings, + const char *key, + GtkSettings *gtk_settings) +{ + if (g_settings_get_boolean (settings, key)) + gtk_settings_reset_property (gtk_settings, "gtk-menu-bar-accel"); + else + g_object_set (gtk_settings, "gtk-menu-bar-accel", nullptr, nullptr); +} + +/* The menubar is shown by the app, and the use of mnemonics (e.g. Alt+F for File) is toggled. + * The mnemonic modifier is per window, so it doesn't affect the Find or Preferences windows. + * If the menubar is shown by the shell, a non-mnemonic variant of the menu is loaded instead + * in terminal-app.c. See over there for further details. */ +static void +enable_mnemonics_changed_cb (GSettings *settings, + const char *key, + TerminalWindow *window) +{ + gboolean enabled = g_settings_get_boolean (settings, key); + + if (enabled) + gtk_window_set_mnemonic_modifier (GTK_WINDOW (window), GDK_MOD1_MASK); + else + gtk_window_set_mnemonic_modifier (GTK_WINDOW (window), GdkModifierType(GDK_MODIFIER_MASK & ~GDK_RELEASE_MASK)); +} + +static void +app_setting_notify_destroy_cb (GtkSettings *gtk_settings) +{ + g_signal_handlers_disconnect_by_func (terminal_app_get_global_settings (terminal_app_get ()), + (void*)enable_menubar_accel_changed_cb, + gtk_settings); +} + +/* utility functions */ + +static int +find_tab_num_at_pos (GtkNotebook *notebook, + int screen_x, + int screen_y) +{ + GtkPositionType tab_pos; + int page_num = 0; + GtkNotebook *nb = GTK_NOTEBOOK (notebook); + GtkWidget *page; + GtkAllocation tab_allocation; + + tab_pos = gtk_notebook_get_tab_pos (GTK_NOTEBOOK (notebook)); + + while ((page = gtk_notebook_get_nth_page (nb, page_num))) + { + GtkWidget *tab; + int max_x, max_y, x_root, y_root; + + tab = gtk_notebook_get_tab_label (nb, page); + g_return_val_if_fail (tab != nullptr, -1); + + if (!gtk_widget_get_mapped (GTK_WIDGET (tab))) + { + page_num++; + continue; + } + + gdk_window_get_origin (gtk_widget_get_window (tab), &x_root, &y_root); + + gtk_widget_get_allocation (tab, &tab_allocation); + max_x = x_root + tab_allocation.x + tab_allocation.width; + max_y = y_root + tab_allocation.y + tab_allocation.height; + + if ((tab_pos == GTK_POS_TOP || tab_pos == GTK_POS_BOTTOM) && screen_x <= max_x) + return page_num; + + if ((tab_pos == GTK_POS_LEFT || tab_pos == GTK_POS_RIGHT) && screen_y <= max_y) + return page_num; + + page_num++; + } + + return -1; +} + +static void +terminal_window_update_set_profile_menu_active_profile (TerminalWindow *window) +{ + TerminalWindowPrivate *priv = window->priv; + GSettings *new_active_profile; + TerminalSettingsList *profiles_list; + char *uuid; + + if (priv->active_screen == nullptr) + return; + + new_active_profile = terminal_screen_get_profile (priv->active_screen); + + profiles_list = terminal_app_get_profiles_list (terminal_app_get ()); + uuid = terminal_settings_list_dup_uuid_from_child (profiles_list, new_active_profile); + + g_simple_action_set_state (lookup_action (window, "profile"), + g_variant_new_take_string (uuid)); +} + +static void +terminal_window_update_terminal_menu (TerminalWindow *window) +{ + TerminalWindowPrivate *priv = window->priv; + + if (priv->active_screen == nullptr) + return; + + gboolean read_only = !vte_terminal_get_input_enabled (VTE_TERMINAL (priv->active_screen)); + g_simple_action_set_state (lookup_action (window, "read-only"), + g_variant_new_boolean (read_only)); +} + +/* Actions stuff */ + +static void +terminal_window_update_copy_sensitivity (TerminalScreen *screen, + TerminalWindow *window) +{ + TerminalWindowPrivate *priv = window->priv; + gboolean can_copy; + + if (screen != priv->active_screen) + return; + + can_copy = vte_terminal_get_has_selection (VTE_TERMINAL (screen)); + g_simple_action_set_enabled (lookup_action (window, "copy"), can_copy); +} + +static void +terminal_window_update_zoom_sensitivity (TerminalWindow *window) +{ + TerminalWindowPrivate *priv = window->priv; + TerminalScreen *screen; + + screen = priv->active_screen; + if (screen == nullptr) + return; + + double v; + double zoom = v = vte_terminal_get_font_scale (VTE_TERMINAL (screen)); + g_simple_action_set_enabled (lookup_action (window, "zoom-in"), + find_larger_zoom_factor (&v)); + + v = zoom; + g_simple_action_set_enabled (lookup_action (window, "zoom-out"), + find_smaller_zoom_factor (&v)); +} + +static void +terminal_window_update_search_sensitivity (TerminalScreen *screen, + TerminalWindow *window) +{ + TerminalWindowPrivate *priv = window->priv; + + if (screen != priv->active_screen) + return; + + gboolean can_search = vte_terminal_search_get_regex (VTE_TERMINAL (screen)) != nullptr; + + g_simple_action_set_enabled (lookup_action (window, "find-forward"), can_search); + g_simple_action_set_enabled (lookup_action (window, "find-backward"), can_search); + g_simple_action_set_enabled (lookup_action (window, "find-clear"), can_search); +} + +static void +clipboard_targets_changed_cb (TerminalApp *app, + GtkClipboard *clipboard, + TerminalWindow *window) +{ + TerminalWindowPrivate *priv = window->priv; + + if (clipboard != priv->clipboard) + return; + + terminal_window_update_paste_sensitivity (window); +} + +static void +terminal_window_update_paste_sensitivity (TerminalWindow *window) +{ + TerminalWindowPrivate *priv = window->priv; + + GdkAtom *targets; + int n_targets; + targets = terminal_app_get_clipboard_targets (terminal_app_get(), priv->clipboard, &n_targets); + + gboolean can_paste; + gboolean can_paste_uris; + if (n_targets) { + can_paste = gtk_targets_include_text (targets, n_targets); + can_paste_uris = gtk_targets_include_uri (targets, n_targets); + } else { + can_paste = can_paste_uris = FALSE; + } + + gs_unref_variant GVariant *ro_state = g_action_get_state (g_action_map_lookup_action (G_ACTION_MAP (window), "read-only")); + gboolean read_only = g_variant_get_boolean (ro_state); + + g_simple_action_set_enabled (lookup_action (window, "paste-text"), can_paste && !read_only); + g_simple_action_set_enabled (lookup_action (window, "paste-uris"), can_paste_uris && !read_only); +} + +static void +screen_resize_window_cb (TerminalScreen *screen, + guint columns, + guint rows, + TerminalWindow* window) +{ + auto const priv = window->priv; + + if (priv->realized && + window_state_is_snapped(priv->window_state)) + return; + + vte_terminal_set_size (VTE_TERMINAL (priv->active_screen), columns, rows); + + if (screen == priv->active_screen) + terminal_window_update_size (window); +} + +static void +terminal_window_update_tabs_actions_sensitivity (TerminalWindow *window) +{ + TerminalWindowPrivate *priv = window->priv; + + if (priv->disposed) + return; + + int num_pages = terminal_mdi_container_get_n_screens (priv->mdi_container); + int page_num = terminal_mdi_container_get_active_screen_num (priv->mdi_container); + + gboolean not_only = num_pages > 1; + gboolean not_first = page_num > 0; + gboolean not_last = page_num + 1 < num_pages; + + gboolean not_first_lr, not_last_lr; + if (gtk_widget_get_direction (GTK_WIDGET (window)) == GTK_TEXT_DIR_RTL) { + not_first_lr = not_last; + not_last_lr = not_first; + } else { + not_first_lr = not_first; + not_last_lr = not_last; + } + + /* Hide the tabs menu in single-tab windows */ + g_simple_action_set_enabled (lookup_action (window, "tabs-menu"), not_only); + + /* Disable shadowing of MDI actions in SDI windows */ + g_simple_action_set_enabled (lookup_action (window, "shadow-mdi"), not_only); + + /* Disable tab switching (and all its shortcuts) in SDI windows */ + g_simple_action_set_enabled (lookup_action (window, "active-tab"), not_only); + + /* Set the active tab */ + g_simple_action_set_state (lookup_action (window, "active-tab"), + g_variant_new_int32 (page_num)); + + /* Keynav wraps around? See bug #92139 */ + gboolean keynav_wrap_around; + g_object_get (gtk_widget_get_settings (GTK_WIDGET (window)), + "gtk-keynav-wrap-around", &keynav_wrap_around, + nullptr); + + gboolean wrap = keynav_wrap_around && not_only; + g_simple_action_set_enabled (lookup_action (window, "tab-switch-left"), not_first || wrap); + g_simple_action_set_enabled (lookup_action (window, "tab-switch-right"), not_last || wrap); + g_simple_action_set_enabled (lookup_action (window, "tab-move-left"), not_first_lr || wrap); + g_simple_action_set_enabled (lookup_action (window, "tab-move-right"), not_last_lr || wrap); + g_simple_action_set_enabled (lookup_action (window, "tab-detach"), not_only); +} + +static GtkNotebook * +handle_tab_droped_on_desktop (GtkNotebook *source_notebook, + GtkWidget *container, + gint x, + gint y, + gpointer data G_GNUC_UNUSED) +{ + TerminalWindow *source_window; + TerminalWindow *new_window; + TerminalWindowPrivate *new_priv; + + source_window = TERMINAL_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (source_notebook))); + g_return_val_if_fail (TERMINAL_IS_WINDOW (source_window), nullptr); + + new_window = terminal_window_new (G_APPLICATION (terminal_app_get ())); + new_priv = new_window->priv; + new_priv->present_on_insert = TRUE; + + return GTK_NOTEBOOK (new_priv->mdi_container); +} + +/* Terminal screen popup menu handling */ + +static void +remove_popup_info (TerminalWindow *window) +{ + TerminalWindowPrivate *priv = window->priv; + + if (priv->popup_info != nullptr) + { + terminal_screen_popup_info_unref (priv->popup_info); + priv->popup_info = nullptr; + } +} + +static void +screen_popup_menu_selection_done_cb (GtkWidget *popup, + GtkWidget *window) +{ + g_signal_handlers_disconnect_by_func + (popup, (void*)screen_popup_menu_selection_done_cb, window); + + GtkWidget *attach_widget = gtk_menu_get_attach_widget (GTK_MENU (popup)); + if (attach_widget != window || !TERMINAL_IS_WINDOW (attach_widget)) + return; + + remove_popup_info (TERMINAL_WINDOW (attach_widget)); +} + +static void +screen_show_popup_menu_cb (TerminalScreen *screen, + TerminalScreenPopupInfo *info, + TerminalWindow *window) +{ + TerminalWindowPrivate *priv = window->priv; + TerminalApp *app = terminal_app_get (); + + if (screen != priv->active_screen) + return; + + remove_popup_info (window); + priv->popup_info = terminal_screen_popup_info_ref (info); + + gs_unref_object GMenu *menu = g_menu_new (); + + /* Hyperlink section */ + if (info->hyperlink != nullptr) { + gs_unref_object GMenu *section1 = g_menu_new (); + + g_menu_append (section1, _("Open _Hyperlink"), "win.open-hyperlink"); + g_menu_append (section1, _("Copy Hyperlink _Address"), "win.copy-hyperlink"); + g_menu_append_section (menu, nullptr, G_MENU_MODEL (section1)); + } + /* Matched link section */ + else if (info->url != nullptr) { + gs_unref_object GMenu *section2 = g_menu_new (); + + const char *open_label = nullptr, *copy_label = nullptr; + switch (info->url_flavor) { + case FLAVOR_EMAIL: + open_label = _("Send Mail _To…"); + copy_label = _("Copy Mail _Address"); + break; + case FLAVOR_VOIP_CALL: + open_label = _("Call _To…"); + copy_label = _("Copy Call _Address "); + break; + case FLAVOR_AS_IS: + case FLAVOR_DEFAULT_TO_HTTP: + default: + open_label = _("_Open Link"); + copy_label = _("Copy _Link"); + break; + } + + g_menu_append (section2, open_label, "win.open-match"); + g_menu_append (section2, copy_label, "win.copy-match"); + g_menu_append_section (menu, nullptr, G_MENU_MODEL (section2)); + } + + /* Info section */ + gs_strfreev char** citems = g_settings_get_strv (terminal_app_get_global_settings (terminal_app_get ()), + TERMINAL_SETTING_CONTEXT_INFO_KEY); + + gs_unref_object GMenu *section3 = g_menu_new (); + + for (int i = 0; citems[i] != nullptr; ++i) { + const char *citem = citems[i]; + + if (g_str_equal (citem, "numbers") && + info->number_info != nullptr) { + /* Non-existent action will make this item insensitive */ + gs_unref_object GMenuItem *item3 = g_menu_item_new (info->number_info, "win.notexist"); + g_menu_append_item (section3, item3); + } + + if (g_str_equal (citem, "timestamps") && + info->timestamp_info != nullptr) { + /* Non-existent action will make this item insensitive */ + gs_unref_object GMenuItem *item3 = g_menu_item_new (info->timestamp_info, "win.notexist"); + g_menu_append_item (section3, item3); + } + } + + if (g_menu_model_get_n_items(G_MENU_MODEL (section3)) > 0) + g_menu_append_section (menu, nullptr, G_MENU_MODEL (section3)); + + /* Clipboard section */ + gs_unref_object GMenu *section4 = g_menu_new (); + + g_menu_append (section4, _("_Copy"), "win.copy::text"); + g_menu_append (section4, _("Copy as _HTML"), "win.copy::html"); + g_menu_append (section4, _("_Paste"), "win.paste-text"); + if (g_action_get_enabled (G_ACTION (lookup_action (window, "paste-uris")))) + g_menu_append (section4, _("Paste as _Filenames"), "win.paste-uris"); + + g_menu_append_section (menu, nullptr, G_MENU_MODEL (section4)); + + /* Profile and property section */ + gs_unref_object GMenu *section5 = g_menu_new (); + g_menu_append (section5, _("Read-_Only"), "win.read-only"); + + GMenuModel *profiles_menu = terminal_app_get_profile_section (app); + if (profiles_menu != nullptr && g_menu_model_get_n_items (profiles_menu) > 1) { + gs_unref_object GMenu *submenu5 = g_menu_new (); + g_menu_append_section (submenu5, nullptr, profiles_menu); + + gs_unref_object GMenuItem *item5 = g_menu_item_new (_("P_rofiles"), nullptr); + g_menu_item_set_submenu (item5, G_MENU_MODEL (submenu5)); + g_menu_append_item (section5, item5); + } + + g_menu_append (section5, _("_Preferences"), "win.edit-preferences"); + + g_menu_append_section (menu, nullptr, G_MENU_MODEL (section5)); + + /* New Terminal section */ + gs_unref_object GMenu *section6 = g_menu_new (); + if (terminal_app_get_menu_unified (app)) { + gs_unref_object GMenuItem *item6 = g_menu_item_new (_("New _Terminal"), nullptr); + g_menu_item_set_action_and_target (item6, "win.new-terminal", + "(ss)", "default", "current"); + g_menu_append_item (section6, item6); + } else { + gs_unref_object GMenuItem *item61 = g_menu_item_new (_("New _Window"), nullptr); + g_menu_item_set_action_and_target (item61, "win.new-terminal", + "(ss)", "window", "current"); + g_menu_append_item (section6, item61); + gs_unref_object GMenuItem *item62 = g_menu_item_new (_("New _Tab"), nullptr); + g_menu_item_set_action_and_target (item62, "win.new-terminal", + "(ss)", "tab", "current"); + g_menu_append_item (section6, item62); + } + g_menu_append_section (menu, nullptr, G_MENU_MODEL (section6)); + + /* Window section */ + gs_unref_object GMenu *section7 = g_menu_new (); + + /* Only show this if the WM doesn't show the menubar */ + if (g_action_get_enabled (G_ACTION (lookup_action (window, "menubar-visible")))) + g_menu_append (section7, _("Show _Menubar"), "win.menubar-visible"); + if (g_action_get_enabled (G_ACTION (lookup_action (window, "leave-fullscreen")))) + g_menu_append (section7, _("L_eave Full Screen"), "win.leave-fullscreen"); + + g_menu_append_section (menu, nullptr, G_MENU_MODEL (section7)); + + /* Now create the popup menu and show it */ + GtkWidget *popup_menu = context_menu_new (G_MENU_MODEL (menu), GTK_WIDGET (window)); + + /* Remove the popup info after the menu is done */ + g_signal_connect (popup_menu, "selection-done", + G_CALLBACK (screen_popup_menu_selection_done_cb), window); + + gtk_menu_popup (GTK_MENU (popup_menu), nullptr, nullptr, + nullptr, nullptr, + info->button, + info->timestamp); + + if (info->button == 0) + gtk_menu_shell_select_first (GTK_MENU_SHELL (popup_menu), FALSE); +} + +static gboolean +screen_match_clicked_cb (TerminalScreen *screen, + const char *url, + int url_flavor, + guint state, + TerminalWindow *window) +{ + TerminalWindowPrivate *priv = window->priv; + + if (screen != priv->active_screen) + return FALSE; + + gtk_widget_grab_focus (GTK_WIDGET (screen)); + terminal_util_open_url (GTK_WIDGET (window), url, TerminalURLFlavor(url_flavor), + gtk_get_current_event_time ()); + + return TRUE; +} + +static void +screen_close_cb (TerminalScreen *screen, + TerminalWindow *window) +{ + terminal_window_remove_screen (window, screen); +} + +static void +notebook_update_tabs_menu_cb (GtkMenuButton *button, + TerminalWindow *window) +{ + gs_unref_object GMenu *menu; + gs_free_list GList *tabs; + GList *t; + int i; + + menu = g_menu_new (); + tabs = terminal_window_list_screen_containers (window); + + for (t = tabs, i = 0; t != nullptr; t = t->next, i++) { + TerminalScreenContainer *container = (TerminalScreenContainer*)t->data; + TerminalScreen *screen = terminal_screen_container_get_screen (container); + gs_unref_object GMenuItem *item; + const char *title; + + if (t->next == nullptr) { + /* Last entry. If it has no dedicated shortcut "Switch to Tab N", + * display the accel of "Switch to Last Tab". */ + GtkApplication *app = GTK_APPLICATION (g_application_get_default ()); + gs_free gchar *detailed_action = g_strdup_printf("win.active-tab(%d)", i); + gs_strfreev gchar **accels = gtk_application_get_accels_for_action (app, detailed_action); + if (accels[0] == nullptr) + i = -1; + } + + title = terminal_screen_get_title (screen); + + item = g_menu_item_new (title && title[0] ? title : _("Terminal"), nullptr); + g_menu_item_set_action_and_target (item, "win.active-tab", "i", i); + g_menu_append_item (menu, item); + } + + gtk_menu_button_set_menu_model (button, G_MENU_MODEL (menu)); + + /* Need this so the menu is positioned correctly */ + gtk_widget_set_halign (GTK_WIDGET (gtk_menu_button_get_popup (button)), GTK_ALIGN_END); +} + +static void +terminal_window_fill_notebook_action_box (TerminalWindow *window, + gboolean add_new_tab_button) +{ + TerminalWindowPrivate *priv = window->priv; + GtkWidget *box, *new_tab_button, *tabs_menu_button; + + box = terminal_notebook_get_action_box (TERMINAL_NOTEBOOK (priv->mdi_container), GTK_PACK_END); + + /* Create the NewTerminal button */ + if (add_new_tab_button) + { + new_tab_button = terminal_icon_button_new ("tab-new-symbolic"); + gtk_actionable_set_action_name (GTK_ACTIONABLE (new_tab_button), "win.new-terminal"); + gtk_actionable_set_action_target (GTK_ACTIONABLE (new_tab_button), "(ss)", "tab", "current"); + gtk_box_pack_start (GTK_BOX (box), new_tab_button, FALSE, FALSE, 0); + gtk_widget_show (new_tab_button); + } + + /* Create Tabs menu button */ + tabs_menu_button = terminal_menu_button_new (); + g_signal_connect (tabs_menu_button, "update-menu", + G_CALLBACK (notebook_update_tabs_menu_cb), window); + gtk_box_pack_start (GTK_BOX (box), tabs_menu_button, FALSE, FALSE, 0); + gtk_menu_button_set_align_widget (GTK_MENU_BUTTON (tabs_menu_button), box); + gtk_widget_show (tabs_menu_button); +} + +static void +window_hide_ask_default_terminal(TerminalWindow* window) +{ + auto const priv = window->priv; + + if (!priv->ask_default_infobar || + !gtk_widget_get_visible(priv->ask_default_infobar)) + return; + + gtk_widget_hide(priv->ask_default_infobar); + terminal_window_update_size(window); +} + +static void +window_sync_ask_default_terminal_cb(TerminalApp* app, + GParamSpec* pspect, + TerminalWindow* window) +{ + window_hide_ask_default_terminal(window); +} + +static void +default_infobar_response_cb(GtkInfoBar* infobar, + int response, + TerminalWindow* window) +{ + auto const app = terminal_app_get(); + + if (response == GTK_RESPONSE_YES) { + terminal_app_make_default_terminal(app); + terminal_app_unset_ask_default_terminal(app); + } else if (response == GTK_RESPONSE_NO) { + terminal_app_unset_ask_default_terminal(app); + } else { // GTK_RESPONSE_CLOSE + window_hide_ask_default_terminal(window); + } +} + +/*****************************************/ + +static void +terminal_window_realize (GtkWidget *widget) +{ + TerminalWindow *window = TERMINAL_WINDOW (widget); + TerminalWindowPrivate *priv = window->priv; + GtkAllocation widget_allocation; + + gtk_widget_get_allocation (widget, &widget_allocation); + + _terminal_debug_print (TERMINAL_DEBUG_GEOMETRY, + "[window %p] realize, size %d : %d at (%d, %d)\n", + widget, + widget_allocation.width, widget_allocation.height, + widget_allocation.x, widget_allocation.y); + + GTK_WIDGET_CLASS (terminal_window_parent_class)->realize (widget); + + /* Now that we've been realized, we should know precisely how large the + * client-side decorations are going to be. Recalculate the geometry hints, + * export them to the windowing system, and resize the window accordingly. */ + priv->realized = TRUE; + terminal_window_update_size (window); +} + +static gboolean +terminal_window_state_event (GtkWidget *widget, + GdkEventWindowState *event) +{ + auto const window_state_event = + GTK_WIDGET_CLASS(terminal_window_parent_class)->window_state_event; + auto const window = TERMINAL_WINDOW(widget); + auto const priv = window->priv; + + priv->window_state = event->new_window_state; + + _terminal_debug_print (TERMINAL_DEBUG_GEOMETRY, + "Window state changed mask %x old state %x new state %x\n", + unsigned(event->changed_mask), + unsigned(priv->window_state), + unsigned(event->new_window_state)); + + if (event->changed_mask & GDK_WINDOW_STATE_FULLSCREEN) + { + auto const is_fullscreen = (event->new_window_state & GDK_WINDOW_STATE_FULLSCREEN) != 0; + + g_simple_action_set_state (lookup_action (window, "fullscreen"), + g_variant_new_boolean (is_fullscreen)); + g_simple_action_set_enabled (lookup_action (window, "leave-fullscreen"), + is_fullscreen); + g_simple_action_set_enabled (lookup_action (window, "size-to"), + !is_fullscreen); + } + + if (window_state_is_snapped(event->changed_mask)) + { + if (window_state_is_snapped(event->new_window_state)) + { + _terminal_debug_print (TERMINAL_DEBUG_GEOMETRY, + "Disapplying geometry hints entering snapped state\n"); + gtk_window_set_geometry_hints(GTK_WINDOW(widget), nullptr, nullptr, + GdkWindowHints(0)); + } + else + { + _terminal_debug_print (TERMINAL_DEBUG_GEOMETRY, + "Reapplying geometry hints after leaving snapped state\n"); + gtk_window_set_geometry_hints (GTK_WINDOW (window), + nullptr, + &priv->hints, + GdkWindowHints(GDK_HINT_RESIZE_INC | + GDK_HINT_MIN_SIZE | + GDK_HINT_BASE_SIZE)); + } + } + + if (window_state_event) + return window_state_event (widget, event); + + return false; +} + +static void +terminal_window_screen_update (TerminalWindow *window, + GdkScreen *screen) +{ + GSettings *settings; + GtkSettings *gtk_settings; + + if (GPOINTER_TO_INT (g_object_get_data (G_OBJECT (screen), "GT::HasSettingsConnection"))) + return; + + settings = terminal_app_get_global_settings (terminal_app_get ()); + gtk_settings = gtk_settings_get_for_screen (screen); + + g_object_set_data_full (G_OBJECT (screen), "GT::HasSettingsConnection", + gtk_settings, + (GDestroyNotify) app_setting_notify_destroy_cb); + + g_settings_bind (settings, + TERMINAL_SETTING_ENABLE_SHORTCUTS_KEY, + gtk_settings, + "gtk-enable-accels", + GSettingsBindFlags(G_SETTINGS_BIND_GET)); + + enable_menubar_accel_changed_cb (settings, + TERMINAL_SETTING_ENABLE_MENU_BAR_ACCEL_KEY, + gtk_settings); + g_signal_connect (settings, "changed::" TERMINAL_SETTING_ENABLE_MENU_BAR_ACCEL_KEY, + G_CALLBACK (enable_menubar_accel_changed_cb), + gtk_settings); +} + +static void +terminal_window_screen_changed (GtkWidget *widget, + GdkScreen *previous_screen) +{ + TerminalWindow *window = TERMINAL_WINDOW (widget); + void (* screen_changed) (GtkWidget *, GdkScreen *) = + GTK_WIDGET_CLASS (terminal_window_parent_class)->screen_changed; + GdkScreen *screen; + + if (screen_changed) + screen_changed (widget, previous_screen); + + screen = gtk_widget_get_screen (widget); + if (previous_screen == screen) + return; + + if (!screen) + return; + + terminal_window_screen_update (window, screen); +} + +static void +terminal_window_init (TerminalWindow *window) +{ + const GActionEntry action_entries[] = { + /* Actions without state */ + { "about", action_about_cb, nullptr, nullptr, nullptr }, + { "close", action_close_cb, "s", nullptr, nullptr }, + { "copy", action_copy_cb, "s", nullptr, nullptr }, + { "copy-hyperlink", action_copy_hyperlink_cb, nullptr, nullptr, nullptr }, + { "copy-match", action_copy_match_cb, nullptr, nullptr, nullptr }, + { "edit-preferences", action_edit_preferences_cb, nullptr, nullptr, nullptr }, + { "enter-fullscreen", action_enter_fullscreen_cb, nullptr, nullptr, nullptr }, + { "find", action_find_cb, nullptr, nullptr, nullptr }, + { "find-backward", action_find_backward_cb, nullptr, nullptr, nullptr }, + { "find-clear", action_find_clear_cb, nullptr, nullptr, nullptr }, + { "find-forward", action_find_forward_cb, nullptr, nullptr, nullptr }, + { "help", action_help_cb, nullptr, nullptr, nullptr }, + { "inspector", action_inspector_cb, nullptr, nullptr, nullptr }, + { "leave-fullscreen", action_leave_fullscreen_cb, nullptr, nullptr, nullptr }, + { "new-terminal", action_new_terminal_cb, "(ss)", nullptr, nullptr }, + { "open-match", action_open_match_cb, nullptr, nullptr, nullptr }, + { "open-hyperlink", action_open_hyperlink_cb, nullptr, nullptr, nullptr }, + { "paste-text", action_paste_text_cb, nullptr, nullptr, nullptr }, + { "paste-uris", action_paste_uris_cb, nullptr, nullptr, nullptr }, + { "reset", action_reset_cb, "b", nullptr, nullptr }, + { "select-all", action_select_all_cb, nullptr, nullptr, nullptr }, + { "size-to", action_size_to_cb, "(uu)", nullptr, nullptr }, + { "tab-detach", action_tab_detach_cb, nullptr, nullptr, nullptr }, + { "tab-move-left", action_tab_move_left_cb, nullptr, nullptr, nullptr }, + { "tab-move-right", action_tab_move_right_cb, nullptr, nullptr, nullptr }, + { "tab-switch-left", action_tab_switch_left_cb, nullptr, nullptr, nullptr }, + { "tab-switch-right", action_tab_switch_right_cb, nullptr, nullptr, nullptr }, + { "tabs-menu", nullptr, nullptr, nullptr, nullptr }, + { "zoom-in", action_zoom_in_cb, nullptr, nullptr, nullptr }, + { "zoom-normal", action_zoom_normal_cb, nullptr, nullptr, nullptr }, + { "zoom-out", action_zoom_out_cb, nullptr, nullptr, nullptr }, +#ifdef ENABLE_EXPORT + { "export", action_export_cb, nullptr, nullptr, nullptr }, +#endif +#ifdef ENABLE_PRINT + { "print", action_print_cb, nullptr, nullptr, nullptr }, +#endif +#ifdef ENABLE_SAVE + { "save-contents", action_save_contents_cb, nullptr, nullptr, nullptr }, +#endif + + /* Shadow actions for keybinding comsumption, see comment in terminal-accels.c */ + { "shadow", action_shadow_activate_cb, "s", nullptr, nullptr }, + { "shadow-mdi", action_shadow_activate_cb, "s", nullptr, nullptr }, + + /* Actions with state */ + { "active-tab", action_active_tab_set_cb, "i", "@i 0", action_active_tab_state_cb }, + { "header-menu", nullptr /* toggles state */, nullptr, "false", nullptr }, + { "fullscreen", nullptr /* toggles state */, nullptr, "false", action_fullscreen_state_cb }, + { "menubar-visible", nullptr /* toggles state */, nullptr, "true", action_menubar_visible_state_cb }, + { "profile", nullptr /* changes state */, "s", "''", action_profile_state_cb }, + { "read-only", nullptr /* toggles state */, nullptr, "false", action_read_only_state_cb }, + }; + TerminalWindowPrivate *priv; + TerminalApp *app; + GSettings *gtk_debug_settings; + GtkWindowGroup *window_group; + // GtkAccelGroup *accel_group; + uuid_t u; + char uuidstr[37], role[64]; + gboolean shell_shows_menubar; + gboolean use_headerbar; + GSimpleAction *action; + + app = terminal_app_get (); + + priv = window->priv = G_TYPE_INSTANCE_GET_PRIVATE (window, TERMINAL_TYPE_WINDOW, TerminalWindowPrivate); + + gtk_widget_init_template (GTK_WIDGET (window)); + + uuid_generate (u); + uuid_unparse (u, uuidstr); + priv->uuid = g_strdup (uuidstr); + + g_signal_connect (G_OBJECT (window), "delete_event", + G_CALLBACK(terminal_window_delete_event), + nullptr); + + use_headerbar = terminal_app_get_use_headerbar (app); + if (use_headerbar) { + GtkWidget *headerbar; + + headerbar = terminal_headerbar_new (); + gtk_window_set_titlebar (GTK_WINDOW (window), headerbar); + } + + gtk_window_set_title (GTK_WINDOW (window), _("Terminal")); + + priv->active_screen = nullptr; + + priv->main_vbox = gtk_bin_get_child (GTK_BIN (window)); + + priv->mdi_container = TERMINAL_MDI_CONTAINER (terminal_notebook_new ()); + + g_signal_connect (priv->mdi_container, "screen-close-request", + G_CALLBACK (screen_close_request_cb), window); + + g_signal_connect_after (priv->mdi_container, "screen-switched", + G_CALLBACK (mdi_screen_switched_cb), window); + g_signal_connect_after (priv->mdi_container, "screen-added", + G_CALLBACK (mdi_screen_added_cb), window); + g_signal_connect_after (priv->mdi_container, "screen-removed", + G_CALLBACK (mdi_screen_removed_cb), window); + g_signal_connect_after (priv->mdi_container, "screens-reordered", + G_CALLBACK (mdi_screens_reordered_cb), window); + + g_signal_connect_swapped (priv->mdi_container, "notify::tab-pos", + G_CALLBACK (terminal_window_update_geometry), window); + g_signal_connect_swapped (priv->mdi_container, "notify::show-tabs", + G_CALLBACK (terminal_window_update_geometry), window); + + /* FIXME hack hack! */ + if (GTK_IS_NOTEBOOK (priv->mdi_container)) { + g_signal_connect (priv->mdi_container, "button-press-event", + G_CALLBACK (notebook_button_press_cb), window); + g_signal_connect (priv->mdi_container, "popup-menu", + G_CALLBACK (notebook_popup_menu_cb), window); + g_signal_connect (priv->mdi_container, "create-window", + G_CALLBACK (handle_tab_droped_on_desktop), window); + } + + gtk_box_pack_end (GTK_BOX (priv->main_vbox), GTK_WIDGET (priv->mdi_container), TRUE, TRUE, 0); + gtk_widget_show (GTK_WIDGET (priv->mdi_container)); + + priv->old_char_width = -1; + priv->old_char_height = -1; + + priv->old_chrome_width = -1; + priv->old_chrome_height = -1; + priv->old_csd_width = -1; + priv->old_csd_height = -1; + priv->old_padding_width = -1; + priv->old_padding_height = -1; + + priv->old_geometry_widget = nullptr; + + /* GAction setup */ + g_action_map_add_action_entries (G_ACTION_MAP (window), + action_entries, G_N_ELEMENTS (action_entries), + window); + + g_simple_action_set_enabled (lookup_action (window, "leave-fullscreen"), FALSE); + + GSettings *global_settings = terminal_app_get_global_settings (app); + enable_mnemonics_changed_cb (global_settings, TERMINAL_SETTING_ENABLE_MNEMONICS_KEY, window); + g_signal_connect (global_settings, "changed::" TERMINAL_SETTING_ENABLE_MNEMONICS_KEY, + G_CALLBACK (enable_mnemonics_changed_cb), window); + + /* Hide "menubar-visible" when the menubar is shown by the shell */ + g_object_get (gtk_widget_get_settings (GTK_WIDGET (window)), + "gtk-shell-shows-menubar", &shell_shows_menubar, + nullptr); + if (shell_shows_menubar) { + g_simple_action_set_enabled (lookup_action (window, "menubar-visible"), FALSE); + } else { + priv->menubar = gtk_menu_bar_new_from_model (terminal_app_get_menubar (app)); + gtk_box_pack_start (GTK_BOX (priv->main_vbox), + priv->menubar, + FALSE, FALSE, 0); + + terminal_window_set_menubar_visible (window, !use_headerbar); + priv->use_default_menubar_visibility = !use_headerbar; + } + + /* Add "Set as default terminal" infobar */ + if (terminal_app_get_ask_default_terminal(app)) { + auto const infobar = priv->ask_default_infobar = gtk_info_bar_new(); + gtk_info_bar_set_show_close_button(GTK_INFO_BAR(infobar), true); + gtk_info_bar_set_message_type(GTK_INFO_BAR(infobar), GTK_MESSAGE_QUESTION); + + auto const question = gtk_label_new (_("Set GNOME Terminal as your default terminal?")); + gtk_label_set_line_wrap(GTK_LABEL(question), true); + auto const box = gtk_info_bar_get_content_area(GTK_INFO_BAR(infobar)); + gtk_container_add(GTK_CONTAINER(box), question); + gtk_widget_show(question); + + gtk_info_bar_add_button(GTK_INFO_BAR(infobar), _("_Yes"), GTK_RESPONSE_YES); + gtk_info_bar_add_button(GTK_INFO_BAR(infobar), _("_No"), GTK_RESPONSE_NO); + + g_signal_connect (infobar, "response", + G_CALLBACK(default_infobar_response_cb), window); + + gtk_box_pack_start(GTK_BOX(priv->main_vbox), infobar, false, true, 0); + + gtk_widget_show(infobar); + g_signal_connect(app, "notify::ask-default-terminal", + G_CALLBACK(window_sync_ask_default_terminal_cb), window); + } + + /* Maybe make Inspector available */ + action = lookup_action (window, "inspector"); + gtk_debug_settings = terminal_app_get_gtk_debug_settings (app); + if (gtk_debug_settings != nullptr) + g_settings_bind (gtk_debug_settings, + "enable-inspector-keybinding", + action, + "enabled", + GSettingsBindFlags(G_SETTINGS_BIND_GET | + G_SETTINGS_BIND_NO_SENSITIVITY)); + else + g_simple_action_set_enabled (action, FALSE); + + priv->clipboard = gtk_widget_get_clipboard (GTK_WIDGET (window), GDK_SELECTION_CLIPBOARD); + clipboard_targets_changed_cb (app, priv->clipboard, window); + g_signal_connect (app, "clipboard-targets-changed", + G_CALLBACK (clipboard_targets_changed_cb), window); + + terminal_window_fill_notebook_action_box (window, !use_headerbar); + + /* We have to explicitly call this, since screen-changed is NOT + * emitted for the toplevel the first time! + */ + terminal_window_screen_update (window, gtk_widget_get_screen (GTK_WIDGET (window))); + + window_group = gtk_window_group_new (); + gtk_window_group_add_window (window_group, GTK_WINDOW (window)); + g_object_unref (window_group); + + g_snprintf (role, sizeof (role), "gnome-terminal-window-%s", uuidstr); + gtk_window_set_role (GTK_WINDOW (window), role); +} + +static void +terminal_window_style_updated (GtkWidget *widget) +{ + TerminalWindow *window = TERMINAL_WINDOW (widget); + + GTK_WIDGET_CLASS (terminal_window_parent_class)->style_updated (widget); + + terminal_window_update_size (window); +} + +static void +terminal_window_class_init (TerminalWindowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = terminal_window_dispose; + object_class->finalize = terminal_window_finalize; + + widget_class->show = terminal_window_show; + widget_class->realize = terminal_window_realize; + widget_class->window_state_event = terminal_window_state_event; + widget_class->screen_changed = terminal_window_screen_changed; + widget_class->style_updated = terminal_window_style_updated; + + GtkWindowClass *window_klass; + GtkBindingSet *binding_set; + + window_klass = (GtkWindowClass*)g_type_class_ref (GTK_TYPE_WINDOW); + binding_set = gtk_binding_set_by_class (window_klass); + gtk_binding_entry_skip (binding_set, GDK_KEY_I, GdkModifierType(GDK_CONTROL_MASK|GDK_SHIFT_MASK)); + gtk_binding_entry_skip (binding_set, GDK_KEY_D, GdkModifierType(GDK_CONTROL_MASK|GDK_SHIFT_MASK)); + g_type_class_unref (window_klass); + + g_type_class_add_private (object_class, sizeof (TerminalWindowPrivate)); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/terminal/ui/window.ui"); + + gtk_widget_class_set_css_name(widget_class, TERMINAL_WINDOW_CSS_NAME); +} + +static void +terminal_window_dispose (GObject *object) +{ + TerminalWindow *window = TERMINAL_WINDOW (object); + TerminalWindowPrivate *priv = window->priv; + TerminalApp *app = terminal_app_get (); + + if (!priv->disposed) { + GSettings *global_settings = terminal_app_get_global_settings (app); + g_signal_handlers_disconnect_by_func (global_settings, + (void*)enable_mnemonics_changed_cb, + window); + } + + priv->disposed = TRUE; + + if (priv->clipboard != nullptr) { + g_signal_handlers_disconnect_by_func (app, + (void*)clipboard_targets_changed_cb, + window); + priv->clipboard = nullptr; + } + + if (priv->ask_default_infobar) { + g_signal_handlers_disconnect_by_func(app, + (void*)window_sync_ask_default_terminal_cb, + window); + priv->ask_default_infobar = nullptr; + } + + remove_popup_info (window); + + if (priv->search_popover != nullptr) + { + g_signal_handlers_disconnect_matched (priv->search_popover, G_SIGNAL_MATCH_DATA, + 0, 0, nullptr, nullptr, window); + gtk_widget_destroy (GTK_WIDGET (priv->search_popover)); + priv->search_popover = nullptr; + } + + G_OBJECT_CLASS (terminal_window_parent_class)->dispose (object); +} + +static void +terminal_window_finalize (GObject *object) +{ + TerminalWindow *window = TERMINAL_WINDOW (object); + TerminalWindowPrivate *priv = window->priv; + + if (priv->confirm_close_dialog) + gtk_dialog_response (GTK_DIALOG (priv->confirm_close_dialog), + GTK_RESPONSE_DELETE_EVENT); + + g_free (priv->uuid); + + G_OBJECT_CLASS (terminal_window_parent_class)->finalize (object); +} + +static gboolean +terminal_window_delete_event (GtkWidget *widget, + GdkEvent *event, + gpointer data) +{ + return confirm_close_window_or_tab (TERMINAL_WINDOW (widget), nullptr); +} + +static void +terminal_window_show (GtkWidget *widget) +{ + TerminalWindow *window = TERMINAL_WINDOW (widget); + TerminalWindowPrivate *priv = window->priv; + GtkAllocation widget_allocation; + + gtk_widget_get_allocation (widget, &widget_allocation); + + _terminal_debug_print (TERMINAL_DEBUG_GEOMETRY, + "[window %p] show, size %d : %d at (%d, %d)\n", + widget, + widget_allocation.width, widget_allocation.height, + widget_allocation.x, widget_allocation.y); + + /* Because of the unexpected reentrancy caused by adding the tab to the notebook + * showing the TerminalWindow, we can get here when the first page has been + * added but not yet set current. By setting the page current, we get the + * right size when we first show the window */ + if (GTK_IS_NOTEBOOK (priv->mdi_container) && + gtk_notebook_get_current_page (GTK_NOTEBOOK (priv->mdi_container)) == -1) + gtk_notebook_set_current_page (GTK_NOTEBOOK (priv->mdi_container), 0); + + if (priv->active_screen != nullptr) + { + /* At this point, we have our GdkScreen, and hence the right + * font size, so we can go ahead and size the window. */ + terminal_window_update_size (window); + } + + GTK_WIDGET_CLASS (terminal_window_parent_class)->show (widget); +} + +TerminalWindow* +terminal_window_new (GApplication *app) +{ + return reinterpret_cast<TerminalWindow*> + (g_object_new (TERMINAL_TYPE_WINDOW, + "application", app, + "show-menubar", FALSE, + nullptr)); +} + +static void +profile_set_cb (TerminalScreen *screen, + GSettings *old_profile, + TerminalWindow *window) +{ + TerminalWindowPrivate *priv = window->priv; + + if (!gtk_widget_get_realized (GTK_WIDGET (window))) + return; + + if (screen != priv->active_screen) + return; + + terminal_window_update_set_profile_menu_active_profile (window); +} + +static void +sync_screen_title (TerminalScreen *screen, + GParamSpec *psepc, + TerminalWindow *window) +{ + TerminalWindowPrivate *priv = window->priv; + const char *title; + + if (screen != priv->active_screen) + return; + + title = terminal_screen_get_title (screen); + gtk_window_set_title (GTK_WINDOW (window), + title && title[0] ? title : _("Terminal")); +} + +static void +screen_font_any_changed_cb (TerminalScreen *screen, + GParamSpec *psepc, + TerminalWindow *window) +{ + TerminalWindowPrivate *priv = window->priv; + + if (!gtk_widget_get_realized (GTK_WIDGET (window))) + return; + + if (screen != priv->active_screen) + return; + + terminal_window_update_size (window); +} + +static void +screen_hyperlink_hover_uri_changed (TerminalScreen *screen, + const char *uri, + const GdkRectangle *bbox G_GNUC_UNUSED, + TerminalWindow *window G_GNUC_UNUSED) +{ + gs_free char *label = nullptr; + + if (!gtk_widget_get_realized (GTK_WIDGET (screen))) + return; + + label = terminal_util_hyperlink_uri_label (uri); + + gtk_widget_set_tooltip_text (GTK_WIDGET (screen), label); +} + +/* MDI container callbacks */ + +static void +screen_close_request_cb (TerminalMdiContainer *container, + TerminalScreen *screen, + TerminalWindow *window) +{ + if (confirm_close_window_or_tab (window, screen)) + return; + + terminal_window_remove_screen (window, screen); +} + +int +terminal_window_get_active_screen_num (TerminalWindow *window) +{ + TerminalWindowPrivate *priv = window->priv; + return terminal_mdi_container_get_active_screen_num (priv->mdi_container); +} + +void +terminal_window_add_screen (TerminalWindow *window, + TerminalScreen *screen, + int position) +{ + TerminalWindowPrivate *priv = window->priv; + GtkWidget *old_window; + + old_window = gtk_widget_get_toplevel (GTK_WIDGET (screen)); + if (gtk_widget_is_toplevel (old_window) && + TERMINAL_IS_WINDOW (old_window) && + TERMINAL_WINDOW (old_window)== window) + return; + + if (TERMINAL_IS_WINDOW (old_window)) + terminal_window_remove_screen (TERMINAL_WINDOW (old_window), screen); + + if (position == -1) { + GSettings *global_settings = terminal_app_get_global_settings (terminal_app_get ()); + TerminalNewTabPosition position_pref = TerminalNewTabPosition + (g_settings_get_enum (global_settings, + TERMINAL_SETTING_NEW_TAB_POSITION_KEY)); + switch (position_pref) { + case TERMINAL_NEW_TAB_POSITION_NEXT: + position = terminal_window_get_active_screen_num (window) + 1; + break; + + default: + case TERMINAL_NEW_TAB_POSITION_LAST: + position = -1; + break; + } + } + + terminal_mdi_container_add_screen (priv->mdi_container, screen, position); +} + +void +terminal_window_remove_screen (TerminalWindow *window, + TerminalScreen *screen) +{ + TerminalWindowPrivate *priv = window->priv; + + terminal_mdi_container_remove_screen (priv->mdi_container, screen); +} + +void +terminal_window_move_screen (TerminalWindow *source_window, + TerminalWindow *dest_window, + TerminalScreen *screen, + int dest_position) +{ + TerminalScreenContainer *screen_container; + + g_return_if_fail (TERMINAL_IS_WINDOW (source_window)); + g_return_if_fail (TERMINAL_IS_WINDOW (dest_window)); + g_return_if_fail (TERMINAL_IS_SCREEN (screen)); + g_return_if_fail (gtk_widget_get_toplevel (GTK_WIDGET (screen)) == GTK_WIDGET (source_window)); + g_return_if_fail (dest_position >= -1); + + screen_container = terminal_screen_container_get_from_screen (screen); + g_assert (TERMINAL_IS_SCREEN_CONTAINER (screen_container)); + + /* We have to ref the screen container as well as the screen, + * because otherwise removing the screen container from the source + * window's notebook will cause the container and its containing + * screen to be gtk_widget_destroy()ed! + */ + g_object_ref_sink (screen_container); + g_object_ref_sink (screen); + terminal_window_remove_screen (source_window, screen); + + /* Now we can safely remove the screen from the container and let the container die */ + gtk_container_remove (GTK_CONTAINER (gtk_widget_get_parent (GTK_WIDGET (screen))), GTK_WIDGET (screen)); + g_object_unref (screen_container); + + terminal_window_add_screen (dest_window, screen, dest_position); + terminal_mdi_container_set_active_screen (dest_window->priv->mdi_container, screen); + g_object_unref (screen); +} + +GList* +terminal_window_list_screen_containers (TerminalWindow *window) +{ + TerminalWindowPrivate *priv = window->priv; + + return terminal_mdi_container_list_screen_containers (priv->mdi_container); +} + +void +terminal_window_set_menubar_visible (TerminalWindow *window, + gboolean setting) +{ + TerminalWindowPrivate *priv = window->priv; + + if (priv->menubar == nullptr) + return; + + /* it's been set now, so don't override when adding a screen. + * this side effect must happen before we short-circuit below. + */ + priv->use_default_menubar_visibility = FALSE; + + g_simple_action_set_state (lookup_action (window, "menubar-visible"), + g_variant_new_boolean (setting)); + + g_object_set (priv->menubar, "visible", setting, nullptr); + + /* FIXMEchpe: use gtk_widget_get_realized instead? */ + if (priv->active_screen) + { + _terminal_debug_print (TERMINAL_DEBUG_GEOMETRY, + "[window %p] setting size after toggling menubar visibility\n", + window); + + terminal_window_update_size (window); + } +} + +GtkWidget * +terminal_window_get_mdi_container (TerminalWindow *window) +{ + TerminalWindowPrivate *priv = window->priv; + + g_return_val_if_fail (TERMINAL_IS_WINDOW (window), nullptr); + + return GTK_WIDGET (priv->mdi_container); +} + +void +terminal_window_update_size (TerminalWindow *window) +{ + auto const priv = window->priv; + int grid_width, grid_height; + int pixel_width, pixel_height; + + if (priv->realized && + window_state_is_snapped(priv->window_state)) + { + /* Don't adjust the size of maximized or tiled (snapped, half-maximized) + * windows: if we do, there will be ugly gaps of up to 1 character cell + * around otherwise tiled windows. */ + return; + } + + if (!priv->active_screen) + return; + + /* be sure our geometry is up-to-date */ + terminal_window_update_geometry (window); + + terminal_screen_get_size (priv->active_screen, &grid_width, &grid_height); + _terminal_debug_print (TERMINAL_DEBUG_GEOMETRY, + "[window %p] size is %dx%d cells of %dx%d px\n", + window, grid_width, grid_height, + priv->old_char_width, priv->old_char_height); + + /* the "old" struct members were updated by update_geometry */ + pixel_width = priv->old_chrome_width + grid_width * priv->old_char_width; + pixel_height = priv->old_chrome_height + grid_height * priv->old_char_height; + _terminal_debug_print (TERMINAL_DEBUG_GEOMETRY, + "[window %p] %dx%d + %dx%d = %dx%d\n", + window, grid_width * priv->old_char_width, + grid_height * priv->old_char_height, + priv->old_chrome_width, priv->old_chrome_height, + pixel_width, pixel_height); + + gtk_window_resize (GTK_WINDOW (window), pixel_width, pixel_height); +} + +void +terminal_window_switch_screen (TerminalWindow *window, + TerminalScreen *screen) +{ + TerminalWindowPrivate *priv = window->priv; + + terminal_mdi_container_set_active_screen (priv->mdi_container, screen); +} + +TerminalScreen* +terminal_window_get_active (TerminalWindow *window) +{ + TerminalWindowPrivate *priv = window->priv; + + return terminal_mdi_container_get_active_screen (priv->mdi_container); +} + +static void +notebook_show_context_menu (TerminalWindow *window, + GdkEvent *event, + guint button, + guint32 timestamp) +{ + /* Load the UI */ + gs_unref_object GMenu *menu; + terminal_util_load_objects_resource ("/org/gnome/terminal/ui/notebook-menu.ui", + "notebook-popup", &menu, + nullptr); + + GtkWidget *popup_menu = context_menu_new (G_MENU_MODEL (menu), GTK_WIDGET (window)); + + gtk_widget_set_halign (popup_menu, GTK_ALIGN_START); + gtk_menu_popup (GTK_MENU (popup_menu), nullptr, nullptr, + nullptr, nullptr, + button, timestamp); + + if (button == 0) + gtk_menu_shell_select_first (GTK_MENU_SHELL (popup_menu), FALSE); +} + +static gboolean +notebook_button_press_cb (GtkWidget *widget, + GdkEventButton *event, + TerminalWindow *window) +{ + GtkNotebook *notebook = GTK_NOTEBOOK (widget); + int tab_clicked; + + if (event->type != GDK_BUTTON_PRESS || + event->button != GDK_BUTTON_SECONDARY || + (event->state & gtk_accelerator_get_default_mod_mask ()) != 0) + return FALSE; + + tab_clicked = find_tab_num_at_pos (notebook, event->x_root, event->y_root); + if (tab_clicked < 0) + return FALSE; + + /* switch to the page the mouse is over */ + gtk_notebook_set_current_page (notebook, tab_clicked); + + notebook_show_context_menu (window, (GdkEvent*)event, event->button, event->time); + return TRUE; +} + +static gboolean +notebook_popup_menu_cb (GtkWidget *widget, + TerminalWindow *window) +{ + TerminalWindowPrivate *priv = window->priv; + GtkWidget *focus_widget; + + focus_widget = gtk_window_get_focus (GTK_WINDOW (window)); + /* Only respond if the notebook is the actual focus */ + if (focus_widget != GTK_WIDGET (priv->mdi_container)) + return FALSE; + + notebook_show_context_menu (window, nullptr, 0, gtk_get_current_event_time ()); + return TRUE; +} + +static void +mdi_screen_switched_cb (TerminalMdiContainer *container, + TerminalScreen *old_active_screen, + TerminalScreen *screen, + TerminalWindow *window) +{ + TerminalWindowPrivate *priv = window->priv; + int old_grid_width, old_grid_height; + + _terminal_debug_print (TERMINAL_DEBUG_MDI, + "[window %p] MDI: screen-switched old %p new %p\n", + window, old_active_screen, screen); + + if (priv->disposed) + return; + + if (screen == nullptr || old_active_screen == screen) + return; + + if (priv->search_popover != nullptr) + gtk_widget_hide (GTK_WIDGET (priv->search_popover)); + + _terminal_debug_print (TERMINAL_DEBUG_MDI, + "[window %p] MDI: setting active tab to screen %p (old active screen %p)\n", + window, screen, priv->active_screen); + + if (old_active_screen != nullptr && screen != nullptr) { + terminal_screen_get_size (old_active_screen, &old_grid_width, &old_grid_height); + + /* This is so that we maintain the same grid */ + vte_terminal_set_size (VTE_TERMINAL (screen), old_grid_width, old_grid_height); + } + + priv->active_screen = screen; + + /* Override menubar setting if it wasn't restored from session */ + if (priv->use_default_menubar_visibility) + { + gboolean setting = + g_settings_get_boolean (terminal_app_get_global_settings (terminal_app_get ()), + TERMINAL_SETTING_DEFAULT_SHOW_MENUBAR_KEY); + + terminal_window_set_menubar_visible (window, setting); + } + + sync_screen_title (screen, nullptr, window); + + /* set size of window to current grid size */ + _terminal_debug_print (TERMINAL_DEBUG_GEOMETRY, + "[window %p] setting size after flipping notebook pages\n", + window); + terminal_window_update_size (window); + + terminal_window_update_tabs_actions_sensitivity (window); + terminal_window_update_terminal_menu (window); + terminal_window_update_set_profile_menu_active_profile (window); + terminal_window_update_copy_sensitivity (screen, window); + terminal_window_update_zoom_sensitivity (window); + terminal_window_update_search_sensitivity (screen, window); + terminal_window_update_paste_sensitivity (window); +} + +static void +mdi_screen_added_cb (TerminalMdiContainer *container, + TerminalScreen *screen, + TerminalWindow *window) +{ + TerminalWindowPrivate *priv = window->priv; + int pages; + + _terminal_debug_print (TERMINAL_DEBUG_MDI, + "[window %p] MDI: screen %p inserted\n", + window, screen); + + g_signal_connect (G_OBJECT (screen), + "profile-set", + G_CALLBACK (profile_set_cb), + window); + + /* FIXME: only connect on the active screen, not all screens! */ + g_signal_connect (screen, "notify::title", + G_CALLBACK (sync_screen_title), window); + g_signal_connect (screen, "notify::font-desc", + G_CALLBACK (screen_font_any_changed_cb), window); + g_signal_connect (screen, "notify::font-scale", + G_CALLBACK (screen_font_any_changed_cb), window); + g_signal_connect (screen, "notify::cell-height-scale", + G_CALLBACK (screen_font_any_changed_cb), window); + g_signal_connect (screen, "notify::cell-width-scale", + G_CALLBACK (screen_font_any_changed_cb), window); + g_signal_connect (screen, "selection-changed", + G_CALLBACK (terminal_window_update_copy_sensitivity), window); + g_signal_connect (screen, "hyperlink-hover-uri-changed", + G_CALLBACK (screen_hyperlink_hover_uri_changed), window); + + g_signal_connect (screen, "show-popup-menu", + G_CALLBACK (screen_show_popup_menu_cb), window); + g_signal_connect (screen, "match-clicked", + G_CALLBACK (screen_match_clicked_cb), window); + g_signal_connect (screen, "resize-window", + G_CALLBACK (screen_resize_window_cb), window); + + g_signal_connect (screen, "close-screen", + G_CALLBACK (screen_close_cb), window); + + terminal_window_update_tabs_actions_sensitivity (window); + terminal_window_update_search_sensitivity (screen, window); + terminal_window_update_paste_sensitivity (window); + +#if 0 + /* FIXMEchpe: wtf is this doing? */ + + /* If we have an active screen, match its size and zoom */ + if (priv->active_screen) + { + int current_width, current_height; + double scale; + + terminal_screen_get_size (priv->active_screen, ¤t_width, ¤t_height); + vte_terminal_set_size (VTE_TERMINAL (screen), current_width, current_height); + + scale = terminal_screen_get_font_scale (priv->active_screen); + terminal_screen_set_font_scale (screen, scale); + } +#endif + + if (priv->present_on_insert) + { + gtk_window_present_with_time (GTK_WINDOW (window), gtk_get_current_event_time ()); + priv->present_on_insert = FALSE; + } + + pages = terminal_mdi_container_get_n_screens (container); + if (pages == 2) + { + terminal_window_update_size (window); + } +} + +static void +mdi_screen_removed_cb (TerminalMdiContainer *container, + TerminalScreen *screen, + TerminalWindow *window) +{ + TerminalWindowPrivate *priv = window->priv; + int pages; + + if (priv->disposed) + return; + + _terminal_debug_print (TERMINAL_DEBUG_MDI, + "[window %p] MDI: screen %p removed\n", + window, screen); + + g_signal_handlers_disconnect_by_func (G_OBJECT (screen), + (void*)profile_set_cb, + window); + + g_signal_handlers_disconnect_by_func (G_OBJECT (screen), + (void*)sync_screen_title, + window); + + g_signal_handlers_disconnect_by_func (G_OBJECT (screen), + (void*)screen_font_any_changed_cb, + window); + + g_signal_handlers_disconnect_by_func (G_OBJECT (screen), + (void*)terminal_window_update_copy_sensitivity, + window); + + g_signal_handlers_disconnect_by_func (G_OBJECT (screen), + (void*)screen_hyperlink_hover_uri_changed, + window); + + g_signal_handlers_disconnect_by_func (screen, + (void*)screen_show_popup_menu_cb, + window); + + g_signal_handlers_disconnect_by_func (screen, + (void*)screen_match_clicked_cb, + window); + g_signal_handlers_disconnect_by_func (screen, + (void*)screen_resize_window_cb, + window); + + g_signal_handlers_disconnect_by_func (screen, + (void*)screen_close_cb, + window); + + /* We already got a switch-page signal whose handler sets the active tab to the + * new active tab, unless this screen was the only one in the notebook, so + * priv->active_tab is valid here. + */ + + pages = terminal_mdi_container_get_n_screens (container); + if (pages == 0) + { + priv->active_screen = nullptr; + + /* That was the last tab in the window; close it. */ + gtk_widget_destroy (GTK_WIDGET (window)); + return; + } + + terminal_window_update_tabs_actions_sensitivity (window); + terminal_window_update_search_sensitivity (screen, window); + + if (pages == 1) + { + TerminalScreen *active_screen = terminal_mdi_container_get_active_screen (container); + gtk_widget_grab_focus (GTK_WIDGET(active_screen)); /* bug 742422 */ + + terminal_window_update_size (window); + } +} + +static void +mdi_screens_reordered_cb (TerminalMdiContainer *container, + TerminalWindow *window) +{ + terminal_window_update_tabs_actions_sensitivity (window); +} + +gboolean +terminal_window_parse_geometry (TerminalWindow *window, + const char *geometry) +{ + TerminalWindowPrivate *priv = window->priv; + + /* gtk_window_parse_geometry() needs to have the right base size + * and width/height increment to compute the window size from + * the geometry. + */ + terminal_window_update_geometry (window); + + if (!gtk_window_parse_geometry (GTK_WINDOW (window), geometry)) + return FALSE; + + /* We won't actually get allocated at the size parsed out of the + * geometry until the window is shown. If terminal_window_update_size() + * is called between now and then, that could result in us getting + * snapped back to the old grid size. So we need to immediately + * update the size of the active terminal to grid size from the + * geometry. + */ + if (priv->active_screen) + { + int grid_width, grid_height; + + /* After parse_geometry(), the default size is in units of the + * width/height increment, not a pixel size */ + gtk_window_get_default_size (GTK_WINDOW (window), &grid_width, &grid_height); + + vte_terminal_set_size (VTE_TERMINAL (priv->active_screen), + grid_width, grid_height); + } + + return TRUE; +} + +void +terminal_window_update_geometry (TerminalWindow *window) +{ + TerminalWindowPrivate *priv = window->priv; + GtkWidget *widget; + GdkGeometry *hints = &priv->hints; + GtkBorder padding; + GtkRequisition vbox_request, widget_request; + int grid_width, grid_height; + int char_width, char_height; + int chrome_width, chrome_height; + int csd_width = 0, csd_height = 0; + + if (gtk_widget_in_destruction (GTK_WIDGET (window))) + return; + + if (priv->active_screen == nullptr) + return; + + widget = GTK_WIDGET (priv->active_screen); + + /* We set geometry hints from the active term; best thing + * I can think of to do. Other option would be to try to + * get some kind of union of all hints from all terms in the + * window, but that doesn't make too much sense. + */ + terminal_screen_get_cell_size (priv->active_screen, &char_width, &char_height); + + terminal_screen_get_size (priv->active_screen, &grid_width, &grid_height); + _terminal_debug_print (TERMINAL_DEBUG_GEOMETRY, "%dx%d cells of %dx%d px = %dx%d px\n", + grid_width, grid_height, char_width, char_height, + char_width * grid_width, char_height * grid_height); + + gtk_style_context_get_padding(gtk_widget_get_style_context(widget), + gtk_widget_get_state_flags(widget), + &padding); + + _terminal_debug_print (TERMINAL_DEBUG_GEOMETRY, "padding = %dx%d px\n", + padding.left + padding.right, + padding.top + padding.bottom); + + gtk_widget_get_preferred_size (priv->main_vbox, nullptr, &vbox_request); + _terminal_debug_print (TERMINAL_DEBUG_GEOMETRY, "content area requests %dx%d px\n", + vbox_request.width, vbox_request.height); + + + chrome_width = vbox_request.width - (char_width * grid_width); + chrome_height = vbox_request.height - (char_height * grid_height); + _terminal_debug_print (TERMINAL_DEBUG_GEOMETRY, "chrome: %dx%d px\n", + chrome_width, chrome_height); + + if (priv->realized) + { + /* Only when having been realize the CSD can be calculated. Do this by + * using the actual allocation rather then the preferred size as the + * the preferred size takes the natural size of e.g. the title bar into + * account which can be far wider then the contents size when using a + * very long title */ + GtkAllocation toplevel_allocation, vbox_allocation; + + gtk_widget_get_allocation (GTK_WIDGET (priv->main_vbox), &vbox_allocation); + _terminal_debug_print (TERMINAL_DEBUG_GEOMETRY, + "terminal widget allocation %dx%d px\n", + vbox_allocation.width, vbox_allocation.height); + + gtk_widget_get_allocation (GTK_WIDGET (window), &toplevel_allocation); + _terminal_debug_print (TERMINAL_DEBUG_GEOMETRY, "window allocation %dx%d px\n", + toplevel_allocation.width, toplevel_allocation.height); + + csd_width = toplevel_allocation.width - vbox_allocation.width; + csd_height = toplevel_allocation.height - vbox_allocation.height; + _terminal_debug_print (TERMINAL_DEBUG_GEOMETRY, "CSDs: %dx%d px\n", + csd_width, csd_height); + } + + gtk_widget_get_preferred_size (widget, nullptr, &widget_request); + _terminal_debug_print (TERMINAL_DEBUG_GEOMETRY, "terminal widget requests %dx%d px\n", + widget_request.width, widget_request.height); + + if (!priv->realized) + { + /* Don't actually set the geometry hints until we have been realized, + * because we don't know how large the client-side decorations are going + * to be. We also avoid setting priv->old_csd_width or + * priv->old_csd_height, so that next time through this function we'll + * definitely recalculate the hints. + * + * Similarly, the size request doesn't seem to include the padding + * until we've been redrawn at least once. Don't resize the window + * until we've done that. */ + _terminal_debug_print (TERMINAL_DEBUG_GEOMETRY, "not realized yet\n"); + } + else if (window_state_is_snapped(priv->window_state)) + { + _terminal_debug_print (TERMINAL_DEBUG_GEOMETRY, + "Not applying geometry in snapped state\n"); + } + else if (char_width != priv->old_char_width || + char_height != priv->old_char_height || + padding.left + padding.right != priv->old_padding_width || + padding.top + padding.bottom != priv->old_padding_height || + chrome_width != priv->old_chrome_width || + chrome_height != priv->old_chrome_height || + csd_width != priv->old_csd_width || + csd_height != priv->old_csd_height || + widget != (GtkWidget*) priv->old_geometry_widget) + { + hints->base_width = chrome_width + csd_width; + hints->base_height = chrome_height + csd_height; + + hints->width_inc = char_width; + hints->height_inc = char_height; + + /* min size is min size of the whole window, remember. */ + hints->min_width = hints->base_width + hints->width_inc * MIN_WIDTH_CHARS; + hints->min_height = hints->base_height + hints->height_inc * MIN_HEIGHT_CHARS; + + gtk_window_set_geometry_hints (GTK_WINDOW (window), + nullptr, + hints, + GdkWindowHints(GDK_HINT_RESIZE_INC | + GDK_HINT_MIN_SIZE | + GDK_HINT_BASE_SIZE)); + + _terminal_debug_print (TERMINAL_DEBUG_GEOMETRY, + "[window %p] hints: base %dx%d min %dx%d inc %d %d\n", + window, + hints->base_width, + hints->base_height, + hints->min_width, + hints->min_height, + hints->width_inc, + hints->height_inc); + + priv->old_csd_width = csd_width; + priv->old_csd_height = csd_height; + priv->old_geometry_widget = widget; + } + else + { + _terminal_debug_print (TERMINAL_DEBUG_GEOMETRY, + "[window %p] hints: increment unchanged, not setting\n", + window); + } + + /* We need these for the size calculation in terminal_window_update_size() + * (at least under GTK >= 3.19), so we set them unconditionally. */ + priv->old_char_width = char_width; + priv->old_char_height = char_height; + priv->old_chrome_width = chrome_width; + priv->old_chrome_height = chrome_height; + priv->old_padding_width = padding.left + padding.right; + priv->old_padding_height = padding.top + padding.bottom; +} + +static void +confirm_close_response_cb (GtkWidget *dialog, + int response, + TerminalWindow *window) +{ + TerminalScreen *screen; + + screen = (TerminalScreen*)g_object_get_data (G_OBJECT (dialog), "close-screen"); + + gtk_widget_destroy (dialog); + + if (response != GTK_RESPONSE_ACCEPT) + return; + + if (screen) + terminal_window_remove_screen (window, screen); + else + gtk_widget_destroy (GTK_WIDGET (window)); +} + +/* Returns: TRUE if closing needs to wait until user confirmation; + * FALSE if the terminal or window can close immediately. + */ +static gboolean +confirm_close_window_or_tab (TerminalWindow *window, + TerminalScreen *screen) +{ + TerminalWindowPrivate *priv = window->priv; + GtkWidget *dialog; + gboolean do_confirm; + int n_tabs; + + if (priv->confirm_close_dialog) + { + /* WTF, already have one? It's modal, so how did that happen? */ + gtk_dialog_response (GTK_DIALOG (priv->confirm_close_dialog), + GTK_RESPONSE_DELETE_EVENT); + } + + do_confirm = g_settings_get_boolean (terminal_app_get_global_settings (terminal_app_get ()), + TERMINAL_SETTING_CONFIRM_CLOSE_KEY); + if (!do_confirm) + return FALSE; + + if (screen) + { + do_confirm = terminal_screen_has_foreground_process (screen, nullptr, nullptr); + n_tabs = 1; + } + else + { + GList *tabs, *t; + + do_confirm = FALSE; + + tabs = terminal_window_list_screen_containers (window); + n_tabs = g_list_length (tabs); + + for (t = tabs; t != nullptr; t = t->next) + { + TerminalScreen *terminal_screen; + + terminal_screen = terminal_screen_container_get_screen (TERMINAL_SCREEN_CONTAINER (t->data)); + if (terminal_screen_has_foreground_process (terminal_screen, nullptr, nullptr)) + { + do_confirm = TRUE; + break; + } + } + g_list_free (tabs); + } + + if (!do_confirm) + return FALSE; + + dialog = priv->confirm_close_dialog = + gtk_message_dialog_new (GTK_WINDOW (window), + GtkDialogFlags(GTK_DIALOG_MODAL | + GTK_DIALOG_DESTROY_WITH_PARENT), + GTK_MESSAGE_WARNING, + GTK_BUTTONS_CANCEL, + "%s", n_tabs > 1 ? _("Close this window?") : _("Close this terminal?")); + + if (n_tabs > 1) + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), + "%s", _("There are still processes running in some terminals in this window. " + "Closing the window will kill all of them.")); + else + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), + "%s", _("There is still a process running in this terminal. " + "Closing the terminal will kill it.")); + + gtk_window_set_title (GTK_WINDOW (dialog), ""); + + GtkWidget *remove_button = gtk_dialog_add_button (GTK_DIALOG (dialog), n_tabs > 1 ? _("C_lose Window") : _("C_lose Terminal"), GTK_RESPONSE_ACCEPT); + gtk_style_context_add_class (gtk_widget_get_style_context (remove_button), "destructive-action"); + gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT); + + g_object_set_data (G_OBJECT (dialog), "close-screen", screen); + + g_signal_connect (dialog, "destroy", + G_CALLBACK (gtk_widget_destroyed), &priv->confirm_close_dialog); + g_signal_connect (dialog, "response", + G_CALLBACK (confirm_close_response_cb), window); + + gtk_window_present (GTK_WINDOW (dialog)); + + return TRUE; +} + +void +terminal_window_request_close (TerminalWindow *window) +{ + g_return_if_fail (TERMINAL_IS_WINDOW (window)); + + if (confirm_close_window_or_tab (window, nullptr)) + return; + + gtk_widget_destroy (GTK_WIDGET (window)); +} + +const char * +terminal_window_get_uuid (TerminalWindow *window) +{ + g_return_val_if_fail (TERMINAL_IS_WINDOW (window), nullptr); + + return window->priv->uuid; +} diff --git a/src/terminal-window.hh b/src/terminal-window.hh new file mode 100644 index 0000000..e6659aa --- /dev/null +++ b/src/terminal-window.hh @@ -0,0 +1,97 @@ +/* + * Copyright © 2001 Havoc Pennington + * + * 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/>. + */ + +#ifndef TERMINAL_WINDOW_H +#define TERMINAL_WINDOW_H + +#include <gtk/gtk.h> + +#include "terminal-screen.hh" + +G_BEGIN_DECLS + +#define TERMINAL_TYPE_WINDOW (terminal_window_get_type ()) +#define TERMINAL_WINDOW(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), TERMINAL_TYPE_WINDOW, TerminalWindow)) +#define TERMINAL_WINDOW_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), TERMINAL_TYPE_WINDOW, TerminalWindowClass)) +#define TERMINAL_IS_WINDOW(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), TERMINAL_TYPE_WINDOW)) +#define TERMINAL_IS_WINDOW_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), TERMINAL_TYPE_WINDOW)) +#define TERMINAL_WINDOW_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), TERMINAL_TYPE_WINDOW, TerminalWindowClass)) + +typedef struct _TerminalWindowClass TerminalWindowClass; +typedef struct _TerminalWindowPrivate TerminalWindowPrivate; + +struct _TerminalWindow +{ + GtkApplicationWindow parent_instance; + + TerminalWindowPrivate *priv; +}; + +struct _TerminalWindowClass +{ + GtkApplicationWindowClass parent_class; +}; + +GType terminal_window_get_type (void) G_GNUC_CONST; + +TerminalWindow* terminal_window_new (GApplication *app); + +GtkUIManager *terminal_window_get_ui_manager (TerminalWindow *window); + +int terminal_window_get_active_screen_num (TerminalWindow *window); + +void terminal_window_add_screen (TerminalWindow *window, + TerminalScreen *screen, + int position); + +void terminal_window_remove_screen (TerminalWindow *window, + TerminalScreen *screen); + +void terminal_window_move_screen (TerminalWindow *source_window, + TerminalWindow *dest_window, + TerminalScreen *screen, + int dest_position); + +/* Menubar visibility is part of session state, except that + * if it isn't restored from session, the window gets the setting + * from the profile of the first screen added to the window + */ +void terminal_window_set_menubar_visible (TerminalWindow *window, + gboolean setting); + +void terminal_window_update_size (TerminalWindow *window); + +void terminal_window_switch_screen (TerminalWindow *window, + TerminalScreen *screen); +TerminalScreen* terminal_window_get_active (TerminalWindow *window); + +GList* terminal_window_list_screen_containers (TerminalWindow *window); + +gboolean terminal_window_parse_geometry (TerminalWindow *window, + const char *geometry); + +void terminal_window_update_geometry (TerminalWindow *window); + +GtkWidget* terminal_window_get_mdi_container (TerminalWindow *window); + +void terminal_window_request_close (TerminalWindow *window); + +const char *terminal_window_get_uuid (TerminalWindow *window); + +G_END_DECLS + +#endif /* TERMINAL_WINDOW_H */ diff --git a/src/terminal-window.ui b/src/terminal-window.ui new file mode 100644 index 0000000..b0f63fd --- /dev/null +++ b/src/terminal-window.ui @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <!-- interface-requires gtk+ 3.10 --> + <template class="TerminalWindow" parent="GtkApplicationWindow"> + <property name="can_focus">False</property> + <child> + <object class="GtkBox" id="main_vbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <placeholder/> + </child> + </object> + </child> + </template> +</interface> diff --git a/src/terminal.about b/src/terminal.about new file mode 100644 index 0000000..3b097d5 --- /dev/null +++ b/src/terminal.about @@ -0,0 +1,5 @@ +[About] +Authors=Behdad Esfahbod;Egmont Koblinger;Guilherme de S. Pastore;Havoc Pennington;Christian Persch;Mariano Suárez-Alvarez; +Contributors= +;Artists= +Documenters=GNOME Documentation Team <gnome-doc-list%gnome.org>; diff --git a/src/terminal.cc b/src/terminal.cc new file mode 100644 index 0000000..520b104 --- /dev/null +++ b/src/terminal.cc @@ -0,0 +1,629 @@ +/* + * Copyright © 2001, 2002 Havoc Pennington + * Copyright © 2002 Red Hat, Inc. + * Copyright © 2002 Sun Microsystems + * Copyright © 2003 Mariano Suarez-Alvarez + * Copyright © 2008, 2010, 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 <errno.h> +#include <locale.h> +#include <stdlib.h> +#include <time.h> +#include <unistd.h> +#include <sys/wait.h> + +#include <glib.h> +#include <glib/gstdio.h> +#include <gio/gio.h> +#include <glib/gi18n.h> + +#include <gtk/gtk.h> + +#include "terminal-debug.hh" +#include "terminal-defines.hh" +#include "terminal-i18n.hh" +#include "terminal-options.hh" +#include "terminal-gdbus-generated.h" +#include "terminal-defines.hh" +#include "terminal-client-utils.hh" +#include "terminal-libgsystem.hh" + +GS_DEFINE_CLEANUP_FUNCTION0(TerminalOptions*, gs_local_options_free, terminal_options_free) +#define gs_free_options __attribute__ ((cleanup(gs_local_options_free))) + +/* Wait-for-exit helper */ + +typedef struct { + GMainLoop *loop; + int status; +} RunData; + +static void +receiver_child_exited_cb (TerminalReceiver *receiver, + int status, + RunData *data) +{ + data->status = status; + + if (g_main_loop_is_running (data->loop)) + g_main_loop_quit (data->loop); +} + +static void +factory_name_owner_notify_cb (TerminalFactory *factory, + GParamSpec *pspec, + RunData *data) +{ + /* Name owner change to nullptr can only mean that the server + * went away before it could send out our child-exited signal. + * Assume the server was killed and thus our child process + * too, and return with the corresponding exit code. + */ + if (g_dbus_proxy_get_name_owner(G_DBUS_PROXY (factory)) != nullptr) + return; + + data->status = W_EXITCODE(0, SIGKILL); + + if (g_main_loop_is_running (data->loop)) + g_main_loop_quit (data->loop); +} + +static int +run_receiver (TerminalFactory *factory, + TerminalReceiver *receiver) +{ + RunData data = { g_main_loop_new (nullptr, FALSE), 0 }; + gulong receiver_exited_id = g_signal_connect (receiver, "child-exited", + G_CALLBACK (receiver_child_exited_cb), &data); + gulong factory_notify_id = g_signal_connect (factory, "notify::g-name-owner", + G_CALLBACK (factory_name_owner_notify_cb), &data); + g_main_loop_run (data.loop); + g_signal_handler_disconnect (receiver, receiver_exited_id); + g_signal_handler_disconnect (factory, factory_notify_id); + g_main_loop_unref (data.loop); + + /* Mangle the exit status */ + int exit_code; + if (WIFEXITED (data.status)) + exit_code = WEXITSTATUS (data.status); + else if (WIFSIGNALED (data.status)) + exit_code = 128 + (int) WTERMSIG (data.status); + else if (WCOREDUMP (data.status)) + exit_code = 127; + else + exit_code = 127; + + return exit_code; +} + +/* Factory helpers */ + +static gboolean +get_factory_exit_status (const char *service_name, + const char *message, + int *exit_status) +{ + gs_free char *pattern = nullptr, *number = nullptr; + gs_unref_regex GRegex *regex = nullptr; + gs_free_match_info GMatchInfo *match_info = nullptr; + gint64 v; + char *end; + GError *err = nullptr; + + pattern = g_strdup_printf ("org.freedesktop.DBus.Error.Spawn.ChildExited: Process %s exited with status (\\d+)$", + service_name); + regex = g_regex_new (pattern, GRegexCompileFlags(0), GRegexMatchFlags(0), &err); + g_assert_no_error (err); + + if (!g_regex_match (regex, message, GRegexMatchFlags(0), &match_info)) + return FALSE; + + number = g_match_info_fetch (match_info, 1); + g_assert_nonnull (number); + + errno = 0; + v = g_ascii_strtoll (number, &end, 10); + if (errno || end == number || *end != '\0' || v < 0 || v > G_MAXINT) + return FALSE; + + *exit_status = (int)v; + return TRUE; +} + +static gboolean +handle_factory_error (const char *service_name, + GError *error) +{ + int exit_status; + + if (!g_dbus_error_is_remote_error (error) || + !g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_SPAWN_CHILD_EXITED) || + !get_factory_exit_status (service_name, error->message, &exit_status)) + return FALSE; + + g_dbus_error_strip_remote_error (error); + terminal_printerr ("%s\n\n", error->message); + + switch (exit_status) { + case _EXIT_FAILURE_WRONG_ID: + terminal_printerr ("You tried to run gnome-terminal-server with elevated privileged. This is not supported.\n"); + break; + case _EXIT_FAILURE_NO_UTF8: + terminal_printerr ("The environment that gnome-terminal-server was launched with specified a non-UTF-8 locale. This is not supported.\n"); + break; + case _EXIT_FAILURE_UNSUPPORTED_LOCALE: + terminal_printerr ("The environment that gnome-terminal-server was launched with specified an unsupported locale.\n"); + break; + case _EXIT_FAILURE_GTK_INIT: + terminal_printerr ("The environment that gnome-terminal-server was launched with most likely contained an incorrect or unset \"DISPLAY\" variable.\n"); + break; + default: + break; + } + terminal_printerr ("See https://wiki.gnome.org/Apps/Terminal/FAQ#Exit_status_%d for more information.\n", exit_status); + + return TRUE; +} + +static gboolean +handle_create_instance_error (const char *service_name, + GError *error) +{ + if (handle_factory_error (service_name, error)) + return TRUE; + + g_dbus_error_strip_remote_error (error); + terminal_printerr ("Error creating terminal: %s\n", error->message); + return FALSE; /* don't abort */ +} + +static gboolean +handle_create_receiver_proxy_error (const char *service_name, + GError *error) +{ + if (handle_factory_error (service_name, error)) + return TRUE; + + g_dbus_error_strip_remote_error (error); + terminal_printerr ("Failed to create proxy for terminal: %s\n", error->message); + return FALSE; /* don't abort */ +} + +static gboolean +handle_exec_error (const char *service_name, + GError *error) +{ + if (handle_factory_error (service_name, error)) + return TRUE; + + g_dbus_error_strip_remote_error (error); + terminal_printerr ("Error: %s\n", error->message); + return FALSE; /* don't abort */ +} + +static gboolean +factory_proxy_new_for_service_name (const char *service_name, + gboolean ping_server, + gboolean connect_signals, + TerminalFactory **factory_ptr, + char **service_name_ptr, + GError **error) +{ + if (service_name == nullptr) + service_name = TERMINAL_APPLICATION_ID; + + gs_free_error GError *err = nullptr; + gs_unref_object TerminalFactory *factory = + terminal_factory_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION, + GDBusProxyFlags(G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES | + connect_signals ? 0 : G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS), + service_name, + TERMINAL_FACTORY_OBJECT_PATH, + nullptr /* cancellable */, + &err); + if (factory == nullptr) { + if (!handle_factory_error (service_name, err)) + terminal_printerr ("Error constructing proxy for %s:%s: %s\n", + service_name, TERMINAL_FACTORY_OBJECT_PATH, err->message); + g_propagate_error (error, err); + err = nullptr; + return FALSE; + } + + if (ping_server) { + /* If we try to use the environment specified server, we need to make + * sure it actually exists so we can later fall back to the default name. + * There doesn't appear to a way to fail proxy creation above if the + * unique name doesn't exist; so we do it this way. + */ + gs_unref_variant GVariant *v = g_dbus_proxy_call_sync (G_DBUS_PROXY (factory), + "org.freedesktop.DBus.Peer.Ping", + g_variant_new ("()"), + G_DBUS_CALL_FLAGS_NONE, + 1000 /* 1s */, + nullptr /* cancelleable */, + &err); + if (v == nullptr) { + g_propagate_error (error, err); + err = nullptr; + return FALSE; + } + } + + gs_transfer_out_value (factory_ptr, &factory); + *service_name_ptr = g_strdup (service_name); + return TRUE; +} + +static gboolean +factory_proxy_new (TerminalOptions *options, + TerminalFactory **factory_ptr, + char **service_name_ptr, + char **parent_screen_object_path_ptr, + GError **error) +{ + const char *service_name = options->server_app_id; + + /* If --app-id was specified, or the environment does not specify + * the server to use, create the factory proxy from the given (or default) + * name, with no fallback. + * + * If the server specified by the environment doesn't exist, fall back to the + * default server, and ignore the environment-specified parent screen. + */ + if (options->server_app_id == nullptr && + options->server_unique_name != nullptr) { + gs_free_error GError *err = nullptr; + if (factory_proxy_new_for_service_name (options->server_unique_name, + TRUE, + options->wait, + factory_ptr, + service_name_ptr, + &err)) { + *parent_screen_object_path_ptr = g_strdup (options->parent_screen_object_path); + return TRUE; + } + + terminal_printerr ("Failed to use specified server: %s\n", + err->message); + terminal_printerr ("Falling back to default server.\n"); + + /* Fall back to the default */ + service_name = nullptr; + } + + *parent_screen_object_path_ptr = nullptr; + + return factory_proxy_new_for_service_name (service_name, + FALSE, + options->wait, + factory_ptr, + service_name_ptr, + error); +} + +static bool +handle_show_preferences_remote (TerminalOptions *options, + const char *service_name) +{ + gs_free_error GError *error = nullptr; + gs_unref_object GDBusConnection *bus = nullptr; + gs_free char *object_path = nullptr; + GVariantBuilder builder; + + /* For reasons (!?), the org.gtk.Actions interface's object path + * is derived from the service name, i.e. for service name + * "foo.bar.baz" the object path is "/foo/bar/baz". + * This means that without the name (like when given only the unique name), + * we cannot activate the action. + */ + if (!service_name || + g_dbus_is_unique_name(service_name)) { + return false; + } + + bus = g_bus_get_sync (G_BUS_TYPE_SESSION, nullptr, &error); + if (bus == nullptr) { + terminal_printerr ("Failed to get session bus: %s\n", error->message); + return true; + } + + object_path = g_strdelimit (g_strdup_printf (".%s", service_name), ".", '/'); + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("(sava{sv})")); + g_variant_builder_add (&builder, "s", "preferences"); + g_variant_builder_open (&builder, G_VARIANT_TYPE ("av")); + g_variant_builder_close (&builder); + g_variant_builder_open (&builder, G_VARIANT_TYPE ("a{sv}")); + if (options->startup_id) + g_variant_builder_add (&builder, "{sv}", + "desktop-startup-id", g_variant_new_string (options->startup_id)); + if (options->activation_token) + g_variant_builder_add (&builder, "{sv}", + "activation-token", g_variant_new_string (options->activation_token)); + g_variant_builder_close (&builder); + + if (!g_dbus_connection_call_sync (bus, + service_name, + object_path, + "org.gtk.Actions", + "Activate", + g_variant_builder_end (&builder), + G_VARIANT_TYPE ("()"), + G_DBUS_CALL_FLAGS_NO_AUTO_START, + 30 * 1000 /* ms timeout */, + nullptr /* cancelleable */, + &error)) { + terminal_printerr ("Activate call failed: %s\n", error->message); + return true; + } + + return true; +} + +static void +handle_show_preferences(TerminalOptions *options, + const char *service_name) +{ + // First try remoting to the specified server + if (handle_show_preferences_remote(options, service_name)) + return; + + // If that isn't possible, launch the prefs binary directly + auto launcher = g_subprocess_launcher_new(GSubprocessFlags(0)); + gs_free auto exe = terminal_client_get_file_uninstalled(TERM_BINDIR, + TERM_LIBEXECDIR, + TERMINAL_PREFERENCES_BINARY_NAME, + G_FILE_TEST_IS_EXECUTABLE); + char *argv[2] = {exe, nullptr}; + + gs_free_error GError* error = nullptr; + if (!g_subprocess_launcher_spawnv(launcher, argv, &error)) { + terminal_printerr ("Failed to launch preferences: %s\n", error->message); + } +} + +/** + * handle_options: + * @app: + * @options: a #TerminalOptions + * @allow_resume: whether to merge the terminal configuration from the + * saved session on resume + * @wait_for_receiver: location to store the #TerminalReceiver to wait for + * + * Processes @options. It loads or saves the terminal configuration, or + * opens the specified windows and tabs. + * + * Returns: %TRUE if @options could be successfully handled, or %FALSE on + * error + */ +static gboolean +handle_options (TerminalOptions *options, + TerminalFactory *factory, + const char *service_name, + const char *parent_screen_object_path, + TerminalReceiver **wait_for_receiver) +{ + /* We need to forward the locale encoding to the server, see bug #732128 */ + const char *encoding; + g_get_charset (&encoding); + + if (options->show_preferences) { + handle_show_preferences (options, service_name); + } else { + /* Make sure we open at least one window */ + terminal_options_ensure_window (options); + } + + const char *factory_unique_name = g_dbus_proxy_get_name_owner (G_DBUS_PROXY (factory)); + + for (GList *lw = options->initial_windows; lw != nullptr; lw = lw->next) + { + InitialWindow *iw = (InitialWindow*)lw->data; + + g_assert_nonnull (iw); + + guint window_id = 0; + + gs_free char *previous_screen_object_path = nullptr; + if (iw->implicit_first_window) + previous_screen_object_path = g_strdup (parent_screen_object_path); + + /* Now add the tabs */ + for (GList *lt = iw->tabs; lt != nullptr; lt = lt->next) + { + InitialTab *it = (InitialTab*)lt->data; + g_assert_nonnull (it); + + GVariantBuilder builder; + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{sv}")); + + terminal_client_append_create_instance_options (&builder, + options->display_name, + options->startup_id, + options->activation_token, + iw->geometry, + iw->role, + it->profile ? it->profile : options->default_profile, + encoding, + it->title ? it->title : options->default_title, + it->active, + iw->start_maximized, + iw->start_fullscreen); + + /* This will be used to apply missing defaults */ + if (parent_screen_object_path != nullptr) + g_variant_builder_add (&builder, "{sv}", + "parent-screen", g_variant_new_object_path (parent_screen_object_path)); + + /* This will be used to get the parent window */ + if (previous_screen_object_path) + g_variant_builder_add (&builder, "{sv}", + "window-from-screen", g_variant_new_object_path (previous_screen_object_path)); + if (window_id) + g_variant_builder_add (&builder, "{sv}", + "window-id", g_variant_new_uint32 (window_id)); + /* Restored windows shouldn't demand attention; see bug #586308. */ + if (iw->source_tag == SOURCE_SESSION) + g_variant_builder_add (&builder, "{sv}", + "present-window", g_variant_new_boolean (FALSE)); + if (options->zoom_set || it->zoom_set) + g_variant_builder_add (&builder, "{sv}", + "zoom", g_variant_new_double (it->zoom_set ? it->zoom : options->zoom)); + if (iw->force_menubar_state) + g_variant_builder_add (&builder, "{sv}", + "show-menubar", g_variant_new_boolean (iw->menubar_state)); + + gs_free_error GError *err = nullptr; + gs_free char *object_path = nullptr; + if (!terminal_factory_call_create_instance_sync + (factory, + g_variant_builder_end (&builder), + &object_path, + nullptr /* cancellable */, + &err)) { + if (handle_create_instance_error (service_name, err)) + return FALSE; + else + continue; /* Continue processing the remaining options! */ + } + + /* Deprecated and not working on new server anymore */ + char *p = strstr (object_path, "/window/"); + if (p) { + char *end = nullptr; + guint64 value; + + errno = 0; + p += strlen ("/window/"); + value = g_ascii_strtoull (p, &end, 10); + if (errno == 0 && end != p && *end == '/') + window_id = (guint) value; + } + + g_free (previous_screen_object_path); + previous_screen_object_path = g_strdup (object_path); + + gs_unref_object TerminalReceiver *receiver = + terminal_receiver_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION, + GDBusProxyFlags(G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES | + (it->wait ? 0 : G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS)), + factory_unique_name, + object_path, + nullptr /* cancellable */, + &err); + if (receiver == nullptr) { + if (handle_create_receiver_proxy_error (service_name, err)) + return FALSE; + else + continue; /* Continue processing the remaining options! */ + } + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{sv}")); + + char **argv = it->exec_argv ? it->exec_argv : options->exec_argv; + int argc = argv ? g_strv_length (argv) : 0; + + PassFdElement *fd_array = it->fd_array ? (PassFdElement*)it->fd_array->data : nullptr; + gsize fd_array_len = it->fd_array ? it->fd_array->len : 0; + + terminal_client_append_exec_options (&builder, + !options->no_environment, + it->working_dir ? it->working_dir + : options->default_working_dir, + fd_array, fd_array_len, + argc == 0); + + if (!terminal_receiver_call_exec_sync (receiver, + g_variant_builder_end (&builder), + g_variant_new_bytestring_array ((const char * const *) argv, argc), + it->fd_list, nullptr /* outfdlist */, + nullptr /* cancellable */, + &err)) { + if (handle_exec_error (service_name, err)) + return FALSE; + else + continue; /* Continue processing the remaining options! */ + } + + if (it->wait) + gs_transfer_out_value (wait_for_receiver, &receiver); + + if (options->print_environment) + g_print ("%s=%s\n", TERMINAL_ENV_SCREEN, object_path); + } + } + + return TRUE; +} + +int +main (int argc, char **argv) +{ + int exit_code = EXIT_FAILURE; + + g_log_set_writer_func (terminal_log_writer, nullptr, nullptr); + + g_set_prgname ("gnome-terminal"); + + setlocale (LC_ALL, ""); + + terminal_i18n_init (TRUE); + + _terminal_debug_init (); + + gs_free_error GError *error = nullptr; + gs_free_options TerminalOptions *options = terminal_options_parse (&argc, &argv, &error); + if (options == nullptr) { + terminal_printerr (_("Failed to parse arguments: %s\n"), error->message); + return exit_code; + } + + g_set_application_name (_("Terminal")); + + gs_unref_object TerminalFactory *factory = nullptr; + gs_free char *service_name = nullptr; + gs_free char *parent_screen_object_path = nullptr; + if (!factory_proxy_new (options, + &factory, + &service_name, + &parent_screen_object_path, + &error)) + return exit_code; + + if (options->print_environment) { + const char *name_owner = g_dbus_proxy_get_name_owner (G_DBUS_PROXY (factory)); + if (name_owner != nullptr) + g_print ("%s=%s\n", TERMINAL_ENV_SERVICE_NAME, name_owner); + else + return exit_code; + } + + TerminalReceiver *receiver = nullptr; + if (!handle_options (options, factory, service_name, parent_screen_object_path, &receiver)) + return exit_code; + + if (receiver != nullptr) { + exit_code = run_receiver (factory, receiver); + g_object_unref (receiver); + } else + exit_code = EXIT_SUCCESS; + + return exit_code; +} diff --git a/src/terminal.common.css b/src/terminal.common.css new file mode 100644 index 0000000..4c57726 --- /dev/null +++ b/src/terminal.common.css @@ -0,0 +1,22 @@ +/* Generic theme-independent CSS file */ + +terminal-window scrolledwindow undershoot.top, +terminal-window scrolledwindow undershoot.bottom, +terminal-window scrolledwindow overshoot.top, +terminal-window scrolledwindow overshoot.bottom +{ + background: none; +} + +popover button.model +{ + padding-left: 16px; + padding-right: 16px; +} + +.disclosure-button +{ + padding-left: 4px; + padding-right: 4px; + min-width: 0; +} diff --git a/src/terminal.gresource.xml b/src/terminal.gresource.xml new file mode 100644 index 0000000..dfa8f5c --- /dev/null +++ b/src/terminal.gresource.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright © 2012 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, or (at your option) + any later version. + + This program is distributed in the hope conf 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/>. +--> +<gresources> + <gresource prefix="/org/gnome/terminal"> + <file alias="css/terminal.css" compressed="true">terminal.common.css</file> + <file alias="ui/headerbar.ui" compressed="true" preprocess="xml-stripblanks">terminal-headerbar.ui</file> + <file alias="ui/headerbar-menu.ui" compressed="true" preprocess="xml-stripblanks">terminal-headermenu.ui</file> + <file alias="ui/menubar-with-mnemonics.ui" compressed="true" preprocess="xml-stripblanks">terminal-menubar-with-mnemonics.ui</file> + <file alias="ui/menubar-without-mnemonics.ui" compressed="true" preprocess="xml-stripblanks">terminal-menubar-without-mnemonics.ui</file> + <file alias="ui/notebook-menu.ui" compressed="true" preprocess="xml-stripblanks">terminal-notebook-menu.ui</file> + <file alias="ui/search-popover.ui" compressed="true" preprocess="xml-stripblanks">search-popover.ui</file> + <file alias="ui/terminal.about" compressed="true">terminal.about</file> + <file alias="ui/window.ui" compressed="true" preprocess="xml-stripblanks">terminal-window.ui</file> + </gresource> +</gresources> |