[gnome-builder: 47/139] libide-greeter: add new libide-greeter static library



commit fb47eb9ae435fcad8f8af530686700273f4246f9
Author: Christian Hergert <chergert redhat com>
Date:   Wed Jan 9 16:38:03 2019 -0800

    libide-greeter: add new libide-greeter static library
    
    This creates a new static library that contains the greeter. It breaks
    the greeter into a separate window so that we can vastly simplify the
    code in Builder with regards to workspaces.
    
    Some code is moved to a plugin so that it can hook into command line
    options and provide the new -g option to jump to the greeter.

 src/libide/genesis/ide-genesis-addin.c             |  150 ---
 src/libide/genesis/ide-genesis-addin.h             |   82 --
 src/libide/genesis/meson.build                     |   12 -
 src/libide/greeter/ide-clone-surface.c             |  564 ++++++++
 src/libide/greeter/ide-clone-surface.h             |   42 +
 src/libide/greeter/ide-clone-surface.ui            |  383 ++++++
 src/libide/greeter/ide-greeter-perspective.c       | 1385 --------------------
 src/libide/greeter/ide-greeter-perspective.ui      |  427 ------
 ...greeter-perspective.h => ide-greeter-private.h} |   17 +-
 src/libide/greeter/ide-greeter-section.c           |    2 +-
 src/libide/greeter/ide-greeter-section.h           |    6 +-
 src/libide/greeter/ide-greeter-workspace-actions.c |  223 ++++
 .../greeter/ide-greeter-workspace-shortcuts.c      |   44 +
 src/libide/greeter/ide-greeter-workspace.c         |  808 ++++++++++++
 src/libide/greeter/ide-greeter-workspace.h         |   61 +
 src/libide/greeter/ide-greeter-workspace.ui        |  177 +++
 src/libide/greeter/libide-greeter.gresource.xml    |    7 +
 src/libide/greeter/libide-greeter.h                |   34 +
 src/libide/greeter/meson.build                     |   88 +-
 .../greeter/gbp-greeter-application-addin.c        |  229 ++++
 .../greeter/gbp-greeter-application-addin.h        |   31 +
 src/plugins/greeter/greeter-plugin.c               |   36 +
 src/plugins/greeter/greeter.gresource.xml          |    7 +
 src/plugins/greeter/greeter.plugin                 |   13 +
 src/plugins/greeter/gtk/menus.ui                   |   97 ++
 src/plugins/greeter/meson.build                    |   12 +
 26 files changed, 2856 insertions(+), 2081 deletions(-)
---
diff --git a/src/libide/greeter/ide-clone-surface.c b/src/libide/greeter/ide-clone-surface.c
new file mode 100644
index 000000000..aa038842c
--- /dev/null
+++ b/src/libide/greeter/ide-clone-surface.c
@@ -0,0 +1,564 @@
+/* ide-clone-surface.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-clone-surface"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libpeas/peas.h>
+#include <libide-vcs.h>
+
+#include "ide-clone-surface.h"
+#include "ide-greeter-private.h"
+#include "ide-greeter-workspace.h"
+
+struct _IdeCloneSurface
+{
+  IdeSurface           parent_instance;
+
+  /* This extension set contains IdeVcsCloner implementations which we
+   * use to validate URIs, as well as provide some toggles for how the
+   * user wants to perform the clone operation. Currently, we have a
+   * very limited set of cloning (basically just git), but that could be
+   * expanded in the future based on demand.
+   */
+  PeasExtensionSet    *addins;
+  guint                n_addins;
+
+  /* We calculate the file to the target folder based on the vcs uri and
+   * the destination file chooser. It's cached here so that we don't have
+   * to recaclulate it in multiple code paths.
+   */
+  GFile               *destination;
+
+  /* Template Widgets */
+  DzlFileChooserEntry *destination_chooser;
+  GtkLabel            *destination_label;
+  DzlRadioBox         *kind_radio;
+  GtkLabel            *kind_label;
+  GtkLabel            *status_message;
+  GtkEntry            *uri_entry;
+  GtkEntry            *author_entry;
+  GtkEntry            *email_entry;
+  GtkEntry            *branch_entry;
+  GtkButton           *clone_button;
+  GtkButton           *cancel_button;
+  GtkStack            *button_stack;
+};
+
+G_DEFINE_TYPE (IdeCloneSurface, ide_clone_surface, IDE_TYPE_SURFACE)
+
+enum {
+  PROP_0,
+  PROP_URI,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+/**
+ * ide_clone_surface_new:
+ *
+ * Create a new #IdeCloneSurface.
+ *
+ * Returns: (transfer full): a newly created #IdeCloneSurface
+ *
+ * Since: 3.32
+ */
+IdeCloneSurface *
+ide_clone_surface_new (void)
+{
+  return g_object_new (IDE_TYPE_CLONE_SURFACE, NULL);
+}
+
+static void
+ide_clone_surface_addin_added_cb (PeasExtensionSet *set,
+                                  PeasPluginInfo   *plugin_info,
+                                  PeasExtension    *exten,
+                                  gpointer          user_data)
+{
+  IdeVcsCloner *cloner = (IdeVcsCloner *)exten;
+  IdeCloneSurface *self = user_data;
+  g_autofree gchar *title = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_VCS_CLONER (cloner));
+  g_assert (IDE_IS_CLONE_SURFACE (self));
+
+  self->n_addins++;
+
+  title = ide_vcs_cloner_get_title (cloner);
+
+  dzl_radio_box_add_item (self->kind_radio,
+                          peas_plugin_info_get_module_name (plugin_info),
+                          title);
+
+  if (self->n_addins > 1)
+    {
+      gtk_widget_show (GTK_WIDGET (self->kind_label));
+      gtk_widget_show (GTK_WIDGET (self->kind_radio));
+    }
+}
+
+static void
+ide_clone_surface_addin_removed_cb (PeasExtensionSet *set,
+                                    PeasPluginInfo   *plugin_info,
+                                    PeasExtension    *exten,
+                                    gpointer          user_data)
+{
+  IdeVcsCloner *cloner = (IdeVcsCloner *)exten;
+  IdeCloneSurface *self = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_VCS_CLONER (cloner));
+  g_assert (IDE_IS_CLONE_SURFACE (self));
+
+  self->n_addins--;
+
+  dzl_radio_box_remove_item (self->kind_radio,
+                             peas_plugin_info_get_module_name (plugin_info));
+
+  if (self->n_addins < 2)
+    {
+      gtk_widget_hide (GTK_WIDGET (self->kind_label));
+      gtk_widget_hide (GTK_WIDGET (self->kind_radio));
+    }
+}
+
+static void
+ide_clone_surface_validate_cb (PeasExtensionSet *set,
+                               PeasPluginInfo   *plugin_info,
+                               PeasExtension    *exten,
+                               gpointer          user_data)
+{
+  IdeVcsCloner *cloner = (IdeVcsCloner *)exten;
+  struct {
+    const gchar *text;
+    gchar       *errmsg;
+    gboolean     valid;
+  } *validate = user_data;
+  g_autofree gchar *errmsg = NULL;
+
+  g_assert (IDE_IS_VCS_CLONER (cloner));
+
+  if (validate->valid)
+    return;
+
+  validate->valid = ide_vcs_cloner_validate_uri (cloner, validate->text, &errmsg);
+
+  if (!validate->errmsg)
+    validate->errmsg = g_steal_pointer (&errmsg);
+}
+
+static void
+ide_clone_surface_validate (IdeCloneSurface *self)
+{
+  struct {
+    const gchar *text;
+    gchar       *errmsg;
+    gboolean     valid;
+  } validate;
+
+  g_assert (IDE_IS_CLONE_SURFACE (self));
+
+  validate.text = gtk_entry_get_text (self->uri_entry);
+  validate.errmsg = NULL;
+  validate.valid = FALSE;
+
+  if (self->addins != NULL)
+    peas_extension_set_foreach (self->addins,
+                                ide_clone_surface_validate_cb,
+                                &validate);
+
+  if (validate.valid)
+    dzl_gtk_widget_remove_style_class (GTK_WIDGET (self->uri_entry), "error");
+  else
+    dzl_gtk_widget_add_style_class (GTK_WIDGET (self->uri_entry), "error");
+
+  if (validate.errmsg)
+    gtk_widget_set_tooltip_text (GTK_WIDGET (self->uri_entry), validate.errmsg);
+  else
+    gtk_widget_set_tooltip_text (GTK_WIDGET (self->uri_entry), NULL);
+
+  g_free (validate.errmsg);
+}
+
+static void
+ide_clone_surface_update (IdeCloneSurface *self)
+{
+  g_autoptr(GFile) file = NULL;
+  g_autoptr(GFile) child_file = NULL;
+  g_autoptr(IdeVcsUri) uri = NULL;
+  g_autofree gchar *child = NULL;
+  g_autofree gchar *collapsed = NULL;
+  g_autofree gchar *formatted = NULL;
+  const gchar *text;
+  GtkEntry *entry;
+
+  g_assert (IDE_IS_CLONE_SURFACE (self));
+
+  ide_clone_surface_validate (self);
+
+  file = dzl_file_chooser_entry_get_file (self->destination_chooser);
+  text = gtk_entry_get_text (self->uri_entry);
+  uri = ide_vcs_uri_new (text);
+
+  if (uri != NULL)
+    child = ide_vcs_uri_get_clone_name (uri);
+
+  if (child)
+    child_file = g_file_get_child (file, child);
+  else
+    child_file = g_object_ref (file);
+
+  g_set_object (&self->destination, child_file);
+
+  collapsed = ide_path_collapse (g_file_peek_path (child_file));
+
+  entry = dzl_file_chooser_entry_get_entry (self->destination_chooser);
+
+  if (g_file_query_exists (child_file, NULL))
+    {
+      /* translators: %s is replaced with the path to the project */
+      formatted = g_strdup_printf (_("The directory “%s” already exists. Please choose another directory."),
+                                   collapsed);
+      dzl_gtk_widget_add_style_class (GTK_WIDGET (entry), "error");
+    }
+  else
+    {
+      /* translators: %s is replaced with the path to the project */
+      formatted = g_strdup_printf (_("Your project will be created at %s"), collapsed);
+      dzl_gtk_widget_remove_style_class (GTK_WIDGET (entry), "error");
+    }
+
+  gtk_label_set_label (self->destination_label, formatted);
+}
+
+static void
+ide_clone_surface_uri_entry_changed (IdeCloneSurface *self,
+                                     GtkEntry        *entry)
+{
+  g_assert (IDE_IS_CLONE_SURFACE (self));
+  g_assert (GTK_IS_ENTRY (entry));
+
+  ide_clone_surface_update (self);
+}
+
+static void
+ide_clone_surface_destination_changed (IdeCloneSurface     *self,
+                                       GParamSpec          *pspec,
+                                       DzlFileChooserEntry *chooser)
+{
+  g_assert (IDE_IS_CLONE_SURFACE (self));
+  g_assert (DZL_IS_FILE_CHOOSER_ENTRY (chooser));
+
+  ide_clone_surface_update (self);
+}
+
+static void
+ide_clone_surface_grab_focus (GtkWidget *widget)
+{
+  gtk_widget_grab_focus (GTK_WIDGET (IDE_CLONE_SURFACE (widget)->uri_entry));
+}
+
+static void
+ide_clone_surface_destroy (GtkWidget *widget)
+{
+  IdeCloneSurface *self = (IdeCloneSurface *)widget;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_CLONE_SURFACE (self));
+
+  g_clear_object (&self->addins);
+  g_clear_object (&self->destination);
+
+  GTK_WIDGET_CLASS (ide_clone_surface_parent_class)->destroy (widget);
+}
+
+static void
+ide_clone_surface_constructed (GObject *object)
+{
+  IdeCloneSurface *self = (IdeCloneSurface *)object;
+  g_autoptr(GFile) file = NULL;
+
+  G_OBJECT_CLASS (ide_clone_surface_parent_class)->constructed (object);
+
+  gtk_entry_set_text (self->author_entry, g_get_real_name ());
+
+  file = g_file_new_for_path (ide_get_projects_dir ());
+  dzl_file_chooser_entry_set_file (self->destination_chooser, file);
+
+  self->addins = peas_extension_set_new (peas_engine_get_default (),
+                                         IDE_TYPE_VCS_CLONER,
+                                         NULL);
+
+  g_signal_connect (self->addins,
+                    "extension-added",
+                    G_CALLBACK (ide_clone_surface_addin_added_cb),
+                    self);
+
+  g_signal_connect (self->addins,
+                    "extension-removed",
+                    G_CALLBACK (ide_clone_surface_addin_removed_cb),
+                    self);
+
+  peas_extension_set_foreach (self->addins,
+                              ide_clone_surface_addin_added_cb,
+                              self);
+
+  ide_clone_surface_update (self);
+}
+
+static void
+ide_clone_surface_get_property (GObject    *object,
+                                guint       prop_id,
+                                GValue     *value,
+                                GParamSpec *pspec)
+{
+  IdeCloneSurface *self = IDE_CLONE_SURFACE (object);
+
+  switch (prop_id)
+    {
+    case PROP_URI:
+      g_value_set_string (value, ide_clone_surface_get_uri (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_clone_surface_set_property (GObject      *object,
+                                guint         prop_id,
+                                const GValue *value,
+                                GParamSpec   *pspec)
+{
+  IdeCloneSurface *self = IDE_CLONE_SURFACE (object);
+
+  switch (prop_id)
+    {
+    case PROP_URI:
+      ide_clone_surface_set_uri (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_clone_surface_class_init (IdeCloneSurfaceClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->constructed = ide_clone_surface_constructed;
+  object_class->get_property = ide_clone_surface_get_property;
+  object_class->set_property = ide_clone_surface_set_property;
+
+  widget_class->destroy = ide_clone_surface_destroy;
+  widget_class->grab_focus = ide_clone_surface_grab_focus;
+
+  /**
+   * IdeCloneSurface:uri:
+   *
+   * The "uri" property is the URI of the version control repository to
+   * be cloned. Usually, this is something like
+   *
+   *   "https://gitlab.gnome.org/GNOME/gnome-builder.git";
+   *
+   * Since: 3.32
+   */
+  properties [PROP_URI] =
+    g_param_spec_string ("uri",
+                         "Uri",
+                         "The URI of the repository to clone.",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/builder/ui/ide-clone-surface.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeCloneSurface, author_entry);
+  gtk_widget_class_bind_template_child (widget_class, IdeCloneSurface, branch_entry);
+  gtk_widget_class_bind_template_child (widget_class, IdeCloneSurface, button_stack);
+  gtk_widget_class_bind_template_child (widget_class, IdeCloneSurface, cancel_button);
+  gtk_widget_class_bind_template_child (widget_class, IdeCloneSurface, clone_button);
+  gtk_widget_class_bind_template_child (widget_class, IdeCloneSurface, destination_chooser);
+  gtk_widget_class_bind_template_child (widget_class, IdeCloneSurface, destination_label);
+  gtk_widget_class_bind_template_child (widget_class, IdeCloneSurface, email_entry);
+  gtk_widget_class_bind_template_child (widget_class, IdeCloneSurface, kind_label);
+  gtk_widget_class_bind_template_child (widget_class, IdeCloneSurface, kind_radio);
+  gtk_widget_class_bind_template_child (widget_class, IdeCloneSurface, status_message);
+  gtk_widget_class_bind_template_child (widget_class, IdeCloneSurface, uri_entry);
+  gtk_widget_class_bind_template_callback (widget_class, ide_clone_surface_clone);
+  gtk_widget_class_bind_template_callback (widget_class, ide_clone_surface_destination_changed);
+  gtk_widget_class_bind_template_callback (widget_class, ide_clone_surface_uri_entry_changed);
+}
+
+static void
+ide_clone_surface_init (IdeCloneSurface *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+const gchar *
+ide_clone_surface_get_uri (IdeCloneSurface *self)
+{
+  g_return_val_if_fail (IDE_IS_CLONE_SURFACE (self), NULL);
+
+  return gtk_entry_get_text (self->uri_entry);
+}
+
+void
+ide_clone_surface_set_uri (IdeCloneSurface *self,
+                           const gchar     *uri)
+{
+  g_return_if_fail (IDE_IS_CLONE_SURFACE (self));
+
+  gtk_entry_set_text (self->uri_entry, uri);
+}
+
+static void
+ide_clone_surface_clone_cb (GObject      *object,
+                            GAsyncResult *result,
+                            gpointer      user_data)
+{
+  IdeVcsCloner *cloner = (IdeVcsCloner *)object;
+  g_autoptr(IdeCloneSurface) self = user_data;
+  g_autoptr(IdeProjectInfo) project_info = NULL;
+  g_autoptr(GError) error = NULL;
+  GtkWidget *workspace;
+
+  g_assert (IDE_IS_VCS_CLONER (cloner));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_CLONE_SURFACE (self));
+
+  workspace = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GREETER_WORKSPACE);
+  ide_greeter_workspace_end (IDE_GREETER_WORKSPACE (workspace));
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self->uri_entry), TRUE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->destination_chooser), TRUE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->clone_button), TRUE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->author_entry), TRUE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->email_entry), TRUE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->branch_entry), TRUE);
+  gtk_stack_set_visible_child (self->button_stack, GTK_WIDGET (self->clone_button));
+  gtk_label_set_label (self->status_message, "");
+
+  if (!ide_vcs_cloner_clone_finish (cloner, result, &error))
+    {
+      g_warning ("Failed to clone repository: %s", error->message);
+      gtk_label_set_label (self->status_message, error->message);
+      gtk_entry_set_progress_fraction (self->uri_entry, 0);
+      return;
+    }
+
+  project_info = ide_project_info_new ();
+  ide_project_info_set_vcs_uri (project_info, gtk_entry_get_text (self->uri_entry));
+  ide_project_info_set_file (project_info, self->destination);
+  ide_project_info_set_directory (project_info, self->destination);
+
+  ide_greeter_workspace_open_project (IDE_GREETER_WORKSPACE (workspace), project_info);
+}
+
+void
+ide_clone_surface_clone (IdeCloneSurface *self)
+{
+  PeasEngine *engine = peas_engine_get_default ();
+  g_autoptr(IdeNotification) notif = NULL;
+  g_autoptr(GCancellable) cancellable = NULL;
+  PeasPluginInfo *plugin_info;
+  IdeVcsCloner *addin;
+  GVariantDict dict;
+  const gchar *uri;
+  const gchar *path;
+  const gchar *module_name;
+  const gchar *author;
+  const gchar *email;
+  GtkWidget *workspace;
+
+  g_return_if_fail (IDE_IS_CLONE_SURFACE (self));
+
+  if (!(module_name = dzl_radio_box_get_active_id (self->kind_radio)) ||
+      !(plugin_info = peas_engine_get_plugin_info (engine, module_name)) ||
+      !(addin = (IdeVcsCloner *)peas_extension_set_get_extension (self->addins, plugin_info)))
+    {
+      g_warning ("Failed to locate module to use for cloning");
+      return;
+    }
+
+  g_variant_dict_init (&dict, NULL);
+
+  uri = gtk_entry_get_text (self->uri_entry);
+  author = gtk_entry_get_text (self->author_entry);
+  email = gtk_entry_get_text (self->email_entry);
+  path = g_file_peek_path (self->destination);
+
+  if (!ide_str_empty0 (author) && !g_str_equal (g_get_real_name (), author))
+    g_variant_dict_insert (&dict, "author-name", "s", author);
+
+  if (!ide_str_empty0 (email))
+    g_variant_dict_insert (&dict, "author-email", "s", email);
+
+  g_debug ("Cloning repository using addin: %s", module_name);
+
+  workspace = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GREETER_WORKSPACE);
+  ide_greeter_workspace_begin (IDE_GREETER_WORKSPACE (workspace));
+
+  cancellable = g_cancellable_new ();
+
+  g_signal_connect_object (self->cancel_button,
+                           "clicked",
+                           G_CALLBACK (g_cancellable_cancel),
+                           cancellable,
+                           G_CONNECT_SWAPPED);
+
+  ide_vcs_cloner_clone_async (addin,
+                              uri,
+                              path,
+                              &dict,
+                              cancellable,
+                              &notif,
+                              ide_clone_surface_clone_cb,
+                              g_object_ref (self));
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self->uri_entry), FALSE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->destination_chooser), FALSE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->clone_button), FALSE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->author_entry), FALSE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->email_entry), FALSE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->branch_entry), FALSE);
+  gtk_stack_set_visible_child (self->button_stack, GTK_WIDGET (self->cancel_button));
+
+  if (notif != NULL)
+    {
+      g_object_bind_property (notif, "progress", self->uri_entry, "progress-fraction", 
G_BINDING_SYNC_CREATE);
+      g_object_bind_property (notif, "body", self->status_message, "label", G_BINDING_SYNC_CREATE);
+    }
+
+  g_variant_dict_clear (&dict);
+}
diff --git a/src/libide/greeter/ide-clone-surface.h b/src/libide/greeter/ide-clone-surface.h
new file mode 100644
index 000000000..c9d48c87e
--- /dev/null
+++ b/src/libide/greeter/ide-clone-surface.h
@@ -0,0 +1,42 @@
+/* ide-clone-surface.h
+ *
+ * Copyright 2018-2019 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-gui.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_CLONE_SURFACE (ide_clone_surface_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeCloneSurface, ide_clone_surface, IDE, CLONE_SURFACE, IdeSurface)
+
+IDE_AVAILABLE_IN_3_32
+IdeCloneSurface *ide_clone_surface_new     (void);
+IDE_AVAILABLE_IN_3_32
+const gchar     *ide_clone_surface_get_uri (IdeCloneSurface *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_clone_surface_set_uri (IdeCloneSurface *self,
+                                            const gchar     *uri);
+IDE_AVAILABLE_IN_3_32
+void             ide_clone_surface_clone   (IdeCloneSurface *self);
+
+G_END_DECLS
diff --git a/src/libide/greeter/ide-clone-surface.ui b/src/libide/greeter/ide-clone-surface.ui
new file mode 100644
index 000000000..716a2f752
--- /dev/null
+++ b/src/libide/greeter/ide-clone-surface.ui
@@ -0,0 +1,383 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.24 -->
+  <template class="IdeCloneSurface" parent="IdeSurface">
+    <child>
+      <object class="GtkScrolledWindow">
+        <property name="propagate-natural-width">true</property>
+        <property name="propagate-natural-height">true</property>
+        <property name="hscrollbar-policy">never</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkViewport">
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkBox">
+                <property name="margin">32</property>
+                <property name="orientation">vertical</property>
+                <property name="valign">start</property>
+                <property name="vexpand">true</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="DzlThreeGrid" id="grid">
+                    <property name="column-spacing">12</property>
+                    <!-- can't use row-spacing because we have to animate in
+                         the revealer which messes up the margins.  -->
+                    <property name="row-spacing">0</property>
+                    <property name="expand">true</property>
+                    <property name="visible">true</property>
+                    <child>
+                      <object class="GtkImage" id="splash">
+                        <property name="valign">end</property>
+                        <property name="vexpand">true</property>
+                        <property name="icon-name">document-save-symbolic</property>
+                        <property name="pixel-size">128</property>
+                        <property name="visible">true</property>
+                        <property name="margin">24</property>
+                        <style>
+                          <class name="dim-label"/>
+                        </style>
+                      </object>
+                      <packing>
+                        <property name="row">0</property>
+                        <property name="column">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="repo_label">
+                        <property name="label" translatable="yes">Repository URL</property>
+                        <property name="valign">center</property>
+                        <property name="visible">true</property>
+                        <property name="xalign">1.0</property>
+                        <style>
+                          <class name="dim-label"/>
+                        </style>
+                      </object>
+                      <packing>
+                        <property name="row">1</property>
+                        <property name="column">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="uri_entry_help">
+                        <property name="label" translatable="yes">Enter the repository of the project you 
would like to clone. The URL should look similar to 
"https://gitlab.gnome.org/GNOME/gnome-builder.git";.</property>
+                        <property name="margin-top">3</property>
+                        <property name="width-chars">40</property>
+                        <property name="max-width-chars">60</property>
+                        <property name="visible">true</property>
+                        <property name="wrap">true</property>
+                        <property name="xalign">0.0</property>
+                        <style>
+                          <class name="dim-label"/>
+                        </style>
+                        <attributes>
+                          <attribute name="scale" value="0.833333"/>
+                        </attributes>
+                      </object>
+                      <packing>
+                        <property name="row">2</property>
+                        <property name="column">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkEntry" id="uri_entry">
+                        <property name="placeholder-text" 
translatable="yes">user@host:repository.git</property>
+                        <property name="width-chars">40</property>
+                        <property name="max-width-chars">50</property>
+                        <property name="valign">center</property>
+                        <property name="visible">true</property>
+                        <signal name="changed" handler="ide_clone_surface_uri_entry_changed" swapped="true" 
object="IdeCloneSurface"/>
+                      </object>
+                      <packing>
+                        <property name="row">1</property>
+                        <property name="column">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkToggleButton" id="more_button">
+                        <property name="halign">start</property>
+                        <property name="hexpand">false</property>
+                        <property name="visible">true</property>
+                        <property name="has-tooltip">true</property>
+                        <property name="tooltip-text" translatable="yes">Select branch and other 
options.</property>
+                        <style>
+                          <class name="flat"/>
+                        </style>
+                        <child>
+                          <object class="GtkImage">
+                            <property name="visible">true</property>
+                            <property name="icon-name">view-more-symbolic</property>
+                            <style>
+                              <class name="image-button"/>
+                            </style>
+                          </object>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="row">1</property>
+                        <property name="column">2</property>
+                      </packing>
+                    </child>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkRevealer" id="more_revealer">
+                    <property name="reveal-child" bind-source="more_button" bind-property="active"/>
+                    <property name="visible">true</property>
+                    <child>
+                      <object class="GtkBox">
+                        <property name="margin-top">12</property>
+                        <property name="orientation">vertical</property>
+                        <property name="visible">true</property>
+                        <child>
+                          <object class="DzlThreeGrid">
+                            <property name="column-spacing">12</property>
+                            <property name="row-spacing">12</property>
+                            <property name="visible">true</property>
+                            <child>
+                              <object class="GtkLabel" id="kind_label">
+                                <property name="label" translatable="yes">Repository Kind</property>
+                                <property name="visible">false</property>
+                                <property name="xalign">1.0</property>
+                                <property name="valign">center</property>
+                                <style>
+                                  <class name="dim-label"/>
+                                </style>
+                              </object>
+                              <packing>
+                                <property name="row">0</property>
+                                <property name="column">0</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <object class="DzlRadioBox" id="kind_radio">
+                                <property name="visible">false</property>
+                                <property name="valign">center</property>
+                              </object>
+                              <packing>
+                                <property name="row">0</property>
+                                <property name="column">1</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <object class="GtkLabel" id="branch_label">
+                                <property name="label" translatable="yes">Branch</property>
+                                <property name="visible">true</property>
+                                <property name="xalign">1.0</property>
+                                <property name="valign">center</property>
+                                <style>
+                                  <class name="dim-label"/>
+                                </style>
+                              </object>
+                              <packing>
+                                <property name="row">1</property>
+                                <property name="column">0</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <object class="GtkEntry" id="branch_entry">
+                                <property name="visible">true</property>
+                                <property name="valign">center</property>
+                                <property name="text">master</property>
+                              </object>
+                              <packing>
+                                <property name="row">1</property>
+                                <property name="column">1</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <object class="GtkLabel" id="author_label">
+                                <property name="label" translatable="yes">Author Name</property>
+                                <property name="visible">true</property>
+                                <property name="xalign">1.0</property>
+                                <property name="valign">center</property>
+                                <style>
+                                  <class name="dim-label"/>
+                                </style>
+                              </object>
+                              <packing>
+                                <property name="row">2</property>
+                                <property name="column">0</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <object class="GtkEntry" id="author_entry">
+                                <property name="visible">true</property>
+                                <property name="valign">center</property>
+                              </object>
+                              <packing>
+                                <property name="row">2</property>
+                                <property name="column">1</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <object class="GtkLabel" id="email_label">
+                                <property name="label" translatable="yes">Author Email</property>
+                                <property name="visible">true</property>
+                                <property name="xalign">1.0</property>
+                                <property name="valign">center</property>
+                                <style>
+                                  <class name="dim-label"/>
+                                </style>
+                              </object>
+                              <packing>
+                                <property name="row">3</property>
+                                <property name="column">0</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <object class="GtkEntry" id="email_entry">
+                                <property name="visible">true</property>
+                                <property name="valign">center</property>
+                              </object>
+                              <packing>
+                                <property name="row">3</property>
+                                <property name="column">1</property>
+                              </packing>
+                            </child>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+                <child>
+                  <object class="DzlThreeGrid">
+                    <property name="margin-top">12</property>
+                    <property name="column-spacing">12</property>
+                    <property name="visible">true</property>
+                    <child>
+                      <object class="GtkLabel" id="dest_label">
+                        <property name="label" translatable="yes">Project Destination</property>
+                        <property name="visible">true</property>
+                        <property name="xalign">1.0</property>
+                        <property name="valign">center</property>
+                        <style>
+                          <class name="dim-label"/>
+                        </style>
+                      </object>
+                      <packing>
+                        <property name="row">0</property>
+                        <property name="column">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="DzlFileChooserEntry" id="destination_chooser">
+                        <property name="valign">center</property>
+                        <property name="visible">true</property>
+                        <signal name="notify::file" handler="ide_clone_surface_destination_changed" 
object="IdeCloneSurface" swapped="true"/>
+                      </object>
+                      <packing>
+                        <property name="row">0</property>
+                        <property name="column">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="destination_label">
+                        <property name="visible">true</property>
+                        <property name="xalign">0.0</property>
+                        <property name="valign">center</property>
+                        <property name="margin-top">3</property>
+                        <style>
+                          <class name="dim-label"/>
+                        </style>
+                        <attributes>
+                          <attribute name="scale" value="0.83333"/>
+                        </attributes>
+                      </object>
+                      <packing>
+                        <property name="row">2</property>
+                        <property name="column">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkBox">
+                        <property name="margin-top">32</property>
+                        <property name="visible">true</property>
+                        <property name="orientation">horizontal</property>
+                        <child>
+                          <object class="GtkStack" id="button_stack">
+                            <property name="visible">true</property>
+                            <property name="homogeneous">true</property>
+                            <property name="halign">end</property>
+                            <child>
+                              <object class="GtkButton" id="clone_button">
+                                <property name="label" translatable="yes">Clone Project</property>
+                                <property name="visible">true</property>
+                                <signal name="clicked" handler="ide_clone_surface_clone" 
object="IdeCloneSurface" swapped="true"/>
+                                <style>
+                                  <class name="suggested-action"/>
+                                </style>
+                              </object>
+                            </child>
+                            <child>
+                              <object class="GtkButton" id="cancel_button">
+                                <property name="label" translatable="yes">Cancel</property>
+                                <property name="visible">true</property>
+                                <style>
+                                  <class name="destructive-action"/>
+                                </style>
+                              </object>
+                            </child>
+                          </object>
+                          <packing>
+                            <property name="pack-type">end</property>
+                          </packing>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="row">3</property>
+                        <property name="column">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="status_message">
+                        <property name="margin-top">24</property>
+                        <property name="valign">center</property>
+                        <property name="visible">true</property>
+                        <property name="width-chars">50</property>
+                        <property name="max-width-chars">50</property>
+                        <property name="wrap">true</property>
+                        <property name="xalign">0.0</property>
+                        <style>
+                          <class name="dim-label"/>
+                        </style>
+                      </object>
+                      <packing>
+                        <property name="row">4</property>
+                        <property name="column">1</property>
+                      </packing>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+  <object class="GtkSizeGroup">
+    <property name="mode">horizontal</property>
+    <widgets>
+      <widget name="splash"/>
+      <widget name="branch_entry"/>
+      <widget name="author_entry"/>
+      <widget name="email_entry"/>
+      <widget name="uri_entry"/>
+      <widget name="uri_entry_help"/>
+      <widget name="destination_chooser"/>
+      <widget name="kind_radio"/>
+    </widgets>
+  </object>
+  <object class="GtkSizeGroup">
+    <property name="mode">horizontal</property>
+    <widgets>
+      <widget name="repo_label"/>
+      <widget name="dest_label"/>
+      <widget name="author_label"/>
+      <widget name="email_label"/>
+      <widget name="branch_label"/>
+    </widgets>
+  </object>
+</interface>
diff --git a/src/libide/greeter/ide-greeter-perspective.h b/src/libide/greeter/ide-greeter-private.h
similarity index 56%
rename from src/libide/greeter/ide-greeter-perspective.h
rename to src/libide/greeter/ide-greeter-private.h
index f1d3edf0c..1b41ee4d6 100644
--- a/src/libide/greeter/ide-greeter-perspective.h
+++ b/src/libide/greeter/ide-greeter-private.h
@@ -1,6 +1,6 @@
-/* ide-greeter-perspective.h
+/* ide-greeter-private.h
  *
- * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ * Copyright 2018-2019 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,18 +20,13 @@
 
 #pragma once
 
-#include <gtk/gtk.h>
+#include <libide-projects.h>
 
-#include "projects/ide-recent-projects.h"
+#include "ide-greeter-workspace.h"
 
 G_BEGIN_DECLS
 
-#define IDE_TYPE_GREETER_PERSPECTIVE (ide_greeter_perspective_get_type())
-
-G_DECLARE_FINAL_TYPE (IdeGreeterPerspective, ide_greeter_perspective, IDE, GREETER_PERSPECTIVE, GtkBin)
-
-void ide_greeter_perspective_show_genesis_view (IdeGreeterPerspective *self,
-                                                const gchar *genesis_addin_name,
-                                                const gchar *manifest);
+void _ide_greeter_workspace_init_actions   (IdeGreeterWorkspace *self);
+void _ide_greeter_workspace_init_shortcuts (IdeGreeterWorkspace *self);
 
 G_END_DECLS
diff --git a/src/libide/greeter/ide-greeter-section.c b/src/libide/greeter/ide-greeter-section.c
index b8900937c..6794135d8 100644
--- a/src/libide/greeter/ide-greeter-section.c
+++ b/src/libide/greeter/ide-greeter-section.c
@@ -22,7 +22,7 @@
 
 #include "config.h"
 
-#include "greeter/ide-greeter-section.h"
+#include "ide-greeter-section.h"
 
 G_DEFINE_INTERFACE (IdeGreeterSection, ide_greeter_section, GTK_TYPE_WIDGET)
 
diff --git a/src/libide/greeter/ide-greeter-section.h b/src/libide/greeter/ide-greeter-section.h
index cfd4acf9a..8dc0f939a 100644
--- a/src/libide/greeter/ide-greeter-section.h
+++ b/src/libide/greeter/ide-greeter-section.h
@@ -21,10 +21,8 @@
 #pragma once
 
 #include <dazzle.h>
-
-#include "ide-version-macros.h"
-
-#include "projects/ide-project-info.h"
+#include <libide-core.h>
+#include <libide-projects.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/libide/greeter/ide-greeter-workspace-actions.c 
b/src/libide/greeter/ide-greeter-workspace-actions.c
new file mode 100644
index 000000000..344a8ca10
--- /dev/null
+++ b/src/libide/greeter/ide-greeter-workspace-actions.c
@@ -0,0 +1,223 @@
+/* ide-greeter-workspace-actions.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-greeter-workspace-actions"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libpeas/peas.h>
+
+#include "ide-greeter-private.h"
+#include "ide-greeter-workspace.h"
+
+static void
+ide_greeter_workspace_dialog_response (IdeGreeterWorkspace  *self,
+                                       gint                  response_id,
+                                       GtkFileChooserDialog *dialog)
+{
+  g_assert (IDE_IS_GREETER_WORKSPACE (self));
+  g_assert (GTK_IS_FILE_CHOOSER_DIALOG (dialog));
+
+  if (response_id == GTK_RESPONSE_OK)
+    {
+      g_autoptr(IdeProjectInfo) project_info = NULL;
+      g_autoptr(GFile) project_file = NULL;
+
+      project_file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog));
+
+      project_info = ide_project_info_new ();
+      ide_project_info_set_file (project_info, project_file);
+
+      ide_greeter_workspace_open_project (self, project_info);
+    }
+
+  gtk_widget_destroy (GTK_WIDGET (dialog));
+}
+
+static void
+ide_greeter_workspace_dialog_notify_filter (IdeGreeterWorkspace  *self,
+                                            GParamSpec           *pspec,
+                                            GtkFileChooserDialog *dialog)
+{
+  GtkFileFilter *filter;
+  GtkFileChooserAction action;
+
+  g_assert (IDE_IS_GREETER_WORKSPACE (self));
+  g_assert (pspec != NULL);
+  g_assert (GTK_IS_FILE_CHOOSER_DIALOG (dialog));
+
+  filter = gtk_file_chooser_get_filter (GTK_FILE_CHOOSER (dialog));
+
+  if (filter && g_object_get_data (G_OBJECT (filter), "IS_DIRECTORY"))
+    action = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER;
+  else
+    action = GTK_FILE_CHOOSER_ACTION_OPEN;
+
+  gtk_file_chooser_set_action (GTK_FILE_CHOOSER (dialog), action);
+}
+
+static void
+ide_greeter_workspace_actions_open (GSimpleAction *action,
+                                    GVariant      *param,
+                                    gpointer       user_data)
+{
+  IdeGreeterWorkspace *self = user_data;
+  GtkFileChooserDialog *dialog;
+  GtkFileFilter *all_filter;
+  const GList *list;
+  gint64 last_priority = G_MAXINT64;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (param == NULL);
+  g_assert (IDE_IS_GREETER_WORKSPACE (self));
+
+  list = peas_engine_get_plugin_list (peas_engine_get_default ());
+
+  dialog = g_object_new (GTK_TYPE_FILE_CHOOSER_DIALOG,
+                         "action", GTK_FILE_CHOOSER_ACTION_OPEN,
+                         "transient-for", self,
+                         "modal", TRUE,
+                         "title", _("Open Project"),
+                         "visible", TRUE,
+                         NULL);
+  gtk_dialog_add_buttons (GTK_DIALOG (dialog),
+                          _("Cancel"), GTK_RESPONSE_CANCEL,
+                          _("Open"), GTK_RESPONSE_OK,
+                          NULL);
+  gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_OK);
+
+  g_signal_connect_object (dialog,
+                           "notify::filter",
+                           G_CALLBACK (ide_greeter_workspace_dialog_notify_filter),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  all_filter = gtk_file_filter_new ();
+  gtk_file_filter_set_name (all_filter, _("All Project Types"));
+  gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (dialog), all_filter);
+
+  /* For testing with no plugins */
+  if (list == NULL)
+    gtk_file_filter_add_pattern (all_filter, "*");
+
+  for (; list != NULL; list = list->next)
+    {
+      PeasPluginInfo *plugin_info = list->data;
+      GtkFileFilter *filter;
+      const gchar *pattern;
+      const gchar *content_type;
+      const gchar *name;
+      const gchar *priority;
+      gchar **patterns;
+      gchar **content_types;
+      gint i;
+
+      if (!peas_plugin_info_is_loaded (plugin_info))
+        continue;
+
+      name = peas_plugin_info_get_external_data (plugin_info, "X-Project-File-Filter-Name");
+      if (name == NULL)
+        continue;
+
+      pattern = peas_plugin_info_get_external_data (plugin_info, "X-Project-File-Filter-Pattern");
+      content_type = peas_plugin_info_get_external_data (plugin_info, "X-Project-File-Filter-Content-Type");
+      priority = peas_plugin_info_get_external_data (plugin_info, "X-Project-File-Filter-Priority");
+
+      if (pattern == NULL && content_type == NULL)
+        continue;
+
+      patterns = g_strsplit (pattern ?: "", ",", 0);
+      content_types = g_strsplit (content_type ?: "", ",", 0);
+
+      filter = gtk_file_filter_new ();
+
+      gtk_file_filter_set_name (filter, name);
+
+      for (i = 0; patterns [i] != NULL; i++)
+        {
+          if (*patterns [i])
+            {
+              gtk_file_filter_add_pattern (filter, patterns [i]);
+              gtk_file_filter_add_pattern (all_filter, patterns [i]);
+            }
+        }
+
+      for (i = 0; content_types [i] != NULL; i++)
+        {
+          if (*content_types [i])
+            {
+              gtk_file_filter_add_mime_type (filter, content_types [i]);
+              gtk_file_filter_add_mime_type (all_filter, content_types [i]);
+
+              /* Helper so we can change the file chooser action to OPEN_DIRECTORY,
+               * otherwise the user won't be able to choose a directory, it will
+               * instead dive into the directory.
+               */
+              if (g_strcmp0 (content_types [i], "inode/directory") == 0)
+                g_object_set_data (G_OBJECT (filter), "IS_DIRECTORY", GINT_TO_POINTER (1));
+            }
+        }
+
+      gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (dialog), filter);
+
+      /* Look at the priority to set the default file filter. */
+      if (priority != NULL)
+        {
+          gint64 pval = g_ascii_strtoll (priority, NULL, 10);
+
+          if (pval < last_priority)
+            {
+              gtk_file_chooser_set_filter (GTK_FILE_CHOOSER (dialog), filter);
+              last_priority = pval;
+            }
+        }
+
+      g_strfreev (patterns);
+      g_strfreev (content_types);
+    }
+
+  g_signal_connect_object (dialog,
+                           "response",
+                           G_CALLBACK (ide_greeter_workspace_dialog_response),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  /* If unset, set the default filter */
+  if (last_priority == G_MAXINT64)
+    gtk_file_chooser_set_filter (GTK_FILE_CHOOSER (dialog), all_filter);
+
+  gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER (dialog),
+                                       ide_get_projects_dir ());
+
+  gtk_window_present (GTK_WINDOW (dialog));
+}
+
+static const GActionEntry actions[] = {
+  { "open", ide_greeter_workspace_actions_open },
+};
+
+void
+_ide_greeter_workspace_init_actions (IdeGreeterWorkspace *self)
+{
+  g_return_if_fail (IDE_IS_GREETER_WORKSPACE (self));
+
+  g_action_map_add_action_entries (G_ACTION_MAP (self), actions, G_N_ELEMENTS (actions), self);
+}
diff --git a/src/libide/greeter/ide-greeter-workspace-shortcuts.c 
b/src/libide/greeter/ide-greeter-workspace-shortcuts.c
new file mode 100644
index 000000000..18a1a9472
--- /dev/null
+++ b/src/libide/greeter/ide-greeter-workspace-shortcuts.c
@@ -0,0 +1,44 @@
+/* ide-greeter-workspace-shortcuts.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-greeter-workspace-shortcuts"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "ide-greeter-private.h"
+#include "ide-greeter-workspace.h"
+
+#define I_(s) (g_intern_static_string(s))
+
+void
+_ide_greeter_workspace_init_shortcuts (IdeGreeterWorkspace *self)
+{
+  DzlShortcutController *controller;
+
+  controller = dzl_shortcut_controller_find (GTK_WIDGET (self));
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              I_("org.gnome.builder.greeter.close"),
+                                              "<Primary>w",
+                                              DZL_SHORTCUT_PHASE_DISPATCH,
+                                              I_("win.close"));
+}
diff --git a/src/libide/greeter/ide-greeter-workspace.c b/src/libide/greeter/ide-greeter-workspace.c
new file mode 100644
index 000000000..2a31ce4a7
--- /dev/null
+++ b/src/libide/greeter/ide-greeter-workspace.c
@@ -0,0 +1,808 @@
+/* ide-greeter-workspace.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-greeter-workspace"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <libpeas/peas.h>
+
+#include "ide-clone-surface.h"
+#include "ide-greeter-private.h"
+#include "ide-greeter-workspace.h"
+
+/**
+ * SECTION:ide-greeter-workspace
+ * @title: IdeGreeterWorkspace
+ * @short_description: The greeter upon starting Builder
+ *
+ * Use the #IdeWorkspace APIs to add surfaces for user guides such
+ * as the git workflow or project creation wizard.
+ *
+ * You can add buttons to the headerbar and use actions to change
+ * surfaces such as "win.surface::'surface-name'".
+ *
+ * Since: 3.32
+ */
+
+struct _IdeGreeterWorkspace
+{
+  IdeWorkspace       parent_instance;
+
+  PeasExtensionSet  *addins;
+  DzlPatternSpec    *pattern_spec;
+  GSimpleAction     *delete_action;
+  GSimpleAction     *purge_action;
+
+  /* Template Widgets */
+  IdeCloneSurface   *clone_surface;
+  IdeHeaderBar      *header_bar;
+  DzlPriorityBox    *sections;
+  DzlPriorityBox    *left_box;
+  GtkStack          *surfaces;
+  IdeSurface        *sections_surface;
+  GtkSearchEntry    *search_entry;
+  GtkButton         *back_button;
+  GtkButton         *select_button;
+  GtkActionBar      *action_bar;
+
+  guint              selection_mode : 1;
+};
+
+G_DEFINE_TYPE (IdeGreeterWorkspace, ide_greeter_workspace, IDE_TYPE_WORKSPACE)
+
+enum {
+  PROP_0,
+  PROP_SELECTION_MODE,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_greeter_workspace_filter_sections (PeasExtensionSet *set,
+                                       PeasPluginInfo   *plugin_info,
+                                       PeasExtension    *exten,
+                                       gpointer          user_data)
+{
+  IdeGreeterWorkspace *self = user_data;
+  IdeGreeterSection *section = (IdeGreeterSection *)exten;
+  gboolean has_child;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_GREETER_SECTION (section));
+  g_assert (IDE_IS_GREETER_WORKSPACE (self));
+
+  has_child = ide_greeter_section_filter (section, self->pattern_spec);
+
+  gtk_widget_set_visible (GTK_WIDGET (section), has_child);
+}
+
+static void
+ide_greeter_workspace_apply_filter_all (IdeGreeterWorkspace *self)
+{
+  const gchar *text;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_GREETER_WORKSPACE (self));
+
+  g_clear_pointer (&self->pattern_spec, dzl_pattern_spec_unref);
+
+  if (NULL != (text = gtk_entry_get_text (GTK_ENTRY (self->search_entry))))
+    self->pattern_spec = dzl_pattern_spec_new (text);
+
+  if (self->addins != NULL)
+    peas_extension_set_foreach (self->addins,
+                                ide_greeter_workspace_filter_sections,
+                                self);
+}
+
+static void
+ide_greeter_workspace_activate_cb (GtkWidget *widget,
+                                   gpointer   user_data)
+{
+  gboolean *handled = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GTK_IS_WIDGET (widget));
+  g_assert (handled != NULL);
+
+  if (!IDE_IS_GREETER_SECTION (widget))
+    return;
+
+  if (!*handled)
+    *handled = ide_greeter_section_activate_first (IDE_GREETER_SECTION (widget));
+}
+
+static void
+ide_greeter_workspace_search_entry_activate (IdeGreeterWorkspace *self,
+                                             GtkSearchEntry      *search_entry)
+{
+  gboolean handled = FALSE;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_GREETER_WORKSPACE (self));
+  g_assert (GTK_IS_SEARCH_ENTRY (search_entry));
+
+  gtk_container_foreach (GTK_CONTAINER (self->sections),
+                         ide_greeter_workspace_activate_cb,
+                         &handled);
+
+  if (!handled)
+    gdk_window_beep (gtk_widget_get_window (GTK_WIDGET (search_entry)));
+}
+
+static void
+ide_greeter_workspace_search_entry_changed (IdeGreeterWorkspace *self,
+                                            GtkSearchEntry      *search_entry)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_GREETER_WORKSPACE (self));
+  g_assert (GTK_IS_SEARCH_ENTRY (search_entry));
+
+  ide_greeter_workspace_apply_filter_all (self);
+}
+
+static void
+stack_notify_visible_child_cb (IdeGreeterWorkspace *self,
+                               GParamSpec          *pspec,
+                               GtkStack            *stack)
+{
+  g_autofree gchar *title = NULL;
+  GtkWidget *visible_child;
+  gboolean sections;
+
+  g_assert (IDE_IS_GREETER_WORKSPACE (self));
+  g_assert (GTK_IS_STACK (stack));
+
+  visible_child = gtk_stack_get_visible_child (stack);
+
+  if (DZL_IS_DOCK_ITEM (visible_child))
+    title = dzl_dock_item_get_title (DZL_DOCK_ITEM (visible_child));
+
+  gtk_header_bar_set_title (GTK_HEADER_BAR (self->header_bar), title);
+
+  sections = ide_str_equal0 ("sections", gtk_stack_get_visible_child_name (stack));
+
+  gtk_widget_set_visible (GTK_WIDGET (self->left_box), sections);
+  gtk_widget_set_visible (GTK_WIDGET (self->back_button), !sections);
+  gtk_widget_set_visible (GTK_WIDGET (self->select_button), sections);
+}
+
+static void
+ide_greeter_workspace_addin_added_cb (PeasExtensionSet *set,
+                                      PeasPluginInfo   *plugin_info,
+                                      PeasExtension    *exten,
+                                      gpointer          user_data)
+{
+  IdeGreeterSection *section = (IdeGreeterSection *)exten;
+  IdeGreeterWorkspace *self = user_data;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_GREETER_SECTION (section));
+  g_assert (IDE_IS_GREETER_WORKSPACE (self));
+
+  /* Don't allow floating, to work with extension set*/
+  if (g_object_is_floating (G_OBJECT (section)))
+    g_object_ref_sink (section);
+
+  gtk_widget_show (GTK_WIDGET (section));
+
+  ide_greeter_workspace_add_section (self, section);
+}
+
+static void
+ide_greeter_workspace_addin_removed_cb (PeasExtensionSet *set,
+                                        PeasPluginInfo   *plugin_info,
+                                        PeasExtension    *exten,
+                                        gpointer          user_data)
+{
+  IdeGreeterSection *section = (IdeGreeterSection *)exten;
+  IdeGreeterWorkspace *self = user_data;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_GREETER_SECTION (section));
+  g_assert (IDE_IS_GREETER_WORKSPACE (self));
+
+  gtk_widget_destroy (GTK_WIDGET (section));
+}
+
+static void
+ide_greeter_workspace_constructed (GObject *object)
+{
+  IdeGreeterWorkspace *self = (IdeGreeterWorkspace *)object;
+
+  G_OBJECT_CLASS (ide_greeter_workspace_parent_class)->constructed (object);
+
+  dzl_gtk_widget_add_style_class (GTK_WIDGET (self), "greeter");
+
+  self->addins = peas_extension_set_new (peas_engine_get_default (),
+                                         IDE_TYPE_GREETER_SECTION,
+                                         NULL);
+
+  g_signal_connect (self->addins,
+                    "extension-added",
+                    G_CALLBACK (ide_greeter_workspace_addin_added_cb),
+                    self);
+
+  g_signal_connect (self->addins,
+                    "extension-removed",
+                    G_CALLBACK (ide_greeter_workspace_addin_removed_cb),
+                    self);
+
+  peas_extension_set_foreach (self->addins,
+                              ide_greeter_workspace_addin_added_cb,
+                              self);
+
+  /* Ensure that no plugin changed our page */
+  ide_workspace_set_visible_surface_name (IDE_WORKSPACE (self), "sections");
+
+  gtk_widget_grab_focus (GTK_WIDGET (self->search_entry));
+}
+
+static void
+ide_greeter_workspace_open_project_cb (GObject      *object,
+                                       GAsyncResult *result,
+                                       gpointer      user_data)
+{
+  IdeWorkbench *workbench = (IdeWorkbench *)object;
+  g_autoptr(IdeGreeterWorkspace) self = (IdeGreeterWorkspace *)user_data;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_WORKBENCH (workbench));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_GREETER_WORKSPACE (self));
+
+  if (!ide_workbench_load_project_finish (workbench, result, &error))
+    {
+      GtkWidget *dialog;
+
+      dialog = gtk_message_dialog_new (GTK_WINDOW (workbench),
+                                       GTK_DIALOG_USE_HEADER_BAR,
+                                       GTK_MESSAGE_ERROR,
+                                       GTK_BUTTONS_CLOSE,
+                                       _("Failed to load the project"));
+
+      g_object_set (dialog,
+                    "modal", TRUE,
+                    "secondary-text", error->message,
+                    NULL);
+
+      g_signal_connect (dialog,
+                        "response",
+                        G_CALLBACK (gtk_widget_destroy),
+                        NULL);
+      g_signal_connect_swapped (dialog,
+                                "response",
+                                G_CALLBACK (gtk_widget_destroy),
+                                workbench);
+
+      gtk_window_present (GTK_WINDOW (dialog));
+
+      ide_greeter_workspace_end (self);
+    }
+
+  gtk_widget_destroy (GTK_WIDGET (self));
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_greeter_workspace_open_project:
+ * @self: an #IdeGreeterWorkspace
+ * @project_info: an #IdeProjectInfo
+ *
+ * Opens the project described by @project_info.
+ *
+ * This is useful by greeter workspace extensions that add new surfaces
+ * which may not have other means to activate a project.
+ *
+ * Since: 3.32
+ */
+void
+ide_greeter_workspace_open_project (IdeGreeterWorkspace *self,
+                                    IdeProjectInfo      *project_info)
+{
+  IdeWorkbench *workbench;
+  const gchar *vcs_uri = NULL;
+  GFile *file;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_GREETER_WORKSPACE (self));
+  g_return_if_fail (IDE_IS_PROJECT_INFO (project_info));
+
+  /* If there is a VCS Uri and no project file/directory, then we want
+   * to switch to the clone dialog. However, we can use the VCS Uri to
+   * determine what the check-out directory would be, and if so, we can
+   * just open that directory.
+   */
+  if (!ide_project_info_get_file (project_info) &&
+      !ide_project_info_get_directory (project_info) &&
+      (vcs_uri = ide_project_info_get_vcs_uri (project_info)))
+    {
+      g_autoptr(IdeVcsUri) uri = ide_vcs_uri_new (vcs_uri);
+      g_autofree gchar *suggested = NULL;
+      g_autofree gchar *checkout = NULL;
+
+      if (uri != NULL &&
+          (suggested = ide_vcs_uri_get_clone_name (uri)) &&
+          (checkout = g_build_filename (ide_get_projects_dir (), suggested, NULL)) &&
+          g_file_test (checkout, G_FILE_TEST_IS_DIR))
+        {
+          g_autoptr(GFile) directory = g_file_new_for_path (checkout);
+          ide_project_info_set_directory (project_info, directory);
+        }
+      else
+        {
+          ide_clone_surface_set_uri (self->clone_surface, vcs_uri);
+          ide_workspace_set_visible_surface_name (IDE_WORKSPACE (self), "clone");
+          return;
+        }
+    }
+
+  workbench = ide_widget_get_workbench (GTK_WIDGET (self));
+
+  ide_greeter_workspace_begin (self);
+
+  if (ide_project_info_get_directory (project_info) == NULL)
+    {
+      if ((file = ide_project_info_get_file (project_info)))
+        {
+          g_autoptr(GFile) parent = g_file_get_parent (file);
+
+          /* If it's a directory, set that too, otherwise use the parent */
+          if (g_file_query_file_type (file, 0, NULL) == G_FILE_TYPE_DIRECTORY)
+            ide_project_info_set_directory (project_info, file);
+          else
+            ide_project_info_set_directory (project_info, parent);
+        }
+    }
+
+  ide_workbench_load_project_async (workbench,
+                                    project_info,
+                                    IDE_TYPE_PRIMARY_WORKSPACE,
+                                    ide_workspace_get_cancellable (IDE_WORKSPACE (self)),
+                                    ide_greeter_workspace_open_project_cb,
+                                    g_object_ref (self));
+
+  IDE_EXIT;
+}
+
+static void
+ide_greeter_workspace_project_activated_cb (IdeGreeterWorkspace *self,
+                                            IdeProjectInfo      *project_info,
+                                            IdeGreeterSection   *section)
+{
+  g_assert (IDE_IS_GREETER_WORKSPACE (self));
+  g_assert (IDE_IS_PROJECT_INFO (project_info));
+  g_assert (IDE_IS_GREETER_SECTION (section));
+
+  ide_greeter_workspace_open_project (self, project_info);
+}
+
+static void
+ide_greeter_workspace_delete_selected_rows_cb (GtkWidget *widget,
+                                               gpointer   user_data)
+{
+  if (IDE_IS_GREETER_SECTION (widget))
+    ide_greeter_section_delete_selected (IDE_GREETER_SECTION (widget));
+}
+
+static void
+ide_greeter_workspace_delete_selected_rows (GSimpleAction *action,
+                                            GVariant      *param,
+                                            gpointer       user_data)
+{
+  IdeGreeterWorkspace *self = user_data;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (param == NULL);
+  g_assert (IDE_IS_GREETER_WORKSPACE (self));
+
+  gtk_container_foreach (GTK_CONTAINER (self->sections),
+                         ide_greeter_workspace_delete_selected_rows_cb,
+                         NULL);
+  ide_greeter_workspace_apply_filter_all (self);
+  ide_greeter_workspace_set_selection_mode (self, FALSE);
+}
+
+static void
+ide_greeter_workspace_purge_selected_rows_cb (GtkWidget *widget,
+                                              gpointer   user_data)
+{
+  if (IDE_IS_GREETER_SECTION (widget))
+    ide_greeter_section_purge_selected (IDE_GREETER_SECTION (widget));
+}
+
+static void
+purge_selected_rows_response (IdeGreeterWorkspace *self,
+                              gint                 response,
+                              GtkDialog           *dialog)
+{
+  g_assert (IDE_IS_GREETER_WORKSPACE (self));
+  g_assert (GTK_IS_DIALOG (dialog));
+
+  if (response == GTK_RESPONSE_OK)
+    {
+      gtk_container_foreach (GTK_CONTAINER (self->sections),
+                             ide_greeter_workspace_purge_selected_rows_cb,
+                             NULL);
+      ide_greeter_workspace_apply_filter_all (self);
+      ide_greeter_workspace_set_selection_mode (self, FALSE);
+    }
+
+  gtk_widget_destroy (GTK_WIDGET (dialog));
+}
+
+static void
+ide_greeter_workspace_purge_selected_rows (GSimpleAction *action,
+                                           GVariant      *param,
+                                           gpointer       user_data)
+{
+  IdeGreeterWorkspace *self = user_data;
+  GtkWidget *parent;
+  GtkWidget *button;
+  GtkDialog *dialog;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (param == NULL);
+  g_assert (IDE_IS_GREETER_WORKSPACE (self));
+
+  parent = gtk_widget_get_ancestor (GTK_WIDGET (self), GTK_TYPE_WINDOW);
+  dialog = g_object_new (GTK_TYPE_MESSAGE_DIALOG,
+                         "modal", TRUE,
+                         "transient-for", parent,
+                         "attached-to", parent,
+                         "text", _("Removing project sources will delete them from your computer and cannot 
be undone."),
+                         NULL);
+  gtk_dialog_add_buttons (dialog,
+                          _("Cancel"), GTK_RESPONSE_CANCEL,
+                          _("Delete Project Sources"), GTK_RESPONSE_OK,
+                          NULL);
+  button = gtk_dialog_get_widget_for_response (dialog, GTK_RESPONSE_OK);
+  dzl_gtk_widget_add_style_class (button, "destructive-action");
+  g_signal_connect_data (dialog,
+                         "response",
+                         G_CALLBACK (purge_selected_rows_response),
+                         g_object_ref (self),
+                         (GClosureNotify)g_object_unref,
+                         G_CONNECT_SWAPPED);
+  gtk_window_present (GTK_WINDOW (dialog));
+}
+
+static void
+ide_greeter_workspace_destroy (GtkWidget *widget)
+{
+  IdeGreeterWorkspace *self = (IdeGreeterWorkspace *)widget;
+
+  g_clear_object (&self->addins);
+  g_clear_object (&self->delete_action);
+  g_clear_object (&self->purge_action);
+  g_clear_pointer (&self->pattern_spec, dzl_pattern_spec_unref);
+
+  GTK_WIDGET_CLASS (ide_greeter_workspace_parent_class)->destroy (widget);
+}
+
+static void
+ide_greeter_workspace_get_property (GObject    *object,
+                                    guint       prop_id,
+                                    GValue     *value,
+                                    GParamSpec *pspec)
+{
+  IdeGreeterWorkspace *self = IDE_GREETER_WORKSPACE (object);
+
+  switch (prop_id)
+    {
+    case PROP_SELECTION_MODE:
+      g_value_set_boolean (value, ide_greeter_workspace_get_selection_mode (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_greeter_workspace_set_property (GObject      *object,
+                                    guint         prop_id,
+                                    const GValue *value,
+                                    GParamSpec   *pspec)
+{
+  IdeGreeterWorkspace *self = IDE_GREETER_WORKSPACE (object);
+
+  switch (prop_id)
+    {
+    case PROP_SELECTION_MODE:
+      ide_greeter_workspace_set_selection_mode (self, g_value_get_boolean (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_greeter_workspace_class_init (IdeGreeterWorkspaceClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  IdeWorkspaceClass *workspace_class = IDE_WORKSPACE_CLASS (klass);
+
+  object_class->constructed = ide_greeter_workspace_constructed;
+  object_class->get_property = ide_greeter_workspace_get_property;
+  object_class->set_property = ide_greeter_workspace_set_property;
+
+  widget_class->destroy = ide_greeter_workspace_destroy;
+
+  /**
+   * IdeGreeterWorkspace:selection-mode:
+   *
+   * The "selection-mode" property indicates if the workspace allows
+   * selecting existing projects and removing them, including source files
+   * and cached data.
+   *
+   * This is usually used by the checkmark button to toggle selections.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_SELECTION_MODE] =
+    g_param_spec_boolean ("selection-mode",
+                          "Selection Mode",
+                          "If the workspace is in selection mode",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  ide_workspace_class_set_kind (workspace_class, "greeter");
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/builder/ui/ide-greeter-workspace.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeGreeterWorkspace, action_bar);
+  gtk_widget_class_bind_template_child (widget_class, IdeGreeterWorkspace, back_button);
+  gtk_widget_class_bind_template_child (widget_class, IdeGreeterWorkspace, clone_surface);
+  gtk_widget_class_bind_template_child (widget_class, IdeGreeterWorkspace, header_bar);
+  gtk_widget_class_bind_template_child (widget_class, IdeGreeterWorkspace, left_box);
+  gtk_widget_class_bind_template_child (widget_class, IdeGreeterWorkspace, search_entry);
+  gtk_widget_class_bind_template_child (widget_class, IdeGreeterWorkspace, select_button);
+  gtk_widget_class_bind_template_child (widget_class, IdeGreeterWorkspace, surfaces);
+  gtk_widget_class_bind_template_child (widget_class, IdeGreeterWorkspace, sections);
+  gtk_widget_class_bind_template_callback (widget_class, stack_notify_visible_child_cb);
+
+  g_type_ensure (IDE_TYPE_CLONE_SURFACE);
+}
+
+static void
+ide_greeter_workspace_init (IdeGreeterWorkspace *self)
+{
+  g_autoptr(GPropertyAction) selection_action = NULL;
+  static const GActionEntry actions[] = {
+    { "purge-selected-rows", ide_greeter_workspace_purge_selected_rows },
+    { "delete-selected-rows", ide_greeter_workspace_delete_selected_rows },
+  };
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  selection_action = g_property_action_new ("selection-mode", G_OBJECT (self), "selection-mode");
+  g_action_map_add_action (G_ACTION_MAP (self), G_ACTION (selection_action));
+  g_action_map_add_action_entries (G_ACTION_MAP (self), actions, G_N_ELEMENTS (actions), self);
+
+  g_signal_connect_object (self->search_entry,
+                           "activate",
+                           G_CALLBACK (ide_greeter_workspace_search_entry_activate),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->search_entry,
+                           "changed",
+                           G_CALLBACK (ide_greeter_workspace_search_entry_changed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  stack_notify_visible_child_cb (self, NULL, self->surfaces);
+
+  _ide_greeter_workspace_init_actions (self);
+  _ide_greeter_workspace_init_shortcuts (self);
+}
+
+IdeGreeterWorkspace *
+ide_greeter_workspace_new (IdeApplication *app)
+{
+  return g_object_new (IDE_TYPE_GREETER_WORKSPACE,
+                       "application", app,
+                       "default-width", 1000,
+                       "default-height", 600,
+                       NULL);
+}
+
+/**
+ * ide_greeter_workspace_add_section:
+ * @self: a #IdeGreeterWorkspace
+ * @section: an #IdeGreeterSection based #GtkWidget
+ *
+ * Adds the #IdeGreeterSection to the display.
+ *
+ * Since: 3.32
+ */
+void
+ide_greeter_workspace_add_section (IdeGreeterWorkspace *self,
+                                   IdeGreeterSection   *section)
+{
+  g_return_if_fail (IDE_IS_GREETER_WORKSPACE (self));
+  g_return_if_fail (IDE_IS_GREETER_SECTION (section));
+
+  g_signal_connect_object (section,
+                           "project-activated",
+                           G_CALLBACK (ide_greeter_workspace_project_activated_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  gtk_container_add_with_properties (GTK_CONTAINER (self->sections), GTK_WIDGET (section),
+                                     "priority", ide_greeter_section_get_priority (section),
+                                     NULL);
+}
+
+/**
+ * ide_greeter_workspace_remove_section:
+ * @self: a #IdeGreeterWorkspace
+ * @section: an #IdeGreeterSection based #GtkWidget
+ *
+ * Remvoes the #IdeGreeterSection from the display. This should be a section
+ * that was previously added with ide_greeter_workspace_add_section().
+ *
+ * Plugins should clean up after themselves when they are unloaded, which may
+ * include calling this function.
+ *
+ * Since: 3.32
+ */
+void
+ide_greeter_workspace_remove_section (IdeGreeterWorkspace *self,
+                                      IdeGreeterSection   *section)
+{
+  g_return_if_fail (IDE_IS_GREETER_WORKSPACE (self));
+  g_return_if_fail (IDE_IS_GREETER_SECTION (section));
+
+  gtk_container_remove (GTK_CONTAINER (self->sections), GTK_WIDGET (section));
+}
+
+void
+ide_greeter_workspace_add_button (IdeGreeterWorkspace *self,
+                                  GtkWidget           *button,
+                                  gint                 priority)
+{
+  g_return_if_fail (IDE_IS_GREETER_WORKSPACE (self));
+  g_return_if_fail (GTK_IS_WIDGET (button));
+
+  gtk_container_add_with_properties (GTK_CONTAINER (self->left_box), button,
+                                     "priority", priority,
+                                     NULL);
+}
+
+/**
+ * ide_greeter_workspace_begin:
+ * @self: a #IdeGreeterWorkspace
+ *
+ * This function will disable various actions and should be called before
+ * an #IdeGreeterAddin begins doing work that cannot be undone except to
+ * cancel the operation.
+ *
+ * Actions such as switching guides will be disabled during this process.
+ *
+ * See ide_greeter_workspace_end() to restore actions.
+ *
+ * Since: 3.32
+ */
+void
+ide_greeter_workspace_begin (IdeGreeterWorkspace *self)
+{
+  g_return_if_fail (IDE_IS_GREETER_WORKSPACE (self));
+
+  dzl_gtk_widget_action_set (GTK_WIDGET (self), "win", "open",
+                             "enabled", FALSE,
+                             NULL);
+  dzl_gtk_widget_action_set (GTK_WIDGET (self), "win", "surface",
+                             "enabled", FALSE,
+                             NULL);
+}
+
+/**
+ * ide_greeter_workspace_end:
+ * @self: a #IdeGreeterWorkspace
+ *
+ * Restores actions after a call to ide_greeter_workspace_begin().
+ *
+ * Since: 3.32
+ */
+void
+ide_greeter_workspace_end (IdeGreeterWorkspace *self)
+{
+  g_return_if_fail (IDE_IS_GREETER_WORKSPACE (self));
+
+  dzl_gtk_widget_action_set (GTK_WIDGET (self), "win", "open",
+                             "enabled", TRUE,
+                             NULL);
+  dzl_gtk_widget_action_set (GTK_WIDGET (self), "win", "surface",
+                             "enabled", TRUE,
+                             NULL);
+}
+
+/**
+ * ide_greeter_workspace_get_selection_mode:
+ * @self: a #IdeGreeterWorkspace
+ *
+ * Gets if the greeter is in selection mode, which means that the workspace
+ * allows selecting projects for removal.
+ *
+ * Returns: %TRUE if in selection mode, otherwise %FALSE
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_greeter_workspace_get_selection_mode (IdeGreeterWorkspace *self)
+{
+  g_return_val_if_fail (IDE_IS_GREETER_WORKSPACE (self), FALSE);
+
+  return self->selection_mode;
+}
+
+static void
+ide_greeter_workspace_set_selection_mode_cb (GtkWidget *widget,
+                                             gpointer   user_data)
+{
+  if (IDE_IS_GREETER_SECTION (widget))
+    ide_greeter_section_set_selection_mode (IDE_GREETER_SECTION (widget),
+                                            GPOINTER_TO_INT (user_data));
+}
+
+/**
+ * ide_greeter_workspace_set_selection_mode:
+ * @self: a #IdeGreeterWorkspace
+ * @selection_mode: if the workspace should be in selection mode
+ *
+ * Sets the workspace in selection mode.
+ *
+ * Since: 3.32
+ */
+void
+ide_greeter_workspace_set_selection_mode (IdeGreeterWorkspace *self,
+                                          gboolean             selection_mode)
+{
+  g_return_if_fail (IDE_IS_GREETER_WORKSPACE (self));
+
+  selection_mode = !!selection_mode;
+
+  if (selection_mode != self->selection_mode)
+    {
+      self->selection_mode = selection_mode;
+      gtk_container_foreach (GTK_CONTAINER (self->sections),
+                             ide_greeter_workspace_set_selection_mode_cb,
+                             GINT_TO_POINTER (selection_mode));
+      gtk_widget_set_visible (GTK_WIDGET (self->action_bar), selection_mode);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SELECTION_MODE]);
+    }
+}
diff --git a/src/libide/greeter/ide-greeter-workspace.h b/src/libide/greeter/ide-greeter-workspace.h
new file mode 100644
index 000000000..d3b0dd749
--- /dev/null
+++ b/src/libide/greeter/ide-greeter-workspace.h
@@ -0,0 +1,61 @@
+/* ide-greeter-workspace.h
+ *
+ * Copyright 2018-2019 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-projects.h>
+#include <libide-gui.h>
+
+#include "ide-greeter-section.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_GREETER_WORKSPACE (ide_greeter_workspace_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeGreeterWorkspace, ide_greeter_workspace, IDE, GREETER_WORKSPACE, IdeWorkspace)
+
+IDE_AVAILABLE_IN_3_32
+IdeGreeterWorkspace *ide_greeter_workspace_new                (IdeApplication      *app);
+IDE_AVAILABLE_IN_3_32
+void                 ide_greeter_workspace_add_section        (IdeGreeterWorkspace *self,
+                                                               IdeGreeterSection   *section);
+IDE_AVAILABLE_IN_3_32
+void                 ide_greeter_workspace_remove_section     (IdeGreeterWorkspace *self,
+                                                               IdeGreeterSection   *section);
+IDE_AVAILABLE_IN_3_32
+void                 ide_greeter_workspace_add_button         (IdeGreeterWorkspace *self,
+                                                               GtkWidget           *button,
+                                                               gint                 priority);
+IDE_AVAILABLE_IN_3_32
+void                 ide_greeter_workspace_begin              (IdeGreeterWorkspace *self);
+IDE_AVAILABLE_IN_3_32
+void                 ide_greeter_workspace_end                (IdeGreeterWorkspace *self);
+IDE_AVAILABLE_IN_3_32
+gboolean             ide_greeter_workspace_get_selection_mode (IdeGreeterWorkspace *self);
+IDE_AVAILABLE_IN_3_32
+void                 ide_greeter_workspace_set_selection_mode (IdeGreeterWorkspace *self,
+                                                               gboolean             selection_mode);
+IDE_AVAILABLE_IN_3_32
+void                 ide_greeter_workspace_open_project       (IdeGreeterWorkspace *self,
+                                                               IdeProjectInfo      *project_info);
+
+
+G_END_DECLS
diff --git a/src/libide/greeter/ide-greeter-workspace.ui b/src/libide/greeter/ide-greeter-workspace.ui
new file mode 100644
index 000000000..905e14441
--- /dev/null
+++ b/src/libide/greeter/ide-greeter-workspace.ui
@@ -0,0 +1,177 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.24 -->
+  <template class="IdeGreeterWorkspace" parent="IdeWorkspace">
+    <child type="titlebar">
+      <object class="IdeHeaderBar" id="header_bar">
+        <property name="menu-id">ide-greeter-workspace-menu</property>
+        <property name="show-fullscreen-button">false</property>
+        <property name="show-close-button">true</property>
+        <property name="visible">true</property>
+        <child type="left">
+          <object class="GtkButton" id="back_button">
+            <property name="action-name">win.surface</property>
+            <property name="action-target">'sections'</property>
+            <property name="has-tooltip">true</property>
+            <property name="tooltip-text" translatable="yes">Go back</property>
+            <property name="margin-end">6</property>
+            <child>
+              <object class="GtkImage">
+                <property name="visible">true</property>
+                <property name="icon-name">pan-start-symbolic</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child type="left">
+          <object class="DzlPriorityBox" id="left_box">
+            <property name="spacing">10</property>
+            <property name="hexpand">false</property>
+            <property name="homogeneous">true</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkButton">
+                <property name="action-name">win.open</property>
+                <property name="label" translatable="yes">_Open…</property>
+                <property name="use-underline">true</property>
+                <property name="visible">true</property>
+              </object>
+              <packing>
+                <property name="priority">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkButton">
+                <property name="action-name">win.surface</property>
+                <property name="action-target">'clone'</property>
+                <property name="label" translatable="yes">_Clone…</property>
+                <property name="use-underline">true</property>
+                <property name="visible">true</property>
+              </object>
+              <packing>
+                <property name="priority">10</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="pack-type">start</property>
+          </packing>
+        </child>
+        <child type="right">
+          <object class="GtkToggleButton" id="select_button">
+            <property name="action-name">win.selection-mode</property>
+            <property name="visible">true</property>
+            <style>
+              <class name="image-button"/>
+            </style>
+            <child>
+              <object class="GtkImage">
+                <property name="icon-name">object-select-symbolic</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="pack-type">end</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+    <child internal-child="surfaces">
+      <object class="GtkStack" id="surfaces">
+        <property name="transition-type">crossfade</property>
+        <signal name="notify::visible-child" handler="stack_notify_visible_child_cb" 
object="IdeGreeterWorkspace" swapped="true"/>
+        <child>
+          <object class="IdeSurface" id="sections_surface">
+            <property name="title" translatable="yes">Select a Project</property>
+            <property name="visible">true</property>
+            <style>
+              <class name="sectionssurface"/>
+            </style>
+            <child>
+              <object class="GtkBox">
+                <property name="orientation">vertical</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="GtkScrolledWindow">
+                    <property name="expand">true</property>
+                    <property name="hscrollbar-policy">never</property>
+                    <property name="visible">true</property>
+                    <child>
+                      <object class="GtkViewport">
+                        <property name="expand">true</property>
+                        <property name="visible">true</property>
+                        <child>
+                          <object class="GtkBox">
+                            <property name="margin">32</property>
+                            <property name="orientation">vertical</property>
+                            <property name="spacing">24</property>
+                            <property name="visible">true</property>
+                            <child>
+                              <object class="GtkSearchEntry" id="search_entry">
+                                <property name="halign">center</property>
+                                <property name="visible">true</property>
+                                <property name="width-chars">45</property>
+                              </object>
+                            </child>
+                            <child>
+                              <object class="DzlPriorityBox" id="sections">
+                                <property name="orientation">vertical</property>
+                                <property name="spacing">32</property>
+                                <property name="visible">true</property>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkActionBar" id="action_bar">
+                    <child>
+                      <object class="GtkButton" id="remove_button">
+                        <property name="action-name">win.delete-selected-rows</property>
+                        <property name="label" translatable="yes">_Remove Projects</property>
+                        <property name="use-underline">true</property>
+                        <property name="visible">true</property>
+                        <property name="sensitive">false</property>
+                        <style>
+                          <class name="destructive-action"/>
+                        </style>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkButton" id="purge_button">
+                        <property name="action-name">win.purge-selected-rows</property>
+                        <property name="label" translatable="yes">Remove Projects and Sources…</property>
+                        <property name="use-underline">true</property>
+                        <property name="visible">true</property>
+                        <property name="sensitive">false</property>
+                        <style>
+                          <class name="destructive-action"/>
+                        </style>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="name">sections</property>
+          </packing>
+        </child>
+        <child>
+          <object class="IdeCloneSurface" id="clone_surface">
+            <property name="title" translatable="yes">Clone Project</property>
+            <property name="visible">true</property>
+          </object>
+          <packing>
+            <property name="name">clone</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/libide/greeter/libide-greeter.gresource.xml b/src/libide/greeter/libide-greeter.gresource.xml
new file mode 100644
index 000000000..002f2abb2
--- /dev/null
+++ b/src/libide/greeter/libide-greeter.gresource.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/org/gnome/builder/ui">
+    <file preprocess="xml-stripblanks">ide-clone-surface.ui</file>
+    <file preprocess="xml-stripblanks">ide-greeter-workspace.ui</file>
+  </gresource>
+</gresources>
diff --git a/src/libide/greeter/libide-greeter.h b/src/libide/greeter/libide-greeter.h
new file mode 100644
index 000000000..bce96a180
--- /dev/null
+++ b/src/libide/greeter/libide-greeter.h
@@ -0,0 +1,34 @@
+/* ide-greeter.h
+ *
+ * Copyright 2018-2019 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-core.h>
+#include <libide-gui.h>
+#include <libide-projects.h>
+#include <libide-threading.h>
+
+#define IDE_GREETER_INSIDE
+
+#include "ide-clone-surface.h"
+#include "ide-greeter-section.h"
+#include "ide-greeter-workspace.h"
+
+#undef IDE_GREETER_INSIDE
diff --git a/src/libide/greeter/meson.build b/src/libide/greeter/meson.build
index d74c836fd..68e932c0d 100644
--- a/src/libide/greeter/meson.build
+++ b/src/libide/greeter/meson.build
@@ -1,18 +1,88 @@
-greeter_headers = [
+libide_greeter_header_subdir = join_paths(libide_header_subdir, 'greeter')
+libide_include_directories += include_directories('.')
+
+libide_greeter_generated_headers = []
+
+#
+# Public API Headers
+#
+
+libide_greeter_public_headers = [
+  'ide-clone-surface.h',
   'ide-greeter-section.h',
+  'ide-greeter-workspace.h',
+  'libide-greeter.h',
+]
+
+libide_greeter_private_headers = [
+  'ide-greeter-private.h',
 ]
 
-greeter_sources = [
+install_headers(libide_greeter_public_headers, subdir: libide_greeter_header_subdir)
+
+#
+# Sources
+#
+
+libide_greeter_public_sources = [
+  'ide-clone-surface.c',
   'ide-greeter-section.c',
+  'ide-greeter-workspace.c',
+]
+
+libide_greeter_private_sources = [
+  'ide-greeter-workspace-actions.c',
+  'ide-greeter-workspace-shortcuts.c',
 ]
 
-greeter_private_sources = [
-  'ide-greeter-perspective.c',
-  'ide-greeter-perspective.h',
+#
+# Generated Resource Files
+#
+
+libide_greeter_resources = gnome.compile_resources(
+  'ide-greeter-resources',
+  'libide-greeter.gresource.xml',
+  c_name: 'ide_greeter',
+)
+libide_greeter_generated_headers += [libide_greeter_resources[1]]
+libide_greeter_private_sources += libide_greeter_resources[0]
+
+
+#
+# Dependencies
+#
+
+libide_greeter_deps = [
+  libgio_dep,
+  libgtk_dep,
+  libdazzle_dep,
+
+  libide_core_dep,
+  libide_gui_dep,
+  libide_io_dep,
+  libide_threading_dep,
+  libide_vcs_dep,
 ]
 
-libide_public_headers += files(greeter_headers)
-libide_public_sources += files(greeter_sources)
-libide_private_sources += files(greeter_private_sources)
+#
+# Library Definitions
+#
+
+libide_greeter = static_library('ide-greeter-' + libide_api_version,
+   libide_greeter_public_sources + libide_greeter_private_sources,
+   dependencies: libide_greeter_deps,
+         c_args: libide_args + release_args + ['-DIDE_GREETER_COMPILATION'],
+)
+
+libide_greeter_dep = declare_dependency(
+              sources: libide_greeter_private_headers + libide_greeter_generated_headers,
+         dependencies: libide_greeter_deps,
+           link_whole: libide_greeter,
+  include_directories: include_directories('.'),
+)
 
-install_headers(greeter_headers, subdir: join_paths(libide_header_subdir, 'greeter'))
+gnome_builder_public_sources += files(libide_greeter_public_sources)
+gnome_builder_public_headers += files(libide_greeter_public_headers)
+gnome_builder_generated_headers += libide_greeter_generated_headers
+gnome_builder_include_subdirs += libide_greeter_header_subdir
+gnome_builder_gir_extra_args += ['--c-include=libide-greeter.h', '-DIDE_GREETER_COMPILATION']
diff --git a/src/plugins/greeter/gbp-greeter-application-addin.c 
b/src/plugins/greeter/gbp-greeter-application-addin.c
new file mode 100644
index 000000000..7fd0a2d93
--- /dev/null
+++ b/src/plugins/greeter/gbp-greeter-application-addin.c
@@ -0,0 +1,229 @@
+/* gbp-greeter-application-addin.c
+ *
+ * Copyright 2018-2019 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-greeter-application-addin"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-greeter.h>
+#include <libide-gui.h>
+
+#include "gbp-greeter-application-addin.h"
+
+struct _GbpGreeterApplicationAddin
+{
+  GObject         parent_instance;
+  IdeApplication *application;
+};
+
+static void
+present_greeter_with_surface (GSimpleAction *action,
+                              GVariant      *param,
+                              gpointer       user_data)
+{
+  GbpGreeterApplicationAddin *self = user_data;
+  g_autoptr(IdeWorkbench) workbench = NULL;
+  IdeGreeterWorkspace *workspace;
+  const gchar *name;
+
+  g_assert (!action || G_IS_SIMPLE_ACTION (action));
+  g_assert (!param || g_variant_is_of_type (param, G_VARIANT_TYPE_STRING));
+  g_assert (GBP_IS_GREETER_APPLICATION_ADDIN (self));
+  g_assert (IDE_IS_APPLICATION (self->application));
+
+  workbench = ide_workbench_new ();
+  ide_application_add_workbench (self->application, workbench);
+
+  workspace = ide_greeter_workspace_new (self->application);
+  ide_workbench_add_workspace (workbench, IDE_WORKSPACE (workspace));
+
+  if (param != NULL && (name = g_variant_get_string (param, NULL)))
+    ide_workspace_set_visible_surface_name (IDE_WORKSPACE (workspace), name);
+
+  ide_workbench_focus_workspace (workbench, IDE_WORKSPACE (workspace));
+}
+
+static void
+open_project (GSimpleAction *action,
+              GVariant      *param,
+              gpointer       user_data)
+{
+  GbpGreeterApplicationAddin *self = user_data;
+  g_autoptr(IdeWorkbench) workbench = NULL;
+  IdeGreeterWorkspace *workspace;
+
+  g_assert (!action || G_IS_SIMPLE_ACTION (action));
+  g_assert (!param || g_variant_is_of_type (param, G_VARIANT_TYPE_STRING));
+  g_assert (GBP_IS_GREETER_APPLICATION_ADDIN (self));
+  g_assert (IDE_IS_APPLICATION (self->application));
+
+  workbench = ide_workbench_new ();
+  ide_application_add_workbench (self->application, workbench);
+
+  workspace = ide_greeter_workspace_new (self->application);
+  ide_workbench_add_workspace (workbench, IDE_WORKSPACE (workspace));
+
+  ide_workbench_focus_workspace (workbench, IDE_WORKSPACE (workspace));
+
+  dzl_gtk_widget_action (GTK_WIDGET (workspace), "win", "open", NULL);
+}
+
+static const GActionEntry actions[] = {
+  { "present-greeter-with-surface", present_greeter_with_surface, "s" },
+  { "open-project", open_project },
+};
+
+static void
+gbp_greeter_application_addin_add_option_entries (IdeApplicationAddin *addin,
+                                                  IdeApplication      *app)
+{
+  g_assert (IDE_IS_APPLICATION_ADDIN (addin));
+  g_assert (G_IS_APPLICATION (app));
+
+  g_application_add_main_option (G_APPLICATION (app),
+                                 "greeter",
+                                 'g',
+                                 G_OPTION_FLAG_IN_MAIN,
+                                 G_OPTION_ARG_NONE,
+                                 _("Display a new greeter window"),
+                                 NULL);
+
+  g_application_add_main_option (G_APPLICATION (app),
+                                 "clone",
+                                 0,
+                                 G_OPTION_FLAG_IN_MAIN,
+                                 G_OPTION_ARG_STRING,
+                                 _("Begin cloning project from URI"),
+                                 "URI");
+}
+
+static void
+gbp_greeter_application_addin_handle_command_line (IdeApplicationAddin     *addin,
+                                                   IdeApplication          *application,
+                                                   GApplicationCommandLine *cmdline)
+{
+  GbpGreeterApplicationAddin *self = (GbpGreeterApplicationAddin *)addin;
+  g_auto(GStrv) argv = NULL;
+  GVariantDict *dict;
+  const gchar *clone_uri = NULL;
+  gint argc;
+
+  g_assert (GBP_IS_GREETER_APPLICATION_ADDIN (self));
+  g_assert (IDE_IS_APPLICATION (application));
+  g_assert (G_IS_APPLICATION_COMMAND_LINE (cmdline));
+
+  dict = g_application_command_line_get_options_dict (cmdline);
+  argv = ide_application_get_argv (IDE_APPLICATION (application), cmdline);
+  argc = g_strv_length (argv);
+
+  /*
+   * If we are processing the arguments for the startup of the primary
+   * instance, then we want to show the greeter if no arguments are
+   * provided. (That means argc == 1, the programe executable).
+   *
+   * Also, if they provided --greeter or -g we'll show a new greeter.
+   */
+  if ((!g_application_command_line_get_is_remote (cmdline) && argc == 1) ||
+      g_variant_dict_contains (dict, "greeter"))
+    {
+      present_greeter_with_surface (NULL, NULL, addin);
+      return;
+    }
+
+  /*
+   * If the --clone=URI option was provided, switch the greeter to the
+   * clone surface and begin cloning.
+   */
+  if (dict != NULL && g_variant_dict_lookup (dict, "clone", "&s", &clone_uri))
+    {
+      IdeGreeterWorkspace *workspace;
+      IdeWorkbench *workbench;
+      IdeSurface *surface;
+
+      workbench = ide_workbench_new ();
+      ide_application_add_workbench (self->application, workbench);
+
+      workspace = ide_greeter_workspace_new (self->application);
+      ide_workbench_add_workspace (workbench, IDE_WORKSPACE (workspace));
+
+      surface = ide_workspace_get_surface_by_name (IDE_WORKSPACE (workspace), "clone");
+      ide_workspace_set_visible_surface (IDE_WORKSPACE (workspace), surface);
+
+      if (IDE_IS_CLONE_SURFACE (surface))
+        ide_clone_surface_set_uri (IDE_CLONE_SURFACE (surface), clone_uri);
+
+      ide_workbench_focus_workspace (workbench, IDE_WORKSPACE (workspace));
+    }
+}
+
+static void
+gbp_greeter_application_addin_load (IdeApplicationAddin *addin,
+                                    IdeApplication      *application)
+{
+  GbpGreeterApplicationAddin *self = (GbpGreeterApplicationAddin *)addin;
+
+  g_assert (GBP_IS_GREETER_APPLICATION_ADDIN (self));
+  g_assert (IDE_IS_APPLICATION (application));
+
+  self->application = application;
+
+  g_action_map_add_action_entries (G_ACTION_MAP (application),
+                                   actions,
+                                   G_N_ELEMENTS (actions),
+                                   self);
+}
+
+static void
+gbp_greeter_application_addin_unload (IdeApplicationAddin *addin,
+                                      IdeApplication      *application)
+{
+  GbpGreeterApplicationAddin *self = (GbpGreeterApplicationAddin *)addin;
+
+  g_assert (GBP_IS_GREETER_APPLICATION_ADDIN (self));
+  g_assert (IDE_IS_APPLICATION (application));
+
+  for (guint i = 0; i < G_N_ELEMENTS (actions); i++)
+    g_action_map_remove_action (G_ACTION_MAP (application), actions[i].name);
+
+  self->application = NULL;
+}
+
+static void
+application_addin_iface_init (IdeApplicationAddinInterface *iface)
+{
+  iface->load = gbp_greeter_application_addin_load;
+  iface->unload = gbp_greeter_application_addin_unload;
+  iface->add_option_entries = gbp_greeter_application_addin_add_option_entries;
+  iface->handle_command_line = gbp_greeter_application_addin_handle_command_line;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpGreeterApplicationAddin, gbp_greeter_application_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_APPLICATION_ADDIN, application_addin_iface_init))
+
+static void
+gbp_greeter_application_addin_class_init (GbpGreeterApplicationAddinClass *klass)
+{
+}
+
+static void
+gbp_greeter_application_addin_init (GbpGreeterApplicationAddin *self)
+{
+}
diff --git a/src/plugins/greeter/gbp-greeter-application-addin.h 
b/src/plugins/greeter/gbp-greeter-application-addin.h
new file mode 100644
index 000000000..eb1f26b86
--- /dev/null
+++ b/src/plugins/greeter/gbp-greeter-application-addin.h
@@ -0,0 +1,31 @@
+/* gbp-greeter-application-addin.h
+ *
+ * Copyright 2018-2019 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-core.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_GREETER_APPLICATION_ADDIN (gbp_greeter_application_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpGreeterApplicationAddin, gbp_greeter_application_addin, GBP, 
GREETER_APPLICATION_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/greeter/greeter-plugin.c b/src/plugins/greeter/greeter-plugin.c
new file mode 100644
index 000000000..5d9c55d57
--- /dev/null
+++ b/src/plugins/greeter/greeter-plugin.c
@@ -0,0 +1,36 @@
+/* greeter-plugin.c
+ *
+ * Copyright 2018-2019 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 "greeter-plugin"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libide-greeter.h>
+
+#include "gbp-greeter-application-addin.h"
+
+_IDE_EXTERN void
+_gbp_greeter_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_APPLICATION_ADDIN,
+                                              GBP_TYPE_GREETER_APPLICATION_ADDIN);
+}
diff --git a/src/plugins/greeter/greeter.gresource.xml b/src/plugins/greeter/greeter.gresource.xml
new file mode 100644
index 000000000..13594ee06
--- /dev/null
+++ b/src/plugins/greeter/greeter.gresource.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/plugins/greeter">
+    <file preprocess="xml-stripblanks">gtk/menus.ui</file>
+    <file>greeter.plugin</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/greeter/greeter.plugin b/src/plugins/greeter/greeter.plugin
new file mode 100644
index 000000000..c0114abc4
--- /dev/null
+++ b/src/plugins/greeter/greeter.plugin
@@ -0,0 +1,13 @@
+[Plugin]
+Authors=Christian Hergert <christian hergert me>
+Builtin=true
+Copyright=Copyright © 2014-2018 Christian Hergert
+Description=Builder's greeter window
+Embedded=_gbp_greeter_register_types
+Hidden=true
+Module=greeter
+Name=Greeter
+X-At-Startup=true
+X-Project-File-Filter-Content-Type=inode/directory
+X-Project-File-Filter-Name=Directory
+X-Project-File-Filter-Priority=-100
diff --git a/src/plugins/greeter/gtk/menus.ui b/src/plugins/greeter/gtk/menus.ui
new file mode 100644
index 000000000..ff260345b
--- /dev/null
+++ b/src/plugins/greeter/gtk/menus.ui
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <menu id="ide-greeter-workspace-menu">
+    <section id="ide-greeter-workspace-menu-projects">
+      <item>
+        <attribute name="id">ide-greeter-workspace-menu-open</attribute>
+        <attribute name="label" translatable="yes">_Open Project</attribute>
+        <attribute name="action">win.open</attribute>
+      </item>
+      <item>
+        <attribute name="id">ide-greeter-workspace-menu-clone</attribute>
+        <attribute name="label" translatable="yes">_Clone Repository</attribute>
+        <attribute name="action">win.surface</attribute>
+        <attribute name="target" type="s">'clone'</attribute>
+      </item>
+    </section>
+    <section id="ide-greeter-workspace-menu-close">
+      <item>
+        <attribute name="id">ide-greeter-workspace-menu-close</attribute>
+        <attribute name="label" translatable="yes">Close</attribute>
+        <attribute name="action">win.close</attribute>
+      </item>
+    </section>
+    <section id="ide-greeter-workspace-menu-app">
+      <item>
+        <attribute name="id">ide-greeter-workspace-menu-preferences</attribute>
+        <attribute name="label" translatable="yes">Preferences</attribute>
+        <attribute name="action">app.preferences</attribute>
+        <attribute name="accel">&lt;primary&gt;comma</attribute>
+      </item>
+      <item>
+        <attribute name="id">ide-greeter-workspace-menu-shortcuts</attribute>
+        <attribute name="label" translatable="yes">Keyboard Shortcuts</attribute>
+        <attribute name="action">app.shortcuts</attribute>
+        <attribute name="accel">&lt;primary&gt;question</attribute>
+      </item>
+      <item>
+        <attribute name="id">ide-greeter-workspace-menu-help</attribute>
+        <attribute name="label" translatable="yes">Help</attribute>
+        <attribute name="action">app.help</attribute>
+        <attribute name="accel">F1</attribute>
+      </item>
+      <item>
+        <attribute name="id">ide-greeter-workspace-menu-about</attribute>
+        <attribute name="label" translatable="yes">About Builder</attribute>
+        <attribute name="action">app.about</attribute>
+      </item>
+    </section>
+    <section id="ide-greeter-workspace-menu-quit-section">
+      <item>
+        <attribute name="id">ide-greeter-workspace-menu-quit</attribute>
+        <attribute name="label" translatable="yes">Quit</attribute>
+        <attribute name="action">app.quit</attribute>
+      </item>
+    </section>
+    <!--
+    <section id="ide-greeter-workspace-menu-debug-section">
+      <attribute name="label" translatable="yes">Debugging</attribute>
+      <item>
+        <attribute name="id">ide-greeter-workspace-menu-stats</attribute>
+        <attribute name="label" translatable="yes">Type Statistics</attribute>
+        <attribute name="action">app.about:types</attribute>
+      </item>
+    </section>
+    -->
+  </menu>
+  <menu id="ide-primary-workspace-menu">
+    <section id="ide-primary-workspace-menu-projects-section">
+      <item>
+        <attribute name="id">ide-primary-workspace-menu-open</attribute>
+        <attribute name="label" translatable="yes">_Open Project</attribute>
+        <attribute name="action">app.open-project</attribute>
+      </item>
+      <item>
+        <attribute name="id">ide-primary-workspace-menu-clone</attribute>
+        <attribute name="label" translatable="yes">_Clone Repository</attribute>
+        <attribute name="action">app.present-greeter-with-surface</attribute>
+        <attribute name="target" type="s">'clone'</attribute>
+      </item>
+    </section>
+  </menu>
+  <menu id="ide-editor-workspace-menu">
+    <section id="ide-editor-workspace-menu-projects-section">
+      <item>
+        <attribute name="id">ide-editor-workspace-menu-open</attribute>
+        <attribute name="label" translatable="yes">_Open Project</attribute>
+        <attribute name="action">app.open-project</attribute>
+      </item>
+      <item>
+        <attribute name="id">ide-editor-workspace-menu-clone</attribute>
+        <attribute name="label" translatable="yes">_Clone Repository</attribute>
+        <attribute name="action">app.present-greeter-with-surface</attribute>
+        <attribute name="target" type="s">'clone'</attribute>
+      </item>
+    </section>
+  </menu>
+</interface>
diff --git a/src/plugins/greeter/meson.build b/src/plugins/greeter/meson.build
new file mode 100644
index 000000000..736f31520
--- /dev/null
+++ b/src/plugins/greeter/meson.build
@@ -0,0 +1,12 @@
+plugins_sources += files([
+  'greeter-plugin.c',
+  'gbp-greeter-application-addin.c',
+])
+
+plugin_greeter_resources = gnome.compile_resources(
+  'gbp-greeter-resources',
+  'greeter.gresource.xml',
+  c_name: 'gbp_greeter',
+)
+
+plugins_sources += plugin_greeter_resources[0]


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