[gnome-builder] libide-foundry: add IdeRunCommand



commit 31635d1219d7ade52fbdb7a1537daab048f731d7
Author: Christian Hergert <chergert redhat com>
Date:   Mon Jul 11 20:54:00 2022 -0700

    libide-foundry: add IdeRunCommand
    
    This removes IdeTestProvider and merges it into IdeRunCommandProvider. The
    IdeTestManager now manages a wrapper object around the run command which is
    stateful to track run state of the test.

 src/libide/foundry/ide-run-command-provider.c |  190 +++
 src/libide/foundry/ide-run-command-provider.h |   67 +
 src/libide/foundry/ide-run-command.c          |  543 ++++++++
 src/libide/foundry/ide-run-command.h          |  104 ++
 src/libide/foundry/ide-run-commands.c         |  415 ++++++
 src/libide/foundry/ide-run-commands.h         |   47 +
 src/libide/foundry/ide-run-manager-private.h  |   15 +-
 src/libide/foundry/ide-run-manager.c          | 1774 ++++++++++++++-----------
 src/libide/foundry/ide-run-manager.h          |   64 +-
 src/libide/foundry/ide-runtime.c              |  371 ++----
 src/libide/foundry/ide-runtime.h              |   66 +-
 src/libide/foundry/ide-test-manager.c         | 1062 ++++-----------
 src/libide/foundry/ide-test-manager.h         |   40 +-
 src/libide/foundry/ide-test-private.h         |   43 -
 src/libide/foundry/ide-test-provider.c        |  344 -----
 src/libide/foundry/ide-test-provider.h        |   86 --
 src/libide/foundry/ide-test.c                 |  457 +++----
 src/libide/foundry/ide-test.h                 |   63 +-
 src/libide/foundry/libide-foundry.h           |    5 +-
 src/libide/foundry/meson.build                |    2 -
 20 files changed, 3023 insertions(+), 2735 deletions(-)
---
diff --git a/src/libide/foundry/ide-run-command-provider.c b/src/libide/foundry/ide-run-command-provider.c
new file mode 100644
index 000000000..72bff4af2
--- /dev/null
+++ b/src/libide/foundry/ide-run-command-provider.c
@@ -0,0 +1,190 @@
+/* ide-run-command-provider.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-run-command-provider"
+
+#include "config.h"
+
+#include "ide-build-manager.h"
+#include "ide-pipeline.h"
+#include "ide-run-command-provider.h"
+
+G_DEFINE_INTERFACE (IdeRunCommandProvider, ide_run_command_provider, IDE_TYPE_OBJECT)
+
+enum {
+  INVALIDATED,
+  N_SIGNALS
+};
+
+static guint signals[N_SIGNALS];
+
+static void
+ide_run_command_provider_default_init (IdeRunCommandProviderInterface *iface)
+{
+  signals[INVALIDATED] =
+    g_signal_new ("invalidated",
+                  G_TYPE_FROM_INTERFACE (iface),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (IdeRunCommandProviderInterface, invalidated),
+                  NULL, NULL,
+                  NULL,
+                  G_TYPE_NONE, 0);
+}
+
+void
+ide_run_command_provider_list_commands_async (IdeRunCommandProvider *self,
+                                              GCancellable          *cancellable,
+                                              GAsyncReadyCallback    callback,
+                                              gpointer               user_data)
+{
+  g_return_if_fail (IDE_IS_RUN_COMMAND_PROVIDER (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_RUN_COMMAND_PROVIDER_GET_IFACE (self)->list_commands_async (self, cancellable, callback, user_data);
+}
+
+/**
+ * ide_run_command_provider_list_commands_finish:
+ * @self: a #IdeRunCommandProvider
+ * @result: a #GAsyncResult
+ * @error: location for a #GError
+ *
+ * Completes request to list run commands.
+ *
+ * Returns: (transfer full): a #GListModel of #IdeRunCommand
+ */
+GListModel *
+ide_run_command_provider_list_commands_finish (IdeRunCommandProvider  *self,
+                                               GAsyncResult           *result,
+                                               GError                **error)
+{
+  g_return_val_if_fail (IDE_IS_RUN_COMMAND_PROVIDER (self), NULL);
+
+  return IDE_RUN_COMMAND_PROVIDER_GET_IFACE (self)->list_commands_finish (self, result, error);
+}
+
+/**
+ * ide_run_command_provider_invalidate:
+ * @self: a #IdeRunCommandProvider
+ *
+ * Emits the #IdeRunCommandProvider::invalidated signal.
+ *
+ * This often results in #IdeRunCommands requesting a new set of results for
+ * the run command provider via ide_run_command_provider_list_commands_async().
+ */
+void
+ide_run_command_provider_invalidate (IdeRunCommandProvider *self)
+{
+  g_return_if_fail (IDE_IS_RUN_COMMAND_PROVIDER (self));
+
+  g_signal_emit (self, signals[INVALIDATED], 0);
+}
+
+static inline gulong
+get_signal_handler (IdeRunCommandProvider *self)
+{
+  return GPOINTER_TO_SIZE (g_object_get_data (G_OBJECT (self), "INVALIDATES_AT_PHASE"));
+}
+
+static inline void
+set_signal_handler (IdeRunCommandProvider *self,
+                    gulong                 handler_id)
+{
+  g_object_set_data (G_OBJECT (self), "INVALIDATES_AT_PHASE", GSIZE_TO_POINTER (handler_id));
+}
+
+static void
+ide_run_command_provider_pipeline_notify_phase_cb (IdeRunCommandProvider *self,
+                                                   GParamSpec            *pspec,
+                                                   IdePipeline           *pipeline)
+{
+  IdePipelinePhase current_phase;
+  IdePipelinePhase invalidate_phase;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_RUN_COMMAND_PROVIDER (self));
+  g_assert (IDE_IS_PIPELINE (pipeline));
+
+  invalidate_phase = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (self), "PIPELINE_PHASE"));
+  current_phase = ide_pipeline_get_phase (pipeline);
+
+  /* Only invalidate when the phase exactly matches. We could check to see if
+   * the current phase is > than the last notified phase, but generally
+   * speaking, the users of this have a pipeline stage attached at exactly that
+   * phase to be notified of.
+   */
+  if (invalidate_phase != 0 && current_phase != 0 && invalidate_phase == current_phase)
+    ide_run_command_provider_invalidate (self);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_run_command_provider_invalidates_at_phase:
+ * @self: an #IdeRunCommandProvider
+ * @phase: an #IdePipelinePhase
+ *
+ * Invalidates the provider when @phase is reached.
+ *
+ * This is a helper for run command provider implementations to use which
+ * will automatically invalidate @self when pipeline @phase is reached.
+ *
+ * Calling this function will unset any previous call to the function. Setting
+ * @phase to 0 will not subscribe to any new phase.
+ */
+void
+ide_run_command_provider_invalidates_at_phase  (IdeRunCommandProvider  *self,
+                                                IdePipelinePhase        phase)
+{
+  GSignalGroup *signal_group;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_RUN_COMMAND_PROVIDER (self));
+
+  g_object_set_data (G_OBJECT (self), "PIPELINE_PHASE", GUINT_TO_POINTER (phase));
+
+  if (phase == 0)
+    IDE_EXIT;
+
+
+  if (!(signal_group = g_object_get_data (G_OBJECT (self), "PIPELINE_SIGNAL_GROUP")))
+    {
+      IdeContext *context = ide_object_get_context (IDE_OBJECT (self));
+      IdeBuildManager *build_manager = ide_build_manager_from_context (context);
+
+      signal_group = g_signal_group_new (IDE_TYPE_PIPELINE);
+      g_signal_group_connect_object (signal_group,
+                                     "notify::phase",
+                                     G_CALLBACK (ide_run_command_provider_pipeline_notify_phase_cb),
+                                     self,
+                                     G_CONNECT_SWAPPED);
+      g_object_set_data_full (G_OBJECT (self),
+                              "PIPELINE_SIGNAL_GROUP",
+                              signal_group,
+                              g_object_unref);
+      g_object_bind_property (build_manager, "pipeline", signal_group, "target", G_BINDING_SYNC_CREATE);
+    }
+
+  IDE_EXIT;
+}
diff --git a/src/libide/foundry/ide-run-command-provider.h b/src/libide/foundry/ide-run-command-provider.h
new file mode 100644
index 000000000..c4617dbc7
--- /dev/null
+++ b/src/libide/foundry/ide-run-command-provider.h
@@ -0,0 +1,67 @@
+/* ide-run-command-provider.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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-pipeline-phase.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_RUN_COMMAND_PROVIDER (ide_run_command_provider_get_type())
+
+IDE_AVAILABLE_IN_ALL
+G_DECLARE_INTERFACE (IdeRunCommandProvider, ide_run_command_provider, IDE, RUN_COMMAND_PROVIDER, IdeObject)
+
+struct _IdeRunCommandProviderInterface
+{
+  GTypeInterface parent_iface;
+
+  void        (*invalidated)          (IdeRunCommandProvider  *self);
+  void        (*list_commands_async)  (IdeRunCommandProvider  *self,
+                                       GCancellable           *cancellable,
+                                       GAsyncReadyCallback     callback,
+                                       gpointer                user_data);
+  GListModel *(*list_commands_finish) (IdeRunCommandProvider  *self,
+                                       GAsyncResult           *result,
+                                       GError                **error);
+};
+
+IDE_AVAILABLE_IN_ALL
+void        ide_run_command_provider_invalidate           (IdeRunCommandProvider  *self);
+IDE_AVAILABLE_IN_ALL
+void        ide_run_command_provider_invalidates_at_phase (IdeRunCommandProvider  *self,
+                                                           IdePipelinePhase        phase);
+IDE_AVAILABLE_IN_ALL
+void        ide_run_command_provider_list_commands_async  (IdeRunCommandProvider  *self,
+                                                           GCancellable           *cancellable,
+                                                           GAsyncReadyCallback     callback,
+                                                           gpointer                user_data);
+IDE_AVAILABLE_IN_ALL
+GListModel *ide_run_command_provider_list_commands_finish (IdeRunCommandProvider  *self,
+                                                           GAsyncResult           *result,
+                                                           GError                **error);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-run-command.c b/src/libide/foundry/ide-run-command.c
new file mode 100644
index 000000000..f9dc6a8b1
--- /dev/null
+++ b/src/libide/foundry/ide-run-command.c
@@ -0,0 +1,543 @@
+/* ide-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-run-command"
+
+#include "config.h"
+
+#include "ide-build-manager.h"
+#include "ide-foundry-enums.h"
+#include "ide-pipeline.h"
+#include "ide-run-command.h"
+#include "ide-run-context.h"
+
+typedef struct
+{
+  char *id;
+  char *cwd;
+  char *display_name;
+  char **environ;
+  char **argv;
+  char **languages;
+  int priority;
+  IdeRunCommandKind kind;
+} IdeRunCommandPrivate;
+
+enum {
+  PROP_0,
+  PROP_ARGV,
+  PROP_CWD,
+  PROP_DISPLAY_NAME,
+  PROP_ENVIRON,
+  PROP_ID,
+  PROP_KIND,
+  PROP_LANGUAGES,
+  PROP_PRIORITY,
+  N_PROPS
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeRunCommand, ide_run_command, G_TYPE_OBJECT)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_run_command_real_prepare_to_run (IdeRunCommand *self,
+                                     IdeRunContext *run_context,
+                                     IdeContext    *context)
+{
+  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;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_RUN_COMMAND (self));
+  g_assert (IDE_IS_RUN_CONTEXT (run_context));
+  g_assert (IDE_IS_CONTEXT (context));
+
+  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);
+
+  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);
+
+  IDE_EXIT;
+}
+
+static void
+ide_run_command_finalize (GObject *object)
+{
+  IdeRunCommand *self = (IdeRunCommand *)object;
+  IdeRunCommandPrivate *priv = ide_run_command_get_instance_private (self);
+
+  g_clear_pointer (&priv->id, g_free);
+  g_clear_pointer (&priv->cwd, g_free);
+  g_clear_pointer (&priv->display_name, g_free);
+  g_clear_pointer (&priv->environ, g_strfreev);
+  g_clear_pointer (&priv->argv, g_strfreev);
+  g_clear_pointer (&priv->languages, g_strfreev);
+
+  G_OBJECT_CLASS (ide_run_command_parent_class)->finalize (object);
+}
+
+static void
+ide_run_command_get_property (GObject    *object,
+                              guint       prop_id,
+                              GValue     *value,
+                              GParamSpec *pspec)
+{
+  IdeRunCommand *self = IDE_RUN_COMMAND (object);
+
+  switch (prop_id)
+    {
+    case PROP_CWD:
+      g_value_set_string (value, ide_run_command_get_cwd (self));
+      break;
+
+    case PROP_DISPLAY_NAME:
+      g_value_set_string (value, ide_run_command_get_display_name (self));
+      break;
+
+    case PROP_ARGV:
+      g_value_set_boxed (value, ide_run_command_get_argv (self));
+      break;
+
+    case PROP_ENVIRON:
+      g_value_set_boxed (value, ide_run_command_get_environ (self));
+      break;
+
+    case PROP_ID:
+      g_value_set_string (value, ide_run_command_get_id (self));
+      break;
+
+    case PROP_KIND:
+      g_value_set_enum (value, ide_run_command_get_kind (self));
+      break;
+
+    case PROP_LANGUAGES:
+      g_value_set_boxed (value, ide_run_command_get_languages (self));
+      break;
+
+    case PROP_PRIORITY:
+      g_value_set_int (value, ide_run_command_get_priority (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_run_command_set_property (GObject      *object,
+                              guint         prop_id,
+                              const GValue *value,
+                              GParamSpec   *pspec)
+{
+  IdeRunCommand *self = IDE_RUN_COMMAND (object);
+
+  switch (prop_id)
+    {
+    case PROP_CWD:
+      ide_run_command_set_cwd (self, g_value_get_string (value));
+      break;
+
+    case PROP_DISPLAY_NAME:
+      ide_run_command_set_display_name (self, g_value_get_string (value));
+      break;
+
+    case PROP_ARGV:
+      ide_run_command_set_argv (self, g_value_get_boxed (value));
+      break;
+
+    case PROP_ENVIRON:
+      ide_run_command_set_environ (self, g_value_get_boxed (value));
+      break;
+
+    case PROP_ID:
+      ide_run_command_set_id (self, g_value_get_string (value));
+      break;
+
+    case PROP_KIND:
+      ide_run_command_set_kind (self, g_value_get_enum (value));
+      break;
+
+    case PROP_LANGUAGES:
+      ide_run_command_set_languages (self, g_value_get_boxed (value));
+      break;
+
+    case PROP_PRIORITY:
+      ide_run_command_set_priority (self, g_value_get_int (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_run_command_class_init (IdeRunCommandClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_run_command_finalize;
+  object_class->get_property = ide_run_command_get_property;
+  object_class->set_property = ide_run_command_set_property;
+
+  klass->prepare_to_run = ide_run_command_real_prepare_to_run;
+
+  properties [PROP_ARGV] =
+    g_param_spec_boxed ("argv", NULL, NULL,
+                        G_TYPE_STRV,
+                        (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_CWD] =
+    g_param_spec_string ("cwd", NULL, NULL,
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_DISPLAY_NAME] =
+    g_param_spec_string ("display-name", NULL, NULL,
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_ENVIRON] =
+    g_param_spec_boxed ("environ", NULL, NULL,
+                        G_TYPE_STRV,
+                        (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_ID] =
+    g_param_spec_string ("id", NULL, NULL,
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_KIND] =
+    g_param_spec_enum ("kind", NULL, NULL,
+                       IDE_TYPE_RUN_COMMAND_KIND,
+                       IDE_RUN_COMMAND_KIND_UNKNOWN,
+                       (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeRunCommand:languages:
+   *
+   * Contains the programming languages used.
+   *
+   * This is to be set by run command providers when they know what languages
+   * are used to create the program spawned by the run command. This can be
+   * used by debuggers to ensure that a suitable debugger is chosen for a given
+   * language used.
+   */
+  properties [PROP_LANGUAGES] =
+    g_param_spec_boxed ("languages", NULL, NULL,
+                        G_TYPE_STRV,
+                        (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_PRIORITY] =
+    g_param_spec_int ("priority", NULL, NULL,
+                      G_MININT, G_MAXINT, 0,
+                      (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_run_command_init (IdeRunCommand *self)
+{
+}
+
+IdeRunCommand *
+ide_run_command_new (void)
+{
+  return g_object_new (IDE_TYPE_RUN_COMMAND, NULL);
+}
+
+const char *
+ide_run_command_get_id (IdeRunCommand *self)
+{
+  IdeRunCommandPrivate *priv = ide_run_command_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_RUN_COMMAND (self), NULL);
+
+  return priv->id;
+}
+
+void
+ide_run_command_set_id (IdeRunCommand *self,
+                        const char    *id)
+{
+  IdeRunCommandPrivate *priv = ide_run_command_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_RUN_COMMAND (self));
+
+  if (g_strcmp0 (priv->id, id) != 0)
+    {
+      g_free (priv->id);
+      priv->id = g_strdup (id);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ID]);
+    }
+}
+
+const char *
+ide_run_command_get_cwd (IdeRunCommand *self)
+{
+  IdeRunCommandPrivate *priv = ide_run_command_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_RUN_COMMAND (self), NULL);
+
+  return priv->cwd;
+}
+
+void
+ide_run_command_set_cwd (IdeRunCommand *self,
+                         const char    *cwd)
+{
+  IdeRunCommandPrivate *priv = ide_run_command_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_RUN_COMMAND (self));
+
+  if (g_strcmp0 (priv->cwd, cwd) != 0)
+    {
+      g_free (priv->cwd);
+      priv->cwd = g_strdup (cwd);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CWD]);
+    }
+}
+
+const char *
+ide_run_command_get_display_name (IdeRunCommand *self)
+{
+  IdeRunCommandPrivate *priv = ide_run_command_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_RUN_COMMAND (self), NULL);
+
+  return priv->display_name;
+}
+
+void
+ide_run_command_set_display_name (IdeRunCommand *self,
+                                  const char    *display_name)
+{
+  IdeRunCommandPrivate *priv = ide_run_command_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_RUN_COMMAND (self));
+
+  if (g_strcmp0 (priv->display_name, display_name) != 0)
+    {
+      g_free (priv->display_name);
+      priv->display_name = g_strdup (display_name);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_DISPLAY_NAME]);
+    }
+}
+
+const char * const *
+ide_run_command_get_argv (IdeRunCommand *self)
+{
+  IdeRunCommandPrivate *priv = ide_run_command_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_RUN_COMMAND (self), NULL);
+
+  return (const char * const *)priv->argv;
+}
+
+void
+ide_run_command_set_argv (IdeRunCommand      *self,
+                          const char * const *argv)
+{
+  IdeRunCommandPrivate *priv = ide_run_command_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_RUN_COMMAND (self));
+
+  if (argv == (const char * const *)priv->argv)
+    return;
+
+  g_strfreev (priv->argv);
+  priv->argv = g_strdupv ((char **)argv);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ARGV]);
+}
+
+const char * const *
+ide_run_command_get_environ (IdeRunCommand *self)
+{
+  IdeRunCommandPrivate *priv = ide_run_command_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_RUN_COMMAND (self), NULL);
+
+  return (const char * const *)priv->environ;
+}
+
+void
+ide_run_command_set_environ (IdeRunCommand      *self,
+                             const char * const *environ)
+{
+  IdeRunCommandPrivate *priv = ide_run_command_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_RUN_COMMAND (self));
+
+  if (environ == (const char * const *)priv->environ)
+    return;
+
+  g_strfreev (priv->environ);
+  priv->environ = g_strdupv ((char **)environ);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ENVIRON]);
+}
+
+int
+ide_run_command_get_priority (IdeRunCommand *self)
+{
+  IdeRunCommandPrivate *priv = ide_run_command_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_RUN_COMMAND (self), -1);
+
+  return priv->priority;
+}
+
+void
+ide_run_command_set_priority (IdeRunCommand *self,
+                              int            priority)
+{
+  IdeRunCommandPrivate *priv = ide_run_command_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_RUN_COMMAND (self));
+
+  if (priority != priv->priority)
+    {
+      priv->priority = priority;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PRIORITY]);
+    }
+}
+
+IdeRunCommandKind
+ide_run_command_get_kind (IdeRunCommand *self)
+{
+  IdeRunCommandPrivate *priv = ide_run_command_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_RUN_COMMAND (self), 0);
+
+  return priv->kind;
+}
+
+/**
+ * ide_run_command_set_kind:
+ * @self: a #IdeRunCommand
+ *
+ * Sets the kind of command.
+ *
+ * This is useful for #IdeRunCommandProvider that want to specify
+ * the type of command that is being provided. Doing so allows tooling
+ * in Builder to treat that information specially, such as showing tags
+ * next to the row in UI or including it in "Unit Test" browsers.
+ */
+void
+ide_run_command_set_kind (IdeRunCommand     *self,
+                          IdeRunCommandKind  kind)
+{
+  IdeRunCommandPrivate *priv = ide_run_command_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_RUN_COMMAND (self));
+  g_return_if_fail (kind <= IDE_RUN_COMMAND_KIND_USER_DEFINED);
+
+  if (priv->kind != kind)
+    {
+      priv->kind = kind;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_KIND]);
+    }
+}
+
+const char * const *
+ide_run_command_get_languages (IdeRunCommand *self)
+{
+  IdeRunCommandPrivate *priv = ide_run_command_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_RUN_COMMAND (self), NULL);
+
+  return (const char * const *)priv->languages;
+}
+
+void
+ide_run_command_set_languages (IdeRunCommand      *self,
+                               const char * const *languages)
+{
+  IdeRunCommandPrivate *priv = ide_run_command_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_RUN_COMMAND (self));
+
+  if (languages == (const char * const *)priv->languages ||
+      (languages != NULL &&
+       priv->languages != NULL &&
+       g_strv_equal ((const char * const *)priv->languages, languages)))
+    return;
+
+  g_strfreev (priv->languages);
+  priv->languages = g_strdupv ((char **)languages);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_LANGUAGES]);
+}
+
+/**
+ * ide_run_command_prepare_to_run:
+ * @self: a #IdeRunCommand
+ * @run_context: an #IdeRunContext
+ * @context: an #IdeContext
+ *
+ * Prepares the run command to be run within @run_context.
+ *
+ * This requires that the run command add anything necessary to the
+ * @run_context so that the command can be run.
+ *
+ * Subclasses may override this to implement custom functionality such as
+ * locality-based execution (see shellcmd plugin).
+ */
+void
+ide_run_command_prepare_to_run (IdeRunCommand *self,
+                                IdeRunContext *run_context,
+                                IdeContext    *context)
+{
+  g_return_if_fail (IDE_IS_RUN_COMMAND (self));
+  g_return_if_fail (IDE_IS_RUN_CONTEXT (run_context));
+  g_return_if_fail (IDE_IS_CONTEXT (context));
+
+  IDE_RUN_COMMAND_GET_CLASS (self)->prepare_to_run (self, run_context, context);
+}
diff --git a/src/libide/foundry/ide-run-command.h b/src/libide/foundry/ide-run-command.h
new file mode 100644
index 000000000..a516dc41f
--- /dev/null
+++ b/src/libide/foundry/ide-run-command.h
@@ -0,0 +1,104 @@
+/* ide-run-command.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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-foundry-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_RUN_COMMAND (ide_run_command_get_type())
+
+typedef enum
+{
+  IDE_RUN_COMMAND_KIND_UNKNOWN = 0,
+  IDE_RUN_COMMAND_KIND_APPLICATION,
+  IDE_RUN_COMMAND_KIND_UTILITY,
+  IDE_RUN_COMMAND_KIND_TEST,
+  IDE_RUN_COMMAND_KIND_BENCHMARK,
+  IDE_RUN_COMMAND_KIND_USER_DEFINED,
+} IdeRunCommandKind;
+
+IDE_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (IdeRunCommand, ide_run_command, IDE, RUN_COMMAND, GObject)
+
+struct _IdeRunCommandClass
+{
+  GObjectClass parent_class;
+
+  void (*prepare_to_run) (IdeRunCommand *self,
+                          IdeRunContext *run_context,
+                          IdeContext    *context);
+};
+
+IDE_AVAILABLE_IN_ALL
+IdeRunCommand      *ide_run_command_new              (void);
+IDE_AVAILABLE_IN_ALL
+const char         *ide_run_command_get_id           (IdeRunCommand      *self);
+IDE_AVAILABLE_IN_ALL
+void                ide_run_command_set_id           (IdeRunCommand      *self,
+                                                      const char         *id);
+IDE_AVAILABLE_IN_ALL
+const char         *ide_run_command_get_cwd          (IdeRunCommand      *self);
+IDE_AVAILABLE_IN_ALL
+void                ide_run_command_set_cwd          (IdeRunCommand      *self,
+                                                      const char         *cwd);
+IDE_AVAILABLE_IN_ALL
+const char         *ide_run_command_get_display_name (IdeRunCommand      *self);
+IDE_AVAILABLE_IN_ALL
+void                ide_run_command_set_display_name (IdeRunCommand      *self,
+                                                      const char         *display_name);
+IDE_AVAILABLE_IN_ALL
+const char * const *ide_run_command_get_argv         (IdeRunCommand      *self);
+IDE_AVAILABLE_IN_ALL
+void                ide_run_command_set_argv         (IdeRunCommand      *self,
+                                                      const char * const *argv);
+IDE_AVAILABLE_IN_ALL
+const char * const *ide_run_command_get_environ      (IdeRunCommand      *self);
+IDE_AVAILABLE_IN_ALL
+void                ide_run_command_set_environ      (IdeRunCommand      *self,
+                                                      const char * const *environ);
+IDE_AVAILABLE_IN_ALL
+int                 ide_run_command_get_priority     (IdeRunCommand      *self);
+IDE_AVAILABLE_IN_ALL
+void                ide_run_command_set_priority     (IdeRunCommand      *self,
+                                                      int                 priority);
+IDE_AVAILABLE_IN_ALL
+IdeRunCommandKind   ide_run_command_get_kind         (IdeRunCommand      *self);
+IDE_AVAILABLE_IN_ALL
+void                ide_run_command_set_kind         (IdeRunCommand      *self,
+                                                      IdeRunCommandKind   kind);
+IDE_AVAILABLE_IN_ALL
+const char * const *ide_run_command_get_languages    (IdeRunCommand      *self);
+IDE_AVAILABLE_IN_ALL
+void                ide_run_command_set_languages    (IdeRunCommand      *self,
+                                                      const char * const *languages);
+IDE_AVAILABLE_IN_ALL
+void                ide_run_command_prepare_to_run   (IdeRunCommand      *self,
+                                                      IdeRunContext      *run_context,
+                                                      IdeContext         *context);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-run-commands.c b/src/libide/foundry/ide-run-commands.c
new file mode 100644
index 000000000..fd3056f41
--- /dev/null
+++ b/src/libide/foundry/ide-run-commands.c
@@ -0,0 +1,415 @@
+/* ide-run-commands.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-run-commands"
+
+#include "config.h"
+
+#include <gtk/gtk.h>
+
+#include <libide-plugins.h>
+#include <libide-threading.h>
+
+#include "ide-run-command.h"
+#include "ide-run-command-provider.h"
+#include "ide-run-commands.h"
+
+#define RELOAD_TIMEOUT_MSEC 250
+
+struct _IdeRunCommands
+{
+  IdeObject               parent_instance;
+  IdeExtensionSetAdapter *addins;
+  GListStore             *models;
+  GtkFlattenListModel    *flatten_model;
+  GHashTable             *provider_to_model;
+  GQueue                  invalid;
+  guint                   reload_source;
+};
+
+static GType
+ide_run_commands_get_item_type (GListModel *model)
+{
+  return IDE_TYPE_RUN_COMMAND;
+}
+
+static guint
+ide_run_commands_get_n_items (GListModel *model)
+{
+  IdeRunCommands *self = IDE_RUN_COMMANDS (model);
+  return g_list_model_get_n_items (G_LIST_MODEL (self->flatten_model));
+}
+
+static gpointer
+ide_run_commands_get_item (GListModel *model,
+                           guint       position)
+{
+  IdeRunCommands *self = IDE_RUN_COMMANDS (model);
+  return g_list_model_get_item (G_LIST_MODEL (self->flatten_model), position);
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+  iface->get_item_type = ide_run_commands_get_item_type;
+  iface->get_n_items = ide_run_commands_get_n_items;
+  iface->get_item = ide_run_commands_get_item;
+}
+
+G_DEFINE_FINAL_TYPE_WITH_CODE (IdeRunCommands, ide_run_commands, IDE_TYPE_OBJECT,
+                               G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
+static void
+ide_run_commands_items_changed_cb (IdeRunCommands *self,
+                                   guint           position,
+                                   guint           removed,
+                                   guint           added,
+                                   GListModel     *model)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_RUN_COMMANDS (self));
+  g_assert (G_IS_LIST_MODEL (model));
+
+  g_list_model_items_changed (G_LIST_MODEL (self), position, removed, added);
+
+  IDE_EXIT;
+}
+
+static void
+ide_run_commands_list_commands_cb (GObject      *object,
+                                   GAsyncResult *result,
+                                   gpointer      user_data)
+{
+  IdeRunCommandProvider *provider = (IdeRunCommandProvider *)object;
+  g_autoptr(IdeRunCommands) self = user_data;
+  g_autoptr(GListModel) model = NULL;
+  g_autoptr(GError) error = NULL;
+  GListModel *old_model;
+  gboolean found;
+  guint position;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_RUN_COMMAND_PROVIDER (provider));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_RUN_COMMANDS (self));
+
+  if (!(model = ide_run_command_provider_list_commands_finish (provider, result, &error)))
+    {
+      /* Just keep the old one around until things succeed */
+      g_debug ("Failed to list run commands from %s: %s",
+               G_OBJECT_TYPE_NAME (provider), error->message);
+      IDE_EXIT;
+    }
+
+  /* Do nothing if the model didn't change */
+  old_model = g_hash_table_lookup (self->provider_to_model, provider);
+  if (old_model == model)
+    IDE_EXIT;
+
+  g_assert (old_model != model);
+  g_assert (!old_model || G_IS_LIST_MODEL (old_model));
+  g_assert (G_IS_LIST_MODEL (model));
+
+  /* First try to locate our model */
+  if (old_model != NULL)
+    found = g_list_store_find (self->models, old_model, &position);
+  else
+    found = FALSE;
+
+  /* Now ensure our hashtable is up to date for re-entrancy purposes */
+  g_hash_table_insert (self->provider_to_model,
+                       g_object_ref (provider),
+                       g_object_ref (model));
+
+  if (found)
+    g_list_store_splice (self->models, position, 1, (gpointer *)&model, 1);
+  else
+    g_list_store_append (self->models, model);
+
+  IDE_EXIT;
+}
+
+static gboolean
+ide_run_commands_reload_source_func (gpointer data)
+{
+  g_autoptr(GCancellable) cancellable = NULL;
+  IdeRunCommandProvider *provider;
+  IdeRunCommands *self = data;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_RUN_COMMANDS (self));
+
+  self->reload_source = 0;
+
+  cancellable = ide_object_ref_cancellable (IDE_OBJECT (self));
+
+  while ((provider = g_queue_pop_head (&self->invalid)))
+    ide_run_command_provider_list_commands_async (provider,
+                                                  cancellable,
+                                                  ide_run_commands_list_commands_cb,
+                                                  g_object_ref (self));
+
+  IDE_RETURN (G_SOURCE_REMOVE);
+}
+
+static void
+ide_run_commands_provider_invalidated_cb (IdeRunCommands        *self,
+                                          IdeRunCommandProvider *provider)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_RUN_COMMANDS (self));
+  g_assert (IDE_IS_RUN_COMMAND_PROVIDER (provider));
+
+  if (g_queue_find (&self->invalid, provider) == NULL)
+    {
+      g_queue_push_tail (&self->invalid, provider);
+
+      if (self->reload_source == 0)
+        self->reload_source = g_timeout_add (RELOAD_TIMEOUT_MSEC,
+                                             ide_run_commands_reload_source_func,
+                                             self);
+    }
+
+  IDE_EXIT;
+}
+
+static void
+ide_run_commands_provider_added_cb (IdeExtensionSetAdapter *set,
+                                    PeasPluginInfo         *plugin_info,
+                                    PeasExtension          *exten,
+                                    gpointer                user_data)
+{
+  IdeRunCommandProvider *provider = (IdeRunCommandProvider *)exten;
+  IdeRunCommands *self = user_data;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_RUN_COMMAND_PROVIDER (provider));
+  g_assert (IDE_IS_RUN_COMMANDS (self));
+
+  g_signal_connect_object (provider,
+                           "invalidated",
+                           G_CALLBACK (ide_run_commands_provider_invalidated_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  ide_run_commands_provider_invalidated_cb (self, provider);
+
+  IDE_EXIT;
+}
+
+static void
+ide_run_commands_provider_removed_cb (IdeExtensionSetAdapter *set,
+                                      PeasPluginInfo         *plugin_info,
+                                      PeasExtension          *exten,
+                                      gpointer                user_data)
+{
+  IdeRunCommandProvider *provider = (IdeRunCommandProvider *)exten;
+  g_autoptr(IdeRunCommandProvider) stolen_key = NULL;
+  g_autoptr(GListModel) stolen_value = NULL;
+  IdeRunCommands *self = user_data;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_RUN_COMMAND_PROVIDER (provider));
+  g_assert (IDE_IS_RUN_COMMANDS (self));
+
+  if (g_hash_table_steal_extended (self->provider_to_model,
+                                   provider,
+                                   (gpointer *)&stolen_key,
+                                   (gpointer *)&stolen_value))
+    {
+      g_queue_remove (&self->invalid, provider);
+
+      if (self->invalid.length == 0)
+        g_clear_handle_id (&self->reload_source, g_source_remove);
+
+      if (stolen_value != NULL)
+        {
+          guint position;
+
+          if (g_list_store_find (self->models, stolen_value, &position))
+            g_list_store_remove (self->models, position);
+        }
+    }
+
+  IDE_EXIT;
+}
+
+static void
+ide_run_commands_parent_set (IdeObject *object,
+                             IdeObject *parent)
+{
+  IdeRunCommands *self = (IdeRunCommands *)object;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_RUN_COMMANDS (self));
+  g_assert (!parent || IDE_IS_OBJECT (parent));
+
+  if (parent == NULL)
+    IDE_EXIT;
+
+  self->addins = ide_extension_set_adapter_new (IDE_OBJECT (self),
+                                                peas_engine_get_default (),
+                                                IDE_TYPE_RUN_COMMAND_PROVIDER,
+                                                NULL, NULL);
+  g_signal_connect (self->addins,
+                    "extension-added",
+                    G_CALLBACK (ide_run_commands_provider_added_cb),
+                    self);
+  g_signal_connect (self->addins,
+                    "extension-removed",
+                    G_CALLBACK (ide_run_commands_provider_removed_cb),
+                    self);
+  ide_extension_set_adapter_foreach (self->addins,
+                                     ide_run_commands_provider_added_cb,
+                                     self);
+
+  IDE_EXIT;
+}
+
+static void
+ide_run_commands_dispose (GObject *object)
+{
+  IdeRunCommands *self = (IdeRunCommands *)object;
+
+  g_queue_clear (&self->invalid);
+  g_clear_handle_id (&self->reload_source, g_source_remove);
+  ide_clear_and_destroy_object (&self->addins);
+  g_clear_object (&self->models);
+  g_clear_object (&self->flatten_model);
+  g_clear_pointer (&self->provider_to_model, g_hash_table_unref);
+
+  G_OBJECT_CLASS (ide_run_commands_parent_class)->dispose (object);
+}
+
+static void
+ide_run_commands_class_init (IdeRunCommandsClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeObjectClass *ide_object_class = IDE_OBJECT_CLASS (klass);
+
+  object_class->dispose = ide_run_commands_dispose;
+
+  ide_object_class->parent_set = ide_run_commands_parent_set;
+}
+
+static void
+ide_run_commands_init (IdeRunCommands *self)
+{
+  self->provider_to_model = g_hash_table_new_full (NULL, NULL, g_object_unref, g_object_unref);
+  self->models = g_list_store_new (G_TYPE_LIST_MODEL);
+  self->flatten_model = gtk_flatten_list_model_new (g_object_ref (G_LIST_MODEL (self->models)));
+
+  g_signal_connect_object (self->flatten_model,
+                           "items-changed",
+                           G_CALLBACK (ide_run_commands_items_changed_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+}
+
+/**
+ * ide_run_commands_dup_by_id:
+ * @self: a #IdeRunCommands
+ * @id: (nullable): the id of the run command
+ *
+ * Finds an #IdeRunCommand by it's id.
+ *
+ * %NULL is allowed for @id out of convenience, but will return %NULL.
+ *
+ * Returns: (transfer full) (nullable): an #IdeRunCommand or %NULL
+ */
+IdeRunCommand *
+ide_run_commands_dup_by_id (IdeRunCommands *self,
+                            const char     *id)
+{
+  guint n_items;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_RUN_COMMANDS (self), NULL);
+
+  if (id == NULL)
+    IDE_RETURN (NULL);
+
+  n_items = g_list_model_get_n_items (G_LIST_MODEL (self));
+
+  IDE_TRACE_MSG ("Locating command by id %s in list of %u commands", id, n_items);
+
+  for (guint i = 0; i < n_items; i++)
+    {
+      g_autoptr(IdeRunCommand) run_command = g_list_model_get_item (G_LIST_MODEL (self), i);
+      const char *run_command_id = ide_run_command_get_id (run_command);
+
+      if (ide_str_equal0 (run_command_id, id))
+        IDE_RETURN (g_steal_pointer (&run_command));
+    }
+
+  IDE_RETURN (NULL);
+}
+
+static gboolean
+filter_run_command_by_kind (gpointer item,
+                            gpointer user_data)
+{
+  return ide_run_command_get_kind (item) == GPOINTER_TO_INT (user_data);
+}
+
+/**
+ * ide_run_commands_list_by_kind:
+ * @self: an #IdeRunCommands
+ * @kind: an #IdeRunCommandKind
+ *
+ * Creates a new #GListModel of #IdeRunCommand filtered by @kind
+ *
+ * The model will update as new commands are added or removed from @self.
+ *
+ * Returns: (transfer full): a #GListModel
+ */
+GListModel *
+ide_run_commands_list_by_kind (IdeRunCommands    *self,
+                               IdeRunCommandKind  kind)
+{
+  GtkCustomFilter *filter = NULL;
+  GtkFilterListModel *model = NULL;
+
+  g_return_val_if_fail (IDE_IS_RUN_COMMANDS (self), NULL);
+
+  filter = gtk_custom_filter_new (filter_run_command_by_kind, GINT_TO_POINTER (kind), NULL);
+  model = gtk_filter_list_model_new (g_object_ref (G_LIST_MODEL (self)), GTK_FILTER (filter));
+
+  return G_LIST_MODEL (model);
+}
diff --git a/src/libide/foundry/ide-run-commands.h b/src/libide/foundry/ide-run-commands.h
new file mode 100644
index 000000000..77ad0533b
--- /dev/null
+++ b/src/libide/foundry/ide-run-commands.h
@@ -0,0 +1,47 @@
+/* ide-run-commands.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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-run-command.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_RUN_COMMANDS (ide_run_commands_get_type())
+
+IDE_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (IdeRunCommands, ide_run_commands, IDE, RUN_COMMANDS, IdeObject)
+
+IDE_AVAILABLE_IN_ALL
+IdeRunCommands *ide_run_commands_from_context (IdeContext        *context);
+IDE_AVAILABLE_IN_ALL
+GListModel     *ide_run_commands_list_by_kind (IdeRunCommands    *self,
+                                               IdeRunCommandKind  kind);
+IDE_AVAILABLE_IN_ALL
+IdeRunCommand  *ide_run_commands_dup_by_id    (IdeRunCommands    *self,
+                                               const char        *id);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-run-manager-private.h b/src/libide/foundry/ide-run-manager-private.h
index c841a8948..8b2e7c762 100644
--- a/src/libide/foundry/ide-run-manager-private.h
+++ b/src/libide/foundry/ide-run-manager-private.h
@@ -24,19 +24,6 @@
 
 G_BEGIN_DECLS
 
-typedef struct
-{
-  gchar          *id;
-  gchar          *title;
-  gchar          *icon_name;
-  gchar          *accel;
-  gint            priority;
-  IdeRunHandler   handler;
-  gpointer        handler_data;
-  GDestroyNotify  handler_data_destroy;
-} IdeRunHandlerInfo;
-
-const GList *_ide_run_manager_get_handlers (IdeRunManager *self);
-void         _ide_run_manager_drop_caches  (IdeRunManager *self);
+void _ide_run_manager_drop_caches (IdeRunManager *self);
 
 G_END_DECLS
diff --git a/src/libide/foundry/ide-run-manager.c b/src/libide/foundry/ide-run-manager.c
index c70b16170..a1a910705 100644
--- a/src/libide/foundry/ide-run-manager.c
+++ b/src/libide/foundry/ide-run-manager.c
@@ -23,24 +23,31 @@
 #include "config.h"
 
 #include <glib/gi18n.h>
-#include <libide-threading.h>
-#include <libide-vcs.h>
+#include <gtk/gtk.h>
+#include <unistd.h>
+
 #include <libpeas/peas.h>
 #include <libpeas/peas-autocleanups.h>
 
+#include <libide-core.h>
+#include <libide-plugins.h>
+#include <libide-threading.h>
+#include <libide-vcs.h>
+
 #include "ide-private.h"
 
 #include "ide-build-manager.h"
 #include "ide-build-system.h"
-#include "ide-build-target-provider.h"
-#include "ide-build-target.h"
-#include "ide-config-manager.h"
-#include "ide-config.h"
+#include "ide-deploy-strategy.h"
 #include "ide-device-manager.h"
 #include "ide-foundry-compat.h"
+#include "ide-no-tool-private.h"
+#include "ide-run-command.h"
+#include "ide-run-command-provider.h"
+#include "ide-run-context.h"
 #include "ide-run-manager-private.h"
 #include "ide-run-manager.h"
-#include "ide-runner.h"
+#include "ide-run-tool-private.h"
 #include "ide-runtime.h"
 
 struct _IdeRunManager
@@ -48,11 +55,13 @@ struct _IdeRunManager
   IdeObject                parent_instance;
 
   GCancellable            *cancellable;
-  IdeBuildTarget          *build_target;
   IdeNotification         *notif;
+  IdeExtensionSetAdapter  *run_command_providers;
+  IdeExtensionSetAdapter  *run_tools;
+  IdeRunTool              *run_tool;
 
-  const IdeRunHandlerInfo *handler;
-  GList                   *handlers;
+  IdeSubprocess           *current_subprocess;
+  IdeRunCommand           *current_run_command;
 
   /* Keep track of last change sequence from the file monitor
    * so that we can maybe skip past install phase and make
@@ -61,45 +70,59 @@ struct _IdeRunManager
   guint64                  last_change_seq;
   guint64                  pending_last_change_seq;
 
-  guint                    busy : 1;
+  char                    *default_run_command;
+
+  guint                    busy;
+
+  guint                    messages_debug_all : 1;
+  guint                    has_installed_once : 1;
+  guint                    sent_signal : 1;
 };
 
-typedef struct
-{
-  GList     *providers;
-  GPtrArray *results;
-  guint      active;
-} DiscoverState;
-
-static void initable_iface_init                      (GInitableIface *iface);
-static void ide_run_manager_actions_run              (IdeRunManager  *self,
-                                                      GVariant       *param);
-static void ide_run_manager_actions_run_with_handler (IdeRunManager  *self,
-                                                      GVariant       *param);
-static void ide_run_manager_actions_stop             (IdeRunManager  *self,
-                                                      GVariant       *param);
-
-DZL_DEFINE_ACTION_GROUP (IdeRunManager, ide_run_manager, {
+static void initable_iface_init                         (GInitableIface *iface);
+static void ide_run_manager_actions_run                 (IdeRunManager  *self,
+                                                         GVariant       *param);
+static void ide_run_manager_actions_run_with_handler    (IdeRunManager  *self,
+                                                         GVariant       *param);
+static void ide_run_manager_actions_stop                (IdeRunManager  *self,
+                                                         GVariant       *param);
+static void ide_run_manager_actions_messages_debug_all  (IdeRunManager  *self,
+                                                         GVariant       *param);
+static void ide_run_manager_actions_default_run_command (IdeRunManager  *self,
+                                                         GVariant       *param);
+static void ide_run_manager_actions_color_scheme        (IdeRunManager  *self,
+                                                         GVariant       *param);
+static void ide_run_manager_actions_high_contrast       (IdeRunManager  *self,
+                                                         GVariant       *param);
+static void ide_run_manager_actions_text_direction      (IdeRunManager  *self,
+                                                         GVariant       *param);
+
+IDE_DEFINE_ACTION_GROUP (IdeRunManager, ide_run_manager, {
   { "run", ide_run_manager_actions_run },
   { "run-with-handler", ide_run_manager_actions_run_with_handler, "s" },
   { "stop", ide_run_manager_actions_stop },
+  { "messages-debug-all", ide_run_manager_actions_messages_debug_all, NULL, "false" },
+  { "default-run-command", ide_run_manager_actions_default_run_command, "s", "''" },
+  { "color-scheme", ide_run_manager_actions_color_scheme, "s", "'follow'" },
+  { "high-contrast", ide_run_manager_actions_high_contrast, NULL, "false" },
+  { "text-direction", ide_run_manager_actions_text_direction, "s", "''" },
 })
 
 G_DEFINE_TYPE_EXTENDED (IdeRunManager, ide_run_manager, IDE_TYPE_OBJECT, G_TYPE_FLAG_FINAL,
                         G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init)
-                        G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP,
-                                               ide_run_manager_init_action_group))
+                        G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP, ide_run_manager_init_action_group))
 
 enum {
   PROP_0,
   PROP_BUSY,
-  PROP_HANDLER,
-  PROP_BUILD_TARGET,
+  PROP_ICON_NAME,
+  PROP_RUN_TOOL,
   N_PROPS
 };
 
 enum {
   RUN,
+  STARTED,
   STOPPED,
   N_SIGNALS
 };
@@ -107,63 +130,132 @@ enum {
 static GParamSpec *properties [N_PROPS];
 static guint signals [N_SIGNALS];
 
-static void
-discover_state_free (gpointer data)
+static IdeRunTool *
+ide_run_manager_get_run_tool (IdeRunManager *self)
+{
+  g_assert (IDE_IS_RUN_MANAGER (self));
+  g_assert (IDE_IS_RUN_TOOL (self->run_tool));
+
+  return self->run_tool;
+}
+
+void
+ide_run_manager_set_run_tool_from_plugin_info (IdeRunManager  *self,
+                                               PeasPluginInfo *plugin_info)
 {
-  DiscoverState *state = data;
+  g_autoptr(IdeRunTool) no_tool = NULL;
+  PeasExtension *exten = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_RUN_MANAGER (self));
 
-  g_assert (state->active == 0);
+  if (plugin_info != NULL)
+    exten = ide_extension_set_adapter_get_extension (self->run_tools, plugin_info);
 
-  g_list_free_full (state->providers, g_object_unref);
-  g_clear_pointer (&state->results, g_ptr_array_unref);
-  g_slice_free (DiscoverState, state);
+  if (exten == NULL)
+    {
+      if (IDE_IS_NO_TOOL (self->run_tool))
+        return;
+      no_tool = ide_no_tool_new ();
+      exten = (PeasExtension *)no_tool;
+    }
+
+  if (g_set_object (&self->run_tool, IDE_RUN_TOOL (exten)))
+    {
+      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_RUN_TOOL]);
+      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ICON_NAME]);
+    }
 }
 
 static void
-ide_run_manager_real_run (IdeRunManager *self,
-                          IdeRunner     *runner)
+ide_run_manager_set_run_tool_from_module_name (IdeRunManager *self,
+                                               const char    *name)
 {
+  PeasPluginInfo *plugin_info = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (IDE_IS_RUN_MANAGER (self));
-  g_assert (IDE_IS_RUNNER (runner));
 
-  /*
-   * If the current handler has a callback specified (our default "run" handler
-   * does not), then we need to allow that handler to prepare the runner.
-   */
-  if (self->handler != NULL && self->handler->handler != NULL)
-    self->handler->handler (self, runner, self->handler->handler_data);
+  g_debug ("Looking for run-tool from module %s", name);
+
+  if (!ide_str_empty0 (name))
+    plugin_info = peas_engine_get_plugin_info (peas_engine_get_default (), name);
+
+  ide_run_manager_set_run_tool_from_plugin_info (self, plugin_info);
 }
 
 static void
-ide_run_handler_info_free (gpointer data)
+ide_run_manager_actions_high_contrast (IdeRunManager *self,
+                                       GVariant      *param)
 {
-  IdeRunHandlerInfo *info = data;
+  GVariant *state;
+
+  g_assert (IDE_IS_RUN_MANAGER (self));
 
-  g_free (info->id);
-  g_free (info->title);
-  g_free (info->icon_name);
-  g_free (info->accel);
+  state = ide_run_manager_get_action_state (self, "high-contrast");
+  ide_run_manager_set_action_state (self,
+                                    "high-contrast",
+                                    g_variant_new_boolean (!g_variant_get_boolean (state)));
+}
 
-  if (info->handler_data_destroy)
-    info->handler_data_destroy (info->handler_data);
+static void
+ide_run_manager_actions_text_direction (IdeRunManager *self,
+                                        GVariant      *param)
+{
+  const char *str;
 
-  g_slice_free (IdeRunHandlerInfo, info);
+  g_assert (IDE_IS_RUN_MANAGER (self));
+  g_assert (param != NULL);
+  g_assert (g_variant_is_of_type (param, G_VARIANT_TYPE_STRING));
+
+  str = g_variant_get_string (param, NULL);
+  if (g_strv_contains (IDE_STRV_INIT ("ltr", "rtl"), str))
+    ide_run_manager_set_action_state (self,
+                                      "text-direction",
+                                      g_variant_new_string (str));
 }
 
 static void
-ide_run_manager_dispose (GObject *object)
+ide_run_manager_actions_color_scheme (IdeRunManager *self,
+                                      GVariant      *param)
 {
-  IdeRunManager *self = (IdeRunManager *)object;
+  const char *str;
 
-  self->handler = NULL;
+  g_assert (IDE_IS_RUN_MANAGER (self));
+  g_assert (param != NULL);
+  g_assert (g_variant_is_of_type (param, G_VARIANT_TYPE_STRING));
 
-  g_clear_object (&self->cancellable);
-  ide_clear_and_destroy_object (&self->build_target);
+  str = g_variant_get_string (param, NULL);
+  if (!g_strv_contains (IDE_STRV_INIT ("follow", "force-light", "force-dark"), str))
+    str = "follow";
 
-  g_list_free_full (self->handlers, ide_run_handler_info_free);
-  self->handlers = NULL;
+  ide_run_manager_set_action_state (self,
+                                    "color-scheme",
+                                    g_variant_new_string (str));
+}
 
-  G_OBJECT_CLASS (ide_run_manager_parent_class)->dispose (object);
+static void
+ide_run_manager_actions_default_run_command (IdeRunManager *self,
+                                             GVariant      *param)
+{
+  const char *str;
+
+  g_assert (IDE_IS_RUN_MANAGER (self));
+  g_assert (param != NULL);
+  g_assert (g_variant_is_of_type (param, G_VARIANT_TYPE_STRING));
+
+  str = g_variant_get_string (param, NULL);
+  if (ide_str_empty0 (str))
+    str = NULL;
+
+  if (g_strcmp0 (str, self->default_run_command) != 0)
+    {
+      g_free (self->default_run_command);
+      self->default_run_command = g_strdup (str);
+      ide_run_manager_set_action_state (self,
+                                        "default-run-command",
+                                        g_variant_new_string (str ? str : ""));
+    }
 }
 
 static void
@@ -180,10 +272,65 @@ ide_run_manager_update_action_enabled (IdeRunManager *self)
   can_build = ide_build_manager_get_can_build (build_manager);
 
   ide_run_manager_set_action_enabled (self, "run",
-                                      self->busy == FALSE && can_build == TRUE);
+                                      self->busy == 0 && can_build == TRUE);
   ide_run_manager_set_action_enabled (self, "run-with-handler",
-                                      self->busy == FALSE && can_build == TRUE);
-  ide_run_manager_set_action_enabled (self, "stop", self->busy == TRUE);
+                                      self->busy == 0 && can_build == TRUE);
+  ide_run_manager_set_action_enabled (self, "stop", self->busy > 0);
+}
+
+
+static void
+ide_run_manager_mark_busy (IdeRunManager *self)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_RUN_MANAGER (self));
+
+  self->busy++;
+
+  if (self->busy == 1)
+    {
+      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_BUSY]);
+      ide_run_manager_update_action_enabled (self);
+    }
+
+  IDE_EXIT;
+}
+
+static void
+ide_run_manager_unmark_busy (IdeRunManager *self)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_RUN_MANAGER (self));
+
+  self->busy--;
+
+  if (self->busy == 0)
+    {
+      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_BUSY]);
+      ide_run_manager_update_action_enabled (self);
+    }
+
+  IDE_EXIT;
+}
+
+static void
+ide_run_manager_dispose (GObject *object)
+{
+  IdeRunManager *self = (IdeRunManager *)object;
+
+  g_clear_pointer (&self->default_run_command, g_free);
+
+  g_clear_object (&self->cancellable);
+  g_clear_object (&self->current_run_command);
+  g_clear_object (&self->current_subprocess);
+  g_clear_object (&self->run_tool);
+
+  ide_clear_and_destroy_object (&self->run_command_providers);
+  ide_clear_and_destroy_object (&self->run_tools);
+
+  G_OBJECT_CLASS (ide_run_manager_parent_class)->dispose (object);
 }
 
 static void
@@ -202,6 +349,17 @@ ide_run_manager_notify_can_build (IdeRunManager   *self,
   IDE_EXIT;
 }
 
+const char *
+ide_run_manager_get_icon_name (IdeRunManager *self)
+{
+  g_assert (IDE_IS_RUN_MANAGER (self));
+
+  if (self->run_tool == NULL)
+    return NULL;
+
+  return ide_run_tool_get_icon_name (self->run_tool);
+}
+
 static gboolean
 initable_init (GInitable     *initable,
                GCancellable  *cancellable,
@@ -227,6 +385,16 @@ initable_init (GInitable     *initable,
 
   ide_run_manager_update_action_enabled (self);
 
+  self->run_command_providers = ide_extension_set_adapter_new (IDE_OBJECT (self),
+                                                               peas_engine_get_default (),
+                                                               IDE_TYPE_RUN_COMMAND_PROVIDER,
+                                                               NULL, NULL);
+
+  self->run_tools = ide_extension_set_adapter_new (IDE_OBJECT (self),
+                                                   peas_engine_get_default (),
+                                                   IDE_TYPE_RUN_TOOL,
+                                                   NULL, NULL);
+
   IDE_RETURN (TRUE);
 }
 
@@ -250,12 +418,12 @@ ide_run_manager_get_property (GObject    *object,
       g_value_set_boolean (value, ide_run_manager_get_busy (self));
       break;
 
-    case PROP_HANDLER:
-      g_value_set_string (value, ide_run_manager_get_handler (self));
+    case PROP_ICON_NAME:
+      g_value_set_string (value, ide_run_manager_get_icon_name (self));
       break;
 
-    case PROP_BUILD_TARGET:
-      g_value_set_object (value, ide_run_manager_get_build_target (self));
+    case PROP_RUN_TOOL:
+      g_value_set_object (value, ide_run_manager_get_run_tool (self));
       break;
 
     default:
@@ -263,25 +431,6 @@ ide_run_manager_get_property (GObject    *object,
     }
 }
 
-static void
-ide_run_manager_set_property (GObject      *object,
-                              guint         prop_id,
-                              const GValue *value,
-                              GParamSpec   *pspec)
-{
-  IdeRunManager *self = IDE_RUN_MANAGER (object);
-
-  switch (prop_id)
-    {
-    case PROP_BUILD_TARGET:
-      ide_run_manager_set_build_target (self, g_value_get_object (value));
-      break;
-
-    default:
-      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
-    }
-}
-
 static void
 ide_run_manager_class_init (IdeRunManagerClass *klass)
 {
@@ -289,78 +438,76 @@ ide_run_manager_class_init (IdeRunManagerClass *klass)
 
   object_class->dispose = ide_run_manager_dispose;
   object_class->get_property = ide_run_manager_get_property;
-  object_class->set_property = ide_run_manager_set_property;
 
   properties [PROP_BUSY] =
-    g_param_spec_boolean ("busy",
-                          "Busy",
-                          "Busy",
+    g_param_spec_boolean ("busy", NULL, NULL,
                           FALSE,
                           (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
 
-  properties [PROP_HANDLER] =
-    g_param_spec_string ("handler",
-                         "Handler",
-                         "Handler",
-                         "run",
+  properties [PROP_ICON_NAME] =
+    g_param_spec_string ("icon-name", NULL, NULL,
+                         NULL,
                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
 
-  properties [PROP_BUILD_TARGET] =
-    g_param_spec_object ("build-target",
-                         "Build Target",
-                         "The IdeBuildTarget that will be run",
-                         IDE_TYPE_BUILD_TARGET,
-                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+  properties [PROP_RUN_TOOL] =
+    g_param_spec_object ("run-tool", NULL, NULL,
+                         IDE_TYPE_RUN_TOOL,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
 
   g_object_class_install_properties (object_class, N_PROPS, properties);
 
   /**
    * IdeRunManager::run:
    * @self: An #IdeRunManager
-   * @runner: An #IdeRunner
+   * @run_context: An #IdeRunContext
    *
-   * This signal is emitted right before ide_runner_run_async() is called
-   * on an #IdeRunner. It can be used by plugins to tweak things right
-   * before the runner is executed.
+   * This signal is emitted to allow plugins to add additional settings to a
+   * run context before a launcher is created.
    *
-   * The current run handler (debugger, profiler, etc) is run as the default
-   * handler for this function. So connect with %G_SIGNAL_AFTER if you want
-   * to be nofied after the run handler has executed. It's unwise to change
-   * things that the run handler might expect. Generally if you want to
-   * change settings, do that before the run handler has exected.
-   *
-   * Since: 3.32
+   * Generally this can only be used in certain situations and you probably
+   * want to modify the run context in another way such as a deploy strategry,
+   * runtime, or similar.
    */
   signals [RUN] =
     g_signal_new_class_handler ("run",
                                 G_TYPE_FROM_CLASS (klass),
                                 G_SIGNAL_RUN_LAST,
-                                G_CALLBACK (ide_run_manager_real_run),
+                                NULL,
                                 NULL,
                                 NULL,
                                 NULL,
                                 G_TYPE_NONE,
                                 1,
-                                IDE_TYPE_RUNNER);
+                                IDE_TYPE_RUN_CONTEXT);
 
   /**
-   * IdeRunManager::stopped:
+   * IdeRunManager::started:
    *
-   * This signal is emitted when the run manager has stopped the currently
-   * executing inferior.
+   * This signal is emitted when the run manager has spawned a new subprocess.
+   */
+  signals [STARTED] =
+    g_signal_new ("started",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL,
+                  NULL,
+                  G_TYPE_NONE, 0);
+
+  /**
+   * IdeRunManager::stopped:
    *
-   * Since: 3.32
+   * This signal is emitted when the run manager has detected the running
+   * subprocess has exited.
    */
   signals [STOPPED] =
     g_signal_new ("stopped",
                   G_TYPE_FROM_CLASS (klass),
                   G_SIGNAL_RUN_LAST,
                   0,
+                  NULL, NULL,
                   NULL,
-                  NULL,
-                  NULL,
-                  G_TYPE_NONE,
-                  0);
+                  G_TYPE_NONE, 0);
 }
 
 gboolean
@@ -368,7 +515,7 @@ ide_run_manager_get_busy (IdeRunManager *self)
 {
   g_return_val_if_fail (IDE_IS_RUN_MANAGER (self), FALSE);
 
-  return self->busy;
+  return self->busy > 0;
 }
 
 static gboolean
@@ -392,306 +539,114 @@ ide_run_manager_check_busy (IdeRunManager  *self,
 }
 
 static void
-ide_run_manager_run_cb (GObject      *object,
-                        GAsyncResult *result,
-                        gpointer      user_data)
+apply_messages_debug (IdeRunContext *run_context,
+                      gboolean       messages_debug_all)
 {
-  IdeRunner *runner = (IdeRunner *)object;
-  g_autoptr(IdeTask) task = user_data;
-  g_autoptr(GError) error = NULL;
-  IdeRunManager *self;
-
   IDE_ENTRY;
 
-  g_assert (IDE_IS_RUNNER (runner));
-  g_assert (IDE_IS_TASK (task));
-
-  self = ide_task_get_source_object (task);
-
-  if (self->notif != NULL)
-    {
-      ide_notification_withdraw (self->notif);
-      g_clear_object (&self->notif);
-    }
-
-  if (!ide_runner_run_finish (runner, result, &error))
-    ide_task_return_error (task, g_steal_pointer (&error));
-  else
-    ide_task_return_boolean (task, TRUE);
-
-  g_signal_emit (self, signals [STOPPED], 0);
+  g_assert (IDE_IS_RUN_CONTEXT (run_context));
 
-  ide_object_destroy (IDE_OBJECT (runner));
+  if (messages_debug_all)
+    ide_run_context_setenv (run_context, "G_MESSAGES_DEBUG", "all");
 
   IDE_EXIT;
 }
 
 static void
-copy_builtin_envvars (IdeEnvironment *environment)
-{
-  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",
-    "XDG_MENU_PREFIX",
-#if 0
-    /* Can't copy these as they could mess up Flatpak */
-    "XDG_DATA_DIRS",
-    "XDG_RUNTIME_DIR",
-#endif
-    "XDG_SEAT",
-    "XDG_SESSION_DESKTOP",
-    "XDG_SESSION_ID",
-    "XDG_SESSION_TYPE",
-    "XDG_VTNR",
-  };
-  const gchar * const *host_environ = _ide_host_environ ();
-
-  for (guint i = 0; i < G_N_ELEMENTS (copy_env); i++)
-    {
-      const gchar *key = copy_env[i];
-      const gchar *val = g_environ_getenv ((gchar **)host_environ, key);
-
-      if (val != NULL && ide_environment_getenv (environment, key) == NULL)
-        ide_environment_setenv (environment, key, val);
-    }
-}
-
-static void
-create_runner_cb (GObject      *object,
-                  GAsyncResult *result,
-                  gpointer      user_data)
+apply_color_scheme (IdeRunContext *run_context,
+                    const char    *color_scheme)
 {
-  IdeRunManager *self;
-  IdeDeviceManager *device_manager = (IDE_DEVICE_MANAGER (object));
-  g_autoptr(IdeTask) task = user_data;
-  g_autoptr(GError) error = NULL;
-  g_autofree gchar *name = NULL;
-  g_autofree gchar *title = NULL;
-  IdeBuildTarget *build_target;
-  IdeContext *context;
-  IdeConfigManager *config_manager;
-  IdeConfig *config;
-  IdeEnvironment *environment;
-  IdeRuntime *runtime;
-  g_autoptr(IdeRunner) runner = NULL;
-  GCancellable *cancellable;
-  const gchar *run_opts;
-
   IDE_ENTRY;
 
-  g_assert (IDE_IS_DEVICE_MANAGER (device_manager));
-  g_assert (G_IS_ASYNC_RESULT (result));
-  g_assert (IDE_IS_TASK (task));
-
-  self = ide_task_get_source_object (task);
-  g_assert (IDE_IS_RUN_MANAGER (self));
-
-  runner = ide_device_manager_create_runner_finish (device_manager, result, &error);
+  g_assert (IDE_IS_RUN_CONTEXT (run_context));
+  g_assert (color_scheme != NULL);
 
-  if (error != NULL)
-    {
-      ide_task_return_error (task, g_steal_pointer (&error));
-      IDE_EXIT;
-    }
-
-  build_target = ide_task_get_task_data (task);
-  context = ide_object_get_context (IDE_OBJECT (self));
-
-  g_assert (IDE_IS_BUILD_TARGET (build_target));
-  g_assert (IDE_IS_CONTEXT (context));
+  g_debug ("Applying color-scheme \"%s\"", color_scheme);
 
-  config_manager = ide_config_manager_from_context (context);
-  config = ide_config_manager_get_current (config_manager);
-  runtime = ide_config_get_runtime (config);
-
-  if (runner == NULL)
+  if (ide_str_equal0 (color_scheme, "follow"))
     {
-      if (runtime == NULL)
-        {
-          ide_task_return_new_error (task,
-                                     IDE_RUNTIME_ERROR,
-                                     IDE_RUNTIME_ERROR_NO_SUCH_RUNTIME,
-                                     "%s “%s”",
-                                     _("Failed to locate runtime"),
-                                     ide_config_get_runtime_id (config));
-          IDE_EXIT;
-        }
-
-      runner = ide_runtime_create_runner (runtime, build_target);
+      ide_run_context_unsetenv (run_context, "ADW_DEBUG_COLOR_SCHEME");
+      ide_run_context_unsetenv (run_context, "HDY_DEBUG_COLOR_SCHEME");
     }
-
-  cancellable = ide_task_get_cancellable (task);
-
-  g_assert (IDE_IS_RUNNER (runner));
-  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
-
-  /* Add our run arguments if specified in the config. */
-  if (NULL != (run_opts = ide_config_get_run_opts (config)))
+  else if (ide_str_equal0 (color_scheme, "force-light"))
     {
-      g_auto(GStrv) argv = NULL;
-      gint argc;
-
-      if (g_shell_parse_argv (run_opts, &argc, &argv, NULL))
-        {
-          for (gint i = 0; i < argc; i++)
-            ide_runner_append_argv (runner, argv[i]);
-        }
+      ide_run_context_setenv (run_context, "ADW_DEBUG_COLOR_SCHEME", "prefer-light");
+      ide_run_context_setenv (run_context, "HDY_DEBUG_COLOR_SCHEME", "prefer-light");
     }
-
-  /* Add our runtime environment variables. */
-  environment = ide_runner_get_environment (runner);
-  copy_builtin_envvars (environment);
-  ide_environment_copy_into (ide_config_get_runtime_environment (config), environment, TRUE);
-
-  g_signal_emit (self, signals [RUN], 0, runner);
-
-  if (ide_runner_get_failed (runner))
+  else if (ide_str_equal0 (color_scheme, "force-dark"))
     {
-      ide_task_return_new_error (task,
-                                 IDE_RUNTIME_ERROR,
-                                 IDE_RUNTIME_ERROR_SPAWN_FAILED,
-                                 "Failed to execute the application");
-      IDE_EXIT;
+      ide_run_context_setenv (run_context, "ADW_DEBUG_COLOR_SCHEME", "prefer-dark");
+      ide_run_context_setenv (run_context, "HDY_DEBUG_COLOR_SCHEME", "prefer-dark");
     }
-
-  if (self->notif != NULL)
+  else
     {
-      ide_notification_withdraw (self->notif);
-      g_clear_object (&self->notif);
+      g_warn_if_reached ();
     }
 
-  self->notif = ide_notification_new ();
-  name = ide_build_target_get_name (build_target);
-  /* translators: %s is replaced with the name of the users executable */
-  title = g_strdup_printf (_("Running %s…"), name);
-  ide_notification_set_title (self->notif, title);
-  ide_notification_attach (self->notif, IDE_OBJECT (self));
-
-  ide_runner_run_async (runner,
-                        cancellable,
-                        ide_run_manager_run_cb,
-                        g_object_ref (task));
-
   IDE_EXIT;
 }
 
 static void
-deploy_cb (GObject      *object,
-           GAsyncResult *result,
-           gpointer      user_data)
+apply_high_contrast (IdeRunContext *run_context,
+                     gboolean       high_contrast)
 {
-  IdeDeviceManager *device_manager = (IdeDeviceManager *)object;
-  IdeBuildManager *build_manager;
-  IdeContext *context;
-  IdePipeline *pipeline;
-  IdeRunManager *self;
-  g_autoptr(IdeTask) task = user_data;
-  g_autoptr(GError) error = NULL;
-
   IDE_ENTRY;
 
-  g_assert (IDE_IS_DEVICE_MANAGER (device_manager));
-  g_assert (IDE_IS_TASK (task));
+  g_assert (IDE_IS_RUN_CONTEXT (run_context));
+
+  g_debug ("Applying high-contrast %d", high_contrast);
 
-  if (!ide_device_manager_deploy_finish (device_manager, result, &error))
+  if (high_contrast)
     {
-      ide_task_return_error (task, g_steal_pointer (&error));
-      IDE_EXIT;
+      ide_run_context_setenv (run_context, "ADW_DEBUG_HIGH_CONTRAST", "1");
+      ide_run_context_setenv (run_context, "HDY_DEBUG_HIGH_CONTRAST", "1");
+    }
+  else
+    {
+      ide_run_context_unsetenv (run_context, "ADW_DEBUG_HIGH_CONTRAST");
+      ide_run_context_unsetenv (run_context, "HDY_DEBUG_HIGH_CONTRAST");
     }
-
-  self = ide_task_get_source_object (task);
-  g_assert (IDE_IS_RUN_MANAGER (self));
-
-  context = ide_object_get_context (IDE_OBJECT (self));
-  g_assert (IDE_IS_CONTEXT (context));
-
-  build_manager = ide_build_manager_from_context (context);
-  pipeline = ide_build_manager_get_pipeline (build_manager);
-
-  ide_device_manager_create_runner_async (device_manager,
-                                          pipeline,
-                                          ide_task_get_cancellable (task),
-                                          create_runner_cb,
-                                          g_object_ref (task));
 
   IDE_EXIT;
 }
 
 static void
-do_run_async (IdeRunManager *self,
-              IdeTask       *task)
+apply_text_direction (IdeRunContext *run_context,
+                      const char    *text_dir_str)
 {
-  IdeBuildManager *build_manager;
-  IdeContext *context;
-  IdeDeviceManager *device_manager;
-  IdePipeline *pipeline;
-  GCancellable *cancellable;
+  GtkTextDirection dir;
 
   IDE_ENTRY;
 
-  g_assert (IDE_IS_RUN_MANAGER (self));
-  g_assert (IDE_IS_TASK (task));
-
-  context = ide_object_get_context (IDE_OBJECT (self));
-  g_assert (IDE_IS_CONTEXT (context));
-
-  build_manager = ide_build_manager_from_context (context);
-  pipeline = ide_build_manager_get_pipeline (build_manager);
-  device_manager = ide_device_manager_from_context (context);
+  g_assert (IDE_IS_RUN_CONTEXT (run_context));
 
-  cancellable = ide_task_get_cancellable (task);
-  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+  if (ide_str_equal0 (text_dir_str, "rtl"))
+    dir = GTK_TEXT_DIR_RTL;
+  else if (ide_str_equal0 (text_dir_str, "ltr"))
+    dir = GTK_TEXT_DIR_LTR;
+  else
+    g_return_if_reached ();
 
-  ide_device_manager_deploy_async (device_manager,
-                                   pipeline,
-                                   cancellable,
-                                   deploy_cb,
-                                   g_object_ref (task));
+  if (dir != gtk_widget_get_default_direction ())
+    ide_run_context_setenv (run_context, "GTK_DEBUG", "invert-text-dir");
 
   IDE_EXIT;
 }
 
-static void
-ide_run_manager_run_discover_cb (GObject      *object,
-                                 GAsyncResult *result,
-                                 gpointer      user_data)
+static inline const char *
+get_action_state_string (IdeRunManager *self,
+                         const char    *action_name)
 {
-  IdeRunManager *self = (IdeRunManager *)object;
-  g_autoptr(IdeBuildTarget) build_target = NULL;
-  g_autoptr(IdeTask) task = user_data;
-  g_autoptr(GError) error = NULL;
-
-  IDE_ENTRY;
-
-  g_assert (IDE_IS_RUN_MANAGER (self));
-  g_assert (G_IS_ASYNC_RESULT (result));
-
-  build_target = ide_run_manager_discover_default_target_finish (self, result, &error);
-
-  if (build_target == NULL)
-    {
-      ide_task_return_error (task, g_steal_pointer (&error));
-      IDE_EXIT;
-    }
-
-  ide_run_manager_set_build_target (self, build_target);
-
-  ide_task_set_task_data (task, g_steal_pointer (&build_target), g_object_unref);
-
-  do_run_async (self, task);
+  GVariant *state = ide_run_manager_get_action_state (self, action_name);
+  return g_variant_get_string (state, NULL);
+}
 
-  IDE_EXIT;
+static inline gboolean
+get_action_state_bool (IdeRunManager *self,
+                       const char    *action_name)
+{
+  GVariant *state = ide_run_manager_get_action_state (self, action_name);
+  return g_variant_get_boolean (state);
 }
 
 static void
@@ -702,144 +657,402 @@ ide_run_manager_install_cb (GObject      *object,
   IdeBuildManager *build_manager = (IdeBuildManager *)object;
   g_autoptr(IdeTask) task = user_data;
   g_autoptr(GError) error = NULL;
-  IdeRunManager *self;
-  IdeBuildTarget *build_target;
-  GCancellable *cancellable;
 
   IDE_ENTRY;
 
   g_assert (IDE_IS_BUILD_MANAGER (build_manager));
+  g_assert (G_IS_ASYNC_RESULT (result));
   g_assert (IDE_IS_TASK (task));
 
-  self = ide_task_get_source_object (task);
+  if (!ide_build_manager_build_finish (build_manager, result, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_boolean (task, TRUE);
+
+  IDE_EXIT;
+}
+
+static void
+ide_run_manager_install_async (IdeRunManager       *self,
+                               GCancellable        *cancellable,
+                               GAsyncReadyCallback  callback,
+                               gpointer             user_data)
+{
+  g_autoptr(IdeContext) context = NULL;
+  g_autoptr(GSettings) project_settings = NULL;
+  g_autoptr(IdeTask) task = NULL;
+  IdeBuildManager *build_manager;
+  IdeVcsMonitor *monitor;
+  guint64 sequence = 0;
+
+  IDE_ENTRY;
+
   g_assert (IDE_IS_RUN_MANAGER (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
 
-  if (!ide_build_manager_build_finish (build_manager, result, &error))
+  context = ide_object_ref_context (IDE_OBJECT (self));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_run_manager_install_async);
+
+  project_settings = ide_context_ref_project_settings (context);
+  if (!g_settings_get_boolean (project_settings, "install-before-run"))
     {
-      /* We want to let the consumer know there was a build error
-       * (but don't need to pass the specific error code) so that
-       * they have an error code to check against.
-       */
-      ide_task_return_new_error (task,
-                                 IDE_RUNTIME_ERROR,
-                                 IDE_RUNTIME_ERROR_BUILD_FAILED,
-                                 /* translators: %s is replaced with the specific error reason */
-                                 _("The build target failed to build: %s"),
-                                 error->message);
+      ide_task_return_boolean (task, TRUE);
       IDE_EXIT;
     }
 
-  self->last_change_seq = self->pending_last_change_seq;
-
-  build_target = ide_run_manager_get_build_target (self);
+  monitor = ide_vcs_monitor_from_context (context);
+  if (monitor != NULL)
+    sequence = ide_vcs_monitor_get_sequence (monitor);
 
-  if (build_target == NULL)
+  if (self->has_installed_once && sequence == self->last_change_seq)
     {
-      cancellable = ide_task_get_cancellable (task);
-      g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
-
-      ide_run_manager_discover_default_target_async (self,
-                                                     cancellable,
-                                                     ide_run_manager_run_discover_cb,
-                                                     g_steal_pointer (&task));
+      ide_task_return_boolean (task, TRUE);
       IDE_EXIT;
     }
 
-  ide_task_set_task_data (task, g_object_ref (build_target), g_object_unref);
+  self->pending_last_change_seq = sequence;
 
-  do_run_async (self, task);
+  build_manager = ide_build_manager_from_context (context);
+  ide_build_manager_build_async (build_manager,
+                                 IDE_PIPELINE_PHASE_INSTALL,
+                                 NULL,
+                                 cancellable,
+                                 ide_run_manager_install_cb,
+                                 g_steal_pointer (&task));
 
   IDE_EXIT;
 }
 
-static void
-ide_run_manager_task_completed (IdeRunManager *self,
-                                GParamSpec    *pspec,
-                                IdeTask       *task)
+static gboolean
+ide_run_manager_install_finish (IdeRunManager  *self,
+                                GAsyncResult   *result,
+                                GError        **error)
 {
+  gboolean ret;
+
   IDE_ENTRY;
 
   g_assert (IDE_IS_RUN_MANAGER (self));
-  g_assert (pspec != NULL);
+  g_assert (IDE_IS_TASK (result));
+
+  ret = ide_task_propagate_boolean (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+static void
+ide_run_manager_run_subprocess_wait_check_cb (GObject      *object,
+                                              GAsyncResult *result,
+                                              gpointer      user_data)
+{
+  IdeSubprocess *subprocess = (IdeSubprocess *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeRunManager *self;
+
+  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));
 
-  self->busy = FALSE;
-  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUSY]);
+  self = ide_task_get_source_object (task);
+  g_assert (IDE_IS_RUN_MANAGER (self));
 
-  ide_run_manager_update_action_enabled (self);
+  if (self->notif != NULL)
+    ide_notification_withdraw (self->notif);
+
+  g_clear_object (&self->notif);
+  g_clear_object (&self->current_subprocess);
+
+  if (!ide_subprocess_wait_check_finish (subprocess, result, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_boolean (task, TRUE);
+
+  if (self->run_tool != NULL)
+    _ide_run_tool_emit_stopped (self->run_tool);
+
+  g_signal_emit (self, signals[STOPPED], 0);
 
   IDE_EXIT;
 }
 
 static void
-ide_run_manager_do_install_before_run (IdeRunManager *self,
-                                       IdeTask       *task)
+ide_run_manager_prepare_run_context (IdeRunManager *self,
+                                     IdeRunContext *run_context,
+                                     IdeRunCommand *run_command,
+                                     IdePipeline   *pipeline)
 {
-  g_autoptr(IdeContext) context = NULL;
-  IdeBuildManager *build_manager;
-  IdeVcsMonitor *monitor;
-  guint64 sequence = 0;
+  g_auto(GStrv) environ = NULL;
 
   IDE_ENTRY;
 
-  g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (IDE_IS_RUN_MANAGER (self));
-  g_assert (IDE_IS_TASK (task));
+  g_assert (IDE_IS_RUN_CONTEXT (run_context));
+  g_assert (IDE_IS_RUN_COMMAND (run_command));
+  g_assert (IDE_IS_PIPELINE (pipeline));
+  g_assert (IDE_IS_RUN_TOOL (self->run_tool));
 
-  context = ide_object_ref_context (IDE_OBJECT (self));
-  build_manager = ide_build_manager_from_context (context);
-  monitor = ide_vcs_monitor_from_context (context);
+  g_debug ("Preparing run context using run tool %s",
+           G_OBJECT_TYPE_NAME (self->run_tool));
 
-  /*
-   * First we need to make sure the target is up to date and installed
-   * so that all the dependent resources are available.
+  /* The very first thing we need to do is allow the current run tool
+   * to inject any command wrapper it needs. This might be something like
+   * gdb, or valgrind, etc.
    */
+  ide_run_tool_prepare_to_run (self->run_tool, pipeline, run_command, run_context);
 
-  self->busy = TRUE;
-  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUSY]);
+  /* Now push a new layer so that we can keep those values separate from
+   * what is configured in the run command. We use an expansion layer so
+   * that we can expand common variables at this layer and not allow them
+   * to be visible at lower layers.
+   */
+  environ = g_environ_setenv (environ, "BUILDDIR", ide_pipeline_get_builddir (pipeline), TRUE);
+  environ = g_environ_setenv (environ, "SRCDIR", ide_pipeline_get_srcdir (pipeline), TRUE);
+  environ = g_environ_setenv (environ, "HOME", g_get_home_dir (), TRUE);
+  environ = g_environ_setenv (environ, "USER", g_get_user_name (), TRUE);
+  ide_run_context_push_expansion (run_context, (const char * const *)environ);
 
-  g_signal_connect_object (task,
-                           "notify::completed",
-                           G_CALLBACK (ide_run_manager_task_completed),
-                           self,
-                           G_CONNECT_SWAPPED);
+  /* Setup working directory */
+  {
+    const char *cwd = ide_run_command_get_cwd (run_command);
 
-  if (monitor != NULL)
-    sequence = ide_vcs_monitor_get_sequence (monitor);
+    if (cwd != NULL)
+      ide_run_context_set_cwd (run_context, cwd);
+  }
 
-  /*
-   * If we detect that nothing has changed in the project directory since the
-   * last time we ran, we can avoid installing the project. This will not help
-   * in situations where external resources have changed outside of builders
-   * control, but users can simply force a Build in that case.
+  /* Setup command arguments */
+  {
+    const char * const *argv = ide_run_command_get_argv (run_command);
+
+    if (argv != NULL)
+      ide_run_context_append_args (run_context, argv);
+  }
+
+  /* Setup command environment */
+  {
+    const char * const *env = ide_run_command_get_environ (run_command);
+
+    if (env != NULL && env[0] != NULL)
+      ide_run_context_add_environ (run_context, env);
+  }
+
+  /* Now overlay runtime-tweaks as needed. Put this in a layer so that
+   * we can debug where things are set/changed to help us when we need
+   * to track down bugs in handlers/runtimes/devices/etc. All of our
+   * changes will get persisted to the lower layer when merging anyway.
+   *
+   * TODO: These could probably be moved into a plugin rather than in
+   * the foundry itself. That way they can be disabled by users who are
+   * doing nothing with GTK/GNOME applications.
    */
-  if (self->build_target != NULL && sequence == self->last_change_seq)
+  ide_run_context_push (run_context, NULL, NULL, NULL);
+  apply_color_scheme (run_context, get_action_state_string (self, "color-scheme"));
+  apply_high_contrast (run_context, get_action_state_bool (self, "high-contrast"));
+  apply_text_direction (run_context, get_action_state_string (self, "text-direction"));
+  apply_messages_debug (run_context, self->messages_debug_all);
+
+  /* Allow plugins to track anything in the mix. For example the
+   * terminal plugin will attach a PTY here for stdin/stdout/stderr.
+   */
+  g_signal_emit (self, signals [RUN], 0, run_context);
+
+  IDE_EXIT;
+}
+
+static void
+ide_run_manager_run_deploy_cb (GObject      *object,
+                               GAsyncResult *result,
+                               gpointer      user_data)
+{
+  IdeDeployStrategy *deploy_strategy = (IdeDeployStrategy *)object;
+  g_autoptr(IdeSubprocess) subprocess = NULL;
+  g_autoptr(IdeRunContext) run_context = NULL;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeNotification *notif;
+  IdeRunManager *self;
+  IdePipeline *pipeline;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_DEPLOY_STRATEGY (deploy_strategy));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  self = ide_task_get_source_object (task);
+  pipeline = ide_task_get_task_data (task);
+  notif = g_object_get_data (G_OBJECT (deploy_strategy), "PROGRESS");
+
+  g_assert (IDE_IS_RUN_MANAGER (self));
+  g_assert (IDE_IS_PIPELINE (pipeline));
+  g_assert (IDE_IS_NOTIFICATION (notif));
+
+  /* Withdraw our deploy notification */
+  ide_notification_withdraw (notif);
+  ide_object_destroy (IDE_OBJECT (notif));
+
+  if (!ide_deploy_strategy_deploy_finish (deploy_strategy, result, &error))
     {
-      g_debug ("Skipping install phase as no files appear to have changed "
-               "(sequence %"G_GUINT64_FORMAT")", sequence);
-      ide_run_manager_update_action_enabled (self);
-      ide_task_set_task_data (task, g_object_ref (self->build_target), g_object_unref);
-      do_run_async (self, task);
+      ide_task_return_error (task, g_steal_pointer (&error));
       IDE_EXIT;
     }
 
-  self->pending_last_change_seq = sequence;
+  if (self->current_run_command == NULL)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_CANCELLED,
+                                 "The operation was cancelled");
+      IDE_EXIT;
+    }
 
-  ide_build_manager_build_async (build_manager,
-                                 IDE_PIPELINE_PHASE_INSTALL,
-                                 NULL,
-                                 ide_task_get_cancellable (task),
-                                 ide_run_manager_install_cb,
-                                 g_object_ref (task));
+  /* Setup the run context */
+  run_context = ide_run_context_new ();
+  ide_deploy_strategy_prepare_run_context (deploy_strategy, pipeline, run_context);
+  ide_run_manager_prepare_run_context (self, run_context, self->current_run_command, pipeline);
 
-  ide_run_manager_update_action_enabled (self);
+  /* Now spawn the subprocess or bail if there was a failure to build command */
+  if (!(subprocess = ide_run_context_spawn (run_context, &error)))
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  /* Keep subprocess around for send_signal/force_exit */
+  g_set_object (&self->current_subprocess, subprocess);
+
+  if (self->notif != NULL)
+    ide_notification_withdraw (self->notif);
+
+  /* Setup notification */
+  {
+    const char *name = ide_run_command_get_display_name (self->current_run_command);
+    /* translators: %s is replaced with the name of the users run command */
+    g_autofree char *title = g_strdup_printf (_("Running %s…"), name);
+
+    g_clear_object (&self->notif);
+    self->notif = g_object_new (IDE_TYPE_NOTIFICATION,
+                                "id", "org.gnome.builder.run-manager.run",
+                                "title", title,
+                                NULL);
+    ide_notification_attach (self->notif, IDE_OBJECT (self));
+  }
+
+  _ide_run_tool_emit_started (self->run_tool, subprocess);
+
+  g_signal_emit (self, signals[STARTED], 0);
+
+  /* Wait for the application to finish running */
+  ide_subprocess_wait_check_async (subprocess,
+                                   ide_task_get_cancellable (task),
+                                   ide_run_manager_run_subprocess_wait_check_cb,
+                                   g_object_ref (task));
+
+  IDE_EXIT;
+}
+
+static void
+ide_run_manager_run_discover_run_command_cb (GObject      *object,
+                                             GAsyncResult *result,
+                                             gpointer      user_data)
+{
+  IdeRunManager *self = (IdeRunManager *)object;
+  g_autoptr(IdeNotification) notif = NULL;
+  g_autoptr(IdeRunCommand) run_command = NULL;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeDeployStrategy *deploy_strategy;
+  GCancellable *cancellable;
+  IdePipeline *pipeline;
+  IdeContext *context;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_RUN_MANAGER (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!(run_command = ide_run_manager_discover_run_command_finish (self, result, &error)))
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  g_set_object (&self->current_run_command, run_command);
+
+  cancellable = ide_task_get_cancellable (task);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  pipeline = ide_task_get_task_data (task);
+  g_assert (IDE_IS_PIPELINE (pipeline));
+
+  context = ide_object_get_context (IDE_OBJECT (pipeline));
+  g_assert (IDE_IS_CONTEXT (context));
+
+  deploy_strategy = ide_pipeline_get_deploy_strategy (pipeline);
+  g_assert (IDE_IS_DEPLOY_STRATEGY (deploy_strategy));
+
+  notif = g_object_new (IDE_TYPE_NOTIFICATION,
+                        "id", "org.gnome.builder.run-manager.deploy",
+                        "title", _("Deploying to device…"),
+                        "icon-name", "package-x-generic-symbolic",
+                        "has-progress", TRUE,
+                        "progress-is-imprecise", FALSE,
+                        NULL);
+  ide_notification_attach (notif, IDE_OBJECT (context));
+  g_object_set_data_full (G_OBJECT (deploy_strategy),
+                          "PROGRESS",
+                          g_object_ref (notif),
+                          g_object_unref);
+
+  ide_deploy_strategy_deploy_async (deploy_strategy,
+                                    pipeline,
+                                    ide_notification_file_progress_callback,
+                                    g_object_ref (notif),
+                                    g_object_unref,
+                                    cancellable,
+                                    ide_run_manager_run_deploy_cb,
+                                    g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+static void
+ide_run_manager_run_install_cb (GObject      *object,
+                                GAsyncResult *result,
+                                gpointer      user_data)
+{
+  IdeRunManager *self = (IdeRunManager *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_RUN_MANAGER (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!ide_run_manager_install_finish (self, result, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_run_manager_discover_run_command_async (self,
+                                                ide_task_get_cancellable (task),
+                                                ide_run_manager_run_discover_run_command_cb,
+                                                g_object_ref (task));
 
   IDE_EXIT;
 }
 
 void
 ide_run_manager_run_async (IdeRunManager       *self,
-                           IdeBuildTarget      *build_target,
                            GCancellable        *cancellable,
                            GAsyncReadyCallback  callback,
                            gpointer             user_data)
@@ -847,23 +1060,25 @@ ide_run_manager_run_async (IdeRunManager       *self,
   g_autoptr(IdeTask) task = NULL;
   g_autoptr(GCancellable) local_cancellable = NULL;
   g_autoptr(GError) error = NULL;
+  IdeBuildManager *build_manager;
+  IdePipeline *pipeline;
+  IdeContext *context;
 
   IDE_ENTRY;
 
   g_return_if_fail (IDE_IS_MAIN_THREAD ());
   g_return_if_fail (IDE_IS_RUN_MANAGER (self));
-  g_return_if_fail (!build_target || IDE_IS_BUILD_TARGET (build_target));
   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
   g_return_if_fail (!g_cancellable_is_cancelled (self->cancellable));
 
   if (cancellable == NULL)
     cancellable = local_cancellable = g_cancellable_new ();
+  ide_cancellable_chain (cancellable, self->cancellable);
 
-  dzl_cancellable_chain (cancellable, self->cancellable);
+  self->sent_signal = FALSE;
 
   task = ide_task_new (self, cancellable, callback, user_data);
   ide_task_set_source_tag (task, ide_run_manager_run_async);
-  ide_task_set_priority (task, G_PRIORITY_LOW);
 
   if (ide_task_return_error_if_cancelled (task))
     IDE_EXIT;
@@ -874,10 +1089,32 @@ ide_run_manager_run_async (IdeRunManager       *self,
       IDE_EXIT;
     }
 
-  if (build_target != NULL)
-    ide_run_manager_set_build_target (self, build_target);
+  ide_run_manager_mark_busy (self);
+  g_signal_connect_object (task,
+                           "notify::completed",
+                           G_CALLBACK (ide_run_manager_unmark_busy),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  build_manager = ide_build_manager_from_context (context);
+  pipeline = ide_build_manager_get_pipeline (build_manager);
+
+  if (pipeline == NULL)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_NOT_FOUND,
+                                 "A pipeline cannot be found");
+      IDE_EXIT;
+    }
+
+  ide_task_set_task_data (task, g_object_ref (pipeline), g_object_unref);
 
-  ide_run_manager_do_install_before_run (self, task);
+  ide_run_manager_install_async (self,
+                                 cancellable,
+                                 ide_run_manager_run_install_cb,
+                                 g_steal_pointer (&task));
 
   IDE_EXIT;
 }
@@ -914,13 +1151,54 @@ do_cancel_in_timeout (gpointer user_data)
   IDE_RETURN (G_SOURCE_REMOVE);
 }
 
+static int
+ide_run_manager_get_exit_signal (IdeRunManager *self)
+{
+  g_autoptr(GSettings) settings = NULL;
+  g_autofree char *stop_signal = NULL;
+  IdeContext *context;
+  int signum;
+
+  g_assert (IDE_IS_RUN_MANAGER (self));
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  settings = ide_context_ref_project_settings (context);
+  stop_signal = g_settings_get_string (settings, "stop-signal");
+
+  if (0) {}
+  else if (ide_str_equal0 (stop_signal, "SIGKILL")) signum = SIGKILL;
+  else if (ide_str_equal0 (stop_signal, "SIGINT"))  signum = SIGINT;
+  else if (ide_str_equal0 (stop_signal, "SIGHUP"))  signum = SIGHUP;
+  else if (ide_str_equal0 (stop_signal, "SIGUSR1")) signum = SIGUSR1;
+  else if (ide_str_equal0 (stop_signal, "SIGUSR2")) signum = SIGUSR2;
+  else if (ide_str_equal0 (stop_signal, "SIGABRT")) signum = SIGABRT;
+  else if (ide_str_equal0 (stop_signal, "SIGQUIT")) signum = SIGQUIT;
+  else signum = SIGKILL;
+
+  return signum;
+}
+
 void
 ide_run_manager_cancel (IdeRunManager *self)
 {
   IDE_ENTRY;
 
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
   g_return_if_fail (IDE_IS_RUN_MANAGER (self));
 
+  if (self->current_subprocess != NULL)
+    {
+      int exit_signal = ide_run_manager_get_exit_signal (self);
+
+      if (!self->sent_signal)
+        ide_run_tool_send_signal (self->run_tool, exit_signal);
+      else
+        ide_run_tool_force_exit (self->run_tool);
+
+      self->sent_signal = TRUE;
+    }
+
+  /* Make sure tasks are cancelled too */
   if (self->cancellable != NULL)
     g_timeout_add (0, do_cancel_in_timeout, g_steal_pointer (&self->cancellable));
   self->cancellable = g_cancellable_new ();
@@ -928,444 +1206,382 @@ ide_run_manager_cancel (IdeRunManager *self)
   IDE_EXIT;
 }
 
-void
-ide_run_manager_set_handler (IdeRunManager *self,
-                             const gchar   *id)
+static void
+ide_run_manager_run_action_cb (GObject      *object,
+                               GAsyncResult *result,
+                               gpointer      user_data)
 {
-  g_return_if_fail (IDE_IS_RUN_MANAGER (self));
+  IdeRunManager *self = (IdeRunManager *)object;
+  IdeContext *context;
+  g_autoptr(GError) error = NULL;
 
-  self->handler = NULL;
+  g_assert (IDE_IS_RUN_MANAGER (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
 
-  for (GList *iter = self->handlers; iter; iter = iter->next)
-    {
-      const IdeRunHandlerInfo *info = iter->data;
+  context = ide_object_get_context (IDE_OBJECT (self));
 
-      if (g_strcmp0 (info->id, id) == 0)
-        {
-          self->handler = info;
-          IDE_TRACE_MSG ("run handler set to %s", info->title);
-          g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HANDLER]);
-          break;
-        }
-    }
+  /* Propagate the error to the context */
+  if (!ide_run_manager_run_finish (self, result, &error))
+    ide_context_warning (context, "%s", error->message);
 }
 
-void
-ide_run_manager_add_handler (IdeRunManager  *self,
-                             const gchar    *id,
-                             const gchar    *title,
-                             const gchar    *icon_name,
-                             const gchar    *accel,
-                             IdeRunHandler   run_handler,
-                             gpointer        user_data,
-                             GDestroyNotify  user_data_destroy)
+static void
+ide_run_manager_actions_run (IdeRunManager *self,
+                             GVariant      *param)
 {
-  IdeRunHandlerInfo *info;
-  DzlShortcutManager *manager;
-  DzlShortcutTheme *theme;
-  g_autofree gchar *action_name = NULL;
-  GApplication *app;
+  IDE_ENTRY;
 
-  g_return_if_fail (IDE_IS_RUN_MANAGER (self));
-  g_return_if_fail (id != NULL);
-  g_return_if_fail (title != NULL);
-
-  info = g_slice_new0 (IdeRunHandlerInfo);
-  info->id = g_strdup (id);
-  info->title = g_strdup (title);
-  info->icon_name = g_strdup (icon_name);
-  info->accel = g_strdup (accel);
-  info->handler = run_handler;
-  info->handler_data = user_data;
-  info->handler_data_destroy = user_data_destroy;
-
-  self->handlers = g_list_append (self->handlers, info);
-
-  app = g_application_get_default ();
-  manager = dzl_application_get_shortcut_manager (DZL_APPLICATION (app));
-  theme = g_object_ref (dzl_shortcut_manager_get_theme (manager));
-
-  action_name = g_strdup_printf ("run-manager.run-with-handler('%s')", id);
-
-  dzl_shortcut_manager_add_action (manager,
-                                   action_name,
-                                   N_("Workbench shortcuts"),
-                                   N_("Build and Run"),
-                                   g_dgettext (GETTEXT_PACKAGE, title),
-                                   NULL);
-
-  dzl_shortcut_theme_set_accel_for_action (theme,
-                                           action_name,
-                                           accel,
-                                           DZL_SHORTCUT_PHASE_GLOBAL | DZL_SHORTCUT_PHASE_CAPTURE);
-
-  if (self->handler == NULL)
-    self->handler = info;
+  g_assert (IDE_IS_RUN_MANAGER (self));
+
+  ide_run_manager_run_async (self,
+                             NULL,
+                             ide_run_manager_run_action_cb,
+                             NULL);
+
+  IDE_EXIT;
 }
 
-void
-ide_run_manager_remove_handler (IdeRunManager *self,
-                                const gchar   *id)
+static void
+ide_run_manager_actions_run_with_handler (IdeRunManager *self,
+                                          GVariant      *param)
 {
-  g_return_if_fail (IDE_IS_RUN_MANAGER (self));
-  g_return_if_fail (id != NULL);
+  IDE_ENTRY;
 
-  for (GList *iter = self->handlers; iter; iter = iter->next)
-    {
-      IdeRunHandlerInfo *info = iter->data;
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_RUN_MANAGER (self));
+  g_assert (g_variant_is_of_type (param, G_VARIANT_TYPE_STRING));
 
-      if (g_strcmp0 (info->id, id) == 0)
-        {
-          self->handlers = g_list_delete_link (self->handlers, iter);
+  ide_run_manager_set_run_tool_from_module_name (self, g_variant_get_string (param, NULL));
 
-          if (self->handler == info && self->handlers != NULL)
-            self->handler = self->handlers->data;
-          else
-            self->handler = NULL;
+  ide_run_manager_run_async (self,
+                             NULL,
+                             ide_run_manager_run_action_cb,
+                             NULL);
 
-          ide_run_handler_info_free (info);
+  IDE_EXIT;
+}
 
-          break;
-        }
-    }
+static void
+ide_run_manager_actions_stop (IdeRunManager *self,
+                              GVariant      *param)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_RUN_MANAGER (self));
+
+  ide_run_manager_cancel (self);
+
+  IDE_EXIT;
 }
 
-/**
- * ide_run_manager_get_build_target:
- *
- * Gets the build target that will be executed by the run manager which
- * was either specified to ide_run_manager_run_async() or determined by
- * the build system.
- *
- * Returns: (transfer none): An #IdeBuildTarget or %NULL if no build target
- *   has been set.
- *
- * Since: 3.32
- */
-IdeBuildTarget *
-ide_run_manager_get_build_target (IdeRunManager *self)
+static void
+ide_run_manager_init (IdeRunManager *self)
 {
-  g_return_val_if_fail (IDE_IS_RUN_MANAGER (self), NULL);
+  GtkTextDirection text_dir;
 
-  return self->build_target;
+  self->cancellable = g_cancellable_new ();
+  self->run_tool = ide_no_tool_new ();
+
+  /* Setup initial text direction state */
+  text_dir = gtk_widget_get_default_direction ();
+  if (text_dir == GTK_TEXT_DIR_LTR)
+    ide_run_manager_set_action_state (self,
+                                      "text-direction",
+                                      text_dir == GTK_TEXT_DIR_LTR ?
+                                        g_variant_new_string ("ltr") :
+                                        g_variant_new_string ("rtl"));
 }
 
 void
-ide_run_manager_set_build_target (IdeRunManager  *self,
-                                  IdeBuildTarget *build_target)
+_ide_run_manager_drop_caches (IdeRunManager *self)
 {
   g_return_if_fail (IDE_IS_RUN_MANAGER (self));
-  g_return_if_fail (!build_target || IDE_IS_BUILD_TARGET (build_target));
 
-  if (build_target == self->build_target)
-    return;
+  self->last_change_seq = 0;
+}
+
+static void
+ide_run_manager_actions_messages_debug_all (IdeRunManager *self,
+                                            GVariant      *param)
+{
+  IDE_ENTRY;
 
-  if (self->build_target)
-    ide_clear_and_destroy_object (&self->build_target);
+  g_assert (IDE_IS_RUN_MANAGER (self));
 
-  if (build_target)
-    self->build_target = g_object_ref (build_target);
+  self->messages_debug_all = !self->messages_debug_all;
+  ide_run_manager_set_action_state (self,
+                                    "messages-debug-all",
+                                    g_variant_new_boolean (self->messages_debug_all));
 
-  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUILD_TARGET]);
+  IDE_EXIT;
 }
 
-static gint
-compare_targets (gconstpointer a,
-                 gconstpointer b)
+typedef struct
 {
-  const IdeBuildTarget * const *a_target = a;
-  const IdeBuildTarget * const *b_target = b;
-
-  return ide_build_target_compare (*a_target, *b_target);
-}
+  GString *errors;
+  GListStore *store;
+  int n_active;
+} ListCommands;
 
 static void
-collect_extensions (PeasExtensionSet *set,
-                    PeasPluginInfo   *plugin_info,
-                    PeasExtension    *exten,
-                    gpointer          user_data)
+list_commands_free (ListCommands *state)
 {
-  DiscoverState *state = user_data;
-
   g_assert (state != NULL);
-  g_assert (IDE_IS_BUILD_TARGET_PROVIDER (exten));
+  g_assert (state->n_active == 0);
 
-  state->providers = g_list_append (state->providers, g_object_ref (exten));
-  state->active++;
+  g_string_free (state->errors, TRUE);
+  state->errors = NULL;
+  g_clear_object (&state->store);
+  g_slice_free (ListCommands, state);
 }
 
 static void
-ide_run_manager_provider_get_targets_cb (GObject      *object,
-                                         GAsyncResult *result,
-                                         gpointer      user_data)
+ide_run_manager_list_commands_cb (GObject      *object,
+                                  GAsyncResult *result,
+                                  gpointer      user_data)
 {
-  IdeBuildTargetProvider *provider = (IdeBuildTargetProvider *)object;
-  g_autoptr(IdeBuildTarget) first = NULL;
+  IdeRunCommandProvider *provider = (IdeRunCommandProvider *)object;
+  g_autoptr(GListModel) model = NULL;
   g_autoptr(IdeTask) task = user_data;
-  g_autoptr(GPtrArray) ret = NULL;
   g_autoptr(GError) error = NULL;
-  IdeRunManager *self;
-  DiscoverState *state;
-
-  IDE_ENTRY;
+  ListCommands *state;
 
-  g_assert (IDE_IS_BUILD_TARGET_PROVIDER (provider));
+  g_assert (IDE_IS_RUN_COMMAND_PROVIDER (provider));
   g_assert (G_IS_ASYNC_RESULT (result));
   g_assert (IDE_IS_TASK (task));
 
-  self = ide_task_get_source_object (task);
   state = ide_task_get_task_data (task);
 
-  g_assert (IDE_IS_RUN_MANAGER (self));
   g_assert (state != NULL);
-  g_assert (state->active > 0);
-  g_assert (g_list_find (state->providers, provider) != NULL);
+  g_assert (state->n_active > 0);
+  g_assert (G_IS_LIST_STORE (state->store));
 
-  ret = ide_build_target_provider_get_targets_finish (provider, result, &error);
-  IDE_PTR_ARRAY_SET_FREE_FUNC (ret, g_object_unref);
-
-  if (ret != NULL)
+  if (!(model = ide_run_command_provider_list_commands_finish (provider, result, &error)))
     {
-      for (guint i = 0; i < ret->len; i++)
+      if (!ide_error_ignore (error))
         {
-          IdeBuildTarget *target = g_ptr_array_index (ret, i);
-
-          if (ide_object_is_root (IDE_OBJECT (target)))
-            ide_object_append (IDE_OBJECT (self), IDE_OBJECT (target));
-
-          g_ptr_array_add (state->results, g_object_ref (target));
+          if (state->errors->len > 0)
+            g_string_append (state->errors, "; ");
+          g_string_append (state->errors, error->message);
         }
     }
+  else
+    {
+      g_list_store_append (state->store, model);
+    }
 
-  ide_object_destroy (IDE_OBJECT (provider));
-
-  state->active--;
-
-  if (state->active > 0)
-    return;
+  state->n_active--;
 
-  if (state->results->len == 0)
+  if (state->n_active == 0)
     {
-      if (error != NULL)
-        ide_task_return_error (task, g_steal_pointer (&error));
-      else
+      if (state->errors->len > 0)
         ide_task_return_new_error (task,
-                                   IDE_RUNTIME_ERROR,
-                                   IDE_RUNTIME_ERROR_TARGET_NOT_FOUND,
-                                   _("Failed to locate a build target"));
-      IDE_EXIT;
+                                   G_IO_ERROR,
+                                   G_IO_ERROR_FAILED,
+                                   "%s",
+                                   state->errors->str);
+      else
+        ide_task_return_pointer (task,
+                                 gtk_flatten_list_model_new (G_LIST_MODEL (g_steal_pointer (&state->store))),
+                                 g_object_unref);
     }
+}
 
-  g_ptr_array_sort (state->results, compare_targets);
+static void
+ide_run_manager_list_commands_foreach_cb (IdeExtensionSetAdapter *set,
+                                          PeasPluginInfo         *plugin_info,
+                                          PeasExtension          *exten,
+                                          gpointer                user_data)
+{
+  IdeRunCommandProvider *provider = (IdeRunCommandProvider *)exten;
+  IdeTask *task = user_data;
+  ListCommands *state;
 
-  /* Steal the first item so that it is not destroyed */
-  first = ide_ptr_array_steal_index (state->results,
-                                     0,
-                                     (GDestroyNotify)ide_object_unref_and_destroy);
-  ide_task_return_pointer (task,
-                           IDE_OBJECT (g_steal_pointer (&first)),
-                           ide_object_unref_and_destroy);
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_RUN_COMMAND_PROVIDER (provider));
+  g_assert (IDE_IS_TASK (task));
 
-  IDE_EXIT;
+  state = ide_task_get_task_data (task);
+  state->n_active++;
+
+  ide_run_command_provider_list_commands_async (provider,
+                                                ide_task_get_cancellable (task),
+                                                ide_run_manager_list_commands_cb,
+                                                g_object_ref (task));
 }
 
 void
-ide_run_manager_discover_default_target_async (IdeRunManager       *self,
-                                               GCancellable        *cancellable,
-                                               GAsyncReadyCallback  callback,
-                                               gpointer             user_data)
+ide_run_manager_list_commands_async (IdeRunManager       *self,
+                                     GCancellable        *cancellable,
+                                     GAsyncReadyCallback  callback,
+                                     gpointer             user_data)
 {
-  g_autoptr(PeasExtensionSet) set = NULL;
   g_autoptr(IdeTask) task = NULL;
-  DiscoverState *state;
+  ListCommands *state;
 
   IDE_ENTRY;
 
   g_return_if_fail (IDE_IS_RUN_MANAGER (self));
   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
 
-  task = ide_task_new (self, cancellable, callback, user_data);
-  ide_task_set_source_tag (task, ide_run_manager_discover_default_target_async);
-  ide_task_set_priority (task, G_PRIORITY_LOW);
+  state = g_slice_new0 (ListCommands);
+  state->store = g_list_store_new (G_TYPE_LIST_MODEL);
+  state->errors = g_string_new (NULL);
 
-  set = peas_extension_set_new (peas_engine_get_default (),
-                                IDE_TYPE_BUILD_TARGET_PROVIDER,
-                                NULL);
-
-  state = g_slice_new0 (DiscoverState);
-  state->results = g_ptr_array_new_with_free_func ((GDestroyNotify)ide_object_unref_and_destroy);
-  state->providers = NULL;
-  state->active = 0;
-
-  peas_extension_set_foreach (set, collect_extensions, state);
-
-  for (const GList *iter = state->providers; iter; iter = iter->next)
-    ide_object_append (IDE_OBJECT (self), IDE_OBJECT (iter->data));
-
-  ide_task_set_task_data (task, state, discover_state_free);
-
-  for (const GList *iter = state->providers; iter != NULL; iter = iter->next)
-    {
-      IdeBuildTargetProvider *provider = iter->data;
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_run_manager_list_commands_async);
+  ide_task_set_task_data (task, state, list_commands_free);
 
-      ide_build_target_provider_get_targets_async (provider,
-                                                   cancellable,
-                                                   ide_run_manager_provider_get_targets_cb,
-                                                   g_object_ref (task));
-    }
+  if (self->run_command_providers)
+    ide_extension_set_adapter_foreach (self->run_command_providers,
+                                       ide_run_manager_list_commands_foreach_cb,
+                                       task);
 
-  if (state->active == 0)
+  if (state->n_active == 0)
     ide_task_return_new_error (task,
-                               IDE_RUNTIME_ERROR,
-                               IDE_RUNTIME_ERROR_TARGET_NOT_FOUND,
-                               _("Failed to locate a build target"));
+                               G_IO_ERROR,
+                               G_IO_ERROR_NOT_SUPPORTED,
+                               "No run command providers available");
 
   IDE_EXIT;
 }
 
 /**
- * ide_run_manager_discover_default_target_finish:
+ * ide_run_manager_list_commands_finish:
  *
- * Returns: (transfer full): An #IdeBuildTarget if successful; otherwise %NULL
- *   and @error is set.
- *
- * Since: 3.32
+ * Returns: (transfer full): a #GListModel of #IdeRunCommand
  */
-IdeBuildTarget *
-ide_run_manager_discover_default_target_finish (IdeRunManager  *self,
-                                                GAsyncResult   *result,
-                                                GError        **error)
+GListModel *
+ide_run_manager_list_commands_finish (IdeRunManager  *self,
+                                      GAsyncResult   *result,
+                                      GError        **error)
 {
-  IdeBuildTarget *ret;
-
-  IDE_ENTRY;
-
   g_return_val_if_fail (IDE_IS_RUN_MANAGER (self), NULL);
   g_return_val_if_fail (IDE_IS_TASK (result), NULL);
 
-  ret = ide_task_propagate_pointer (IDE_TASK (result), error);
-
-  IDE_RETURN (ret);
-}
-
-const GList *
-_ide_run_manager_get_handlers (IdeRunManager *self)
-{
-  g_return_val_if_fail (IDE_IS_RUN_MANAGER (self), NULL);
-
-  return self->handlers;
-}
-
-const gchar *
-ide_run_manager_get_handler (IdeRunManager *self)
-{
-  g_return_val_if_fail (IDE_IS_RUN_MANAGER (self), NULL);
-
-  if (self->handler != NULL)
-    return self->handler->id;
-
-  return NULL;
+  return ide_task_propagate_pointer (IDE_TASK (result), error);
 }
 
 static void
-ide_run_manager_run_action_cb (GObject      *object,
-                               GAsyncResult *result,
-                               gpointer      user_data)
+ide_run_manager_discover_run_command_cb (GObject      *object,
+                                         GAsyncResult *result,
+                                         gpointer      user_data)
 {
   IdeRunManager *self = (IdeRunManager *)object;
-  IdeContext *context;
+  g_autoptr(IdeRunCommand) best = NULL;
+  g_autoptr(GListModel) model = NULL;
+  g_autoptr(IdeTask) task = user_data;
   g_autoptr(GError) error = NULL;
+  const char *default_id;
+  guint n_items;
+  int best_priority = G_MAXINT;
 
-  g_assert (IDE_IS_RUN_MANAGER (self));
-  g_assert (G_IS_ASYNC_RESULT (result));
-
-  context = ide_object_get_context (IDE_OBJECT (self));
-
-  /* Propagate the error to the context */
-  if (!ide_run_manager_run_finish (self, result, &error))
-    ide_context_warning (context, "%s", error->message);
-}
-
-static void
-ide_run_manager_actions_run (IdeRunManager *self,
-                             GVariant      *param)
-{
   IDE_ENTRY;
 
   g_assert (IDE_IS_RUN_MANAGER (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
 
-  ide_run_manager_run_async (self,
-                             NULL,
-                             NULL,
-                             ide_run_manager_run_action_cb,
-                             NULL);
+  if (!(model = ide_run_manager_list_commands_finish (self, result, &error)))
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
 
-  IDE_EXIT;
-}
+  default_id = ide_task_get_task_data (task);
+  n_items = g_list_model_get_n_items (model);
 
-static void
-ide_run_manager_actions_run_with_handler (IdeRunManager *self,
-                                          GVariant      *param)
-{
-  const gchar *handler = NULL;
-  g_autoptr(GVariant) sunk = NULL;
+  for (guint i = 0; i < n_items; i++)
+    {
+      g_autoptr(IdeRunCommand) run_command = g_list_model_get_item (model, i);
+      const char *id;
+      int priority;
 
-  IDE_ENTRY;
+      g_assert (IDE_IS_RUN_COMMAND (run_command));
 
-  g_assert (IDE_IS_RUN_MANAGER (self));
+      id = ide_run_command_get_id (run_command);
+      priority = ide_run_command_get_priority (run_command);
 
-  if (param != NULL)
-  {
-    handler = g_variant_get_string (param, NULL);
-    if (g_variant_is_floating (param))
-      sunk = g_variant_ref_sink (param);
-  }
+      if (!ide_str_empty0 (id) &&
+          !ide_str_empty0 (default_id) &&
+          strcmp (default_id, id) == 0)
+        {
+          ide_task_return_pointer (task,
+                                   g_steal_pointer (&run_command),
+                                   g_object_unref);
+          IDE_EXIT;
+        }
 
-  /* Use specified handler, if provided */
-  if (!ide_str_empty0 (handler))
-    ide_run_manager_set_handler (self, handler);
+      if (best == NULL || priority < best_priority)
+        {
+          g_set_object (&best, run_command);
+          best_priority = priority;
+        }
+    }
 
-  ide_run_manager_run_async (self,
-                             NULL,
-                             NULL,
-                             ide_run_manager_run_action_cb,
-                             NULL);
+  if (best != NULL)
+    ide_task_return_pointer (task,
+                             g_steal_pointer (&best),
+                             g_object_unref);
+  else
+    ide_task_return_new_error (task,
+                               G_IO_ERROR,
+                               G_IO_ERROR_NOT_FOUND,
+                               "No run command discovered. Set one manually.");
 
   IDE_EXIT;
 }
 
-static void
-ide_run_manager_actions_stop (IdeRunManager *self,
-                              GVariant      *param)
+void
+ide_run_manager_discover_run_command_async (IdeRunManager       *self,
+                                            GCancellable        *cancellable,
+                                            GAsyncReadyCallback  callback,
+                                            gpointer             user_data)
 {
+  g_autoptr(IdeTask) task = NULL;
+
   IDE_ENTRY;
 
-  g_assert (IDE_IS_RUN_MANAGER (self));
+  g_return_if_fail (IDE_IS_RUN_MANAGER (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
 
-  ide_run_manager_cancel (self);
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_run_manager_discover_run_command_async);
+  ide_task_set_task_data (task, g_strdup (self->default_run_command), g_free);
+
+  ide_run_manager_list_commands_async (self,
+                                       cancellable,
+                                       ide_run_manager_discover_run_command_cb,
+                                       g_steal_pointer (&task));
 
   IDE_EXIT;
 }
 
-static void
-ide_run_manager_init (IdeRunManager *self)
+/**
+ * ide_run_manager_discover_run_command_finish:
+ * @self: a #IdeRunManager
+ *
+ * Complete request to discover the default run command.
+ *
+ * Returns: (transfer full): an #IdeRunCommand if successful; otherwise
+ *   %NULL and @error is set.
+ */
+IdeRunCommand *
+ide_run_manager_discover_run_command_finish (IdeRunManager  *self,
+                                             GAsyncResult   *result,
+                                             GError        **error)
 {
-  self->cancellable = g_cancellable_new ();
+  IdeRunCommand *run_command;
 
-  ide_run_manager_add_handler (self,
-                               "run",
-                               _("Run"),
-                               "builder-run-start-symbolic",
-                               "<primary>F5",
-                               NULL,
-                               NULL,
-                               NULL);
-}
+  IDE_ENTRY;
 
-void
-_ide_run_manager_drop_caches (IdeRunManager *self)
-{
-  g_return_if_fail (IDE_IS_RUN_MANAGER (self));
+  g_return_val_if_fail (IDE_IS_RUN_MANAGER (self), NULL);
+  g_return_val_if_fail (IDE_IS_TASK (result), NULL);
 
-  self->last_change_seq = 0;
+  run_command = ide_task_propagate_pointer (IDE_TASK (result), error);
+
+  g_return_val_if_fail (!run_command || IDE_IS_RUN_COMMAND (run_command), NULL);
+
+  IDE_RETURN (run_command);
 }
diff --git a/src/libide/foundry/ide-run-manager.h b/src/libide/foundry/ide-run-manager.h
index 3889bb9c1..d10ab5501 100644
--- a/src/libide/foundry/ide-run-manager.h
+++ b/src/libide/foundry/ide-run-manager.h
@@ -24,6 +24,8 @@
 # error "Only <libide-foundry.h> can be included directly."
 #endif
 
+#include <libpeas/peas.h>
+
 #include <libide-core.h>
 
 #include "ide-foundry-types.h"
@@ -32,59 +34,47 @@ G_BEGIN_DECLS
 
 #define IDE_TYPE_RUN_MANAGER (ide_run_manager_get_type())
 
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 G_DECLARE_FINAL_TYPE (IdeRunManager, ide_run_manager, IDE, RUN_MANAGER, IdeObject)
 
-typedef void (*IdeRunHandler) (IdeRunManager *self,
-                               IdeRunner     *runner,
-                               gpointer       user_data);
-
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 IdeRunManager  *ide_run_manager_from_context                   (IdeContext           *context);
-IDE_AVAILABLE_IN_3_32
-IdeBuildTarget *ide_run_manager_get_build_target               (IdeRunManager        *self);
-IDE_AVAILABLE_IN_3_32
-void            ide_run_manager_set_build_target               (IdeRunManager        *self,
-                                                                IdeBuildTarget       *build_target);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 void            ide_run_manager_cancel                         (IdeRunManager        *self);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
+const char     *ide_run_manager_get_icon_name                  (IdeRunManager        *self);
+IDE_AVAILABLE_IN_ALL
 gboolean        ide_run_manager_get_busy                       (IdeRunManager        *self);
-IDE_AVAILABLE_IN_3_32
-const gchar    *ide_run_manager_get_handler                    (IdeRunManager        *self);
-IDE_AVAILABLE_IN_3_32
-void            ide_run_manager_set_handler                    (IdeRunManager        *self,
-                                                                const gchar          *id);
-IDE_AVAILABLE_IN_3_32
-void            ide_run_manager_add_handler                    (IdeRunManager        *self,
-                                                                const gchar          *id,
-                                                                const gchar          *title,
-                                                                const gchar          *icon_name,
-                                                                const gchar          *accel,
-                                                                IdeRunHandler         run_handler,
-                                                                gpointer              user_data,
-                                                                GDestroyNotify        user_data_destroy);
-IDE_AVAILABLE_IN_3_32
-void            ide_run_manager_remove_handler                 (IdeRunManager        *self,
-                                                                const gchar          *id);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
+void            ide_run_manager_set_run_tool_from_plugin_info  (IdeRunManager        *self,
+                                                                PeasPluginInfo       *plugin_info);
+IDE_AVAILABLE_IN_ALL
 void            ide_run_manager_run_async                      (IdeRunManager        *self,
-                                                                IdeBuildTarget       *build_target,
                                                                 GCancellable         *cancellable,
                                                                 GAsyncReadyCallback   callback,
                                                                 gpointer              user_data);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 gboolean        ide_run_manager_run_finish                     (IdeRunManager        *self,
                                                                 GAsyncResult         *result,
                                                                 GError              **error);
-IDE_AVAILABLE_IN_3_32
-void            ide_run_manager_discover_default_target_async  (IdeRunManager        *self,
+IDE_AVAILABLE_IN_ALL
+void            ide_run_manager_list_commands_async            (IdeRunManager        *self,
+                                                                GCancellable         *cancellable,
+                                                                GAsyncReadyCallback   callback,
+                                                                gpointer              user_data);
+IDE_AVAILABLE_IN_ALL
+GListModel     *ide_run_manager_list_commands_finish           (IdeRunManager        *self,
+                                                                GAsyncResult         *result,
+                                                                GError              **error);
+IDE_AVAILABLE_IN_ALL
+void            ide_run_manager_discover_run_command_async     (IdeRunManager        *self,
                                                                 GCancellable         *cancellable,
                                                                 GAsyncReadyCallback   callback,
                                                                 gpointer              user_data);
-IDE_AVAILABLE_IN_3_32
-IdeBuildTarget *ide_run_manager_discover_default_target_finish (IdeRunManager        *self,
+IDE_AVAILABLE_IN_ALL
+IdeRunCommand  *ide_run_manager_discover_run_command_finish    (IdeRunManager        *self,
                                                                 GAsyncResult         *result,
                                                                 GError              **error);
 
+
 G_END_DECLS
diff --git a/src/libide/foundry/ide-runtime.c b/src/libide/foundry/ide-runtime.c
index f58b29718..c187cabc0 100644
--- a/src/libide/foundry/ide-runtime.c
+++ b/src/libide/foundry/ide-runtime.c
@@ -22,7 +22,6 @@
 
 #include "config.h"
 
-#include <dazzle.h>
 #include <glib/gi18n.h>
 #include <string.h>
 
@@ -33,11 +32,13 @@
 # include "../terminal/ide-terminal-util.h"
 #undef IDE_TERMINAL_INSIDE
 
+#include "ide-build-manager.h"
 #include "ide-build-target.h"
 #include "ide-config.h"
 #include "ide-config-manager.h"
+#include "ide-pipeline.h"
+#include "ide-run-context.h"
 #include "ide-runtime.h"
-#include "ide-runner.h"
 #include "ide-toolchain.h"
 #include "ide-triplet.h"
 
@@ -64,89 +65,22 @@ enum {
 
 static GParamSpec *properties [N_PROPS];
 
-static IdeSubprocessLauncher *
-ide_runtime_real_create_launcher (IdeRuntime  *self,
-                                  GError     **error)
-{
-  IdeSubprocessLauncher *ret;
-
-  IDE_ENTRY;
-
-  g_assert (IDE_IS_RUNTIME (self));
-
-  ret = ide_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDOUT_PIPE | G_SUBPROCESS_FLAGS_STDERR_PIPE);
-
-  if (ret != NULL)
-    {
-      ide_subprocess_launcher_set_run_on_host (ret, TRUE);
-      ide_subprocess_launcher_set_clear_env (ret, FALSE);
-    }
-  else
-    {
-      g_set_error (error,
-                   G_IO_ERROR,
-                   G_IO_ERROR_FAILED,
-                   "An unknown error ocurred");
-    }
-
-  IDE_RETURN (ret);
-}
-
 static gboolean
 ide_runtime_real_contains_program_in_path (IdeRuntime   *self,
-                                           const gchar  *program,
+                                           const char   *program,
                                            GCancellable *cancellable)
 {
+  g_autofree char *path = NULL;
+
   g_assert (IDE_IS_RUNTIME (self));
   g_assert (program != NULL);
   g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
 
-  if (!ide_is_flatpak ())
-    {
-      g_autofree gchar *path = NULL;
-      path = g_find_program_in_path (program);
-      return path != NULL;
-    }
-  else
-    {
-      g_autoptr(IdeSubprocessLauncher) launcher = NULL;
-
-      /*
-       * If we are in flatpak, we have to execute a program on the host to
-       * determine if there is a program available, as we cannot resolve
-       * file paths from inside the mount namespace.
-       */
-
-      if (NULL != (launcher = ide_runtime_create_launcher (self, NULL)))
-        {
-          g_autoptr(IdeSubprocess) subprocess = NULL;
-          g_autofree char *escaped = g_shell_quote (program);
-          g_autofree char *command = g_strdup_printf ("which %s", escaped);
-          const char *user_shell = ide_get_user_shell ();
-
-          ide_subprocess_launcher_set_run_on_host (launcher, TRUE);
-
-          /* Try to get a real PATH by using the preferred shell */
-          if (ide_shell_supports_dash_c (user_shell))
-            ide_subprocess_launcher_push_argv (launcher, user_shell);
-          else
-            ide_subprocess_launcher_push_argv (launcher, "sh");
-
-          /* Try a login shell as well to improve reliability */
-          if (ide_shell_supports_dash_login (user_shell))
-            ide_subprocess_launcher_push_argv (launcher, "--login");
+  path = g_find_program_in_path (program);
 
-          ide_subprocess_launcher_push_argv (launcher, "-c");
-          ide_subprocess_launcher_push_argv (launcher, command);
+  IDE_TRACE_MSG ("Locating program %s => %s", program, path ? path : "missing");
 
-          if (NULL != (subprocess = ide_subprocess_launcher_spawn (launcher, cancellable, NULL)))
-            return ide_subprocess_wait_check (subprocess, NULL, NULL);
-        }
-
-      return FALSE;
-    }
-
-  g_assert_not_reached ();
+  return path != NULL;
 }
 
 gboolean
@@ -199,130 +133,22 @@ ide_runtime_real_prepare_configuration (IdeRuntime *self,
     }
 }
 
-static IdeRunner *
-ide_runtime_real_create_runner (IdeRuntime     *self,
-                                IdeBuildTarget *build_target)
+static GFile *
+ide_runtime_null_translate_file (IdeRuntime *self,
+                                 GFile      *file)
 {
-  IdeRuntimePrivate *priv = ide_runtime_get_instance_private (self);
-  IdeEnvironment *env;
-  g_autoptr(GFile) installdir = NULL;
-  g_auto(GStrv) argv = NULL;
-  g_autofree gchar *cwd = NULL;
-  IdeConfigManager *config_manager;
-  const gchar *prefix;
-  IdeContext *context;
-  IdeRunner *runner;
-  IdeConfig *config;
-
-  g_assert (IDE_IS_RUNTIME (self));
-  g_assert (!build_target || IDE_IS_BUILD_TARGET (build_target));
-
-  context = ide_object_get_context (IDE_OBJECT (self));
-  g_assert (IDE_IS_CONTEXT (context));
-
-  config_manager = ide_config_manager_from_context (context);
-  config = ide_config_manager_get_current (config_manager);
-  prefix = ide_config_get_prefix (config);
-
-  runner = ide_runner_new (context);
-  g_assert (IDE_IS_RUNNER (runner));
-
-  ide_object_append (IDE_OBJECT (self), IDE_OBJECT (runner));
-
-  env = ide_runner_get_environment (runner);
-
-  if (ide_str_equal0 (priv->id, "host"))
-    ide_runner_set_run_on_host (runner, TRUE);
-
-  if (build_target != NULL)
-    {
-      ide_runner_set_build_target (runner, build_target);
-
-      installdir = ide_build_target_get_install_directory (build_target);
-      argv = ide_build_target_get_argv (build_target);
-      cwd = ide_build_target_get_cwd (build_target);
-    }
-
-  /* Possibly translate relative paths for the binary */
-  if (argv && argv[0] && !g_path_is_absolute (argv[0]))
-    {
-      const gchar *slash = strchr (argv[0], '/');
-
-      if (slash != NULL)
-        {
-          g_autofree gchar *copy = g_strdup (slash ? (slash + 1) : argv[0]);
-
-          g_free (argv[0]);
-
-          if (installdir != NULL)
-            {
-              g_autoptr(GFile) dest = g_file_get_child (installdir, copy);
-              argv[0] = g_file_get_path (dest);
-            }
-          else
-            argv[0] = g_steal_pointer (&copy);
-        }
-    }
-
-  if (installdir != NULL)
-    {
-      g_autoptr(GFile) parentdir = NULL;
-      g_autofree gchar *schemadir = NULL;
-      g_autofree gchar *parentpath = NULL;
-
-      /* GSettings requires an env var for non-standard dirs */
-      if ((parentdir = g_file_get_parent (installdir)))
-        {
-          parentpath = g_file_get_path (parentdir);
-          schemadir = g_build_filename (parentpath, "share", "glib-2.0", "schemas", NULL);
-          ide_environment_setenv (env, "GSETTINGS_SCHEMA_DIR", schemadir);
-        }
-    }
-
-  if (prefix != NULL)
-    {
-      static const gchar *tries[] = { "lib64", "lib", "lib32", };
-      const gchar *old_path = ide_environment_getenv (env, "LD_LIBRARY_PATH");
-
-      for (guint i = 0; i < G_N_ELEMENTS (tries); i++)
-        {
-          g_autofree gchar *ld_library_path = g_build_filename (prefix, tries[i], NULL);
-
-          if (g_file_test (ld_library_path, G_FILE_TEST_IS_DIR))
-            {
-              if (old_path != NULL)
-                {
-                  g_autofree gchar *freeme = g_steal_pointer (&ld_library_path);
-                  ld_library_path = g_strdup_printf ("%s:%s", freeme, old_path);
-                }
-
-              ide_environment_setenv (env, "LD_LIBRARY_PATH", ld_library_path);
-              break;
-            }
-        }
-    }
-
-  if (argv != NULL)
-    ide_runner_push_args (runner, (const gchar * const *)argv);
-
-  if (cwd != NULL)
-    ide_runner_set_cwd (runner, cwd);
-
-  return runner;
+  return NULL;
 }
 
 static GFile *
-ide_runtime_real_translate_file (IdeRuntime *self,
-                                 GFile      *file)
+ide_runtime_flatpak_translate_file (IdeRuntime *self,
+                                    GFile      *file)
 {
   g_autofree gchar *path = NULL;
 
   g_assert (IDE_IS_RUNTIME (self));
   g_assert (G_IS_FILE (file));
-
-  /* We only need to translate when running as flatpak */
-  if (!ide_is_flatpak ())
-    return NULL;
+  g_assert (ide_is_flatpak ());
 
   /* Only deal with native files */
   if (!g_file_is_native (file) || NULL == (path = g_file_get_path (file)))
@@ -449,11 +275,13 @@ ide_runtime_class_init (IdeRuntimeClass *klass)
 
   i_object_class->repr = ide_runtime_repr;
 
-  klass->create_launcher = ide_runtime_real_create_launcher;
-  klass->create_runner = ide_runtime_real_create_runner;
   klass->contains_program_in_path = ide_runtime_real_contains_program_in_path;
   klass->prepare_configuration = ide_runtime_real_prepare_configuration;
-  klass->translate_file = ide_runtime_real_translate_file;
+
+  if (ide_is_flatpak ())
+    klass->translate_file = ide_runtime_flatpak_translate_file;
+  else
+    klass->translate_file = ide_runtime_null_translate_file;
 
   properties [PROP_ID] =
     g_param_spec_string ("id",
@@ -654,29 +482,6 @@ ide_runtime_new (const gchar *id,
                        NULL);
 }
 
-/**
- * ide_runtime_create_launcher:
- *
- * Creates a launcher for the runtime.
- *
- * This can be used to execute a command within a runtime.
- *
- * It is important that this function can be run from a thread without
- * side effects.
- *
- * Returns: (transfer full): An #IdeSubprocessLauncher or %NULL upon failure.
- *
- * Since: 3.32
- */
-IdeSubprocessLauncher *
-ide_runtime_create_launcher (IdeRuntime  *self,
-                             GError     **error)
-{
-  g_return_val_if_fail (IDE_IS_RUNTIME (self), NULL);
-
-  return IDE_RUNTIME_GET_CLASS (self)->create_launcher (self, error);
-}
-
 void
 ide_runtime_prepare_configuration (IdeRuntime       *self,
                                    IdeConfig *configuration)
@@ -687,33 +492,6 @@ ide_runtime_prepare_configuration (IdeRuntime       *self,
   IDE_RUNTIME_GET_CLASS (self)->prepare_configuration (self, configuration);
 }
 
-/**
- * ide_runtime_create_runner:
- * @self: An #IdeRuntime
- * @build_target: (nullable): An #IdeBuildTarget or %NULL
- *
- * Creates a new runner that can be used to execute the build target within
- * the runtime. This should be used to implement such features as "run target"
- * or "run unit test" inside the target runtime.
- *
- * If @build_target is %NULL, the runtime should create a runner that allows
- * the caller to specify the binary using the #IdeRunner API.
- *
- * Returns: (transfer full) (nullable): An #IdeRunner if successful, otherwise
- *   %NULL and @error is set.
- *
- * Since: 3.32
- */
-IdeRunner *
-ide_runtime_create_runner (IdeRuntime     *self,
-                           IdeBuildTarget *build_target)
-{
-  g_return_val_if_fail (IDE_IS_RUNTIME (self), NULL);
-  g_return_val_if_fail (!build_target || IDE_IS_BUILD_TARGET (build_target), NULL);
-
-  return IDE_RUNTIME_GET_CLASS (self)->create_runner (self, build_target);
-}
-
 GQuark
 ide_runtime_error_quark (void)
 {
@@ -734,8 +512,6 @@ ide_runtime_error_quark (void)
  * be accessed from the host system.
  *
  * Returns: (transfer full) (not nullable): a #GFile.
- *
- * Since: 3.32
  */
 GFile *
 ide_runtime_translate_file (IdeRuntime *self,
@@ -764,8 +540,6 @@ ide_runtime_translate_file (IdeRuntime *self,
  *
  * Returns: (transfer full) (array zero-terminated=1): A newly allocated
  *   string containing the include dirs.
- *
- * Since: 3.32
  */
 gchar **
 ide_runtime_get_system_include_dirs (IdeRuntime *self)
@@ -791,8 +565,6 @@ ide_runtime_get_system_include_dirs (IdeRuntime *self)
  *
  * Returns: (transfer full) (not nullable): the architecture triplet the runtime
  * will build for.
- *
- * Since: 3.32
  */
 IdeTriplet *
 ide_runtime_get_triplet (IdeRuntime *self)
@@ -824,8 +596,6 @@ ide_runtime_get_triplet (IdeRuntime *self)
  *
  * Returns: (transfer full) (not nullable): the name of the architecture
  * the runtime will build for.
- *
- * Since: 3.32
  */
 gchar *
 ide_runtime_get_arch (IdeRuntime *self)
@@ -849,8 +619,6 @@ ide_runtime_get_arch (IdeRuntime *self)
  * Informs wether a toolchain is supported by this.
  *
  * Returns: %TRUE if the toolchain is supported
- *
- * Since: 3.32
  */
 gboolean
 ide_runtime_supports_toolchain (IdeRuntime   *self,
@@ -870,3 +638,100 @@ ide_runtime_supports_toolchain (IdeRuntime   *self,
 
   return TRUE;
 }
+
+/**
+ * ide_runtime_prepare_to_run:
+ * @self: a #IdeRuntime
+ * @pipeline: (nullable): an #IdePipeline or %NULL for the current
+ * @run_context: an #IdeRunContext
+ *
+ * Prepares a run context to run an application.
+ *
+ * The virtual function implementation should add to the run context anything
+ * necessary to be able to run within the runtime.
+ *
+ * That might include pushing a new layer so that the command will run within
+ * a subcommand such as "flatpak", "jhbuild", or "podman".
+ *
+ * This is meant to be able to run applications, so additional work is expected
+ * of runtimes to ensure access to things like graphical displays.
+ */
+void
+ide_runtime_prepare_to_run (IdeRuntime    *self,
+                            IdePipeline   *pipeline,
+                            IdeRunContext *run_context)
+{
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_RUNTIME (self));
+  g_return_if_fail (!pipeline || IDE_IS_PIPELINE (pipeline));
+  g_return_if_fail (IDE_IS_RUN_CONTEXT (run_context));
+
+  if (IDE_RUNTIME_GET_CLASS (self)->prepare_to_run == NULL)
+    IDE_EXIT;
+
+  if (pipeline == NULL)
+    {
+      IdeContext *context = ide_object_get_context (IDE_OBJECT (self));
+      IdeBuildManager *build_manager = ide_build_manager_from_context (context);
+
+      pipeline = ide_build_manager_get_pipeline (build_manager);
+    }
+
+  g_return_if_fail (IDE_IS_PIPELINE (pipeline));
+  g_return_if_fail (ide_pipeline_get_runtime (pipeline) == self);
+
+  IDE_RUNTIME_GET_CLASS (self)->prepare_to_run (self, pipeline, run_context);
+
+  /* Give the run_context access to some environment */
+  ide_run_context_add_minimal_environment (run_context);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_runtime_prepare_to_build:
+ * @self: a #IdeRuntime
+ * @pipeline: (nullable): an #IdePipeline or %NULL for the current
+ * @run_context: an #IdeRunContext
+ *
+ * Prepares a run context for running a build command.
+ *
+ * The virtual function implementation should add to the run context anything
+ * necessary to be able to run within the runtime.
+ *
+ * That might include pushing a new layer so that the command will run within
+ * a subcommand such as "flatpak", "jhbuild", or "podman".
+ *
+ * This is meant to be able to run a build command, so it may not require
+ * access to some features like network or graphical displays.
+ */
+void
+ide_runtime_prepare_to_build (IdeRuntime    *self,
+                              IdePipeline   *pipeline,
+                              IdeRunContext *run_context)
+{
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_RUNTIME (self));
+  g_return_if_fail (!pipeline || IDE_IS_PIPELINE (pipeline));
+  g_return_if_fail (IDE_IS_RUN_CONTEXT (run_context));
+
+  if (IDE_RUNTIME_GET_CLASS (self)->prepare_to_build == NULL)
+    IDE_EXIT;
+
+  if (pipeline == NULL)
+    {
+      IdeContext *context = ide_object_get_context (IDE_OBJECT (self));
+      IdeBuildManager *build_manager = ide_build_manager_from_context (context);
+
+      pipeline = ide_build_manager_get_pipeline (build_manager);
+    }
+
+  g_return_if_fail (IDE_IS_PIPELINE (pipeline));
+  g_return_if_fail (ide_pipeline_get_runtime (pipeline) == self);
+
+  IDE_RUNTIME_GET_CLASS (self)->prepare_to_build (self, pipeline, run_context);
+
+  IDE_EXIT;
+}
diff --git a/src/libide/foundry/ide-runtime.h b/src/libide/foundry/ide-runtime.h
index 65bf2e238..c353bef6e 100644
--- a/src/libide/foundry/ide-runtime.h
+++ b/src/libide/foundry/ide-runtime.h
@@ -42,7 +42,7 @@ typedef enum
 #define IDE_TYPE_RUNTIME (ide_runtime_get_type())
 #define IDE_RUNTIME_ERROR (ide_runtime_error_quark())
 
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 G_DECLARE_DERIVABLE_TYPE (IdeRuntime, ide_runtime, IDE, RUNTIME, IdeObject)
 
 struct _IdeRuntimeClass
@@ -52,12 +52,14 @@ struct _IdeRuntimeClass
   gboolean                (*contains_program_in_path) (IdeRuntime           *self,
                                                        const gchar          *program,
                                                        GCancellable         *cancellable);
-  IdeSubprocessLauncher  *(*create_launcher)          (IdeRuntime           *self,
-                                                       GError              **error);
   void                    (*prepare_configuration)    (IdeRuntime           *self,
-                                                       IdeConfig     *configuration);
-  IdeRunner              *(*create_runner)            (IdeRuntime           *self,
-                                                       IdeBuildTarget       *build_target);
+                                                       IdeConfig            *config);
+  void                    (*prepare_to_run)           (IdeRuntime           *self,
+                                                       IdePipeline          *pipeline,
+                                                       IdeRunContext        *run_context);
+  void                    (*prepare_to_build)         (IdeRuntime           *self,
+                                                       IdePipeline          *pipeline,
+                                                       IdeRunContext        *run_context);
   GFile                  *(*translate_file)           (IdeRuntime           *self,
                                                        GFile                *file);
   gchar                 **(*get_system_include_dirs)  (IdeRuntime           *self);
@@ -69,59 +71,61 @@ struct _IdeRuntimeClass
   gpointer _reserved[12];
 };
 
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 GQuark                  ide_runtime_error_quark              (void) G_GNUC_CONST;
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 gboolean                ide_runtime_contains_program_in_path (IdeRuntime      *self,
                                                               const gchar     *program,
                                                               GCancellable    *cancellable);
-IDE_AVAILABLE_IN_3_32
-IdeSubprocessLauncher  *ide_runtime_create_launcher          (IdeRuntime      *self,
-                                                              GError         **error);
-IDE_AVAILABLE_IN_3_32
-IdeRunner              *ide_runtime_create_runner            (IdeRuntime      *self,
-                                                              IdeBuildTarget  *build_target);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
+void                    ide_runtime_prepare_to_run           (IdeRuntime      *self,
+                                                              IdePipeline     *pipeline,
+                                                              IdeRunContext   *run_context);
+IDE_AVAILABLE_IN_ALL
+void                    ide_runtime_prepare_to_build         (IdeRuntime      *self,
+                                                              IdePipeline     *pipeline,
+                                                              IdeRunContext   *run_context);
+IDE_AVAILABLE_IN_ALL
 void                    ide_runtime_prepare_configuration    (IdeRuntime      *self,
                                                               IdeConfig       *configuration);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 IdeRuntime             *ide_runtime_new                      (const gchar     *id,
                                                               const gchar     *title);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 const gchar            *ide_runtime_get_id                   (IdeRuntime      *self);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 void                    ide_runtime_set_id                   (IdeRuntime      *self,
                                                               const gchar     *id);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 const gchar            *ide_runtime_get_short_id             (IdeRuntime      *self);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 void                    ide_runtime_set_short_id             (IdeRuntime      *self,
                                                               const gchar     *short_id);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 const gchar            *ide_runtime_get_category             (IdeRuntime      *self);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 void                    ide_runtime_set_category             (IdeRuntime      *self,
                                                               const gchar     *category);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 const gchar            *ide_runtime_get_display_name         (IdeRuntime      *self);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 void                    ide_runtime_set_display_name         (IdeRuntime      *self,
                                                               const gchar     *display_name);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 const gchar            *ide_runtime_get_name                 (IdeRuntime      *self);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 void                    ide_runtime_set_name                 (IdeRuntime      *self,
                                                               const gchar     *name);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 GFile                  *ide_runtime_translate_file           (IdeRuntime      *self,
                                                               GFile           *file);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 gchar                 **ide_runtime_get_system_include_dirs  (IdeRuntime      *self);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 gchar                  *ide_runtime_get_arch                 (IdeRuntime      *self);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 IdeTriplet             *ide_runtime_get_triplet              (IdeRuntime      *self);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 gboolean                ide_runtime_supports_toolchain       (IdeRuntime      *self,
                                                               IdeToolchain    *toolchain);
 
diff --git a/src/libide/foundry/ide-test-manager.c b/src/libide/foundry/ide-test-manager.c
index efc377d27..9227339b8 100644
--- a/src/libide/foundry/ide-test-manager.c
+++ b/src/libide/foundry/ide-test-manager.c
@@ -1,6 +1,6 @@
 /* ide-test-manager.c
  *
- * Copyright 2017-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2017-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
@@ -22,17 +22,20 @@
 
 #include "config.h"
 
-#include <dazzle.h>
+#include <libpeas/peas.h>
+
+#include <libide-core.h>
 #include <libide-io.h>
 #include <libide-threading.h>
-#include <libpeas/peas.h>
 
 #include "ide-build-manager.h"
-#include "ide-pipeline.h"
 #include "ide-foundry-compat.h"
+#include "ide-pipeline.h"
+#include "ide-run-command.h"
+#include "ide-run-commands.h"
+#include "ide-run-manager.h"
+#include "ide-test.h"
 #include "ide-test-manager.h"
-#include "ide-test-private.h"
-#include "ide-test-provider.h"
 
 #define MAX_UNIT_TESTS 4
 
@@ -47,483 +50,201 @@
  *
  * You can access the test manager using ide_context_get_text_manager()
  * using the #IdeContext for the loaded project.
- *
- * Since: 3.32
  */
 
 struct _IdeTestManager
 {
-  IdeObject         parent_instance;
-
-  PeasExtensionSet *providers;
-  GPtrArray        *tests_by_provider;
-  GtkTreeStore     *tests_store;
-  GCancellable     *cancellable;
-  VtePty           *pty;
-  gint              child_pty;
-  gint              n_active;
+  IdeObject           parent_instance;
+  GtkFilterListModel *filtered;
+  IdeCachedListModel *tests;
+  VtePty             *pty;
 };
 
 typedef struct
 {
-  IdeTestProvider *provider;
-  GPtrArray       *tests;
-} TestsByProvider;
+  IdePipeline *pipeline;
+  GPtrArray   *tests;
+  VtePty      *pty;
+  guint        n_active;
+} RunAll;
+
+static void ide_test_manager_actions_test     (IdeTestManager *self,
+                                               GVariant       *param);
+static void ide_test_manager_actions_test_all (IdeTestManager *self,
+                                               GVariant       *param);
+
+IDE_DEFINE_ACTION_GROUP (IdeTestManager, ide_test_manager, {
+  { "test", ide_test_manager_actions_test, "s" },
+  { "test-all", ide_test_manager_actions_test_all },
+})
 
-typedef struct
-{
-  GQueue queue;
-  guint  n_active;
-} RunAllTaskData;
+G_DEFINE_FINAL_TYPE_WITH_CODE (IdeTestManager, ide_test_manager, IDE_TYPE_OBJECT,
+                               G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP, 
ide_test_manager_init_action_group))
 
 enum {
   PROP_0,
-  PROP_LOADING,
+  PROP_MODEL,
   N_PROPS
 };
 
-static void initable_iface_init              (GInitableIface *iface);
-static void ide_test_manager_actions_run_all (IdeTestManager *self,
-                                              GVariant       *param);
-static void ide_test_manager_actions_reload  (IdeTestManager *self,
-                                              GVariant       *param);
-static void ide_test_manager_actions_cancel  (IdeTestManager *self,
-                                              GVariant       *param);
-
-DZL_DEFINE_ACTION_GROUP (IdeTestManager, ide_test_manager, {
-  { "cancel", ide_test_manager_actions_cancel },
-  { "run-all", ide_test_manager_actions_run_all },
-  { "reload-tests", ide_test_manager_actions_reload },
-})
-
-G_DEFINE_FINAL_TYPE_WITH_CODE (IdeTestManager, ide_test_manager, IDE_TYPE_OBJECT,
-                         G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init)
-                         G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP,
-                                                ide_test_manager_init_action_group))
-
-static GParamSpec *properties [N_PROPS];
-
-static void
-tests_by_provider_free (gpointer data)
-{
-  TestsByProvider *info = data;
-
-  g_clear_pointer (&info->tests, g_ptr_array_unref);
-  g_clear_object (&info->provider);
-  g_slice_free (TestsByProvider, info);
-}
-
-static void
-ide_test_manager_destroy (IdeObject *object)
-{
-  IdeTestManager *self = (IdeTestManager *)object;
-
-  if (self->child_pty != -1)
-    {
-      close (self->child_pty);
-      self->child_pty = -1;
-    }
-
-  if (self->tests_store != NULL)
-    {
-      gtk_tree_store_clear (self->tests_store);
-      g_clear_object (&self->tests_store);
-    }
-
-  g_cancellable_cancel (self->cancellable);
-  g_clear_object (&self->cancellable);
-
-  g_clear_object (&self->providers);
-  g_clear_pointer (&self->tests_by_provider, g_ptr_array_unref);
-
-  g_clear_object (&self->pty);
-
-  IDE_OBJECT_CLASS (ide_test_manager_parent_class)->destroy (object);
-}
-
-static void
-ide_test_manager_get_property (GObject    *object,
-                               guint       prop_id,
-                               GValue     *value,
-                               GParamSpec *pspec)
-{
-  IdeTestManager *self = IDE_TEST_MANAGER (object);
-
-  switch (prop_id)
-    {
-    case PROP_LOADING:
-      g_value_set_boolean (value, ide_test_manager_get_loading (self));
-      break;
-
-    default:
-      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
-    }
-}
+static GParamSpec *properties[N_PROPS];
 
-static void
-ide_test_manager_class_init (IdeTestManagerClass *klass)
-{
-  GObjectClass *object_class = G_OBJECT_CLASS (klass);
-  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
-
-  object_class->get_property = ide_test_manager_get_property;
-
-  i_object_class->destroy = ide_test_manager_destroy;
-
-  /**
-   * IdeTestManager:loading:
-   *
-   * The "loading" property denotes if a test provider is busy loading
-   * tests in the background.
-   *
-   * Since: 3.32
-   */
-  properties [PROP_LOADING] =
-    g_param_spec_boolean ("loading",
-                          "Loading",
-                          "If a test provider is loading tests",
-                          FALSE,
-                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
-
-  g_object_class_install_properties (object_class, N_PROPS, properties);
-}
-
-static void
-ide_test_manager_init (IdeTestManager *self)
-{
-  self->child_pty = -1;
-  self->cancellable = g_cancellable_new ();
-  self->tests_by_provider = g_ptr_array_new_with_free_func (tests_by_provider_free);
-  self->tests_store = gtk_tree_store_new (2, G_TYPE_STRING, IDE_TYPE_TEST);
-
-  ide_test_manager_set_action_enabled (self, "cancel", FALSE);
-}
-
-static void
-ide_test_manager_locate_group (IdeTestManager *self,
-                               GtkTreeIter    *iter,
-                               const gchar    *group)
-{
-  g_assert (IDE_IS_TEST_MANAGER (self));
-  g_assert (iter != NULL);
-
-  if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (self->tests_store), iter))
-    {
-      do
-        {
-          g_autofree gchar *row_group = NULL;
-
-          gtk_tree_model_get (GTK_TREE_MODEL (self->tests_store), iter,
-                              IDE_TEST_COLUMN_GROUP, &row_group,
-                              -1);
-
-          if (ide_str_equal0 (row_group, group))
-            return;
-        }
-      while (gtk_tree_model_iter_next (GTK_TREE_MODEL (self->tests_store), iter));
-    }
-
-  /* TODO: Sort groups by name? */
-
-  gtk_tree_store_append (self->tests_store, iter, NULL);
-  gtk_tree_store_set (self->tests_store, iter,
-                      IDE_TEST_COLUMN_GROUP, group,
-                      -1);
-}
-
-static void
-ide_test_manager_test_notify_status (IdeTestManager *self,
-                                     GParamSpec     *pspec,
-                                     IdeTest        *test)
+static gboolean
+filter_tests_func (gpointer item,
+                   gpointer user_data)
 {
-  const gchar *group;
-  GtkTreeIter parent;
-  GtkTreeIter iter;
-
-  g_assert (IDE_IS_TEST_MANAGER (self));
-  g_assert (IDE_IS_TEST (test));
-
-  group = ide_test_get_group (test);
+  IdeRunCommand *run_command = item;
 
-  ide_test_manager_locate_group (self, &parent, group);
-
-  if (gtk_tree_model_iter_children (GTK_TREE_MODEL (self->tests_store), &iter, &parent))
-    {
-      do
-        {
-          g_autoptr(IdeTest) row_test = NULL;
-
-          gtk_tree_model_get (GTK_TREE_MODEL (self->tests_store), &iter,
-                              IDE_TEST_COLUMN_TEST, &row_test,
-                              -1);
-
-          if (row_test == test)
-            {
-              GtkTreePath *path;
-
-              path = gtk_tree_model_get_path (GTK_TREE_MODEL (self->tests_store), &iter);
-              gtk_tree_model_row_changed (GTK_TREE_MODEL (self->tests_store), path, &iter);
-              gtk_tree_path_free (path);
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_RUN_COMMAND (run_command));
 
-              break;
-            }
-        }
-      while (gtk_tree_model_iter_next (GTK_TREE_MODEL (self->tests_store), &iter));
-    }
+  return ide_run_command_get_kind (run_command) == IDE_RUN_COMMAND_KIND_TEST;
 }
 
 static void
-ide_test_manager_add_test (IdeTestManager        *self,
-                           const TestsByProvider *info,
-                           guint                  position,
-                           IdeTest               *test)
+run_all_free (RunAll *state)
 {
-  const gchar *group;
-  GtkTreeIter iter;
-  GtkTreeIter parent;
-
-  IDE_ENTRY;
-
-  g_assert (IDE_IS_TEST_MANAGER (self));
-  g_assert (info != NULL);
-  g_assert (IDE_IS_TEST (test));
-
-  g_ptr_array_insert (info->tests, position, g_object_ref (test));
-
-  group = ide_test_get_group (test);
+  g_assert (state != NULL);
+  g_assert (state->n_active == 0);
 
-  ide_test_manager_locate_group (self, &parent, group);
-  gtk_tree_store_append (self->tests_store, &iter, &parent);
-  gtk_tree_store_set (self->tests_store, &iter,
-                      IDE_TEST_COLUMN_GROUP, NULL,
-                      IDE_TEST_COLUMN_TEST, test,
-                      -1);
-
-  g_signal_connect_object (test,
-                           "notify::status",
-                           G_CALLBACK (ide_test_manager_test_notify_status),
-                           self,
-                           G_CONNECT_SWAPPED);
-
-  IDE_EXIT;
+  g_clear_pointer (&state->tests, g_ptr_array_unref);
+  g_clear_object (&state->pipeline);
+  g_clear_object (&state->pty);
+  g_slice_free (RunAll, state);
 }
 
 static void
-ide_test_manager_remove_test (IdeTestManager        *self,
-                              const TestsByProvider *info,
-                              IdeTest               *test)
+ide_test_manager_actions_test (IdeTestManager *self,
+                               GVariant       *param)
 {
-  const gchar *group;
-  GtkTreeIter iter;
-  GtkTreeIter parent;
+  GListModel *tests;
+  const char *test_id;
+  guint n_items;
 
   IDE_ENTRY;
 
+  g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (IDE_IS_TEST_MANAGER (self));
-  g_assert (info != NULL);
-  g_assert (IDE_IS_TEST (test));
-
-  group = ide_test_get_group (test);
+  g_assert (g_variant_is_of_type (param, G_VARIANT_TYPE_STRING));
 
-  ide_test_manager_locate_group (self, &parent, group);
+  test_id = g_variant_get_string (param, NULL);
+  tests = ide_test_manager_list_tests (self);
+  n_items = g_list_model_get_n_items (tests);
 
-  if (gtk_tree_model_iter_children (GTK_TREE_MODEL (self->tests_store), &iter, &parent))
+  for (guint i = 0; i < n_items; i++)
     {
-      do
+      g_autoptr(IdeTest) test = g_list_model_get_item (tests, i);
+
+      if (ide_str_equal0 (test_id, ide_test_get_id (test)))
         {
-          g_autoptr(IdeTest) row = NULL;
-
-          gtk_tree_model_get (GTK_TREE_MODEL (self->tests_store), &iter,
-                              IDE_TEST_COLUMN_TEST, &row,
-                              -1);
-
-          if (row == test)
-            {
-              g_signal_handlers_disconnect_by_func (test,
-                                                    G_CALLBACK (ide_test_manager_test_notify_status),
-                                                    self);
-              gtk_tree_store_remove (self->tests_store, &iter);
-              break;
-            }
+          ide_test_manager_run_async (self, test, NULL, NULL, NULL);
+          IDE_EXIT;
         }
-      while (gtk_tree_model_iter_next (GTK_TREE_MODEL (self->tests_store), &iter));
     }
 
-  g_ptr_array_remove (info->tests, test);
-
   IDE_EXIT;
 }
 
 static void
-ide_test_manager_provider_items_changed (IdeTestManager  *self,
-                                         guint            position,
-                                         guint            removed,
-                                         guint            added,
-                                         IdeTestProvider *provider)
+ide_test_manager_actions_test_all (IdeTestManager *self,
+                                   GVariant       *param)
 {
   IDE_ENTRY;
 
+  g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (IDE_IS_TEST_MANAGER (self));
-  g_assert (IDE_IS_TEST_PROVIDER (provider));
-
-  for (guint i = 0; i < self->tests_by_provider->len; i++)
-    {
-      const TestsByProvider *info = g_ptr_array_index (self->tests_by_provider, i);
+  g_assert (param == NULL);
 
-      if (info->provider == provider)
-        {
-          /* Remove tests from cache that were deleted */
-          for (guint j = 0; j < removed; j++)
-            {
-              IdeTest *test = g_ptr_array_index (info->tests, position);
-
-              g_assert (IDE_IS_TEST (test));
-              ide_test_manager_remove_test (self, info, test);
-            }
-
-          /* Add tests to cache that were added */
-          for (guint j = 0; j < added; j++)
-            {
-              g_autoptr(IdeTest) test = NULL;
-
-              test = g_list_model_get_item (G_LIST_MODEL (provider), position + j);
-              g_assert (IDE_IS_TEST (test));
-              ide_test_manager_add_test (self, info, position + j, test);
-            }
-        }
-    }
+  ide_test_manager_run_all_async (self, NULL, NULL, NULL);
 
   IDE_EXIT;
 }
 
-static void
-ide_test_manager_provider_notify_loading (IdeTestManager  *self,
-                                          GParamSpec      *pspec,
-                                          IdeTestProvider *provider)
+static gpointer
+map_run_command_to_test (gpointer item,
+                         gpointer user_data)
 {
-  g_assert (IDE_IS_TEST_MANAGER (self));
-  g_assert (IDE_IS_TEST_PROVIDER (provider));
+  g_autoptr(IdeRunCommand) run_command = item;
 
-  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_LOADING]);
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_RUN_COMMAND (run_command));
+
+  return ide_test_new (run_command);
 }
 
 static void
-ide_test_manager_provider_added (PeasExtensionSet *set,
-                                 PeasPluginInfo   *plugin_info,
-                                 PeasExtension    *exten,
-                                 gpointer          user_data)
+ide_test_manager_dispose (GObject *object)
 {
-  IdeTestManager *self = user_data;
-  IdeTestProvider *provider = (IdeTestProvider *)exten;
-  TestsByProvider *tests;
-  guint len;
-
-  IDE_ENTRY;
-
-  g_assert (PEAS_IS_EXTENSION_SET (set));
-  g_assert (plugin_info != NULL);
-  g_assert (IDE_IS_TEST_PROVIDER (provider));
-  g_assert (G_IS_LIST_MODEL (provider));
-  g_assert (IDE_IS_TEST_MANAGER (self));
-
-  tests = g_slice_new0 (TestsByProvider);
-  tests->provider = g_object_ref (provider);
-  tests->tests = g_ptr_array_new_with_free_func (g_object_unref);
-  g_ptr_array_add (self->tests_by_provider, tests);
-
-  g_signal_connect_swapped (provider,
-                            "items-changed",
-                            G_CALLBACK (ide_test_manager_provider_items_changed),
-                            self);
-  g_signal_connect_swapped (provider,
-                            "notify::loading",
-                            G_CALLBACK (ide_test_manager_provider_notify_loading),
-                            self);
-
-  len = g_list_model_get_n_items (G_LIST_MODEL (provider));
-  ide_test_manager_provider_items_changed (self, 0, 0, len, provider);
+  IdeTestManager *self = (IdeTestManager *)object;
 
-  ide_object_append (IDE_OBJECT (self), IDE_OBJECT (provider));
+  g_clear_object (&self->pty);
+  g_clear_object (&self->filtered);
+  g_clear_object (&self->tests);
 
-  IDE_EXIT;
+  G_OBJECT_CLASS (ide_test_manager_parent_class)->dispose (object);
 }
 
 static void
-ide_test_manager_provider_removed (PeasExtensionSet *set,
-                                   PeasPluginInfo   *plugin_info,
-                                   PeasExtension    *exten,
-                                   gpointer          user_data)
+ide_test_manager_get_property (GObject    *object,
+                               guint       prop_id,
+                               GValue     *value,
+                               GParamSpec *pspec)
 {
-  IdeTestManager *self = user_data;
-  IdeTestProvider *provider = (IdeTestProvider *)exten;
-
-  IDE_ENTRY;
-
-  g_assert (PEAS_IS_EXTENSION_SET (set));
-  g_assert (plugin_info != NULL);
-  g_assert (IDE_IS_TEST_PROVIDER (provider));
-  g_assert (IDE_IS_TEST_MANAGER (self));
+  IdeTestManager *self = IDE_TEST_MANAGER (object);
 
-  for (guint i = 0; i < self->tests_by_provider->len; i++)
+  switch (prop_id)
     {
-      const TestsByProvider *info = g_ptr_array_index (self->tests_by_provider, i);
+    case PROP_MODEL:
+      g_value_set_object (value, ide_test_manager_list_tests (self));
+      break;
 
-      if (info->provider == provider)
-        {
-          g_ptr_array_remove_index (self->tests_by_provider, i);
-          break;
-        }
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
     }
-
-  g_signal_handlers_disconnect_by_func (provider,
-                                        G_CALLBACK (ide_test_manager_provider_items_changed),
-                                        self);
-  g_signal_handlers_disconnect_by_func (provider,
-                                        G_CALLBACK (ide_test_manager_provider_notify_loading),
-                                        self);
-
-  ide_object_destroy (IDE_OBJECT (provider));
-
-  IDE_EXIT;
 }
 
-static gboolean
-ide_test_manager_initiable_init (GInitable     *initable,
-                                 GCancellable  *cancellable,
-                                 GError       **error)
+static void
+ide_test_manager_class_init (IdeTestManagerClass *klass)
 {
-  IdeTestManager *self = (IdeTestManager *)initable;
-
-  IDE_ENTRY;
-
-  g_assert (IDE_IS_TEST_MANAGER (self));
-  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
-
-  self->providers = peas_extension_set_new (peas_engine_get_default (),
-                                            IDE_TYPE_TEST_PROVIDER,
-                                            NULL);
-
-  g_signal_connect (self->providers,
-                    "extension-added",
-                    G_CALLBACK (ide_test_manager_provider_added),
-                    self);
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
 
-  g_signal_connect (self->providers,
-                    "extension-removed",
-                    G_CALLBACK (ide_test_manager_provider_removed),
-                    self);
+  object_class->dispose = ide_test_manager_dispose;
+  object_class->get_property = ide_test_manager_get_property;
 
-  peas_extension_set_foreach (self->providers,
-                              ide_test_manager_provider_added,
-                              self);
+  /**
+   * IdeTestManager:model:
+   *
+   * The "model" property contains a #GListModel of #IdeTest.
+   *
+   * Fetching this property will not cause the #GListModel to be
+   * populated. That is only done by calling ide_test_manager_list_tests().
+   *
+   * This may be a more convenient way to get access to the model when you
+   * do not want the tests to autopopulate just to be bound to UI elements.
+   */
+  properties [PROP_MODEL] =
+    g_param_spec_object ("model", NULL, NULL,
+                         G_TYPE_LIST_MODEL,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
 
-  IDE_RETURN (TRUE);
+  g_object_class_install_properties (object_class, N_PROPS, properties);
 }
 
 static void
-initable_iface_init (GInitableIface *iface)
+ide_test_manager_init (IdeTestManager *self)
 {
-  iface->init = ide_test_manager_initiable_init;
+  GtkCustomFilter *filter;
+  GtkMapListModel *map;
+
+  self->pty = vte_pty_new_sync (VTE_PTY_DEFAULT, NULL, NULL);
+  vte_pty_set_utf8 (self->pty, TRUE, NULL);
+
+  filter = gtk_custom_filter_new (filter_tests_func, NULL, NULL);
+  self->filtered = gtk_filter_list_model_new (NULL, GTK_FILTER (filter));
+  map = gtk_map_list_model_new (g_object_ref (G_LIST_MODEL (self->filtered)),
+                                map_run_command_to_test,
+                                NULL, NULL);
+  self->tests = ide_cached_list_model_new (G_LIST_MODEL (map));
 }
 
 static void
@@ -531,43 +252,49 @@ ide_test_manager_run_all_cb (GObject      *object,
                              GAsyncResult *result,
                              gpointer      user_data)
 {
-  IdeTestManager *self = (IdeTestManager *)object;
-  g_autoptr(GTask) task = user_data;
+  IdeTest *test = (IdeTest *)object;
+  g_autoptr(IdeTask) task = user_data;
   g_autoptr(GError) error = NULL;
-  g_autoptr(IdeTest) test = NULL;
-  RunAllTaskData *task_data;
   GCancellable *cancellable;
+  RunAll *state;
 
   IDE_ENTRY;
 
-  g_assert (IDE_IS_TEST_MANAGER (self));
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TEST (test));
   g_assert (G_IS_ASYNC_RESULT (result));
-  g_assert (G_IS_TASK (task));
+  g_assert (IDE_IS_TASK (task));
 
-  cancellable = g_task_get_cancellable (task);
-  task_data = g_task_get_task_data (task);
-  g_assert (task_data != NULL);
-  g_assert (task_data->n_active > 0);
+  cancellable = ide_task_get_cancellable (task);
+  state = ide_task_get_task_data (task);
 
-  if (!ide_test_manager_run_finish (self, result, &error))
-    g_message ("%s", error->message);
+  g_assert (state != NULL);
+  g_assert (state->n_active > 0);
+  g_assert (!state->pty || VTE_IS_PTY (state->pty));
+  g_assert (state->tests != NULL);
 
-  test = g_queue_pop_head (&task_data->queue);
+  if (!ide_test_run_finish (test, result, &error))
+    g_message ("%s", error->message);
 
-  if (test != NULL)
+  if (state->tests->len > 0 &&
+      !g_cancellable_is_cancelled (cancellable))
     {
-      task_data->n_active++;
-      ide_test_manager_run_async (self,
-                                  test,
-                                  cancellable,
-                                  ide_test_manager_run_all_cb,
-                                  g_object_ref (task));
+      g_autoptr(IdeTest) next_test = g_ptr_array_steal_index (state->tests, state->tests->len-1);
+
+      state->n_active++;
+
+      ide_test_run_async (next_test,
+                          state->pipeline,
+                          state->pty,
+                          cancellable,
+                          ide_test_manager_run_all_cb,
+                          g_object_ref (task));
     }
 
-  task_data->n_active--;
+  state->n_active--;
 
-  if (task_data->n_active == 0)
-    g_task_return_boolean (task, TRUE);
+  if (state->n_active == 0)
+    ide_task_return_boolean (task, TRUE);
 
   IDE_EXIT;
 }
@@ -586,8 +313,6 @@ ide_test_manager_run_all_cb (GObject      *object,
  *
  * Note that the individual test result information will be attached
  * to the specific #IdeTest instances.
- *
- * Since: 3.32
  */
 void
 ide_test_manager_run_all_async (IdeTestManager      *self,
@@ -595,55 +320,67 @@ ide_test_manager_run_all_async (IdeTestManager      *self,
                                 GAsyncReadyCallback  callback,
                                 gpointer             user_data)
 {
-  g_autoptr(GTask) task = NULL;
-  RunAllTaskData *task_data;
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GPtrArray) ar = NULL;
+  IdeBuildManager *build_manager;
+  IdePipeline *pipeline;
+  GListModel *tests;
+  IdeContext *context;
+  RunAll *state;
+  guint n_items;
 
   IDE_ENTRY;
 
   g_return_if_fail (IDE_IS_TEST_MANAGER (self));
   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
 
-  task = g_task_new (self, cancellable, callback, user_data);
-  g_task_set_priority (task, G_PRIORITY_LOW);
-  g_task_set_source_tag (task, ide_test_manager_run_all_async);
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_test_manager_run_all_async);
 
-  task_data = g_new0 (RunAllTaskData, 1);
-  g_task_set_task_data (task, task_data, g_free);
+  context = ide_object_get_context (IDE_OBJECT (self));
+  build_manager = ide_build_manager_from_context (context);
+  pipeline = ide_build_manager_get_pipeline (build_manager);
 
-  for (guint i = 0; i < self->tests_by_provider->len; i++)
+  if (pipeline == NULL)
     {
-      TestsByProvider *info = g_ptr_array_index (self->tests_by_provider, i);
-
-      for (guint j = 0; j < info->tests->len; j++)
-        {
-          IdeTest *test = g_ptr_array_index (info->tests, j);
-
-          g_queue_push_tail (&task_data->queue, g_object_ref (test));
-        }
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_NOT_INITIALIZED,
+                                 "Cannot run test until pipeline is ready");
+      IDE_EXIT;
     }
 
-  task_data->n_active = MIN (MAX_UNIT_TESTS, task_data->queue.length);
+  tests = ide_test_manager_list_tests (self);
+  n_items = g_list_model_get_n_items (tests);
 
-  if (task_data->n_active == 0)
-    {
-      g_task_return_boolean (task, TRUE);
-      IDE_EXIT;
-    }
+  ar = g_ptr_array_new_with_free_func (g_object_unref);
+  for (guint i = n_items; i > 0; i--)
+    g_ptr_array_add (ar, g_list_model_get_item (tests, i-1));
+
+  state = g_slice_new0 (RunAll);
+  state->tests = g_ptr_array_ref (ar);
+  state->pipeline = g_object_ref (pipeline);
+  state->pty = g_object_ref (self->pty);
+  state->n_active = 0;
+  ide_task_set_task_data (task, state, run_all_free);
 
-  for (guint i = 0; i < MAX_UNIT_TESTS; i++)
+  for (guint i = 0; i < MAX_UNIT_TESTS && ar->len > 0; i++)
     {
-      g_autoptr(IdeTest) test = g_queue_pop_head (&task_data->queue);
+      g_autoptr(IdeTest) test = g_ptr_array_steal_index (state->tests, ar->len-1);
 
-      if (test == NULL)
-        break;
+      state->n_active++;
 
-      ide_test_manager_run_async (self,
-                                  test,
-                                  cancellable,
-                                  ide_test_manager_run_all_cb,
-                                  g_object_ref (task));
+      ide_test_run_async (test,
+                          state->pipeline,
+                          state->pty,
+                          cancellable,
+                          ide_test_manager_run_all_cb,
+                          g_object_ref (task));
     }
 
+  if (state->n_active == 0)
+    ide_task_return_boolean (task, TRUE);
+
   IDE_EXIT;
 }
 
@@ -660,8 +397,6 @@ ide_test_manager_run_all_async (IdeTestManager      *self,
  * attached to the #IdeTest instances.
  *
  * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
- *
- * Since: 3.32
  */
 gboolean
 ide_test_manager_run_all_finish (IdeTestManager  *self,
@@ -673,46 +408,33 @@ ide_test_manager_run_all_finish (IdeTestManager  *self,
   IDE_ENTRY;
 
   g_return_val_if_fail (IDE_IS_TEST_MANAGER (self), FALSE);
-  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
 
-  ret = g_task_propagate_boolean (G_TASK (result), error);
+  ret = ide_task_propagate_boolean (IDE_TASK (result), error);
 
   IDE_RETURN (ret);
 }
 
-static void
-run_task_completed_cb (IdeTestManager *self,
-                       GParamSpec     *pspec,
-                       IdeTask        *task)
-{
-  g_assert (IDE_IS_TEST_MANAGER (self));
-  g_assert (G_IS_TASK (task));
-  g_assert (self->n_active > 0);
-
-  self->n_active--;
-
-  ide_test_manager_set_action_enabled (self, "cancel", self->n_active > 0);
-}
-
 static void
 ide_test_manager_run_cb (GObject      *object,
                          GAsyncResult *result,
                          gpointer      user_data)
 {
-  IdeTestProvider *provider = (IdeTestProvider *)object;
-  g_autoptr(GTask) task = user_data;
+  IdeTest *test = (IdeTest *)object;
+  g_autoptr(IdeTask) task = user_data;
   g_autoptr(GError) error = NULL;
 
   IDE_ENTRY;
 
-  g_assert (IDE_IS_TEST_PROVIDER (provider));
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TEST (test));
   g_assert (G_IS_ASYNC_RESULT (result));
-  g_assert (G_IS_TASK (task));
+  g_assert (IDE_IS_TASK (task));
 
-  if (!ide_test_provider_run_finish (provider, result, &error))
-    g_task_return_error (task, g_steal_pointer (&error));
+  if (!ide_test_run_finish (test, result, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
   else
-    g_task_return_boolean (task, TRUE);
+    ide_task_return_boolean (task, TRUE);
 
   IDE_EXIT;
 }
@@ -729,8 +451,6 @@ ide_test_manager_run_cb (GObject      *object,
  *
  * The caller can access the result of the operation from @callback
  * by calling ide_test_manager_run_finish() with the provided result.
- *
- * Since: 3.32
  */
 void
 ide_test_manager_run_async (IdeTestManager      *self,
@@ -739,10 +459,9 @@ ide_test_manager_run_async (IdeTestManager      *self,
                             GAsyncReadyCallback  callback,
                             gpointer             user_data)
 {
-  g_autoptr(GTask) task = NULL;
-  IdePipeline *pipeline;
-  IdeTestProvider *provider;
+  g_autoptr(IdeTask) task = NULL;
   IdeBuildManager *build_manager;
+  IdePipeline *pipeline;
   IdeContext *context;
 
   IDE_ENTRY;
@@ -751,51 +470,25 @@ ide_test_manager_run_async (IdeTestManager      *self,
   g_return_if_fail (IDE_IS_TEST (test));
   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
 
-  task = g_task_new (self, cancellable, callback, user_data);
-  g_task_set_priority (task, G_PRIORITY_LOW);
-  g_task_set_source_tag (task, ide_test_manager_run_async);
-
-  self->n_active++;
-  g_signal_connect_object (task,
-                           "notify::completed",
-                           G_CALLBACK (run_task_completed_cb),
-                           self,
-                           G_CONNECT_SWAPPED);
-  ide_test_manager_set_action_enabled (self, "cancel", TRUE);
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_test_manager_run_async);
 
   context = ide_object_get_context (IDE_OBJECT (self));
   build_manager = ide_build_manager_from_context (context);
   pipeline = ide_build_manager_get_pipeline (build_manager);
 
   if (pipeline == NULL)
-    {
-      g_task_return_new_error (task,
+    ide_task_return_new_error (task,
                                G_IO_ERROR,
-                               G_IO_ERROR_FAILED,
+                               G_IO_ERROR_NOT_INITIALIZED,
                                "Pipeline is not ready, cannot run test");
-      IDE_EXIT;
-    }
-
-  provider = _ide_test_get_provider (test);
-
-  if (self->pty == NULL)
-    {
-      g_autoptr(GError) error = NULL;
-
-      if (!(self->pty = vte_pty_new_sync (VTE_PTY_DEFAULT, cancellable, &error)))
-        {
-          g_task_return_error (task, g_steal_pointer (&error));
-          IDE_EXIT;
-        }
-    }
-
-  ide_test_provider_run_async (provider,
-                               test,
-                               pipeline,
-                               self->pty,
-                               cancellable,
-                               ide_test_manager_run_cb,
-                               g_steal_pointer (&task));
+  else
+    ide_test_run_async (test,
+                        pipeline,
+                        self->pty,
+                        cancellable,
+                        ide_test_manager_run_cb,
+                        g_steal_pointer (&task));
 
   IDE_EXIT;
 }
@@ -814,8 +507,6 @@ ide_test_manager_run_async (IdeTestManager      *self,
  *
  * Returns: %TRUE if the test was executed; otherwise %FALSE
  *   and @error is set.
- *
- * Since: 3.32
  */
 gboolean
 ide_test_manager_run_finish (IdeTestManager  *self,
@@ -827,258 +518,13 @@ ide_test_manager_run_finish (IdeTestManager  *self,
   IDE_ENTRY;
 
   g_return_val_if_fail (IDE_IS_TEST_MANAGER (self), FALSE);
-  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
 
-  ret = g_task_propagate_boolean (G_TASK (result), error);
+  ret = ide_task_propagate_boolean (IDE_TASK (result), error);
 
   IDE_RETURN (ret);
 }
 
-static void
-ide_test_manager_actions_run_all (IdeTestManager *self,
-                                  GVariant       *param)
-{
-  g_assert (IDE_IS_TEST_MANAGER (self));
-
-  ide_test_manager_run_all_async (self, NULL, NULL, NULL);
-}
-
-static void
-ide_test_manager_actions_reload (IdeTestManager *self,
-                                 GVariant       *param)
-{
-  g_assert (IDE_IS_TEST_MANAGER (self));
-
-  gtk_tree_store_clear (self->tests_store);
-
-  for (guint i = 0; i < self->tests_by_provider->len; i++)
-    {
-      const TestsByProvider *info = g_ptr_array_index (self->tests_by_provider, i);
-
-      ide_test_provider_reload (info->provider);
-    }
-}
-
-GtkTreeModel *
-_ide_test_manager_get_model (IdeTestManager *self)
-{
-  g_return_val_if_fail (IDE_IS_TEST_MANAGER (self), NULL);
-
-  return GTK_TREE_MODEL (self->tests_store);
-}
-
-static void
-ide_test_manager_get_loading_cb (PeasExtensionSet *set,
-                                 PeasPluginInfo   *plugin_info,
-                                 PeasExtension    *exten,
-                                 gpointer          user_data)
-{
-  IdeTestProvider *provider = (IdeTestProvider *)exten;
-  gboolean *loading = user_data;
-
-  g_assert (PEAS_IS_EXTENSION_SET (set));
-  g_assert (plugin_info != NULL);
-  g_assert (IDE_IS_TEST_PROVIDER (provider));
-  g_assert (loading != NULL);
-
-  *loading |= ide_test_provider_get_loading (provider);
-}
-
-gboolean
-ide_test_manager_get_loading (IdeTestManager *self)
-{
-  gboolean loading = FALSE;
-
-  g_return_val_if_fail (IDE_IS_TEST_MANAGER (self), FALSE);
-
-  peas_extension_set_foreach (self->providers,
-                              ide_test_manager_get_loading_cb,
-                              &loading);
-
-  return loading;
-}
-
-/**
- * ide_test_manager_get_tests:
- * @self: a #IdeTestManager
- * @path: (nullable): the path to the test or %NULL for the root path
- *
- * Locates and returns any #IdeTest that is found as a direct child
- * of @path.
- *
- * Returns: (transfer full) (element-type IdeTest): an array of #IdeTest
- *
- * Since: 3.32
- */
-GPtrArray *
-ide_test_manager_get_tests (IdeTestManager *self,
-                            const gchar    *path)
-{
-  GPtrArray *ret;
-  GtkTreeIter iter;
-
-  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
-  g_return_val_if_fail (IDE_IS_TEST_MANAGER (self), NULL);
-
-  ret = g_ptr_array_new ();
-
-  if (path == NULL)
-    {
-      if (!gtk_tree_model_get_iter_first (GTK_TREE_MODEL (self->tests_store), &iter))
-        goto failure;
-    }
-  else
-    {
-      GtkTreeIter parent;
-
-      ide_test_manager_locate_group (self, &parent, path);
-
-      if (!gtk_tree_model_iter_children (GTK_TREE_MODEL (self->tests_store), &iter, &parent))
-        goto failure;
-    }
-
-  do
-    {
-      IdeTest *test = NULL;
-
-      gtk_tree_model_get (GTK_TREE_MODEL (self->tests_store), &iter,
-                          IDE_TEST_COLUMN_TEST, &test,
-                          -1);
-      if (test != NULL)
-        g_ptr_array_add (ret, g_steal_pointer (&test));
-    }
-  while (gtk_tree_model_iter_next (GTK_TREE_MODEL (self->tests_store), &iter));
-
-failure:
-  return g_steal_pointer (&ret);
-}
-
-/**
- * ide_test_manager_get_folders:
- * @self: a #IdeTestManager
- * @path: (nullable): the path to the test or %NULL for the root path
- *
- * Gets the sub-paths of @path that are not individual tests.
- *
- * Returns: (transfer full) (array zero-terminated=1): an array of strings
- *   describing available sub-paths to @path.
- *
- * Since: 3.32
- */
-gchar **
-ide_test_manager_get_folders (IdeTestManager *self,
-                              const gchar    *path)
-{
-  static const gchar *empty[] = { NULL };
-  GPtrArray *ret;
-  GtkTreeIter iter;
-
-  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
-  g_return_val_if_fail (IDE_IS_TEST_MANAGER (self), NULL);
-
-  ret = g_ptr_array_new ();
-
-  if (path == NULL)
-    {
-      if (!gtk_tree_model_get_iter_first (GTK_TREE_MODEL (self->tests_store), &iter))
-        return g_strdupv ((gchar **)empty);
-    }
-  else
-    {
-      GtkTreeIter parent;
-
-      ide_test_manager_locate_group (self, &parent, path);
-
-      if (!gtk_tree_model_iter_children (GTK_TREE_MODEL (self->tests_store), &iter, &parent))
-        return g_strdupv ((gchar **)empty);
-    }
-
-  do
-    {
-      gchar *group = NULL;
-
-      gtk_tree_model_get (GTK_TREE_MODEL (self->tests_store), &iter,
-                          IDE_TEST_COLUMN_GROUP, &group,
-                          -1);
-      if (group != NULL)
-        g_ptr_array_add (ret, g_steal_pointer (&group));
-    }
-  while (gtk_tree_model_iter_next (GTK_TREE_MODEL (self->tests_store), &iter));
-
-  g_ptr_array_add (ret, NULL);
-
-  return (gchar **)g_ptr_array_free (ret, FALSE);
-}
-
-static void
-ide_test_manager_ensure_loaded_cb (IdeTestManager *self,
-                                   GParamSpec     *pspec,
-                                   IdeTask        *task)
-{
-  g_assert (IDE_IS_MAIN_THREAD ());
-  g_assert (IDE_IS_TEST_MANAGER (self));
-  g_assert (IDE_IS_TASK (task));
-
-  if (!ide_test_manager_get_loading (self))
-    {
-      g_signal_handlers_disconnect_by_func (self,
-                                            G_CALLBACK (ide_test_manager_ensure_loaded_cb),
-                                            task);
-      ide_task_return_boolean (task, TRUE);
-    }
-}
-
-/**
- * ide_test_manager_ensure_loaded_async:
- * @self: a #IdeTestManager
- *
- * Calls @callback after the test manager has loaded tests.
- *
- * If the test manager has already loaded tests, then @callback will
- * be called after returning to the main loop.
- *
- * Since: 3.32
- */
-void
-ide_test_manager_ensure_loaded_async (IdeTestManager      *self,
-                                      GCancellable        *cancellable,
-                                      GAsyncReadyCallback  callback,
-                                      gpointer             user_data)
-{
-  g_autoptr(IdeTask) task = NULL;
-
-  g_return_if_fail (IDE_IS_TEST_MANAGER (self));
-  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
-
-  task = ide_task_new (self, cancellable, callback, user_data);
-  ide_task_set_source_tag (task, ide_test_manager_ensure_loaded_async);
-
-  if (ide_test_manager_get_loading (self))
-    {
-      g_signal_connect_data (self,
-                             "notify::loading",
-                             G_CALLBACK (ide_test_manager_ensure_loaded_cb),
-                             g_steal_pointer (&task),
-                             (GClosureNotify)g_object_unref,
-                             0);
-      return;
-    }
-
-  ide_task_return_boolean (task, TRUE);
-}
-
-gboolean
-ide_test_manager_ensure_loaded_finish (IdeTestManager  *self,
-                                       GAsyncResult    *result,
-                                       GError         **error)
-{
-  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
-  g_return_val_if_fail (IDE_IS_TEST_MANAGER (self), FALSE);
-  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
-
-  return ide_task_propagate_boolean (IDE_TASK (result), error);
-}
-
 /**
  * ide_test_manager_get_pty:
  * @self: a #IdeTestManager
@@ -1086,70 +532,42 @@ ide_test_manager_ensure_loaded_finish (IdeTestManager  *self,
  * Gets the #VtePty to use for running unit tests.
  *
  * Returns: (transfer none): a #VtePty
- *
- * Since: 3.32
  */
 VtePty *
 ide_test_manager_get_pty (IdeTestManager *self)
 {
   g_return_val_if_fail (IDE_IS_TEST_MANAGER (self), NULL);
 
-  if (self->pty == NULL)
-    self->pty = vte_pty_new_sync (VTE_PTY_DEFAULT, NULL, NULL);
-
   return self->pty;
 }
 
 /**
- * ide_test_manager_open_pty:
+ * ide_test_manager_list_tests:
  * @self: a #IdeTestManager
  *
- * Gets a FD that maps to the child side of the PTY device.
+ * Gets a #GListModel of #IdeTest.
  *
- * Returns: a new FD or -1 on failure
+ * This will return a #GListModel immediately, but that list may not complete
+ * until some time in the future based on how quickly various
+ * #IdeRunCommandProvider return commands.
  *
- * Since: 3.34
+ * Returns: (transfer none): an #GListModel of #IdeTest
  */
-gint
-ide_test_manager_open_pty (IdeTestManager *self)
+GListModel *
+ide_test_manager_list_tests (IdeTestManager *self)
 {
-  g_return_val_if_fail (IDE_IS_TEST_MANAGER (self), -1);
-
-  if (self->child_pty == -1)
-    {
-      VtePty *pty = ide_test_manager_get_pty (self);
-      self->child_pty = ide_pty_intercept_create_slave (vte_pty_get_fd (pty), TRUE);
-    }
-
-  return dup (self->child_pty);
-}
+  IDE_ENTRY;
 
-/**
- * ide_test_manager_get_cancellable:
- * @self: a #IdeTestManager
- *
- * Gets the cancellable for the test manager which will be cancelled
- * when the cancel action is called.
- *
- * Returns: (transfer none): a #GCancellable
- *
- * Since: 3.34
- */
-GCancellable *
-ide_test_manager_get_cancellable (IdeTestManager *self)
-{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
   g_return_val_if_fail (IDE_IS_TEST_MANAGER (self), NULL);
 
-  return self->cancellable;
-}
+  if (gtk_filter_list_model_get_model (self->filtered) == NULL)
+    {
+      IdeContext *context = ide_object_get_context (IDE_OBJECT (self));
+      IdeRunCommands *run_commands = ide_run_commands_from_context (context);
 
-static void
-ide_test_manager_actions_cancel (IdeTestManager *self,
-                                 GVariant       *param)
-{
-  g_assert (IDE_IS_TEST_MANAGER (self));
+      gtk_filter_list_model_set_model (self->filtered, G_LIST_MODEL (run_commands));
+    }
 
-  g_cancellable_cancel (self->cancellable);
-  g_clear_object (&self->cancellable);
-  self->cancellable = g_cancellable_new ();
+  IDE_RETURN (G_LIST_MODEL (self->tests));
 }
diff --git a/src/libide/foundry/ide-test-manager.h b/src/libide/foundry/ide-test-manager.h
index edb3e28b5..3e7c60e63 100644
--- a/src/libide/foundry/ide-test-manager.h
+++ b/src/libide/foundry/ide-test-manager.h
@@ -24,61 +24,43 @@
 # error "Only <libide-foundry.h> can be included directly."
 #endif
 
-#include <libide-core.h>
 #include <vte/vte.h>
 
+#include <libide-core.h>
+
 #include "ide-foundry-types.h"
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_TEST_MANAGER (ide_test_manager_get_type())
 
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 G_DECLARE_FINAL_TYPE (IdeTestManager, ide_test_manager, IDE, TEST_MANAGER, IdeObject)
 
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 IdeTestManager  *ide_test_manager_from_context         (IdeContext           *context);
-IDE_AVAILABLE_IN_3_32
-gboolean         ide_test_manager_get_loading          (IdeTestManager       *self);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 VtePty          *ide_test_manager_get_pty              (IdeTestManager       *self);
-IDE_AVAILABLE_IN_3_34
-gint             ide_test_manager_open_pty             (IdeTestManager       *self);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
+GListModel      *ide_test_manager_list_tests           (IdeTestManager       *self);
+IDE_AVAILABLE_IN_ALL
 void             ide_test_manager_run_async            (IdeTestManager       *self,
                                                         IdeTest              *test,
                                                         GCancellable         *cancellable,
                                                         GAsyncReadyCallback   callback,
                                                         gpointer              user_data);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 gboolean         ide_test_manager_run_finish           (IdeTestManager       *self,
                                                         GAsyncResult         *result,
                                                         GError              **error);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 void             ide_test_manager_run_all_async        (IdeTestManager       *self,
                                                         GCancellable         *cancellable,
                                                         GAsyncReadyCallback   callback,
                                                         gpointer              user_data);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 gboolean         ide_test_manager_run_all_finish       (IdeTestManager       *self,
                                                         GAsyncResult         *result,
                                                         GError              **error);
-IDE_AVAILABLE_IN_3_32
-GPtrArray       *ide_test_manager_get_tests            (IdeTestManager       *self,
-                                                        const gchar          *path);
-IDE_AVAILABLE_IN_3_32
-gchar          **ide_test_manager_get_folders          (IdeTestManager       *self,
-                                                        const gchar          *path);
-IDE_AVAILABLE_IN_3_34
-GCancellable    *ide_test_manager_get_cancellable      (IdeTestManager       *self);
-IDE_AVAILABLE_IN_3_32
-void             ide_test_manager_ensure_loaded_async  (IdeTestManager       *self,
-                                                        GCancellable         *cancellable,
-                                                        GAsyncReadyCallback   callback,
-                                                        gpointer              user_data);
-IDE_AVAILABLE_IN_3_32
-gboolean         ide_test_manager_ensure_loaded_finish (IdeTestManager       *self,
-                                                        GAsyncResult         *result,
-                                                        GError              **error);
 
 G_END_DECLS
diff --git a/src/libide/foundry/ide-test.c b/src/libide/foundry/ide-test.c
index f2980bbe4..55790ccc9 100644
--- a/src/libide/foundry/ide-test.c
+++ b/src/libide/foundry/ide-test.c
@@ -1,6 +1,6 @@
 /* ide-test.c
  *
- * Copyright 2017-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2017-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
@@ -22,57 +22,60 @@
 
 #include "config.h"
 
-#include "ide-foundry-enums.h"
+#include <libide-io.h>
+#include <libide-threading.h>
 
+#include "ide-run-command.h"
+#include "ide-run-context.h"
+#include "ide-foundry-enums.h"
+#include "ide-pipeline.h"
+#include "ide-runtime.h"
 #include "ide-test.h"
-#include "ide-test-private.h"
-#include "ide-test-provider.h"
 
-typedef struct
+struct _IdeTest
 {
-  /* Unowned references */
-  IdeTestProvider *provider;
-
-  /* Owned references */
-  gchar *display_name;
-  gchar *group;
-  gchar *id;
-
-  IdeTestStatus status;
-} IdeTestPrivate;
-
-G_DEFINE_TYPE_WITH_PRIVATE (IdeTest, ide_test, G_TYPE_OBJECT)
+  GObject        parent_instance;
+  IdeRunCommand *run_command;
+  IdeTestStatus  status;
+};
 
 enum {
   PROP_0,
-  PROP_DISPLAY_NAME,
-  PROP_GROUP,
+  PROP_ICON_NAME,
   PROP_ID,
+  PROP_RUN_COMMAND,
   PROP_STATUS,
+  PROP_TITLE,
   N_PROPS
 };
 
+G_DEFINE_FINAL_TYPE (IdeTest, ide_test, G_TYPE_OBJECT)
+
 static GParamSpec *properties [N_PROPS];
 
-IdeTest *
-ide_test_new (void)
+static void
+ide_test_set_status (IdeTest       *self,
+                     IdeTestStatus  status)
 {
-  return g_object_new (IDE_TYPE_TEST, NULL);
+  g_assert (IDE_IS_TEST (self));
+
+  if (status != self->status)
+    {
+      self->status = status;
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_STATUS]);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ICON_NAME]);
+    }
 }
 
 static void
-ide_test_finalize (GObject *object)
+ide_test_dispose (GObject *object)
 {
   IdeTest *self = (IdeTest *)object;
-  IdeTestPrivate *priv = ide_test_get_instance_private (self);
-
-  priv->provider = NULL;
 
-  g_clear_pointer (&priv->group, g_free);
-  g_clear_pointer (&priv->id, g_free);
-  g_clear_pointer (&priv->display_name, g_free);
+  g_clear_object (&self->run_command);
 
-  G_OBJECT_CLASS (ide_test_parent_class)->finalize (object);
+  G_OBJECT_CLASS (ide_test_parent_class)->dispose (object);
 }
 
 static void
@@ -85,22 +88,26 @@ ide_test_get_property (GObject    *object,
 
   switch (prop_id)
     {
-    case PROP_ID:
-      g_value_set_string (value, ide_test_get_id (self));
+    case PROP_ICON_NAME:
+      g_value_set_string (value, ide_test_get_icon_name (self));
       break;
 
-    case PROP_GROUP:
-      g_value_set_string (value, ide_test_get_group (self));
+    case PROP_ID:
+      g_value_set_string (value, ide_test_get_id (self));
       break;
 
-    case PROP_DISPLAY_NAME:
-      g_value_set_string (value, ide_test_get_display_name (self));
+    case PROP_RUN_COMMAND:
+      g_value_set_object (value, ide_test_get_run_command (self));
       break;
 
     case PROP_STATUS:
       g_value_set_enum (value, ide_test_get_status (self));
       break;
 
+    case PROP_TITLE:
+      g_value_set_string (value, ide_test_get_title (self));
+      break;
+
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
     }
@@ -116,20 +123,8 @@ ide_test_set_property (GObject      *object,
 
   switch (prop_id)
     {
-    case PROP_GROUP:
-      ide_test_set_group (self, g_value_get_string (value));
-      break;
-
-    case PROP_ID:
-      ide_test_set_id (self, g_value_get_string (value));
-      break;
-
-    case PROP_DISPLAY_NAME:
-      ide_test_set_display_name (self, g_value_get_string (value));
-      break;
-
-    case PROP_STATUS:
-      ide_test_set_status (self, g_value_get_enum (value));
+    case PROP_RUN_COMMAND:
+      g_set_object (&self->run_command, g_value_get_object (value));
       break;
 
     default:
@@ -142,69 +137,32 @@ ide_test_class_init (IdeTestClass *klass)
 {
   GObjectClass *object_class = G_OBJECT_CLASS (klass);
 
-  object_class->finalize = ide_test_finalize;
+  object_class->dispose = ide_test_dispose;
   object_class->get_property = ide_test_get_property;
   object_class->set_property = ide_test_set_property;
 
-  /**
-   * IdeTest:display_name:
-   *
-   * The "display-name" property contains the display name of the test as
-   * the user is expected to read in UI elements.
-   *
-   * Since: 3.32
-   */
-  properties [PROP_DISPLAY_NAME] =
-    g_param_spec_string ("display-name",
-                         "Name",
-                         "The display_name of the unit test",
-                         NULL,
-                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
-
-  /**
-   * IdeTest:id:
-   *
-   * The "id" property contains the unique identifier of the test.
-   *
-   * Since: 3.32
-   */
+  properties [PROP_ICON_NAME] =
+    g_param_spec_string ("icon-name", NULL, NULL, NULL,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
   properties [PROP_ID] =
-    g_param_spec_string ("id",
-                         "Id",
-                         "The unique identifier of the test",
-                         NULL,
-                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
-
-  /**
-   * IdeTest:group:
-   *
-   * The "group" property contains the name of the gruop the test belongs
-   * to, if any.
-   *
-   * Since: 3.32
-   */
-  properties [PROP_GROUP] =
-    g_param_spec_string ("group",
-                         "Group",
-                         "The name of the group the test belongs to, if any",
-                         NULL,
-                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
-
-  /**
-   * IdeTest::status:
-   *
-   * The "status" property contains the status of the test, updated by
-   * providers when they have run the test.
-   *
-   * Since: 3.32
-   */
+    g_param_spec_string ("id", NULL, NULL, NULL,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_RUN_COMMAND] =
+    g_param_spec_object ("run-command", NULL, NULL,
+                         IDE_TYPE_RUN_COMMAND,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
   properties [PROP_STATUS] =
-    g_param_spec_enum ("status",
-                       "Status",
-                       "The status of the test",
+    g_param_spec_enum ("status", NULL, NULL,
                        IDE_TYPE_TEST_STATUS,
                        IDE_TEST_STATUS_NONE,
-                       (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+                       (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title", NULL, NULL, NULL,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
 
   g_object_class_install_properties (object_class, N_PROPS, properties);
 }
@@ -214,216 +172,195 @@ ide_test_init (IdeTest *self)
 {
 }
 
-IdeTestProvider *
-_ide_test_get_provider (IdeTest *self)
+IdeTest *
+ide_test_new (IdeRunCommand *run_command)
 {
-  IdeTestPrivate *priv = ide_test_get_instance_private (self);
+  g_return_val_if_fail (IDE_IS_RUN_COMMAND (run_command), NULL);
+
+  return g_object_new (IDE_TYPE_TEST,
+                       "run-command", run_command,
+                       NULL);
+}
 
+const char *
+ide_test_get_id (IdeTest *self)
+{
   g_return_val_if_fail (IDE_IS_TEST (self), NULL);
+  g_return_val_if_fail (IDE_IS_RUN_COMMAND (self->run_command), NULL);
 
-  return priv->provider;
+  return ide_run_command_get_id (self->run_command);
 }
 
-void
-_ide_test_set_provider (IdeTest         *self,
-                        IdeTestProvider *provider)
+IdeTestStatus
+ide_test_get_status (IdeTest *self)
 {
-  IdeTestPrivate *priv = ide_test_get_instance_private (self);
-
-  g_return_if_fail (IDE_IS_TEST (self));
-  g_return_if_fail (!provider || IDE_IS_TEST_PROVIDER (provider));
+  g_return_val_if_fail (IDE_IS_TEST (self), 0);
 
-  priv->provider = provider;
+  return self->status;
 }
 
-/**
- * ide_test_get_display_name:
- * @self: An #IdeTest
- *
- * Gets the "display-name" property of the test.
- *
- * Returns: (nullable): The display_name of the test or %NULL
- *
- * Since: 3.32
- */
-const gchar *
-ide_test_get_display_name (IdeTest *self)
+const char *
+ide_test_get_title (IdeTest *self)
 {
-  IdeTestPrivate *priv = ide_test_get_instance_private (self);
-
   g_return_val_if_fail (IDE_IS_TEST (self), NULL);
+  g_return_val_if_fail (IDE_IS_RUN_COMMAND (self->run_command), NULL);
 
-  return priv->display_name;
+  return ide_run_command_get_display_name (self->run_command);
 }
 
-/**
- * ide_test_set_display_name:
- * @self: An #IdeTest
- * @display_name: (nullable): The display_name of the test, or %NULL to unset
- *
- * Sets the "display-name" property of the unit test.
- *
- * Since: 3.32
- */
-void
-ide_test_set_display_name (IdeTest     *self,
-                           const gchar *display_name)
+const char *
+ide_test_get_icon_name (IdeTest *self)
 {
-  IdeTestPrivate *priv = ide_test_get_instance_private (self);
-
-  g_return_if_fail (IDE_IS_TEST (self));
+  g_return_val_if_fail (IDE_IS_TEST (self), NULL);
 
-  if (g_strcmp0 (display_name, priv->display_name) != 0)
+  switch (self->status)
     {
-      g_free (priv->display_name);
-      priv->display_name = g_strdup (display_name);
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_DISPLAY_NAME]);
+    case IDE_TEST_STATUS_NONE:
+      return "builder-unit-tests-symbolic";
+
+    case IDE_TEST_STATUS_RUNNING:
+      return "builder-unit-tests-running-symbolic";
+
+    case IDE_TEST_STATUS_FAILED:
+      return "builder-unit-tests-fail-symbolic";
+
+    case IDE_TEST_STATUS_SUCCESS:
+      return "builder-unit-tests-pass-symbolic";
+
+    default:
+      g_return_val_if_reached (NULL);
     }
 }
 
 /**
- * ide_test_get_group:
+ * ide_test_get_run_command:
  * @self: a #IdeTest
  *
- * Gets the "group" property.
- *
- * The group name is used to group tests together.
- *
- * Returns: (nullable): The group name or %NULL.
+ * Gets the run command for the test.
  *
- * Since: 3.32
+ * Returns: (transfer none): an #IdeTest
  */
-const gchar *
-ide_test_get_group (IdeTest *self)
+IdeRunCommand *
+ide_test_get_run_command (IdeTest *self)
 {
-  IdeTestPrivate *priv = ide_test_get_instance_private (self);
-
   g_return_val_if_fail (IDE_IS_TEST (self), NULL);
 
-  return priv->group;
+  return self->run_command;
 }
 
-/**
- * ide_test_set_group:
- * @self: a #IdeTest
- * @group: (nullable): the name of the group or %NULL
- *
- * Sets the #IdeTest:group property.
- *
- * The group property is used to group related tests together.
- *
- * Since: 3.32
- */
-void
-ide_test_set_group (IdeTest     *self,
-                    const gchar *group)
+static void
+ide_test_wait_check_cb (GObject      *object,
+                        GAsyncResult *result,
+                        gpointer      user_data)
 {
-  IdeTestPrivate *priv = ide_test_get_instance_private (self);
+  IdeSubprocess *subprocess = (IdeSubprocess *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeTest *self;
 
-  g_return_if_fail (IDE_IS_TEST (self));
+  IDE_ENTRY;
 
-  if (g_strcmp0 (group, priv->group) != 0)
-    {
-      g_free (priv->group);
-      priv->group = g_strdup (group);
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_GROUP]);
-    }
-}
+  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));
 
-/**
- * ide_test_get_id:
- * @self: a #IdeTest
- *
- * Gets the #IdeTest:id property.
- *
- * Returns: (nullable): The id of the test, or %NULL if it has not been set.
- *
- * Since: 3.32
- */
-const gchar *
-ide_test_get_id (IdeTest *self)
-{
-  IdeTestPrivate *priv = ide_test_get_instance_private (self);
+  self = ide_task_get_source_object (task);
 
-  g_return_val_if_fail (IDE_IS_TEST (self), NULL);
+  if (!ide_subprocess_wait_check_finish (subprocess, result, &error))
+    {
+      ide_test_set_status (self, IDE_TEST_STATUS_FAILED);
+      ide_task_return_error (task, g_steal_pointer (&error));
+    }
+  else
+    {
+      ide_test_set_status (self, IDE_TEST_STATUS_SUCCESS);
+      ide_task_return_boolean (task, TRUE);
+    }
 
-  return priv->id;
+  IDE_EXIT;
 }
 
-/**
- * ide_test_set_id:
- * @self: a #IdeTest
- * @id: (nullable): the id of the test or %NULL
- *
- * Sets the #IdeTest:id property.
- *
- * The id property is used to uniquely identify the test.
- *
- * Since: 3.32
- */
 void
-ide_test_set_id (IdeTest     *self,
-                 const gchar *id)
+ide_test_run_async (IdeTest             *self,
+                    IdePipeline         *pipeline,
+                    VtePty              *pty,
+                    GCancellable        *cancellable,
+                    GAsyncReadyCallback  callback,
+                    gpointer             user_data)
 {
-  IdeTestPrivate *priv = ide_test_get_instance_private (self);
+  g_autoptr(IdeRunContext) run_context = NULL;
+  g_autoptr(IdeSubprocess) subprocess = NULL;
+  g_autoptr(GSettings) settings = NULL;
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autofree char *locality = NULL;
+  IdeContext *context;
+  IdeRuntime *runtime;
 
-  g_return_if_fail (IDE_IS_TEST (self));
+  IDE_ENTRY;
 
-  if (g_strcmp0 (id, priv->id) != 0)
-    {
-      g_free (priv->id);
-      priv->id = g_strdup (id);
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ID]);
-    }
-}
+  g_return_if_fail (IDE_IS_TEST (self));
+  g_return_if_fail (IDE_IS_PIPELINE (pipeline));
+  g_return_if_fail (!pty || VTE_IS_PTY (pty));
+  g_return_if_fail (IDE_IS_RUN_COMMAND (self->run_command));
 
-IdeTestStatus
-ide_test_get_status (IdeTest *self)
-{
-  IdeTestPrivate *priv = ide_test_get_instance_private (self);
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_test_run_async);
 
-  g_return_val_if_fail (IDE_IS_TEST (self), 0);
+  if (ide_task_return_error_if_cancelled (task))
+    IDE_EXIT;
 
-  return priv->status;
-}
+  context = ide_object_get_context (IDE_OBJECT (pipeline));
+  runtime = ide_pipeline_get_runtime (pipeline);
+  settings = ide_context_ref_project_settings (context);
+  locality = g_settings_get_string (settings, "unit-test-locality");
 
-void
-ide_test_set_status (IdeTest       *self,
-                     IdeTestStatus  status)
-{
-  IdeTestPrivate *priv = ide_test_get_instance_private (self);
+  if (ide_str_equal0 (locality, "runtime"))
+    {
+      run_context = ide_run_context_new ();
+      ide_runtime_prepare_to_run (runtime, pipeline, run_context);
+      ide_run_command_prepare_to_run (self->run_command, run_context, context);
+    }
+  else /* "pipeline" */
+    {
+      run_context = ide_pipeline_create_run_context (pipeline, self->run_command);
+    }
 
-  g_return_if_fail (IDE_IS_TEST (self));
+  if (pty != NULL)
+    ide_run_context_set_pty (run_context, pty);
 
-  if (priv->status != status)
+  if (!(subprocess = ide_run_context_spawn (run_context, &error)))
     {
-      priv->status = status;
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_STATUS]);
+      ide_test_set_status (self, IDE_TEST_STATUS_FAILED);
+      ide_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+  else
+    {
+      ide_test_set_status (self, IDE_TEST_STATUS_RUNNING);
+      ide_subprocess_wait_check_async (subprocess,
+                                       cancellable,
+                                       ide_test_wait_check_cb,
+                                       g_steal_pointer (&task));
+      IDE_EXIT;
     }
 }
 
-const gchar *
-ide_test_get_icon_name (IdeTest *self)
+gboolean
+ide_test_run_finish (IdeTest       *self,
+                     GAsyncResult  *result,
+                     GError       **error)
 {
-  IdeTestPrivate *priv = ide_test_get_instance_private (self);
-
-  g_return_val_if_fail (IDE_IS_TEST (self), NULL);
-
-  switch (priv->status)
-    {
-    case IDE_TEST_STATUS_NONE:
-      return "builder-unit-tests-symbolic";
+  gboolean ret;
 
-    case IDE_TEST_STATUS_RUNNING:
-      return "builder-unit-tests-running-symbolic";
+  IDE_ENTRY;
 
-    case IDE_TEST_STATUS_FAILED:
-      return "builder-unit-tests-fail-symbolic";
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TEST (self));
+  g_assert (IDE_IS_TASK (result));
 
-    case IDE_TEST_STATUS_SUCCESS:
-      return "builder-unit-tests-pass-symbolic";
+  ret = ide_task_propagate_boolean (IDE_TASK (result), error);
 
-    default:
-      g_return_val_if_reached (NULL);
-    }
+  IDE_RETURN (ret);
 }
diff --git a/src/libide/foundry/ide-test.h b/src/libide/foundry/ide-test.h
index f6423ca98..de4883ce1 100644
--- a/src/libide/foundry/ide-test.h
+++ b/src/libide/foundry/ide-test.h
@@ -24,8 +24,12 @@
 # error "Only <libide-foundry.h> can be included directly."
 #endif
 
+#include <vte/vte.h>
+
 #include <libide-core.h>
 
+#include "ide-foundry-types.h"
+
 G_BEGIN_DECLS
 
 #define IDE_TYPE_TEST (ide_test_get_type())
@@ -38,40 +42,31 @@ typedef enum
   IDE_TEST_STATUS_FAILED,
 } IdeTestStatus;
 
-IDE_AVAILABLE_IN_3_32
-G_DECLARE_DERIVABLE_TYPE (IdeTest, ide_test, IDE, TEST, GObject)
-
-struct _IdeTestClass
-{
-  GObjectClass parent;
-
-  /*< private >*/
-  gpointer _reserved[16];
-};
+IDE_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (IdeTest, ide_test, IDE, TEST, GObject)
 
-IDE_AVAILABLE_IN_3_32
-IdeTest       *ide_test_new              (void);
-IDE_AVAILABLE_IN_3_32
-const gchar   *ide_test_get_display_name (IdeTest       *self);
-IDE_AVAILABLE_IN_3_32
-void           ide_test_set_display_name (IdeTest       *self,
-                                          const gchar   *display_name);
-IDE_AVAILABLE_IN_3_32
-const gchar   *ide_test_get_group        (IdeTest       *self);
-IDE_AVAILABLE_IN_3_32
-void           ide_test_set_group        (IdeTest       *self,
-                                          const gchar   *group);
-IDE_AVAILABLE_IN_3_32
-const gchar   *ide_test_get_icon_name    (IdeTest       *self);
-IDE_AVAILABLE_IN_3_32
-const gchar   *ide_test_get_id           (IdeTest       *self);
-IDE_AVAILABLE_IN_3_32
-void           ide_test_set_id           (IdeTest       *self,
-                                          const gchar   *id);
-IDE_AVAILABLE_IN_3_32
-IdeTestStatus  ide_test_get_status       (IdeTest       *self);
-IDE_AVAILABLE_IN_3_32
-void           ide_test_set_status       (IdeTest       *self,
-                                          IdeTestStatus  status);
+IDE_AVAILABLE_IN_ALL
+IdeTest       *ide_test_new             (IdeRunCommand        *run_command);
+IDE_AVAILABLE_IN_ALL
+const char    *ide_test_get_id          (IdeTest              *self);
+IDE_AVAILABLE_IN_ALL
+IdeTestStatus  ide_test_get_status      (IdeTest              *self);
+IDE_AVAILABLE_IN_ALL
+const char    *ide_test_get_title       (IdeTest              *self);
+IDE_AVAILABLE_IN_ALL
+const char    *ide_test_get_icon_name   (IdeTest              *self);
+IDE_AVAILABLE_IN_ALL
+IdeRunCommand *ide_test_get_run_command (IdeTest              *self);
+IDE_AVAILABLE_IN_ALL
+void           ide_test_run_async       (IdeTest              *self,
+                                         IdePipeline          *pipeline,
+                                         VtePty               *pty,
+                                         GCancellable         *cancellable,
+                                         GAsyncReadyCallback   callback,
+                                         gpointer              user_data);
+IDE_AVAILABLE_IN_ALL
+gboolean        ide_test_run_finish     (IdeTest              *self,
+                                         GAsyncResult         *result,
+                                         GError              **error);
 
 G_END_DECLS
diff --git a/src/libide/foundry/libide-foundry.h b/src/libide/foundry/libide-foundry.h
index 7d73248ef..6b0ecdda0 100644
--- a/src/libide/foundry/libide-foundry.h
+++ b/src/libide/foundry/libide-foundry.h
@@ -55,6 +55,10 @@ G_BEGIN_DECLS
 #include "ide-pipeline-stage-transfer.h"
 #include "ide-pipeline-stage.h"
 #include "ide-pipeline.h"
+#include "ide-run-command.h"
+#include "ide-run-command-provider.h"
+#include "ide-run-commands.h"
+#include "ide-run-context.h"
 #include "ide-run-manager.h"
 #include "ide-runtime-manager.h"
 #include "ide-runtime-provider.h"
@@ -63,7 +67,6 @@ G_BEGIN_DECLS
 #include "ide-simple-build-target.h"
 #include "ide-simple-toolchain.h"
 #include "ide-test-manager.h"
-#include "ide-test-provider.h"
 #include "ide-test.h"
 #include "ide-toolchain-manager.h"
 #include "ide-toolchain-provider.h"
diff --git a/src/libide/foundry/meson.build b/src/libide/foundry/meson.build
index 56fae1a26..7b591bda2 100644
--- a/src/libide/foundry/meson.build
+++ b/src/libide/foundry/meson.build
@@ -50,7 +50,6 @@ libide_foundry_public_headers = [
   'ide-simple-toolchain.h',
   'ide-test.h',
   'ide-test-manager.h',
-  'ide-test-provider.h',
   'ide-toolchain-manager.h',
   'ide-toolchain-provider.h',
   'ide-toolchain.h',
@@ -122,7 +121,6 @@ libide_foundry_public_sources = [
   'ide-simple-build-target.c',
   'ide-simple-toolchain.c',
   'ide-test-manager.c',
-  'ide-test-provider.c',
   'ide-test.c',
   'ide-toolchain-manager.c',
   'ide-toolchain-provider.c',


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