diff --git a/io.elementary.terminal.yml b/io.elementary.terminal.yml index d187bb21c0..02469def80 100644 --- a/io.elementary.terminal.yml +++ b/io.elementary.terminal.yml @@ -47,4 +47,4 @@ modules: - type: dir path: . run-tests: true - + diff --git a/meson.build b/meson.build index 4a2cc42070..a0d3d9487a 100644 --- a/meson.build +++ b/meson.build @@ -25,6 +25,7 @@ granite_dep = dependency('granite-7', version: '>=7.5') adwaita_dep = dependency('libadwaita-1', version: '>=1.5') vte_dep = dependency('vte-2.91-gtk4', version: '>=0.76') pcre2_dep = dependency('libpcre2-8', version: '>=10.4') # Perl Regular Expression library +portal_dep = dependency ('libportal') posix_dep = valac.find_library('posix') linux_dep = valac.find_library('linux', required: false) m_dep = cc.find_library('m', required : false) diff --git a/src/Application.vala b/src/Application.vala index f6c1104aa8..e90e99af6a 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -16,12 +16,12 @@ public class Terminal.Application : Gtk.Application { public bool is_testing { get; set construct; } - public static bool is_running_in_flatpak; - private static Themes themes; - - static construct { - is_running_in_flatpak = FileUtils.test ("/.flatpak-info", FileTest.IS_REGULAR); + public static Terminal.PortalHelper portal_helper; + public static Terminal.DBus dbus_helper; + public static bool is_running_in_flatpak { + get { return portal_helper.is_running_in_flatpak; } } + private static Themes themes; public Application () { Object ( @@ -54,6 +54,12 @@ public class Terminal.Application : Gtk.Application { window_to_present.present (); }); + + portal_helper = PortalHelper.get_instance (); + portal_helper.action_invoked.connect ((id, action, target) => { + warning ("action invoked received"); + }); + add_main_option ("version", 'v', 0, OptionArg.NONE, _("Show version"), null); // -n flag forces a new window add_main_option ("new-window", 'n', 0, OptionArg.NONE, _("Open a new terminal window"), null); @@ -69,6 +75,8 @@ public class Terminal.Application : Gtk.Application { add_main_option ( "commandline", 'x', 0, OptionArg.FILENAME, _("Run remainder of line as a command in terminal"), "COMMAND" ); + + dbus_helper = new Terminal.DBus (); } protected override bool local_command_line (ref unowned string[] args, out int exit_status) { @@ -156,12 +164,19 @@ public class Terminal.Application : Gtk.Application { } protected override bool dbus_register (DBusConnection connection, string object_path) throws Error { - base.dbus_register (connection, object_path); - var dbus = new DBus (); - dbus_id = connection.register_object (object_path, dbus); + if (!is_running_in_flatpak) { + // Connects to a DBus signal emitted due to insertion of + // SEND_PROCESS_FINISHED_BASH into the Bash prompt (see TerminalWidget). + // This does not seem to work in Flatpak. + base.dbus_register (connection, object_path); + dbus_id = connection.register_object (object_path, dbus_helper); + } else { + // In Flatpak we will emit `dbus_helper.finished_process` signal ourselves + // when polling detects the foreground process has ended + } - dbus.finished_process.connect ((id, process, exit_status) => { + dbus_helper.finished_process.connect ((terminal_id, process, exit_status) => { TerminalWidget terminal = null; foreach (var window in (List) get_windows ()) { @@ -169,60 +184,94 @@ public class Terminal.Application : Gtk.Application { break; } - terminal = window.get_terminal (id); + terminal = window.get_terminal (terminal_id); } if (terminal == null) { return; } else if (!terminal.is_init_complete ()) { - terminal.set_init_complete (); + // Suppress startup notifications return; } - var process_string = _("Process completed"); - var process_icon = new ThemedIcon ("process-completed-symbolic"); - if (exit_status != 0) { - process_string = _("Process exited with errors"); - process_icon = new ThemedIcon ("process-error-symbolic"); + var title = exit_status != 0 ? _("Process exited with errors") : _("Process completed"); + var process_icon = exit_status != 0 ? "process-error-symbolic" : "process-completed-symbolic"; + + if (terminal.main_window.is_active && terminal == terminal.main_window.current_terminal) { + // No notifications for active window with terminal visible + return; } if (terminal != terminal.main_window.current_terminal) { - terminal.tab.icon = process_icon; + terminal.tab.icon = new ThemedIcon (process_icon); } + var action_name = "app.process-finished"; + var target = new Variant.string (terminal_id); + var notification_id = "process-finished-%s".printf (terminal_id); if (!(get_active_window ().is_active)) { - var notification = new Notification (process_string); - notification.set_body (process); - notification.set_icon (process_icon); - notification.set_default_action_and_target_value ("app.process-finished", new Variant.string (id)); - send_notification ("process-finished-%s".printf (id), notification); - + if (is_running_in_flatpak) { + portal_helper.send_notification ( + notification_id, + title, + process, + process_icon, + action_name, + target + ); + } else { + var notification = new Notification (title); + notification.set_body (process); + notification.set_icon (new ThemedIcon (process_icon)); + notification.set_default_action_and_target_value ( + action_name, + target + ); + send_notification (notification_id, notification); + } ulong tab_change_handler = 0; ulong focus_in_handler = 0; - tab_change_handler = terminal.main_window.notify["current-terminal"].connect (() => { - withdraw_notification_for_terminal (terminal, id, tab_change_handler, focus_in_handler); - }); - - focus_in_handler = terminal.main_window.notify["is-active"].connect (() => { - if (terminal.main_window.is_active) { - withdraw_notification_for_terminal (terminal, id, tab_change_handler, focus_in_handler); + tab_change_handler = terminal.main_window.notify["current-terminal"].connect ( + () => { + withdraw_notification_for_terminal ( + terminal, notification_id, tab_change_handler, focus_in_handler + ); + } + ); + + focus_in_handler = terminal.main_window.notify["is-active"].connect ( + () => { + if (terminal.main_window.is_active) { + withdraw_notification_for_terminal ( + terminal, notification_id, tab_change_handler, focus_in_handler + ); + } } - }); + ); } }); return true; } - private void withdraw_notification_for_terminal (TerminalWidget terminal, string id, ulong tab_change_handler, ulong focus_in_handler) { + private void withdraw_notification_for_terminal ( + TerminalWidget terminal, + string notification_id, + ulong tab_change_handler, + ulong focus_in_handler + ) { if (terminal.main_window.current_terminal != terminal) { return; } - terminal.tab.icon = null; - withdraw_notification ("process-finished-%s".printf (id)); + if (is_running_in_flatpak) { + portal_helper.withdraw_notification (notification_id); + } else { + withdraw_notification (notification_id); + } + terminal.tab.icon = null; terminal.main_window.disconnect (tab_change_handler); terminal.main_window.disconnect (focus_in_handler); } diff --git a/src/Flatpak/Portal.vala b/src/Flatpak/Portal.vala new file mode 100644 index 0000000000..4df110bbc7 --- /dev/null +++ b/src/Flatpak/Portal.vala @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2021 elementary, Inc. (https://elementary.io) + * + * 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 . + * + */ + +public class Terminal.PortalHelper : Object { + private static PortalHelper? instance = null; + public static PortalHelper get_instance () { + if (instance == null) { + instance = new PortalHelper (); + } + + return instance; + } + + private Xdp.Portal xdp_portal; + private PortalHelper () {} + + public bool is_running_in_flatpak { + get { return xdp_portal.running_under_flatpak (); } + } + + public signal void action_invoked (string id, string? action, Variant? target); + + construct { + xdp_portal = new Xdp.Portal (); + xdp_portal.notification_action_invoked.connect ((id, action, target) => { + action_invoked (id, action, target); + }); + } + + public void send_notification ( + string notification_id, + string title, + string process, + string process_icon_name, + string action_name, + Variant target + ) { + var process_icon = new ThemedIcon (process_icon_name); + var builder = new GLib.VariantBuilder (VariantType.VARDICT); + builder.add ("{sv}", "title", new GLib.Variant.string (title)); + builder.add ("{sv}", "body", new GLib.Variant.string (process)); + builder.add ("{sv}", "icon", process_icon.serialize ()); + builder.add ("{sv}", "default-action", new GLib.Variant.string (action_name)); + builder.add ("{sv}", "target", target); + + + xdp_portal.add_notification.begin ( + notification_id, + builder.end (), + NONE, + null, + (obj, res) => { + try { + var success = xdp_portal.add_notification.end (res); + } catch (Error e) { + warning ("Notification failed. %s", e.message); + } + } + ); + } + + public void withdraw_notification (string notification_id) { + xdp_portal.remove_notification (notification_id); + } +} diff --git a/src/Widgets/TerminalWidget.vala b/src/Widgets/TerminalWidget.vala index 822ec5a5f6..37a905862b 100644 --- a/src/Widgets/TerminalWidget.vala +++ b/src/Widgets/TerminalWidget.vala @@ -775,7 +775,6 @@ namespace Terminal { string _dir = GLib.Environment.get_current_dir (), string command = "" ) { - Array argv = new Array (); Array envv = new Array (); bool flatpak = Terminal.Application.is_running_in_flatpak; @@ -864,9 +863,14 @@ namespace Terminal { // but after respawning the shell, the terminal widget may have extraneous // content. feed_child ("clear\n".data); + // Suppress spurious notifications while starting up by + // delaying setting initialisation complete + Timeout.add (50, () => { + set_init_complete (); + return Source.REMOVE; + }); } - // delegate void TerminalSpawnAsyncCallback (Terminal terminal, Pid pid, Error error); // The following function is derived from the work of [BlackBox] tweaked to make interface // more like Vte.spawn_async private GLib.Cancellable? fp_spawn_host_command_callback_cancellable = null; @@ -1193,6 +1197,16 @@ namespace Terminal { return Source.CONTINUE; // Continue to poll while there is a fg process to detect it ending. } else if (fg_pid != child_pid && fg_pid != -1) { // Process just ended debug ("process finished"); + if (Terminal.Application.is_running_in_flatpak) { + // Send signal via dbus_helper for now to ensure consistency with + // non-flatpak code + Terminal.Application.dbus_helper.process_finished ( + this.terminal_id, + program_string, + 0 // Cannot currently get exit status so assume success + ); + } + fg_pid = -1; program_string = ""; foreground_process_changed (); diff --git a/src/meson.build b/src/meson.build index 109c8a4ad6..467542ef15 100644 --- a/src/meson.build +++ b/src/meson.build @@ -9,6 +9,7 @@ terminal_deps = [ vte_dep, posix_dep, linux_dep, + portal_dep, m_dep ] @@ -17,6 +18,7 @@ terminal_sources = [ 'Dialogs/ForegroundProcessDialog.vala', 'Dialogs/UnsafePasteDialog.vala', 'Flatpak/Utils.vala', + 'Flatpak/Portal.vala', 'Widgets/SearchToolbar.vala', 'Widgets/SettingsPopover.vala', 'Widgets/TerminalView.vala',