[gnome-builder] plugins/shellcmd: port to GTK 4 and run commands



commit 2c666f0d8cc491375f5c5a7824698399a0c770bc
Author: Christian Hergert <chergert redhat com>
Date:   Mon Jul 11 23:01:48 2022 -0700

    plugins/shellcmd: port to GTK 4 and run commands
    
     - Remove dazzle usage
     - Port to GTK 4 API
     - Port to new shortcut provider API
     - Use IdeRunCommandPrivate and IdeRunCommand
     - Add preferences addin to edit commands
     - Implement locality support

 .../shellcmd/gbp-shellcmd-application-addin.c      |   91 --
 src/plugins/shellcmd/gbp-shellcmd-command-dialog.c |  626 +++++++++++
 ...ation-addin.h => gbp-shellcmd-command-dialog.h} |   17 +-
 .../shellcmd/gbp-shellcmd-command-dialog.ui        |  272 +++++
 src/plugins/shellcmd/gbp-shellcmd-command-editor.c |  286 -----
 src/plugins/shellcmd/gbp-shellcmd-command-editor.h |   37 -
 .../shellcmd/gbp-shellcmd-command-editor.ui        |  209 ----
 src/plugins/shellcmd/gbp-shellcmd-command-model.c  |  543 +++++-----
 src/plugins/shellcmd/gbp-shellcmd-command-model.h  |   26 +-
 .../shellcmd/gbp-shellcmd-command-provider.c       |  309 ------
 src/plugins/shellcmd/gbp-shellcmd-command-row.c    |   93 --
 src/plugins/shellcmd/gbp-shellcmd-command-row.h    |   36 -
 src/plugins/shellcmd/gbp-shellcmd-command-row.ui   |   31 -
 src/plugins/shellcmd/gbp-shellcmd-command.c        | 1131 --------------------
 src/plugins/shellcmd/gbp-shellcmd-command.h        |   70 --
 src/plugins/shellcmd/gbp-shellcmd-list.c           |  186 ----
 .../shellcmd/gbp-shellcmd-preferences-addin.c      |  253 +++--
 .../shellcmd/gbp-shellcmd-preferences-addin.h      |    2 +-
 .../shellcmd/gbp-shellcmd-run-command-provider.c   |  109 ++
 ...-list.h => gbp-shellcmd-run-command-provider.h} |   14 +-
 src/plugins/shellcmd/gbp-shellcmd-run-command.c    |  416 +++++++
 src/plugins/shellcmd/gbp-shellcmd-run-command.h    |   57 +
 .../shellcmd/gbp-shellcmd-shortcut-provider.c      |  265 +++++
 ...provider.h => gbp-shellcmd-shortcut-provider.h} |   10 +-
 src/plugins/shellcmd/meson.build                   |   20 +-
 .../org.gnome.builder.shellcmd.command.gschema.xml |   35 +
 .../org.gnome.builder.shellcmd.gschema.xml         |   10 +
 src/plugins/shellcmd/shellcmd-plugin.c             |   22 +-
 src/plugins/shellcmd/shellcmd.gresource.xml        |    3 +-
 src/plugins/shellcmd/shellcmd.plugin               |    6 +-
 30 files changed, 2269 insertions(+), 2916 deletions(-)
---
diff --git a/src/plugins/shellcmd/gbp-shellcmd-command-dialog.c 
b/src/plugins/shellcmd/gbp-shellcmd-command-dialog.c
new file mode 100644
index 000000000..872069c4d
--- /dev/null
+++ b/src/plugins/shellcmd/gbp-shellcmd-command-dialog.c
@@ -0,0 +1,626 @@
+/* gbp-shellcmd-command-dialog.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 "gbp-shellcmd-command-dialog"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include <libide-gtk.h>
+
+#include "gbp-shellcmd-command-dialog.h"
+#include "gbp-shellcmd-enums.h"
+
+struct _GbpShellcmdCommandDialog
+{
+  AdwWindow              parent_instance;
+
+  GbpShellcmdRunCommand *command;
+
+  AdwEntryRow           *argv;
+  AdwEntryRow           *location;
+  AdwEntryRow           *name;
+  AdwComboRow           *locality;
+  GtkStringList         *envvars;
+  GtkListBox            *envvars_list_box;
+  GtkLabel              *shortcut_label;
+  GtkButton             *save;
+  GtkButton             *delete_button;
+
+  char                  *accel;
+
+  guint                  delete_on_cancel : 1;
+};
+
+enum {
+  PROP_0,
+  PROP_COMMAND,
+  PROP_DELETE_ON_CANCEL,
+  N_PROPS
+};
+
+G_DEFINE_FINAL_TYPE (GbpShellcmdCommandDialog, gbp_shellcmd_command_dialog, ADW_TYPE_WINDOW)
+
+static GParamSpec *properties [N_PROPS];
+
+static char **
+string_list_to_strv (GtkStringList *strlist)
+{
+  g_autoptr(GStrvBuilder) builder = g_strv_builder_new ();
+  GListModel *model = G_LIST_MODEL (strlist);
+  guint n_items = g_list_model_get_n_items (model);
+
+  for (guint i = 0; i < n_items; i++)
+    {
+      g_autoptr(GtkStringObject) strobj = g_list_model_get_item (model, i);
+      const char *str = gtk_string_object_get_string (strobj);
+
+      g_strv_builder_add (builder, str);
+    }
+
+  return g_strv_builder_end (builder);
+}
+
+static void
+delete_envvar_cb (GbpShellcmdCommandDialog *self,
+                  GtkButton                *button)
+{
+  const char *envvar;
+  guint n_items;
+
+  g_assert (GBP_IS_SHELLCMD_COMMAND_DIALOG (self));
+  g_assert (GTK_IS_BUTTON (button));
+
+  envvar = g_object_get_data (G_OBJECT (button), "ENVVAR");
+  n_items = g_list_model_get_n_items (G_LIST_MODEL (self->envvars));
+
+  for (guint i = 0; i < n_items; i++)
+    {
+      g_autoptr(GtkStringObject) str = g_list_model_get_item (G_LIST_MODEL (self->envvars), i);
+
+      if (g_strcmp0 (envvar, gtk_string_object_get_string (str)) == 0)
+        {
+          gtk_string_list_remove (self->envvars, i);
+          break;
+        }
+    }
+}
+
+static GtkWidget *
+create_envvar_row_cb (gpointer item,
+                      gpointer user_data)
+{
+  GbpShellcmdCommandDialog *self = user_data;
+  GtkStringObject *obj = item;
+  const char *str = gtk_string_object_get_string (obj);
+  g_autofree char *markup = NULL;
+  g_autofree char *escaped = NULL;
+  AdwActionRow *row;
+  GtkButton *button;
+
+  escaped = g_markup_escape_text (str, -1);
+  markup = g_strdup_printf ("<tt>%s</tt>", escaped);
+  row = g_object_new (ADW_TYPE_ACTION_ROW,
+                      "title", markup,
+                      "title-selectable", TRUE,
+                      NULL);
+  button = g_object_new (GTK_TYPE_BUTTON,
+                         "icon-name", "list-remove-symbolic",
+                         "css-classes", IDE_STRV_INIT ("flat", "circular"),
+                         "valign", GTK_ALIGN_CENTER,
+                         NULL);
+  g_object_set_data_full (G_OBJECT (button),
+                          "ENVVAR",
+                          g_strdup (str),
+                          g_free);
+  g_signal_connect_object (button,
+                           "clicked",
+                           G_CALLBACK (delete_envvar_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+  adw_action_row_add_suffix (row, GTK_WIDGET (button));
+
+  return GTK_WIDGET (row);
+}
+
+static void
+on_env_entry_changed_cb (GbpShellcmdCommandDialog *self,
+                         IdeEntryPopover          *popover)
+{
+  gboolean valid = FALSE;
+  const char *text;
+  const char *eq;
+
+  g_assert (GBP_IS_SHELLCMD_COMMAND_DIALOG (self));
+  g_assert (IDE_IS_ENTRY_POPOVER (popover));
+
+  text = ide_entry_popover_get_text (popover);
+  eq = strchr (text, '=');
+
+  if (eq != NULL && eq != text)
+    {
+      for (const char *iter = text; iter < eq; iter = g_utf8_next_char (iter))
+        {
+          gunichar ch = g_utf8_get_char (iter);
+
+          if (!g_unichar_isalnum (ch) && ch != '_')
+            goto failure;
+        }
+
+      if (g_ascii_isalpha (*text))
+        valid = TRUE;
+    }
+
+failure:
+  ide_entry_popover_set_ready (popover, valid);
+}
+
+static void
+on_env_entry_activate_cb (GbpShellcmdCommandDialog *self,
+                          const char               *text,
+                          IdeEntryPopover          *popover)
+{
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_SHELLCMD_COMMAND_DIALOG (self));
+  g_assert (IDE_IS_ENTRY_POPOVER (popover));
+  g_assert (GTK_IS_STRING_LIST (self->envvars));
+
+  gtk_string_list_append (self->envvars, text);
+  ide_entry_popover_set_text (popover, "");
+
+  IDE_EXIT;
+}
+
+static char *
+normalize_argv (const char * const *argv)
+{
+  g_autofree char *joined = NULL;
+  g_auto(GStrv) parsed = NULL;
+  int argc;
+
+  if (argv == NULL || argv[0] == NULL)
+    return g_strdup ("");
+
+  /* The goal here is to only quote the argv if the string would
+   * parse back differently than it's initial form.
+   */
+  joined = g_strjoinv (" ", (char **)argv);
+  if (!g_shell_parse_argv (joined, &argc, &parsed, NULL) ||
+      !g_strv_equal ((const char * const *)parsed, argv))
+    {
+      GString *str = g_string_new (NULL);
+
+      for (guint i = 0; argv[i]; i++)
+        {
+          g_autofree char *quoted = g_shell_quote (argv[i]);
+
+          if (str->len > 0)
+            g_string_append_c (str, ' ');
+          g_string_append (str, quoted);
+        }
+
+      return g_string_free (str, FALSE);
+    }
+
+  return g_steal_pointer (&joined);
+}
+
+static void
+set_accel (GbpShellcmdCommandDialog *self,
+           const char               *accel)
+{
+  g_autofree char *label = NULL;
+  guint keyval;
+  GdkModifierType state;
+
+  g_assert (GBP_IS_SHELLCMD_COMMAND_DIALOG (self));
+
+  if (ide_str_equal0 (self->accel, accel))
+    return;
+
+  g_free (self->accel);
+  self->accel = g_strdup (accel);
+
+  if (accel && gtk_accelerator_parse (accel, &keyval, &state))
+    label = gtk_accelerator_get_label (keyval, state);
+
+  gtk_label_set_label (self->shortcut_label, label);
+}
+
+static void
+gbp_shellcmd_command_dialog_set_command (GbpShellcmdCommandDialog *self,
+                                         GbpShellcmdRunCommand    *command)
+{
+  g_autofree char *argvstr = NULL;
+  GbpShellcmdLocality locality;
+  const char * const *argv;
+  const char * const *env;
+  const char *accel;
+  const char *name;
+  const char *cwd;
+
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_SHELLCMD_COMMAND_DIALOG (self));
+  g_assert (!command || GBP_IS_SHELLCMD_RUN_COMMAND (command));
+
+  if (!g_set_object (&self->command, command))
+    IDE_EXIT;
+
+  name = ide_run_command_get_display_name (IDE_RUN_COMMAND (command));
+  argv = ide_run_command_get_argv (IDE_RUN_COMMAND (command));
+  env = ide_run_command_get_environ (IDE_RUN_COMMAND (command));
+  cwd = ide_run_command_get_cwd (IDE_RUN_COMMAND (command));
+  accel = gbp_shellcmd_run_command_get_accelerator (command);
+  locality = gbp_shellcmd_run_command_get_locality (command);
+
+  argvstr = normalize_argv (argv);
+
+  gtk_editable_set_text (GTK_EDITABLE (self->argv), argvstr);
+  gtk_editable_set_text (GTK_EDITABLE (self->location), cwd);
+  gtk_editable_set_text (GTK_EDITABLE (self->name), name);
+  set_accel (self, accel);
+
+  /* locality value equates to position in list model for simplicity */
+  adw_combo_row_set_selected (self->locality, locality);
+
+  if (env != NULL)
+    {
+      for (guint i = 0; env[i]; i++)
+        gtk_string_list_append (self->envvars, env[i]);
+    }
+
+  IDE_EXIT;
+}
+
+static void
+on_shortcut_dialog_respnose (GbpShellcmdCommandDialog *self,
+                             int                       response_id,
+                             IdeShortcutAccelDialog   *dialog)
+{
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_SHELLCMD_COMMAND_DIALOG (self));
+  g_assert (IDE_IS_SHORTCUT_ACCEL_DIALOG (dialog));
+
+  if (response_id == GTK_RESPONSE_ACCEPT)
+    {
+      const char *accel;
+
+      accel = ide_shortcut_accel_dialog_get_accelerator (dialog);
+      set_accel (self, accel);
+    }
+
+  gtk_window_destroy (GTK_WINDOW (dialog));
+
+  IDE_EXIT;
+}
+
+static void
+on_shortcut_activated_cb (GbpShellcmdCommandDialog *self,
+                          AdwActionRow             *shortcut_row)
+{
+  IdeShortcutAccelDialog *dialog;
+  const char *name;
+
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_SHELLCMD_COMMAND_DIALOG (self));
+  g_assert (ADW_IS_ACTION_ROW (shortcut_row));
+
+  name = gtk_editable_get_text (GTK_EDITABLE (self->name));
+  if (ide_str_empty0 (name))
+    name = _("Untitled Command");
+
+  dialog = g_object_new (IDE_TYPE_SHORTCUT_ACCEL_DIALOG,
+                         "accelerator", self->accel,
+                         "transient-for", self,
+                         "modal", TRUE,
+                         "shortcut-title", name,
+                         "title", _("Set Shortcut"),
+                         "use-header-bar", 1,
+                         NULL);
+  g_signal_connect_object (dialog,
+                           "response",
+                           G_CALLBACK (on_shortcut_dialog_respnose),
+                           self,
+                           G_CONNECT_SWAPPED);
+  gtk_window_present (GTK_WINDOW (dialog));
+
+  IDE_EXIT;
+}
+
+static void
+command_delete_action (GtkWidget  *widget,
+                       const char *action_name,
+                       GVariant   *param)
+{
+  GbpShellcmdCommandDialog *self = (GbpShellcmdCommandDialog *)widget;
+
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_SHELLCMD_COMMAND_DIALOG (self));
+
+  gbp_shellcmd_run_command_delete (self->command);
+
+  gtk_window_destroy (GTK_WINDOW (self));
+
+  IDE_EXIT;
+}
+
+static void
+command_cancel_action (GtkWidget  *widget,
+                       const char *action_name,
+                       GVariant   *param)
+{
+  GbpShellcmdCommandDialog *self = (GbpShellcmdCommandDialog *)widget;
+
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_SHELLCMD_COMMAND_DIALOG (self));
+
+  if (self->delete_on_cancel)
+    gbp_shellcmd_run_command_delete (self->command);
+
+  gtk_window_destroy (GTK_WINDOW (self));
+
+  IDE_EXIT;
+}
+
+static void
+command_save_action (GtkWidget  *widget,
+                     const char *action_name,
+                     GVariant   *param)
+{
+  GbpShellcmdCommandDialog *self = (GbpShellcmdCommandDialog *)widget;
+  g_autoptr(GEnumClass) enum_class = NULL;
+  g_auto(GStrv) argv = NULL;
+  g_auto(GStrv) env = NULL;
+  const char *argvstr;
+  IdeEnumObject *item;
+  const char *nick;
+  GEnumValue *value;
+  int argc;
+
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_SHELLCMD_COMMAND_DIALOG (self));
+
+  g_object_freeze_notify (G_OBJECT (self->command));
+
+  argvstr = gtk_editable_get_text (GTK_EDITABLE (self->argv));
+  if (g_shell_parse_argv (argvstr, &argc, &argv, NULL))
+    ide_run_command_set_argv (IDE_RUN_COMMAND (self->command), (const char * const *)argv);
+
+  ide_run_command_set_display_name (IDE_RUN_COMMAND (self->command),
+                                    gtk_editable_get_text (GTK_EDITABLE (self->name)));
+  ide_run_command_set_cwd (IDE_RUN_COMMAND (self->command),
+                           gtk_editable_get_text (GTK_EDITABLE (self->location)));
+  gbp_shellcmd_run_command_set_accelerator (self->command, self->accel);
+
+  env = string_list_to_strv (self->envvars);
+  ide_run_command_set_environ (IDE_RUN_COMMAND (self->command),
+                               (const char * const *)env);
+
+  item = adw_combo_row_get_selected_item (self->locality);
+  nick = ide_enum_object_get_nick (item);
+  enum_class = g_type_class_ref (GBP_TYPE_SHELLCMD_LOCALITY);
+  value = g_enum_get_value_by_nick (enum_class, nick);
+  gbp_shellcmd_run_command_set_locality (self->command, value->value);
+
+  g_object_thaw_notify (G_OBJECT (self->command));
+
+ gtk_window_destroy (GTK_WINDOW (self));
+
+  IDE_EXIT;
+}
+
+static void
+select_folder_response_cb (GtkFileChooserNative *native,
+                           int                   response_id,
+                           AdwEntryRow          *row)
+{
+  g_assert (ADW_IS_ENTRY_ROW (row));
+  g_assert (GTK_IS_FILE_CHOOSER_NATIVE (native));
+
+  if (response_id == GTK_RESPONSE_ACCEPT)
+    {
+      g_autoptr(GFile) file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (native));
+      g_autofree char *path = ide_path_collapse (g_file_peek_path (file));
+
+      gtk_editable_set_text (GTK_EDITABLE (row), path);
+    }
+
+  gtk_native_dialog_destroy (GTK_NATIVE_DIALOG (native));
+}
+
+static void
+select_folder_action (GtkWidget  *widget,
+                      const char *action_name,
+                      GVariant   *param)
+{
+  GbpShellcmdCommandDialog *self = (GbpShellcmdCommandDialog *)widget;
+  GtkFileChooserNative *native;
+  g_autofree char *expanded = NULL;
+  g_autoptr(GFile) file = NULL;
+  const char *cwd;
+  GtkRoot *root;
+
+  g_assert (GBP_IS_SHELLCMD_COMMAND_DIALOG (self));
+
+  cwd = gtk_editable_get_text (GTK_EDITABLE (self->location));
+  expanded = ide_path_expand (cwd);
+  file = g_file_new_for_path (expanded);
+
+  root = gtk_widget_get_root (widget);
+  native = gtk_file_chooser_native_new (_("Select Working Directory"),
+                                        GTK_WINDOW (root),
+                                        GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER,
+                                        _("Select"),
+                                        _("Cancel"));
+  gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER (native), file, NULL);
+  g_signal_connect_object (native,
+                           "response",
+                           G_CALLBACK (select_folder_response_cb),
+                           self->location,
+                           0);
+  gtk_native_dialog_show (GTK_NATIVE_DIALOG (native));
+}
+
+static void
+gbp_shellcmd_command_dialog_dispose (GObject *object)
+{
+  GbpShellcmdCommandDialog *self = (GbpShellcmdCommandDialog *)object;
+
+  g_clear_object (&self->command);
+  g_clear_pointer (&self->accel, g_free);
+
+  G_OBJECT_CLASS (gbp_shellcmd_command_dialog_parent_class)->dispose (object);
+}
+
+static void
+gbp_shellcmd_command_dialog_get_property (GObject    *object,
+                                          guint       prop_id,
+                                          GValue     *value,
+                                          GParamSpec *pspec)
+{
+  GbpShellcmdCommandDialog *self = GBP_SHELLCMD_COMMAND_DIALOG (object);
+
+  switch (prop_id)
+    {
+    case PROP_COMMAND:
+      g_value_set_object (value, self->command);
+      break;
+
+    case PROP_DELETE_ON_CANCEL:
+      g_value_set_boolean (value, self->delete_on_cancel);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_shellcmd_command_dialog_set_property (GObject      *object,
+                                          guint         prop_id,
+                                          const GValue *value,
+                                          GParamSpec   *pspec)
+{
+  GbpShellcmdCommandDialog *self = GBP_SHELLCMD_COMMAND_DIALOG (object);
+
+  switch (prop_id)
+    {
+    case PROP_COMMAND:
+      gbp_shellcmd_command_dialog_set_command (self, g_value_get_object (value));
+      break;
+
+    case PROP_DELETE_ON_CANCEL:
+      self->delete_on_cancel = g_value_get_boolean (value);
+      if (self->delete_on_cancel)
+        {
+          gtk_window_set_title (GTK_WINDOW (self), _("Create Command"));
+          gtk_button_set_label (self->save, _("Cre_ate"));
+          gtk_widget_hide (GTK_WIDGET (self->delete_button));
+        }
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_shellcmd_command_dialog_class_init (GbpShellcmdCommandDialogClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->dispose = gbp_shellcmd_command_dialog_dispose;
+  object_class->get_property = gbp_shellcmd_command_dialog_get_property;
+  object_class->set_property = gbp_shellcmd_command_dialog_set_property;
+
+  properties [PROP_COMMAND] =
+    g_param_spec_object ("command", NULL, NULL,
+                         GBP_TYPE_SHELLCMD_RUN_COMMAND,
+                         (G_PARAM_READWRITE |
+                          G_PARAM_CONSTRUCT_ONLY |
+                          G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_DELETE_ON_CANCEL] =
+    g_param_spec_boolean ("delete-on-cancel", NULL, NULL, FALSE,
+                         (G_PARAM_READWRITE |
+                          G_PARAM_CONSTRUCT_ONLY |
+                          G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_install_action (widget_class, "command.save", NULL, command_save_action);
+  gtk_widget_class_install_action (widget_class, "command.delete", NULL, command_delete_action);
+  gtk_widget_class_install_action (widget_class, "command.cancel", NULL, command_cancel_action);
+  gtk_widget_class_install_action (widget_class, "command.select-folder", NULL, select_folder_action);
+
+  gtk_widget_class_add_binding_action (widget_class, GDK_KEY_Escape, 0, "command.cancel", NULL);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/plugins/shellcmd/gbp-shellcmd-command-dialog.ui");
+  gtk_widget_class_bind_template_child (widget_class, GbpShellcmdCommandDialog, argv);
+  gtk_widget_class_bind_template_child (widget_class, GbpShellcmdCommandDialog, delete_button);
+  gtk_widget_class_bind_template_child (widget_class, GbpShellcmdCommandDialog, envvars);
+  gtk_widget_class_bind_template_child (widget_class, GbpShellcmdCommandDialog, envvars_list_box);
+  gtk_widget_class_bind_template_child (widget_class, GbpShellcmdCommandDialog, locality);
+  gtk_widget_class_bind_template_child (widget_class, GbpShellcmdCommandDialog, location);
+  gtk_widget_class_bind_template_child (widget_class, GbpShellcmdCommandDialog, name);
+  gtk_widget_class_bind_template_child (widget_class, GbpShellcmdCommandDialog, save);
+  gtk_widget_class_bind_template_child (widget_class, GbpShellcmdCommandDialog, shortcut_label);
+  gtk_widget_class_bind_template_callback (widget_class, on_env_entry_changed_cb);
+  gtk_widget_class_bind_template_callback (widget_class, on_env_entry_activate_cb);
+  gtk_widget_class_bind_template_callback (widget_class, on_shortcut_activated_cb);
+}
+
+static void
+gbp_shellcmd_command_dialog_init (GbpShellcmdCommandDialog *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+#if DEVELOPMENT_BUILD
+  gtk_widget_add_css_class (GTK_WIDGET (self), "devel");
+#endif
+
+  gtk_list_box_bind_model (self->envvars_list_box,
+                           G_LIST_MODEL (self->envvars),
+                           create_envvar_row_cb,
+                           self, NULL);
+  ide_gtk_widget_hide_when_empty (GTK_WIDGET (self->envvars_list_box),
+                                  G_LIST_MODEL (self->envvars));
+}
+
+GbpShellcmdCommandDialog *
+gbp_shellcmd_command_dialog_new (GbpShellcmdRunCommand *command,
+                                 gboolean               delete_on_cancel)
+{
+  g_return_val_if_fail (GBP_IS_SHELLCMD_RUN_COMMAND (command), NULL);
+
+  return g_object_new (GBP_TYPE_SHELLCMD_COMMAND_DIALOG,
+                       "command", command,
+                       "delete-on-cancel", delete_on_cancel,
+                       NULL);
+}
diff --git a/src/plugins/shellcmd/gbp-shellcmd-application-addin.h 
b/src/plugins/shellcmd/gbp-shellcmd-command-dialog.h
similarity index 56%
rename from src/plugins/shellcmd/gbp-shellcmd-application-addin.h
rename to src/plugins/shellcmd/gbp-shellcmd-command-dialog.h
index dc29e3132..abefa6af3 100644
--- a/src/plugins/shellcmd/gbp-shellcmd-application-addin.h
+++ b/src/plugins/shellcmd/gbp-shellcmd-command-dialog.h
@@ -1,6 +1,6 @@
-/* gbp-shellcmd-application-addin.h
+/* gbp-shellcmd-command-dialog.h
  *
- * Copyright 2019 Christian Hergert <chergert redhat com>
+ * 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
@@ -20,16 +20,19 @@
 
 #pragma once
 
-#include <libide-gui.h>
+#include <adwaita.h>
 
-#include "gbp-shellcmd-command-model.h"
+#include <libide-foundry.h>
+
+#include "gbp-shellcmd-run-command.h"
 
 G_BEGIN_DECLS
 
-#define GBP_TYPE_SHELLCMD_APPLICATION_ADDIN (gbp_shellcmd_application_addin_get_type())
+#define GBP_TYPE_SHELLCMD_COMMAND_DIALOG (gbp_shellcmd_command_dialog_get_type())
 
-G_DECLARE_FINAL_TYPE (GbpShellcmdApplicationAddin, gbp_shellcmd_application_addin, GBP, 
SHELLCMD_APPLICATION_ADDIN, GObject)
+G_DECLARE_FINAL_TYPE (GbpShellcmdCommandDialog, gbp_shellcmd_command_dialog, GBP, SHELLCMD_COMMAND_DIALOG, 
AdwWindow)
 
-GbpShellcmdCommandModel *gbp_shellcmd_application_addin_get_model (GbpShellcmdApplicationAddin *self);
+GbpShellcmdCommandDialog *gbp_shellcmd_command_dialog_new (GbpShellcmdRunCommand *command,
+                                                           gboolean               delete_on_cancel);
 
 G_END_DECLS
diff --git a/src/plugins/shellcmd/gbp-shellcmd-command-dialog.ui 
b/src/plugins/shellcmd/gbp-shellcmd-command-dialog.ui
new file mode 100644
index 000000000..d12a9f3e4
--- /dev/null
+++ b/src/plugins/shellcmd/gbp-shellcmd-command-dialog.ui
@@ -0,0 +1,272 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GbpShellcmdCommandDialog" parent="AdwWindow">
+    <property name="default-width">650</property>
+    <property name="default-height">650</property>
+    <property name="title" translatable="yes">Edit Command</property>
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="AdwHeaderBar">
+            <property name="show-start-title-buttons">false</property>
+            <property name="show-end-title-buttons">false</property>
+            <child type="start">
+              <object class="GtkButton" id="cancel">
+                <property name="label" translatable="yes">_Cancel</property>
+                <property name="use-underline">true</property>
+                <property name="action-name">command.cancel</property>
+              </object>
+            </child>
+            <child type="end">
+              <object class="GtkButton" id="save">
+                <property name="label" translatable="yes">S_ave</property>
+                <property name="action-name">command.save</property>
+                <property name="use-underline">true</property>
+                <style>
+                  <class name="suggested-action"/>
+                </style>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="AdwPreferencesPage">
+            <property name="vexpand">true</property>
+            <child>
+              <object class="AdwPreferencesGroup">
+                <child>
+                  <object class="AdwEntryRow" id="name">
+                    <property name="title" translatable="yes">Name</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="AdwPreferencesGroup">
+                <child>
+                  <object class="AdwActionRow" id="shortcut_row">
+                    <signal name="activated" handler="on_shortcut_activated_cb" swapped="true" 
object="GbpShellcmdCommandDialog"/>
+                    <property name="activatable">true</property>
+                    <property name="title" translatable="yes">Keyboard Shortcut</property>
+                    <property name="subtitle" translatable="yes">An optional shourtcut to run the 
command</property>
+                    <property name="icon-name">preferences-desktop-keyboard-shortcuts-symbolic</property>
+                    <child type="suffix">
+                      <object class="GtkLabel" id="shortcut_label">
+                        <property name="valign">center</property>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="AdwPreferencesGroup">
+                <child>
+                  <object class="AdwEntryRow" id="argv">
+                    <property name="title" translatable="yes">Shell Command</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkLabel">
+                    <property name="label" translatable="yes">You may use single or double quotes for 
parameters.</property>
+                    <property name="use-markup">true</property>
+                    <property name="margin-top">12</property>
+                    <property name="wrap">true</property>
+                    <property name="xalign">0</property>
+                    <style>
+                      <class name="caption"/>
+                      <class name="dim-label"/>
+                    </style>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="AdwPreferencesGroup">
+                <property name="title" translatable="yes">Environment</property>
+                <child>
+                  <object class="AdwEntryRow" id="location">
+                    <property name="title" translatable="yes">Working Directory</property>
+                    <child type="suffix">
+                      <object class="GtkButton">
+                        <property name="action-name">command.select-folder</property>
+                        <property name="valign">center</property>
+                        <property name="icon-name">folder-symbolic</property>
+                        <style>
+                          <class name="flat"/>
+                        </style>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkLabel">
+                    <property name="label" translatable="yes">The command will be run from this location. 
Use &lt;tt&gt;$BUILDDIR&lt;/tt&gt;, &lt;tt&gt;$SRCDIR&lt;/tt&gt;, or &lt;tt&gt;$HOME&lt;/tt&gt; to define a 
relative path.</property>
+                    <property name="use-markup">true</property>
+                    <property name="margin-top">12</property>
+                    <property name="wrap">true</property>
+                    <property name="xalign">0</property>
+                    <style>
+                      <class name="caption"/>
+                      <class name="dim-label"/>
+                    </style>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="AdwPreferencesGroup">
+                <child>
+                  <object class="AdwComboRow" id="locality">
+                    <property name="title" translatable="yes">Locality</property>
+                    <property name="subtitle" translatable="yes">Builder can run your command from a number 
of localities including the host system or build containers.</property>
+                    <property name="model">localities</property>
+                    <property name="expression">
+                      <lookup name="title" type="IdeEnumObject"/>
+                    </property>
+                    <property name="list-factory">
+                      <object class="GtkBuilderListItemFactory">
+                        <property name="bytes"><![CDATA[
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GtkListItem">
+    <property name="child">
+      <object class="GtkBox">
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="GtkLabel">
+            <property name="xalign">0</property>
+            <property name="hexpand">true</property>
+            <binding name="label">
+              <lookup name="title" type="IdeEnumObject">
+                <lookup name="item">GtkListItem</lookup>
+              </lookup>
+            </binding>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel">
+            <property name="xalign">0</property>
+            <property name="hexpand">true</property>
+            <style>
+              <class name="caption"/>
+              <class name="dim-label"/>
+            </style>
+            <binding name="label">
+              <lookup name="description" type="IdeEnumObject">
+                <lookup name="item">GtkListItem</lookup>
+              </lookup>
+            </binding>
+          </object>
+        </child>
+      </object>
+    </property>
+  </template>
+</interface>
+]]>
+                        </property>
+                      </object>
+                    </property>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="AdwPreferencesGroup">
+                <child>
+                  <object class="GtkListBox" id="envvars_list_box">
+                    <property name="selection-mode">none</property>
+                    <style>
+                      <class name="boxed-list"/>
+                    </style>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkMenuButton">
+                    <property name="direction">left</property>
+                    <property name="margin-top">12</property>
+                    <property name="halign">end</property>
+                    <property name="child">
+                      <object class="GtkLabel">
+                        <property name="label" translatable="yes">Add _Variable</property>
+                        <property name="use-underline">true</property>
+                      </object>
+                    </property>
+                    <property name="popover">
+                      <object class="IdeEntryPopover">
+                        <property name="title" translatable="yes">Add Variable</property>
+                        <property name="button-text" translatable="yes">_Add</property>
+                        <signal name="changed" handler="on_env_entry_changed_cb" swapped="true" 
object="GbpShellcmdCommandDialog"/>
+                        <signal name="activate" handler="on_env_entry_activate_cb" swapped="true" 
object="GbpShellcmdCommandDialog"/>
+                      </object>
+                    </property>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="AdwPreferencesGroup">
+                <property name="vexpand">true</property>
+                <child>
+                  <object class="GtkLabel">
+                    <property name="vexpand">true</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkButton" id="delete_button">
+                    <property name="label" translatable="yes">_Delete Command</property>
+                    <property name="use-underline">true</property>
+                    <property name="action-name">command.delete</property>
+                    <property name="halign">end</property>
+                    <style>
+                      <class name="destructive-action"/>
+                    </style>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+  <object class="GtkSizeGroup">
+    <widgets>
+      <widget name="save"/>
+      <widget name="cancel"/>
+    </widgets>
+  </object>
+  <object class="GtkStringList" id="envvars">
+  </object>
+  <object class="GListStore" id="localities">
+    <child>
+      <object class="IdeEnumObject">
+        <property name="nick">subprocess</property>
+        <property name="title" translatable="yes">Subprocess</property>
+        <property name="description" translatable="yes">Runs the command as a subprocess of 
Builder.</property>
+      </object>
+    </child>
+    <child>
+      <object class="IdeEnumObject">
+        <property name="nick">host</property>
+        <property name="title" translatable="yes">Host System</property>
+        <property name="description" translatable="yes">Runs the command on the host system.</property>
+      </object>
+    </child>
+    <child>
+      <object class="IdeEnumObject">
+        <property name="nick">pipeline</property>
+        <property name="title" translatable="yes">Build Pipeline</property>
+        <property name="description" translatable="yes">Runs the command within the build 
pipeline.</property>
+      </object>
+    </child>
+    <child>
+      <object class="IdeEnumObject">
+        <property name="nick">runtime</property>
+        <property name="title" translatable="yes">As Target Application</property>
+        <property name="description" translatable="yes">Runs the command as if it were the target 
application.</property>
+      </object>
+    </child>
+  </object>
+</interface>
diff --git a/src/plugins/shellcmd/gbp-shellcmd-command-model.c 
b/src/plugins/shellcmd/gbp-shellcmd-command-model.c
index 00b849aec..91797163a 100644
--- a/src/plugins/shellcmd/gbp-shellcmd-command-model.c
+++ b/src/plugins/shellcmd/gbp-shellcmd-command-model.c
@@ -1,6 +1,6 @@
 /* gbp-shellcmd-command-model.c
  *
- * Copyright 2019 Christian Hergert <chergert redhat com>
+ * 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
@@ -22,414 +22,349 @@
 
 #include "config.h"
 
-#include <glib/gstdio.h>
-#include <libide-core.h>
-#include <libide-sourceview.h>
-#include <libide-threading.h>
-
-#include "gbp-shellcmd-command.h"
 #include "gbp-shellcmd-command-model.h"
+#include "gbp-shellcmd-run-command.h"
+
+#define SHELLCMD_SETTINGS_BASE "/org/gnome/builder/shellcmd/"
 
 struct _GbpShellcmdCommandModel
 {
-  GObject    parent_instance;
-
-  GPtrArray *items;
-  GKeyFile  *keyfile;
-
-  guint      queue_save;
-
-  guint      keybindings_changed : 1;
+  GObject      parent_instance;
+  GSettings   *settings;
+  char        *key;
+  GHashTable  *id_to_command;
+  char       **ids;
+  guint        n_items;
 };
 
-static void list_model_iface_init (GListModelInterface *iface);
-
-G_DEFINE_FINAL_TYPE_WITH_CODE (GbpShellcmdCommandModel, gbp_shellcmd_command_model, G_TYPE_OBJECT,
-                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
-
 enum {
-  KEYBINDINGS_CHANGED,
-  N_SIGNALS
+  PROP_0,
+  PROP_KEY,
+  PROP_SETTINGS,
+  PROP_N_ITEMS,
+  N_PROPS
 };
 
-static guint signals [N_SIGNALS];
+static GParamSpec *properties [N_PROPS];
 
-static gboolean
-gbp_shellcmd_command_model_queue_save_cb (gpointer data)
+static gpointer
+gbp_shellcmd_command_model_get_item (GListModel *model,
+                                     guint       position)
 {
-  GbpShellcmdCommandModel *self = data;
-  g_autoptr(GError) error = NULL;
+  GbpShellcmdCommandModel *self = (GbpShellcmdCommandModel *)model;
+  GbpShellcmdRunCommand *command;
+  const char *id;
 
   g_assert (GBP_IS_SHELLCMD_COMMAND_MODEL (self));
 
-  self->queue_save = 0;
+  if (position >= self->n_items)
+    return NULL;
 
-  if (!gbp_shellcmd_command_model_save (self, NULL, &error))
-    g_warning ("Failed to save external-commands: %s", error->message);
+  id = self->ids[position];
+  command = g_hash_table_lookup (self->id_to_command, id);
 
-  /* Now ask everything to reload (as we might have new keybindings) */
-  if (self->keybindings_changed)
+  if (command == NULL)
     {
-      g_signal_emit (self, signals [KEYBINDINGS_CHANGED], 0);
-      self->keybindings_changed = FALSE;
+      g_autofree char *base_path = NULL;
+      g_autofree char *settings_path = NULL;
+
+      g_object_get (self->settings,
+                    "path", &base_path,
+                    NULL);
+      settings_path = g_strconcat (base_path, id, "/", NULL);
+      command = gbp_shellcmd_run_command_new (settings_path);
+      g_hash_table_insert (self->id_to_command, g_strdup (id), command);
     }
 
-  return G_SOURCE_REMOVE;
+  return g_object_ref (command);
 }
 
-static void
-gbp_shellcmd_command_model_queue_save (GbpShellcmdCommandModel *self)
+static guint
+gbp_shellcmd_command_model_get_n_items (GListModel *model)
 {
-  g_assert (GBP_IS_SHELLCMD_COMMAND_MODEL (self));
-
-  g_object_ref (self);
-
-  if (self->queue_save != 0)
-    g_source_remove (self->queue_save);
-
-  self->queue_save =
-    g_timeout_add_seconds_full (G_PRIORITY_HIGH,
-                                1,
-                                gbp_shellcmd_command_model_queue_save_cb,
-                                g_object_ref (self),
-                                g_object_unref);
-
-  g_object_unref (self);
+  return GBP_SHELLCMD_COMMAND_MODEL (model)->n_items;
 }
 
-static void
-on_command_changed_cb (GbpShellcmdCommandModel *self,
-                       GbpShellcmdCommand      *command)
+static GType
+gbp_shellcmd_command_model_get_item_type (GListModel *model)
 {
-  g_assert (GBP_SHELLCMD_COMMAND_MODEL (self));
-  g_assert (GBP_SHELLCMD_COMMAND (command));
-
-  gbp_shellcmd_command_model_queue_save (self);
+  return IDE_TYPE_RUN_COMMAND;
 }
 
 static void
-on_command_shortcut_changed_cb (GbpShellcmdCommandModel *self,
-                                GParamSpec              *pspec,
-                                GbpShellcmdCommand      *command)
+list_model_iface_init (GListModelInterface *iface)
 {
-  g_assert (GBP_SHELLCMD_COMMAND_MODEL (self));
-  g_assert (GBP_SHELLCMD_COMMAND (command));
-
-  self->keybindings_changed = TRUE;
+  iface->get_n_items = gbp_shellcmd_command_model_get_n_items;
+  iface->get_item = gbp_shellcmd_command_model_get_item;
+  iface->get_item_type = gbp_shellcmd_command_model_get_item_type;
 }
 
+G_DEFINE_FINAL_TYPE_WITH_CODE (GbpShellcmdCommandModel, gbp_shellcmd_command_model, G_TYPE_OBJECT,
+                               G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
 static void
-gbp_shellcmd_command_model_finalize (GObject *object)
+gbp_shellcmd_command_model_replace (GbpShellcmdCommandModel  *self,
+                                    char                    **commands)
 {
-  GbpShellcmdCommandModel *self = (GbpShellcmdCommandModel *)object;
+  g_auto(GStrv) old_ids = NULL;
+  guint old_len;
 
-  g_clear_pointer (&self->items, g_ptr_array_unref);
-  g_clear_pointer (&self->keyfile, g_key_file_free);
+  g_assert (GBP_IS_SHELLCMD_COMMAND_MODEL (self));
+  g_assert (self->ids != NULL);
+  g_assert (commands != NULL);
 
-  G_OBJECT_CLASS (gbp_shellcmd_command_model_parent_class)->finalize (object);
-}
+  if (g_strv_equal ((const char * const *)self->ids,
+                    (const char * const *)commands))
+    {
+      g_strfreev (commands);
+      return;
+    }
 
-static void
-gbp_shellcmd_command_model_class_init (GbpShellcmdCommandModelClass *klass)
-{
-  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  old_ids = g_steal_pointer (&self->ids);
+  old_len = self->n_items;
 
-  object_class->finalize = gbp_shellcmd_command_model_finalize;
+  self->ids = g_steal_pointer (&commands);
+  self->n_items = g_strv_length (self->ids);
 
-  signals [KEYBINDINGS_CHANGED] =
-    g_signal_new ("keybindings-changed",
-                  G_TYPE_FROM_CLASS (klass),
-                  G_SIGNAL_RUN_LAST,
-                  0, NULL, NULL, NULL, G_TYPE_NONE, 0);
-}
+  g_assert (g_strv_length (old_ids) == old_len);
+  g_assert (g_strv_length (self->ids) == self->n_items);
+  g_assert (g_hash_table_size (self->id_to_command) <= old_len);
 
-static void
-gbp_shellcmd_command_model_init (GbpShellcmdCommandModel *self)
-{
-  self->items = g_ptr_array_new_with_free_func ((GDestroyNotify)ide_object_unref_and_destroy);
-  self->keyfile = g_key_file_new ();
-}
+  for (guint i = 0; old_ids[i]; i++)
+    {
+      if (!g_strv_contains ((const char * const *)self->ids, old_ids[i]))
+        g_hash_table_remove (self->id_to_command, old_ids[i]);
+    }
 
-GbpShellcmdCommandModel *
-gbp_shellcmd_command_model_new (void)
-{
-  return g_object_new (GBP_TYPE_SHELLCMD_COMMAND_MODEL, NULL);
-}
+  g_assert (g_hash_table_size (self->id_to_command) <= self->n_items);
 
-static GType
-gbp_shellcmd_command_model_get_item_type (GListModel *model)
-{
-  return GBP_TYPE_SHELLCMD_COMMAND;
+  g_list_model_items_changed (G_LIST_MODEL (self), 0, old_len, self->n_items);
+
+  if (old_len != self->n_items)
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_N_ITEMS]);
 }
 
-static gpointer
-gbp_shellcmd_command_model_get_item (GListModel *model,
-                                     guint       position)
+static void
+gbp_shellcmd_command_model_settings_changed_cb (GbpShellcmdCommandModel *self,
+                                                const char              *key,
+                                                GSettings               *settings)
 {
-  GbpShellcmdCommandModel *self = GBP_SHELLCMD_COMMAND_MODEL (model);
+  g_auto(GStrv) commands = NULL;
 
   g_assert (GBP_IS_SHELLCMD_COMMAND_MODEL (self));
-  g_assert (position < self->items->len);
+  g_assert (ide_str_equal0 (key, self->key));
+  g_assert (G_IS_SETTINGS (settings));
+
+  commands = g_settings_get_strv (settings, self->key);
 
-  return g_object_ref (g_ptr_array_index (self->items, position));
+  gbp_shellcmd_command_model_replace (self, g_steal_pointer (&commands));
 }
 
-static guint
-gbp_shellcmd_command_model_get_n_items (GListModel *model)
+static void
+gbp_shellcmd_command_model_constructed (GObject *object)
 {
-  GbpShellcmdCommandModel *self = GBP_SHELLCMD_COMMAND_MODEL (model);
+  GbpShellcmdCommandModel *self = (GbpShellcmdCommandModel *)object;
+  g_autofree char *signal_name = NULL;
 
-  g_assert (GBP_IS_SHELLCMD_COMMAND_MODEL (self));
+  G_OBJECT_CLASS (gbp_shellcmd_command_model_parent_class)->constructed (object);
 
-  return self->items->len;
-}
+  g_assert (self->key != NULL);
+  g_assert (G_IS_SETTINGS (self->settings));
 
-static void
-list_model_iface_init (GListModelInterface *iface)
-{
-  iface->get_item = gbp_shellcmd_command_model_get_item;
-  iface->get_item_type = gbp_shellcmd_command_model_get_item_type;
-  iface->get_n_items = gbp_shellcmd_command_model_get_n_items;
-}
+  self->ids = g_settings_get_strv (self->settings, self->key);
+  self->n_items = g_strv_length (self->ids);
 
-static gchar *
-get_filename (void)
-{
-  return g_build_filename (g_get_user_config_dir (),
-                           ide_get_program_name (),
-                           "external-commands",
-                           NULL);
+  signal_name = g_strconcat ("changed::", self->key, NULL);
+  g_signal_connect_object (self->settings,
+                           signal_name,
+                           G_CALLBACK (gbp_shellcmd_command_model_settings_changed_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
 }
 
 static void
-set_items (GbpShellcmdCommandModel *self,
-           GPtrArray               *items)
+gbp_shellcmd_command_model_dispose (GObject *object)
 {
-  g_autoptr(GPtrArray) old_items = NULL;
+  GbpShellcmdCommandModel *self = (GbpShellcmdCommandModel *)object;
 
-  g_assert (GBP_IS_SHELLCMD_COMMAND_MODEL (self));
-  g_assert (items != NULL);
+  g_clear_pointer (&self->key, g_free);
+  g_clear_pointer (&self->id_to_command, g_hash_table_unref);
+  g_clear_pointer (&self->ids, g_strfreev);
 
-  old_items = g_steal_pointer (&self->items);
-  self->items = g_ptr_array_ref (items);
+  g_clear_object (&self->settings);
 
-  for (guint i = 0; i < items->len; i++)
-    {
-      GbpShellcmdCommand *command = g_ptr_array_index (items, i);
-
-      g_signal_connect_object (command,
-                               "changed",
-                               G_CALLBACK (on_command_changed_cb),
-                               self,
-                               G_CONNECT_SWAPPED);
-
-      g_signal_connect_object (command,
-                               "notify::shortcut",
-                               G_CALLBACK (on_command_shortcut_changed_cb),
-                               self,
-                               G_CONNECT_SWAPPED);
-    }
-
-  if (old_items->len || self->items->len)
-    g_list_model_items_changed (G_LIST_MODEL (self), 0, old_items->len, self->items->len);
+  G_OBJECT_CLASS (gbp_shellcmd_command_model_parent_class)->dispose (object);
 }
 
-gboolean
-gbp_shellcmd_command_model_load (GbpShellcmdCommandModel  *self,
-                                 GCancellable             *cancellable,
-                                 GError                  **error)
+static void
+gbp_shellcmd_command_model_get_property (GObject    *object,
+                                         guint       prop_id,
+                                         GValue     *value,
+                                         GParamSpec *pspec)
 {
-  g_autofree gchar *path = NULL;
-  g_autoptr(GPtrArray) items = NULL;
-  g_autoptr(GKeyFile) keyfile = NULL;
-  g_autoptr(GError) err = NULL;
-  g_auto(GStrv) groups = NULL;
-  gsize len;
-
-  g_return_val_if_fail (GBP_IS_SHELLCMD_COMMAND_MODEL (self), FALSE);
-  g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE);
-
-  path = get_filename ();
-  keyfile = g_key_file_new ();
-  items = g_ptr_array_new_with_free_func ((GDestroyNotify)ide_object_unref_and_destroy);
-
-  /* Parse keybindings keyfile from storage, but ignore if missing */
-  if (!g_key_file_load_from_file (keyfile, path, G_KEY_FILE_KEEP_COMMENTS, &err))
-    {
-      if (g_error_matches (err, G_IO_ERROR, G_IO_ERROR_NOT_FOUND) ||
-          g_error_matches (err, G_FILE_ERROR, G_FILE_ERROR_NOENT))
-        return TRUE;
+  GbpShellcmdCommandModel *self = GBP_SHELLCMD_COMMAND_MODEL (object);
 
-      g_propagate_error (error, g_steal_pointer (&err));
-      return FALSE;
-    }
-
-  groups = g_key_file_get_groups (keyfile, &len);
-
-  for (guint i = 0; i < len; i++)
+  switch (prop_id)
     {
-      g_autoptr(GbpShellcmdCommand) command = NULL;
-      g_autoptr(GError) cmderr = NULL;
+    case PROP_KEY:
+      g_value_set_string (value, self->key);
+      break;
 
-      if (!(command = gbp_shellcmd_command_from_key_file (keyfile, groups[i], &cmderr)))
-        {
-          g_warning ("Failed to parse command from group %s", groups[i]);
-          continue;
-        }
+    case PROP_SETTINGS:
+      g_value_set_object (value, self->settings);
+      break;
 
-      g_ptr_array_add (items, g_steal_pointer (&command));
-    }
+    case PROP_N_ITEMS:
+      g_value_set_uint (value, self->n_items);
+      break;
 
-  g_clear_pointer (&self->keyfile, g_key_file_unref);
-  self->keyfile = g_steal_pointer (&keyfile);
-  set_items (self, items);
-
-  return TRUE;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
 }
 
-gboolean
-gbp_shellcmd_command_model_save (GbpShellcmdCommandModel  *self,
-                                 GCancellable             *cancellable,
-                                 GError                  **error)
+static void
+gbp_shellcmd_command_model_set_property (GObject      *object,
+                                         guint         prop_id,
+                                         const GValue *value,
+                                         GParamSpec   *pspec)
 {
-  g_autofree gchar *path = NULL;
-  g_auto(GStrv) groups = NULL;
-  gsize n_groups = 0;
+  GbpShellcmdCommandModel *self = GBP_SHELLCMD_COMMAND_MODEL (object);
 
-  g_return_val_if_fail (GBP_IS_SHELLCMD_COMMAND_MODEL (self), FALSE);
-  g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE);
-  g_return_val_if_fail (self->keyfile != NULL, FALSE);
-
-  path = get_filename ();
-
-  for (guint i = 0; i < self->items->len; i++)
+  switch (prop_id)
     {
-      GbpShellcmdCommand *command = g_ptr_array_index (self->items, i);
-      gbp_shellcmd_command_to_key_file (command, self->keyfile);
-    }
+    case PROP_KEY:
+      self->key = g_value_dup_string (value);
+      break;
 
-  groups = g_key_file_get_groups (self->keyfile, &n_groups);
+    case PROP_SETTINGS:
+      self->settings = g_value_dup_object (value);
+      break;
 
-  if (n_groups == 0)
-    {
-      g_unlink (path);
-      return TRUE;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
     }
-
-  return g_key_file_save_to_file (self->keyfile, path, error);
 }
 
-/**
- * gbp_shellcmd_command_model_get_command:
- *
- * Returns: (transfer none) (nullable): an #GbpShellcmdCommand or %NULL
- */
-GbpShellcmdCommand *
-gbp_shellcmd_command_model_get_command (GbpShellcmdCommandModel *self,
-                                        const gchar             *command_id)
+static void
+gbp_shellcmd_command_model_class_init (GbpShellcmdCommandModelClass *klass)
 {
-  g_return_val_if_fail (GBP_IS_SHELLCMD_COMMAND_MODEL (self), NULL);
-
-  for (guint i = 0; i < self->items->len; i++)
-    {
-      GbpShellcmdCommand *command = g_ptr_array_index (self->items, i);
-      const gchar *id = gbp_shellcmd_command_get_id (command);
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
 
-      if (ide_str_equal0 (id, command_id))
-        return command;
-    }
+  object_class->constructed = gbp_shellcmd_command_model_constructed;
+  object_class->dispose = gbp_shellcmd_command_model_dispose;
+  object_class->get_property = gbp_shellcmd_command_model_get_property;
+  object_class->set_property = gbp_shellcmd_command_model_set_property;
+
+  properties [PROP_KEY] =
+    g_param_spec_string ("key", NULL, NULL,
+                         NULL,
+                         (G_PARAM_READWRITE |
+                          G_PARAM_CONSTRUCT_ONLY |
+                          G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_SETTINGS] =
+    g_param_spec_object ("settings", NULL, NULL,
+                         G_TYPE_SETTINGS,
+                         (G_PARAM_READWRITE |
+                          G_PARAM_CONSTRUCT_ONLY |
+                          G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_N_ITEMS] =
+    g_param_spec_uint ("n-items", NULL, NULL,
+                       0, G_MAXUINT, 0,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
 
-  return NULL;
+static void
+gbp_shellcmd_command_model_init (GbpShellcmdCommandModel *self)
+{
+  self->id_to_command = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
 }
 
-void
-gbp_shellcmd_command_model_query (GbpShellcmdCommandModel *self,
-                                  GPtrArray               *items,
-                                  const gchar             *typed_text)
+GbpShellcmdCommandModel *
+gbp_shellcmd_command_model_new (GSettings  *settings,
+                                const char *key)
 {
-  g_autofree gchar *q = NULL;
+  g_autoptr(GSettingsSchema) schema = NULL;
 
-  g_return_if_fail (GBP_IS_SHELLCMD_COMMAND_MODEL (self));
-  g_return_if_fail (items != NULL);
-  g_return_if_fail (typed_text != NULL);
+  g_return_val_if_fail (G_SETTINGS (settings), NULL);
+  g_return_val_if_fail (key != NULL, NULL);
 
-  q = g_utf8_casefold (typed_text, -1);
+  g_object_get (settings,
+                "settings-schema", &schema,
+                NULL);
 
-  for (guint i = 0; i < self->items->len; i++)
-    {
-      GbpShellcmdCommand *command = g_ptr_array_index (self->items, i);
-      g_autofree gchar *title = ide_command_get_title (IDE_COMMAND (command));
-      const gchar *cmdstr = gbp_shellcmd_command_get_command (command);
-      guint prio1 = G_MAXINT;
-      guint prio2 = G_MAXINT;
-
-      if (ide_completion_fuzzy_match (title, q, &prio1) ||
-          ide_completion_fuzzy_match (cmdstr, q, &prio2))
-        {
-          GbpShellcmdCommand *copy = gbp_shellcmd_command_copy (command);
-          gbp_shellcmd_command_set_priority (copy, MIN (prio1, prio2));
-          g_ptr_array_add (items, g_steal_pointer (&copy));
-        }
-    }
+  g_return_val_if_fail (schema != NULL, NULL);
+  g_return_val_if_fail (g_settings_schema_has_key (schema, key), NULL);
+
+  return g_object_new (GBP_TYPE_SHELLCMD_COMMAND_MODEL,
+                       "settings", settings,
+                       "key", key,
+                       NULL);
 }
 
-void
-gbp_shellcmd_command_model_add (GbpShellcmdCommandModel *self,
-                                GbpShellcmdCommand      *command)
+GbpShellcmdCommandModel *
+gbp_shellcmd_command_model_new_for_app (void)
 {
-  guint position;
+  g_autoptr(GSettings) settings = NULL;
 
-  g_return_if_fail (GBP_IS_SHELLCMD_COMMAND_MODEL (self));
-  g_return_if_fail (GBP_IS_SHELLCMD_COMMAND (command));
+  settings = g_settings_new_with_path ("org.gnome.builder.shellcmd", SHELLCMD_SETTINGS_BASE);
+  return gbp_shellcmd_command_model_new (settings, "run-commands");
+}
 
-  g_signal_connect_object (command,
-                           "changed",
-                           G_CALLBACK (on_command_changed_cb),
-                           self,
-                           G_CONNECT_SWAPPED);
+GbpShellcmdCommandModel *
+gbp_shellcmd_command_model_new_for_project (IdeContext *context)
+{
+  g_autofree char *project_id = NULL;
+  g_autofree char *project_settings_path = NULL;
+  g_autoptr(GSettings) settings = NULL;
 
-  self->keybindings_changed = TRUE;
+  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
 
-  position = self->items->len;
-  g_ptr_array_add (self->items, g_object_ref (command));
-  g_list_model_items_changed (G_LIST_MODEL (self), position, 0, 1);
+  project_id = ide_context_dup_project_id (context);
+  project_settings_path = g_strconcat (SHELLCMD_SETTINGS_BASE"projects/", project_id, "/", NULL);
+  settings = g_settings_new_with_path ("org.gnome.builder.shellcmd", project_settings_path);
 
-  gbp_shellcmd_command_model_queue_save (self);
+  return gbp_shellcmd_command_model_new (settings, "run-commands");
 }
 
-void
-gbp_shellcmd_command_model_remove (GbpShellcmdCommandModel *self,
-                                   GbpShellcmdCommand      *command)
+GbpShellcmdRunCommand *
+gbp_shellcmd_run_command_create (IdeContext *context)
 {
-  g_return_if_fail (GBP_IS_SHELLCMD_COMMAND_MODEL (self));
-  g_return_if_fail (GBP_IS_SHELLCMD_COMMAND (command));
+  g_autofree char *uuid = NULL;
+  g_autofree char *project_id = NULL;
+  g_autofree char *settings_path = NULL;
+  g_autofree char *parent_path = NULL;
+  g_autoptr(GStrvBuilder) builder = NULL;
+  g_autoptr(GSettings) settings = NULL;
+  g_auto(GStrv) strv = NULL;
 
-  for (guint i = 0; i < self->items->len; i++)
-    {
-      GbpShellcmdCommand *ele = g_ptr_array_index (self->items, i);
+  g_return_val_if_fail (!context || IDE_IS_CONTEXT (context), NULL);
 
-      if (ele == command)
-        {
-          const gchar *id = gbp_shellcmd_command_get_id (ele);
+  uuid = g_uuid_string_random ();
+  if (context != NULL)
+    project_id = ide_context_dup_project_id (context);
 
-          self->keybindings_changed = TRUE;
+  if (project_id == NULL)
+    parent_path = g_strdup (SHELLCMD_SETTINGS_BASE);
+  else
+    parent_path = g_strconcat (SHELLCMD_SETTINGS_BASE"projects/", project_id, "/", NULL);
 
-          if (id != NULL)
-            g_key_file_remove_group (self->keyfile, id, NULL);
+  settings_path = g_strconcat (parent_path, uuid, "/", NULL);
+  settings = g_settings_new_with_path ("org.gnome.builder.shellcmd", parent_path);
+  strv = g_settings_get_strv (settings, "run-commands");
 
-          g_signal_handlers_disconnect_by_func (command,
-                                                G_CALLBACK (on_command_changed_cb),
-                                                self);
+  builder = g_strv_builder_new ();
+  g_strv_builder_addv (builder, (const char **)strv);
+  g_strv_builder_add (builder, uuid);
 
-          g_signal_handlers_disconnect_by_func (command,
-                                                G_CALLBACK (on_command_shortcut_changed_cb),
-                                                self);
+  g_clear_pointer (&strv, g_strfreev);
+  strv = g_strv_builder_end (builder);
 
-          g_ptr_array_remove_index (self->items, i);
-          g_list_model_items_changed (G_LIST_MODEL (self), i, 1, 0);
-          gbp_shellcmd_command_model_queue_save (self);
+  g_settings_set_strv (settings, "run-commands", (const char * const *)strv);
 
-          break;
-        }
-    }
+  return gbp_shellcmd_run_command_new (settings_path);
 }
diff --git a/src/plugins/shellcmd/gbp-shellcmd-command-model.h 
b/src/plugins/shellcmd/gbp-shellcmd-command-model.h
index 6c86f655a..6310c970f 100644
--- a/src/plugins/shellcmd/gbp-shellcmd-command-model.h
+++ b/src/plugins/shellcmd/gbp-shellcmd-command-model.h
@@ -1,6 +1,6 @@
 /* gbp-shellcmd-command-model.h
  *
- * Copyright 2019 Christian Hergert <chergert redhat com>
+ * 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
@@ -20,9 +20,7 @@
 
 #pragma once
 
-#include <gio/gio.h>
-
-#include "gbp-shellcmd-command.h"
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
@@ -30,21 +28,9 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (GbpShellcmdCommandModel, gbp_shellcmd_command_model, GBP, SHELLCMD_COMMAND_MODEL, 
GObject)
 
-GbpShellcmdCommandModel *gbp_shellcmd_command_model_new         (void);
-GbpShellcmdCommand      *gbp_shellcmd_command_model_get_command (GbpShellcmdCommandModel  *self,
-                                                                 const gchar              *command_id);
-void                     gbp_shellcmd_command_model_add         (GbpShellcmdCommandModel  *self,
-                                                                 GbpShellcmdCommand       *command);
-void                     gbp_shellcmd_command_model_remove      (GbpShellcmdCommandModel  *self,
-                                                                 GbpShellcmdCommand       *command);
-void                     gbp_shellcmd_command_model_query       (GbpShellcmdCommandModel  *self,
-                                                                 GPtrArray                *items,
-                                                                 const gchar              *typed_text);
-gboolean                 gbp_shellcmd_command_model_load        (GbpShellcmdCommandModel  *self,
-                                                                 GCancellable             *cancellable,
-                                                                 GError                  **error);
-gboolean                 gbp_shellcmd_command_model_save        (GbpShellcmdCommandModel  *self,
-                                                                 GCancellable             *cancellable,
-                                                                 GError                  **error);
+GbpShellcmdCommandModel *gbp_shellcmd_command_model_new_for_app     (void);
+GbpShellcmdCommandModel *gbp_shellcmd_command_model_new_for_project (IdeContext *context);
+GbpShellcmdCommandModel *gbp_shellcmd_command_model_new             (GSettings  *settings,
+                                                                     const char *key);
 
 G_END_DECLS
diff --git a/src/plugins/shellcmd/gbp-shellcmd-preferences-addin.c 
b/src/plugins/shellcmd/gbp-shellcmd-preferences-addin.c
index c695ed217..28d8d8f01 100644
--- a/src/plugins/shellcmd/gbp-shellcmd-preferences-addin.c
+++ b/src/plugins/shellcmd/gbp-shellcmd-preferences-addin.c
@@ -1,6 +1,6 @@
 /* gbp-shellcmd-preferences-addin.c
  *
- * Copyright 2019 Christian Hergert <chergert redhat com>
+ * 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
@@ -22,112 +22,231 @@
 
 #include "config.h"
 
-#include <dazzle.h>
 #include <glib/gi18n.h>
+
 #include <libide-gui.h>
 
-#include "gbp-shellcmd-application-addin.h"
-#include "gbp-shellcmd-command-editor.h"
+#include "gbp-shellcmd-command-dialog.h"
 #include "gbp-shellcmd-command-model.h"
-#include "gbp-shellcmd-command-row.h"
-#include "gbp-shellcmd-list.h"
 #include "gbp-shellcmd-preferences-addin.h"
+#include "gbp-shellcmd-run-command.h"
 
 struct _GbpShellcmdPreferencesAddin
 {
-  GObject parent_instance;
-
-  GbpShellcmdCommandEditor *editor;
+  GObject               parent_instance;
+  IdePreferencesWindow *window;
+  GSettings            *settings;
 };
 
-static GbpShellcmdCommandModel *
-get_model (void)
+static GtkWidget *
+gbp_shellcmd_preferences_addin_create_row_cb (gpointer item,
+                                              gpointer item_data)
 {
-  GbpShellcmdApplicationAddin *app_addin;
-  GbpShellcmdCommandModel *model;
+  GbpShellcmdRunCommand *command = item;
+  AdwActionRow *row;
+  GtkLabel *accel;
 
-  app_addin = ide_application_find_addin_by_module_name (NULL, "shellcmd");
-  g_assert (GBP_IS_SHELLCMD_APPLICATION_ADDIN (app_addin));
+  g_assert (GBP_IS_SHELLCMD_RUN_COMMAND (command));
 
-  model = gbp_shellcmd_application_addin_get_model (app_addin);
-  g_assert (GBP_IS_SHELLCMD_COMMAND_MODEL (model));
+  row = g_object_new (ADW_TYPE_ACTION_ROW,
+                      "activatable", TRUE,
+                      NULL);
+  g_object_bind_property (command, "display-name", row, "title",
+                          G_BINDING_SYNC_CREATE);
+  g_object_bind_property (command, "subtitle", row, "subtitle",
+                          G_BINDING_SYNC_CREATE);
 
-  return model;
+  accel = g_object_new (GTK_TYPE_LABEL,
+                        "margin-start", 6,
+                        "margin-end", 6,
+                        NULL);
+  g_object_bind_property (command, "accelerator-label", accel, "label",
+                          G_BINDING_SYNC_CREATE);
+  adw_action_row_add_suffix (row, GTK_WIDGET (accel));
+  adw_action_row_add_suffix (row,
+                             g_object_new (GTK_TYPE_IMAGE,
+                                           "icon-name", "go-next-symbolic",
+                                           NULL));
+  g_object_set_data_full (G_OBJECT (row),
+                          "COMMAND",
+                          g_object_ref (command),
+                          g_object_unref);
+
+  return GTK_WIDGET (row);
 }
 
 static void
-on_command_selected_cb (GbpShellcmdPreferencesAddin *self,
-                        GbpShellcmdCommand          *command,
-                        GbpShellcmdList             *list)
+on_row_activated_cb (GtkListBox           *list_box,
+                     AdwActionRow         *row,
+                     IdePreferencesWindow *window)
 {
-  GtkWidget *preferences;
+  g_autoptr(GbpShellcmdRunCommand) new_command = NULL;
+  GbpShellcmdRunCommand *command;
+  GbpShellcmdCommandDialog *dialog;
 
-  g_assert (GBP_IS_SHELLCMD_PREFERENCES_ADDIN (self));
-  g_assert (!command || GBP_IS_SHELLCMD_COMMAND (command));
-  g_assert (GBP_IS_SHELLCMD_LIST (list));
+  IDE_ENTRY;
 
-  if (!(preferences = gtk_widget_get_ancestor (GTK_WIDGET (list), DZL_TYPE_PREFERENCES)))
-    return;
+  g_assert (GTK_IS_LIST_BOX (list_box));
+  g_assert (ADW_IS_ACTION_ROW (row));
+  g_assert (IDE_IS_PREFERENCES_WINDOW (window));
 
-  if (command != NULL)
+  command = g_object_get_data (G_OBJECT (row), "COMMAND");
+
+  g_assert (!command || GBP_IS_SHELLCMD_RUN_COMMAND (command));
+
+  if (command == NULL)
     {
-      g_autoptr(GHashTable) map = NULL;
+      IdePreferencesMode mode = ide_preferences_window_get_mode (window);
+      IdeContext *context = ide_preferences_window_get_context (window);
+
+      if (mode == IDE_PREFERENCES_MODE_PROJECT)
+        new_command = gbp_shellcmd_run_command_create (context);
+      else
+        new_command = gbp_shellcmd_run_command_create (NULL);
+
+      command = new_command;
+    }
+
+  dialog = gbp_shellcmd_command_dialog_new (command, new_command != NULL);
+  gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (window));
+  gtk_window_set_modal (GTK_WINDOW (dialog), TRUE);
+  gtk_window_present (GTK_WINDOW (dialog));
+
+  IDE_EXIT;
+}
+
+static void
+handle_shellcmd_list (const char                   *page_name,
+                      const IdePreferenceItemEntry *entry,
+                      AdwPreferencesGroup          *group,
+                      gpointer                      user_data)
+{
+  IdePreferencesWindow *window = user_data;
+  GbpShellcmdCommandModel *model;
+  IdePreferencesMode mode;
+  AdwActionRow *create_row;
+  IdeContext *context;
+  GtkListBox *list_box;
+  GtkLabel *label;
+
+  IDE_ENTRY;
 
-      map = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, g_free);
-      g_hash_table_insert (map, (gchar *)"{id}", g_strdup (gbp_shellcmd_command_get_id (command)));
-      dzl_preferences_set_page (DZL_PREFERENCES (preferences), "shellcmd.id", map);
+  g_assert (ide_str_equal0 (page_name, "commands"));
+  g_assert (ADW_IS_PREFERENCES_GROUP (group));
+  g_assert (IDE_IS_PREFERENCES_WINDOW (window));
+
+  context = ide_preferences_window_get_context (window);
+  mode = ide_preferences_window_get_mode (window);
+
+  if (mode == IDE_PREFERENCES_MODE_PROJECT)
+    {
+      model = gbp_shellcmd_command_model_new_for_project (context);
+      adw_preferences_group_set_title (group, _("Project Commands"));
+    }
+  else
+    {
+      model = gbp_shellcmd_command_model_new_for_app ();
+      adw_preferences_group_set_title (group, _("Shared Commands"));
     }
 
-  gbp_shellcmd_command_editor_set_command (self->editor, command);
+  list_box = g_object_new (GTK_TYPE_LIST_BOX,
+                           "css-classes", IDE_STRV_INIT ("boxed-list"),
+                           "selection-mode", GTK_SELECTION_NONE,
+                           NULL);
+  create_row = g_object_new (ADW_TYPE_ACTION_ROW,
+                             "activatable", TRUE,
+                             "title", _("Create Command"),
+                             "subtitle", _("Commands can be used to build, run, or modify your projects"),
+                             NULL);
+  adw_action_row_add_suffix (create_row,
+                             g_object_new (GTK_TYPE_IMAGE,
+                                           "icon-name", "go-next-symbolic",
+                                           NULL));
+  g_signal_connect_object (list_box,
+                           "row-activated",
+                           G_CALLBACK (on_row_activated_cb),
+                           window,
+                           0);
+  gtk_list_box_append (list_box, GTK_WIDGET (create_row));
+  adw_preferences_group_add (group, GTK_WIDGET (list_box));
+
+  label = g_object_new (GTK_TYPE_LABEL,
+                        "css-classes", IDE_STRV_INIT ("dim-label", "caption"),
+                        "margin-top", 6,
+                        "xalign", .0f,
+                        NULL);
+  if (mode == IDE_PREFERENCES_MODE_PROJECT)
+    gtk_label_set_label (label,
+                         _("These commands may be run from this project only."));
+  else
+    gtk_label_set_label (label,
+                         _("These commands may be shared across any project in Builder."));
+  adw_preferences_group_add (group, GTK_WIDGET (label));
+
+  list_box = g_object_new (GTK_TYPE_LIST_BOX,
+                           "css-classes", IDE_STRV_INIT ("boxed-list"),
+                           "selection-mode", GTK_SELECTION_NONE,
+                           "margin-top", 18,
+                           NULL);
+  gtk_list_box_bind_model (list_box,
+                           G_LIST_MODEL (model),
+                           gbp_shellcmd_preferences_addin_create_row_cb,
+                           NULL, NULL);
+  adw_preferences_group_add (group, GTK_WIDGET (list_box));
+  ide_gtk_widget_hide_when_empty (GTK_WIDGET (list_box), G_LIST_MODEL (model));
+  g_signal_connect_object (list_box,
+                           "row-activated",
+                           G_CALLBACK (on_row_activated_cb),
+                           window,
+                           0);
+
+
+  IDE_EXIT;
 }
 
+static const IdePreferenceGroupEntry groups[] = {
+  { "commands", "shellcmd", 0 },
+};
+
+static const IdePreferenceItemEntry items[] = {
+  { "commands", "shellcmd", "list", 0, handle_shellcmd_list },
+};
+
 static void
-gbp_shellcmd_preferences_addin_load (IdePreferencesAddin *addin,
-                                     DzlPreferences      *prefs)
+gbp_shellcmd_preferences_addin_load (IdePreferencesAddin  *addin,
+                                     IdePreferencesWindow *window,
+                                     IdeContext           *context)
 {
   GbpShellcmdPreferencesAddin *self = (GbpShellcmdPreferencesAddin *)addin;
-  GtkWidget *list;
+
+  IDE_ENTRY;
 
   g_assert (GBP_IS_SHELLCMD_PREFERENCES_ADDIN (self));
-  g_assert (DZL_IS_PREFERENCES (prefs));
-
-  dzl_preferences_add_page (prefs, "shellcmd", _("External Commands"), 650);
-  dzl_preferences_add_group (prefs, "shellcmd", "commands", _("External Commands"), 0);
-
-  list = gbp_shellcmd_list_new (get_model ());
-  g_signal_connect_object (list,
-                           "command-selected",
-                           G_CALLBACK (on_command_selected_cb),
-                           self,
-                           G_CONNECT_SWAPPED);
-  dzl_preferences_add_custom (prefs, "shellcmd", "commands", list, NULL, 0);
-
-  dzl_preferences_add_page (prefs, "shellcmd.id", NULL, 0);
-  dzl_preferences_add_group (prefs, "shellcmd.id", "basic", _("Command"), 0);
-
-  self->editor = g_object_new (GBP_TYPE_SHELLCMD_COMMAND_EDITOR,
-                               "visible", TRUE,
-                               NULL);
-  g_signal_connect (self->editor,
-                    "destroy",
-                    G_CALLBACK (gtk_widget_destroyed),
-                    &self->editor);
-  dzl_preferences_add_custom (prefs, "shellcmd.id", "basic", GTK_WIDGET (self->editor), NULL, 0);
+  g_assert (IDE_IS_PREFERENCES_WINDOW (window));
+
+  self->window = window;
+
+  ide_preferences_window_add_groups (window, groups, G_N_ELEMENTS (groups), GETTEXT_PACKAGE);
+  ide_preferences_window_add_items (window, items, G_N_ELEMENTS (items), window, NULL);
+
+  IDE_EXIT;
 }
 
 static void
-gbp_shellcmd_preferences_addin_unload (IdePreferencesAddin *addin,
-                                       DzlPreferences      *prefs)
+gbp_shellcmd_preferences_addin_unload (IdePreferencesAddin  *addin,
+                                       IdePreferencesWindow *window,
+                                       IdeContext           *context)
 {
   GbpShellcmdPreferencesAddin *self = (GbpShellcmdPreferencesAddin *)addin;
 
+  IDE_ENTRY;
+
   g_assert (GBP_IS_SHELLCMD_PREFERENCES_ADDIN (self));
-  g_assert (DZL_IS_PREFERENCES (prefs));
+  g_assert (IDE_IS_PREFERENCES_WINDOW (window));
 
-  if (self->editor != NULL)
-    gtk_widget_destroy (GTK_WIDGET (self->editor));
+  self->window = NULL;
 
-  g_assert (self->editor == NULL);
+  IDE_EXIT;
 }
 
 static void
@@ -138,7 +257,7 @@ preferences_addin_iface_init (IdePreferencesAddinInterface *iface)
 }
 
 G_DEFINE_FINAL_TYPE_WITH_CODE (GbpShellcmdPreferencesAddin, gbp_shellcmd_preferences_addin, G_TYPE_OBJECT,
-                         G_IMPLEMENT_INTERFACE (IDE_TYPE_PREFERENCES_ADDIN, preferences_addin_iface_init))
+                               G_IMPLEMENT_INTERFACE (IDE_TYPE_PREFERENCES_ADDIN, 
preferences_addin_iface_init))
 
 static void
 gbp_shellcmd_preferences_addin_class_init (GbpShellcmdPreferencesAddinClass *klass)
diff --git a/src/plugins/shellcmd/gbp-shellcmd-preferences-addin.h 
b/src/plugins/shellcmd/gbp-shellcmd-preferences-addin.h
index 6041fa0c1..f9520182e 100644
--- a/src/plugins/shellcmd/gbp-shellcmd-preferences-addin.h
+++ b/src/plugins/shellcmd/gbp-shellcmd-preferences-addin.h
@@ -1,6 +1,6 @@
 /* gbp-shellcmd-preferences-addin.h
  *
- * Copyright 2019 Christian Hergert <chergert redhat com>
+ * 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
diff --git a/src/plugins/shellcmd/gbp-shellcmd-run-command-provider.c 
b/src/plugins/shellcmd/gbp-shellcmd-run-command-provider.c
new file mode 100644
index 000000000..93d28be31
--- /dev/null
+++ b/src/plugins/shellcmd/gbp-shellcmd-run-command-provider.c
@@ -0,0 +1,109 @@
+/* gbp-shellcmd-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 "gbp-shellcmd-run-command-provider"
+
+#include "config.h"
+
+#include <libide-threading.h>
+
+#include "gbp-shellcmd-command-model.h"
+#include "gbp-shellcmd-run-command.h"
+#include "gbp-shellcmd-run-command-provider.h"
+
+struct _GbpShellcmdRunCommandProvider
+{
+  IdeObject parent_instance;
+};
+
+static void
+gbp_shellcmd_run_command_provider_list_commands_async (IdeRunCommandProvider *provider,
+                                                       GCancellable          *cancellable,
+                                                       GAsyncReadyCallback    callback,
+                                                       gpointer               user_data)
+{
+  g_autoptr(GbpShellcmdCommandModel) app_commands = NULL;
+  g_autoptr(GbpShellcmdCommandModel) project_commands = NULL;
+  g_autoptr(GListStore) store = NULL;
+  g_autoptr(IdeTask) task = NULL;
+  IdeContext *context;
+
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_SHELLCMD_RUN_COMMAND_PROVIDER (provider));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (provider, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, gbp_shellcmd_run_command_provider_list_commands_async);
+
+  context = ide_object_get_context (IDE_OBJECT (provider));
+
+  app_commands = gbp_shellcmd_command_model_new_for_app ();
+  project_commands = gbp_shellcmd_command_model_new_for_project (context);
+
+  store = g_list_store_new (G_TYPE_LIST_MODEL);
+  g_list_store_append (store, project_commands);
+  g_list_store_append (store, app_commands);
+
+  ide_task_return_pointer (task,
+                           g_object_new (GTK_TYPE_FLATTEN_LIST_MODEL,
+                                         "model", store,
+                                         NULL),
+                           g_object_unref);
+
+  IDE_EXIT;
+}
+
+static GListModel *
+gbp_shellcmd_run_command_provider_list_commands_finish (IdeRunCommandProvider  *provider,
+                                                        GAsyncResult           *result,
+                                                        GError                **error)
+{
+  GListModel *ret;
+
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_SHELLCMD_RUN_COMMAND_PROVIDER (provider));
+  g_assert (IDE_IS_TASK (result));
+
+  ret = ide_task_propagate_pointer (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+static void
+run_command_provider_iface_init (IdeRunCommandProviderInterface *iface)
+{
+  iface->list_commands_async = gbp_shellcmd_run_command_provider_list_commands_async;
+  iface->list_commands_finish = gbp_shellcmd_run_command_provider_list_commands_finish;
+}
+
+G_DEFINE_FINAL_TYPE_WITH_CODE (GbpShellcmdRunCommandProvider, gbp_shellcmd_run_command_provider, 
IDE_TYPE_OBJECT,
+                               G_IMPLEMENT_INTERFACE (IDE_TYPE_RUN_COMMAND_PROVIDER, 
run_command_provider_iface_init))
+
+static void
+gbp_shellcmd_run_command_provider_class_init (GbpShellcmdRunCommandProviderClass *klass)
+{
+}
+
+static void
+gbp_shellcmd_run_command_provider_init (GbpShellcmdRunCommandProvider *self)
+{
+}
diff --git a/src/plugins/shellcmd/gbp-shellcmd-list.h 
b/src/plugins/shellcmd/gbp-shellcmd-run-command-provider.h
similarity index 67%
rename from src/plugins/shellcmd/gbp-shellcmd-list.h
rename to src/plugins/shellcmd/gbp-shellcmd-run-command-provider.h
index 2ac4cf91c..91e9ea4e9 100644
--- a/src/plugins/shellcmd/gbp-shellcmd-list.h
+++ b/src/plugins/shellcmd/gbp-shellcmd-run-command-provider.h
@@ -1,6 +1,6 @@
-/* gbp-shellcmd-list.h
+/* gbp-shellcmd-run-command-provider.h
  *
- * Copyright 2019 Christian Hergert <chergert redhat com>
+ * 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
@@ -20,16 +20,12 @@
 
 #pragma once
 
-#include <gtk/gtk.h>
-
-#include "gbp-shellcmd-command-model.h"
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
-#define GBP_TYPE_SHELLCMD_LIST (gbp_shellcmd_list_get_type())
-
-G_DECLARE_FINAL_TYPE (GbpShellcmdList, gbp_shellcmd_list, GBP, SHELLCMD_LIST, GtkFrame)
+#define GBP_TYPE_SHELLCMD_RUN_COMMAND_PROVIDER (gbp_shellcmd_run_command_provider_get_type())
 
-GtkWidget *gbp_shellcmd_list_new (GbpShellcmdCommandModel *model);
+G_DECLARE_FINAL_TYPE (GbpShellcmdRunCommandProvider, gbp_shellcmd_run_command_provider, GBP, 
SHELLCMD_RUN_COMMAND_PROVIDER, IdeObject)
 
 G_END_DECLS
diff --git a/src/plugins/shellcmd/gbp-shellcmd-run-command.c b/src/plugins/shellcmd/gbp-shellcmd-run-command.c
new file mode 100644
index 000000000..293cbd080
--- /dev/null
+++ b/src/plugins/shellcmd/gbp-shellcmd-run-command.c
@@ -0,0 +1,416 @@
+/* gbp-shellcmd-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 "gbp-shellcmd-run-command"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "gbp-shellcmd-enums.h"
+#include "gbp-shellcmd-run-command.h"
+
+struct _GbpShellcmdRunCommand
+{
+  IdeRunCommand        parent_instance;
+
+  char                *settings_path;
+  GSettings           *settings;
+  char                *id;
+  char                *accelerator;
+
+  GbpShellcmdLocality  locality;
+};
+
+enum {
+  PROP_0,
+  PROP_ACCELERATOR,
+  PROP_ACCELERATOR_LABEL,
+  PROP_LOCALITY,
+  PROP_SETTINGS_PATH,
+  PROP_SUBTITLE,
+  N_PROPS
+};
+
+G_DEFINE_FINAL_TYPE (GbpShellcmdRunCommand, gbp_shellcmd_run_command, IDE_TYPE_RUN_COMMAND)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+gbp_shellcmd_run_command_prepare_to_run (IdeRunCommand *run_command,
+                                         IdeRunContext *run_context,
+                                         IdeContext    *context)
+{
+  GbpShellcmdRunCommand *self = (GbpShellcmdRunCommand *)run_command;
+  IdePipeline *pipeline = NULL;
+  IdeRuntime *runtime = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_SHELLCMD_RUN_COMMAND (self));
+  g_assert (IDE_IS_RUN_CONTEXT (run_context));
+  g_assert (IDE_IS_CONTEXT (context));
+
+  if (ide_context_has_project (context))
+    {
+      IdeBuildManager *build_manager = ide_build_manager_from_context (context);
+
+      if ((pipeline = ide_build_manager_get_pipeline (build_manager)))
+        runtime = ide_pipeline_get_runtime (pipeline);
+    }
+
+  switch (self->locality)
+    {
+    case GBP_SHELLCMD_LOCALITY_PIPELINE:
+      if (pipeline == NULL)
+        ide_run_context_push_error (run_context,
+                                    g_error_new (G_IO_ERROR,
+                                                 G_IO_ERROR_NOT_INITIALIZED,
+                                                 "No pipeline available for run command"));
+      else
+        ide_pipeline_prepare_run_context (pipeline, run_context);
+      break;
+
+    case GBP_SHELLCMD_LOCALITY_HOST:
+      ide_run_context_push_host (run_context);
+      break;
+
+    case GBP_SHELLCMD_LOCALITY_SUBPROCESS:
+      break;
+
+    case GBP_SHELLCMD_LOCALITY_RUNTIME: {
+      if (pipeline == NULL || runtime == NULL)
+        ide_run_context_push_error (run_context,
+                                    g_error_new (G_IO_ERROR,
+                                                 G_IO_ERROR_NOT_INITIALIZED,
+                                                 "No pipeline available for run command"));
+      else
+        ide_runtime_prepare_to_run (runtime, pipeline, run_context);
+      break;
+    }
+
+    default:
+      g_assert_not_reached ();
+    }
+
+  IDE_RUN_COMMAND_CLASS (gbp_shellcmd_run_command_parent_class)->prepare_to_run (run_command, run_context, 
context);
+
+  IDE_EXIT;
+}
+
+static void
+gbp_shellcmd_run_command_constructed (GObject *object)
+{
+  GbpShellcmdRunCommand *self = (GbpShellcmdRunCommand *)object;
+  g_autofree char *id = NULL;
+  g_auto(GStrv) path_split = NULL;
+  gsize n_parts;
+
+  g_assert (GBP_IS_SHELLCMD_RUN_COMMAND (self));
+  g_assert (self->settings_path != NULL);
+  g_assert (g_str_has_suffix (self->settings_path, "/"));
+
+  self->settings = g_settings_new_with_path ("org.gnome.builder.shellcmd.command", self->settings_path);
+
+  path_split = g_strsplit (self->settings_path, "/", 0);
+  n_parts = g_strv_length (path_split);
+  g_assert (n_parts >= 2);
+
+  self->id = g_strdup (path_split[n_parts-2]);
+  id = g_strdup_printf ("shellcmd:%s", self->id);
+
+  ide_run_command_set_id (IDE_RUN_COMMAND (self), id);
+  g_settings_bind (self->settings, "display-name", self, "display-name", G_SETTINGS_BIND_DEFAULT);
+  g_settings_bind (self->settings, "env", self, "environ", G_SETTINGS_BIND_DEFAULT);
+  g_settings_bind (self->settings, "argv", self, "argv", G_SETTINGS_BIND_DEFAULT);
+  g_settings_bind (self->settings, "cwd", self, "cwd", G_SETTINGS_BIND_DEFAULT);
+  g_settings_bind (self->settings, "accelerator", self, "accelerator", G_SETTINGS_BIND_DEFAULT);
+  g_settings_bind (self->settings, "locality", self, "locality", G_SETTINGS_BIND_DEFAULT);
+}
+
+static void
+subtitle_changed_cb (GbpShellcmdRunCommand *self)
+{
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SUBTITLE]);
+}
+
+static char *
+get_subtitle (GbpShellcmdRunCommand *self)
+{
+  g_autofree char *joined = NULL;
+  const char * const *argv;
+  const char *cwd;
+
+  g_assert (GBP_IS_SHELLCMD_RUN_COMMAND (self));
+
+  argv = ide_run_command_get_argv (IDE_RUN_COMMAND (self));
+  cwd = ide_run_command_get_cwd (IDE_RUN_COMMAND (self));
+
+  if (argv != NULL)
+    joined = g_strjoinv (" ", (char **)argv);
+
+  if (joined && cwd)
+    /* something like a bash prompt */
+    return g_strdup_printf ("<tt>%s&gt; %s</tt>", cwd, joined);
+
+  if (cwd)
+    return g_strdup_printf ("%s&gt; ", cwd);
+
+  return g_steal_pointer (&joined);
+}
+
+static void
+accelerator_label_changed_cb (GbpShellcmdRunCommand *self)
+{
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ACCELERATOR_LABEL]);
+}
+
+static char *
+get_accelerator_label (GbpShellcmdRunCommand *self)
+{
+  GdkModifierType state;
+  guint keyval;
+
+  if (ide_str_empty0 (self->accelerator))
+    return NULL;
+
+  if (gtk_accelerator_parse (self->accelerator, &keyval, &state))
+    return gtk_accelerator_get_label (keyval, state);
+
+  return NULL;
+}
+
+static void
+gbp_shellcmd_run_command_dispose (GObject *object)
+{
+  GbpShellcmdRunCommand *self = (GbpShellcmdRunCommand *)object;
+
+  g_clear_pointer (&self->accelerator, g_free);
+  g_clear_pointer (&self->id, g_free);
+  g_clear_pointer (&self->settings_path, g_free);
+  g_clear_object (&self->settings);
+
+  G_OBJECT_CLASS (gbp_shellcmd_run_command_parent_class)->dispose (object);
+}
+
+static void
+gbp_shellcmd_run_command_get_property (GObject    *object,
+                                       guint       prop_id,
+                                       GValue     *value,
+                                       GParamSpec *pspec)
+{
+  GbpShellcmdRunCommand *self = GBP_SHELLCMD_RUN_COMMAND (object);
+
+  switch (prop_id)
+    {
+    case PROP_ACCELERATOR:
+      g_value_set_string (value, gbp_shellcmd_run_command_get_accelerator (self));
+      break;
+
+    case PROP_ACCELERATOR_LABEL:
+      g_value_take_string (value, get_accelerator_label (self));
+      break;
+
+    case PROP_LOCALITY:
+      g_value_set_enum (value, gbp_shellcmd_run_command_get_locality (self));
+      break;
+
+    case PROP_SETTINGS_PATH:
+      g_value_set_string (value, self->settings_path);
+      break;
+
+    case PROP_SUBTITLE:
+      g_value_take_string (value, get_subtitle (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_shellcmd_run_command_set_property (GObject      *object,
+                                       guint         prop_id,
+                                       const GValue *value,
+                                       GParamSpec   *pspec)
+{
+  GbpShellcmdRunCommand *self = GBP_SHELLCMD_RUN_COMMAND (object);
+
+  switch (prop_id)
+    {
+    case PROP_ACCELERATOR:
+      gbp_shellcmd_run_command_set_accelerator (self, g_value_get_string (value));
+      break;
+
+    case PROP_LOCALITY:
+      gbp_shellcmd_run_command_set_locality (self, g_value_get_enum (value));
+      break;
+
+    case PROP_SETTINGS_PATH:
+      self->settings_path = g_value_dup_string (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_shellcmd_run_command_class_init (GbpShellcmdRunCommandClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeRunCommandClass *run_command_class = IDE_RUN_COMMAND_CLASS (klass);
+
+  object_class->constructed = gbp_shellcmd_run_command_constructed;
+  object_class->dispose = gbp_shellcmd_run_command_dispose;
+  object_class->get_property = gbp_shellcmd_run_command_get_property;
+  object_class->set_property = gbp_shellcmd_run_command_set_property;
+
+  run_command_class->prepare_to_run = gbp_shellcmd_run_command_prepare_to_run;
+
+  properties [PROP_ACCELERATOR] =
+    g_param_spec_string ("accelerator", NULL, NULL, NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_ACCELERATOR_LABEL] =
+    g_param_spec_string ("accelerator-label", NULL, NULL, NULL,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_LOCALITY] =
+    g_param_spec_enum ("locality", NULL, NULL,
+                       GBP_TYPE_SHELLCMD_LOCALITY,
+                       GBP_SHELLCMD_LOCALITY_PIPELINE,
+                       (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_SETTINGS_PATH] =
+    g_param_spec_string ("settings-path", NULL, NULL, NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_SUBTITLE] =
+    g_param_spec_string ("subtitle", NULL, NULL, NULL,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+gbp_shellcmd_run_command_init (GbpShellcmdRunCommand *self)
+{
+  self->locality = GBP_SHELLCMD_LOCALITY_PIPELINE;
+
+  ide_run_command_set_kind (IDE_RUN_COMMAND (self),
+                            IDE_RUN_COMMAND_KIND_USER_DEFINED);
+
+  g_signal_connect (self, "notify::accelerator", G_CALLBACK (accelerator_label_changed_cb), NULL);
+  g_signal_connect (self, "notify::cwd", G_CALLBACK (subtitle_changed_cb), NULL);
+  g_signal_connect (self, "notify::argv", G_CALLBACK (subtitle_changed_cb), NULL);
+}
+
+GbpShellcmdRunCommand *
+gbp_shellcmd_run_command_new (const char *settings_path)
+{
+  return g_object_new (GBP_TYPE_SHELLCMD_RUN_COMMAND,
+                       "settings-path", settings_path,
+                       NULL);
+}
+
+void
+gbp_shellcmd_run_command_delete (GbpShellcmdRunCommand *self)
+{
+  g_autoptr(GSettingsSchema) schema = NULL;
+  g_autoptr(GStrvBuilder) builder = NULL;
+  g_autoptr(GSettings) list = NULL;
+  g_autoptr(GString) parent_path = NULL;
+  g_auto(GStrv) commands = NULL;
+  g_auto(GStrv) keys = NULL;
+
+  g_return_if_fail (GBP_IS_SHELLCMD_RUN_COMMAND (self));
+
+  /* Get parent settings path */
+  parent_path = g_string_new (self->settings_path);
+  if (parent_path->len)
+    g_string_truncate (parent_path, parent_path->len-1);
+  while (parent_path->len && parent_path->str[parent_path->len-1] != '/')
+    g_string_truncate (parent_path, parent_path->len-1);
+
+  /* First remove the item from the parent list of commands */
+  list = g_settings_new_with_path ("org.gnome.builder.shellcmd", parent_path->str);
+  commands = g_settings_get_strv (list, "run-commands");
+  builder = g_strv_builder_new ();
+  for (guint i = 0; commands[i]; i++)
+    {
+      if (!ide_str_equal0 (commands[i], self->id))
+        g_strv_builder_add (builder, commands[i]);
+    }
+  g_clear_pointer (&commands, g_strfreev);
+  commands = g_strv_builder_end (builder);
+  g_settings_set_strv (list, "run-commands", (const char * const *)commands);
+
+  /* Now reset the keys so the entry does not take up space in storage */
+  g_object_get (self->settings,
+                "settings-schema", &schema,
+                NULL);
+  keys = g_settings_schema_list_keys (schema);
+  for (guint i = 0; keys[i]; i++)
+    g_settings_reset (self->settings, keys[i]);
+}
+
+const char *
+gbp_shellcmd_run_command_get_accelerator (GbpShellcmdRunCommand *self)
+{
+  g_return_val_if_fail (GBP_IS_SHELLCMD_RUN_COMMAND (self), NULL);
+
+  return self->accelerator;
+}
+
+void
+gbp_shellcmd_run_command_set_accelerator (GbpShellcmdRunCommand *self,
+                                          const char            *accelerator)
+{
+  g_return_if_fail (GBP_IS_SHELLCMD_RUN_COMMAND (self));
+
+  if (ide_set_string (&self->accelerator, accelerator))
+    {
+      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ACCELERATOR]);
+      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ACCELERATOR_LABEL]);
+    }
+}
+
+GbpShellcmdLocality
+gbp_shellcmd_run_command_get_locality (GbpShellcmdRunCommand *self)
+{
+  g_return_val_if_fail (GBP_IS_SHELLCMD_RUN_COMMAND (self), 0);
+
+  return self->locality;
+}
+
+void
+gbp_shellcmd_run_command_set_locality (GbpShellcmdRunCommand *self,
+                                       GbpShellcmdLocality    locality)
+{
+  g_return_if_fail (GBP_IS_SHELLCMD_RUN_COMMAND (self));
+
+  if (locality != self->locality)
+    {
+      self->locality = locality;
+      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_LOCALITY]);
+    }
+}
diff --git a/src/plugins/shellcmd/gbp-shellcmd-run-command.h b/src/plugins/shellcmd/gbp-shellcmd-run-command.h
new file mode 100644
index 000000000..ee1a524b7
--- /dev/null
+++ b/src/plugins/shellcmd/gbp-shellcmd-run-command.h
@@ -0,0 +1,57 @@
+/* gbp-shellcmd-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
+
+#include <libide-foundry.h>
+#include <libide-terminal.h>
+
+G_BEGIN_DECLS
+
+/**
+ * GbpShellcmdLocality:
+ * %GBP_SHELLCMD_LOCALITY_SUBPROCESS: run as a subprocess of builder
+ * %GBP_SHELLCMD_LOCALITY_HOST: run on the host system, possibly bypassing container
+ * %GBP_SHELLCMD_LOCALITY_PIPELINE: run from build pipeline
+ * %GBP_SHELLCMD_LOCALITY_RUNTIME: run like a target application
+ */
+typedef enum
+{
+  GBP_SHELLCMD_LOCALITY_SUBPROCESS = 0,
+  GBP_SHELLCMD_LOCALITY_HOST,
+  GBP_SHELLCMD_LOCALITY_PIPELINE,
+  GBP_SHELLCMD_LOCALITY_RUNTIME,
+} GbpShellcmdLocality;
+
+#define GBP_TYPE_SHELLCMD_RUN_COMMAND (gbp_shellcmd_run_command_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpShellcmdRunCommand, gbp_shellcmd_run_command, GBP, SHELLCMD_RUN_COMMAND, 
IdeRunCommand)
+
+GbpShellcmdRunCommand *gbp_shellcmd_run_command_new             (const char            *settings_path);
+GbpShellcmdRunCommand *gbp_shellcmd_run_command_create          (IdeContext            *context);
+void                   gbp_shellcmd_run_command_delete          (GbpShellcmdRunCommand *self);
+const char            *gbp_shellcmd_run_command_get_accelerator (GbpShellcmdRunCommand *self);
+void                   gbp_shellcmd_run_command_set_accelerator (GbpShellcmdRunCommand *self,
+                                                                 const char            *accelerator);
+GbpShellcmdLocality    gbp_shellcmd_run_command_get_locality    (GbpShellcmdRunCommand *self);
+void                   gbp_shellcmd_run_command_set_locality    (GbpShellcmdRunCommand *self,
+                                                                 GbpShellcmdLocality    locality);
+
+G_END_DECLS
diff --git a/src/plugins/shellcmd/gbp-shellcmd-shortcut-provider.c 
b/src/plugins/shellcmd/gbp-shellcmd-shortcut-provider.c
new file mode 100644
index 000000000..b96169602
--- /dev/null
+++ b/src/plugins/shellcmd/gbp-shellcmd-shortcut-provider.c
@@ -0,0 +1,265 @@
+/* gbp-shellcmd-shortcut-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 "gbp-shellcmd-shortcut-provider"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include <libide-editor.h>
+#include <libide-gui.h>
+#include <libide-terminal.h>
+
+#include "gbp-shellcmd-command-model.h"
+#include "gbp-shellcmd-run-command.h"
+#include "gbp-shellcmd-shortcut-provider.h"
+
+struct _GbpShellcmdShortcutProvider
+{
+  IdeObject   parent_instance;
+  GListStore *model;
+};
+
+static gboolean
+gbp_shellcmd_shortcut_func (GtkWidget *widget,
+                            GVariant  *args,
+                            gpointer   user_data)
+{
+  GbpShellcmdRunCommand *run_command = user_data;
+  g_autoptr(IdePanelPosition) position = NULL;
+  g_autoptr(IdeTerminalLauncher) launcher = NULL;
+  IdeWorkspace *workspace;
+  IdeContext *context;
+  const char *title;
+  IdePage *page;
+
+  IDE_ENTRY;
+
+  g_assert (GTK_IS_WIDGET (widget));
+  g_assert (args == NULL);
+  g_assert (GBP_IS_SHELLCMD_RUN_COMMAND (run_command));
+
+  g_debug ("Shortcut triggered to run command “%s” which has accelerator %s",
+           ide_run_command_get_display_name (IDE_RUN_COMMAND (run_command)),
+           gbp_shellcmd_run_command_get_accelerator (run_command));
+
+  if (!(workspace = ide_widget_get_workspace (widget)) ||
+      !(context = ide_workspace_get_context (workspace)))
+    IDE_RETURN (FALSE);
+
+  if (!IDE_IS_PRIMARY_WORKSPACE (workspace) &&
+      !IDE_IS_EDITOR_WORKSPACE (workspace))
+    IDE_RETURN (FALSE);
+
+  if (!(title = ide_run_command_get_display_name (IDE_RUN_COMMAND (run_command))))
+    title = _("Untitled command");
+
+  launcher = ide_terminal_launcher_new (context, IDE_RUN_COMMAND (run_command));
+
+  page = g_object_new (IDE_TYPE_TERMINAL_PAGE,
+                       "close-on-exit", FALSE,
+                       "icon-name", "text-x-script-symbolic",
+                       "launcher", launcher,
+                       "manage-spawn", TRUE,
+                       "respawn-on-exit", FALSE,
+                       "title", title,
+                       NULL);
+
+  position = ide_panel_position_new ();
+
+  ide_workspace_add_page (workspace, page, position);
+  panel_widget_raise (PANEL_WIDGET (page));
+  gtk_widget_grab_focus (GTK_WIDGET (page));
+
+  IDE_RETURN (TRUE);
+}
+
+static gboolean
+accelerator_to_trigger (GBinding     *binding,
+                        const GValue *from_value,
+                        GValue       *to_value,
+                        gpointer      user_data)
+{
+  const char *accel;
+
+  g_assert (G_IS_BINDING (binding));
+  g_assert (G_VALUE_HOLDS_STRING (from_value));
+
+  accel = g_value_get_string (from_value);
+
+  if (!ide_str_empty0 (accel))
+    {
+      GtkShortcutTrigger *trigger;
+
+      if ((trigger = gtk_shortcut_trigger_parse_string (accel)))
+        {
+          g_value_take_object (to_value, trigger);
+          return TRUE;
+        }
+    }
+
+  g_value_set_object (to_value, gtk_never_trigger_get ());
+
+  return TRUE;
+}
+
+static gpointer
+gbp_shellcmd_shortcut_provider_map_func (gpointer item,
+                                         gpointer user_data)
+{
+  g_autoptr(GbpShellcmdRunCommand) command = item;
+  g_autoptr(GtkShortcutAction) action = NULL;
+  GtkShortcut *ret;
+
+  g_assert (GBP_IS_SHELLCMD_RUN_COMMAND (command));
+  g_assert (user_data == NULL);
+
+  action = gtk_callback_action_new (gbp_shellcmd_shortcut_func,
+                                    g_object_ref (command),
+                                    g_object_unref);
+  ret = gtk_shortcut_new (NULL, g_steal_pointer (&action));
+
+  /* We want the accelerator to update when the command changes it */
+  g_object_bind_property_full (command, "accelerator", ret, "trigger",
+                               G_BINDING_SYNC_CREATE,
+                               accelerator_to_trigger, NULL,
+                               NULL, NULL);
+
+  /* Our capture/bubble filters require a phase set. Keep in sync
+   * with ide_shortcut_is_phase().
+   */
+  g_object_set_data (G_OBJECT (ret),
+                     "PHASE",
+                     GINT_TO_POINTER (GTK_PHASE_BUBBLE));
+
+  return g_steal_pointer (&ret);
+}
+
+static void
+add_with_mapping (GListStore *store,
+                  GListModel *commands,
+                  gboolean    prepend)
+{
+  g_autoptr(GtkMapListModel) map = NULL;
+
+  g_assert (G_IS_LIST_STORE (store));
+  g_assert (GBP_IS_SHELLCMD_COMMAND_MODEL (commands));
+
+  map = gtk_map_list_model_new (g_object_ref (commands),
+                                gbp_shellcmd_shortcut_provider_map_func,
+                                NULL, NULL);
+
+  if (prepend)
+    g_list_store_insert (store, 0, map);
+  else
+    g_list_store_append (store, map);
+}
+
+static void
+project_id_changed_cb (GbpShellcmdShortcutProvider *self,
+                       GParamSpec                  *pspec,
+                       IdeContext                  *context)
+{
+  g_autoptr(GbpShellcmdCommandModel) project_model = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_SHELLCMD_SHORTCUT_PROVIDER (self));
+  g_assert (IDE_IS_CONTEXT (context));
+
+  if (g_list_model_get_n_items (G_LIST_MODEL (self->model)) > 1)
+    g_list_store_remove (self->model, 0);
+
+  if (ide_context_has_project (context))
+    {
+      project_model = gbp_shellcmd_command_model_new_for_project (context);
+      add_with_mapping (self->model, G_LIST_MODEL (project_model), TRUE);
+    }
+}
+
+static GListModel *
+gbp_shellcmd_shortcut_provider_list_shortcuts (IdeShortcutProvider *provider)
+{
+  GbpShellcmdShortcutProvider *self = (GbpShellcmdShortcutProvider *)provider;
+  GtkFlattenListModel *ret;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_SHELLCMD_SHORTCUT_PROVIDER (self));
+
+  if (self->model == NULL)
+    {
+      IdeContext *context = ide_object_get_context (IDE_OBJECT (self));
+      g_autoptr(GbpShellcmdCommandModel) app_model = gbp_shellcmd_command_model_new_for_app ();
+      g_autoptr(GbpShellcmdCommandModel) project_model = NULL;
+
+      self->model = g_list_store_new (G_TYPE_LIST_MODEL);
+
+      if (ide_context_has_project (context))
+        project_model = gbp_shellcmd_command_model_new_for_project (context);
+      else
+        g_signal_connect_object (context,
+                                 "notify::project-id",
+                                 G_CALLBACK (project_id_changed_cb),
+                                 self,
+                                 G_CONNECT_SWAPPED);
+
+      if (project_model != NULL)
+        add_with_mapping (self->model, G_LIST_MODEL (project_model), TRUE);
+      add_with_mapping (self->model, G_LIST_MODEL (app_model), FALSE);
+    }
+
+  ret = gtk_flatten_list_model_new (g_object_ref (G_LIST_MODEL (self->model)));
+
+  IDE_RETURN (G_LIST_MODEL (ret));
+}
+
+static void
+shortcut_provider_iface_init (IdeShortcutProviderInterface *iface)
+{
+  iface->list_shortcuts = gbp_shellcmd_shortcut_provider_list_shortcuts;
+}
+
+G_DEFINE_FINAL_TYPE_WITH_CODE (GbpShellcmdShortcutProvider, gbp_shellcmd_shortcut_provider, IDE_TYPE_OBJECT,
+                               G_IMPLEMENT_INTERFACE (IDE_TYPE_SHORTCUT_PROVIDER, 
shortcut_provider_iface_init))
+
+static void
+gbp_shellcmd_shortcut_provider_dispose (GObject *object)
+{
+  GbpShellcmdShortcutProvider *self = (GbpShellcmdShortcutProvider *)object;
+
+  g_clear_object (&self->model);
+
+  G_OBJECT_CLASS (gbp_shellcmd_shortcut_provider_parent_class)->dispose (object);
+}
+
+static void
+gbp_shellcmd_shortcut_provider_class_init (GbpShellcmdShortcutProviderClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = gbp_shellcmd_shortcut_provider_dispose;
+}
+
+static void
+gbp_shellcmd_shortcut_provider_init (GbpShellcmdShortcutProvider *self)
+{
+}
diff --git a/src/plugins/shellcmd/gbp-shellcmd-command-provider.h 
b/src/plugins/shellcmd/gbp-shellcmd-shortcut-provider.h
similarity index 69%
rename from src/plugins/shellcmd/gbp-shellcmd-command-provider.h
rename to src/plugins/shellcmd/gbp-shellcmd-shortcut-provider.h
index b744937a1..7707d7b61 100644
--- a/src/plugins/shellcmd/gbp-shellcmd-command-provider.h
+++ b/src/plugins/shellcmd/gbp-shellcmd-shortcut-provider.h
@@ -1,6 +1,6 @@
-/* gbp-shellcmd-command-provider.h
+/* gbp-shellcmd-shortcut-provider.h
  *
- * Copyright 2019 Christian Hergert <chergert redhat com>
+ * 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
@@ -20,12 +20,12 @@
 
 #pragma once
 
-#include <libide-gui.h>
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
-#define GBP_TYPE_SHELLCMD_COMMAND_PROVIDER (gbp_shellcmd_command_provider_get_type())
+#define GBP_TYPE_SHELLCMD_SHORTCUT_PROVIDER (gbp_shellcmd_shortcut_provider_get_type())
 
-G_DECLARE_FINAL_TYPE (GbpShellcmdCommandProvider, gbp_shellcmd_command_provider, GBP, 
SHELLCMD_COMMAND_PROVIDER, GObject)
+G_DECLARE_FINAL_TYPE (GbpShellcmdShortcutProvider, gbp_shellcmd_shortcut_provider, GBP, 
SHELLCMD_SHORTCUT_PROVIDER, IdeObject)
 
 G_END_DECLS
diff --git a/src/plugins/shellcmd/meson.build b/src/plugins/shellcmd/meson.build
index e6d03e030..23251b1a6 100644
--- a/src/plugins/shellcmd/meson.build
+++ b/src/plugins/shellcmd/meson.build
@@ -2,24 +2,23 @@ if get_option('plugin_shellcmd')
 
 plugins_sources += files([
   'shellcmd-plugin.c',
-  'gbp-shellcmd-application-addin.c',
-  'gbp-shellcmd-command.c',
-  'gbp-shellcmd-command-editor.c',
+  'gbp-shellcmd-command-dialog.c',
   'gbp-shellcmd-command-model.c',
-  'gbp-shellcmd-command-provider.c',
-  'gbp-shellcmd-command-row.c',
-  'gbp-shellcmd-list.c',
   'gbp-shellcmd-preferences-addin.c',
+  'gbp-shellcmd-run-command.c',
+  'gbp-shellcmd-run-command-provider.c',
+  'gbp-shellcmd-shortcut-provider.c',
 ])
 
 plugin_shellcmd_enum_headers = [
-  'gbp-shellcmd-command.h',
+  'gbp-shellcmd-run-command.h',
 ]
 
 plugin_shellcmd_enums = gnome.mkenums_simple('gbp-shellcmd-enums',
      body_prefix: '#include "config.h"',
-   header_prefix: '#include <libide-gui.h>',
+   header_prefix: '#include <libide-core.h>',
          sources: plugin_shellcmd_enum_headers,
+  install_header: false,
 )
 
 plugin_shellcmd_resources = gnome.compile_resources(
@@ -28,8 +27,11 @@ plugin_shellcmd_resources = gnome.compile_resources(
   c_name: 'gbp_shellcmd',
 )
 
-plugins_sources += plugin_shellcmd_enums
 plugins_sources += plugin_shellcmd_resources
+plugins_sources += plugin_shellcmd_enums
 plugins_include_directories += [include_directories('.')]
 
+install_data(['org.gnome.builder.shellcmd.gschema.xml'], install_dir: schema_dir)
+install_data(['org.gnome.builder.shellcmd.command.gschema.xml'], install_dir: schema_dir)
+
 endif
diff --git a/src/plugins/shellcmd/org.gnome.builder.shellcmd.command.gschema.xml 
b/src/plugins/shellcmd/org.gnome.builder.shellcmd.command.gschema.xml
new file mode 100644
index 000000000..24b537223
--- /dev/null
+++ b/src/plugins/shellcmd/org.gnome.builder.shellcmd.command.gschema.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schemalist>
+  <schema id="org.gnome.builder.shellcmd.command" gettext-domain="gnome-builder">
+    <key name="display-name" type="s">
+      <default>''</default>
+      <summary>Display Name</summary>
+    </key>
+    <key name="accelerator" type="s">
+      <default>''</default>
+      <summary>Keyboard Accelerator</summary>
+    </key>
+    <key name="cwd" type="s">
+      <default>'$BUILDDIR/'</default>
+      <summary>Current Working Directory</summary>
+    </key>
+    <key name="argv" type="as">
+      <default>[]</default>
+      <summary>Command Arguments</summary>
+    </key>
+    <key name="env" type="as">
+      <default>[]</default>
+      <summary>Command Environment</summary>
+    </key>
+    <key name="locality" type="s">
+      <choices>
+        <choice value="host"/>
+        <choice value="pipeline"/>
+        <choice value="runtime"/>
+        <choice value="subprocess"/>
+      </choices>
+      <default>'pipeline'</default>
+      <summary>Command Locality</summary>
+    </key>
+  </schema>
+</schemalist>
diff --git a/src/plugins/shellcmd/org.gnome.builder.shellcmd.gschema.xml 
b/src/plugins/shellcmd/org.gnome.builder.shellcmd.gschema.xml
new file mode 100644
index 000000000..cb6abf007
--- /dev/null
+++ b/src/plugins/shellcmd/org.gnome.builder.shellcmd.gschema.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schemalist>
+  <schema id="org.gnome.builder.shellcmd" gettext-domain="gnome-builder">
+    <key name="run-commands" type="as">
+      <default>[]</default>
+      <summary>Run Commands</summary>
+      <description>A list of run-command ids to load for the application or project.</description>
+    </key>
+  </schema>
+</schemalist>
diff --git a/src/plugins/shellcmd/shellcmd-plugin.c b/src/plugins/shellcmd/shellcmd-plugin.c
index 26b841cc8..4a22fe1c2 100644
--- a/src/plugins/shellcmd/shellcmd-plugin.c
+++ b/src/plugins/shellcmd/shellcmd-plugin.c
@@ -1,6 +1,6 @@
 /* shellcmd-plugin.c
  *
- * Copyright 2019 Christian Hergert <chergert redhat com>
+ * Copyright 2019-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
@@ -20,23 +20,25 @@
 
 #include "config.h"
 
-#include <libide-gui.h>
 #include <libpeas/peas.h>
 
-#include "gbp-shellcmd-application-addin.h"
-#include "gbp-shellcmd-command-provider.h"
+#include <libide-foundry.h>
+#include <libide-gui.h>
+
 #include "gbp-shellcmd-preferences-addin.h"
+#include "gbp-shellcmd-run-command-provider.h"
+#include "gbp-shellcmd-shortcut-provider.h"
 
 _IDE_EXTERN void
 _gbp_shellcmd_register_types (PeasObjectModule *module)
 {
-  peas_object_module_register_extension_type (module,
-                                              IDE_TYPE_APPLICATION_ADDIN,
-                                              GBP_TYPE_SHELLCMD_APPLICATION_ADDIN);
-  peas_object_module_register_extension_type (module,
-                                              IDE_TYPE_COMMAND_PROVIDER,
-                                              GBP_TYPE_SHELLCMD_COMMAND_PROVIDER);
   peas_object_module_register_extension_type (module,
                                               IDE_TYPE_PREFERENCES_ADDIN,
                                               GBP_TYPE_SHELLCMD_PREFERENCES_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_RUN_COMMAND_PROVIDER,
+                                              GBP_TYPE_SHELLCMD_RUN_COMMAND_PROVIDER);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_SHORTCUT_PROVIDER,
+                                              GBP_TYPE_SHELLCMD_SHORTCUT_PROVIDER);
 }
diff --git a/src/plugins/shellcmd/shellcmd.gresource.xml b/src/plugins/shellcmd/shellcmd.gresource.xml
index 47849fb90..9a01bc34d 100644
--- a/src/plugins/shellcmd/shellcmd.gresource.xml
+++ b/src/plugins/shellcmd/shellcmd.gresource.xml
@@ -2,7 +2,6 @@
 <gresources>
   <gresource prefix="/plugins/shellcmd">
     <file>shellcmd.plugin</file>
-    <file preprocess="xml-stripblanks">gbp-shellcmd-command-editor.ui</file>
-    <file preprocess="xml-stripblanks">gbp-shellcmd-command-row.ui</file>
+    <file preprocess="xml-stripblanks">gbp-shellcmd-command-dialog.ui</file>
   </gresource>
 </gresources>
diff --git a/src/plugins/shellcmd/shellcmd.plugin b/src/plugins/shellcmd/shellcmd.plugin
index bfd130ff7..8aa4d21fb 100644
--- a/src/plugins/shellcmd/shellcmd.plugin
+++ b/src/plugins/shellcmd/shellcmd.plugin
@@ -1,10 +1,10 @@
 [Plugin]
 Authors=Christian Hergert <christian hergert me>
 Builtin=true
-Copyright=Copyright © 2019 Christian Hergert
-Depends=editor;
-Description=Run shell commands from your project
+Copyright=Copyright © 2019-2022 Christian Hergert
+Description=Use custom commands to run your project
 Embedded=_gbp_shellcmd_register_types
 Hidden=true
 Module=shellcmd
 Name=Shell Commands
+X-Preferences-Kind=application;project;


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