[gnome-builder/wip/gtk4-port] libide/terminal: rebuild terminal launches on run commands



commit a367cf0cbf51777f03f9e8532ec8109a77de302e
Author: Christian Hergert <chergert redhat com>
Date:   Mon Jun 27 17:17:19 2022 -0700

    libide/terminal: rebuild terminal launches on run commands
    
    This allows us to simplify a whole lot of code and delete all the
    divergance in terminal launchers.

 src/libide/gui/ide-frame.ui                        |   3 +-
 src/libide/terminal/ide-terminal-launcher.c        | 859 +++------------------
 src/libide/terminal/ide-terminal-launcher.h        |  57 +-
 src/libide/terminal/ide-terminal-page.c            |  46 +-
 src/libide/terminal/ide-terminal-page.h            |   2 +
 src/libide/terminal/ide-terminal-private.h         |   6 +-
 .../terminal/ide-terminal-run-command-private.h    |  43 ++
 src/libide/terminal/ide-terminal-run-command.c     | 142 ++++
 src/libide/terminal/meson.build                    |   2 +
 src/plugins/shellcmd/gbp-shellcmd-run-command.c    |  93 ---
 src/plugins/shellcmd/gbp-shellcmd-run-command.h    |   2 -
 .../shellcmd/gbp-shellcmd-shortcut-provider.c      |  13 +-
 .../terminal/gbp-terminal-workspace-addin.c        | 391 ++++------
 src/plugins/terminal/gtk/keybindings.json          |   6 +-
 src/plugins/terminal/gtk/menus.ui                  |  16 +-
 15 files changed, 491 insertions(+), 1190 deletions(-)
---
diff --git a/src/libide/gui/ide-frame.ui b/src/libide/gui/ide-frame.ui
index 90e978f9d..8c9e363e2 100644
--- a/src/libide/gui/ide-frame.ui
+++ b/src/libide/gui/ide-frame.ui
@@ -108,7 +108,8 @@
                 <child>
                   <object class="GtkButton">
                     <property name="label" translatable="yes">New Terminal</property>
-                    <property name="action-name">win.new-terminal</property>
+                    <property name="action-name">terminal.terminal-on-host</property>
+                    <property name="action-target">''</property>
                   </object>
                 </child>
               </object>
diff --git a/src/libide/terminal/ide-terminal-launcher.c b/src/libide/terminal/ide-terminal-launcher.c
index cc98d82f3..728727f4a 100644
--- a/src/libide/terminal/ide-terminal-launcher.c
+++ b/src/libide/terminal/ide-terminal-launcher.c
@@ -23,176 +23,31 @@
 #include "config.h"
 
 #include <errno.h>
+
 #include <glib/gi18n.h>
+
 #include <libide-foundry.h>
 #include <libide-threading.h>
 
-#include "ide-private.h"
-
 #include "ide-terminal-launcher.h"
-#include "ide-terminal-private.h"
-#include "ide-terminal-util.h"
-
-typedef enum
-{
-  LAUNCHER_KIND_HOST = 0,
-  LAUNCHER_KIND_DEBUG,
-  LAUNCHER_KIND_RUNTIME,
-  LAUNCHER_KIND_RUNNER,
-  LAUNCHER_KIND_LAUNCHER,
-  LAUNCHER_KIND_CONFIG,
-} LauncherKind;
 
 struct _IdeTerminalLauncher
 {
-  GObject                parent_instance;
-  gchar                 *cwd;
-  gchar                 *shell;
-  gchar                 *title;
-  gchar                **args;
-  IdeRuntime            *runtime;
-  IdeContext            *context;
-  IdeConfig             *config;
-  IdeSubprocessLauncher *launcher;
-  LauncherKind           kind;
+  GObject        parent_instance;
+  IdeRunCommand *run_command;
+  IdeContext    *context;
 };
 
 G_DEFINE_FINAL_TYPE (IdeTerminalLauncher, ide_terminal_launcher, G_TYPE_OBJECT)
 
 enum {
   PROP_0,
-  PROP_ARGS,
-  PROP_CWD,
-  PROP_SHELL,
-  PROP_TITLE,
+  PROP_CONTEXT,
+  PROP_RUN_COMMAND,
   N_PROPS
 };
 
 static GParamSpec *properties [N_PROPS];
-static const struct {
-  const gchar *key;
-  const gchar *value;
-} default_environment[] = {
-  { "INSIDE_GNOME_BUILDER", PACKAGE_VERSION },
-  { "TERM", "xterm-256color" },
-};
-
-static gboolean
-shell_supports_login (const gchar *shell)
-{
-  g_autofree gchar *name = NULL;
-
-  /* Shells that support --login */
-  static const gchar *supported[] = {
-    "sh", "bash",
-  };
-
-  if (shell == NULL)
-    return FALSE;
-
-  if (!(name = g_path_get_basename (shell)))
-    return FALSE;
-
-  for (guint i = 0; i < G_N_ELEMENTS (supported); i++)
-    {
-      if (g_str_equal (name, supported[i]))
-        return TRUE;
-    }
-
-  return FALSE;
-}
-
-static void
-copy_envvars (gpointer instance)
-{
-  static const gchar *copy_env[] = {
-    "AT_SPI_BUS_ADDRESS",
-    "COLORTERM",
-    "DBUS_SESSION_BUS_ADDRESS",
-    "DBUS_SYSTEM_BUS_ADDRESS",
-    "DESKTOP_SESSION",
-    "DISPLAY",
-    "LANG",
-    "SHELL",
-    "SSH_AUTH_SOCK",
-    "USER",
-    "WAYLAND_DISPLAY",
-    "XAUTHORITY",
-    "XDG_CURRENT_DESKTOP",
-#if 0
-    /* Can't copy these as they could mess up Flatpak */
-    "XDG_DATA_DIRS",
-    "XDG_RUNTIME_DIR",
-#endif
-    "XDG_MENU_PREFIX",
-    "XDG_SEAT",
-    "XDG_SESSION_DESKTOP",
-    "XDG_SESSION_ID",
-    "XDG_SESSION_TYPE",
-    "XDG_VTNR",
-  };
-  const gchar * const *host_environ;
-  IdeEnvironment *env = NULL;
-
-  g_assert (IDE_IS_SUBPROCESS_LAUNCHER (instance) || IDE_IS_RUNNER (instance));
-
-  if (IDE_IS_RUNNER (instance))
-    env = ide_runner_get_environment (instance);
-
-  host_environ = _ide_host_environ ();
-
-  for (guint i = 0; i < G_N_ELEMENTS (copy_env); i++)
-    {
-      const gchar *val = g_environ_getenv ((gchar **)host_environ, copy_env[i]);
-
-      if (val != NULL)
-        {
-          if (IDE_IS_SUBPROCESS_LAUNCHER (instance))
-            ide_subprocess_launcher_setenv (instance, copy_env[i], val, FALSE);
-          else
-            ide_environment_setenv (env, copy_env[i], val);
-        }
-    }
-}
-
-static void
-apply_pipeline_info (gpointer   instance,
-                     IdeObject *object)
-{
-  g_autoptr(GFile) workdir = NULL;
-  IdeEnvironment *env = NULL;
-  IdeContext *context;
-
-  g_assert (IDE_IS_SUBPROCESS_LAUNCHER (instance) || IDE_IS_RUNNER (instance));
-  g_assert (IDE_IS_OBJECT (object));
-
-  context = ide_object_get_context (object);
-  workdir = ide_context_ref_workdir (context);
-
-  if (IDE_IS_RUNNER (instance))
-    env = ide_runner_get_environment (instance);
-
-  if (IDE_IS_SUBPROCESS_LAUNCHER (instance))
-    ide_subprocess_launcher_setenv (instance, "SRCDIR", g_file_peek_path (workdir), FALSE);
-  else
-    ide_environment_setenv (env, "SRCDIR", g_file_peek_path (workdir));
-
-  if (ide_context_has_project (context))
-    {
-      IdeBuildManager *build_manager = ide_build_manager_from_context (context);
-      IdePipeline *pipeline = ide_build_manager_get_pipeline (build_manager);
-
-      if (pipeline != NULL)
-        {
-          const gchar *builddir = ide_pipeline_get_builddir (pipeline);
-
-          if (IDE_IS_SUBPROCESS_LAUNCHER (instance))
-            ide_subprocess_launcher_setenv (instance, "BUILDDIR", builddir, FALSE);
-          else
-            ide_environment_setenv (env, "BUILDDIR", builddir);
-        }
-    }
-}
 
 static void
 ide_terminal_launcher_wait_check_cb (GObject      *object,
@@ -203,6 +58,9 @@ ide_terminal_launcher_wait_check_cb (GObject      *object,
   g_autoptr(IdeTask) task = user_data;
   g_autoptr(GError) error = NULL;
 
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (IDE_IS_SUBPROCESS (subprocess));
   g_assert (G_IS_ASYNC_RESULT (result));
   g_assert (IDE_IS_TASK (task));
@@ -211,281 +69,8 @@ ide_terminal_launcher_wait_check_cb (GObject      *object,
     ide_task_return_error (task, g_steal_pointer (&error));
   else
     ide_task_return_boolean (task, TRUE);
-}
-
-static void
-spawn_host_launcher (IdeTerminalLauncher *self,
-                     IdeTask             *task,
-                     gint                 pty_fd,
-                     gboolean             run_on_host)
-{
-  g_autoptr(IdeSubprocessLauncher) launcher = NULL;
-  g_autoptr(IdeSubprocess) subprocess = NULL;
-  g_autoptr(GError) error = NULL;
-  const gchar *shell;
-
-  g_assert (IDE_IS_TERMINAL_LAUNCHER (self));
-  g_assert (IDE_IS_TASK (task));
-  g_assert (pty_fd >= 0);
-
-  if (!(shell = ide_terminal_launcher_get_shell (self)))
-    shell = ide_get_user_shell ();
-
-  /* We only have sh/bash in our flatpak */
-  if (self->kind == LAUNCHER_KIND_DEBUG && ide_is_flatpak ())
-    shell = "/bin/bash";
-
-  launcher = ide_subprocess_launcher_new (0);
-  ide_subprocess_launcher_set_run_on_host (launcher, run_on_host);
-  ide_subprocess_launcher_set_cwd (launcher, self->cwd ? self->cwd : g_get_home_dir ());
-  ide_subprocess_launcher_set_clear_env (launcher, FALSE);
-
-  ide_subprocess_launcher_push_argv (launcher, shell);
-  if (shell_supports_login (shell))
-    ide_subprocess_launcher_push_argv (launcher, "--login");
-
-  ide_subprocess_launcher_take_stdin_fd (launcher, dup (pty_fd));
-  ide_subprocess_launcher_take_stdout_fd (launcher, dup (pty_fd));
-  ide_subprocess_launcher_take_stderr_fd (launcher, dup (pty_fd));
-
-  g_assert (ide_subprocess_launcher_get_needs_tty (launcher));
-
-  for (guint i = 0; i < G_N_ELEMENTS (default_environment); i++)
-    ide_subprocess_launcher_setenv (launcher,
-                                    default_environment[i].key,
-                                    default_environment[i].value,
-                                    FALSE);
-
-  ide_subprocess_launcher_setenv (launcher, "SHELL", shell, TRUE);
-
-  if (self->context != NULL)
-    {
-      g_autoptr(GFile) workdir = ide_context_ref_workdir (self->context);
-
-      ide_subprocess_launcher_setenv (launcher,
-                                      "SRCDIR",
-                                      g_file_peek_path (workdir),
-                                      FALSE);
-    }
-
-  if (!(subprocess = ide_subprocess_launcher_spawn (launcher, NULL, &error)))
-    ide_task_return_error (task, g_steal_pointer (&error));
-  else
-    ide_subprocess_wait_check_async (subprocess,
-                                     ide_task_get_cancellable (task),
-                                     ide_terminal_launcher_wait_check_cb,
-                                     g_object_ref (task));
-}
-
-static void
-spawn_launcher (IdeTerminalLauncher   *self,
-                IdeTask               *task,
-                IdeSubprocessLauncher *launcher,
-                gint                   pty_fd)
-{
-  g_autoptr(IdeSubprocess) subprocess = NULL;
-  g_autoptr(GError) error = NULL;
-
-  g_assert (IDE_IS_TERMINAL_LAUNCHER (self));
-  g_assert (IDE_IS_TASK (task));
-  g_assert (!launcher || IDE_IS_SUBPROCESS_LAUNCHER (launcher));
-  g_assert (pty_fd >= 0);
-
-  if (launcher == NULL)
-    {
-      ide_task_return_new_error (task,
-                                 G_IO_ERROR,
-                                 G_IO_ERROR_FAILED,
-                                 "process may only be spawned once");
-      return;
-    }
-
-  ide_subprocess_launcher_set_flags (launcher, 0);
-
-  ide_subprocess_launcher_take_stdin_fd (launcher, dup (pty_fd));
-  ide_subprocess_launcher_take_stdout_fd (launcher, dup (pty_fd));
-  ide_subprocess_launcher_take_stderr_fd (launcher, dup (pty_fd));
-
-  if (!(subprocess = ide_subprocess_launcher_spawn (launcher, NULL, &error)))
-    ide_task_return_error (task, g_steal_pointer (&error));
-  else
-    ide_subprocess_wait_check_async (subprocess,
-                                     ide_task_get_cancellable (task),
-                                     ide_terminal_launcher_wait_check_cb,
-                                     g_object_ref (task));
-}
-
-static void
-spawn_runtime_launcher (IdeTerminalLauncher *self,
-                        IdeTask             *task,
-                        IdeRuntime          *runtime,
-                        IdeConfig           *config,
-                        gint                 pty_fd)
-{
-  g_autoptr(IdeSubprocessLauncher) launcher = NULL;
-  g_autoptr(IdeSubprocess) subprocess = NULL;
-  g_autoptr(GError) error = NULL;
-  const gchar *shell;
-
-  g_assert (IDE_IS_TERMINAL_LAUNCHER (self));
-  g_assert (IDE_IS_TASK (task));
-  g_assert (!runtime || IDE_IS_RUNTIME (runtime));
-  g_assert (pty_fd >= 0);
-
-  if (runtime == NULL)
-    {
-      ide_task_return_new_error (task,
-                                 G_IO_ERROR,
-                                 G_IO_ERROR_FAILED,
-                                 _("Requested runtime is not installed"));
-      return;
-    }
-
-  if (!(shell = ide_terminal_launcher_get_shell (self)))
-    shell = ide_get_user_shell ();
-
-  if (!(launcher = ide_runtime_create_launcher (runtime, NULL)))
-    {
-      ide_task_return_new_error (task,
-                                 G_IO_ERROR,
-                                 G_IO_ERROR_FAILED,
-                                 _("Failed to create shell within runtime ā€œ%sā€"),
-                                 ide_runtime_get_display_name (runtime));
-      return;
-    }
-
-  ide_subprocess_launcher_set_flags (launcher, 0);
-
-  if (!ide_runtime_contains_program_in_path (runtime, shell, NULL))
-    shell = "/bin/sh";
-
-  ide_subprocess_launcher_set_cwd (launcher, self->cwd ? self->cwd : g_get_home_dir ());
-
-  ide_subprocess_launcher_push_argv (launcher, shell);
-  if (shell_supports_login (shell))
-    ide_subprocess_launcher_push_argv (launcher, "--login");
-
-  ide_subprocess_launcher_take_stdin_fd (launcher, dup (pty_fd));
-  ide_subprocess_launcher_take_stdout_fd (launcher, dup (pty_fd));
-  ide_subprocess_launcher_take_stderr_fd (launcher, dup (pty_fd));
-
-  g_assert (ide_subprocess_launcher_get_needs_tty (launcher));
-
-  for (guint i = 0; i < G_N_ELEMENTS (default_environment); i++)
-    ide_subprocess_launcher_setenv (launcher,
-                                    default_environment[i].key,
-                                    default_environment[i].value,
-                                    FALSE);
-
-  apply_pipeline_info (launcher, IDE_OBJECT (self->runtime));
-  copy_envvars (launcher);
-
-  ide_subprocess_launcher_setenv (launcher, "SHELL", shell, TRUE);
-
-  if (config != NULL)
-    ide_config_apply_path (config, launcher);
-
-  if (!(subprocess = ide_subprocess_launcher_spawn (launcher, NULL, &error)))
-    ide_task_return_error (task, g_steal_pointer (&error));
-  else
-    ide_subprocess_wait_check_async (subprocess,
-                                     ide_task_get_cancellable (task),
-                                     ide_terminal_launcher_wait_check_cb,
-                                     g_object_ref (task));
-}
-
-static void
-ide_terminal_launcher_run_cb (GObject      *object,
-                              GAsyncResult *result,
-                              gpointer      user_data)
-{
-  IdeRunner *runner = (IdeRunner *)object;
-  g_autoptr(IdeTask) task = user_data;
-  g_autoptr(GError) error = NULL;
-
-  g_assert (IDE_IS_RUNNER (runner));
-  g_assert (G_IS_ASYNC_RESULT (result));
-  g_assert (IDE_IS_TASK (task));
-
-  if (!ide_runner_run_finish (runner, result, &error))
-    ide_task_return_error (task, g_steal_pointer (&error));
-  else
-    ide_task_return_boolean (task, TRUE);
 
-  ide_object_destroy (IDE_OBJECT (runner));
-}
-
-static void
-spawn_runner_launcher (IdeTerminalLauncher *self,
-                       IdeTask             *task,
-                       IdeRuntime          *runtime,
-                       gint                 pty_fd)
-{
-  g_autoptr(IdeSimpleBuildTarget) build_target = NULL;
-  g_autoptr(IdeRunner) runner = NULL;
-  g_autoptr(GPtrArray) argv = NULL;
-  IdeEnvironment *env;
-  const gchar *shell;
-
-  g_assert (IDE_IS_TERMINAL_LAUNCHER (self));
-  g_assert (IDE_IS_TASK (task));
-  g_assert (!runtime || IDE_IS_RUNTIME (runtime));
-  g_assert (pty_fd >= 0);
-
-  if (runtime == NULL)
-    {
-      ide_task_return_new_error (task,
-                                 G_IO_ERROR,
-                                 G_IO_ERROR_FAILED,
-                                 _("Requested runtime is not installed"));
-      return;
-    }
-
-  if (!(shell = ide_terminal_launcher_get_shell (self)))
-    shell = ide_get_user_shell ();
-
-  if (!ide_runtime_contains_program_in_path (runtime, shell, NULL))
-    shell = "/bin/sh";
-
-  argv = g_ptr_array_new ();
-  g_ptr_array_add (argv, (gchar *)shell);
-
-  if (self->args == NULL)
-    {
-      if (shell_supports_login (shell))
-        g_ptr_array_add (argv, (gchar *)"--login");
-    }
-  else
-    {
-      for (guint i = 0; self->args[i]; i++)
-        g_ptr_array_add (argv, self->args[i]);
-    }
-
-  g_ptr_array_add (argv, NULL);
-
-  build_target = ide_simple_build_target_new (NULL);
-  ide_simple_build_target_set_argv (build_target, (const gchar * const *)argv->pdata);
-  ide_simple_build_target_set_cwd (build_target, self->cwd ? self->cwd : g_get_home_dir ());
-
-  /* Creating runner should always succeed, but run_async() may fail */
-  runner = ide_runtime_create_runner (runtime, IDE_BUILD_TARGET (build_target));
-  env = ide_runner_get_environment (runner);
-  ide_runner_take_tty_fd (runner, dup (pty_fd));
-
-  for (guint i = 0; i < G_N_ELEMENTS (default_environment); i++)
-    ide_environment_setenv (env,
-                            default_environment[i].key,
-                            default_environment[i].value);
-
-  apply_pipeline_info (runner, IDE_OBJECT (self->runtime));
-  copy_envvars (runner);
-
-  ide_environment_setenv (env, "SHELL", shell);
-
-  ide_runner_run_async (runner,
-                        ide_task_get_cancellable (task),
-                        ide_terminal_launcher_run_cb,
-                        g_object_ref (task));
+  IDE_EXIT;
 }
 
 void
@@ -495,9 +80,14 @@ ide_terminal_launcher_spawn_async (IdeTerminalLauncher *self,
                                    GAsyncReadyCallback  callback,
                                    gpointer             user_data)
 {
+  g_autoptr(IdeRunContext) run_context = NULL;
+  g_autoptr(IdeSubprocess) subprocess = NULL;
   g_autoptr(IdeTask) task = NULL;
-  gint pty_fd = -1;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
 
+  g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (IDE_IS_TERMINAL_LAUNCHER (self));
   g_assert (VTE_IS_PTY (pty));
   g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
@@ -505,44 +95,38 @@ ide_terminal_launcher_spawn_async (IdeTerminalLauncher *self,
   task = ide_task_new (self, cancellable, callback, user_data);
   ide_task_set_source_tag (task, ide_terminal_launcher_spawn_async);
 
-  if ((pty_fd = ide_vte_pty_create_producer (pty)) == -1)
+  run_context = ide_run_context_new ();
+
+  /* Paranoia check to ensure we've been constructed right */
+  if (self->run_command == NULL || self->context == NULL)
     {
-      int errsv = errno;
       ide_task_return_new_error (task,
                                  G_IO_ERROR,
-                                 g_io_error_from_errno (errsv),
-                                 "%s", g_strerror (errsv));
-      return;
+                                 G_IO_ERROR_NOT_INITIALIZED,
+                                 "%s is improperly configured",
+                                 G_OBJECT_TYPE_NAME (self));
+      IDE_EXIT;
     }
 
-  switch (self->kind)
-    {
-    case LAUNCHER_KIND_RUNTIME:
-      spawn_runtime_launcher (self, task, self->runtime, NULL, pty_fd);
-      break;
-
-    case LAUNCHER_KIND_CONFIG:
-      spawn_runtime_launcher (self, task, self->runtime, self->config, pty_fd);
-      break;
+  ide_run_command_prepare_to_run (self->run_command, run_context, self->context);
 
-    case LAUNCHER_KIND_RUNNER:
-      spawn_runner_launcher (self, task, self->runtime, pty_fd);
-      break;
+  /* Add some environment for custom bashrc, VTE, etc */
+  ide_run_context_setenv (run_context, "INSIDE_GNOME_BUILDER", PACKAGE_VERSION);
+  ide_run_context_setenv (run_context, "TERM", "xterm-256color");
 
-    case LAUNCHER_KIND_LAUNCHER:
-      spawn_launcher (self, task, self->launcher, pty_fd);
-      g_clear_object (&self->launcher);
-      break;
+  /* Attach the PTY to stdin/stdout/stderr */
+  ide_run_context_set_pty (run_context, pty);
 
-    case LAUNCHER_KIND_DEBUG:
-    case LAUNCHER_KIND_HOST:
-    default:
-      spawn_host_launcher (self, task, pty_fd, self->kind == LAUNCHER_KIND_HOST);
-      break;
-    }
+  /* Now attempt to spawn the process */
+  if (!(subprocess = ide_run_context_spawn (run_context, &error)))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_subprocess_wait_check_async (subprocess,
+                                     cancellable,
+                                     ide_terminal_launcher_wait_check_cb,
+                                     g_steal_pointer (&task));
 
-  if (pty_fd != -1)
-    close (pty_fd);
+  IDE_EXIT;
 }
 
 /**
@@ -559,26 +143,27 @@ ide_terminal_launcher_spawn_finish (IdeTerminalLauncher  *self,
                                     GAsyncResult         *result,
                                     GError              **error)
 {
+  gboolean ret;
+
+  IDE_ENTRY;
+
   g_assert (IDE_IS_TERMINAL_LAUNCHER (self));
   g_assert (IDE_IS_TASK (result));
 
-  return ide_task_propagate_boolean (IDE_TASK (result), error);
+  ret = ide_task_propagate_boolean (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
 }
 
 static void
-ide_terminal_launcher_finalize (GObject *object)
+ide_terminal_launcher_dispose (GObject *object)
 {
   IdeTerminalLauncher *self = (IdeTerminalLauncher *)object;
 
-  g_clear_pointer (&self->args, g_strfreev);
-  g_clear_pointer (&self->cwd, g_free);
-  g_clear_pointer (&self->shell, g_free);
-  g_clear_pointer (&self->title, g_free);
-  g_clear_object (&self->launcher);
-  g_clear_object (&self->runtime);
-  g_clear_object (&self->config);
+  g_clear_object (&self->context);
+  g_clear_object (&self->run_command);
 
-  G_OBJECT_CLASS (ide_terminal_launcher_parent_class)->finalize (object);
+  G_OBJECT_CLASS (ide_terminal_launcher_parent_class)->dispose (object);
 }
 
 static void
@@ -591,20 +176,12 @@ ide_terminal_launcher_get_property (GObject    *object,
 
   switch (prop_id)
     {
-    case PROP_ARGS:
-      g_value_set_boxed (value, ide_terminal_launcher_get_args (self));
-      break;
-
-    case PROP_CWD:
-      g_value_set_string (value, ide_terminal_launcher_get_cwd (self));
-      break;
-
-    case PROP_SHELL:
-      g_value_set_string (value, ide_terminal_launcher_get_shell (self));
+    case PROP_CONTEXT:
+      g_value_set_object (value, self->context);
       break;
 
-    case PROP_TITLE:
-      g_value_set_string (value, ide_terminal_launcher_get_title (self));
+    case PROP_RUN_COMMAND:
+      g_value_set_object (value, self->run_command);
       break;
 
     default:
@@ -622,20 +199,12 @@ ide_terminal_launcher_set_property (GObject      *object,
 
   switch (prop_id)
     {
-    case PROP_ARGS:
-      ide_terminal_launcher_set_args (self, g_value_get_boxed (value));
-      break;
-
-    case PROP_CWD:
-      ide_terminal_launcher_set_cwd (self, g_value_get_string (value));
-      break;
-
-    case PROP_SHELL:
-      ide_terminal_launcher_set_shell (self, g_value_get_string (value));
+    case PROP_CONTEXT:
+      g_set_object (&self->context, g_value_get_object (value));
       break;
 
-    case PROP_TITLE:
-      ide_terminal_launcher_set_title (self, g_value_get_string (value));
+    case PROP_RUN_COMMAND:
+      g_set_object (&self->run_command, g_value_get_object (value));
       break;
 
     default:
@@ -648,36 +217,22 @@ ide_terminal_launcher_class_init (IdeTerminalLauncherClass *klass)
 {
   GObjectClass *object_class = G_OBJECT_CLASS (klass);
 
-  object_class->finalize = ide_terminal_launcher_finalize;
+  object_class->dispose = ide_terminal_launcher_dispose;
   object_class->get_property = ide_terminal_launcher_get_property;
   object_class->set_property = ide_terminal_launcher_set_property;
 
-  properties [PROP_ARGS] =
-    g_param_spec_boxed ("args",
-                         "Args",
-                         "Arguments to shell",
-                         G_TYPE_STRV,
-                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
-
-  properties [PROP_CWD] =
-    g_param_spec_string ("cwd",
-                         "Cwd",
-                         "The cwd to spawn in the subprocess",
-                         NULL,
-                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
-
-  properties [PROP_SHELL] =
-    g_param_spec_string ("shell",
-                         "Shell",
-                         "The shell to spawn in the subprocess",
-                         NULL,
-                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
-
-  properties [PROP_TITLE] =
-    g_param_spec_string ("title",
-                         "Title",
-                         "The title for the subprocess launcher",
-                         NULL,
+  properties [PROP_CONTEXT] =
+    g_param_spec_object ("context",
+                         "Context",
+                         "The context for the launcher",
+                         IDE_TYPE_CONTEXT,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_RUN_COMMAND] =
+    g_param_spec_object ("run-command",
+                         "Run Command",
+                         "The run command to spawn",
+                         IDE_TYPE_RUN_COMMAND,
                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
 
   g_object_class_install_properties (object_class, N_PROPS, properties);
@@ -686,279 +241,45 @@ ide_terminal_launcher_class_init (IdeTerminalLauncherClass *klass)
 static void
 ide_terminal_launcher_init (IdeTerminalLauncher *self)
 {
-  self->cwd = NULL;
-  self->shell = NULL;
-  self->title = g_strdup (_("Untitled Terminal"));
-}
-
-const gchar *
-ide_terminal_launcher_get_cwd (IdeTerminalLauncher *self)
-{
-  g_return_val_if_fail (IDE_IS_TERMINAL_LAUNCHER (self), NULL);
-
-  return self->cwd;
-}
-
-void
-ide_terminal_launcher_set_cwd (IdeTerminalLauncher *self,
-                               const gchar         *cwd)
-{
-  g_return_if_fail (IDE_IS_TERMINAL_LAUNCHER (self));
-
-  if (g_strcmp0 (self->cwd, cwd) != 0)
-    {
-      g_free (self->cwd);
-      self->cwd = g_strdup (cwd);
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CWD]);
-    }
-}
-
-const gchar *
-ide_terminal_launcher_get_shell (IdeTerminalLauncher *self)
-{
-  g_return_val_if_fail (IDE_IS_TERMINAL_LAUNCHER (self), NULL);
-
-  return self->shell;
-}
-
-void
-ide_terminal_launcher_set_shell (IdeTerminalLauncher *self,
-                                 const gchar         *shell)
-{
-  g_return_if_fail (IDE_IS_TERMINAL_LAUNCHER (self));
-
-  if (g_strcmp0 (self->shell, shell) != 0)
-    {
-      g_free (self->shell);
-      self->shell = g_strdup (shell);
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SHELL]);
-    }
-}
-
-const gchar *
-ide_terminal_launcher_get_title (IdeTerminalLauncher *self)
-{
-  g_return_val_if_fail (IDE_IS_TERMINAL_LAUNCHER (self), NULL);
-
-  return self->title;
-}
-
-void
-ide_terminal_launcher_set_title (IdeTerminalLauncher *self,
-                                 const gchar         *title)
-{
-  g_return_if_fail (IDE_IS_TERMINAL_LAUNCHER (self));
-
-  if (g_strcmp0 (self->title, title) != 0)
-    {
-      g_free (self->title);
-      self->title = g_strdup (title);
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]);
-    }
 }
 
 /**
  * ide_terminal_launcher_new:
+ * @context: an #IdeContext
+ * @run_command: an #IdeRunCommand to spawn
  *
- * Create a new #IdeTerminalLauncher that will spawn a terminal on the host.
+ * Create an #IdeTerminalLauncher that spawns @run_command.
  *
  * Returns: (transfer full): a newly created #IdeTerminalLauncher
  */
 IdeTerminalLauncher *
-ide_terminal_launcher_new (IdeContext *context)
+ide_terminal_launcher_new (IdeContext    *context,
+                           IdeRunCommand *run_command)
 {
-  IdeTerminalLauncher *self;
-  g_autoptr(GFile) workdir = NULL;
-
   g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
+  g_return_val_if_fail (IDE_IS_RUN_COMMAND (run_command), NULL);
 
-  workdir = ide_context_ref_workdir (context);
-
-  self = g_object_new (IDE_TYPE_TERMINAL_LAUNCHER, NULL);
-  self->kind = LAUNCHER_KIND_HOST;
-  self->cwd = g_file_get_path (workdir);
-  self->context = g_object_ref (context);
-
-  return g_steal_pointer (&self);
+  return g_object_new (IDE_TYPE_TERMINAL_LAUNCHER,
+                       "context", context,
+                       "run-command", run_command,
+                       NULL);
 }
 
 /**
- * ide_terminal_launcher_new_for_launcher:
- * @launcher: an #IdeSubprocessLauncher
+ * ide_terminal_launcher_copy:
+ * @self: an #IdeTerminalLauncher
  *
- * Creates a new #IdeTerminalLauncher that can be used to launch a process
- * using the provided #IdeSubprocessLauncher.
- *
- * Returns: (transfer full): an #IdeTerminalLauncher
- */
-IdeTerminalLauncher *
-ide_terminal_launcher_new_for_launcher (IdeSubprocessLauncher *launcher)
-{
-  IdeTerminalLauncher *self;
-
-  g_return_val_if_fail (IDE_IS_SUBPROCESS_LAUNCHER (launcher), NULL);
-
-  self = g_object_new (IDE_TYPE_TERMINAL_LAUNCHER, NULL);
-  self->kind = LAUNCHER_KIND_LAUNCHER;
-  self->launcher = g_object_ref (launcher);
-
-  return g_steal_pointer (&self);
-}
-
-/**
- * ide_terminal_launcher_new_for_debug
- *
- * Create a new #IdeTerminalLauncher that will spawn a terminal on the host.
- *
- * Returns: (transfer full): a newly created #IdeTerminalLauncher
- */
-IdeTerminalLauncher *
-ide_terminal_launcher_new_for_debug (void)
-{
-  IdeTerminalLauncher *self;
-
-  self = g_object_new (IDE_TYPE_TERMINAL_LAUNCHER, NULL);
-  self->kind = LAUNCHER_KIND_DEBUG;
-
-  return g_steal_pointer (&self);
-}
-
-/**
- * ide_terminal_launcher_new_for_config:
- * @config: an #IdeConfig
- *
- * Create a new #IdeTerminalLauncher that will spawn a terminal in the runtime
- * of the configuration with various build options applied.
+ * Copies @self into a new launcher.
  *
  * Returns: (transfer full): a newly created #IdeTerminalLauncher
  */
 IdeTerminalLauncher *
-ide_terminal_launcher_new_for_config (IdeConfig *config)
-{
-  IdeTerminalLauncher *self;
-  IdeRuntime *runtime;
-
-  g_return_val_if_fail (IDE_IS_CONFIG (config), NULL);
-
-  runtime = ide_config_get_runtime (config);
-
-  self = g_object_new (IDE_TYPE_TERMINAL_LAUNCHER, NULL);
-  self->runtime = g_object_ref (runtime);
-  self->config = g_object_ref (config);
-  self->kind = LAUNCHER_KIND_CONFIG;
-
-  ide_terminal_launcher_set_title (self, ide_runtime_get_name (runtime));
-
-  return g_steal_pointer (&self);
-}
-
-/**
- * ide_terminal_launcher_new_for_runtime:
- * @runtime: an #IdeRuntime
- *
- * Create a new #IdeTerminalLauncher that will spawn a terminal in the runtime.
- *
- * Returns: (transfer full): a newly created #IdeTerminalLauncher
- */
-IdeTerminalLauncher *
-ide_terminal_launcher_new_for_runtime (IdeRuntime *runtime)
-{
-  IdeTerminalLauncher *self;
-
-  g_return_val_if_fail (IDE_IS_RUNTIME (runtime), NULL);
-
-  self = g_object_new (IDE_TYPE_TERMINAL_LAUNCHER, NULL);
-  self->runtime = g_object_ref (runtime);
-  self->kind = LAUNCHER_KIND_RUNTIME;
-
-  ide_terminal_launcher_set_title (self, ide_runtime_get_name (runtime));
-
-  return g_steal_pointer (&self);
-}
-
-/**
- * ide_terminal_launcher_new_for_runner:
- * @runtime: an #IdeRuntime
- *
- * Create a new #IdeTerminalLauncher that will spawn a terminal in the runtime
- * but with a "runner" context similar to how the application would execute.
- *
- * Returns: (transfer full): a newly created #IdeTerminalLauncher
- */
-IdeTerminalLauncher *
-ide_terminal_launcher_new_for_runner (IdeRuntime *runtime)
-{
-  IdeTerminalLauncher *self;
-
-  g_return_val_if_fail (IDE_IS_RUNTIME (runtime), NULL);
-
-  self = g_object_new (IDE_TYPE_TERMINAL_LAUNCHER, NULL);
-  self->runtime = g_object_ref (runtime);
-  self->kind = LAUNCHER_KIND_RUNNER;
-
-  ide_terminal_launcher_set_title (self, ide_runtime_get_name (runtime));
-
-  return g_steal_pointer (&self);
-}
-
-gboolean
-ide_terminal_launcher_can_respawn (IdeTerminalLauncher *self)
-{
-  g_return_val_if_fail (IDE_IS_TERMINAL_LAUNCHER (self), FALSE);
-
-  return self->kind != LAUNCHER_KIND_LAUNCHER;
-}
-
-const gchar * const *
-ide_terminal_launcher_get_args (IdeTerminalLauncher *self)
+ide_terminal_launcher_copy (IdeTerminalLauncher *self)
 {
   g_return_val_if_fail (IDE_IS_TERMINAL_LAUNCHER (self), NULL);
 
-  return (const gchar * const *)self->args;
-}
-
-void
-ide_terminal_launcher_set_args (IdeTerminalLauncher *self,
-                                const gchar * const *args)
-{
-  g_return_if_fail (IDE_IS_TERMINAL_LAUNCHER (self));
-
-  if ((gchar **)args != self->args)
-    {
-      gchar **freeme = g_steal_pointer (&self->args);
-      self->args = g_strdupv ((gchar **)args);
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ARGS]);
-      g_strfreev (freeme);
-    }
-}
-
-gboolean
-_ide_terminal_launcher_are_similar (IdeTerminalLauncher *a,
-                                    IdeTerminalLauncher *b)
-{
-  g_return_val_if_fail (IDE_IS_TERMINAL_LAUNCHER (a), FALSE);
-  g_return_val_if_fail (IDE_IS_TERMINAL_LAUNCHER (b), FALSE);
-
-  if (a->kind != b->kind)
-    return FALSE;
-
-  switch (a->kind)
-    {
-      case LAUNCHER_KIND_HOST:
-      case LAUNCHER_KIND_DEBUG:
-        return TRUE;
-
-      case LAUNCHER_KIND_RUNTIME:
-      case LAUNCHER_KIND_RUNNER:
-        return a->runtime == b->runtime;
-
-      case LAUNCHER_KIND_CONFIG:
-        return a->config == b->config;
-
-      case LAUNCHER_KIND_LAUNCHER:
-        return FALSE;
-
-      default:
-        return FALSE;
-    }
+  return g_object_new (IDE_TYPE_TERMINAL_LAUNCHER,
+                       "context", self->context,
+                       "run-command", self->run_command,
+                       NULL);
 }
diff --git a/src/libide/terminal/ide-terminal-launcher.h b/src/libide/terminal/ide-terminal-launcher.h
index 1e3b8ef6e..f6e66196d 100644
--- a/src/libide/terminal/ide-terminal-launcher.h
+++ b/src/libide/terminal/ide-terminal-launcher.h
@@ -20,7 +20,11 @@
 
 #pragma once
 
-#include <libide-gui.h>
+#if !defined (IDE_TERMINAL_INSIDE) && !defined (IDE_TERMINAL_COMPILATION)
+# error "Only <libide-terminal.h> can be included directly."
+#endif
+
+#include <libide-foundry.h>
 #include <libide-threading.h>
 
 G_BEGIN_DECLS
@@ -31,48 +35,19 @@ IDE_AVAILABLE_IN_ALL
 G_DECLARE_FINAL_TYPE (IdeTerminalLauncher, ide_terminal_launcher, IDE, TERMINAL_LAUNCHER, GObject)
 
 IDE_AVAILABLE_IN_ALL
-IdeTerminalLauncher *ide_terminal_launcher_new              (IdeContext             *context);
-IDE_AVAILABLE_IN_ALL
-IdeTerminalLauncher *ide_terminal_launcher_new_for_launcher (IdeSubprocessLauncher  *launcher);
-IDE_AVAILABLE_IN_ALL
-IdeTerminalLauncher *ide_terminal_launcher_new_for_config   (IdeConfig              *config);
-IDE_AVAILABLE_IN_ALL
-IdeTerminalLauncher *ide_terminal_launcher_new_for_debug    (void);
-IDE_AVAILABLE_IN_ALL
-IdeTerminalLauncher *ide_terminal_launcher_new_for_runtime  (IdeRuntime             *runtime);
-IDE_AVAILABLE_IN_ALL
-IdeTerminalLauncher *ide_terminal_launcher_new_for_runner   (IdeRuntime             *runtime);
-IDE_AVAILABLE_IN_ALL
-gboolean             ide_terminal_launcher_can_respawn      (IdeTerminalLauncher    *self);
-IDE_AVAILABLE_IN_ALL
-const gchar * const *ide_terminal_launcher_get_args         (IdeTerminalLauncher    *self);
-IDE_AVAILABLE_IN_ALL
-void                 ide_terminal_launcher_set_args         (IdeTerminalLauncher    *self,
-                                                             const gchar * const    *args);
-IDE_AVAILABLE_IN_ALL
-const gchar         *ide_terminal_launcher_get_cwd          (IdeTerminalLauncher    *self);
-IDE_AVAILABLE_IN_ALL
-void                 ide_terminal_launcher_set_cwd          (IdeTerminalLauncher    *self,
-                                                             const gchar            *cwd);
-IDE_AVAILABLE_IN_ALL
-const gchar         *ide_terminal_launcher_get_shell        (IdeTerminalLauncher    *self);
-IDE_AVAILABLE_IN_ALL
-void                 ide_terminal_launcher_set_shell        (IdeTerminalLauncher    *self,
-                                                             const gchar            *shell);
-IDE_AVAILABLE_IN_ALL
-const gchar         *ide_terminal_launcher_get_title        (IdeTerminalLauncher    *self);
+IdeTerminalLauncher *ide_terminal_launcher_new                   (IdeContext             *context,
+                                                                  IdeRunCommand          *run_command);
 IDE_AVAILABLE_IN_ALL
-void                 ide_terminal_launcher_set_title        (IdeTerminalLauncher    *self,
-                                                             const gchar            *title);
+IdeTerminalLauncher *ide_terminal_launcher_copy                  (IdeTerminalLauncher    *self);
 IDE_AVAILABLE_IN_ALL
-void                 ide_terminal_launcher_spawn_async      (IdeTerminalLauncher    *self,
-                                                             VtePty                 *pty,
-                                                             GCancellable           *cancellable,
-                                                             GAsyncReadyCallback     callback,
-                                                             gpointer                user_data);
+void                 ide_terminal_launcher_spawn_async           (IdeTerminalLauncher    *self,
+                                                                  VtePty                 *pty,
+                                                                  GCancellable           *cancellable,
+                                                                  GAsyncReadyCallback     callback,
+                                                                  gpointer                user_data);
 IDE_AVAILABLE_IN_ALL
-gboolean             ide_terminal_launcher_spawn_finish     (IdeTerminalLauncher    *self,
-                                                             GAsyncResult           *result,
-                                                             GError                **error);
+gboolean             ide_terminal_launcher_spawn_finish          (IdeTerminalLauncher    *self,
+                                                                  GAsyncResult           *result,
+                                                                  GError                **error);
 
 G_END_DECLS
diff --git a/src/libide/terminal/ide-terminal-page.c b/src/libide/terminal/ide-terminal-page.c
index c7dd26485..b85039e65 100644
--- a/src/libide/terminal/ide-terminal-page.c
+++ b/src/libide/terminal/ide-terminal-page.c
@@ -37,6 +37,7 @@
 #include "ide-terminal-page.h"
 #include "ide-terminal-page-private.h"
 #include "ide-terminal-page-actions.h"
+#include "ide-terminal-run-command-private.h"
 
 #define FLAPPING_DURATION_USEC (G_USEC_PER_SEC / 20)
 
@@ -346,8 +347,14 @@ ide_terminal_page_context_set (GtkWidget  *widget,
   g_assert (IDE_IS_TERMINAL_PAGE (self));
   g_assert (!context || IDE_IS_CONTEXT (context));
 
-  if (self->launcher == NULL && context != NULL)
-    self->launcher = ide_terminal_launcher_new (context);
+  if (context == NULL)
+    return;
+
+  if (self->launcher == NULL)
+    {
+      g_autoptr(IdeRunCommand) run_command = ide_terminal_run_command_new (IDE_TERMINAL_RUN_ON_HOST);
+      self->launcher = ide_terminal_launcher_new (context, run_command);
+    }
 }
 
 static GFile *
@@ -509,7 +516,7 @@ ide_terminal_page_class_init (IdeTerminalPageClass *klass)
                          "Launcher",
                          "The launcher to use for spawning",
                          IDE_TYPE_TERMINAL_LAUNCHER,
-                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
 
   g_object_class_install_properties (object_class, N_PROPS, properties);
 }
@@ -543,6 +550,22 @@ ide_terminal_page_init (IdeTerminalPage *self)
   ide_widget_set_context_handler (self, ide_terminal_page_context_set);
 }
 
+/**
+ * ide_terminal_page_get_pty:
+ * @self: a #IdeTerminalPage
+ *
+ * Gets the #VtePty for the page.
+ *
+ * Returns: (transfer none): a #VtePty
+ */
+VtePty *
+ide_terminal_page_get_pty (IdeTerminalPage *self)
+{
+  g_return_val_if_fail (IDE_IS_TERMINAL_PAGE (self), NULL);
+
+  return self->pty;
+}
+
 void
 ide_terminal_page_set_pty (IdeTerminalPage *self,
                            VtePty          *pty)
@@ -581,21 +604,8 @@ ide_terminal_page_set_launcher (IdeTerminalPage     *self,
 
   if (g_set_object (&self->launcher, launcher))
     {
-      gboolean can_split;
-
-      if (launcher != NULL)
-        {
-          const gchar *title = ide_terminal_launcher_get_title (launcher);
-          panel_widget_set_title (PANEL_WIDGET (self), title);
-          can_split = ide_terminal_launcher_can_respawn (launcher);
-        }
-      else
-        {
-          self->manage_spawn = FALSE;
-          can_split = FALSE;
-        }
-
-      ide_page_set_can_split (IDE_PAGE (self), can_split);
+      ide_page_set_can_split (IDE_PAGE (self), TRUE);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_LAUNCHER]);
     }
 }
 
diff --git a/src/libide/terminal/ide-terminal-page.h b/src/libide/terminal/ide-terminal-page.h
index 056bb6721..c6dc9d4ce 100644
--- a/src/libide/terminal/ide-terminal-page.h
+++ b/src/libide/terminal/ide-terminal-page.h
@@ -43,6 +43,8 @@ void                 ide_terminal_page_set_launcher              (IdeTerminalPag
 IDE_AVAILABLE_IN_ALL
 IdeTerminalLauncher *ide_terminal_page_get_launcher              (IdeTerminalPage     *self);
 IDE_AVAILABLE_IN_ALL
+VtePty              *ide_terminal_page_get_pty                   (IdeTerminalPage     *self);
+IDE_AVAILABLE_IN_ALL
 void                 ide_terminal_page_set_pty                   (IdeTerminalPage     *self,
                                                                   VtePty              *pty);
 IDE_AVAILABLE_IN_ALL
diff --git a/src/libide/terminal/ide-terminal-private.h b/src/libide/terminal/ide-terminal-private.h
index 9eb7f1d05..d9015f5b6 100644
--- a/src/libide/terminal/ide-terminal-private.h
+++ b/src/libide/terminal/ide-terminal-private.h
@@ -22,12 +22,8 @@
 
 #include <glib.h>
 
-#include "ide-terminal-launcher.h"
-
 G_BEGIN_DECLS
 
-void     _ide_terminal_init                 (void);
-gboolean _ide_terminal_launcher_are_similar (IdeTerminalLauncher *a,
-                                             IdeTerminalLauncher *b);
+void _ide_terminal_init (void);
 
 G_END_DECLS
diff --git a/src/libide/terminal/ide-terminal-run-command-private.h 
b/src/libide/terminal/ide-terminal-run-command-private.h
new file mode 100644
index 000000000..0ae78f6b8
--- /dev/null
+++ b/src/libide/terminal/ide-terminal-run-command-private.h
@@ -0,0 +1,43 @@
+/* ide-terminal-run-command-private.h
+ *
+ * Copyright 2022 Christian Hergert <chergert redhat com>
+ *
+ * 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/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <libide-foundry.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TERMINAL_RUN_COMMAND (ide_terminal_run_command_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeTerminalRunCommand, ide_terminal_run_command, IDE, TERMINAL_RUN_COMMAND, 
IdeRunCommand)
+
+typedef enum
+{
+  IDE_TERMINAL_RUN_ON_HOST,
+  IDE_TERMINAL_RUN_AS_SUBPROCESS,
+  IDE_TERMINAL_RUN_IN_PIPELINE,
+  IDE_TERMINAL_RUN_IN_RUNTIME,
+
+  IDE_TERMINAL_RUN_LAST
+} IdeTerminalRunLocality;
+
+IdeRunCommand *ide_terminal_run_command_new (IdeTerminalRunLocality  locality);
+
+G_END_DECLS
diff --git a/src/libide/terminal/ide-terminal-run-command.c b/src/libide/terminal/ide-terminal-run-command.c
new file mode 100644
index 000000000..9f20bf3c2
--- /dev/null
+++ b/src/libide/terminal/ide-terminal-run-command.c
@@ -0,0 +1,142 @@
+/* ide-terminal-run-command.c
+ *
+ * Copyright 2022 Christian Hergert <chergert redhat com>
+ *
+ * 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/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-terminal-run-command"
+
+#include "config.h"
+
+#include <libide-io.h>
+
+#include "ide-terminal-run-command-private.h"
+
+struct _IdeTerminalRunCommand
+{
+  IdeRunCommand          parent_instance;
+  IdeTerminalRunLocality locality;
+};
+
+G_DEFINE_FINAL_TYPE (IdeTerminalRunCommand, ide_terminal_run_command, IDE_TYPE_RUN_COMMAND)
+
+static void
+ide_terminal_run_command_prepare_to_run (IdeRunCommand *run_command,
+                                         IdeRunContext *run_context,
+                                         IdeContext    *context)
+{
+  IdeTerminalRunCommand *self = (IdeTerminalRunCommand *)run_command;
+  const char *user_shell;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TERMINAL_RUN_COMMAND (self));
+  g_assert (IDE_IS_RUN_CONTEXT (run_context));
+  g_assert (IDE_IS_CONTEXT (context));
+
+  user_shell = ide_get_user_shell ();
+
+  switch (self->locality)
+    {
+    case IDE_TERMINAL_RUN_ON_HOST:
+      ide_run_context_push_host (run_context);
+      ide_run_context_append_argv (run_context, user_shell);
+      if (ide_shell_supports_dash_login (user_shell))
+        ide_run_context_append_argv (run_context, "--login");
+      break;
+
+    case IDE_TERMINAL_RUN_AS_SUBPROCESS:
+      if (g_find_program_in_path (user_shell))
+        {
+          ide_run_context_append_argv (run_context, user_shell);
+          if (ide_shell_supports_dash_login (user_shell))
+            ide_run_context_append_argv (run_context, "--login");
+        }
+      else
+        {
+          ide_run_context_append_argv (run_context, "/bin/sh");
+          ide_run_context_append_argv (run_context, "--login");
+        }
+      break;
+
+    case IDE_TERMINAL_RUN_IN_RUNTIME:
+    case IDE_TERMINAL_RUN_IN_PIPELINE:
+      {
+        IdeBuildManager *build_manager;
+        IdePipeline *pipeline;
+        IdeRuntime *runtime;
+
+        if (!ide_context_has_project (context) ||
+            !(build_manager = ide_build_manager_from_context (context)) ||
+            !(pipeline = ide_build_manager_get_pipeline (build_manager)) ||
+            !(runtime = ide_pipeline_get_runtime (pipeline)))
+          {
+            ide_run_context_push_error (run_context,
+                                        g_error_new (G_IO_ERROR,
+                                                     G_IO_ERROR_NOT_INITIALIZED,
+                                                     "Cannot spawn terminal without a pipeline"));
+            break;
+          }
+
+        if (!ide_runtime_contains_program_in_path (runtime, user_shell, NULL))
+          user_shell = "/bin/sh";
+
+        if (self->locality == IDE_TERMINAL_RUN_IN_PIPELINE)
+          ide_pipeline_prepare_run_context (pipeline, run_context);
+        else
+          ide_runtime_prepare_to_run (runtime, pipeline, run_context);
+
+        ide_run_context_append_argv (run_context, user_shell);
+        if (ide_shell_supports_dash_login (user_shell))
+          ide_run_context_append_argv (run_context, "--login");
+      }
+      break;
+
+    case IDE_TERMINAL_RUN_LAST:
+    default:
+      g_assert_not_reached ();
+    }
+
+  IDE_RUN_COMMAND_CLASS (ide_terminal_run_command_parent_class)->prepare_to_run (run_command, run_context, 
context);
+
+  IDE_EXIT;
+}
+
+static void
+ide_terminal_run_command_class_init (IdeTerminalRunCommandClass *klass)
+{
+  IdeRunCommandClass *run_command_class = IDE_RUN_COMMAND_CLASS (klass);
+
+  run_command_class->prepare_to_run = ide_terminal_run_command_prepare_to_run;
+}
+
+static void
+ide_terminal_run_command_init (IdeTerminalRunCommand *self)
+{
+}
+
+IdeRunCommand *
+ide_terminal_run_command_new (IdeTerminalRunLocality locality)
+{
+  IdeTerminalRunCommand *self;
+
+  self = g_object_new (IDE_TYPE_TERMINAL_RUN_COMMAND, NULL);
+  self->locality = locality;
+
+  return IDE_RUN_COMMAND (self);
+}
diff --git a/src/libide/terminal/meson.build b/src/libide/terminal/meson.build
index eb69d8da3..1878d3ba7 100644
--- a/src/libide/terminal/meson.build
+++ b/src/libide/terminal/meson.build
@@ -30,6 +30,7 @@ libide_terminal_private_headers = [
   'ide-terminal-page-actions.h',
   'ide-terminal-page-private.h',
   'ide-terminal-popover-row.h',
+  'ide-terminal-run-command-private.h',
   'ide-terminal-search-private.h',
   'ide-terminal-private.h',
 ]
@@ -47,6 +48,7 @@ libide_terminal_private_sources = [
   'ide-terminal-init.c',
   'ide-terminal-page-actions.c',
   'ide-terminal-popover-row.c',
+  'ide-terminal-run-command.c',
 ]
 
 #
diff --git a/src/plugins/shellcmd/gbp-shellcmd-run-command.c b/src/plugins/shellcmd/gbp-shellcmd-run-command.c
index 453a95327..3b8ea1e17 100644
--- a/src/plugins/shellcmd/gbp-shellcmd-run-command.c
+++ b/src/plugins/shellcmd/gbp-shellcmd-run-command.c
@@ -394,99 +394,6 @@ gbp_shellcmd_run_command_set_accelerator (GbpShellcmdRunCommand *self,
     }
 }
 
-IdeTerminalLauncher *
-gbp_shellcmd_run_command_create_launcher (GbpShellcmdRunCommand *self,
-                                          IdeContext            *context)
-{
-  g_autoptr(IdeSubprocessLauncher) launcher = NULL;
-  g_autoptr(IdeRunContext) run_context = NULL;
-  g_autoptr(GError) error = NULL;
-  g_autoptr(GFile) workdir = NULL;
-  IdeBuildManager *build_manager = NULL;
-  g_auto(GStrv) environ = NULL;
-  IdePipeline *pipeline = NULL;
-  const char * const *argv;
-  const char * const *env;
-  const char *builddir;
-  const char *srcdir;
-  const char *cwd;
-
-  g_return_val_if_fail (GBP_IS_SHELLCMD_RUN_COMMAND (self), NULL);
-  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
-
-  workdir = ide_context_ref_workdir (context);
-  srcdir = g_file_peek_path (workdir);
-  builddir = g_file_peek_path (workdir);
-
-  if (ide_context_has_project (context))
-    {
-      build_manager = ide_build_manager_from_context (context);
-      pipeline = ide_build_manager_get_pipeline (build_manager);
-      builddir = ide_pipeline_get_builddir (pipeline);
-      srcdir = ide_pipeline_get_srcdir (pipeline);
-    }
-
-  environ = g_environ_setenv (environ, "BUILDDIR", builddir, TRUE);
-  environ = g_environ_setenv (environ, "SRCDIR", srcdir, TRUE);
-  environ = g_environ_setenv (environ, "USER", g_get_user_name (), TRUE);
-  environ = g_environ_setenv (environ, "HOME", g_get_home_dir (), TRUE);
-
-  run_context = ide_run_context_new ();
-
-  switch (self->locality)
-    {
-    case GBP_SHELLCMD_LOCALITY_PIPELINE:
-      if (pipeline == NULL ||
-          !(launcher = ide_pipeline_create_launcher (pipeline, &error)))
-        goto handle_error;
-      ide_pipeline_prepare_run_context (pipeline, run_context);
-      break;
-
-    case GBP_SHELLCMD_LOCALITY_HOST:
-      ide_run_context_push_host (run_context);
-      break;
-
-    case GBP_SHELLCMD_LOCALITY_SUBPROCESS:
-      break;
-
-    case GBP_SHELLCMD_LOCALITY_RUNNER: {
-      IdeRuntime *runtime = ide_pipeline_get_runtime (pipeline);
-      if (runtime == NULL)
-        goto handle_error;
-      ide_runtime_prepare_to_run (runtime, pipeline, run_context);
-      break;
-    }
-
-    default:
-      g_assert_not_reached ();
-    }
-
-  ide_run_context_push_expansion (run_context, (const char * const *)environ);
-
-  if ((cwd = ide_run_command_get_cwd (IDE_RUN_COMMAND (self))))
-    ide_run_context_set_cwd (run_context, cwd);
-
-  if ((argv = ide_run_command_get_argv (IDE_RUN_COMMAND (self))))
-    ide_run_context_append_args (run_context, argv);
-
-  if ((env = ide_run_command_get_environ (IDE_RUN_COMMAND (self))))
-    ide_run_context_add_environ (run_context, env);
-
-  if (!(launcher = ide_run_context_end (run_context, &error)))
-    goto handle_error;
-
-  return ide_terminal_launcher_new_for_launcher (launcher);
-
-handle_error:
-  if (error != NULL)
-    ide_object_warning (context,
-                        "%s: %s",
-                        _("Failed to launch command"),
-                        error->message);
-
-  return NULL;
-}
-
 GbpShellcmdLocality
 gbp_shellcmd_run_command_get_locality (GbpShellcmdRunCommand *self)
 {
diff --git a/src/plugins/shellcmd/gbp-shellcmd-run-command.h b/src/plugins/shellcmd/gbp-shellcmd-run-command.h
index c25ac2e55..6b3ef0754 100644
--- a/src/plugins/shellcmd/gbp-shellcmd-run-command.h
+++ b/src/plugins/shellcmd/gbp-shellcmd-run-command.h
@@ -53,7 +53,5 @@ void                   gbp_shellcmd_run_command_set_accelerator (GbpShellcmdRunC
 GbpShellcmdLocality    gbp_shellcmd_run_command_get_locality    (GbpShellcmdRunCommand *self);
 void                   gbp_shellcmd_run_command_set_locality    (GbpShellcmdRunCommand *self,
                                                                  GbpShellcmdLocality    locality);
-IdeTerminalLauncher   *gbp_shellcmd_run_command_create_launcher (GbpShellcmdRunCommand *self,
-                                                                 IdeContext            *context);
 
 G_END_DECLS
diff --git a/src/plugins/shellcmd/gbp-shellcmd-shortcut-provider.c 
b/src/plugins/shellcmd/gbp-shellcmd-shortcut-provider.c
index a31e0a031..b96169602 100644
--- a/src/plugins/shellcmd/gbp-shellcmd-shortcut-provider.c
+++ b/src/plugins/shellcmd/gbp-shellcmd-shortcut-provider.c
@@ -43,7 +43,7 @@ gbp_shellcmd_shortcut_func (GtkWidget *widget,
                             GVariant  *args,
                             gpointer   user_data)
 {
-  GbpShellcmdRunCommand *command = user_data;
+  GbpShellcmdRunCommand *run_command = user_data;
   g_autoptr(IdePanelPosition) position = NULL;
   g_autoptr(IdeTerminalLauncher) launcher = NULL;
   IdeWorkspace *workspace;
@@ -55,11 +55,11 @@ gbp_shellcmd_shortcut_func (GtkWidget *widget,
 
   g_assert (GTK_IS_WIDGET (widget));
   g_assert (args == NULL);
-  g_assert (GBP_IS_SHELLCMD_RUN_COMMAND (command));
+  g_assert (GBP_IS_SHELLCMD_RUN_COMMAND (run_command));
 
   g_debug ("Shortcut triggered to run command ā€œ%sā€ which has accelerator %s",
-           ide_run_command_get_display_name (IDE_RUN_COMMAND (command)),
-           gbp_shellcmd_run_command_get_accelerator (command));
+           ide_run_command_get_display_name (IDE_RUN_COMMAND (run_command)),
+           gbp_shellcmd_run_command_get_accelerator (run_command));
 
   if (!(workspace = ide_widget_get_workspace (widget)) ||
       !(context = ide_workspace_get_context (workspace)))
@@ -69,10 +69,11 @@ gbp_shellcmd_shortcut_func (GtkWidget *widget,
       !IDE_IS_EDITOR_WORKSPACE (workspace))
     IDE_RETURN (FALSE);
 
-  if (!(title = ide_run_command_get_display_name (IDE_RUN_COMMAND (command))))
+  if (!(title = ide_run_command_get_display_name (IDE_RUN_COMMAND (run_command))))
     title = _("Untitled command");
 
-  launcher = gbp_shellcmd_run_command_create_launcher (command, context);
+  launcher = ide_terminal_launcher_new (context, IDE_RUN_COMMAND (run_command));
+
   page = g_object_new (IDE_TYPE_TERMINAL_PAGE,
                        "close-on-exit", FALSE,
                        "icon-name", "text-x-script-symbolic",
diff --git a/src/plugins/terminal/gbp-terminal-workspace-addin.c 
b/src/plugins/terminal/gbp-terminal-workspace-addin.c
index 866faf3c1..e6a834ddd 100644
--- a/src/plugins/terminal/gbp-terminal-workspace-addin.c
+++ b/src/plugins/terminal/gbp-terminal-workspace-addin.c
@@ -28,234 +28,148 @@
 #include <libide-terminal.h>
 #include <libide-gui.h>
 
-#include "ide-terminal-private.h"
+#include "ide-terminal-run-command-private.h"
 
 #include "gbp-terminal-workspace-addin.h"
 
 struct _GbpTerminalWorkspaceAddin
 {
-  GObject             parent_instance;
-
-  IdeWorkspace       *workspace;
-
-  IdePane            *bottom_dock;
-  IdeTerminalPage    *bottom;
-
-  IdePane            *run_panel;
-  IdeTerminalPage    *run_terminal;
+  GObject       parent_instance;
+  IdeWorkspace *workspace;
 };
 
-static IdeRuntime *
-find_runtime (IdeWorkspace *workspace)
-{
-  IdeContext *context;
-  IdeConfigManager *config_manager;
-  IdeConfig *config;
-
-  g_assert (IDE_IS_WORKSPACE (workspace));
-
-  context = ide_workspace_get_context (workspace);
-  config_manager = ide_config_manager_from_context (context);
-  config = ide_config_manager_get_current (config_manager);
-
-  return ide_config_get_runtime (config);
-}
-
-static gchar *
-find_builddir (IdeWorkspace *workspace)
-{
-  IdeContext *context;
-  IdeBuildManager *build_manager;
-  IdePipeline *pipeline;
-  const gchar *builddir = NULL;
-
-  if ((context = ide_workspace_get_context (workspace)) &&
-      (build_manager = ide_build_manager_from_context (context)) &&
-      (pipeline = ide_build_manager_get_pipeline (build_manager)) &&
-      (builddir = ide_pipeline_get_builddir (pipeline)) &&
-      g_file_test (builddir, G_FILE_TEST_IS_DIR))
-    return g_strdup (builddir);
-
-  return NULL;
-}
+static void terminal_on_host_action       (GbpTerminalWorkspaceAddin *self,
+                                           GVariant                  *param);
+static void terminal_as_subprocess_action (GbpTerminalWorkspaceAddin *self,
+                                           GVariant                  *param);
+static void terminal_in_pipeline_action   (GbpTerminalWorkspaceAddin *self,
+                                           GVariant                  *param);
+static void terminal_in_runtime_action    (GbpTerminalWorkspaceAddin *self,
+                                           GVariant                  *param);
+
+IDE_DEFINE_ACTION_GROUP (GbpTerminalWorkspaceAddin, gbp_terminal_workspace_addin, {
+  { "terminal-on-host", terminal_on_host_action, "s" },
+  { "terminal-as-subprocess", terminal_as_subprocess_action, "s" },
+  { "terminal-in-pipeline", terminal_in_pipeline_action, "s" },
+  { "terminal-in-runtime", terminal_in_runtime_action, "s" },
+})
 
 static void
-new_terminal_activate (GSimpleAction *action,
-                       GVariant      *param,
-                       gpointer       user_data)
+gbp_terminal_workspace_addin_add_page (GbpTerminalWorkspaceAddin *self,
+                                       IdeTerminalRunLocality     locality,
+                                       const char                *cwd)
 {
-  GbpTerminalWorkspaceAddin *self = user_data;
   g_autoptr(IdeTerminalLauncher) launcher = NULL;
   g_autoptr(IdePanelPosition) position = NULL;
-  g_autoptr(GFile) workdir = NULL;
-  g_autofree gchar *cwd = NULL;
-  IdePage *page;
-  IdeRuntime *runtime = NULL;
+  g_autoptr(IdeRunCommand) run_command = NULL;
   IdeContext *context;
-  const gchar *name;
-  GtkWidget *current_frame = NULL;
   IdePage *current_page;
-  const gchar *uri = NULL;
+  IdePage *page;
+
+  IDE_ENTRY;
 
   g_assert (IDE_IS_MAIN_THREAD ());
-  g_assert (G_IS_SIMPLE_ACTION (action));
   g_assert (GBP_IS_TERMINAL_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_WORKSPACE (self->workspace));
+  g_assert (locality >= IDE_TERMINAL_RUN_ON_HOST);
+  g_assert (locality < IDE_TERMINAL_RUN_LAST);
 
-  /* If we are creating a new terminal while we already have a terminal
-   * focused, then try to copy some details from that terminal.
-   */
-  if ((current_page = ide_workspace_get_most_recent_page (self->workspace)))
-    current_frame = gtk_widget_get_ancestor (GTK_WIDGET (current_page), IDE_TYPE_FRAME);
+  run_command = ide_terminal_run_command_new (locality);
+
+  if (!ide_str_empty0 (cwd))
+    ide_run_command_set_cwd (run_command, cwd);
 
   context = ide_workspace_get_context (self->workspace);
-  workdir = ide_context_ref_workdir (context);
-  name = g_action_get_name (G_ACTION (action));
+  launcher = ide_terminal_launcher_new (context, run_command);
 
-  /* Only allow plain terminals unless this is a project */
-  if (!ide_context_has_project (context) &&
-      !ide_str_equal0 (name, "debug-terminal"))
-    name = "new-terminal";
+  if ((current_page = ide_workspace_get_most_recent_page (self->workspace)))
+    position = ide_page_get_position (current_page);
 
-  if (ide_str_equal0 (name, "new-terminal-in-config"))
-    {
-      IdeConfigManager *config_manager = ide_config_manager_from_context (context);
-      IdeConfig *config = ide_config_manager_get_current (config_manager);
+  if (position == NULL)
+    position = ide_panel_position_new ();
 
-      cwd = find_builddir (self->workspace);
-      launcher = ide_terminal_launcher_new_for_config (config);
-    }
-  else if (ide_str_equal0 (name, "new-terminal-in-runtime"))
-    {
-      runtime = find_runtime (self->workspace);
-      cwd = find_builddir (self->workspace);
-      launcher = ide_terminal_launcher_new_for_runtime (runtime);
-    }
-  else if (ide_str_equal0 (name, "debug-terminal"))
-    {
-      launcher = ide_terminal_launcher_new_for_debug ();
-    }
-  else if (ide_str_equal0 (name, "new-terminal-in-runner"))
-    {
-      runtime = find_runtime (self->workspace);
-      launcher = ide_terminal_launcher_new_for_runner (runtime);
-    }
-  else if (ide_str_equal0 (name, "new-terminal-in-dir"))
-    {
-      page = ide_workspace_get_most_recent_page (self->workspace);
+  page = g_object_new (IDE_TYPE_TERMINAL_PAGE,
+                       "respawn-on-exit", FALSE,
+                       "manage-spawn", TRUE,
+                       "launcher", launcher,
+                       "visible", TRUE,
+                       NULL);
+  ide_workspace_add_page (self->workspace, page, position);
+  panel_widget_raise (PANEL_WIDGET (page));
+  gtk_widget_grab_focus (GTK_WIDGET (page));
 
-      if (IDE_IS_EDITOR_PAGE (page))
-        {
-          IdeBuffer *buffer = ide_editor_page_get_buffer (IDE_EDITOR_PAGE (page));
+  IDE_EXIT;
+}
 
-          if (buffer != NULL)
-            {
-              GFile *file = ide_buffer_get_file (buffer);
-              g_autoptr(GFile) parent = g_file_get_parent (file);
+static void
+terminal_on_host_action (GbpTerminalWorkspaceAddin *self,
+                         GVariant                  *param)
+{
+  const char *cwd = g_variant_get_string (param, NULL);
+  g_autofree char *project_dir = NULL;
 
-              cwd = g_file_get_path (parent);
-            }
-        }
-    }
-  else
+  if (ide_str_empty0 (cwd))
     {
-      launcher = ide_terminal_launcher_new (context);
-      cwd = g_file_get_path (workdir);
+      IdeContext *context = ide_workspace_get_context (self->workspace);
+      g_autoptr(GFile) workdir = ide_context_ref_workdir (context);
+      cwd = project_dir = g_file_get_path (workdir);
     }
 
-  if (IDE_IS_TERMINAL_PAGE (current_page))
-    {
-      if (_ide_terminal_launcher_are_similar (launcher,
-                                              ide_terminal_page_get_launcher (IDE_TERMINAL_PAGE 
(current_page))))
-        uri = ide_terminal_page_get_current_directory_uri (IDE_TERMINAL_PAGE (current_page));
-    }
+  gbp_terminal_workspace_addin_add_page (self, IDE_TERMINAL_RUN_ON_HOST, cwd);
+}
 
-  if (uri != NULL)
-    {
-      g_autoptr(GFile) file = g_file_new_for_uri (uri);
+static void
+terminal_as_subprocess_action (GbpTerminalWorkspaceAddin *self,
+                               GVariant                  *param)
+{
+  const char *cwd = g_variant_get_string (param, NULL);
 
-      if (g_file_is_native (file))
-        ide_terminal_launcher_set_cwd (launcher, g_file_peek_path (file));
-      else if (cwd != NULL)
-        ide_terminal_launcher_set_cwd (launcher, cwd);
-    }
-  else if (cwd != NULL)
-    {
-      ide_terminal_launcher_set_cwd (launcher, cwd);
-    }
+  if (ide_str_empty0 (cwd))
+    cwd = g_get_home_dir ();
 
-  page = g_object_new (IDE_TYPE_TERMINAL_PAGE,
-                       "launcher", launcher,
-                       "respawn-on-exit", FALSE,
-                       "visible", TRUE,
-                       NULL);
+  gbp_terminal_workspace_addin_add_page (self, IDE_TERMINAL_RUN_AS_SUBPROCESS, cwd);
+}
 
-  if (current_frame != NULL)
-    position = ide_frame_get_position (IDE_FRAME (current_frame));
-  else
-    position = ide_panel_position_new ();
+static void
+terminal_in_pipeline_action (GbpTerminalWorkspaceAddin *self,
+                             GVariant                  *param)
+{
+  gbp_terminal_workspace_addin_add_page (self,
+                                         IDE_TERMINAL_RUN_IN_PIPELINE,
+                                         g_variant_get_string (param, NULL));
+}
 
-  ide_workspace_add_page (self->workspace, page, position);
-  panel_widget_raise (PANEL_WIDGET (page));
-  gtk_widget_grab_focus (GTK_WIDGET (page));
+static void
+terminal_in_runtime_action (GbpTerminalWorkspaceAddin *self,
+                            GVariant                  *param)
+{
+  const char *cwd = g_variant_get_string (param, NULL);
+
+  if (ide_str_empty0 (cwd))
+    cwd = g_get_home_dir ();
+
+  gbp_terminal_workspace_addin_add_page (self, IDE_TERMINAL_RUN_AS_SUBPROCESS, cwd);
+  gbp_terminal_workspace_addin_add_page (self, IDE_TERMINAL_RUN_IN_RUNTIME, cwd);
 }
 
 static void
-on_run_manager_run (GbpTerminalWorkspaceAddin *self,
-                    IdeRunContext             *run_context,
-                    IdeRunManager             *run_manager)
+on_run_manager_run (IdeRunManager   *run_manager,
+                    IdeRunContext   *run_context,
+                    IdeTerminalPage *page)
 {
   g_autoptr(GDateTime) now = NULL;
   g_autofree char *formatted = NULL;
   g_autofree char *tmp = NULL;
-  VtePty *pty = NULL;
+  VtePty *pty;
 
   IDE_ENTRY;
 
-  g_assert (GBP_IS_TERMINAL_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (IDE_IS_RUN_CONTEXT (run_context));
   g_assert (IDE_IS_RUN_MANAGER (run_manager));
+  g_assert (IDE_IS_TERMINAL_PAGE (page));
 
-  /*
-   * We need to create a new or re-use our existing terminal page
-   * for run output. Additionally, we need to override the stdin,
-   * stdout, and stderr file-descriptors to our pty master for the
-   * terminal instance.
-   */
-
-  if (!(pty = vte_pty_new_sync (VTE_PTY_DEFAULT, NULL, NULL)))
-    {
-      g_warning ("Failed to allocate PTY for run output");
-      IDE_GOTO (failure);
-    }
-
-  if (self->run_terminal == NULL)
-    {
-      g_autoptr(IdePanelPosition) position = NULL;
-
-      self->run_terminal = g_object_new (IDE_TYPE_TERMINAL_PAGE,
-                                         "manage-spawn", FALSE,
-                                         "pty", pty,
-                                         NULL);
-      ide_pane_observe (g_object_new (IDE_TYPE_PANE,
-                                      "child", self->run_terminal,
-                                      "vexpand", TRUE,
-                                      "hexpand", TRUE,
-                                      "icon-name", "builder-run-start-symbolic",
-                                      "title", _("Application Output"),
-                                      NULL),
-                        &self->run_panel);
-
-      position = ide_panel_position_new ();
-      ide_panel_position_set_edge (position, PANEL_DOCK_POSITION_BOTTOM);
-      ide_workspace_add_pane (self->workspace, self->run_panel, position);
-    }
-  else
-    {
-      ide_terminal_page_set_pty (self->run_terminal, pty);
-    }
-
-  if (self->run_panel != NULL)
-    panel_widget_raise (PANEL_WIDGET (self->run_panel));
+  pty = ide_terminal_page_get_pty (page);
 
   ide_run_context_push (run_context, NULL, NULL, NULL);
   ide_run_context_set_pty (run_context, pty);
@@ -267,43 +181,37 @@ on_run_manager_run (GbpTerminalWorkspaceAddin *self,
 
   /* translators: %s is replaced with the current local time of day */
   formatted = g_strdup_printf (_("Application started at %s\r\n"), tmp);
-  ide_terminal_page_feed (self->run_terminal, formatted);
+  ide_terminal_page_feed (page, formatted);
 
-failure:
-  g_clear_object (&pty);
+  panel_widget_raise (PANEL_WIDGET (page));
 
   IDE_EXIT;
 }
 
 static void
-on_run_manager_stopped (GbpTerminalWorkspaceAddin *self,
-                        IdeRunManager             *run_manager)
+on_run_manager_stopped (IdeRunManager   *run_manager,
+                        IdeTerminalPage *page)
 {
+  IDE_ENTRY;
+
   g_assert (IDE_IS_MAIN_THREAD ());
-  g_assert (GBP_IS_TERMINAL_WORKSPACE_ADDIN (self));
   g_assert (IDE_IS_RUN_MANAGER (run_manager));
+  g_assert (IDE_IS_TERMINAL_PAGE (page));
 
-  ide_terminal_page_feed (self->run_terminal, _("Application exited\r\n"));
-}
+  ide_terminal_page_feed (page, _("Application exited"));
+  ide_terminal_page_feed (page, "\r\n");
 
-static const GActionEntry terminal_actions[] = {
-  { "new-terminal", new_terminal_activate },
-  { "new-terminal-in-config", new_terminal_activate },
-  { "new-terminal-in-runner", new_terminal_activate },
-  { "new-terminal-in-runtime", new_terminal_activate },
-  { "new-terminal-in-dir", new_terminal_activate },
-  { "debug-terminal", new_terminal_activate },
-};
+  IDE_EXIT;
+}
 
 static void
 gbp_terminal_workspace_addin_load (IdeWorkspaceAddin *addin,
                                    IdeWorkspace      *workspace)
 {
   GbpTerminalWorkspaceAddin *self = (GbpTerminalWorkspaceAddin *)addin;
-  IdeWorkbench *workbench;
   g_autoptr(IdePanelPosition) position = NULL;
-  IdeRunManager *run_manager;
-  IdeContext *context;
+  IdePage *page;
+  IdePane *pane;
 
   g_assert (GBP_IS_TERMINAL_WORKSPACE_ADDIN (self));
   g_assert (IDE_IS_PRIMARY_WORKSPACE (workspace) ||
@@ -311,45 +219,54 @@ gbp_terminal_workspace_addin_load (IdeWorkspaceAddin *addin,
 
   self->workspace = workspace;
 
-  g_action_map_add_action_entries (G_ACTION_MAP (workspace),
-                                   terminal_actions,
-                                   G_N_ELEMENTS (terminal_actions),
-                                   self);
-
+  gtk_widget_insert_action_group (GTK_WIDGET (workspace),
+                                  "terminal",
+                                  G_ACTION_GROUP (self));
 
+  /* Always add the terminal panel to primary/editor workspaces */
   position = ide_panel_position_new ();
   ide_panel_position_set_edge (position, PANEL_DOCK_POSITION_BOTTOM);
+  page = g_object_new (IDE_TYPE_TERMINAL_PAGE,
+                       "respawn-on-exit", TRUE,
+                       "visible", TRUE,
+                       NULL);
+  pane = g_object_new (IDE_TYPE_PANE,
+                       "title", _("Terminal"),
+                       "icon-name", "builder-terminal-symbolic",
+                       "child", page,
+                       NULL);
+  ide_workspace_add_pane (workspace, pane, position);
 
-  self->bottom = g_object_new (IDE_TYPE_TERMINAL_PAGE,
-                               "respawn-on-exit", TRUE,
-                               "visible", TRUE,
-                               NULL);
-  ide_pane_observe (g_object_new (IDE_TYPE_PANE,
-                                  "title", _("Terminal"),
-                                  "icon-name", "builder-terminal-symbolic",
-                                  "child", self->bottom,
-                                  NULL),
-                    &self->bottom_dock);
-
-  ide_workspace_add_pane (workspace, IDE_PANE (self->bottom_dock), position);
-
-  workbench = ide_widget_get_workbench (GTK_WIDGET (workspace));
-
-  if (ide_workbench_has_project (workbench) && IDE_IS_PRIMARY_WORKSPACE (workspace))
+  /* Setup panel for application output */
+  if (IDE_IS_PRIMARY_WORKSPACE (workspace))
     {
-      /* Setup terminals when a project is run */
-      context = ide_widget_get_context (GTK_WIDGET (workspace));
-      run_manager = ide_run_manager_from_context (context);
+      IdeWorkbench *workbench = ide_workspace_get_workbench (workspace);
+      IdeContext *context = ide_workbench_get_context (workbench);
+      IdeRunManager *run_manager = ide_run_manager_from_context (context);
+      VtePty *pty = vte_pty_new_sync (VTE_PTY_DEFAULT, NULL, NULL);
+
+      page = g_object_new (IDE_TYPE_TERMINAL_PAGE,
+                           "respawn-on-exit", FALSE,
+                           "manage-spawn", FALSE,
+                           "pty", pty,
+                           NULL);
+      pane = g_object_new (IDE_TYPE_PANE,
+                           "title", _("Application Output"),
+                           "icon-name", "builder-run-start-symbolic",
+                           "child", page,
+                           NULL);
+      ide_workspace_add_pane (workspace, pane, position);
+
       g_signal_connect_object (run_manager,
                                "run",
                                G_CALLBACK (on_run_manager_run),
-                               self,
-                               G_CONNECT_SWAPPED);
+                               page,
+                               0);
       g_signal_connect_object (run_manager,
                                "stopped",
                                G_CALLBACK (on_run_manager_stopped),
-                               self,
-                               G_CONNECT_SWAPPED);
+                               page,
+                               0);
     }
 }
 
@@ -369,11 +286,9 @@ gbp_terminal_workspace_addin_unload (IdeWorkspaceAddin *addin,
 
   if (ide_workbench_has_project (workbench))
     {
-      IdeRunManager *run_manager;
-      IdeContext *context;
+      IdeContext *context = ide_widget_get_context (GTK_WIDGET (workspace));
+      IdeRunManager *run_manager = ide_run_manager_from_context (context);
 
-      context = ide_widget_get_context (GTK_WIDGET (workspace));
-      run_manager = ide_run_manager_from_context (context);
       g_signal_handlers_disconnect_by_func (run_manager,
                                             G_CALLBACK (on_run_manager_run),
                                             self);
@@ -382,16 +297,7 @@ gbp_terminal_workspace_addin_unload (IdeWorkspaceAddin *addin,
                                             self);
     }
 
-  for (guint i = 0; i < G_N_ELEMENTS (terminal_actions); i++)
-    g_action_map_remove_action (G_ACTION_MAP (workspace), terminal_actions[i].name);
-
-  ide_clear_pane (&self->bottom_dock);
-  ide_clear_pane (&self->run_panel);
-
-  self->bottom = NULL;
-  self->bottom_dock = NULL;
-  self->run_terminal = NULL;
-  self->run_panel = NULL;
+  gtk_widget_insert_action_group (GTK_WIDGET (workspace), "terminal", NULL);
 
   self->workspace = NULL;
 }
@@ -404,6 +310,7 @@ workspace_addin_iface_init (IdeWorkspaceAddinInterface *iface)
 }
 
 G_DEFINE_FINAL_TYPE_WITH_CODE (GbpTerminalWorkspaceAddin, gbp_terminal_workspace_addin, G_TYPE_OBJECT,
+                               G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP, 
gbp_terminal_workspace_addin_init_action_group)
                                G_IMPLEMENT_INTERFACE (IDE_TYPE_WORKSPACE_ADDIN, workspace_addin_iface_init))
 
 static void
diff --git a/src/plugins/terminal/gtk/keybindings.json b/src/plugins/terminal/gtk/keybindings.json
index cfa21b1a4..271ccd108 100644
--- a/src/plugins/terminal/gtk/keybindings.json
+++ b/src/plugins/terminal/gtk/keybindings.json
@@ -1,7 +1,7 @@
 /* New Terminal Actions */
-{ "trigger" : "<Control><Shift>t", "action" : "win.new-terminal", "when" : "canEdit()", "phase" : "capture" 
},
-{ "trigger" : "<Control><Alt><Shift>t", "action" : "win.new-terminal-in-config", "when" : "canEdit()", 
"phase" : "capture" },
-{ "trigger" : "<Control><Alt>t", "action" : "win.new-terminal-in-runner", "when" : "canEdit()", "phase" : 
"capture" },
+{ "trigger" : "<Control><Shift>t", "action" : "terminal.terminal-on-host", "args" : "''", "when" : 
"canEdit()", "phase" : "capture" },
+{ "trigger" : "<Control><Alt><Shift>t", "action" : "terminal.terminal-in-pipeline", "args" : "''", "when" : 
"canEdit()", "phase" : "capture" },
+{ "trigger" : "<Control><Alt>t", "action" : "terminal.terminal-in-runtime", "args" : "''", "when" : 
"canEdit()", "phase" : "capture" },
 
 /* IdeTerminal */
 { "trigger" : "<Control><Shift>c", "action" : "clipboard.copy", "when" : "inTerminal()", "phase" : "capture" 
},
diff --git a/src/plugins/terminal/gtk/menus.ui b/src/plugins/terminal/gtk/menus.ui
index 75480c185..e2faa4933 100644
--- a/src/plugins/terminal/gtk/menus.ui
+++ b/src/plugins/terminal/gtk/menus.ui
@@ -3,25 +3,21 @@
   <menu id="new-document-menu">
     <section id="new-terminal-section">
       <item>
-        <attribute name="id">new-terminal</attribute>
-        <attribute name="after">new-file</attribute>
-        <attribute name="before">new-documentation-page</attribute>
         <attribute name="label" translatable="yes">New _Terminal</attribute>
-        <attribute name="action">win.new-terminal</attribute>
+        <attribute name="action">terminal.terminal-on-host</attribute>
+        <attribute name="target" type="s">''</attribute>
         <attribute name="accel">&lt;ctrl&gt;&lt;shift&gt;t</attribute>
       </item>
       <item>
-        <attribute name="id">new-build-terminal</attribute>
-        <attribute name="after">new-terminal</attribute>
         <attribute name="label" translatable="yes">New _Build Terminal</attribute>
-        <attribute name="action">win.new-terminal-in-config</attribute>
+        <attribute name="action">terminal.terminal-in-pipeline</attribute>
+        <attribute name="target" type="s">''</attribute>
         <attribute name="accel">&lt;ctrl&gt;&lt;shift&gt;&lt;alt&gt;t</attribute>
       </item>
       <item>
-        <attribute name="id">new-run-terminal</attribute>
-        <attribute name="after">new-build-terminal</attribute>
         <attribute name="label" translatable="yes">New _Runtime Terminal</attribute>
-        <attribute name="action">win.new-terminal-in-runner</attribute>
+        <attribute name="action">terminal.terminal-in-runtime</attribute>
+        <attribute name="target" type="s">''</attribute>
         <attribute name="accel">&lt;ctrl&gt;&lt;alt&gt;t</attribute>
       </item>
     </section>


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]