[gnome-builder] libide: more libide, more interfaces, get rid of most of src/



commit 18bd7ef46c9e83371641663ffd1249553ef34962
Author: Christian Hergert <chergert redhat com>
Date:   Wed Nov 4 01:48:02 2015 -0800

    libide: more libide, more interfaces, get rid of most of src/

 contrib/egg/Makefile.am                            |   18 +
 contrib/egg/egg-date-time.c                        |   70 ++
 contrib/egg/egg-date-time.h                        |   30 +
 .../egg/egg-pill-box.c                             |   60 +-
 .../egg/egg-pill-box.h                             |   20 +-
 .../egg/egg-pill-box.ui                            |    4 +-
 contrib/egg/egg-scrolled-window.c                  |  280 ++++++
 .../egg/egg-scrolled-window.h                      |   24 +-
 contrib/egg/egg.gresource.xml                      |    6 +
 data/theme/Adwaita-dark.css                        |    6 +
 data/theme/Adwaita.css                             |    2 +-
 data/theme/shared.css                              |   32 +-
 data/ui/ide-editor-perspective.ui                  |   73 ++
 data/ui/ide-greeter-perspective.ui                 |  281 ++++++
 data/ui/ide-greeter-project-row.ui                 |   96 ++
 data/ui/ide-workbench-header-bar.ui                |   35 +
 data/ui/ide-workbench.ui                           |   53 +
 libide/Makefile.am                                 |   43 +-
 libide/editor/ide-editor-perspective.c             |  121 +++
 .../editor/ide-editor-perspective.h                |   21 +-
 libide/greeter/ide-greeter-perspective.c           |  699 ++++++++++++++
 .../greeter/ide-greeter-perspective.h              |   19 +-
 libide/greeter/ide-greeter-project-row.c           |  350 +++++++
 .../greeter/ide-greeter-project-row.h              |   22 +-
 libide/ide-application-actions.c                   |  317 ++++++
 .../ide-application-actions.h                      |   19 +-
 libide/ide-application-addin.c                     |   81 ++
 libide/ide-application-addin.h                     |   47 +
 libide/ide-application-credits.h                   |  554 +++++++++++
 .../ide-application-private.h                      |   37 +-
 libide/ide-application.c                           | 1002 ++++++++++++++++++++
 libide/ide-application.h                           |   57 ++
 libide/ide-css-provider.c                          |  176 ++++
 .../ide-css-provider.h                             |   17 +-
 libide/ide-internal.h                              |    2 +-
 libide/ide-keybindings.c                           |  282 ++++++
 libide/ide-keybindings.h                           |   39 +
 libide/ide-layout-manager.c                        |   72 ++
 libide/ide-layout-manager.h                        |   60 ++
 libide/ide-perspective.c                           |  263 +++++
 libide/ide-perspective.h                           |   63 ++
 libide/ide-preferences-addin.c                     |   81 ++
 libide/ide-preferences-addin.h                     |   49 +
 libide/ide-preferences.c                           |  116 +++
 libide/ide-preferences.h                           |  105 ++
 libide/ide-thread-pool.c                           |   21 +-
 libide/ide-view.c                                  |  129 +++
 libide/ide-view.h                                  |   61 ++
 libide/ide-workbench-addin.c                       |   83 ++
 libide/ide-workbench-addin.h                       |   47 +
 libide/ide-workbench-header-bar.c                  |   58 ++
 .../ide-workbench-header-bar.h                     |   24 +-
 libide/ide-workbench-private.h                     |   59 ++
 libide/ide-workbench.c                             |  396 ++++++++
 libide/ide-workbench.h                             |   69 ++
 libide/ide.c                                       |   36 -
 libide/ide.h                                       |    6 +
 libide/resources/libide.gresource.xml              |    9 +
 libide/util/ide-window-settings.c                  |  154 +++
 .../util/ide-window-settings.h                     |   17 +-
 src/Makefile.am                                    |    2 -
 src/greeter/gb-greeter-project-row.c               |    4 +-
 src/main.c                                         |    3 +-
 src/resources/gnome-builder.gresource.xml          |    1 -
 src/util/gb-glib.h                                 |    2 -
 65 files changed, 6784 insertions(+), 201 deletions(-)
---
diff --git a/contrib/egg/Makefile.am b/contrib/egg/Makefile.am
index 3c0f260..7ca0a1f 100644
--- a/contrib/egg/Makefile.am
+++ b/contrib/egg/Makefile.am
@@ -1,6 +1,7 @@
 CLEANFILES =
 DISTCLEANFILES =
 EXTRA_DIST =
+BUILT_SOURCES =
 
 pkglibdir = $(libdir)/gnome-builder
 pkglib_LTLIBRARIES = libegg-private.la
@@ -12,11 +13,17 @@ libegg_private_la_SOURCES = \
        egg-binding-group.h \
        egg-counter.c \
        egg-counter.h \
+       egg-date-time.c \
+       egg-date-time.h \
        egg-frame-source.c \
        egg-frame-source.h \
        egg-heap.c \
        egg-heap.h \
+       egg-pill-box.c \
+       egg-pill-box.h \
        egg-private.h \
+       egg-scrolled-window.c \
+       egg-scrolled-window.h \
        egg-search-bar.c \
        egg-search-bar.h \
        egg-settings-flag-action.c \
@@ -35,6 +42,17 @@ libegg_private_la_SOURCES = \
        egg-task-cache.h \
        $(NULL)
 
+nodist_libegg_private_la_SOURCES = \
+       egg-resources.c \
+       egg-resources.h \
+       $(NULL)
+
+glib_resources_c = egg-resources.c
+glib_resources_h = egg-resources.h
+glib_resources_xml = egg.gresource.xml
+glib_resources_namespace = egg
+include $(top_srcdir)/build/autotools/Makefile.am.gresources
+
 libegg_private_la_CFLAGS = \
        $(EGG_CFLAGS) \
        $(NULL)
diff --git a/contrib/egg/egg-date-time.c b/contrib/egg/egg-date-time.c
new file mode 100644
index 0000000..acf3918
--- /dev/null
+++ b/contrib/egg/egg-date-time.c
@@ -0,0 +1,70 @@
+/* egg-date-time.c
+ *
+ * Copyright (C) 2015 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file 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
+ * Lesser 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/>.
+ */
+
+#include <glib/gi18n.h>
+
+#include "egg-date-time.h"
+
+/**
+ * egg_date_time_format_for_display:
+ * @self: A #GDateTime
+ *
+ * Helper function to "humanize" a #GDateTime into a relative time
+ * relationship string.
+ *
+ * Returns: (transfer full): A newly allocated string describing the
+ *   date and time imprecisely such as "Yesterday".
+ */
+gchar *
+egg_date_time_format_for_display (GDateTime *self)
+{
+  GDateTime *now;
+  GTimeSpan diff;
+  gint years;
+
+  /*
+   * TODO:
+   *
+   * There is probably a lot more we can do here to be friendly for
+   * various locales, but this will get us started.
+   */
+
+  g_return_val_if_fail (self != NULL, NULL);
+
+  now = g_date_time_new_now_utc ();
+  diff = g_date_time_difference (now, self) / G_USEC_PER_SEC;
+
+  if (diff < 0)
+    return g_strdup ("");
+  else if (diff < (60 * 45))
+    return g_strdup (_("Just now"));
+  else if (diff < (60 * 90))
+    return g_strdup (_("An hour ago"));
+  else if (diff < (60 * 60 * 24 * 2))
+    return g_strdup (_("Yesterday"));
+  else if (diff < (60 * 60 * 24 * 7))
+    return g_date_time_format (self, "%A");
+  else if (diff < (60 * 60 * 24 * 365))
+    return g_date_time_format (self, "%B");
+  else if (diff < (60 * 60 * 24 * 365 * 1.5))
+    return g_strdup (_("About a year ago"));
+
+  years = MAX (2, diff / (60 * 60 * 24 * 365));
+
+  return g_strdup_printf (ngettext ("About %u year ago", "About %u years ago", years), years);
+}
diff --git a/contrib/egg/egg-date-time.h b/contrib/egg/egg-date-time.h
new file mode 100644
index 0000000..635f3f9
--- /dev/null
+++ b/contrib/egg/egg-date-time.h
@@ -0,0 +1,30 @@
+/* egg-date-time.h
+ *
+ * Copyright (C) 2015 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file 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
+ * Lesser 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/>.
+ */
+
+#ifndef EGG_DATE_TIME_H
+#define EGG_DATE_TIME_H
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+gchar *egg_date_time_format_for_display (GDateTime *self);
+
+G_END_DECLS
+
+#endif /* EGG_DATE_TIME_H */
diff --git a/src/greeter/gb-greeter-pill-box.c b/contrib/egg/egg-pill-box.c
similarity index 55%
rename from src/greeter/gb-greeter-pill-box.c
rename to contrib/egg/egg-pill-box.c
index fdf1e56..a40630a 100644
--- a/src/greeter/gb-greeter-pill-box.c
+++ b/contrib/egg/egg-pill-box.c
@@ -1,4 +1,4 @@
-/* gb-greeter-pill-box.c
+/* egg-pill-box.c
  *
  * Copyright (C) 2015 Christian Hergert <christian hergert me>
  *
@@ -16,20 +16,20 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#define G_LOG_DOMAIN "gb-greeter-pill-box"
+#define G_LOG_DOMAIN "egg-pill-box"
 
 #include <glib/gi18n.h>
 
-#include "gb-greeter-pill-box.h"
+#include "egg-pill-box.h"
 
-struct _GbGreeterPillBox
+struct _EggPillBox
 {
   GtkEventBox  parent_instance;
 
   GtkLabel    *label;
 };
 
-G_DEFINE_TYPE (GbGreeterPillBox, gb_greeter_pill_box, GTK_TYPE_EVENT_BOX)
+G_DEFINE_TYPE (EggPillBox, egg_pill_box, GTK_TYPE_EVENT_BOX)
 
 enum {
   PROP_0,
@@ -40,42 +40,42 @@ enum {
 static GParamSpec *properties [LAST_PROP];
 
 const gchar *
-gb_greeter_pill_box_get_label (GbGreeterPillBox *self)
+egg_pill_box_get_label (EggPillBox *self)
 {
-  g_return_val_if_fail (GB_IS_GREETER_PILL_BOX (self), NULL);
+  g_return_val_if_fail (EGG_IS_PILL_BOX (self), NULL);
 
   return gtk_label_get_label (self->label);
 }
 
 void
-gb_greeter_pill_box_set_label (GbGreeterPillBox *self,
-                               const gchar      *label)
+egg_pill_box_set_label (EggPillBox  *self,
+                        const gchar *label)
 {
-  g_return_if_fail (GB_IS_GREETER_PILL_BOX (self));
+  g_return_if_fail (EGG_IS_PILL_BOX (self));
 
   gtk_label_set_label (self->label, label);
 }
 
 GtkWidget *
-gb_greeter_pill_box_new (const gchar *label)
+egg_pill_box_new (const gchar *label)
 {
-  return g_object_new (GB_TYPE_GREETER_PILL_BOX,
+  return g_object_new (EGG_TYPE_PILL_BOX,
                        "label", label,
                        NULL);
 }
 
 static void
-gb_greeter_pill_box_get_property (GObject    *object,
-                                  guint       prop_id,
-                                  GValue     *value,
-                                  GParamSpec *pspec)
+egg_pill_box_get_property (GObject    *object,
+                           guint       prop_id,
+                           GValue     *value,
+                           GParamSpec *pspec)
 {
-  GbGreeterPillBox *self = GB_GREETER_PILL_BOX (object);
+  EggPillBox *self = EGG_PILL_BOX (object);
 
   switch (prop_id)
     {
     case PROP_LABEL:
-      g_value_set_string (value, gb_greeter_pill_box_get_label (self));
+      g_value_set_string (value, egg_pill_box_get_label (self));
       break;
 
     default:
@@ -84,17 +84,17 @@ gb_greeter_pill_box_get_property (GObject    *object,
 }
 
 static void
-gb_greeter_pill_box_set_property (GObject      *object,
-                                  guint         prop_id,
-                                  const GValue *value,
-                                  GParamSpec   *pspec)
+egg_pill_box_set_property (GObject      *object,
+                           guint         prop_id,
+                           const GValue *value,
+                           GParamSpec   *pspec)
 {
-  GbGreeterPillBox *self = GB_GREETER_PILL_BOX (object);
+  EggPillBox *self = EGG_PILL_BOX (object);
 
   switch (prop_id)
     {
     case PROP_LABEL:
-      gb_greeter_pill_box_set_label (self, g_value_get_string (value));
+      egg_pill_box_set_label (self, g_value_get_string (value));
       break;
 
     default:
@@ -103,13 +103,13 @@ gb_greeter_pill_box_set_property (GObject      *object,
 }
 
 static void
-gb_greeter_pill_box_class_init (GbGreeterPillBoxClass *klass)
+egg_pill_box_class_init (EggPillBoxClass *klass)
 {
   GObjectClass *object_class = G_OBJECT_CLASS (klass);
   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
 
-  object_class->get_property = gb_greeter_pill_box_get_property;
-  object_class->set_property = gb_greeter_pill_box_set_property;
+  object_class->get_property = egg_pill_box_get_property;
+  object_class->set_property = egg_pill_box_set_property;
 
   properties [PROP_LABEL] =
     g_param_spec_string ("label",
@@ -119,12 +119,12 @@ gb_greeter_pill_box_class_init (GbGreeterPillBoxClass *klass)
                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
   g_object_class_install_property (object_class, PROP_LABEL, properties [PROP_LABEL]);
 
-  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/builder/ui/gb-greeter-pill-box.ui");
-  gtk_widget_class_bind_template_child (widget_class, GbGreeterPillBox, label);
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/libegg-private/egg-pill-box.ui");
+  gtk_widget_class_bind_template_child (widget_class, EggPillBox, label);
 }
 
 static void
-gb_greeter_pill_box_init (GbGreeterPillBox *self)
+egg_pill_box_init (EggPillBox *self)
 {
   gtk_widget_init_template (GTK_WIDGET (self));
 }
diff --git a/src/greeter/gb-greeter-pill-box.h b/contrib/egg/egg-pill-box.h
similarity index 58%
copy from src/greeter/gb-greeter-pill-box.h
copy to contrib/egg/egg-pill-box.h
index 394a333..88faf6c 100644
--- a/src/greeter/gb-greeter-pill-box.h
+++ b/contrib/egg/egg-pill-box.h
@@ -1,4 +1,4 @@
-/* gb-greeter-pill-box.h
+/* egg-pill-box.h
  *
  * Copyright (C) 2015 Christian Hergert <christian hergert me>
  *
@@ -16,22 +16,22 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#ifndef GB_GREETER_PILL_BOX_H
-#define GB_GREETER_PILL_BOX_H
+#ifndef EGG_PILL_BOX_H
+#define EGG_PILL_BOX_H
 
 #include <gtk/gtk.h>
 
 G_BEGIN_DECLS
 
-#define GB_TYPE_GREETER_PILL_BOX (gb_greeter_pill_box_get_type())
+#define EGG_TYPE_PILL_BOX (egg_pill_box_get_type())
 
-G_DECLARE_FINAL_TYPE (GbGreeterPillBox, gb_greeter_pill_box, GB, GREETER_PILL_BOX, GtkEventBox)
+G_DECLARE_FINAL_TYPE (EggPillBox, egg_pill_box, EGG, PILL_BOX, GtkEventBox)
 
-GtkWidget   *gb_greeter_pill_box_new       (const gchar      *label);
-const gchar *gb_greeter_pill_box_get_label (GbGreeterPillBox *self);
-void         gb_greeter_pill_box_set_label (GbGreeterPillBox *self,
-                                            const gchar      *label);
+GtkWidget   *egg_pill_box_new       (const gchar *label);
+const gchar *egg_pill_box_get_label (EggPillBox  *self);
+void         egg_pill_box_set_label (EggPillBox  *self,
+                                     const gchar *label);
 
 G_END_DECLS
 
-#endif /* GB_GREETER_PILL_BOX_H */
+#endif /* EGG_PILL_BOX_H */
diff --git a/data/ui/gb-greeter-pill-box.ui b/contrib/egg/egg-pill-box.ui
similarity index 91%
rename from data/ui/gb-greeter-pill-box.ui
rename to contrib/egg/egg-pill-box.ui
index 069e75f..1fb94f7 100644
--- a/data/ui/gb-greeter-pill-box.ui
+++ b/contrib/egg/egg-pill-box.ui
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
-  <!-- interface-requires gtk+ 3.15 -->
-  <template class="GbGreeterPillBox" parent="GtkEventBox">
+  <!-- interface-requires gtk+ 3.18 -->
+  <template class="EggPillBox" parent="GtkEventBox">
     <style>
       <class name="pill-box"/>
     </style>
diff --git a/contrib/egg/egg-scrolled-window.c b/contrib/egg/egg-scrolled-window.c
new file mode 100644
index 0000000..5d1ed66
--- /dev/null
+++ b/contrib/egg/egg-scrolled-window.c
@@ -0,0 +1,280 @@
+/* egg-scrolled-window.c
+ *
+ * Copyright (C) 2014 Christian Hergert <christian hergert me>
+ *
+ * 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/>.
+ */
+
+#include <glib/gi18n.h>
+
+#include "egg-scrolled-window.h"
+
+typedef struct
+{
+  gint max_content_height;
+  gint max_content_width;
+} EggScrolledWindowPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (EggScrolledWindow, egg_scrolled_window, GTK_TYPE_SCROLLED_WINDOW)
+
+enum {
+  PROP_0,
+  PROP_MAX_CONTENT_HEIGHT,
+  PROP_MAX_CONTENT_WIDTH,
+  LAST_PROP
+};
+
+static GParamSpec *properties [LAST_PROP];
+
+GtkWidget *
+egg_scrolled_window_new (void)
+{
+  return g_object_new (EGG_TYPE_SCROLLED_WINDOW, NULL);
+}
+
+gint
+egg_scrolled_window_get_max_content_height (EggScrolledWindow *self)
+{
+  EggScrolledWindowPrivate *priv = egg_scrolled_window_get_instance_private (self);
+
+  g_return_val_if_fail (EGG_IS_SCROLLED_WINDOW (self), -1);
+
+  return priv->max_content_height;
+}
+
+/**
+ * egg_scrolled_window_set_max_content_height:
+ * @max_content_height: the max allowed height request or -1 to ignore.
+ *
+ * This function will set the "max-content-height" property. This property is
+ * used to determine the maximum height that the scrolled window will request.
+ *
+ * This is useful if you want to have a scrolled window grow with the child
+ * allocation, but only up to a certain height.
+ */
+void
+egg_scrolled_window_set_max_content_height (EggScrolledWindow *self,
+                                            gint               max_content_height)
+{
+  EggScrolledWindowPrivate *priv = egg_scrolled_window_get_instance_private (self);
+
+  g_return_if_fail (EGG_IS_SCROLLED_WINDOW (self));
+
+  if (max_content_height != priv->max_content_height)
+    {
+      priv->max_content_height = max_content_height;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MAX_CONTENT_HEIGHT]);
+      gtk_widget_queue_resize (GTK_WIDGET (self));
+    }
+}
+
+gint
+egg_scrolled_window_get_max_content_width (EggScrolledWindow *self)
+{
+  EggScrolledWindowPrivate *priv = egg_scrolled_window_get_instance_private (self);
+
+  g_return_val_if_fail (EGG_IS_SCROLLED_WINDOW (self), -1);
+
+  return priv->max_content_width;
+}
+
+/**
+ * egg_scrolled_window_set_max_content_width:
+ * @max_content_width: the max allowed width request or -1 to ignore.
+ *
+ * This function will set the "max-content-width" property. This property is
+ * used to determine the maximum width that the scrolled window will request.
+ *
+ * This is useful if you want to have a scrolled window grow with the child
+ * allocation, but only up to a certain width.
+ */
+void
+egg_scrolled_window_set_max_content_width (EggScrolledWindow *self,
+                                           gint               max_content_width)
+{
+  EggScrolledWindowPrivate *priv = egg_scrolled_window_get_instance_private (self);
+
+  g_return_if_fail (EGG_IS_SCROLLED_WINDOW (self));
+
+  if (max_content_width != priv->max_content_width)
+    {
+      priv->max_content_width = max_content_width;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MAX_CONTENT_HEIGHT]);
+      gtk_widget_queue_resize (GTK_WIDGET (self));
+    }
+}
+
+static void
+egg_scrolled_window_get_preferred_height (GtkWidget *widget,
+                                          gint      *minimum_height,
+                                          gint      *natural_height)
+{
+  EggScrolledWindow *self = (EggScrolledWindow *)widget;
+  EggScrolledWindowPrivate *priv = egg_scrolled_window_get_instance_private (self);
+
+  g_return_if_fail (EGG_IS_SCROLLED_WINDOW (self));
+
+  GTK_WIDGET_CLASS (egg_scrolled_window_parent_class)->get_preferred_height (widget, minimum_height, 
natural_height);
+
+  if (natural_height)
+    {
+      if (priv->max_content_height > -1)
+        {
+          GtkWidget *child;
+          GtkStyleContext *style;
+          GtkBorder border;
+          gint child_min_height;
+          gint child_nat_height;
+          gint additional;
+
+          if (!(child = gtk_bin_get_child (GTK_BIN (widget))))
+            return;
+
+          style = gtk_widget_get_style_context (widget);
+          gtk_style_context_get_border (style, gtk_widget_get_state_flags (widget), &border);
+          additional = border.top + border.bottom;
+
+          gtk_widget_get_preferred_height (child, &child_min_height, &child_nat_height);
+
+          if ((child_nat_height > *natural_height) && (priv->max_content_height > *natural_height))
+            *natural_height = MIN (priv->max_content_height, child_nat_height) + additional;
+        }
+    }
+}
+
+static void
+egg_scrolled_window_get_preferred_width (GtkWidget *widget,
+                                         gint      *minimum_width,
+                                         gint      *natural_width)
+{
+  EggScrolledWindow *self = (EggScrolledWindow *)widget;
+  EggScrolledWindowPrivate *priv = egg_scrolled_window_get_instance_private (self);
+
+  g_return_if_fail (EGG_IS_SCROLLED_WINDOW (self));
+
+  GTK_WIDGET_CLASS (egg_scrolled_window_parent_class)->get_preferred_width (widget, minimum_width, 
natural_width);
+
+  if (natural_width)
+    {
+      if (priv->max_content_width > -1)
+        {
+          GtkWidget *child;
+          GtkStyleContext *style;
+          GtkBorder border;
+          gint child_min_width;
+          gint child_nat_width;
+          gint additional;
+
+          if (!(child = gtk_bin_get_child (GTK_BIN (widget))))
+            return;
+
+          style = gtk_widget_get_style_context (widget);
+          gtk_style_context_get_border (style, gtk_widget_get_state_flags (widget), &border);
+          additional = border.left = border.right + 1;
+
+          gtk_widget_get_preferred_width (child, &child_min_width, &child_nat_width);
+
+          if ((child_nat_width > *natural_width) && (priv->max_content_width > *natural_width))
+            *natural_width = MIN (priv->max_content_width, child_nat_width) + additional;
+        }
+    }
+}
+
+static void
+egg_scrolled_window_get_property (GObject    *object,
+                                  guint       prop_id,
+                                  GValue     *value,
+                                  GParamSpec *pspec)
+{
+  EggScrolledWindow *self = EGG_SCROLLED_WINDOW (object);
+
+  switch (prop_id)
+    {
+    case PROP_MAX_CONTENT_HEIGHT:
+      g_value_set_int (value, egg_scrolled_window_get_max_content_height (self));
+      break;
+
+    case PROP_MAX_CONTENT_WIDTH:
+      g_value_set_int (value, egg_scrolled_window_get_max_content_width (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+egg_scrolled_window_set_property (GObject      *object,
+                                  guint         prop_id,
+                                  const GValue *value,
+                                  GParamSpec   *pspec)
+{
+  EggScrolledWindow *self = EGG_SCROLLED_WINDOW (object);
+
+  switch (prop_id)
+    {
+    case PROP_MAX_CONTENT_HEIGHT:
+      egg_scrolled_window_set_max_content_height (self, g_value_get_int (value));
+      break;
+
+    case PROP_MAX_CONTENT_WIDTH:
+      egg_scrolled_window_set_max_content_width (self, g_value_get_int (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+egg_scrolled_window_class_init (EggScrolledWindowClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->get_property = egg_scrolled_window_get_property;
+  object_class->set_property = egg_scrolled_window_set_property;
+
+  widget_class->get_preferred_width = egg_scrolled_window_get_preferred_width;
+  widget_class->get_preferred_height = egg_scrolled_window_get_preferred_height;
+
+  properties [PROP_MAX_CONTENT_HEIGHT] =
+    g_param_spec_int ("max-content-height",
+                      "Max Content Height",
+                      "The maximum height request that can be made.",
+                      -1,
+                      G_MAXINT,
+                      -1,
+                      (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_MAX_CONTENT_WIDTH] =
+    g_param_spec_int ("max-content-width",
+                      "Max Content Width",
+                      "The maximum width request that can be made.",
+                      -1,
+                      G_MAXINT,
+                      -1,
+                      (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+}
+
+static void
+egg_scrolled_window_init (EggScrolledWindow *self)
+{
+  EggScrolledWindowPrivate *priv = egg_scrolled_window_get_instance_private (self);
+
+  priv->max_content_height = -1;
+  priv->max_content_width = -1;
+}
diff --git a/src/greeter/gb-greeter-pill-box.h b/contrib/egg/egg-scrolled-window.h
similarity index 53%
copy from src/greeter/gb-greeter-pill-box.h
copy to contrib/egg/egg-scrolled-window.h
index 394a333..9b66dd9 100644
--- a/src/greeter/gb-greeter-pill-box.h
+++ b/contrib/egg/egg-scrolled-window.h
@@ -1,6 +1,6 @@
-/* gb-greeter-pill-box.h
+/* egg-scrolled-window.h
  *
- * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ * Copyright (C) 2014 Christian Hergert <christian hergert me>
  *
  * 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
@@ -16,22 +16,24 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#ifndef GB_GREETER_PILL_BOX_H
-#define GB_GREETER_PILL_BOX_H
+#ifndef EGG_SCROLLED_WINDOW_H
+#define EGG_SCROLLED_WINDOW_H
 
 #include <gtk/gtk.h>
 
 G_BEGIN_DECLS
 
-#define GB_TYPE_GREETER_PILL_BOX (gb_greeter_pill_box_get_type())
+#define EGG_TYPE_SCROLLED_WINDOW (egg_scrolled_window_get_type())
 
-G_DECLARE_FINAL_TYPE (GbGreeterPillBox, gb_greeter_pill_box, GB, GREETER_PILL_BOX, GtkEventBox)
+G_DECLARE_DERIVABLE_TYPE (EggScrolledWindow, egg_scrolled_window, EGG, SCROLLED_WINDOW, GtkScrolledWindow)
 
-GtkWidget   *gb_greeter_pill_box_new       (const gchar      *label);
-const gchar *gb_greeter_pill_box_get_label (GbGreeterPillBox *self);
-void         gb_greeter_pill_box_set_label (GbGreeterPillBox *self,
-                                            const gchar      *label);
+struct _EggScrolledWindowClass
+{
+  GtkScrolledWindowClass parent_class;
+};
+
+GtkWidget *egg_scrolled_window_new (void);
 
 G_END_DECLS
 
-#endif /* GB_GREETER_PILL_BOX_H */
+#endif /* EGG_SCROLLED_WINDOW_H */
diff --git a/contrib/egg/egg.gresource.xml b/contrib/egg/egg.gresource.xml
new file mode 100644
index 0000000..41879df
--- /dev/null
+++ b/contrib/egg/egg.gresource.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/org/gnome/libegg-private">
+    <file>egg-pill-box.ui</file>
+  </gresource>
+</gresources>
diff --git a/data/theme/Adwaita-dark.css b/data/theme/Adwaita-dark.css
index 7ae52c2..35d7e2b 100644
--- a/data/theme/Adwaita-dark.css
+++ b/data/theme/Adwaita-dark.css
@@ -15,6 +15,12 @@ treeview.view.cell:selected {
 }
 
 
+EggPillBox {
+  background-color: #1e1e1e;
+  border-radius: 3px;
+}
+
+
 RgGraph {
   background-color: #2e3436;
   background-size: 8px 8px;
diff --git a/data/theme/Adwaita.css b/data/theme/Adwaita.css
index 5be481f..32ef4f4 100644
--- a/data/theme/Adwaita.css
+++ b/data/theme/Adwaita.css
@@ -15,7 +15,7 @@ GbWorkbench treeview.view.cell:selected {
 }
 
 
-GbGreeterPillBox {
+EggPillBox {
   background-color: #eeeeec;
   border-radius: 3px;
 }
diff --git a/data/theme/shared.css b/data/theme/shared.css
index a17facf..e5dd4f8 100644
--- a/data/theme/shared.css
+++ b/data/theme/shared.css
@@ -110,20 +110,20 @@ GbSearchDisplayGroup GtkListBox .list-row {
 }
 
 
-GbGreeterProjectRow,
+IdeGreeterProjectRow,
 GtkListBoxRow.with-header {
   border-bottom: 1px solid alpha(@borders, 0.4);
 }
 
-GbGreeterWindow .frame {
+IdeGreeterPerspective .frame {
   border-top: 1px solid alpha(@borders, 0.4);
   border-left: 1px solid alpha(@borders, 0.4);
   border-right: 1px solid alpha(@borders, 0.4);
   border-bottom: none;
 }
 /* remove viewport frame */
-GbGreeterWindow .frame > GbScrolledWindow > .frame,
-GbGreeterWindow > GtkBox > GtkScrolledWindow > .frame {
+IdeGreeterPerspective .frame > EggScrolledWindow > .frame,
+IdeGreeterPerspective > GtkBox > EggScrolledWindow > .frame {
   border: none;
 }
 
@@ -164,3 +164,27 @@ GbWorkspacePane .pane-separator {
 GbEditorFrame textview {
   padding-bottom: 250px;
 }
+
+
+/* Workbench perspective selection bar */
+/*
+IdeWorkbench > GtkStack > GtkBox.horizontal > GtkStackSwitcher > button {
+  background-image: none;
+  background-color: transparent;
+  border: none;
+  padding: 10px;
+  box-shadow: none;
+  border-radius: 0px;
+}
+IdeWorkbench > GtkStack > GtkBox.horizontal > GtkStackSwitcher > button:active:hover,
+IdeWorkbench > GtkStack > GtkBox.horizontal > GtkStackSwitcher > button:active:checked,
+IdeWorkbench > GtkStack > GtkBox.horizontal > GtkStackSwitcher > button:active {
+  background-color: mix(@theme_bg_color, @borders, 0.75);
+}
+IdeWorkbench > GtkStack > GtkBox.horizontal > GtkStackSwitcher > button:checked {
+  background-color: mix(@theme_bg_color, @borders, 0.5);
+}
+IdeWorkbench > GtkStack > GtkBox.horizontal > GtkStackSwitcher > button:hover {
+  background-color: mix(@theme_bg_color, @borders, 0.25);
+}
+*/
diff --git a/data/ui/ide-editor-perspective.ui b/data/ui/ide-editor-perspective.ui
new file mode 100644
index 0000000..2a58133
--- /dev/null
+++ b/data/ui/ide-editor-perspective.ui
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.18 -->
+  <template class="IdeEditorPerspective" parent="GtkBin">
+    <child>
+      <object class="GtkLabel">
+        <property name="visible">true</property>
+        <property name="label">Editor Perspective</property>
+      </object>
+    </child>
+  </template>
+  <object class="IdeWorkbenchHeaderBar" id="titlebar">
+    <property name="visible">true</property>
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">horizontal</property>
+        <property name="visible">true</property>
+        <style>
+          <class name="linked"/>
+        </style>
+        <child>
+          <object class="GtkToggleButton">
+            <property name="focus-on-click">false</property>
+            <property name="visible">true</property>
+            <style>
+              <class name="image-button"/>
+            </style>
+            <child>
+              <object class="GtkImage">
+                <property name="icon-name">builder-view-left-pane-symbolic</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkToggleButton">
+            <property name="focus-on-click">false</property>
+            <property name="visible">true</property>
+            <style>
+              <class name="image-button"/>
+            </style>
+            <child>
+              <object class="GtkImage">
+                <property name="icon-name">builder-view-bottom-pane-symbolic</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkToggleButton">
+            <property name="focus-on-click">false</property>
+            <property name="visible">true</property>
+            <style>
+              <class name="image-button"/>
+            </style>
+            <child>
+              <object class="GtkImage">
+                <property name="icon-name">builder-view-right-pane-symbolic</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="position">1</property>
+        <property name="pack-type">end</property>
+      </packing>
+    </child>
+  </object>
+</interface>
diff --git a/data/ui/ide-greeter-perspective.ui b/data/ui/ide-greeter-perspective.ui
new file mode 100644
index 0000000..9e6da6c
--- /dev/null
+++ b/data/ui/ide-greeter-perspective.ui
@@ -0,0 +1,281 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.18 -->
+  <template class="IdeGreeterPerspective" parent="GtkBin">
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">vertical</property>
+        <property name="expand">true</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="EggSearchBar">
+            <property name="visible">true</property>
+            <property name="vexpand">false</property>
+            <property name="search-mode-enabled" bind-source="search_button" bind-property="active" 
bind-flags="bidirectional"/>
+            <child internal-child="entry">
+              <object class="GtkSearchEntry" id="search_entry">
+                <property name="placeholder-text"></property>
+                <property name="width-chars">45</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkScrolledWindow" id="scrolled_window">
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkViewport" id="viewport">
+                <property name="expand">true</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="GtkBox">
+                    <property name="halign">center</property>
+                    <property name="margin-bottom">32</property>
+                    <property name="margin-top">32</property>
+                    <property name="orientation">vertical</property>
+                    <property name="spacing">32</property>
+                    <property name="visible">true</property>
+                    <property name="width-request">600</property>
+                    <child>
+                      <object class="GtkBox" id="my_projects_container">
+                        <property name="orientation">vertical</property>
+                        <property name="spacing">6</property>
+                        <property name="visible">true</property>
+                        <child>
+                          <object class="GtkLabel" id="my_projects_label">
+                            <property name="label" translatable="yes">Recent Projects</property>
+                            <property name="visible">true</property>
+                            <property name="xalign">0.0</property>
+                            <style>
+                              <class name="dim-label"/>
+                            </style>
+                            <attributes>
+                              <attribute name="weight" value="bold"/>
+                            </attributes>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="EggScrolledWindow">
+                            <property name="min-content-height">100</property>
+                            <property name="min-content-width">475</property>
+                            <property name="max-content-width">600</property>
+                            <property name="visible">true</property>
+                            <property name="vscrollbar-policy">never</property>
+                            <child>
+                              <object class="GtkListBox" id="my_projects_list_box">
+                                <property name="visible">true</property>
+                                <property name="selection-mode">none</property>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkBox" id="other_projects_container">
+                        <property name="orientation">vertical</property>
+                        <property name="spacing">6</property>
+                        <property name="visible">true</property>
+                        <child>
+                          <object class="GtkLabel" id="other_projects_label">
+                            <property name="label" translatable="yes">Other Projects</property>
+                            <property name="visible">true</property>
+                            <property name="xalign">0.0</property>
+                            <style>
+                              <class name="dim-label"/>
+                            </style>
+                            <attributes>
+                              <attribute name="weight" value="bold"/>
+                            </attributes>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="EggScrolledWindow">
+                            <property name="min-content-height">100</property>
+                            <property name="min-content-width">300</property>
+                            <property name="max-content-width">600</property>
+                            <property name="visible">true</property>
+                            <property name="vscrollbar-policy">never</property>
+                            <child>
+                              <object class="GtkListBox" id="other_projects_list_box">
+                                <property name="visible">true</property>
+                                <property name="selection-mode">none</property>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">true</property>
+            <property name="fill">true</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkActionBar" id="action_bar">
+            <property name="visible">false</property>
+            <child>
+              <object class="GtkButton" id="remove_button">
+                <property name="action-name">perspective.delete-selected-rows</property>
+                <property name="label" translatable="yes">_Remove</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>
+  </template>
+  <object class="GtkHeaderBar" id="titlebar">
+    <property name="visible">true</property>
+    <child type="title">
+      <object class="GtkStack" id="title_stack">
+        <property name="visible">true</property>
+        <property name="transition-type">crossfade</property>
+        <child>
+          <object class="GtkLabel" id="browse_title">
+            <property name="visible">true</property>
+            <property name="label" translatable="yes">Select a Project</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel" id="selection_title">
+            <property name="visible">true</property>
+            <property name="label" translatable="yes">Click an item to select</property>
+          </object>
+        </child>
+      </object>
+    </child>
+    <child>
+      <object class="GtkButton" id="new_button">
+        <property name="action-name">app.new-project</property>
+        <property name="visible">true</property>
+        <property name="label" translatable="yes">_New</property>
+        <property name="use-underline">true</property>
+        <style>
+          <class name="text-button"/>
+        </style>
+      </object>
+      <packing>
+        <property name="position">0</property>
+        <property name="pack-type">start</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkToggleButton" id="search_button">
+        <property name="visible">true</property>
+        <style>
+          <class name="image-button"/>
+        </style>
+        <child>
+          <object class="GtkImage">
+            <property name="icon-name">edit-find-symbolic</property>
+            <property name="visible">true</property>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="position">2</property>
+        <property name="pack-type">end</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkToggleButton" id="selection_button">
+        <property name="action-name">perspective.state</property>
+        <property name="action-target">'selection'</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="position">1</property>
+        <property name="pack-type">end</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkButton" id="cancel_button">
+        <property name="label" translatable="yes">_Cancel</property>
+        <property name="use-underline">true</property>
+        <property name="action-name">perspective.state</property>
+        <property name="action-target">'browse'</property>
+        <style>
+          <class name="text-button"/>
+        </style>
+      </object>
+      <packing>
+        <property name="position">0</property>
+        <property name="pack-type">end</property>
+      </packing>
+    </child>
+  </object>
+  <object class="EggStateMachine" id="state_machine">
+    <property name="state">browse</property>
+    <states>
+      <state name="browse">
+        <object id="titlebar">
+          <property name="show-close-button">true</property>
+        </object>
+        <object id="title_stack">
+          <property name="visible-child">browse_title</property>
+        </object>
+        <object id="cancel_button">
+          <property name="visible">false</property>
+        </object>
+        <object id="selection_button">
+          <property name="visible">true</property>
+        </object>
+        <object id="new_button">
+          <property name="visible">true</property>
+        </object>
+        <object id="action_bar">
+          <property name="visible">false</property>
+        </object>
+        <object id="other_projects_container">
+          <property name="visible">true</property>
+        </object>
+      </state>
+      <state name="selection">
+        <object id="titlebar">
+          <property name="show-close-button">false</property>
+          <style>
+            <class name="selection-mode"/>
+          </style>
+        </object>
+        <object id="action_bar">
+          <property name="visible">true</property>
+        </object>
+        <object id="title_stack">
+          <property name="visible-child">selection_title</property>
+        </object>
+        <object id="cancel_button">
+          <property name="visible">true</property>
+        </object>
+        <object id="selection_button">
+          <property name="visible">false</property>
+        </object>
+        <object id="new_button">
+          <property name="visible">false</property>
+        </object>
+        <object id="other_projects_container">
+          <property name="visible">false</property>
+        </object>
+      </state>
+    </states>
+  </object>
+</interface>
diff --git a/data/ui/ide-greeter-project-row.ui b/data/ui/ide-greeter-project-row.ui
new file mode 100644
index 0000000..b177245
--- /dev/null
+++ b/data/ui/ide-greeter-project-row.ui
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.18 -->
+  <template class="IdeGreeterProjectRow" parent="GtkListBoxRow">
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">horizontal</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkCheckButton" id="checkbox">
+            <property name="margin-start">12</property>
+            <property name="valign">center</property>
+            <property name="vexpand">true</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkBox">
+            <property name="orientation">vertical</property>
+            <property name="margin">12</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkBox">
+                <property name="orientation">horizontal</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="GtkLabel" id="title_label">
+                    <property name="visible">true</property>
+                    <property name="hexpand">true</property>
+                    <property name="valign">baseline</property>
+                    <property name="xalign">0.0</property>
+                    <attributes>
+                      <attribute name="scale" value="1.2"/>
+                      <attribute name="weight" value="bold"/>
+                    </attributes>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkLabel" id="date_label">
+                    <property name="visible">true</property>
+                    <property name="hexpand">true</property>
+                    <property name="valign">baseline</property>
+                    <property name="xalign">1.0</property>
+                    <attributes>
+                      <attribute name="scale" value="0.833333"/>
+                    </attributes>
+                    <style>
+                      <class name="dim-label"/>
+                    </style>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel" id="description_label">
+                <property name="single-line-mode">true</property>
+                <property name="ellipsize">end</property>
+                <property name="valign">baseline</property>
+                <property name="visible">true</property>
+                <property name="xalign">0.0</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkBox">
+                <property name="orientation">horizontal</property>
+                <property name="visible">true</property>
+                <property name="spacing">6</property>
+                <child>
+                  <object class="GtkLabel" id="location_label">
+                    <property name="hexpand">true</property>
+                    <property name="visible">true</property>
+                    <property name="valign">baseline</property>
+                    <property name="xalign">0.0</property>
+                    <property name="ellipsize">middle</property>
+                    <style>
+                      <class name="dim-label"/>
+                    </style>
+                    <attributes>
+                      <attribute name="scale" value="0.833333"/>
+                    </attributes>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkBox" id="languages_box">
+                    <property name="spacing">3</property>
+                    <property name="orientation">horizontal</property>
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/data/ui/ide-workbench-header-bar.ui b/data/ui/ide-workbench-header-bar.ui
new file mode 100644
index 0000000..39e92b6
--- /dev/null
+++ b/data/ui/ide-workbench-header-bar.ui
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.18 -->
+  <template class="IdeWorkbenchHeaderBar" parent="GtkHeaderBar">
+    <property name="show-close-button">true</property>
+    <child type="title">
+      <object class="GtkSearchEntry" id="search_entry">
+        <property name="hexpand">true</property>
+        <property name="margin-end">6</property>
+        <property name="margin-start">6</property>
+        <property name="max-width-chars">50</property>
+        <property name="visible">true</property>
+      </object>
+    </child>
+    <child>
+      <object class="GtkMenuButton" id="menu_button">
+        <property name="focus-on-click">false</property>
+        <property name="visible">true</property>
+        <style>
+          <class name="image-button"/>
+        </style>
+        <child>
+          <object class="GtkImage">
+            <property name="icon-name">open-menu-symbolic</property>
+            <property name="visible">true</property>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="pack-type">end</property>
+        <property name="position">0</property>
+      </packing>
+    </child>
+  </template>
+</interface>
diff --git a/data/ui/ide-workbench.ui b/data/ui/ide-workbench.ui
new file mode 100644
index 0000000..185bfb2
--- /dev/null
+++ b/data/ui/ide-workbench.ui
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.17 -->
+  <template class="IdeWorkbench" parent="GtkApplicationWindow">
+    <child type="titlebar">
+      <object class="GtkStack" id="titlebar_stack">
+        <property name="visible">true</property>
+        <property name="transition-type">slide-left-right</property>
+        <property name="transition-duration">333</property>
+      </object>
+    </child>
+    <child>
+      <!--
+        top_stack is used so that we can have a perspective,
+        (the greeter perspective) as the initial perspective without
+        a stack switcher. Once we animate in the real perspectives, we
+        want the switcher. also, we want to animate in the switcher
+        as part of that transition, otherwise it looks clunky.
+      -->
+      <object class="GtkStack" id="top_stack">
+        <property name="visible">true</property>
+        <property name="transition-duration">333</property>
+        <property name="transition-type">slide-left-right</property>
+        <child>
+          <object class="GtkBox">
+            <property name="expand">true</property>
+            <property name="orientation">horizontal</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkStackSwitcher" id="perspectives_stack_switcher">
+                <property name="margin">6</property>
+                <property name="orientation">vertical</property>
+                <property name="stack">perspectives_stack</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkStack" id="perspectives_stack">
+                <property name="homogeneous">false</property>
+                <property name="transition-type">crossfade</property>
+                <property name="transition-duration">333</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="name">perspectives</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/libide/Makefile.am b/libide/Makefile.am
index a3d8054..eb27bbe 100644
--- a/libide/Makefile.am
+++ b/libide/Makefile.am
@@ -13,10 +13,16 @@ libide_1_0_la_public_sources = \
        doap/ide-doap-person.h \
        doap/ide-doap.c \
        doap/ide-doap.h \
+       editor/ide-editor-perspective.c \
+       editor/ide-editor-perspective.h \
        git/ide-git-remote-callbacks.c \
        git/ide-git-remote-callbacks.h \
        git/ide-git-vcs.c \
        git/ide-git-vcs.h \
+       ide-application.c \
+       ide-application.h \
+       ide-application-addin.c \
+       ide-application-addin.h \
        ide-back-forward-item.c \
        ide-back-forward-item.h \
        ide-back-forward-list.c \
@@ -86,9 +92,17 @@ libide_1_0_la_public_sources = \
        ide-indent-style.h \
        ide-indenter.c \
        ide-indenter.h \
+       ide-layout-manager.c \
+       ide-layout-manager.h \
        ide-log.c \
        ide-log.h \
        ide-macros.h \
+       ide-perspective.c \
+       ide-perspective.h \
+       ide-preferences.c \
+       ide-preferences.h \
+       ide-preferences-addin.c \
+       ide-preferences-addin.h \
        ide-object.c \
        ide-object.h \
        ide-pattern-spec.c \
@@ -172,10 +186,18 @@ libide_1_0_la_public_sources = \
        ide-unsaved-file.h \
        ide-unsaved-files.c \
        ide-unsaved-files.h \
+       ide-view.c \
+       ide-view.h \
        ide-vcs-uri.c \
        ide-vcs-uri.h \
        ide-vcs.c \
        ide-vcs.h \
+       ide-workbench.c \
+       ide-workbench.h \
+       ide-workbench-addin.c \
+       ide-workbench-addin.h \
+       ide-workbench-header-bar.c \
+       ide-workbench-header-bar.h \
        ide-worker.c \
        ide-worker.h \
        ide.c \
@@ -193,14 +215,23 @@ libide_1_0_la_SOURCES = \
        gconstructor.h \
        git/ide-git-buffer-change-monitor.c \
        git/ide-git-buffer-change-monitor.h \
+       greeter/ide-greeter-perspective.c \
+       greeter/ide-greeter-perspective.h \
+       greeter/ide-greeter-project-row.c \
+       greeter/ide-greeter-project-row.h \
        gsettings/ide-gsettings-file-settings.c \
        gsettings/ide-gsettings-file-settings.h \
        gsettings/ide-language-defaults.c \
        gsettings/ide-language-defaults.h \
+       ide-application-actions.c \
+       ide-application-actions.h \
+       ide-application-private.h \
        ide-async-helper.c \
        ide-async-helper.h \
        ide-battery-monitor.c \
        ide-battery-monitor.h \
+       ide-css-provider.c \
+       ide-css-provider.h \
        ide-debug.h \
        ide-extension-util.c \
        ide-extension-util.h \
@@ -209,6 +240,8 @@ libide_1_0_la_SOURCES = \
        ide-line-change-gutter-renderer.h \
        ide-line-diagnostics-gutter-renderer.c \
        ide-line-diagnostics-gutter-renderer.h \
+       ide-keybindings.c \
+       ide-keybindings.h \
        ide-ref-ptr.c \
        ide-ref-ptr.h \
        ide-search-reducer.c \
@@ -228,6 +261,7 @@ libide_1_0_la_SOURCES = \
        ide-source-view-movements.h \
        ide-text-iter.c \
        ide-text-iter.h \
+       ide-workbench-private.h \
        ide-worker-manager.c \
        ide-worker-manager.h \
        ide-worker-process.c \
@@ -253,11 +287,16 @@ libide_1_0_la_SOURCES = \
        util/ide-rgba.h \
        util/ide-text-util.c \
        util/ide-text-util.h \
+       util/ide-window-settings.c \
+       util/ide-window-settings.h \
        $(NULL)
 
 libide_1_0_la_includes = \
        $(DEBUG_CFLAGS) \
-       -DLIBDIR="\"$(libdir)\"" \
+       -DPACKAGE_DATADIR="\"${datadir}\"" \
+       -DPACKAGE_LOCALE_DIR=\""${datadir}/locale"\" \
+       -DPACKAGE_LIBDIR=\""${libdir}"\" \
+       -DBUILDDIR=\""${abs_top_builddir}"\" \
        -I$(top_builddir)/libide \
        -I$(top_srcdir)/contrib/egg \
        -I$(top_srcdir)/contrib/libeditorconfig \
@@ -266,8 +305,10 @@ libide_1_0_la_includes = \
        -I$(srcdir) \
        -I$(srcdir)/directory \
        -I$(srcdir)/doap \
+       -I$(srcdir)/editor \
        -I$(srcdir)/editorconfig \
        -I$(srcdir)/git \
+       -I$(srcdir)/greeter \
        -I$(srcdir)/gsettings \
        -I$(srcdir)/local \
        -I$(srcdir)/modelines \
diff --git a/libide/editor/ide-editor-perspective.c b/libide/editor/ide-editor-perspective.c
new file mode 100644
index 0000000..adc7299
--- /dev/null
+++ b/libide/editor/ide-editor-perspective.c
@@ -0,0 +1,121 @@
+/* ide-editor-perspective.c
+ *
+ * Copyright (C) 2015 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/>.
+ */
+
+#define G_LOG_DOMAIN "ide-editor-perspective"
+
+#include <glib/gi18n.h>
+
+#include "ide-editor-perspective.h"
+#include "ide-workbench-header-bar.h"
+
+struct _IdeEditorPerspective
+{
+  GtkBin                 parent_instance;
+
+  IdeWorkbenchHeaderBar *titlebar;
+};
+
+static void ide_perspective_iface_init (IdePerspectiveInterface *iface);
+
+G_DEFINE_TYPE_EXTENDED (IdeEditorPerspective, ide_editor_perspective, GTK_TYPE_BIN, 0,
+                        G_IMPLEMENT_INTERFACE (IDE_TYPE_PERSPECTIVE, ide_perspective_iface_init))
+
+enum {
+  PROP_0,
+  LAST_PROP
+};
+
+static GParamSpec *properties [LAST_PROP];
+
+static void
+ide_editor_perspective_finalize (GObject *object)
+{
+  G_OBJECT_CLASS (ide_editor_perspective_parent_class)->finalize (object);
+}
+
+static void
+ide_editor_perspective_get_property (GObject    *object,
+                                     guint       prop_id,
+                                     GValue     *value,
+                                     GParamSpec *pspec)
+{
+  switch (prop_id)
+    {
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_editor_perspective_set_property (GObject      *object,
+                                     guint         prop_id,
+                                     const GValue *value,
+                                     GParamSpec   *pspec)
+{
+  switch (prop_id)
+    {
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_editor_perspective_class_init (IdeEditorPerspectiveClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = ide_editor_perspective_finalize;
+  object_class->get_property = ide_editor_perspective_get_property;
+  object_class->set_property = ide_editor_perspective_set_property;
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/builder/ui/ide-editor-perspective.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeEditorPerspective, titlebar);
+}
+
+static void
+ide_editor_perspective_init (IdeEditorPerspective *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+static const gchar *
+ide_editor_perspective_get_title (IdePerspective *perspective)
+{
+  return _("Editor");
+}
+
+static GtkWidget *
+ide_editor_perspective_get_titlebar (IdePerspective *perspective)
+{
+  return GTK_WIDGET (IDE_EDITOR_PERSPECTIVE (perspective)->titlebar);
+}
+
+static const gchar *
+ide_editor_perspective_get_icon_name (IdePerspective *perspective)
+{
+  return "text-editor-symbolic";
+}
+
+static void
+ide_perspective_iface_init (IdePerspectiveInterface *iface)
+{
+  iface->get_title = ide_editor_perspective_get_title;
+  iface->get_titlebar = ide_editor_perspective_get_titlebar;
+  iface->get_icon_name = ide_editor_perspective_get_icon_name;
+}
diff --git a/src/greeter/gb-greeter-pill-box.h b/libide/editor/ide-editor-perspective.h
similarity index 53%
copy from src/greeter/gb-greeter-pill-box.h
copy to libide/editor/ide-editor-perspective.h
index 394a333..b8548bc 100644
--- a/src/greeter/gb-greeter-pill-box.h
+++ b/libide/editor/ide-editor-perspective.h
@@ -1,6 +1,6 @@
-/* gb-greeter-pill-box.h
+/* ide-editor-perspective.h
  *
- * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ * Copyright (C) 2015 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
@@ -16,22 +16,19 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#ifndef GB_GREETER_PILL_BOX_H
-#define GB_GREETER_PILL_BOX_H
+#ifndef IDE_EDITOR_PERSPECTIVE_H
+#define IDE_EDITOR_PERSPECTIVE_H
 
 #include <gtk/gtk.h>
 
-G_BEGIN_DECLS
+#include "ide-perspective.h"
 
-#define GB_TYPE_GREETER_PILL_BOX (gb_greeter_pill_box_get_type())
+G_BEGIN_DECLS
 
-G_DECLARE_FINAL_TYPE (GbGreeterPillBox, gb_greeter_pill_box, GB, GREETER_PILL_BOX, GtkEventBox)
+#define IDE_TYPE_EDITOR_PERSPECTIVE (ide_editor_perspective_get_type())
 
-GtkWidget   *gb_greeter_pill_box_new       (const gchar      *label);
-const gchar *gb_greeter_pill_box_get_label (GbGreeterPillBox *self);
-void         gb_greeter_pill_box_set_label (GbGreeterPillBox *self,
-                                            const gchar      *label);
+G_DECLARE_FINAL_TYPE (IdeEditorPerspective, ide_editor_perspective, IDE, EDITOR_PERSPECTIVE, GtkBin)
 
 G_END_DECLS
 
-#endif /* GB_GREETER_PILL_BOX_H */
+#endif /* IDE_EDITOR_PERSPECTIVE_H */
diff --git a/libide/greeter/ide-greeter-perspective.c b/libide/greeter/ide-greeter-perspective.c
new file mode 100644
index 0000000..f37a4ea
--- /dev/null
+++ b/libide/greeter/ide-greeter-perspective.c
@@ -0,0 +1,699 @@
+/* ide-greeter-perspective.c
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * 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/>.
+ */
+
+#define G_LOG_DOMAIN "ide-greeter-perspective"
+
+#include <glib/gi18n.h>
+
+#include "egg-search-bar.h"
+#include "egg-signal-group.h"
+#include "egg-state-machine.h"
+
+#include "ide-application.h"
+#include "ide-greeter-perspective.h"
+#include "ide-greeter-project-row.h"
+#include "ide-macros.h"
+#include "ide-pattern-spec.h"
+#include "ide-perspective.h"
+#include "ide-workbench.h"
+#include "ide-workbench-private.h"
+
+struct _IdeGreeterPerspective
+{
+  GtkBin                parent_instance;
+
+  EggSignalGroup       *signal_group;
+  IdeRecentProjects    *recent_projects;
+  IdePatternSpec       *pattern_spec;
+  GActionMap           *actions;
+
+  GtkViewport          *viewport;
+  GtkWidget            *titlebar;
+  GtkBox               *my_projects_container;
+  GtkListBox           *my_projects_list_box;
+  GtkBox               *other_projects_container;
+  GtkListBox           *other_projects_list_box;
+  GtkButton            *remove_button;
+  GtkSearchEntry       *search_entry;
+  EggStateMachine      *state_machine;
+  GtkScrolledWindow    *scrolled_window;
+
+  gint                  selected_count;
+};
+
+static void ide_perspective_iface_init (IdePerspectiveInterface *iface);
+
+G_DEFINE_TYPE_EXTENDED (IdeGreeterPerspective, ide_greeter_perspective, GTK_TYPE_BIN, 0,
+                        G_IMPLEMENT_INTERFACE (IDE_TYPE_PERSPECTIVE,
+                                               ide_perspective_iface_init))
+
+enum {
+  PROP_0,
+  PROP_RECENT_PROJECTS,
+  LAST_PROP
+};
+
+static GParamSpec *properties [LAST_PROP];
+
+static GtkWidget *
+ide_greeter_perspective_get_titlebar (IdePerspective *perspective)
+{
+  return IDE_GREETER_PERSPECTIVE (perspective)->titlebar;
+}
+
+static GActionGroup *
+ide_greeter_perspective_get_actions (IdePerspective *perspective)
+{
+  return G_ACTION_GROUP (IDE_GREETER_PERSPECTIVE (perspective)->actions);
+}
+
+static void
+ide_perspective_iface_init (IdePerspectiveInterface *iface)
+{
+  iface->get_actions = ide_greeter_perspective_get_actions;
+  iface->get_titlebar = ide_greeter_perspective_get_titlebar;
+}
+
+static void
+ide_greeter_perspective_first_visible_cb (GtkWidget *widget,
+                                          gpointer   user_data)
+{
+  GtkWidget **row = user_data;
+
+  if ((*row == NULL) && gtk_widget_get_child_visible (widget))
+    *row = widget;
+}
+
+static void
+ide_greeter_perspective__search_entry_activate (IdeGreeterPerspective *self,
+                                                GtkSearchEntry        *search_entry)
+{
+  GtkWidget *row = NULL;
+
+  g_assert (IDE_IS_GREETER_PERSPECTIVE (self));
+  g_assert (GTK_IS_SEARCH_ENTRY (search_entry));
+
+  gtk_container_foreach (GTK_CONTAINER (self->my_projects_list_box),
+                         ide_greeter_perspective_first_visible_cb,
+                         &row);
+  if (row == NULL)
+    gtk_container_foreach (GTK_CONTAINER (self->other_projects_list_box),
+                           ide_greeter_perspective_first_visible_cb,
+                           &row);
+
+  if (row != NULL)
+    g_signal_emit_by_name (row, "activate");
+}
+
+IdeRecentProjects *
+ide_greeter_perspective_get_recent_projects (IdeGreeterPerspective *self)
+{
+  g_return_val_if_fail (IDE_IS_GREETER_PERSPECTIVE (self), NULL);
+
+  return self->recent_projects;
+}
+
+static void
+ide_greeter_perspective_apply_filter_cb (GtkWidget *widget,
+                                   gpointer   user_data)
+{
+  gboolean *visible = user_data;
+
+  g_assert (IDE_IS_GREETER_PROJECT_ROW (widget));
+
+  if (gtk_widget_get_child_visible (widget))
+    *visible = TRUE;
+}
+
+static void
+ide_greeter_perspective_apply_filter (IdeGreeterPerspective *self,
+                                      GtkListBox            *list_box,
+                                      GtkWidget             *container)
+{
+  gboolean visible = FALSE;
+
+  g_assert (IDE_IS_GREETER_PERSPECTIVE (self));
+  g_assert (GTK_IS_LIST_BOX (list_box));
+  g_assert (GTK_IS_CONTAINER (container));
+
+  gtk_list_box_invalidate_filter (list_box);
+  gtk_container_foreach (GTK_CONTAINER (list_box), ide_greeter_perspective_apply_filter_cb, &visible);
+  gtk_widget_set_visible (GTK_WIDGET (container), visible);
+}
+
+static void
+ide_greeter_perspective_apply_filter_all (IdeGreeterPerspective *self)
+{
+  const gchar *text;
+
+  g_assert (IDE_IS_GREETER_PERSPECTIVE (self));
+
+  g_clear_pointer (&self->pattern_spec, ide_pattern_spec_unref);
+  if ((text = gtk_entry_get_text (GTK_ENTRY (self->search_entry))))
+    self->pattern_spec = ide_pattern_spec_new (text);
+
+  ide_greeter_perspective_apply_filter (self,
+                                  self->my_projects_list_box,
+                                  GTK_WIDGET (self->my_projects_container));
+  ide_greeter_perspective_apply_filter (self,
+                                  self->other_projects_list_box,
+                                  GTK_WIDGET (self->other_projects_container));
+}
+
+static void
+ide_greeter_perspective__search_entry_changed (IdeGreeterPerspective *self,
+                                               GtkSearchEntry        *search_entry)
+{
+  g_assert (IDE_IS_GREETER_PERSPECTIVE (self));
+  g_assert (GTK_IS_SEARCH_ENTRY (search_entry));
+
+  ide_greeter_perspective_apply_filter_all (self);
+}
+
+static gboolean
+row_focus_in_event (IdeGreeterPerspective *self,
+                    GdkEventFocus         *focus,
+                    IdeGreeterProjectRow  *row)
+{
+  GtkAllocation alloc;
+  GtkAllocation row_alloc;
+  gint dest_x;
+  gint dest_y;
+
+  g_assert (IDE_IS_GREETER_PERSPECTIVE (self));
+
+  gtk_widget_get_allocation (GTK_WIDGET (self->viewport), &alloc);
+  gtk_widget_get_allocation (GTK_WIDGET (row), &row_alloc);
+
+  /*
+   * If we are smaller than the visible area, don't do anything for now.
+   * This can happen during creation of the window and resize process.
+   */
+  if (row_alloc.height > alloc.height)
+    return GDK_EVENT_PROPAGATE;
+
+  if (gtk_widget_translate_coordinates (GTK_WIDGET (row), GTK_WIDGET (self->viewport), 0, 0, &dest_x, 
&dest_y))
+    {
+      gint distance = 0;
+
+      if (dest_y < 0)
+        {
+          distance = dest_y;
+        }
+      else if ((dest_y + row_alloc.height) > alloc.height)
+        {
+          distance = dest_y + row_alloc.height - alloc.height;
+        }
+
+      if (distance != 0)
+        {
+          GtkAdjustment *vadj;
+          gdouble value;
+
+          vadj = gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (self->viewport));
+          value = gtk_adjustment_get_value (vadj);
+          gtk_adjustment_set_value (vadj, value + distance);
+        }
+    }
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean
+selection_to_true (GBinding     *binding,
+                   const GValue *from_value,
+                   GValue       *to_value,
+                   gpointer      user_data)
+{
+  if (G_VALUE_HOLDS_STRING (from_value) && G_VALUE_HOLDS_BOOLEAN (to_value))
+    {
+      const gchar *str;
+
+      str = g_value_get_string (from_value);
+      g_value_set_boolean (to_value, ide_str_equal0 (str, "selection"));
+
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+static void
+ide_greeter_perspective__row_notify_selected (IdeGreeterPerspective *self,
+                                              GParamSpec            *pspec,
+                                              IdeGreeterProjectRow  *row)
+{
+  gboolean selected = FALSE;
+  GAction *action;
+
+  g_assert (IDE_IS_GREETER_PERSPECTIVE (self));
+  g_assert (pspec != NULL);
+  g_assert (IDE_IS_GREETER_PROJECT_ROW (row));
+
+  g_object_get (row, "selected", &selected, NULL);
+  self->selected_count += selected ? 1 : -1;
+
+  action = g_action_map_lookup_action (self->actions, "delete-selected-rows");
+  g_simple_action_set_enabled (G_SIMPLE_ACTION (action), (self->selected_count > 0));
+}
+
+static void
+recent_projects_items_changed (IdeGreeterPerspective *self,
+                               guint                  position,
+                               guint                  removed,
+                               guint                  added,
+                               GListModel            *list_model)
+{
+  IdeGreeterProjectRow *row;
+  gsize i;
+
+  /*
+   * TODO: We ignore removed out of simplicity for now.
+   *       But IdeRecentProjects doesn't currently remove anything anyway.
+   */
+
+  g_assert (IDE_IS_GREETER_PERSPECTIVE (self));
+  g_assert (G_IS_LIST_MODEL (list_model));
+  g_assert (IDE_IS_RECENT_PROJECTS (list_model));
+
+  for (i = 0; i < added; i++)
+    {
+      IdeProjectInfo *project_info;
+      GtkListBox *list_box;
+
+      project_info = g_list_model_get_item (list_model, position + i);
+
+      row = g_object_new (IDE_TYPE_GREETER_PROJECT_ROW,
+                          "visible", TRUE,
+                          "project-info", project_info,
+                          NULL);
+      g_signal_connect_object (row,
+                               "focus-in-event",
+                               G_CALLBACK (row_focus_in_event),
+                               self,
+                               G_CONNECT_SWAPPED);
+      g_signal_connect_object (row,
+                               "notify::selected",
+                               G_CALLBACK (ide_greeter_perspective__row_notify_selected),
+                               self,
+                               G_CONNECT_SWAPPED);
+
+      if (ide_project_info_get_is_recent (project_info))
+        {
+          list_box = self->my_projects_list_box;
+          g_object_bind_property_full (self->state_machine, "state",
+                                       row, "selection-mode",
+                                       G_BINDING_SYNC_CREATE,
+                                       selection_to_true, NULL,
+                                       NULL, NULL);
+        }
+      else
+        {
+          list_box = self->other_projects_list_box;
+        }
+
+      gtk_container_add (GTK_CONTAINER (list_box), GTK_WIDGET (row));
+    }
+
+  ide_greeter_perspective_apply_filter_all (self);
+}
+
+static gint
+ide_greeter_perspective_sort_rows (GtkListBoxRow *row1,
+                                   GtkListBoxRow *row2,
+                                   gpointer       user_data)
+{
+  IdeProjectInfo *info1;
+  IdeProjectInfo *info2;
+
+  info1 = ide_greeter_project_row_get_project_info (IDE_GREETER_PROJECT_ROW (row1));
+  info2 = ide_greeter_project_row_get_project_info (IDE_GREETER_PROJECT_ROW (row2));
+
+  return ide_project_info_compare (info1, info2);
+}
+
+static void
+ide_greeter_perspective_set_recent_projects (IdeGreeterPerspective *self,
+                                             IdeRecentProjects     *recent_projects)
+{
+  g_return_if_fail (IDE_IS_GREETER_PERSPECTIVE (self));
+  g_return_if_fail (!recent_projects || IDE_IS_RECENT_PROJECTS (recent_projects));
+
+  if (g_set_object (&self->recent_projects, recent_projects))
+    {
+      egg_signal_group_set_target (self->signal_group, recent_projects);
+
+      if (recent_projects != NULL)
+        {
+          GListModel *list_model;
+          guint n_items;
+
+          list_model = G_LIST_MODEL (recent_projects);
+          n_items = g_list_model_get_n_items (list_model);
+          recent_projects_items_changed (self, 0, 0, n_items, list_model);
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RECENT_PROJECTS]);
+    }
+}
+
+static gboolean
+ide_greeter_perspective_filter_row (GtkListBoxRow *row,
+                                    gpointer       user_data)
+{
+  IdeGreeterPerspective *self = user_data;
+  IdeGreeterProjectRow *project_row = (IdeGreeterProjectRow *)row;
+  const gchar *search_text;
+  gboolean ret;
+
+  g_assert (IDE_IS_GREETER_PERSPECTIVE (self));
+  g_assert (IDE_IS_GREETER_PROJECT_ROW (project_row));
+
+  if (self->pattern_spec == NULL)
+    return TRUE;
+
+  search_text = ide_greeter_project_row_get_search_text (project_row);
+  ret = ide_pattern_spec_match (self->pattern_spec, search_text);
+
+  return ret;
+}
+
+static void
+ide_greeter_perspective_context_cb (GObject      *object,
+                                    GAsyncResult *result,
+                                    gpointer      user_data)
+{
+  g_autoptr(IdeGreeterPerspective) self = user_data;
+  IdeWorkbench *workbench;
+  IdeContext *context;
+  GError *error = NULL;
+
+  g_assert (IDE_IS_GREETER_PERSPECTIVE (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+
+  context = ide_context_new_finish (result, &error);
+
+  if (context == NULL)
+    {
+      /* TODO: error handling */
+      g_error ("%s", error->message);
+    }
+
+  workbench = IDE_WORKBENCH (gtk_widget_get_toplevel (GTK_WIDGET (self)));
+  _ide_workbench_set_context (workbench, context);
+}
+
+static void
+ide_greeter_perspective__row_activated (IdeGreeterPerspective *self,
+                                        IdeGreeterProjectRow  *row,
+                                        GtkListBox            *list_box)
+{
+  IdeProjectInfo *project_info;
+  GFile *project_file;
+
+  g_assert (IDE_IS_GREETER_PERSPECTIVE (self));
+  g_assert (IDE_IS_GREETER_PROJECT_ROW (row));
+  g_assert (GTK_IS_LIST_BOX (list_box));
+
+  if (ide_str_equal0 (egg_state_machine_get_state (self->state_machine), "selection"))
+    {
+      gboolean selected = FALSE;
+
+      g_object_get (row, "selected", &selected, NULL);
+      g_object_set (row, "selected", !selected, NULL);
+      return;
+    }
+
+  project_info = ide_greeter_project_row_get_project_info (row);
+  project_file = ide_project_info_get_file (project_info);
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self), FALSE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->titlebar), FALSE);
+
+  /*
+   * TODO: Check if the project is already open somewhere else.
+   */
+
+  ide_context_new_async (project_file,
+                         NULL,
+                         ide_greeter_perspective_context_cb,
+                         g_object_ref (self));
+
+  ide_project_info_set_is_recent (project_info, TRUE);
+}
+
+static gboolean
+ide_greeter_perspective__keynav_failed (IdeGreeterPerspective *self,
+                                        GtkDirectionType       dir,
+                                        GtkListBox            *list_box)
+{
+  g_assert (IDE_IS_GREETER_PERSPECTIVE (self));
+  g_assert (GTK_IS_LIST_BOX (list_box));
+
+  if ((list_box == self->my_projects_list_box) && (dir == GTK_DIR_DOWN))
+    {
+      gtk_widget_child_focus (GTK_WIDGET (self->other_projects_list_box), GTK_DIR_DOWN);
+      return GDK_EVENT_STOP;
+    }
+  else if ((list_box == self->other_projects_list_box) && (dir == GTK_DIR_UP))
+    {
+      gtk_widget_child_focus (GTK_WIDGET (self->my_projects_list_box), GTK_DIR_UP);
+      return GDK_EVENT_STOP;
+    }
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static void
+delete_selected_rows (GSimpleAction *action,
+                      GVariant      *parameter,
+                      gpointer       user_data)
+{
+  IdeGreeterPerspective *self = user_data;
+  GList *rows;
+  GList *iter;
+  GList *projects = NULL;
+
+  g_assert (IDE_IS_GREETER_PERSPECTIVE (self));
+  g_assert (G_IS_SIMPLE_ACTION (action));
+
+  rows = gtk_container_get_children (GTK_CONTAINER (self->my_projects_list_box));
+
+  for (iter = rows; iter; iter = iter->next)
+    {
+      IdeGreeterProjectRow *row = iter->data;
+      gboolean selected = FALSE;
+
+      g_object_get (row, "selected", &selected, NULL);
+
+      if (selected)
+        {
+          IdeProjectInfo *info;
+
+          info = ide_greeter_project_row_get_project_info (row);
+          projects = g_list_prepend (projects, g_object_ref (info));
+          gtk_container_remove (GTK_CONTAINER (self->my_projects_list_box), iter->data);
+        }
+    }
+
+  g_list_free (rows);
+
+  ide_recent_projects_remove (self->recent_projects, projects);
+  g_list_free_full (projects, g_object_unref);
+
+  self->selected_count = 0;
+  g_simple_action_set_enabled (G_SIMPLE_ACTION (action), FALSE);
+
+  egg_state_machine_set_state (self->state_machine, "browse");
+
+  ide_greeter_perspective_apply_filter_all (self);
+}
+
+static void
+ide_greeter_perspective_constructed (GObject *object)
+{
+  IdeGreeterPerspective *self = (IdeGreeterPerspective *)object;
+  IdeRecentProjects *recent_projects;
+
+  G_OBJECT_CLASS (ide_greeter_perspective_parent_class)->constructed (object);
+
+  recent_projects = ide_application_get_recent_projects (IDE_APPLICATION_DEFAULT);
+  ide_greeter_perspective_set_recent_projects (self, recent_projects);
+}
+
+static void
+ide_greeter_perspective_finalize (GObject *object)
+{
+  IdeGreeterPerspective *self = (IdeGreeterPerspective *)object;
+
+  g_clear_pointer (&self->pattern_spec, ide_pattern_spec_unref);
+  g_clear_object (&self->signal_group);
+  g_clear_object (&self->recent_projects);
+
+  G_OBJECT_CLASS (ide_greeter_perspective_parent_class)->finalize (object);
+}
+
+static void
+ide_greeter_perspective_get_property (GObject    *object,
+                                      guint       prop_id,
+                                      GValue     *value,
+                                      GParamSpec *pspec)
+{
+  IdeGreeterPerspective *self = IDE_GREETER_PERSPECTIVE (object);
+
+  switch (prop_id)
+    {
+    case PROP_RECENT_PROJECTS:
+      g_value_set_object (value, ide_greeter_perspective_get_recent_projects (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_greeter_perspective_set_property (GObject      *object,
+                                      guint         prop_id,
+                                      const GValue *value,
+                                      GParamSpec   *pspec)
+{
+  IdeGreeterPerspective *self = IDE_GREETER_PERSPECTIVE (object);
+
+  switch (prop_id)
+    {
+    case PROP_RECENT_PROJECTS:
+      ide_greeter_perspective_set_recent_projects (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_greeter_perspective_class_init (IdeGreeterPerspectiveClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = ide_greeter_perspective_finalize;
+  object_class->constructed = ide_greeter_perspective_constructed;
+  object_class->get_property = ide_greeter_perspective_get_property;
+  object_class->set_property = ide_greeter_perspective_set_property;
+
+  properties [PROP_RECENT_PROJECTS] =
+    g_param_spec_object ("recent-projects",
+                         "Recent Projects",
+                         "The recent projects that have been mined.",
+                         IDE_TYPE_RECENT_PROJECTS,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/builder/ui/ide-greeter-perspective.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeGreeterPerspective, titlebar);
+  gtk_widget_class_bind_template_child (widget_class, IdeGreeterPerspective, my_projects_container);
+  gtk_widget_class_bind_template_child (widget_class, IdeGreeterPerspective, my_projects_list_box);
+  gtk_widget_class_bind_template_child (widget_class, IdeGreeterPerspective, other_projects_container);
+  gtk_widget_class_bind_template_child (widget_class, IdeGreeterPerspective, other_projects_list_box);
+  gtk_widget_class_bind_template_child (widget_class, IdeGreeterPerspective, remove_button);
+  gtk_widget_class_bind_template_child (widget_class, IdeGreeterPerspective, search_entry);
+  gtk_widget_class_bind_template_child (widget_class, IdeGreeterPerspective, scrolled_window);
+  gtk_widget_class_bind_template_child (widget_class, IdeGreeterPerspective, state_machine);
+  gtk_widget_class_bind_template_child (widget_class, IdeGreeterPerspective, viewport);
+}
+
+static void
+ide_greeter_perspective_init (IdeGreeterPerspective *self)
+{
+  GActionEntry actions[] = {
+    { "delete-selected-rows", delete_selected_rows },
+  };
+  GAction *action;
+
+  self->signal_group = egg_signal_group_new (IDE_TYPE_RECENT_PROJECTS);
+  egg_signal_group_connect_object (self->signal_group,
+                                   "items-changed",
+                                   G_CALLBACK (recent_projects_items_changed),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  g_signal_connect_object (self->search_entry,
+                           "activate",
+                           G_CALLBACK (ide_greeter_perspective__search_entry_activate),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->search_entry,
+                           "changed",
+                           G_CALLBACK (ide_greeter_perspective__search_entry_changed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->my_projects_list_box,
+                           "row-activated",
+                           G_CALLBACK (ide_greeter_perspective__row_activated),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->my_projects_list_box,
+                           "keynav-failed",
+                           G_CALLBACK (ide_greeter_perspective__keynav_failed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->other_projects_list_box,
+                           "row-activated",
+                           G_CALLBACK (ide_greeter_perspective__row_activated),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->other_projects_list_box,
+                           "keynav-failed",
+                           G_CALLBACK (ide_greeter_perspective__keynav_failed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  gtk_list_box_set_sort_func (self->my_projects_list_box,
+                              ide_greeter_perspective_sort_rows,
+                              NULL, NULL);
+  gtk_list_box_set_sort_func (self->other_projects_list_box,
+                              ide_greeter_perspective_sort_rows,
+                              NULL, NULL);
+
+  gtk_list_box_set_filter_func (self->my_projects_list_box,
+                                ide_greeter_perspective_filter_row,
+                                self, NULL);
+  gtk_list_box_set_filter_func (self->other_projects_list_box,
+                                ide_greeter_perspective_filter_row,
+                                self, NULL);
+
+  self->actions = G_ACTION_MAP (g_simple_action_group_new ());
+
+  action = egg_state_machine_create_action (self->state_machine, "state");
+  g_action_map_add_action (self->actions, action);
+  g_object_unref (action);
+
+  g_action_map_add_action_entries (self->actions, actions, G_N_ELEMENTS (actions), self);
+
+  action = g_action_map_lookup_action (self->actions, "delete-selected-rows");
+  g_simple_action_set_enabled (G_SIMPLE_ACTION (action), FALSE);
+}
diff --git a/src/greeter/gb-greeter-pill-box.h b/libide/greeter/ide-greeter-perspective.h
similarity index 58%
copy from src/greeter/gb-greeter-pill-box.h
copy to libide/greeter/ide-greeter-perspective.h
index 394a333..5512ffe 100644
--- a/src/greeter/gb-greeter-pill-box.h
+++ b/libide/greeter/ide-greeter-perspective.h
@@ -1,4 +1,4 @@
-/* gb-greeter-pill-box.h
+/* ide-greeter-perspective.h
  *
  * Copyright (C) 2015 Christian Hergert <christian hergert me>
  *
@@ -16,22 +16,19 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#ifndef GB_GREETER_PILL_BOX_H
-#define GB_GREETER_PILL_BOX_H
+#ifndef IDE_GREETER_PERSPECTIVE_H
+#define IDE_GREETER_PERSPECTIVE_H
 
 #include <gtk/gtk.h>
 
-G_BEGIN_DECLS
+#include "ide-recent-projects.h"
 
-#define GB_TYPE_GREETER_PILL_BOX (gb_greeter_pill_box_get_type())
+G_BEGIN_DECLS
 
-G_DECLARE_FINAL_TYPE (GbGreeterPillBox, gb_greeter_pill_box, GB, GREETER_PILL_BOX, GtkEventBox)
+#define IDE_TYPE_GREETER_PERSPECTIVE (ide_greeter_perspective_get_type())
 
-GtkWidget   *gb_greeter_pill_box_new       (const gchar      *label);
-const gchar *gb_greeter_pill_box_get_label (GbGreeterPillBox *self);
-void         gb_greeter_pill_box_set_label (GbGreeterPillBox *self,
-                                            const gchar      *label);
+G_DECLARE_FINAL_TYPE (IdeGreeterPerspective, ide_greeter_perspective, IDE, GREETER_PERSPECTIVE, GtkBin)
 
 G_END_DECLS
 
-#endif /* GB_GREETER_PILL_BOX_H */
+#endif /* IDE_GREETER_PERSPECTIVE_H */
diff --git a/libide/greeter/ide-greeter-project-row.c b/libide/greeter/ide-greeter-project-row.c
new file mode 100644
index 0000000..6988216
--- /dev/null
+++ b/libide/greeter/ide-greeter-project-row.c
@@ -0,0 +1,350 @@
+/* ide-greeter-project-row.c
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * 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/>.
+ */
+
+#define G_LOG_DOMAIN "ide-greeter-project-row"
+
+#include <glib/gi18n.h>
+
+#include "egg-binding-group.h"
+#include "egg-date-time.h"
+#include "egg-pill-box.h"
+
+#include "ide-greeter-project-row.h"
+
+struct _IdeGreeterProjectRow
+{
+  GtkListBoxRow    parent_instance;
+
+  IdeProjectInfo  *project_info;
+  EggBindingGroup *bindings;
+  gchar           *search_text;
+
+  GtkLabel        *date_label;
+  GtkLabel        *description_label;
+  GtkBox          *languages_box;
+  GtkLabel        *location_label;
+  GtkLabel        *title_label;
+  GtkCheckButton  *checkbox;
+};
+
+G_DEFINE_TYPE (IdeGreeterProjectRow, ide_greeter_project_row, GTK_TYPE_LIST_BOX_ROW)
+
+enum {
+  PROP_0,
+  PROP_PROJECT_INFO,
+  PROP_SELECTED,
+  PROP_SELECTION_MODE,
+  LAST_PROP
+};
+
+static GParamSpec *properties [LAST_PROP];
+static GFile      *home_dir;
+
+void
+ide_greeter_project_row_set_selection_mode (IdeGreeterProjectRow *self,
+                                            gboolean              selection_mode)
+{
+  g_return_if_fail (IDE_IS_GREETER_PROJECT_ROW (self));
+
+  gtk_widget_set_visible (GTK_WIDGET (self->checkbox), selection_mode);
+}
+
+IdeProjectInfo *
+ide_greeter_project_row_get_project_info (IdeGreeterProjectRow *self)
+{
+  g_return_val_if_fail (IDE_IS_GREETER_PROJECT_ROW (self), NULL);
+
+  return self->project_info;
+}
+
+static void
+ide_greeter_project_row_create_search_text (IdeGreeterProjectRow *self,
+                                            IdeProjectInfo       *project_info)
+{
+  const gchar *tmp;
+  IdeDoap *doap;
+  GString *str;
+
+  g_assert (IDE_IS_GREETER_PROJECT_ROW (self));
+
+  str = g_string_new (NULL);
+
+  if ((tmp = ide_project_info_get_name (project_info)))
+    {
+      g_autofree gchar *downcase = g_utf8_strdown (g_strdup (tmp), -1);
+
+      g_string_append (str, tmp);
+      g_string_append (str, " ");
+      g_string_append (str, downcase);
+      g_string_append (str, " ");
+    }
+
+  if ((tmp = ide_project_info_get_description (project_info)))
+    {
+      g_string_append (str, tmp);
+      g_string_append (str, " ");
+    }
+
+  doap = ide_project_info_get_doap (project_info);
+
+  if (doap != NULL)
+    {
+      if ((tmp = ide_doap_get_description (doap)))
+        {
+          g_string_append (str, tmp);
+          g_string_append (str, " ");
+        }
+    }
+
+  g_free (self->search_text);
+  self->search_text = g_strdelimit (g_string_free (str, FALSE), "\n", ' ');
+}
+
+static void
+ide_greeter_project_row_add_languages (IdeGreeterProjectRow *self,
+                                       IdeProjectInfo       *project_info)
+{
+  gchar **languages;
+
+  g_return_if_fail (IDE_IS_GREETER_PROJECT_ROW (self));
+  g_return_if_fail (IDE_IS_PROJECT_INFO (project_info));
+
+  if ((languages = ide_project_info_get_languages (project_info)))
+    {
+      guint len = g_strv_length (languages);
+      gsize i;
+
+      for (i = len; i > 0; i--)
+        {
+          const gchar *name = languages [i - 1];
+          GtkWidget *pill;
+
+          pill = g_object_new (EGG_TYPE_PILL_BOX,
+                               "visible", TRUE,
+                               "label", name,
+                               NULL);
+          gtk_container_add (GTK_CONTAINER (self->languages_box), pill);
+        }
+    }
+}
+
+static void
+ide_greeter_project_row_set_project_info (IdeGreeterProjectRow *self,
+                                          IdeProjectInfo       *project_info)
+{
+  g_return_if_fail (IDE_IS_GREETER_PROJECT_ROW (self));
+  g_return_if_fail (!project_info || IDE_IS_PROJECT_INFO (project_info));
+
+  if (g_set_object (&self->project_info, project_info))
+    {
+      egg_binding_group_set_source (self->bindings, project_info);
+
+      if (project_info != NULL)
+        {
+          ide_greeter_project_row_add_languages (self, project_info);
+          ide_greeter_project_row_create_search_text (self, project_info);
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PROJECT_INFO]);
+    }
+}
+
+static gboolean
+humanize_date_time (GBinding     *binding,
+                    const GValue *from_value,
+                    GValue       *to_value,
+                    gpointer      user_data)
+{
+  GDateTime *dt;
+  gchar *str;
+
+  g_assert (G_VALUE_HOLDS (from_value, G_TYPE_DATE_TIME));
+  g_assert (G_VALUE_HOLDS (to_value, G_TYPE_STRING));
+
+  if (!(dt = g_value_get_boxed (from_value)))
+    return FALSE;
+
+  str = egg_date_time_format_for_display (dt);
+  g_value_take_string (to_value, str);
+
+  return TRUE;
+}
+
+static gboolean
+truncate_location (GBinding     *binding,
+                   const GValue *from_value,
+                   GValue       *to_value,
+                   gpointer      user_data)
+{
+  GFile *file;
+  gchar *uri;
+
+  g_assert (G_VALUE_HOLDS (from_value, G_TYPE_FILE));
+  g_assert (G_VALUE_HOLDS (to_value, G_TYPE_STRING));
+
+  if (!(file = g_value_get_object (from_value)))
+    return FALSE;
+
+  if (g_file_is_native (file))
+    {
+      gchar *relative_path;
+
+      if ((relative_path = g_file_get_relative_path (home_dir, file)) ||
+          (relative_path = g_file_get_path (file)))
+        {
+          g_value_set_string (to_value, relative_path);
+          return TRUE;
+        }
+    }
+
+  uri = g_file_get_uri (file);
+  g_value_set_string (to_value, uri);
+
+  return TRUE;
+}
+
+const gchar *
+ide_greeter_project_row_get_search_text (IdeGreeterProjectRow *self)
+{
+  g_return_val_if_fail (IDE_IS_GREETER_PROJECT_ROW (self), NULL);
+
+  return self->search_text;
+}
+
+static void
+ide_greeter_project_row_finalize (GObject *object)
+{
+  IdeGreeterProjectRow *self = (IdeGreeterProjectRow *)object;
+
+  g_clear_object (&self->project_info);
+  g_clear_object (&self->bindings);
+  g_clear_pointer (&self->search_text, g_free);
+
+  G_OBJECT_CLASS (ide_greeter_project_row_parent_class)->finalize (object);
+}
+
+static void
+ide_greeter_project_row_get_property (GObject    *object,
+                                      guint       prop_id,
+                                      GValue     *value,
+                                      GParamSpec *pspec)
+{
+  IdeGreeterProjectRow *self = IDE_GREETER_PROJECT_ROW (object);
+
+  switch (prop_id)
+    {
+    case PROP_PROJECT_INFO:
+      g_value_set_object (value, ide_greeter_project_row_get_project_info (self));
+      break;
+
+    case PROP_SELECTED:
+      g_object_get_property (G_OBJECT (self->checkbox), "active", value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_greeter_project_row_set_property (GObject      *object,
+                                      guint         prop_id,
+                                      const GValue *value,
+                                      GParamSpec   *pspec)
+{
+  IdeGreeterProjectRow *self = IDE_GREETER_PROJECT_ROW (object);
+
+  switch (prop_id)
+    {
+    case PROP_SELECTED:
+      g_object_set_property (G_OBJECT (self->checkbox), "active", value);
+      break;
+
+    case PROP_SELECTION_MODE:
+      ide_greeter_project_row_set_selection_mode (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_PROJECT_INFO:
+      ide_greeter_project_row_set_project_info (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_greeter_project_row_class_init (IdeGreeterProjectRowClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = ide_greeter_project_row_finalize;
+  object_class->get_property = ide_greeter_project_row_get_property;
+  object_class->set_property = ide_greeter_project_row_set_property;
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/builder/ui/ide-greeter-project-row.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeGreeterProjectRow, checkbox);
+  gtk_widget_class_bind_template_child (widget_class, IdeGreeterProjectRow, date_label);
+  gtk_widget_class_bind_template_child (widget_class, IdeGreeterProjectRow, description_label);
+  gtk_widget_class_bind_template_child (widget_class, IdeGreeterProjectRow, location_label);
+  gtk_widget_class_bind_template_child (widget_class, IdeGreeterProjectRow, languages_box);
+  gtk_widget_class_bind_template_child (widget_class, IdeGreeterProjectRow, title_label);
+
+  properties [PROP_SELECTED] =
+    g_param_spec_boolean ("selected",
+                          "Selected",
+                          "Selected",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_SELECTION_MODE] =
+    g_param_spec_boolean ("selection-mode",
+                          "Selection Mode",
+                          "Selection Mode",
+                          FALSE,
+                          (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_PROJECT_INFO] =
+    g_param_spec_object ("project-info",
+                         "Project Information",
+                         "The project information to render.",
+                         IDE_TYPE_PROJECT_INFO,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+
+  home_dir = g_file_new_for_path (g_get_home_dir ());
+}
+
+static void
+ide_greeter_project_row_init (IdeGreeterProjectRow *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  self->bindings = egg_binding_group_new ();
+
+  egg_binding_group_bind (self->bindings, "name", self->title_label, "label", 0);
+  egg_binding_group_bind_full (self->bindings, "last-modified-at", self->date_label, "label", 0,
+                               humanize_date_time, NULL, NULL, NULL);
+  egg_binding_group_bind_full (self->bindings, "directory", self->location_label, "label", 0,
+                               truncate_location, NULL, NULL, NULL);
+  egg_binding_group_bind (self->bindings, "description", self->description_label, "label", 0);
+
+  g_object_bind_property (self->checkbox, "active", self, "selected", 0);
+}
diff --git a/src/greeter/gb-greeter-pill-box.h b/libide/greeter/ide-greeter-project-row.h
similarity index 51%
copy from src/greeter/gb-greeter-pill-box.h
copy to libide/greeter/ide-greeter-project-row.h
index 394a333..8394b0b 100644
--- a/src/greeter/gb-greeter-pill-box.h
+++ b/libide/greeter/ide-greeter-project-row.h
@@ -1,4 +1,4 @@
-/* gb-greeter-pill-box.h
+/* ide-greeter-project-row.h
  *
  * Copyright (C) 2015 Christian Hergert <christian hergert me>
  *
@@ -16,22 +16,24 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#ifndef GB_GREETER_PILL_BOX_H
-#define GB_GREETER_PILL_BOX_H
+#ifndef IDE_GREETER_PROJECT_ROW_H
+#define IDE_GREETER_PROJECT_ROW_H
 
 #include <gtk/gtk.h>
 
+#include "ide-project-info.h"
+
 G_BEGIN_DECLS
 
-#define GB_TYPE_GREETER_PILL_BOX (gb_greeter_pill_box_get_type())
+#define IDE_TYPE_GREETER_PROJECT_ROW (ide_greeter_project_row_get_type())
 
-G_DECLARE_FINAL_TYPE (GbGreeterPillBox, gb_greeter_pill_box, GB, GREETER_PILL_BOX, GtkEventBox)
+G_DECLARE_FINAL_TYPE (IdeGreeterProjectRow, ide_greeter_project_row, IDE, GREETER_PROJECT_ROW, GtkListBoxRow)
 
-GtkWidget   *gb_greeter_pill_box_new       (const gchar      *label);
-const gchar *gb_greeter_pill_box_get_label (GbGreeterPillBox *self);
-void         gb_greeter_pill_box_set_label (GbGreeterPillBox *self,
-                                            const gchar      *label);
+IdeProjectInfo *ide_greeter_project_row_get_project_info   (IdeGreeterProjectRow *self);
+const gchar    *ide_greeter_project_row_get_search_text    (IdeGreeterProjectRow *self);
+void            ide_greeter_project_row_set_selection_mode (IdeGreeterProjectRow *self,
+                                                            gboolean              selection_mode);
 
 G_END_DECLS
 
-#endif /* GB_GREETER_PILL_BOX_H */
+#endif /* IDE_GREETER_PROJECT_ROW_H */
diff --git a/libide/ide-application-actions.c b/libide/ide-application-actions.c
new file mode 100644
index 0000000..0bd0970
--- /dev/null
+++ b/libide/ide-application-actions.c
@@ -0,0 +1,317 @@
+/* ide-application-actions.c
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * 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/>.
+ */
+
+#define G_LOG_DOMAIN "ide-application-actions"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "ide-application.h"
+#include "ide-application-actions.h"
+#include "ide-application-credits.h"
+#include "ide-application-private.h"
+#include "ide-debug.h"
+#include "ide-workbench.h"
+
+static void
+ide_application_actions_preferences (GSimpleAction *action,
+                                     GVariant      *parameter,
+                                     gpointer       user_data)
+{
+#if 0
+  IdeApplication *self = user_data;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_APPLICATION (self));
+
+  if (self->preferences_window == NULL)
+    {
+      IdePreferencesWindow *window;
+
+      window = g_object_new (IDE_TYPE_PREFERENCES_WINDOW,
+                             "type-hint", GDK_WINDOW_TYPE_HINT_DIALOG,
+                             "window-position", GTK_WIN_POS_CENTER,
+                             NULL);
+      ide_set_weak_pointer (&self->preferences_window, window);
+    }
+
+  gtk_window_present (GTK_WINDOW (self->preferences_window));
+
+  IDE_EXIT;
+#endif
+}
+
+#if 0
+static void
+ide_application_actions_support (GSimpleAction *action,
+                                 GVariant      *parameter,
+                                 gpointer       user_data)
+{
+  IdeApplication *self = user_data;
+  GtkWidget *dialog;
+  gchar *text = NULL;
+  GList *windows;
+  GError *error = NULL;
+  gchar *str = NULL;
+  gchar *log_path = NULL;
+  gchar *name = NULL;
+
+  name = g_strdup_printf ("gnome-builder-%u.log", (int)getpid ());
+  log_path = g_build_filename (g_get_home_dir (), name, NULL);
+  g_free (name);
+
+  windows = gtk_application_get_windows (GTK_APPLICATION (self));
+
+  str = ide_get_support_log ();
+
+  if (!g_file_set_contents (log_path, str, -1, &error))
+    {
+      g_printerr ("%s\n", error->message);
+      goto cleanup;
+    }
+
+  text = g_strdup_printf (_("The support log file has been written to '%s'. "
+                            "Please provide this file as an attachment on "
+                            "your bug report or support request."),
+                            log_path);
+
+  g_message ("%s", text);
+
+  dialog = gtk_message_dialog_new (windows ? windows->data : NULL,
+                                   GTK_DIALOG_DESTROY_WITH_PARENT,
+                                   GTK_MESSAGE_INFO,
+                                   GTK_BUTTONS_CLOSE,
+                                   "%s", text);
+  gtk_window_set_modal (GTK_WINDOW (dialog), TRUE);
+  g_signal_connect (dialog, "response", G_CALLBACK (gtk_widget_destroy), NULL);
+  gtk_window_present (GTK_WINDOW (dialog));
+
+cleanup:
+  g_free (text);
+  g_clear_error (&error);
+  g_free (str);
+  g_free (log_path);
+}
+#endif
+
+static void
+ide_application_actions_quit (GSimpleAction *action,
+                              GVariant      *param,
+                              gpointer       user_data)
+{
+  IdeApplication *self = user_data;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_APPLICATION (self));
+
+  g_application_quit (G_APPLICATION (self));
+
+  IDE_EXIT;
+}
+
+static void
+ide_application_actions_about (GSimpleAction *action,
+                               GVariant      *param,
+                               gpointer       user_data)
+{
+  IdeApplication *self = user_data;
+  GtkDialog *dialog;
+  GtkWindow *parent = NULL;
+  GList *iter;
+  GList *windows;
+
+  g_assert (IDE_IS_APPLICATION (self));
+
+  windows = gtk_application_get_windows (GTK_APPLICATION (self));
+
+  for (iter = windows; iter; iter = iter->next)
+    {
+      if (IDE_IS_WORKBENCH (iter->data))
+        {
+          parent = iter->data;
+          break;
+        }
+    }
+
+  dialog = g_object_new (GTK_TYPE_ABOUT_DIALOG,
+                         "artists", ide_application_credits_artists,
+                         "authors", ide_application_credits_authors,
+                         "comments", _("An IDE for GNOME"),
+                         "documenters", ide_application_credits_documenters,
+                         "license-type", GTK_LICENSE_GPL_3_0,
+                         "logo-icon-name", "builder",
+                         "modal", FALSE,
+                         "program-name", _("GNOME Builder"),
+                         "transient-for", parent,
+                         "translator-credits", _("translator-credits"),
+                         "version", PACKAGE_VERSION,
+                         "website", "https://wiki.gnome.org/Apps/Builder";,
+                         "website-label", _("Learn more about GNOME Builder"),
+                         NULL);
+  gtk_about_dialog_add_credit_section (GTK_ABOUT_DIALOG (dialog),
+                                       _("Funded By"),
+                                       ide_application_credits_funders);
+
+  g_signal_connect (dialog, "response", G_CALLBACK (gtk_widget_destroy), NULL);
+  gtk_window_present (GTK_WINDOW (dialog));
+}
+
+#if 0
+static void
+ide_application_actions_open_project (GSimpleAction *action,
+                                      GVariant      *variant,
+                                      gpointer       user_data)
+{
+  IdeApplication *self = user_data;
+
+  g_assert (IDE_IS_APPLICATION (self));
+
+  ide_application_show_projects_window (self);
+}
+
+static void
+ide_application_actions_open_project_cb (GObject      *object,
+                                         GAsyncResult *result,
+                                         gpointer      user_data)
+{
+  IdeApplication *self = (IdeApplication *)object;
+  g_autoptr(IdeNewProjectDialog) window = user_data;
+  g_autoptr(GError) error = NULL;
+  GtkWindow *transient_for;
+
+  g_assert (IDE_IS_NEW_PROJECT_DIALOG (window));
+
+  if (!ide_application_open_project_finish (self, result, &error))
+    {
+      /* todo: warning message */
+      g_warning ("%s", error->message);
+    }
+
+  transient_for = gtk_window_get_transient_for (GTK_WINDOW (window));
+
+  if (IDE_IS_GREETER_WINDOW (transient_for))
+    g_object_ref (transient_for);
+  else
+    transient_for = NULL;
+
+  gtk_widget_destroy (GTK_WIDGET (window));
+
+  if (transient_for != NULL)
+    {
+      gtk_widget_destroy (GTK_WIDGET (transient_for));
+      g_object_unref (transient_for);
+    }
+}
+
+static void
+ide_application_actions__window_open_project (IdeApplication      *self,
+                                              GFile               *project_file,
+                                              IdeNewProjectDialog *window)
+{
+  g_assert (IDE_IS_APPLICATION (self));
+  g_assert (G_IS_FILE (project_file));
+  g_assert (IDE_IS_NEW_PROJECT_DIALOG (window));
+
+  ide_application_open_project_async (self, project_file, NULL, NULL,
+                                     ide_application_actions_open_project_cb,
+                                     g_object_ref (window));
+}
+
+static void
+ide_application_actions_new_project (GSimpleAction *action,
+                                     GVariant      *variant,
+                                     gpointer       user_data)
+{
+  IdeApplication *self = user_data;
+  GtkWindow *transient_for = NULL;
+  GtkWindow *window;
+  GList *windows;
+
+  g_assert (IDE_IS_APPLICATION (self));
+
+  for (windows = gtk_window_group_list_windows (self->greeter_group);
+       windows;
+       windows = windows->next)
+    {
+      if (IDE_IS_NEW_PROJECT_DIALOG (windows->data))
+        {
+          gtk_window_present (windows->data);
+          goto cleanup;
+        }
+      else if (IDE_IS_GREETER_WINDOW (windows->data))
+        {
+          transient_for = windows->data;
+        }
+    }
+
+  window = g_object_new (IDE_TYPE_NEW_PROJECT_DIALOG,
+                         "type-hint", GDK_WINDOW_TYPE_HINT_DIALOG,
+                         "transient-for", transient_for,
+                         "window-position", transient_for ? GTK_WIN_POS_CENTER_ON_PARENT
+                                                          : GTK_WIN_POS_CENTER,
+                         NULL);
+
+  g_signal_connect_object (window,
+                           "open-project",
+                           G_CALLBACK (ide_application_actions__window_open_project),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  gtk_window_group_add_window (self->greeter_group, GTK_WINDOW (window));
+  gtk_window_present (window);
+
+cleanup:
+  g_list_free (windows);
+}
+
+static void
+ide_application_actions_shortcuts (GSimpleAction *action,
+                                   GVariant      *variant,
+                                   gpointer       user_data)
+{
+  IdeShortcutsWindow *window;
+
+  window = g_object_new (IDE_TYPE_SHORTCUTS_WINDOW,
+                         "window-position", GTK_WIN_POS_CENTER,
+                         "default-width", 800,
+                         "default-height", 600,
+                         NULL);
+
+  gtk_window_present (GTK_WINDOW (window));
+}
+#endif
+
+static const GActionEntry IdeApplicationActions[] = {
+  { "about",        ide_application_actions_about },
+  //{ "open-project", ide_application_actions_open_project },
+  //{ "new-project",  ide_application_actions_new_project },
+  { "preferences",  ide_application_actions_preferences },
+  { "quit",         ide_application_actions_quit },
+  //{ "shortcuts",    ide_application_actions_shortcuts },
+  //{ "support",      ide_application_actions_support },
+};
+
+void
+ide_application_actions_init (IdeApplication *self)
+{
+  g_action_map_add_action_entries (G_ACTION_MAP (self), IdeApplicationActions,
+                                   G_N_ELEMENTS (IdeApplicationActions), self);
+}
diff --git a/src/greeter/gb-greeter-pill-box.h b/libide/ide-application-actions.h
similarity index 56%
copy from src/greeter/gb-greeter-pill-box.h
copy to libide/ide-application-actions.h
index 394a333..e4cb961 100644
--- a/src/greeter/gb-greeter-pill-box.h
+++ b/libide/ide-application-actions.h
@@ -1,4 +1,4 @@
-/* gb-greeter-pill-box.h
+/* ide-application-actions.h
  *
  * Copyright (C) 2015 Christian Hergert <christian hergert me>
  *
@@ -16,22 +16,15 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#ifndef GB_GREETER_PILL_BOX_H
-#define GB_GREETER_PILL_BOX_H
+#ifndef IDE_APPLICATION_ACTIONS_H
+#define IDE_APPLICATION_ACTIONS_H
 
-#include <gtk/gtk.h>
+#include "ide-application.h"
 
 G_BEGIN_DECLS
 
-#define GB_TYPE_GREETER_PILL_BOX (gb_greeter_pill_box_get_type())
-
-G_DECLARE_FINAL_TYPE (GbGreeterPillBox, gb_greeter_pill_box, GB, GREETER_PILL_BOX, GtkEventBox)
-
-GtkWidget   *gb_greeter_pill_box_new       (const gchar      *label);
-const gchar *gb_greeter_pill_box_get_label (GbGreeterPillBox *self);
-void         gb_greeter_pill_box_set_label (GbGreeterPillBox *self,
-                                            const gchar      *label);
+void ide_application_actions_init (IdeApplication *self);
 
 G_END_DECLS
 
-#endif /* GB_GREETER_PILL_BOX_H */
+#endif /* IDE_APPLICATION_ACTIONS_H */
diff --git a/libide/ide-application-addin.c b/libide/ide-application-addin.c
new file mode 100644
index 0000000..d03d5f5
--- /dev/null
+++ b/libide/ide-application-addin.c
@@ -0,0 +1,81 @@
+/* ide-application-addin.c
+ *
+ * Copyright (C) 2015 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/>.
+ */
+
+#include "ide-application-addin.h"
+
+G_DEFINE_INTERFACE (IdeApplicationAddin, ide_application_addin, G_TYPE_OBJECT)
+
+static void
+ide_application_addin_real_load (IdeApplicationAddin *self,
+                                 IdeApplication      *application)
+{
+}
+
+static void
+ide_application_addin_real_unload (IdeApplicationAddin *self,
+                                   IdeApplication      *application)
+{
+}
+
+static void
+ide_application_addin_default_init (IdeApplicationAddinInterface *iface)
+{
+  iface->load = ide_application_addin_real_load;
+  iface->unload = ide_application_addin_real_unload;
+}
+
+/**
+ * ide_application_addin_load:
+ * @self: An #IdeApplicationAddin.
+ * @application: An #IdeApplication.
+ *
+ * This interface method is called when the application is started or
+ * the plugin has just been activated.
+ *
+ * Use this to setup code in your plugin that needs to be loaded once
+ * per application process.
+ */
+void
+ide_application_addin_load (IdeApplicationAddin *self,
+                            IdeApplication      *application)
+{
+  g_return_if_fail (IDE_IS_APPLICATION_ADDIN (self));
+  g_return_if_fail (IDE_IS_APPLICATION (application));
+
+  IDE_APPLICATION_ADDIN_GET_IFACE (self)->load (self, application);
+}
+
+/**
+ * ide_application_addin_unload:
+ * @self: An #IdeApplicationAddin.
+ * @application: An #IdeApplication.
+ *
+ * This inteface method is called when the application is shutting down
+ * or the plugin has been unloaded.
+ *
+ * Use this function to cleanup after anything setup in ide_application_addin_load().
+ */
+void
+ide_application_addin_unload (IdeApplicationAddin *self,
+                              IdeApplication      *application)
+{
+  g_return_if_fail (IDE_IS_APPLICATION_ADDIN (self));
+  g_return_if_fail (IDE_IS_APPLICATION (application));
+
+  IDE_APPLICATION_ADDIN_GET_IFACE (self)->unload (self, application);
+}
diff --git a/libide/ide-application-addin.h b/libide/ide-application-addin.h
new file mode 100644
index 0000000..8ecde9b
--- /dev/null
+++ b/libide/ide-application-addin.h
@@ -0,0 +1,47 @@
+/* ide-application-addin.h
+ *
+ * Copyright (C) 2015 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/>.
+ */
+
+#ifndef IDE_APPLICATION_ADDIN_H
+#define IDE_APPLICATION_ADDIN_H
+
+#include "ide-application.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_APPLICATION_ADDIN (ide_application_addin_get_type())
+
+G_DECLARE_INTERFACE (IdeApplicationAddin, ide_application_addin, IDE, APPLICATION_ADDIN, GObject)
+
+struct _IdeApplicationAddinInterface
+{
+  GTypeInterface parent_interface;
+
+  void (*load)   (IdeApplicationAddin *self,
+                  IdeApplication      *application);
+  void (*unload) (IdeApplicationAddin *self,
+                  IdeApplication      *application);
+};
+
+void ide_application_addin_load   (IdeApplicationAddin *self,
+                                   IdeApplication      *application);
+void ide_application_addin_unload (IdeApplicationAddin *self,
+                                   IdeApplication      *application);
+
+G_END_DECLS
+
+#endif /* IDE_APPLICATION_ADDIN_H */
diff --git a/libide/ide-application-credits.h b/libide/ide-application-credits.h
new file mode 100644
index 0000000..abe7f92
--- /dev/null
+++ b/libide/ide-application-credits.h
@@ -0,0 +1,554 @@
+/* ide-application-credits.h
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file 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
+ * Lesser 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/>.
+ */
+
+#ifndef IDE_APPLICATION_CREDITS_H
+#define IDE_APPLICATION_CREDITS_H
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+static const gchar *ide_application_credits_artists[] = {
+  "Allan Day",
+  "Hylke Bons",
+  "Jakub Steiner",
+  NULL
+};
+
+static const gchar *ide_application_credits_authors[] = {
+  "Alexander Larsson",
+  "Alexandre Franke",
+  "Andika Triwidada",
+  "Andreas Henriksson",
+  "Antoine Jacoutot",
+  "Aurimas Černius",
+  "Baurzhan Muftakhidinov",
+  "Ben Iofel",
+  "Bernd Homuth",
+  "burningTyger",
+  "Carlos Garnacho",
+  "Carlos Soriano",
+  "Changwoo Ryu",
+  "Chao-Hsiung Liao",
+  "Cheng-Chia Tseng",
+  "Christian Hergert",
+  "Christian Kirbach",
+  "Cosimo Cecchi",
+  "Damien Lespiau",
+  "Daniel Korostil",
+  "Daniel Mustieles",
+  "David King",
+  "Dimitris Zenios",
+  "Dušan Kazik",
+  "Ekaterina Gerasimova",
+  "Elad Alfassa",
+  "Erick Pérez Castellanos",
+  "Fabiano Fidêncio",
+  "Florian Bäuerle",
+  "Florian Müllner",
+  "Fran Dieguez",
+  "Gabor Kelemen",
+  "Gábor Kelemen",
+  "Garrett Regier",
+  "Giovanni Campagna",
+  "Hashem Nasarat",
+  "Hylke Bons",
+  "Ian Hernandez",
+  "Ignacio Casal Quinteiro",
+  "Igor Gnatenko",
+  "Jakub Steiner",
+  "Jasper St. Pierre",
+  "Jonathon Jongsma",
+  "Jordi Mas",
+  "Kalev Lember",
+  "Kjartan Maraas",
+  "Kristjan SCHMIDT",
+  "Lars Uebernickel",
+  "Lionel Landwerlin",
+  "Marek Černocký",
+  "Matej Urbančič",
+  "Mathieu Bridon",
+  "Matthias Clasen",
+  "Megh Parikh",
+  "Michael Catanzaro",
+  "Mohan R",
+  "Muhammet Kara",
+  "Мирослав Николић",
+  "Paolo Borelli",
+  "Patrick Griffis",
+  "Pedro Albuquerque",
+  "Pete Travis",
+  "Piotr Drąg",
+  "Ray Strode",
+  "Roberto Majadas",
+  "Samir Ribic",
+  "Sébastien Lafargue",
+  "Timm Bäder",
+  "TingPing",
+  "Ting-Wei Lan",
+  "Tobias Schönberg",
+  "Tom Tryfonidis",
+  "Trinh Anh Ngoc",
+  "Wolf Vollprecht",
+  "Yannick Inizan",
+  "Yosef Or Boczko",
+  "zilla hmt im",
+  NULL
+};
+
+static const gchar *ide_application_credits_documenters[] = {
+  "Christian Hergert",
+  NULL
+};
+
+static const gchar *ide_application_credits_funders[] = {
+  "曾政嘉",
+  "Aaron Hergert",
+  "Abdul Kadri Gündoğdu",
+  "Abimael Martinez Carrete",
+  "Adam grunden",
+  "Adrian Bradshaw",
+  "Adrian Rocha",
+  "Adrià Arrufat",
+  "Alaska Subedi",
+  "Albert Murciego Rico",
+  "Alessandro Bono",
+  "Alexander B Libby",
+  "Alexander Gleason",
+  "Alexander Khatsayuk",
+  "Alexander Larsson",
+  "Alexander Murray",
+  "Alexander Murray",
+  "Alexandre Amoedo",
+  "Alexandre Franke",
+  "Alexandros Diavatis",
+  "Alfonso de Cala Bravo",
+  "Alfred Santacatalina Gea",
+  "Ambrose Andrews",
+  "Andreas Nilsson",
+  "Andrew Stiegmann",
+  "Andrew Walton",
+  "Anthony Taranto",
+  "Anton Shafarenko",
+  "Aram J Agajanian",
+  "Arne Hoch",
+  "Arturo Buentello G",
+  "Arun Raghavan",
+  "Ashley Sommer",
+  "Aurélien Naldi",
+  "B. Mille-Mathias",
+  "Baldessari Michele",
+  "Bastian Ilsø Hougaard",
+  "Bastien Nocera",
+  "Bastien Nocera",
+  "Benjamin Grimm-Lebsanft",
+  "Bernd Homuth",
+  "Bill Roth",
+  "Brad Taylor",
+  "Brendan Long",
+  "Brijesh Kartha",
+  "Bruce Cowan",
+  "Bruce M Franklin",
+  "Búza Géza",
+  "Canek Pelaez Valdes",
+  "Carlos Soriano Sanchez",
+  "Casey E Megginson",
+  "Cedric Briner",
+  "Cees Meijer",
+  "Centricular Ltd",
+  "Chema Casanova",
+  "Cheng-Chia Tseng",
+  "Chris Bagwell",
+  "Chris Kühl",
+  "Chris Tonkinson",
+  "Christian Hergert",
+  "Christian Lange",
+  "Christopher Brian Sherlock",
+  "Christopher Horton",
+  "Christopher Kim",
+  "Christopher William Bell",
+  "Chun-Sheng Wu",
+  "Cody Russell",
+  "Cosimo Cecchi",
+  "Craig A Cabrey",
+  "Dag Robøle",
+  "Daiki Ueno",
+  "Damián Nohales",
+  "Daniel Buch",
+  "Daniel Dui",
+  "Daniel Espinosa Ortiz",
+  "Daniel Menchaca",
+  "Daniel Nemec",
+  "Daniel Pfeifer",
+  "Daniel Vazquez Rivera",
+  "Danilo Gropelo",
+  "Danylo Korostil",
+  "Debarshi Ray",
+  "Demetris Lambrou",
+  "Dennis Schulmeister",
+  "Denver Gingerich",
+  "Dolgoff ",
+  "Eduardo Silva",
+  "Eitan Isaacson",
+  "Elad Alfassa",
+  "Ellis Kenyo",
+  "Emanuele Aina",
+  "Emanuele Gissi",
+  "Emmanuele Bassi",
+  "Enrique Ocaña González",
+  "Eric Streit",
+  "Eric T Miller",
+  "Erik Helin",
+  "Ernest hershey",
+  "Erwan Bousse",
+  "Erwan Georget",
+  "F. Kooman",
+  "Fabian Alexander Wilms",
+  "Fabien Cortina",
+  "Fabio Valentini",
+  "Faizan Qazi",
+  "Faron S Anslow",
+  "Fasiello Nicomede",
+  "Federico Mena Quintero",
+  "Felix Schröter",
+  "Filipe Santos",
+  "Florian Bäuerle",
+  "Florian Over",
+  "Florian Schweikert",
+  "Florin Florica",
+  "Frank Dietrich",
+  "Frank Hansen",
+  "Fränz Ney",
+  "Fredrik Schaller",
+  "G A Foster",
+  "Gabriel Rauter",
+  "Garrett LeSage",
+  "Georges Seguin",
+  "Georges-Mickael Seguin",
+  "Gianluigi Calcaterra",
+  "Gil Forcada Codinachs",
+  "Giovanni Forte",
+  "Go Min YounG",
+  "Gonzalo Paniagua Javier",
+  "Gordon Martin",
+  "Guilherme Rodrigues",
+  "Guillaume Beaudin",
+  "Guillaume Hain",
+  "Guillaume Quintard",
+  "Gustavo Arejano",
+  "Gustavo N Silva",
+  "Hain Guillaume",
+  "Hannes Ovrén",
+  "Harald Hoyer",
+  "Havoc Pennington",
+  "Hendrik Richter",
+  "Henrique Almeida",
+  "Henry Finucane",
+  "I Martin Rodriguez",
+  "Ian Bolf",
+  "Ian McKellar",
+  "Ignacio Casal Quinteiro",
+  "Igor Gnatenko",
+  "Ilya Novosyolov",
+  "Ioram Gordadze",
+  "Ivan Nezhdanov",
+  "J. Pereira Rocha",
+  "J.A.J. Vermeulen",
+  "Jack Jennings",
+  "James M Cape",
+  "James Mason",
+  "James",
+  "Jan Dudulski",
+  "Jan-Christoph Borchardt",
+  "Jason Carey",
+  "Jason D Levine",
+  "Jason R Anderson",
+  "Jason Scurtu",
+  "Javier Jardón",
+  "Javier Monteagudo",
+  "Jean-François Fortin Tam",
+  "Jeff Waugh",
+  "Jeffrey Dorrycott",
+  "Jesse van den Kieboom",
+  "Jim Campbell",
+  "Jiri Eischmann",
+  "Joakim Söderlund",
+  "Joaquin Mendez",
+  "Johan Dahlin",
+  "John Gary Billings",
+  "John M Carr",
+  "John Palmieri",
+  "Jonathan Lane",
+  "Jonathan Lestrelin",
+  "Jonathan Zuñiga Juarez",
+  "Jonathon Jongsma",
+  "Jorge Rodriguez Flores Esp",
+  "Joseph Hain",
+  "Juan Jose Marin Martinez",
+  "Jugoslav Gacas",
+  "Julien Girardin",
+  "Jussi Henrik Kukkonen",
+  "Justin D Kruger",
+  "Justin Roth",
+  "Justyn Butler",
+  "Katrin Leinweber",
+  "Keith Tokash",
+  "Kenneth Nielsen",
+  "Khalid Eldehairy",
+  "Kris Thomsen",
+  "Lapo Calamandrei",
+  "Lars Uebernickel",
+  "Laurent Mouillart",
+  "Le Guevel Gwendal",
+  "Leif Gruenwoldt",
+  "Lionel Landwerlin",
+  "Logan VanCuren",
+  "Lucas Almeida Rocha",
+  "Lukasz Ochoda",
+  "Luke Gaudreau",
+  "M C V Crouch",
+  "M. Aurélien Couderc",
+  "Mac Baker",
+  "Maciej M Piechotka",
+  "Magnun Leno Silva",
+  "Marc Andre Lureau",
+  "Marc Thomas",
+  "Marcio Sousa Rocha",
+  "Marco Barisione",
+  "Marcus Husar",
+  "Marcus Lundblad",
+  "Marek Suchánek",
+  "Marina Zhurakhinskaya",
+  "Mario Sanchez-Prada",
+  "Marius Heidenreich",
+  "Marius Mather",
+  "Markus Berg",
+  "Mart Roosmaa",
+  "Martin A Stembel",
+  "Martin Andersson",
+  "Martin Blanchard",
+  "Martin C Foster",
+  "Martin Unzner",
+  "Matej Smid",
+  "Mathieu Bridon",
+  "Matthew Nicholson",
+  "Mattias Bengtsson",
+  "Max Whittingham",
+  "Maxim Yaskevich",
+  "Michael Catanzaro",
+  "Michael Grundy",
+  "Michael Hill",
+  "Michael Ivanov",
+  "Michael Kuhn",
+  "Michael Mansell",
+  "Michael S DePaulo",
+  "Michael Scofield",
+  "Michel Alexandre Salim",
+  "Miguel e dos Santos",
+  "Mikel Olasagasti Uranga",
+  "Mikhail Feshchenko",
+  "Molly Shelestak",
+  "Nasser Alshammari",
+  "Nathan Samson",
+  "Neil Stalker",
+  "Neils Nesse",
+  "Nelson Jesus Benitez Leon",
+  "Nicholas E Richards",
+  "Nicholas George",
+  "Nick Melnick",
+  "Niclas Moeslund Overby",
+  "Nicola Mazbar",
+  "Nicolas Jeker",
+  "Niklas Rosenqvist",
+  "Nikola Trifunovic",
+  "Nil Gradisnik",
+  "Nirbheek Chauhan",
+  "Olav Vitters",
+  "Oliver Propst",
+  "Olivier Crete",
+  "Ondřej Holý",
+  "Ondřej Tůma",
+  "Owen Taylor",
+  "P Tunnell Wilson",
+  "P.F. Mulder",
+  "Pacaud Emmanuel",
+  "Pakkanen Jussi T",
+  "Paolo Borelli",
+  "Pascal Garber",
+  "Patrick Griffis",
+  "Patrick Wspanialy",
+  "Patrik Nilsson",
+  "Patrizio Bruno",
+  "Paul R Martin",
+  "Pedro J Ayala Gomariz",
+  "Perry L Peters",
+  "Peter Baumgarten",
+  "Peter Cornelis",
+  "Peter J Shinners",
+  "Peter Weber",
+  "Philip Corbett",
+  "Philip F Chimento",
+  "Philip J Freeman",
+  "Philip Whitfield",
+  "Piotr Zurek",
+  "R A McQueen",
+  "R A McQueen",
+  "RM van Schouwen",
+  "Radosław Sierbiński",
+  "Ray Strode",
+  "Remco Kranenburg",
+  "Remi Grolleau",
+  "Rickard Johansson",
+  "Robert Carr",
+  "Robert Taylor",
+  "Roberto Clapis",
+  "Rodolphe PP",
+  "Rory MacQueen",
+  "Rosanna Blandford",
+  "Ross N Gardiner",
+  "Rouchon Jean-Noel",
+  "Rui Paulo Barreira",
+  "Russell Cox",
+  "Ryan Hartlage",
+  "Ryan Lerch",
+  "Rémi Lauzier",
+  "S Axon",
+  "Sajid Badi-uz-zaman",
+  "Samuel B Thursfield",
+  "Samuel Gyger",
+  "Sasan Namiranian",
+  "Saul Vargas Sandoval",
+  "Sebastian Droege",
+  "Shapor Naghibzadeh",
+  "Shawn Ferris",
+  "Shlomo Choina",
+  "Shuji Narazaki",
+  "Simon Roesch",
+  "Sriram Ramkrishna",
+  "Stefi T Petit",
+  "Stephany Wilkes",
+  "Stephen Genusa",
+  "Stephen Shaw",
+  "Steve Z McCauley",
+  "Steven J Herber",
+  "Steven W Brown",
+  "Steven Wills",
+  "Stewart Webb",
+  "Stuart Ellis",
+  "Stéphane Démurget",
+  "Stéphane Maniaci",
+  "Søren Hauberg",
+  "Tadej Janež",
+  "Ted Hennicke",
+  "Thibault Saunier",
+  "Thomas Andersen",
+  "Thomas Maffia",
+  "Thomas McDonald",
+  "Tom Erik Gundersen",
+  "Tomas Peterka",
+  "Tommi Tauriainen",
+  "Tomáš Krchňák",
+  "Tomáš Popela",
+  "Toni Willberg",
+  "Torsten Scholak",
+  "Travis Hartwell",
+  "Tyler J. Brock",
+  "Uwe Hametner",
+  "Vadzim Rutkouski",
+  "Valter Schütz",
+  "WP MANLEY",
+  "WP Manley",
+  "Wee Weea",
+  "Wesley Wiser",
+  "Will Binns-Smith",
+  "William Hoffmann",
+  "William J Thompson",
+  "William J Thompson",
+  "William Jon McCann",
+  "William R Lachance",
+  "Z Jedrzejewski-Szmek",
+  "adam820",
+  "arclnx",
+  "aurelien.busi",
+  "carwyn",
+  "d.westerik",
+  "daniel.fontaine",
+  "david odenwald",
+  "dchristidis",
+  "demirtas burakk",
+  "eik.w1911",
+  "eliasdorneles",
+  "elken.tdos",
+  "fafatheone",
+  "florian.muellner",
+  "gerald.b.nunn",
+  "gyger",
+  "ich",
+  "ideasman42",
+  "jimmac",
+  "jnoel",
+  "joannis.orlandos",
+  "joaquin8mendez",
+  "joel",
+  "juanjomarin96",
+  "kamilprusko",
+  "kenneth",
+  "kesmarag",
+  "kevin",
+  "kidoz",
+  "kuba",
+  "lovenemesis",
+  "luke.a.morton",
+  "madstitz",
+  "marc.chocolat",
+  "mariospr",
+  "markpariente",
+  "matthias.clasen",
+  "maxlupo",
+  "mcatanzaro",
+  "muflone",
+  "nils.werner",
+  "otaylor",
+  "pedroclg",
+  "peterldev94",
+  "pizzamartijn",
+  "pseus7+indiegogo",
+  "public228",
+  "ray strode",
+  "ross",
+  "sandquist",
+  "scottm2031",
+  "sebastien lafargue",
+  "sebastien.wilmet",
+  "sfs",
+  "slyon",
+  "swalf",
+  "sylvain.pasche",
+  "tglman",
+  "theo",
+  "tommaso.visconti",
+  "vamega",
+  "verduler",
+  "vperetokin",
+  "w.vollprecht",
+  NULL
+};
+
+G_END_DECLS
+
+#endif /* IDE_APPLICATION_CREDITS_H */
diff --git a/src/greeter/gb-greeter-pill-box.h b/libide/ide-application-private.h
similarity index 51%
copy from src/greeter/gb-greeter-pill-box.h
copy to libide/ide-application-private.h
index 394a333..cf3856f 100644
--- a/src/greeter/gb-greeter-pill-box.h
+++ b/libide/ide-application-private.h
@@ -1,4 +1,4 @@
-/* gb-greeter-pill-box.h
+/* gb-application-private.h
  *
  * Copyright (C) 2015 Christian Hergert <christian hergert me>
  *
@@ -16,22 +16,35 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#ifndef GB_GREETER_PILL_BOX_H
-#define GB_GREETER_PILL_BOX_H
+#ifndef IDE_APPLICATION_PRIVATE_H
+#define IDE_APPLICATION_PRIVATE_H
 
 #include <gtk/gtk.h>
+#include <gio/gio.h>
+#include <libpeas/peas.h>
 
-G_BEGIN_DECLS
-
-#define GB_TYPE_GREETER_PILL_BOX (gb_greeter_pill_box_get_type())
+#include "ide-keybindings.h"
+#include "ide-recent-projects.h"
+#include "ide-worker-manager.h"
 
-G_DECLARE_FINAL_TYPE (GbGreeterPillBox, gb_greeter_pill_box, GB, GREETER_PILL_BOX, GtkEventBox)
+G_BEGIN_DECLS
 
-GtkWidget   *gb_greeter_pill_box_new       (const gchar      *label);
-const gchar *gb_greeter_pill_box_get_label (GbGreeterPillBox *self);
-void         gb_greeter_pill_box_set_label (GbGreeterPillBox *self,
-                                            const gchar      *label);
+struct _IdeApplication
+{
+  GtkApplication        parent_instance;
+
+  gchar                *argv0;
+  gchar                *dbus_address;
+  PeasExtensionSet     *extensions;
+  GtkWindowGroup       *greeter_group;
+  IdeKeybindings       *keybindings;
+  GtkWindow            *preferences_window;
+  IdeRecentProjects    *recent_projects;
+  GDateTime            *startup_time;
+  gchar                *type;
+  IdeWorkerManager     *worker_manager;
+};
 
 G_END_DECLS
 
-#endif /* GB_GREETER_PILL_BOX_H */
+#endif /* GB_APPLICATION_PRIVATE_H */
diff --git a/libide/ide-application.c b/libide/ide-application.c
new file mode 100644
index 0000000..faab43a
--- /dev/null
+++ b/libide/ide-application.c
@@ -0,0 +1,1002 @@
+/* ide-application.c
+ *
+ * Copyright (C) 2014 Christian Hergert <christian hergert me>
+ *
+ * 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/>.
+ */
+
+#define G_LOG_DOMAIN "ide-application"
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#ifdef __linux
+# include <sys/prctl.h>
+#endif
+
+#include <glib/gi18n.h>
+#include <gtksourceview/gtksource.h>
+#include <libgit2-glib/ggit.h>
+
+#include "ide-application.h"
+#include "ide-application-actions.h"
+#include "ide-application-addin.h"
+#include "ide-application-private.h"
+#include "ide-css-provider.h"
+#include "ide-debug.h"
+#include "ide-internal.h"
+#include "ide-file.h"
+#include "ide-log.h"
+#include "ide-macros.h"
+#include "ide-resources.h"
+#include "ide-vcs.h"
+#include "ide-workbench.h"
+#include "ide-worker.h"
+
+#include "modeline-parser.h"
+
+G_DEFINE_TYPE (IdeApplication, ide_application, GTK_TYPE_APPLICATION)
+
+static gboolean
+ide_application_can_load_plugin (IdeApplication *self,
+                                 PeasPluginInfo *plugin_info)
+{
+  const gchar *plugin_name;
+
+  g_assert (IDE_IS_APPLICATION (self));
+  g_assert (plugin_info != NULL);
+
+  /* Currently we only allow in-tree plugins */
+  if (!peas_plugin_info_is_builtin (plugin_info))
+    return FALSE;
+
+  plugin_name = peas_plugin_info_get_module_name (plugin_info);
+  if (ide_str_equal0 (plugin_name, self->type))
+    return FALSE;
+
+  return TRUE;
+}
+
+static void
+ide_application_load_plugins (IdeApplication *self)
+{
+  PeasEngine *engine = peas_engine_get_default ();
+  const GList *list;
+
+  peas_engine_enable_loader (engine, "python3");
+
+  if (g_getenv ("BUiLDER_IN_TREE_PLUGINS") != NULL)
+    {
+      GDir *dir;
+
+      g_irepository_require_private (g_irepository_get_default (),
+                                     BUILDDIR"/libide",
+                                     "Ide", "1.0", 0, NULL);
+
+      if ((dir = g_dir_open (BUILDDIR"/plugins", 0, NULL)))
+        {
+          const gchar *name;
+
+          while ((name = g_dir_read_name (dir)))
+            {
+              gchar *path;
+
+              path = g_build_filename (BUILDDIR, "plugins", name, NULL);
+              peas_engine_prepend_search_path (engine, path, path);
+              g_free (path);
+            }
+
+          g_dir_close (dir);
+        }
+    }
+  else
+    {
+      peas_engine_prepend_search_path (engine,
+                                       PACKAGE_LIBDIR"/gnome-builder/plugins",
+                                       PACKAGE_DATADIR"/gnome-builder/plugins");
+    }
+
+  list = peas_engine_get_plugin_list (engine);
+
+  for (; list; list = list->next)
+    {
+      if (ide_application_can_load_plugin (self, list->data))
+        peas_engine_load_plugin (engine, list->data);
+    }
+}
+
+static gboolean
+ide_application_is_worker (IdeApplication *self)
+{
+  g_assert (IDE_IS_APPLICATION (self));
+
+  return (self->type != NULL) && (self->dbus_address != NULL);
+}
+
+static void
+ide_application_load_worker (IdeApplication *self)
+{
+  g_autoptr(GDBusConnection) connection = NULL;
+  PeasEngine *engine;
+  PeasPluginInfo *plugin_info;
+  GError *error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_APPLICATION (self));
+  g_assert (ide_application_is_worker (self));
+
+#ifdef __linux
+  /* Ensure we are killed with our parent */
+  prctl (PR_SET_PDEATHSIG, 15);
+#endif
+
+  IDE_TRACE_MSG ("Connecting to %s", self->dbus_address);
+
+  connection = g_dbus_connection_new_for_address_sync (self->dbus_address,
+                                                       (G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT |
+                                                        G_DBUS_CONNECTION_FLAGS_DELAY_MESSAGE_PROCESSING),
+                                                       NULL, NULL, &error);
+
+  if (error != NULL)
+    {
+      g_error ("DBus failure: %s", error->message);
+      g_clear_error (&error);
+      IDE_EXIT;
+    }
+
+  g_assert (G_IS_DBUS_CONNECTION (connection));
+
+  engine = peas_engine_get_default ();
+  plugin_info = peas_engine_get_plugin_info (engine, self->type);
+
+  if ((plugin_info != NULL) && peas_plugin_info_is_loaded (plugin_info))
+    {
+      PeasExtension *exten;
+
+      exten = peas_engine_create_extension (engine, plugin_info, IDE_TYPE_WORKER, NULL);
+
+      if (exten != NULL)
+        {
+          ide_worker_register_service (IDE_WORKER (exten), connection);
+          IDE_GOTO (success);
+        }
+    }
+
+  g_error ("Failed to create \"%s\" worker.", self->type);
+
+  IDE_EXIT;
+
+success:
+  g_application_hold (G_APPLICATION (self));
+  g_dbus_connection_start_message_processing (connection);
+
+  IDE_EXIT;
+}
+
+static void
+ide_application_setup_search_paths (void)
+{
+  GtkSourceStyleSchemeManager *style_scheme_manager;
+  static gboolean initialized;
+
+  if (initialized)
+    return;
+
+  style_scheme_manager = gtk_source_style_scheme_manager_get_default ();
+  gtk_source_style_scheme_manager_append_search_path (style_scheme_manager,
+                                                      PACKAGE_DATADIR"/gtksourceview-3.0/styles/");
+  initialized = TRUE;
+}
+
+/**
+ * ide_application_make_skeleton_dirs:
+ * @self: A #IdeApplication.
+ *
+ * Creates all the directories we might need later. Simpler to just ensure they
+ * are created during startup.
+ */
+static void
+ide_application_make_skeleton_dirs (IdeApplication *self)
+{
+  gchar *path;
+
+  g_return_if_fail (IDE_IS_APPLICATION (self));
+
+  path = g_build_filename (g_get_user_data_dir (),
+                           "gnome-builder",
+                           NULL);
+  g_mkdir_with_parents (path, 0750);
+  g_free (path);
+
+  path = g_build_filename (g_get_user_config_dir (),
+                           "gnome-builder",
+                           NULL);
+  g_mkdir_with_parents (path, 0750);
+  g_free (path);
+
+  path = g_build_filename (g_get_user_config_dir (),
+                           "gnome-builder",
+                           "snippets",
+                           NULL);
+  g_mkdir_with_parents (path, 0750);
+  g_free (path);
+}
+
+static void
+ide_application_register_theme_overrides (IdeApplication *application)
+{
+  g_autoptr(GSettings) settings = NULL;
+  g_autoptr(GtkCssProvider) provider = NULL;
+  GtkSettings *gtk_settings;
+  GdkScreen *screen;
+
+  IDE_ENTRY;
+
+  gtk_icon_theme_add_resource_path (gtk_icon_theme_get_default (), "/org/gnome/builder/icons/");
+
+  provider = ide_css_provider_new ();
+  screen = gdk_screen_get_default ();
+  gtk_style_context_add_provider_for_screen (screen, GTK_STYLE_PROVIDER (provider),
+                                             GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+
+  gtk_settings = gtk_settings_get_for_screen (screen);
+  settings = g_settings_new ("org.gnome.builder");
+  g_settings_bind (settings, "night-mode", gtk_settings, "gtk-application-prefer-dark-theme",
+                   G_SETTINGS_BIND_DEFAULT);
+
+  IDE_EXIT;
+}
+
+static void
+ide_application_load_keybindings (IdeApplication *self)
+{
+  g_autoptr(GSettings) settings = NULL;
+  g_autofree gchar *name = NULL;
+
+  /* TODO: Move this to keybindings */
+  static const struct { gchar *name; gchar *binding; } shared_bindings[] = {
+    { "workbench.show-left-pane", "F9" },
+    { "workbench.show-right-pane", "<shift>F9" },
+    { "workbench.show-bottom-pane", "<ctrl>F9" },
+    { "workbench.toggle-panels", "<ctrl><shift>F9" },
+    { "workbench.focus-left", "<ctrl>grave" },
+    { "workbench.focus-right", "<ctrl>9" },
+    { "workbench.focus-stack(1)", "<ctrl>1" },
+    { "workbench.focus-stack(2)", "<ctrl>2" },
+    { "workbench.focus-stack(3)", "<ctrl>3" },
+    { "workbench.focus-stack(4)", "<ctrl>4" },
+    { "workbench.focus-stack(5)", "<ctrl>5" },
+    { "workbench.show-gear-menu", "F10" },
+    { "workbench.global-search", "<ctrl>period" },
+    { "app.preferences", "<Primary>comma" },
+    { "app.shortcuts", "<ctrl>question" },
+    { "workbench.new-document", "<ctrl>n" },
+    { "workbench.open-document", "<ctrl>o" },
+    { NULL }
+  };
+  gsize i;
+
+  g_assert (IDE_IS_APPLICATION (self));
+
+  settings = g_settings_new ("org.gnome.builder.editor");
+  name = g_settings_get_string (settings, "keybindings");
+  self->keybindings = ide_keybindings_new (GTK_APPLICATION (self), name);
+  g_settings_bind (settings, "keybindings", self->keybindings, "mode", G_SETTINGS_BIND_GET);
+
+  for (i = 0; shared_bindings [i].name; i++)
+    {
+      const gchar *accels[2] = { shared_bindings [i].binding, NULL };
+      gtk_application_set_accels_for_action (GTK_APPLICATION (self),
+                                             shared_bindings [i].name,
+                                             accels);
+    }
+}
+
+static IdeWorkbench *
+ide_application_find_workbench_for_file (IdeApplication *self,
+                                        GFile         *file)
+{
+  GList *iter;
+  GList *workbenches;
+
+  g_assert (IDE_IS_APPLICATION (self));
+  g_assert (G_IS_FILE (file));
+
+  workbenches = gtk_application_get_windows (GTK_APPLICATION (self));
+
+  /*
+   * Find the a project that contains this file in its working directory.
+   */
+  for (iter = workbenches; iter; iter = iter->next)
+    {
+      if (IDE_IS_WORKBENCH (iter->data))
+        {
+          IdeWorkbench *workbench = iter->data;
+          g_autofree gchar *relpath = NULL;
+          IdeContext *context;
+          IdeVcs *vcs;
+          GFile *workdir;
+
+          context = ide_workbench_get_context (workbench);
+          vcs = ide_context_get_vcs (context);
+          workdir = ide_vcs_get_working_directory (vcs);
+
+          relpath = g_file_get_relative_path (workdir, file);
+
+          if (relpath != NULL)
+            return workbench;
+        }
+    }
+
+  /*
+   * No matches found, take the first workbench we find.
+   */
+  for (iter = workbenches; iter; iter = iter->next)
+    if (IDE_IS_WORKBENCH (iter->data))
+      return iter->data;
+
+  return NULL;
+}
+
+static void
+ide_application__context_new_cb (GObject      *object,
+                                GAsyncResult *result,
+                                gpointer      user_data)
+{
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(IdeContext) context = NULL;
+  IdeApplication *self;
+  IdeWorkbench *workbench;
+  GPtrArray *ar;
+  GError *error = NULL;
+  gsize i;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  ar = g_task_get_task_data (task);
+
+  g_assert (IDE_IS_APPLICATION (self));
+  g_assert (ar);
+
+  context = ide_context_new_finish (result, &error);
+
+  if (!context)
+    {
+      g_task_return_error (task, error);
+      goto cleanup;
+    }
+
+  {
+    IdeVcs *vcs;
+    GFile *workdir;
+    g_autofree gchar *path = NULL;
+
+    vcs = ide_context_get_vcs (context);
+    workdir = ide_vcs_get_working_directory (vcs);
+    path = g_file_get_path (workdir);
+
+    g_debug ("Project working directory: %s", path);
+  }
+
+  workbench = g_object_new (IDE_TYPE_WORKBENCH,
+                            "application", self,
+                            "context", context,
+                            NULL);
+
+  for (i = 0; i < ar->len; i++)
+    {
+      GFile *file;
+
+      file = g_ptr_array_index (ar, i);
+      g_assert (G_IS_FILE (file));
+
+      //ide_workbench_open (workbench, file);
+    }
+
+  gtk_window_present (GTK_WINDOW (workbench));
+
+  g_task_return_boolean (task, TRUE);
+
+cleanup:
+  g_application_unmark_busy (G_APPLICATION (self));
+  g_application_release (G_APPLICATION (self));
+}
+
+/**
+ * ide_application_open_project_async:
+ * @self: A #IdeApplication.
+ * @file: A #GFile.
+ * @additional_files: (element-type GFile) (nullable): A #GPtrArray of #GFile or %NULL.
+ *
+ */
+void
+ide_application_open_project_async (IdeApplication      *self,
+                                    GFile               *file,
+                                    GPtrArray           *additional_files,
+                                    GCancellable        *cancellable,
+                                    GAsyncReadyCallback  callback,
+                                    gpointer             user_data)
+{
+  g_autoptr(GFile) directory = NULL;
+  g_autoptr(GTask) task = NULL;
+  g_autoptr(GPtrArray) ar = NULL;
+  GList *windows;
+  GList *iter;
+
+  g_return_if_fail (IDE_IS_APPLICATION (self));
+  g_return_if_fail (G_IS_FILE (file));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+
+  windows = gtk_application_get_windows (GTK_APPLICATION (self));
+
+  for (iter = windows; iter; iter = iter->next)
+    {
+      if (IDE_IS_WORKBENCH (iter->data))
+        {
+          IdeContext *context;
+
+          context = ide_workbench_get_context (iter->data);
+
+          if (context != NULL)
+            {
+              GFile *project_file;
+
+              project_file = ide_context_get_project_file (context);
+
+              if (g_file_equal (file, project_file))
+                {
+                  gtk_window_present (iter->data);
+                  g_task_return_boolean (task, TRUE);
+                  return;
+                }
+            }
+        }
+    }
+
+  if (additional_files)
+    ar = g_ptr_array_ref (additional_files);
+  else
+    ar = g_ptr_array_new ();
+
+  g_task_set_task_data (task, g_ptr_array_ref (ar), (GDestroyNotify)g_ptr_array_unref);
+
+  if (g_file_query_file_type (file, 0, NULL) == G_FILE_TYPE_DIRECTORY)
+    directory = g_object_ref (file);
+  else
+    directory = g_file_get_parent (file);
+
+  g_application_mark_busy (G_APPLICATION (self));
+  g_application_hold (G_APPLICATION (self));
+
+  ide_context_new_async (directory,
+                         NULL,
+                         ide_application__context_new_cb,
+                         g_object_ref (task));
+}
+
+gboolean
+ide_application_open_project_finish (IdeApplication  *self,
+                                     GAsyncResult    *result,
+                                     GError         **error)
+{
+  GTask *task = (GTask *)result;
+
+  g_return_val_if_fail (IDE_IS_APPLICATION (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+  g_return_val_if_fail (G_IS_TASK (task), FALSE);
+
+  return g_task_propagate_boolean (task, error);
+}
+
+static void
+ide_application_open (GApplication  *application,
+                      GFile        **files,
+                      gint           n_files,
+                      const gchar   *hint)
+{
+  IdeApplication *self = (IdeApplication *)application;
+  IdeWorkbench *workbench;
+  g_autoptr(GPtrArray) ar = NULL;
+  guint i;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_APPLICATION (self));
+
+  /*
+   * Try to open the files using an existing workbench.
+   */
+  for (i = 0; i < n_files; i++)
+    {
+      GFile *file = files [i];
+
+      g_assert (G_IS_FILE (file));
+
+      workbench = ide_application_find_workbench_for_file (self, file);
+
+      if (workbench != NULL)
+        {
+          //ide_workbench_open (workbench, file);
+          gtk_window_present (GTK_WINDOW (workbench));
+          continue;
+        }
+
+      if (!ar)
+        ar = g_ptr_array_new_with_free_func (g_object_unref);
+      g_ptr_array_add (ar, g_object_ref (file));
+    }
+
+  /*
+   * No workbench found for these files, let's create one!
+   */
+  if (ar && ar->len)
+    {
+      GFile *file = g_ptr_array_index (ar, 0);
+
+      ide_application_open_project_async (self, file, ar, NULL, NULL, NULL);
+    }
+
+  IDE_EXIT;
+}
+
+void
+ide_application_show_projects_window (IdeApplication *self)
+{
+#if 0
+  IdeProjectsDialog *window;
+  GList *windows;
+
+  g_assert (IDE_IS_APPLICATION (self));
+
+  windows = gtk_application_get_windows (GTK_APPLICATION (self));
+
+  for (; windows; windows = windows->next)
+    {
+      if (IDE_IS_GREETER_WINDOW (windows->data))
+        {
+          gtk_window_present (windows->data);
+          return;
+        }
+    }
+
+  if (self->recent_projects == NULL)
+    {
+      self->recent_projects = ide_recent_projects_new ();
+      ide_recent_projects_discover_async (self->recent_projects, NULL, NULL, NULL);
+    }
+
+  window = g_object_new (IDE_TYPE_GREETER_WINDOW,
+                         "application", self,
+                         "recent-projects", self->recent_projects,
+                         NULL);
+  gtk_window_group_add_window (self->greeter_group, GTK_WINDOW (window));
+  gtk_window_present (GTK_WINDOW (window));
+#endif
+}
+
+static void
+ide_application_activate (GApplication *application)
+{
+  IdeApplication *self = (IdeApplication *)application;
+  IdeWorkbench *workbench;
+  GList *list;
+
+  g_assert (IDE_IS_APPLICATION (self));
+
+  if (ide_application_is_worker (self))
+    {
+      ide_application_load_worker (self);
+      return;
+    }
+
+  list = gtk_application_get_windows (GTK_APPLICATION (application));
+
+  for (; list; list = list->next)
+    {
+      if (IDE_IS_WORKBENCH (list->data))
+        {
+          gtk_window_present (GTK_WINDOW (list->data));
+          return;
+        }
+    }
+
+  workbench = g_object_new (IDE_TYPE_WORKBENCH,
+                            "application", self,
+                            NULL);
+  gtk_window_maximize (GTK_WINDOW (workbench));
+  gtk_window_present (GTK_WINDOW (workbench));
+}
+
+static void
+ide_application__extension_added (PeasExtensionSet    *extensions,
+                                  PeasPluginInfo      *plugin_info,
+                                  IdeApplicationAddin *addin,
+                                  IdeApplication      *self)
+{
+  g_assert (IDE_IS_APPLICATION (self));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_APPLICATION_ADDIN (addin));
+  g_assert (PEAS_IS_EXTENSION_SET (extensions));
+
+  ide_application_addin_load (addin, self);
+}
+
+static void
+ide_application__extension_removed (PeasExtensionSet    *extensions,
+                                    PeasPluginInfo      *plugin_info,
+                                    IdeApplicationAddin *addin,
+                                    IdeApplication      *self)
+{
+  g_assert (IDE_IS_APPLICATION (self));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_APPLICATION_ADDIN (addin));
+  g_assert (PEAS_IS_EXTENSION_SET (extensions));
+
+  ide_application_addin_unload (addin, self);
+}
+
+static void
+ide_application_load_addins (IdeApplication *self)
+{
+  PeasEngine *engine;
+
+  g_assert (IDE_IS_APPLICATION (self));
+
+  engine = peas_engine_get_default ();
+
+  self->extensions = peas_extension_set_new (engine, IDE_TYPE_APPLICATION_ADDIN, NULL);
+
+  peas_extension_set_foreach (self->extensions,
+                              (PeasExtensionSetForeachFunc)ide_application__extension_added,
+                              self);
+
+  g_signal_connect_object (self->extensions,
+                           "extension-added",
+                           G_CALLBACK (ide_application__extension_added),
+                           self,
+                           0);
+
+  g_signal_connect_object (self->extensions,
+                           "extension-removed",
+                           G_CALLBACK (ide_application__extension_removed),
+                           self,
+                           0);
+}
+
+static void
+ide_application_startup (GApplication *app)
+{
+  IdeApplication *self = (IdeApplication *)app;
+  GgitFeatureFlags ggit_flags;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_APPLICATION (self));
+
+  self->startup_time = g_date_time_new_now_utc ();
+
+  g_resources_register (ide_get_resource ());
+
+  g_application_set_resource_base_path (app, "/org/gnome/builder");
+
+  g_irepository_prepend_search_path (PACKAGE_LIBDIR"/gnome-builder/girepository-1.0");
+
+  if (!ide_application_is_worker (self))
+    self->greeter_group = gtk_window_group_new ();
+
+  _ide_battery_monitor_init ();
+  _ide_thread_pool_init (ide_application_is_worker (self));
+
+  modeline_parser_init ();
+
+  ggit_init ();
+
+  ggit_flags = ggit_get_features ();
+
+  if ((ggit_flags & GGIT_FEATURE_THREADS) == 0)
+    {
+      g_error (_("Builder requires libgit2-glib with threading support."));
+      exit (EXIT_FAILURE);
+    }
+
+  if ((ggit_flags & GGIT_FEATURE_SSH) == 0)
+    {
+      g_error (_("Builder requires libgit2-glib with SSH support."));
+      exit (EXIT_FAILURE);
+    }
+
+  G_APPLICATION_CLASS (ide_application_parent_class)->startup (app);
+
+  if (!ide_application_is_worker (self))
+    {
+      ide_application_make_skeleton_dirs (self);
+      ide_application_actions_init (self);
+      ide_application_register_theme_overrides (self);
+      ide_application_setup_search_paths ();
+      ide_application_load_keybindings (self);
+      ide_application_load_plugins (self);
+      ide_application_load_addins (self);
+    }
+
+  IDE_EXIT;
+}
+
+static gboolean
+ide_application_increase_verbosity (void)
+{
+  ide_log_increase_verbosity ();
+  return TRUE;
+}
+
+static gint
+ide_application_handle_local_options (GApplication *app,
+                                      GVariantDict *options)
+{
+  if (g_variant_dict_contains (options, "version"))
+    {
+      g_print ("%s - Version %s\n", g_get_application_name (), VERSION);
+      return 0;
+    }
+
+   if (g_variant_dict_contains (options, "standalone") || g_variant_dict_contains (options, "type"))
+    {
+      GApplicationFlags flags;
+
+      flags = g_application_get_flags (app);
+      g_application_set_flags (app, flags | G_APPLICATION_NON_UNIQUE);
+    }
+
+  return -1;
+}
+
+static gboolean
+ide_application_local_command_line (GApplication   *application,
+                                    gchar        ***arguments,
+                                    int            *exit_status)
+{
+  IdeApplication *self = (IdeApplication *)application;
+
+  g_assert (IDE_IS_APPLICATION (self));
+  g_assert (arguments != NULL);
+  g_assert (*arguments != NULL);
+  g_assert (exit_status != NULL);
+
+  self->argv0 = g_strdup ((*arguments) [0]);
+
+  return G_APPLICATION_CLASS (ide_application_parent_class)->
+    local_command_line (application, arguments, exit_status);
+}
+
+static void
+ide_application_finalize (GObject *object)
+{
+  IdeApplication *self = (IdeApplication *)object;
+
+  IDE_ENTRY;
+
+  g_clear_object (&self->extensions);
+  g_clear_pointer (&self->startup_time, g_date_time_unref);
+  g_clear_pointer (&self->argv0, g_free);
+  g_clear_object (&self->keybindings);
+  g_clear_object (&self->recent_projects);
+  g_clear_object (&self->greeter_group);
+
+  G_OBJECT_CLASS (ide_application_parent_class)->finalize (object);
+
+  IDE_EXIT;
+}
+
+static void
+ide_application_class_init (IdeApplicationClass *klass)
+{
+  GApplicationClass *app_class = G_APPLICATION_CLASS (klass);
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  IDE_ENTRY;
+
+  object_class->finalize = ide_application_finalize;
+
+  app_class->activate = ide_application_activate;
+  app_class->startup = ide_application_startup;
+  app_class->open = ide_application_open;
+  app_class->local_command_line = ide_application_local_command_line;
+  app_class->handle_local_options = ide_application_handle_local_options;
+
+  IDE_EXIT;
+}
+
+static void
+ide_application_init (IdeApplication *app)
+{
+  GOptionEntry options[] = {
+    { "standalone",
+      's',
+      G_OPTION_FLAG_IN_MAIN,
+      G_OPTION_ARG_NONE,
+      NULL,
+      N_("Run Builder in standalone mode") },
+
+    { "version",
+      0,
+      G_OPTION_FLAG_IN_MAIN,
+      G_OPTION_ARG_NONE,
+      NULL,
+      N_("Show the application's version") },
+
+    { "verbose",
+      'v',
+      G_OPTION_FLAG_NO_ARG | G_OPTION_FLAG_IN_MAIN | G_OPTION_FLAG_HIDDEN,
+      G_OPTION_ARG_CALLBACK,
+      ide_application_increase_verbosity,
+      N_("Increase verbosity. May be specified multiple times.") },
+
+    { "dbus-address",
+      0,
+      G_OPTION_FLAG_HIDDEN,
+      G_OPTION_ARG_STRING,
+      &app->dbus_address,
+      N_("The DBus server address for which to connect.") },
+
+    { "type",
+      0,
+      G_OPTION_FLAG_HIDDEN,
+      G_OPTION_ARG_STRING,
+      &app->type,
+      N_("The type of plugin worker process to run.") },
+
+    { NULL }
+  };
+
+  IDE_ENTRY;
+
+  g_application_add_main_option_entries (G_APPLICATION (app), options);
+
+  IDE_EXIT;
+}
+
+GDateTime *
+ide_application_get_startup_time (IdeApplication *self)
+{
+  g_return_val_if_fail (IDE_IS_APPLICATION (self), NULL);
+
+  return self->startup_time;
+}
+
+const gchar *
+ide_application_get_keybindings_mode (IdeApplication *self)
+{
+  g_return_val_if_fail (IDE_IS_APPLICATION (self), NULL);
+
+  return ide_keybindings_get_mode (self->keybindings);
+}
+
+static void
+ide_application_get_worker_cb (GObject      *object,
+                               GAsyncResult *result,
+                               gpointer      user_data)
+{
+  IdeWorkerManager *worker_manager = (IdeWorkerManager *)object;
+  g_autoptr(GTask) task = user_data;
+  GError *error = NULL;
+  GDBusProxy *proxy;
+
+  g_assert (IDE_IS_WORKER_MANAGER (worker_manager));
+
+  proxy = ide_worker_manager_get_worker_finish (worker_manager, result, &error);
+
+  if (proxy == NULL)
+    g_task_return_error (task, error);
+  else
+    g_task_return_pointer (task, proxy, g_object_unref);
+}
+
+/**
+ * ide_application_get_worker_async:
+ * @self: A #IdeApplication
+ * @plugin_name: The name of the plugin.
+ * @cancellable: (allow-none): A #GCancellable or %NULL.
+ * @callback: A #GAsyncReadyCallback or %NULL.
+ * @user_data: user data for @callback.
+ *
+ * Asynchronously requests a #GDBusProxy to a service provided in a worker
+ * process. The worker should be an #IdeWorker implemented by the plugin named
+ * @plugin_name. The #IdeWorker is responsible for created both the service
+ * registered on the bus and the proxy to it.
+ *
+ * The #IdeApplication is responsible for spawning a subprocess for the worker.
+ *
+ * @callback should call ide_application_get_worker_finish() with the result
+ * provided to retrieve the result.
+ */
+void
+ide_application_get_worker_async (IdeApplication      *self,
+                                  const gchar         *plugin_name,
+                                  GCancellable        *cancellable,
+                                  GAsyncReadyCallback  callback,
+                                  gpointer             user_data)
+{
+  GTask *task = NULL;
+
+  g_return_if_fail (IDE_IS_APPLICATION (self));
+  g_return_if_fail (plugin_name != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  if (self->worker_manager == NULL)
+    self->worker_manager = ide_worker_manager_new (self->argv0);
+
+  task = g_task_new (self, cancellable, callback, user_data);
+
+  ide_worker_manager_get_worker_async (self->worker_manager,
+                                       plugin_name,
+                                       cancellable,
+                                       ide_application_get_worker_cb,
+                                       task);
+}
+
+/**
+ * ide_application_get_worker_finish:
+ * @self: A #IdeApplication.
+ * @result: A #GAsyncResult
+ * @error: a location for a #GError, or %NULL.
+ *
+ * Completes an asynchronous request to get a proxy to a worker process.
+ *
+ * Returns: (transfer full): A #GDBusProxy or %NULL.
+ */
+GDBusProxy *
+ide_application_get_worker_finish (IdeApplication  *self,
+                                   GAsyncResult    *result,
+                                   GError         **error)
+{
+  GTask *task = (GTask *)result;
+
+  g_return_val_if_fail (IDE_IS_APPLICATION (self), NULL);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), NULL);
+  g_return_val_if_fail (G_IS_TASK (task), NULL);
+
+  return g_task_propagate_pointer (task, error);
+}
+
+/**
+ * ide_application_get_recent_projects:
+ * @self: An #IdeApplication.
+ *
+ * This method will retreive an #IdeRecentProjects for the application that
+ * represents recent and discover projects on the system. The first time
+ * the #IdeRecentProjects is loaded, discovery of projects will occur. There
+ * is no need to call ide_recent_projects_discover_async().
+ *
+ * If you would like to display a spinner while discovery is in process, simply
+ * connect to the #IdeRecentProjects:busy: property notification.
+ *
+ * Returns: (transfer none): An #IdeRecentProjects.
+ */
+IdeRecentProjects *
+ide_application_get_recent_projects (IdeApplication *self)
+{
+  g_return_val_if_fail (IDE_IS_APPLICATION (self), NULL);
+
+  if (self->recent_projects == NULL)
+    {
+      self->recent_projects = ide_recent_projects_new ();
+      ide_recent_projects_discover_async (self->recent_projects, NULL, NULL, NULL);
+    }
+
+  return self->recent_projects;
+}
diff --git a/libide/ide-application.h b/libide/ide-application.h
new file mode 100644
index 0000000..ba2282c
--- /dev/null
+++ b/libide/ide-application.h
@@ -0,0 +1,57 @@
+/* ide-application.h
+ *
+ * Copyright (C) 2014 Christian Hergert <christian hergert me>
+ *
+ * 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/>.
+ */
+
+#ifndef IDE_APPLICATION_H
+#define IDE_APPLICATION_H
+
+#include <gtk/gtk.h>
+
+#include "ide-recent-projects.h"
+
+G_BEGIN_DECLS
+
+#define IDE_APPLICATION_DEFAULT (IDE_APPLICATION(g_application_get_default()))
+#define IDE_TYPE_APPLICATION    (ide_application_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeApplication, ide_application, IDE, APPLICATION, GtkApplication)
+
+const gchar       *ide_application_get_keybindings_mode (IdeApplication        *self);
+GDateTime         *ide_application_get_startup_time     (IdeApplication        *self);
+IdeRecentProjects *ide_application_get_recent_projects (IdeApplication *self);
+void               ide_application_show_projects_window (IdeApplication        *self);
+void               ide_application_open_project_async   (IdeApplication        *self,
+                                                        GFile                *file,
+                                                        GPtrArray            *additional_files,
+                                                        GCancellable         *cancellable,
+                                                        GAsyncReadyCallback   callback,
+                                                        gpointer              user_data);
+gboolean           ide_application_open_project_finish  (IdeApplication        *self,
+                                                        GAsyncResult         *result,
+                                                        GError              **error);
+void               ide_application_get_worker_async     (IdeApplication        *self,
+                                                        const gchar          *plugin_name,
+                                                        GCancellable         *cancellable,
+                                                        GAsyncReadyCallback   callback,
+                                                        gpointer              user_data);
+GDBusProxy        *ide_application_get_worker_finish    (IdeApplication        *self,
+                                                        GAsyncResult         *result,
+                                                        GError              **error);
+
+G_END_DECLS
+
+#endif /* IDE_APPLICATION_H */
diff --git a/libide/ide-css-provider.c b/libide/ide-css-provider.c
new file mode 100644
index 0000000..ccf1857
--- /dev/null
+++ b/libide/ide-css-provider.c
@@ -0,0 +1,176 @@
+/* ide-css-provider.c
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * 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/>.
+ */
+
+#define G_LOG_DOMAIN "ide-css-provider"
+
+#include <glib/gi18n.h>
+#include <ide.h>
+
+#include "ide-css-provider.h"
+
+struct _IdeCssProvider
+{
+  GtkCssProvider parent_instance;
+
+  GtkSettings *settings;
+  gulong       notify_gtk_theme_name_handler;
+};
+
+G_DEFINE_TYPE (IdeCssProvider, ide_css_provider, GTK_TYPE_CSS_PROVIDER)
+
+GtkCssProvider *
+ide_css_provider_new (void)
+{
+  return g_object_new (IDE_TYPE_CSS_PROVIDER, NULL);
+}
+
+static void
+ide_css_provider_update (IdeCssProvider *self)
+{
+  g_autofree gchar *theme_name = NULL;
+  g_autofree gchar *resource_path = NULL;
+  gboolean prefer_dark_theme = FALSE;
+  gsize len = 0;
+  guint32 flags = 0;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_CSS_PROVIDER (self));
+  g_assert (GTK_IS_SETTINGS (self->settings));
+
+  g_object_get (self->settings,
+                "gtk-theme-name", &theme_name,
+                "gtk-application-prefer-dark-theme", &prefer_dark_theme,
+                NULL);
+
+  resource_path = g_strdup_printf ("/org/gnome/builder/theme/%s%s.css",
+                                   theme_name,
+                                   prefer_dark_theme ? "-dark" : "");
+
+  if (!g_resources_get_info (resource_path, G_RESOURCE_LOOKUP_FLAGS_NONE, &len, &flags, NULL))
+    {
+      g_free (resource_path);
+      resource_path = g_strdup ("/org/gnome/builder/theme/shared.css");
+    }
+
+  gtk_css_provider_load_from_resource (GTK_CSS_PROVIDER (self), resource_path);
+
+  IDE_EXIT;
+}
+
+static void
+ide_css_provider__settings_notify_gtk_theme_name (IdeCssProvider *self,
+                                                 GParamSpec    *pspec,
+                                                 GtkSettings   *settings)
+{
+  g_assert (IDE_IS_CSS_PROVIDER (self));
+
+  ide_css_provider_update (self);
+}
+
+static void
+ide_css_provider__settings_notify_gtk_application_prefer_dark_theme (IdeCssProvider *self,
+                                                                    GParamSpec    *pspec,
+                                                                    GtkSettings   *settings)
+{
+  g_assert (IDE_IS_CSS_PROVIDER (self));
+
+  ide_css_provider_update (self);
+}
+
+static void
+ide_css_provider_parsing_error (GtkCssProvider *provider,
+                               GtkCssSection  *section,
+                               const GError   *error)
+{
+  g_autofree gchar *uri = NULL;
+  GFile *file;
+  guint line = 0;
+  guint line_offset = 0;
+
+  g_assert (IDE_IS_CSS_PROVIDER (provider));
+  g_assert (error != NULL);
+
+  if (section != NULL)
+    {
+      file = gtk_css_section_get_file (section);
+      uri = g_file_get_uri (file);
+      line = gtk_css_section_get_start_line (section);
+      line_offset = gtk_css_section_get_start_position (section);
+      g_warning ("Parsing Error: %s @ %u:%u: %s", uri, line, line_offset, error->message);
+    }
+  else
+    {
+      g_warning ("%s", error->message);
+    }
+}
+
+static void
+ide_css_provider_constructed (GObject *object)
+{
+  IdeCssProvider *self = (IdeCssProvider *)object;
+
+  G_OBJECT_CLASS (ide_css_provider_parent_class)->constructed (object);
+
+  self->settings = g_object_ref (gtk_settings_get_default ());
+
+  self->notify_gtk_theme_name_handler =
+    g_signal_connect_object (self->settings,
+                             "notify::gtk-theme-name",
+                             G_CALLBACK (ide_css_provider__settings_notify_gtk_theme_name),
+                             self,
+                             G_CONNECT_SWAPPED);
+
+  self->notify_gtk_theme_name_handler =
+    g_signal_connect_object (
+      self->settings,
+      "notify::gtk-application-prefer-dark-theme",
+      G_CALLBACK (ide_css_provider__settings_notify_gtk_application_prefer_dark_theme),
+      self,
+      G_CONNECT_SWAPPED);
+
+  ide_css_provider_update (self);
+}
+
+static void
+ide_css_provider_finalize (GObject *object)
+{
+  IdeCssProvider *self = (IdeCssProvider *)object;
+
+  ide_clear_signal_handler (self->settings, &self->notify_gtk_theme_name_handler);
+  g_clear_object (&self->settings);
+
+  G_OBJECT_CLASS (ide_css_provider_parent_class)->finalize (object);
+}
+
+static void
+ide_css_provider_class_init (IdeCssProviderClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkCssProviderClass *provider_class = GTK_CSS_PROVIDER_CLASS (klass);
+
+  object_class->finalize = ide_css_provider_finalize;
+  object_class->constructed = ide_css_provider_constructed;
+
+  provider_class->parsing_error = ide_css_provider_parsing_error;
+}
+
+static void
+ide_css_provider_init (IdeCssProvider *self)
+{
+}
diff --git a/src/greeter/gb-greeter-pill-box.h b/libide/ide-css-provider.h
similarity index 58%
copy from src/greeter/gb-greeter-pill-box.h
copy to libide/ide-css-provider.h
index 394a333..cd36e30 100644
--- a/src/greeter/gb-greeter-pill-box.h
+++ b/libide/ide-css-provider.h
@@ -1,4 +1,4 @@
-/* gb-greeter-pill-box.h
+/* ide-css-provider.h
  *
  * Copyright (C) 2015 Christian Hergert <christian hergert me>
  *
@@ -16,22 +16,19 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#ifndef GB_GREETER_PILL_BOX_H
-#define GB_GREETER_PILL_BOX_H
+#ifndef IDE_CSS_PROVIDER_H
+#define IDE_CSS_PROVIDER_H
 
 #include <gtk/gtk.h>
 
 G_BEGIN_DECLS
 
-#define GB_TYPE_GREETER_PILL_BOX (gb_greeter_pill_box_get_type())
+#define IDE_TYPE_CSS_PROVIDER (ide_css_provider_get_type())
 
-G_DECLARE_FINAL_TYPE (GbGreeterPillBox, gb_greeter_pill_box, GB, GREETER_PILL_BOX, GtkEventBox)
+G_DECLARE_FINAL_TYPE (IdeCssProvider, ide_css_provider, IDE, CSS_PROVIDER, GtkCssProvider)
 
-GtkWidget   *gb_greeter_pill_box_new       (const gchar      *label);
-const gchar *gb_greeter_pill_box_get_label (GbGreeterPillBox *self);
-void         gb_greeter_pill_box_set_label (GbGreeterPillBox *self,
-                                            const gchar      *label);
+GtkCssProvider *ide_css_provider_new (void);
 
 G_END_DECLS
 
-#endif /* GB_GREETER_PILL_BOX_H */
+#endif /* IDE_CSS_PROVIDER_H */
diff --git a/libide/ide-internal.h b/libide/ide-internal.h
index 54e9d89..474283f 100644
--- a/libide/ide-internal.h
+++ b/libide/ide-internal.h
@@ -96,7 +96,7 @@ void                _ide_source_view_set_count              (IdeSourceView
                                                              gint                   count);
 void                _ide_source_view_set_modifier           (IdeSourceView         *self,
                                                              gunichar               modifier);
-void                _ide_thread_pool_init                   (void);
+void                _ide_thread_pool_init                   (gboolean               is_worker);
 IdeUnsavedFile     *_ide_unsaved_file_new                   (GFile                 *file,
                                                              GBytes                *content,
                                                              const gchar           *temp_path,
diff --git a/libide/ide-keybindings.c b/libide/ide-keybindings.c
new file mode 100644
index 0000000..37b2b32
--- /dev/null
+++ b/libide/ide-keybindings.c
@@ -0,0 +1,282 @@
+/* ide-keybindings.c
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * 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/>.
+ */
+
+#define G_LOG_DOMAIN "ide-keybindings"
+
+#include <glib/gi18n.h>
+#include <ide.h>
+
+#include "ide-keybindings.h"
+
+struct _IdeKeybindings
+{
+  GObject         parent_instance;
+
+  GtkApplication *application;
+  GtkCssProvider *css_provider;
+  gchar          *mode;
+  guint           constructed : 1;
+};
+
+enum
+{
+  PROP_0,
+  PROP_APPLICATION,
+  PROP_MODE,
+  LAST_PROP
+};
+
+G_DEFINE_TYPE (IdeKeybindings, ide_keybindings, G_TYPE_OBJECT)
+
+static GParamSpec *properties [LAST_PROP];
+
+IdeKeybindings *
+ide_keybindings_new (GtkApplication *application,
+                    const gchar    *mode)
+{
+  g_return_val_if_fail (GTK_IS_APPLICATION (application), NULL);
+
+  return g_object_new (IDE_TYPE_KEYBINDINGS,
+                       "application", application,
+                       "mode", mode,
+                       NULL);
+}
+
+static void
+ide_keybindings_reload (IdeKeybindings *self)
+{
+  const gchar *mode;
+  g_autofree gchar *path = NULL;
+  g_autoptr(GBytes) bytes = NULL;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_KEYBINDINGS (self));
+
+  mode = self->mode ? self->mode : "default";
+  IDE_TRACE_MSG ("Loading %s keybindings", mode);
+  path = g_strdup_printf ("/org/gnome/builder/keybindings/%s.css", mode);
+  bytes = g_resources_lookup_data (path, G_RESOURCE_LOOKUP_FLAGS_NONE, &error);
+
+  if (error == NULL)
+    gtk_css_provider_load_from_data (self->css_provider,
+                                     g_bytes_get_data (bytes, NULL),
+                                     g_bytes_get_size (bytes),
+                                     &error);
+
+  if (error)
+    g_warning ("%s", error->message);
+
+  IDE_EXIT;
+}
+
+const gchar *
+ide_keybindings_get_mode (IdeKeybindings *self)
+{
+  g_return_val_if_fail (IDE_IS_KEYBINDINGS (self), NULL);
+
+  return self->mode;
+}
+
+void
+ide_keybindings_set_mode (IdeKeybindings *self,
+                         const gchar   *mode)
+{
+  g_return_if_fail (IDE_IS_KEYBINDINGS (self));
+
+  if (mode != self->mode)
+    {
+      g_free (self->mode);
+      self->mode = g_strdup (mode);
+      if (self->constructed)
+        ide_keybindings_reload (self);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MODE]);
+    }
+}
+
+GtkApplication *
+ide_keybindings_get_application (IdeKeybindings *self)
+{
+  g_return_val_if_fail (IDE_IS_KEYBINDINGS (self), NULL);
+
+  return self->application;
+}
+
+static void
+ide_keybindings_set_application (IdeKeybindings  *self,
+                                GtkApplication *application)
+{
+  g_assert (IDE_IS_KEYBINDINGS (self));
+  g_assert (!application || GTK_IS_APPLICATION (application));
+
+  if (application != self->application)
+    {
+      if (self->application)
+        {
+          /* remove keybindings */
+          g_clear_object (&self->application);
+        }
+
+      if (application)
+        {
+          /* connect keybindings */
+          self->application = g_object_ref (application);
+        }
+    }
+}
+
+static void
+ide_keybindings_parsing_error (GtkCssProvider *css_provider,
+                              GtkCssSection  *section,
+                              GError         *error,
+                              gpointer        user_data)
+{
+  g_autofree gchar *filename = NULL;
+  GFile *file;
+  guint start_line;
+  guint end_line;
+
+  file = gtk_css_section_get_file (section);
+  filename = g_file_get_uri (file);
+  start_line = gtk_css_section_get_start_line (section);
+  end_line = gtk_css_section_get_end_line (section);
+
+  g_warning ("CSS parsing error in %s between lines %u and %u", filename, start_line, end_line);
+}
+
+static void
+ide_keybindings_constructed (GObject *object)
+{
+  IdeKeybindings *self = (IdeKeybindings *)object;
+  GdkScreen *screen;
+
+  IDE_ENTRY;
+
+  G_OBJECT_CLASS (ide_keybindings_parent_class)->constructed (object);
+
+  screen = gdk_screen_get_default ();
+  gtk_style_context_add_provider_for_screen (screen, GTK_STYLE_PROVIDER (self->css_provider),
+                                             GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+
+  self->constructed = TRUE;
+
+  ide_keybindings_reload (self);
+
+  IDE_EXIT;
+}
+
+static void
+ide_keybindings_finalize (GObject *object)
+{
+  IdeKeybindings *self = (IdeKeybindings *)object;
+
+  IDE_ENTRY;
+
+  g_clear_object (&self->application);
+  g_clear_object (&self->css_provider);
+  g_clear_pointer (&self->mode, g_free);
+
+  G_OBJECT_CLASS (ide_keybindings_parent_class)->finalize (object);
+
+  IDE_EXIT;
+}
+
+static void
+ide_keybindings_get_property (GObject    *object,
+                             guint       prop_id,
+                             GValue     *value,
+                             GParamSpec *pspec)
+{
+  IdeKeybindings *self = IDE_KEYBINDINGS (object);
+
+  switch (prop_id)
+    {
+    case PROP_APPLICATION:
+      g_value_set_object (value, ide_keybindings_get_application (self));
+      break;
+
+    case PROP_MODE:
+      g_value_set_string (value, ide_keybindings_get_mode (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_keybindings_set_property (GObject      *object,
+                             guint         prop_id,
+                             const GValue *value,
+                             GParamSpec   *pspec)
+{
+  IdeKeybindings *self = IDE_KEYBINDINGS (object);
+
+  switch (prop_id)
+    {
+    case PROP_APPLICATION:
+      ide_keybindings_set_application (self, g_value_get_object (value));
+      break;
+
+    case PROP_MODE:
+      ide_keybindings_set_mode (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_keybindings_class_init (IdeKeybindingsClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->constructed = ide_keybindings_constructed;
+  object_class->finalize = ide_keybindings_finalize;
+  object_class->get_property = ide_keybindings_get_property;
+  object_class->set_property = ide_keybindings_set_property;
+
+  properties [PROP_APPLICATION] =
+    g_param_spec_object ("application",
+                         "Application",
+                         "The application to register keybindings for.",
+                         GTK_TYPE_APPLICATION,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_MODE] =
+    g_param_spec_string ("mode",
+                         "Mode",
+                         "The name of the keybindings mode.",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+}
+
+static void
+ide_keybindings_init (IdeKeybindings *self)
+{
+  self->css_provider = gtk_css_provider_new ();
+
+  g_signal_connect (self->css_provider,
+                    "parsing-error",
+                    G_CALLBACK (ide_keybindings_parsing_error),
+                    NULL);
+}
diff --git a/libide/ide-keybindings.h b/libide/ide-keybindings.h
new file mode 100644
index 0000000..a1196b6
--- /dev/null
+++ b/libide/ide-keybindings.h
@@ -0,0 +1,39 @@
+/* ide-keybindings.h
+ *
+ * Copyright (C) 2014-2015 Christian Hergert <christian hergert me>
+ *
+ * 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/>.
+ */
+
+#ifndef IDE_KEYBINDINGS_H
+#define IDE_KEYBINDINGS_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_KEYBINDINGS (ide_keybindings_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeKeybindings, ide_keybindings, IDE, KEYBINDINGS, GObject)
+
+IdeKeybindings  *ide_keybindings_new             (GtkApplication *application,
+                                                const gchar    *mode);
+GtkApplication *ide_keybindings_get_application (IdeKeybindings *self);
+const gchar    *ide_keybindings_get_mode        (IdeKeybindings  *self);
+void            ide_keybindings_set_mode        (IdeKeybindings  *self,
+                                                const gchar    *name);
+
+G_END_DECLS
+
+#endif /* IDE_KEYBINDINGS_H */
diff --git a/libide/ide-layout-manager.c b/libide/ide-layout-manager.c
new file mode 100644
index 0000000..ff37280
--- /dev/null
+++ b/libide/ide-layout-manager.c
@@ -0,0 +1,72 @@
+/* ide-layout-manager.c
+ *
+ * Copyright (C) 2015 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/>.
+ */
+
+#include "ide-layout-manager.h"
+
+G_DEFINE_INTERFACE (IdeLayoutManager, ide_layout_manager, G_TYPE_OBJECT)
+
+static void
+ide_layout_manager_default_init (IdeLayoutManagerInterface *iface)
+{
+}
+
+/**
+ * ide_layout_manager_add:
+ * @self: An #IdeLayoutManager.
+ * @hints: (nullable): optional hints for the layout.
+ * @child: A #GtkWidget.
+ *
+ * Adds @child to the layout, optionally hinting to the layout manager about where to
+ * place the child.
+ *
+ * Returns: A layout specific identifier greater than zero, which should be passed to
+ *   ide_layout_manager_remove() to remove the widget from the layout.
+ */
+guint
+ide_layout_manager_add (IdeLayoutManager     *self,
+                        const IdeLayoutHints *hints,
+                        GtkWidget            *child)
+{
+  guint ret;
+
+  g_return_val_if_fail (IDE_IS_LAYOUT_MANAGER (self), 0);
+  g_return_val_if_fail (GTK_IS_WIDGET (child), 0);
+
+  ret = IDE_LAYOUT_MANAGER_GET_IFACE (self)->add (self, hints, child);
+
+  g_return_val_if_fail (ret > 0, 0);
+
+  return ret;
+}
+
+/**
+ * ide_layout_manager_remove:
+ * @self: An #IdeLayoutManager.
+ * @layout_id: The layout id from ide_layout_manager_add()
+ *
+ * Removes a widget from the layout that was generated by calling * ide_layout_manager_add().
+ */
+void
+ide_layout_manager_remove (IdeLayoutManager *self,
+                           guint             layout_id)
+{
+  g_return_if_fail (IDE_IS_LAYOUT_MANAGER (self));
+  g_return_if_fail (layout_id > 0);
+
+  IDE_LAYOUT_MANAGER_GET_IFACE (self)->remove (self, layout_id);
+}
diff --git a/libide/ide-layout-manager.h b/libide/ide-layout-manager.h
new file mode 100644
index 0000000..3372138
--- /dev/null
+++ b/libide/ide-layout-manager.h
@@ -0,0 +1,60 @@
+/* ide-layout-manager.h
+ *
+ * Copyright (C) 2015 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/>.
+ */
+
+#ifndef IDE_LAYOUT_MANAGER_H
+#define IDE_LAYOUT_MANAGER_H
+
+#include <gtk/gtk.h>
+
+#include "ide-view.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_LAYOUT_MANAGER (ide_layout_manager_get_type())
+
+G_DECLARE_INTERFACE (IdeLayoutManager, ide_layout_manager, IDE, LAYOUT_MANAGER, GObject)
+
+typedef struct
+{
+  GtkWidget *left_of;
+  GtkWidget *right_of;
+  GtkWidget *above;
+  GtkWidget *below;
+  gint       column;
+} IdeLayoutHints;
+
+struct _IdeLayoutManagerInterface
+{
+  GTypeInterface parent_instance;
+
+  guint (*add)    (IdeLayoutManager     *self,
+                   const IdeLayoutHints *hints,
+                   GtkWidget            *child);
+  void  (*remove) (IdeLayoutManager     *self,
+                   guint                 layout_id);
+};
+
+guint ide_layout_manager_add    (IdeLayoutManager     *self,
+                                 const IdeLayoutHints *hints,
+                                 GtkWidget            *child);
+void  ide_layout_manager_remove (IdeLayoutManager     *self,
+                                 guint                 layout_id);
+
+G_END_DECLS
+
+#endif /* IDE_LAYOUT_MANAGER_H */
diff --git a/libide/ide-perspective.c b/libide/ide-perspective.c
new file mode 100644
index 0000000..3a322b7
--- /dev/null
+++ b/libide/ide-perspective.c
@@ -0,0 +1,263 @@
+/* ide-perspective.c
+ *
+ * Copyright (C) 2015 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/>.
+ */
+
+#include "ide-perspective.h"
+
+G_DEFINE_INTERFACE (IdePerspective, ide_perspective, G_TYPE_OBJECT)
+
+static gboolean
+ide_perspective_real_agree_to_shutdown (IdePerspective *self)
+{
+  return TRUE;
+}
+
+static GActionGroup *
+ide_perspective_real_get_actions (IdePerspective *self)
+{
+  return NULL;
+}
+
+static const gchar *
+ide_perspective_real_get_icon_name (IdePerspective *self)
+{
+  return NULL;
+}
+
+static const gchar *
+ide_perspective_real_get_id (IdePerspective *self)
+{
+  return G_OBJECT_TYPE_NAME (self);
+}
+
+static gboolean
+ide_perspective_real_get_needs_attention (IdePerspective *self)
+{
+  return FALSE;
+}
+
+static const gchar *
+ide_perspective_real_get_title (IdePerspective *self)
+{
+  return NULL;
+}
+
+static GtkWidget *
+ide_perspective_real_get_titlebar (IdePerspective *self)
+{
+  return NULL;
+}
+
+static void
+ide_perspective_real_set_fullscreen (IdePerspective *self,
+                                     gboolean        fullscreen)
+{
+}
+
+static void
+ide_perspective_real_views_foreach (IdePerspective *self,
+                                    GtkCallback     callback,
+                                    gpointer        user_data)
+{
+}
+
+static void
+ide_perspective_default_init (IdePerspectiveInterface *iface)
+{
+  iface->agree_to_shutdown = ide_perspective_real_agree_to_shutdown;
+  iface->get_actions = ide_perspective_real_get_actions;
+  iface->get_icon_name = ide_perspective_real_get_icon_name;
+  iface->get_id = ide_perspective_real_get_id;
+  iface->get_needs_attention = ide_perspective_real_get_needs_attention;
+  iface->get_title = ide_perspective_real_get_title;
+  iface->get_titlebar = ide_perspective_real_get_titlebar;
+  iface->set_fullscreen = ide_perspective_real_set_fullscreen;
+  iface->views_foreach = ide_perspective_real_views_foreach;
+}
+
+/**
+ * ide_perspective_agree_to_shutdown:
+ * @self: An #IdePerspective.
+ *
+ * This interface method is called when the workbench would like to shutdown.
+ * If the perspective needs to focus and ask the user a question, this is the place
+ * to do so. You may run a #GtkDialog using gtk_dialog_run() or simply focus your
+ * perspective and return %FALSE.
+ *
+ * Returns: %TRUE to allow the workbench to continue shutting down.
+ */
+gboolean
+ide_perspective_agree_to_shutdown (IdePerspective *self)
+{
+  g_return_val_if_fail (IDE_IS_PERSPECTIVE (self), FALSE);
+
+  return IDE_PERSPECTIVE_GET_IFACE (self)->agree_to_shutdown (self);
+}
+
+/**
+ * ide_perspective_get_icon_name:
+ * @self: An #IdePerspective.
+ *
+ * This interface methods retrieves the icon name to use when displaying the
+ * perspective selection sidebar.
+ *
+ * If you implement an "icon-name" property, the icon may change at runtime.
+ *
+ * Returns: A named icon as a string which will not be modified or freed.
+ */
+const gchar *
+ide_perspective_get_icon_name (IdePerspective *self)
+{
+  g_return_val_if_fail (IDE_IS_PERSPECTIVE (self), NULL);
+
+  return IDE_PERSPECTIVE_GET_IFACE (self)->get_icon_name (self);
+}
+
+/**
+ * ide_perspective_get_id:
+ * @self: An #IdePerspective
+ *
+ * This interface method is used to identify the perspective. It should be a short
+ * internal name, such as "editor" which should not be translated. Internally, the
+ * default implementation of this method will return the name of the instances #GType.
+ *
+ * This value should be unique per workspace.
+ *
+ * Returns: A string identifier for the perspective.
+ */
+const gchar *
+ide_perspective_get_id (IdePerspective *self)
+{
+  g_return_val_if_fail (IDE_IS_PERSPECTIVE (self), NULL);
+
+  return IDE_PERSPECTIVE_GET_IFACE (self)->get_id (self);
+}
+
+/**
+ * ide_perspective_get_needs_attention:
+ * @self: An #IdePerspective.
+ *
+ * This interface method returns %TRUE if the interface needs attention.
+ *
+ * One such use of this would be to indicate that contents within a perspective have
+ * changed since the user last focused the perspective. This should also be implemented
+ * with a boolean property named "needs-attention". If you call g_object_notify() (or one
+ * of its variants), the notifcation visual will be rendered with your icon.
+ *
+ * Returns: %TRUE if the perspective needs attention.
+ */
+gboolean
+ide_perspective_get_needs_attention (IdePerspective *self)
+{
+  g_return_val_if_fail (IDE_IS_PERSPECTIVE (self), FALSE);
+
+  return IDE_PERSPECTIVE_GET_IFACE (self)->get_needs_attention (self);
+}
+
+/**
+ * ide_perspective_get_title:
+ * @self: An #IdePerspective
+ *
+ * This interface method gets the title of the perspective. This is used for tooltips
+ * in the perspective selector and potentially other UI components.
+ *
+ * Returns: A string which will not be modified or freed.
+ */
+const gchar *
+ide_perspective_get_title (IdePerspective *self)
+{
+  g_return_val_if_fail (IDE_IS_PERSPECTIVE (self), NULL);
+
+  return IDE_PERSPECTIVE_GET_IFACE (self)->get_title (self);
+}
+
+/**
+ * ide_perspective_get_titlebar:
+ * @self: An #IdePerspective.
+ *
+ * This interface method should return a #GtkWidget suitable for being embedded as the
+ * titlebar for the application. If you return %NULL from this method, a suitable titlebar
+ * will be created for you.
+ *
+ * You may use #IdeHeaderBar for a base implementation to save you the trouble of
+ * creating a titlebar similar to other perspectives in Builder.
+ *
+ * Returns: (transfer none) (nullable): A #GtkWidget or %NULL.
+ */
+GtkWidget *
+ide_perspective_get_titlebar (IdePerspective *self)
+{
+  g_return_val_if_fail (IDE_IS_PERSPECTIVE (self), NULL);
+
+  return IDE_PERSPECTIVE_GET_IFACE (self)->get_titlebar (self);
+}
+
+/**
+ * ide_perspective_set_fullscreen:
+ * @self: An #IdePerspective.
+ * @fullscreen: If fullscreen mode should be activated.
+ *
+ * This interface method is used to notify the perspective that it is going into
+ * fullscreen mode. The #IdeWorkbench will notify the perspective before it is displayed.
+ */
+void
+ide_perspective_set_fullscreen (IdePerspective *self,
+                                gboolean        fullscreen)
+{
+  g_return_if_fail (IDE_IS_PERSPECTIVE (self));
+
+  IDE_PERSPECTIVE_GET_IFACE (self)->set_fullscreen (self, fullscreen);
+}
+
+/**
+ * ide_perspective_views_foreach:
+ * @self: An #IdePerspective.
+ * @callback: (scope call): A #GtkCallback.
+ * @user_data: user data for @callback.
+ *
+ * This interface method is used to iterate all #IdeView's that are descendents of @self.
+ */
+void
+ide_perspective_views_foreach (IdePerspective *self,
+                               GtkCallback     callback,
+                               gpointer        user_data)
+{
+  g_return_if_fail (IDE_IS_PERSPECTIVE (self));
+  g_return_if_fail (callback != NULL);
+
+  IDE_PERSPECTIVE_GET_IFACE (self)->views_foreach (self, callback, user_data);
+}
+
+/**
+ * ide_perspective_get_actions:
+ * @self: An #IdePerspective.
+ *
+ * This interface method should retrieve a #GActionGroup associated with the
+ * perspective, if necessary. The #GActionGroup will automatically be
+ * registered with the "perspective" action prefix while the perspective is
+ * active. A perspective is "active" when it is currently displayed in the
+ * workbench.
+ *
+ * Returns: (nullable) (transfer full): A #GActionGroup or %NULL.
+ */
+GActionGroup *
+ide_perspective_get_actions (IdePerspective *self)
+{
+  g_return_val_if_fail (IDE_IS_PERSPECTIVE (self), NULL);
+
+  return IDE_PERSPECTIVE_GET_IFACE (self)->get_actions (self);
+}
diff --git a/libide/ide-perspective.h b/libide/ide-perspective.h
new file mode 100644
index 0000000..9563d84
--- /dev/null
+++ b/libide/ide-perspective.h
@@ -0,0 +1,63 @@
+/* ide-perspective.h
+ *
+ * Copyright (C) 2015 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/>.
+ */
+
+#ifndef IDE_PERSPECTIVE_H
+#define IDE_PERSPECTIVE_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_PERSPECTIVE (ide_perspective_get_type())
+
+G_DECLARE_INTERFACE (IdePerspective, ide_perspective, IDE, PERSPECTIVE, GtkWidget)
+
+struct _IdePerspectiveInterface
+{
+  GTypeInterface parent;
+
+  gboolean      (*agree_to_shutdown)   (IdePerspective *self);
+  GActionGroup *(*get_actions)         (IdePerspective *self);
+  const gchar  *(*get_icon_name)       (IdePerspective *self);
+  const gchar  *(*get_id)              (IdePerspective *self);
+  gboolean      (*get_needs_attention) (IdePerspective *self);
+  const gchar  *(*get_title)           (IdePerspective *self);
+  GtkWidget    *(*get_titlebar)        (IdePerspective *self);
+  void          (*set_fullscreen)      (IdePerspective *self,
+                                        gboolean        fullscreen);
+  void          (*views_foreach)       (IdePerspective *self,
+                                        GtkCallback     callback,
+                                        gpointer        user_data);
+};
+
+gboolean      ide_perspective_agree_to_shutdown   (IdePerspective *self);
+GActionGroup *ide_perspective_get_actions         (IdePerspective *self);
+const gchar  *ide_perspective_get_icon_name       (IdePerspective *self);
+const gchar  *ide_perspective_get_id              (IdePerspective *self);
+const gchar  *ide_perspective_get_title           (IdePerspective *self);
+GtkWidget    *ide_perspective_get_titlebar        (IdePerspective *self);
+gboolean      ide_perspective_get_needs_attention (IdePerspective *self);
+void          ide_perspective_set_fullscreen      (IdePerspective *self,
+                                                   gboolean        fullscreen);
+void          ide_perspective_views_foreach       (IdePerspective *self,
+                                                   GtkCallback     callback,
+                                                   gpointer        user_data);
+
+G_END_DECLS
+
+#endif /* IDE_PERSPECTIVE_H */
diff --git a/libide/ide-preferences-addin.c b/libide/ide-preferences-addin.c
new file mode 100644
index 0000000..2463942
--- /dev/null
+++ b/libide/ide-preferences-addin.c
@@ -0,0 +1,81 @@
+/* ide-preferences-addin.c
+ *
+ * Copyright (C) 2015 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/>.
+ */
+
+#include "ide-preferences-addin.h"
+
+G_DEFINE_INTERFACE (IdePreferencesAddin, ide_preferences_addin, G_TYPE_OBJECT)
+
+static void
+ide_preferences_addin_real_load (IdePreferencesAddin *self,
+                                 IdePreferences      *preferences)
+{
+}
+
+static void
+ide_preferences_addin_real_unload (IdePreferencesAddin *self,
+                                   IdePreferences      *preferences)
+{
+}
+
+static void
+ide_preferences_addin_default_init (IdePreferencesAddinInterface *iface)
+{
+  iface->load = ide_preferences_addin_real_load;
+  iface->unload = ide_preferences_addin_real_unload;
+}
+
+/**
+ * ide_preferences_addin_load:
+ * @self: An #IdePreferencesAddin.
+ * @preferences: The preferences container implementation.
+ *
+ * This interface method is called when a preferences addin is initialized. It could be
+ * initialized from multiple preferences implementations, so consumers should use the
+ * #IdePreferences interface to add their preferences controls to the container.
+ *
+ * Such implementations might include a preferences dialog window, or a preferences
+ * widget which could be rendered as a perspective.
+ */
+void
+ide_preferences_addin_load (IdePreferencesAddin *self,
+                            IdePreferences      *preferences)
+{
+  g_return_if_fail (IDE_IS_PREFERENCES_ADDIN (self));
+  g_return_if_fail (IDE_IS_PREFERENCES (preferences));
+
+  IDE_PREFERENCES_ADDIN_GET_IFACE (self)->load (self, preferences);
+}
+
+/**
+ * ide_preferences_addin_unload:
+ * @self: An #IdePreferencesAddin.
+ * @preferences: The preferences container implementation.
+ *
+ * This interface method is called when the preferences addin should remove all controls
+ * added to @preferences. This could happen during desctruction of @preferences, or when
+ * the plugin is unloaded.
+ */
+void
+ide_preferences_addin_unload (IdePreferencesAddin *self,
+                              IdePreferences      *preferences)
+{
+  g_return_if_fail (IDE_IS_PREFERENCES_ADDIN (self));
+  g_return_if_fail (IDE_IS_PREFERENCES (preferences));
+
+  IDE_PREFERENCES_ADDIN_GET_IFACE (self)->unload (self, preferences);
+}
diff --git a/libide/ide-preferences-addin.h b/libide/ide-preferences-addin.h
new file mode 100644
index 0000000..308b7be
--- /dev/null
+++ b/libide/ide-preferences-addin.h
@@ -0,0 +1,49 @@
+/* ide-preferences-addin.h
+ *
+ * Copyright (C) 2015 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/>.
+ */
+
+#ifndef IDE_PREFERENCES_ADDIN_H
+#define IDE_PREFERENCES_ADDIN_H
+
+#include <gtk/gtk.h>
+
+#include "ide-preferences.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_PREFERENCES_ADDIN (ide_preferences_addin_get_type())
+
+G_DECLARE_INTERFACE (IdePreferencesAddin, ide_preferences_addin, IDE, PREFERENCES_ADDIN, GObject)
+
+struct _IdePreferencesAddinInterface
+{
+  GTypeInterface parent_interface;
+
+  void (*load)   (IdePreferencesAddin *self,
+                  IdePreferences      *preferences);
+  void (*unload) (IdePreferencesAddin *self,
+                  IdePreferences      *preferences);
+};
+
+void ide_preferences_addin_load   (IdePreferencesAddin *self,
+                                   IdePreferences      *preferences);
+void ide_preferences_addin_unload (IdePreferencesAddin *self,
+                                   IdePreferences      *preferences);
+
+G_END_DECLS
+
+#endif /* IDE_PREFERENCES_ADDIN_H */
diff --git a/libide/ide-preferences.c b/libide/ide-preferences.c
new file mode 100644
index 0000000..eaae59d
--- /dev/null
+++ b/libide/ide-preferences.c
@@ -0,0 +1,116 @@
+/* ide-preferences.c
+ *
+ * Copyright (C) 2015 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/>.
+ */
+
+#include "ide-preferences.h"
+
+G_DEFINE_INTERFACE (IdePreferences, ide_preferences, G_TYPE_OBJECT)
+
+static void
+ide_preferences_default_init (IdePreferencesInterface *iface)
+{
+}
+
+void
+ide_preferences_add_page (IdePreferences *self,
+                          const gchar    *page_name,
+                          const gchar    *title,
+                          gint            priority)
+{
+  g_return_if_fail (IDE_IS_PREFERENCES (self));
+  g_return_if_fail (page_name != NULL);
+  g_return_if_fail (title != NULL);
+
+  IDE_PREFERENCES_GET_IFACE (self)->add_page (self, page_name, title, priority);
+}
+
+void
+ide_preferences_add_group (IdePreferences *self,
+                           const gchar    *page_name,
+                           const gchar    *group_name,
+                           const gchar    *title,
+                           gint            priority)
+{
+  g_return_if_fail (IDE_IS_PREFERENCES (self));
+  g_return_if_fail (page_name != NULL);
+  g_return_if_fail (group_name != NULL);
+  g_return_if_fail (title != NULL);
+
+  IDE_PREFERENCES_GET_IFACE (self)->add_group (self, page_name, group_name, title, priority);
+}
+
+guint
+ide_preferences_add_switch (IdePreferences *self,
+                            const gchar    *page_name,
+                            const gchar    *group_name,
+                            const gchar    *schema_id,
+                            const gchar    *key,
+                            const gchar    *title,
+                            const gchar    *subtitle,
+                            const gchar    *keywords,
+                            gint            priority)
+{
+  g_return_val_if_fail (IDE_IS_PREFERENCES (self), 0);
+  g_return_val_if_fail (page_name != NULL, 0);
+  g_return_val_if_fail (group_name != NULL, 0);
+  g_return_val_if_fail (schema_id != NULL, 0);
+  g_return_val_if_fail (key != NULL, 0);
+  g_return_val_if_fail (title != NULL, 0);
+
+  return IDE_PREFERENCES_GET_IFACE (self)->add_switch (self, page_name, group_name, schema_id, key,
+                                                       title, subtitle, keywords, priority);
+}
+
+guint
+ide_preferences_add_spinbutton (IdePreferences *self,
+                                const gchar    *page_name,
+                                const gchar    *group_name,
+                                const gchar    *schema_id,
+                                const gchar    *key,
+                                const gchar    *title,
+                                const gchar    *subtitle,
+                                const gchar    *keywords,
+                                gint            priority)
+{
+  g_return_val_if_fail (IDE_IS_PREFERENCES (self), 0);
+  g_return_val_if_fail (page_name != NULL, 0);
+  g_return_val_if_fail (group_name != NULL, 0);
+  g_return_val_if_fail (schema_id != NULL, 0);
+  g_return_val_if_fail (key != NULL, 0);
+  g_return_val_if_fail (title != NULL, 0);
+
+  return IDE_PREFERENCES_GET_IFACE (self)->add_spinbutton (self, page_name, group_name, schema_id,
+                                                           key, title, subtitle, keywords,
+                                                           priority);
+}
+
+guint
+ide_preferences_add_custom (IdePreferences *self,
+                            const gchar    *page_name,
+                            const gchar    *group_name,
+                            GtkWidget      *widget,
+                            const gchar    *keywords,
+                            gint            priority)
+{
+  g_return_val_if_fail (IDE_IS_PREFERENCES (self), 0);
+  g_return_val_if_fail (page_name != NULL, 0);
+  g_return_val_if_fail (group_name != NULL, 0);
+  g_return_val_if_fail (GTK_IS_WIDGET (widget), 0);
+
+  return IDE_PREFERENCES_GET_IFACE (self)->add_custom (self, page_name, group_name, widget,
+                                                       keywords, priority);
+}
diff --git a/libide/ide-preferences.h b/libide/ide-preferences.h
new file mode 100644
index 0000000..bca3d13
--- /dev/null
+++ b/libide/ide-preferences.h
@@ -0,0 +1,105 @@
+/* ide-preferences.h
+ *
+ * Copyright (C) 2015 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/>.
+ */
+
+#ifndef IDE_PREFERENCES_H
+#define IDE_PREFERENCES_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_PREFERENCES (ide_preferences_get_type())
+
+G_DECLARE_INTERFACE (IdePreferences, ide_preferences, IDE, PREFERENCES, GObject)
+
+struct _IdePreferencesInterface
+{
+  GTypeInterface parent_interface;
+
+  void  (*add_page)       (IdePreferences *self,
+                           const gchar    *page_name,
+                           const gchar    *title,
+                           gint            priority);
+  void  (*add_group)      (IdePreferences *self,
+                           const gchar    *page_name,
+                           const gchar    *group_name,
+                           const gchar    *title,
+                           gint            priority);
+  guint (*add_switch)     (IdePreferences *self,
+                           const gchar    *page_name,
+                           const gchar    *group_name,
+                           const gchar    *schema_id,
+                           const gchar    *key,
+                           const gchar    *title,
+                           const gchar    *subtitle,
+                           const gchar    *keywords,
+                           gint            priority);
+  guint (*add_spinbutton) (IdePreferences *self,
+                           const gchar    *page_name,
+                           const gchar    *group_name,
+                           const gchar    *schema_id,
+                           const gchar    *key,
+                           const gchar    *title,
+                           const gchar    *subtitle,
+                           const gchar    *keywords,
+                           gint            priority);
+  guint (*add_custom)     (IdePreferences *self,
+                           const gchar    *page_name,
+                           const gchar    *group_name,
+                           GtkWidget      *widget,
+                           const gchar    *keywords,
+                           gint            priority);
+};
+
+void  ide_preferences_add_page       (IdePreferences *self,
+                                      const gchar    *page_name,
+                                      const gchar    *title,
+                                      gint            priority);
+void  ide_preferences_add_group      (IdePreferences *self,
+                                      const gchar    *page_name,
+                                      const gchar    *group_name,
+                                      const gchar    *title,
+                                      gint            priority);
+guint ide_preferences_add_switch     (IdePreferences *self,
+                                      const gchar    *page_name,
+                                      const gchar    *group_name,
+                                      const gchar    *schema_id,
+                                      const gchar    *key,
+                                      const gchar    *title,
+                                      const gchar    *subtitle,
+                                      const gchar    *keywords,
+                                      gint            priority);
+guint ide_preferences_add_spinbutton (IdePreferences *self,
+                                      const gchar    *page_name,
+                                      const gchar    *group_name,
+                                      const gchar    *schema_id,
+                                      const gchar    *key,
+                                      const gchar    *title,
+                                      const gchar    *subtitle,
+                                      const gchar    *keywords,
+                                      gint            priority);
+guint ide_preferences_add_custom     (IdePreferences *self,
+                                      const gchar    *page_name,
+                                      const gchar    *group_name,
+                                      GtkWidget      *widget,
+                                      const gchar    *keywords,
+                                      gint            priority);
+
+G_END_DECLS
+
+#endif /* IDE_PREFERENCES_H */
diff --git a/libide/ide-thread-pool.c b/libide/ide-thread-pool.c
index d45c830..4eaa613 100644
--- a/libide/ide-thread-pool.c
+++ b/libide/ide-thread-pool.c
@@ -183,8 +183,19 @@ ide_thread_pool_worker (gpointer data,
 }
 
 void
-_ide_thread_pool_init (void)
+_ide_thread_pool_init (gboolean is_worker)
 {
+  gint compiler = COMPILER_MAX_THREADS;
+  gint indexer = INDEXER_MAX_THREADS;
+  gboolean shared = FALSE;
+
+  if (is_worker)
+    {
+      compiler = 1;
+      indexer = 1;
+      shared = TRUE;
+    }
+
   /*
    * Create our thread pool exclusive to compiler tasks (such as those from Clang).
    * We don't want to consume threads fro other GTask's such as those regarding IO so we manage
@@ -192,8 +203,8 @@ _ide_thread_pool_init (void)
    */
   thread_pools [IDE_THREAD_POOL_COMPILER] = g_thread_pool_new (ide_thread_pool_worker,
                                                                NULL,
-                                                               COMPILER_MAX_THREADS,
-                                                               FALSE,
+                                                               compiler,
+                                                               shared,
                                                                NULL);
 
   /*
@@ -202,7 +213,7 @@ _ide_thread_pool_init (void)
    */
   thread_pools [IDE_THREAD_POOL_INDEXER] = g_thread_pool_new (ide_thread_pool_worker,
                                                               NULL,
-                                                              INDEXER_MAX_THREADS,
-                                                              FALSE,
+                                                              indexer,
+                                                              shared,
                                                               NULL);
 }
diff --git a/libide/ide-view.c b/libide/ide-view.c
new file mode 100644
index 0000000..6a7ad9c
--- /dev/null
+++ b/libide/ide-view.c
@@ -0,0 +1,129 @@
+/* ide-view.c
+ *
+ * Copyright (C) 2015 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/>.
+ */
+
+#include "ide-view.h"
+
+G_DEFINE_INTERFACE (IdeView, ide_view, GTK_TYPE_WIDGET)
+
+static const gchar *
+ide_view_real_get_title (IdeView *self)
+{
+  return NULL;
+}
+
+static const gchar *
+ide_view_real_get_icon_name (IdeView *self)
+{
+  return NULL;
+}
+
+static gboolean
+ide_view_real_get_can_save (IdeView *self)
+{
+  return FALSE;
+}
+
+static gboolean
+ide_view_real_get_needs_attention (IdeView *self)
+{
+  return FALSE;
+}
+
+static void
+ide_view_real_save_async (IdeView             *self,
+                          GCancellable        *cancellable,
+                          GAsyncReadyCallback  callback,
+                          gpointer             user_data)
+{
+  g_task_report_new_error (self, callback, user_data, ide_view_real_save_async,
+                           G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "Saving is not supported.");
+}
+
+static gboolean
+ide_view_real_save_finish (IdeView       *self,
+                           GAsyncResult  *result,
+                           GError       **error)
+{
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+ide_view_default_init (IdeViewInterface *iface)
+{
+  iface->get_title = ide_view_real_get_title;
+  iface->get_icon_name = ide_view_real_get_icon_name;
+  iface->get_can_save = ide_view_real_get_can_save;
+  iface->get_needs_attention = ide_view_real_get_needs_attention;
+  iface->save_async = ide_view_real_save_async;
+  iface->save_finish = ide_view_real_save_finish;
+}
+
+const gchar *
+ide_view_get_title (IdeView *self)
+{
+  g_return_val_if_fail (IDE_IS_VIEW (self), NULL);
+
+  return NULL;
+}
+
+const gchar *
+ide_view_get_icon_name (IdeView *self)
+{
+  g_return_val_if_fail (IDE_IS_VIEW (self), NULL);
+
+  return IDE_VIEW_GET_IFACE (self)->get_icon_name (self);
+}
+
+gboolean
+ide_view_get_can_save (IdeView *self)
+{
+  g_return_val_if_fail (IDE_IS_VIEW (self), FALSE);
+
+  return IDE_VIEW_GET_IFACE (self)->get_can_save (self);
+}
+
+gboolean
+ide_view_get_needs_attention (IdeView *self)
+{
+  g_return_val_if_fail (IDE_IS_VIEW (self), FALSE);
+
+  return IDE_VIEW_GET_IFACE (self)->get_needs_attention (self);
+}
+
+void
+ide_view_save_async (IdeView             *self,
+                     GCancellable        *cancellable,
+                     GAsyncReadyCallback  callback,
+                     gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_VIEW (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_VIEW_GET_IFACE (self)->save_async (self, cancellable, callback, user_data);
+}
+
+gboolean
+ide_view_save_finish (IdeView       *self,
+                      GAsyncResult  *result,
+                      GError       **error)
+{
+  g_return_val_if_fail (IDE_IS_VIEW (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_VIEW_GET_IFACE (self)->save_finish (self, result, error);
+}
diff --git a/libide/ide-view.h b/libide/ide-view.h
new file mode 100644
index 0000000..d73f462
--- /dev/null
+++ b/libide/ide-view.h
@@ -0,0 +1,61 @@
+/* ide-view.h
+ *
+ * Copyright (C) 2015 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/>.
+ */
+
+#ifndef IDE_VIEW_H
+#define IDE_VIEW_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_VIEW (ide_view_get_type())
+
+G_DECLARE_INTERFACE (IdeView, ide_view, IDE, VIEW, GtkWidget)
+
+struct _IdeViewInterface
+{
+  GTypeInterface parent;
+
+  const gchar *(*get_title)           (IdeView              *self);
+  const gchar *(*get_icon_name)       (IdeView              *self);
+  gboolean     (*get_can_save)        (IdeView              *self);
+  gboolean     (*get_needs_attention) (IdeView              *self);
+  void         (*save_async)          (IdeView              *self,
+                                       GCancellable         *cancellable,
+                                       GAsyncReadyCallback   callback,
+                                       gpointer              user_data);
+  gboolean     (*save_finish)         (IdeView              *self,
+                                       GAsyncResult         *result,
+                                       GError              **error);
+};
+
+const gchar *ide_view_get_title           (IdeView              *self);
+const gchar *ide_view_get_icon_name       (IdeView              *self);
+gboolean     ide_view_get_can_save        (IdeView              *self);
+gboolean     ide_view_get_needs_attention (IdeView              *self);
+void         ide_view_save_async          (IdeView              *self,
+                                           GCancellable         *cancellable,
+                                           GAsyncReadyCallback   callback,
+                                           gpointer              user_data);
+gboolean     ide_view_save_finish         (IdeView              *self,
+                                           GAsyncResult         *result,
+                                           GError              **error);
+
+G_END_DECLS
+
+#endif /* IDE_VIEW_H */
diff --git a/libide/ide-workbench-addin.c b/libide/ide-workbench-addin.c
new file mode 100644
index 0000000..1cf1d61
--- /dev/null
+++ b/libide/ide-workbench-addin.c
@@ -0,0 +1,83 @@
+/* ide-workbench-addin.c
+ *
+ * Copyright (C) 2015 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/>.
+ */
+
+#define G_LOG_DOMAIN "ide-workbench-addin"
+
+#include "ide-workbench-addin.h"
+
+G_DEFINE_INTERFACE (IdeWorkbenchAddin, ide_workbench_addin, G_TYPE_OBJECT)
+
+static void
+ide_workbench_addin_real_load (IdeWorkbenchAddin *self,
+                               IdeWorkbench      *workbench)
+{
+}
+
+static void
+ide_workbench_addin_real_unload (IdeWorkbenchAddin *self,
+                                 IdeWorkbench      *workbench)
+{
+}
+
+static void
+ide_workbench_addin_default_init (IdeWorkbenchAddinInterface *iface)
+{
+  iface->load = ide_workbench_addin_real_load;
+  iface->unload = ide_workbench_addin_real_unload;
+}
+
+/**
+ * ide_workbench_addin_load:
+ * @self: An #IdeWorkbenchAddin
+ * @workbench: An #IdeWorkbench
+ *
+ * This interface method is called to load @self. Addin implementations should add any
+ * required UI or actions to @workbench here. You should remove anything you've added
+ * in ide_workbench_addin_unload(), as that will be called when your plugin is deactivated
+ * or the workbench is in the destruction process.
+ */
+void
+ide_workbench_addin_load (IdeWorkbenchAddin *self,
+                          IdeWorkbench      *workbench)
+{
+  g_return_if_fail (IDE_IS_WORKBENCH_ADDIN (self));
+  g_return_if_fail (IDE_IS_WORKBENCH (workbench));
+
+  IDE_WORKBENCH_ADDIN_GET_IFACE (self)->load (self, workbench);
+}
+
+/**
+ * ide_workbench_addin_unload:
+ * @self: An #IdeWorkbenchAddin
+ * @workbench: An #IdeWorkbench
+ *
+ * This interface method should cleanup after anything added to @workbench in
+ * ide_workbench_addin_load().
+ *
+ * This might be called when a plugin is deactivated, or the workbench is in the
+ * destruction process.
+ */
+void
+ide_workbench_addin_unload (IdeWorkbenchAddin *self,
+                            IdeWorkbench      *workbench)
+{
+  g_return_if_fail (IDE_IS_WORKBENCH_ADDIN (self));
+  g_return_if_fail (IDE_IS_WORKBENCH (workbench));
+
+  IDE_WORKBENCH_ADDIN_GET_IFACE (self)->unload (self, workbench);
+}
diff --git a/libide/ide-workbench-addin.h b/libide/ide-workbench-addin.h
new file mode 100644
index 0000000..9897d2b
--- /dev/null
+++ b/libide/ide-workbench-addin.h
@@ -0,0 +1,47 @@
+/* ide-workbench-addin.h
+ *
+ * Copyright (C) 2015 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/>.
+ */
+
+#ifndef IDE_WORKBENCH_ADDIN_H
+#define IDE_WORKBENCH_ADDIN_H
+
+#include "ide-workbench.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_WORKBENCH_ADDIN (ide_workbench_addin_get_type())
+
+G_DECLARE_INTERFACE (IdeWorkbenchAddin, ide_workbench_addin, IDE, WORKBENCH_ADDIN, GObject)
+
+struct _IdeWorkbenchAddinInterface
+{
+  GTypeInterface parent;
+
+  void (*load)   (IdeWorkbenchAddin *self,
+                  IdeWorkbench      *workbench);
+  void (*unload) (IdeWorkbenchAddin *self,
+                  IdeWorkbench      *workbench);
+};
+
+void ide_workbench_addin_load   (IdeWorkbenchAddin *self,
+                                 IdeWorkbench      *workbench);
+void ide_workbench_addin_unload (IdeWorkbenchAddin *self,
+                                 IdeWorkbench      *workbench);
+
+G_END_DECLS
+
+#endif /* IDE_WORKBENCH_ADDIN_H */
diff --git a/libide/ide-workbench-header-bar.c b/libide/ide-workbench-header-bar.c
new file mode 100644
index 0000000..75a159e
--- /dev/null
+++ b/libide/ide-workbench-header-bar.c
@@ -0,0 +1,58 @@
+/* ide-workbench-header-bar.c
+ *
+ * Copyright (C) 2015 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/>.
+ */
+
+#define G_LOG_DOMAIN "ide-workbench-header-bar"
+
+#include "ide-application.h"
+#include "ide-workbench-header-bar.h"
+
+typedef struct
+{
+  GtkMenuButton *menu_button;
+} IdeWorkbenchHeaderBarPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeWorkbenchHeaderBar, ide_workbench_header_bar, GTK_TYPE_HEADER_BAR)
+
+GtkWidget *
+ide_workbench_header_bar_new (void)
+{
+  return g_object_new (IDE_TYPE_WORKBENCH_HEADER_BAR, NULL);
+}
+
+static void
+ide_workbench_header_bar_class_init (IdeWorkbenchHeaderBarClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/builder/ui/ide-workbench-header-bar.ui");
+  gtk_widget_class_bind_template_child_private (widget_class, IdeWorkbenchHeaderBar, menu_button);
+}
+
+static void
+ide_workbench_header_bar_init (IdeWorkbenchHeaderBar *self)
+{
+  IdeWorkbenchHeaderBarPrivate *priv = ide_workbench_header_bar_get_instance_private (self);
+  GtkWidget *popover;
+  GMenu *model;
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  model = gtk_application_get_menu_by_id (GTK_APPLICATION (IDE_APPLICATION_DEFAULT), "gear-menu");
+  popover = gtk_popover_new_from_model (NULL, G_MENU_MODEL (model));
+  gtk_menu_button_set_popover (priv->menu_button, popover);
+}
diff --git a/src/greeter/gb-greeter-pill-box.h b/libide/ide-workbench-header-bar.h
similarity index 53%
copy from src/greeter/gb-greeter-pill-box.h
copy to libide/ide-workbench-header-bar.h
index 394a333..cb91fa9 100644
--- a/src/greeter/gb-greeter-pill-box.h
+++ b/libide/ide-workbench-header-bar.h
@@ -1,6 +1,6 @@
-/* gb-greeter-pill-box.h
+/* ide-workbench-header-bar.h
  *
- * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ * Copyright (C) 2015 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
@@ -16,22 +16,24 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#ifndef GB_GREETER_PILL_BOX_H
-#define GB_GREETER_PILL_BOX_H
+#ifndef IDE_WORKBENCH_HEADER_BAR_H
+#define IDE_WORKBENCH_HEADER_BAR_H
 
 #include <gtk/gtk.h>
 
 G_BEGIN_DECLS
 
-#define GB_TYPE_GREETER_PILL_BOX (gb_greeter_pill_box_get_type())
+#define IDE_TYPE_WORKBENCH_HEADER_BAR (ide_workbench_header_bar_get_type())
 
-G_DECLARE_FINAL_TYPE (GbGreeterPillBox, gb_greeter_pill_box, GB, GREETER_PILL_BOX, GtkEventBox)
+G_DECLARE_DERIVABLE_TYPE (IdeWorkbenchHeaderBar, ide_workbench_header_bar, IDE, WORKBENCH_HEADER_BAR, 
GtkHeaderBar)
 
-GtkWidget   *gb_greeter_pill_box_new       (const gchar      *label);
-const gchar *gb_greeter_pill_box_get_label (GbGreeterPillBox *self);
-void         gb_greeter_pill_box_set_label (GbGreeterPillBox *self,
-                                            const gchar      *label);
+struct _IdeWorkbenchHeaderBarClass
+{
+  GtkHeaderBarClass parent;
+};
+
+GtkWidget *ide_workbench_header_bar_new (void);
 
 G_END_DECLS
 
-#endif /* GB_GREETER_PILL_BOX_H */
+#endif /* IDE_WORKBENCH_HEADER_BAR_H */
diff --git a/libide/ide-workbench-private.h b/libide/ide-workbench-private.h
new file mode 100644
index 0000000..f917bdc
--- /dev/null
+++ b/libide/ide-workbench-private.h
@@ -0,0 +1,59 @@
+/* ide-workbench-private.h
+ *
+ * Copyright (C) 2015 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/>.
+ */
+
+#ifndef IDE_WORKBENCH_PRIVATE_H
+#define IDE_WORKBENCH_PRIVATE_H
+
+#include <libpeas/peas.h>
+
+#include "ide-editor-perspective.h"
+#include "ide-greeter-perspective.h"
+#include "ide-workbench.h"
+
+G_BEGIN_DECLS
+
+struct _IdeWorkbench
+{
+  GtkApplicationWindow   parent;
+
+  IdeContext            *context;
+  PeasExtensionSet      *addins;
+
+  IdePerspective        *perspective;
+
+  GtkStack              *top_stack;
+  GtkStack              *titlebar_stack;
+  IdeEditorPerspective  *editor_perspective;
+  IdeGreeterPerspective *greeter_perspective;
+  GtkStack              *perspectives_stack;
+  GtkStackSwitcher      *perspectives_stack_switcher;
+  GtkPopover            *perspectives_popover;
+};
+
+typedef struct
+{
+  GtkCallback callback;
+  gpointer    user_data;
+} IdeWorkbenchForeach;
+
+void _ide_workbench_set_context (IdeWorkbench *workbench,
+                                 IdeContext   *context);
+
+G_END_DECLS
+
+#endif /* IDE_WORKBENCH_PRIVATE_H */
diff --git a/libide/ide-workbench.c b/libide/ide-workbench.c
new file mode 100644
index 0000000..231c711
--- /dev/null
+++ b/libide/ide-workbench.c
@@ -0,0 +1,396 @@
+/* ide-workbench.c
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * 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/>.
+ */
+
+#define G_LOG_DOMAIN "ide-workbench"
+
+#include "ide-macros.h"
+#include "ide-window-settings.h"
+#include "ide-workbench.h"
+#include "ide-workbench-addin.h"
+#include "ide-workbench-header-bar.h"
+#include "ide-workbench-private.h"
+
+G_DEFINE_TYPE (IdeWorkbench, ide_workbench, GTK_TYPE_APPLICATION_WINDOW)
+
+enum {
+  PROP_0,
+  PROP_PERSPECTIVE,
+  LAST_PROP
+};
+
+static GParamSpec *properties [LAST_PROP];
+
+static void
+ide_workbench_finalize (GObject *object)
+{
+  IdeWorkbench *self = (IdeWorkbench *)object;
+
+  ide_clear_weak_pointer (&self->perspective);
+  g_clear_object (&self->context);
+
+  G_OBJECT_CLASS (ide_workbench_parent_class)->finalize (object);
+}
+
+static void
+ide_workbench_constructed (GObject *object)
+{
+  G_OBJECT_CLASS (ide_workbench_parent_class)->constructed (object);
+}
+
+static void
+ide_workbench_destroy (GtkWidget *widget)
+{
+  IdeWorkbench *self = (IdeWorkbench *)widget;
+
+  g_assert (IDE_IS_WORKBENCH (self));
+
+  GTK_WIDGET_CLASS (ide_workbench_parent_class)->destroy (widget);
+}
+
+static void
+ide_workbench_get_property (GObject    *object,
+                            guint       prop_id,
+                            GValue     *value,
+                            GParamSpec *pspec)
+{
+  IdeWorkbench *self = IDE_WORKBENCH (object);
+
+  switch (prop_id)
+    {
+    case PROP_PERSPECTIVE:
+      g_value_set_object (value, ide_workbench_get_perspective (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_workbench_set_property (GObject      *object,
+                            guint         prop_id,
+                            const GValue *value,
+                            GParamSpec   *pspec)
+{
+  IdeWorkbench *self = IDE_WORKBENCH (object);
+
+  switch (prop_id)
+    {
+    case PROP_PERSPECTIVE:
+      ide_workbench_set_perspective (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_workbench_class_init (IdeWorkbenchClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->constructed = ide_workbench_constructed;
+  object_class->finalize = ide_workbench_finalize;
+  object_class->get_property = ide_workbench_get_property;
+  object_class->set_property = ide_workbench_set_property;
+
+  widget_class->destroy = ide_workbench_destroy;
+
+  properties [PROP_PERSPECTIVE] =
+    g_param_spec_object ("perspective",
+                         "Perspective",
+                         "Perspective",
+                         IDE_TYPE_PERSPECTIVE,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class,
+                                               "/org/gnome/builder/ui/ide-workbench.ui");
+
+  gtk_widget_class_bind_template_child (widget_class, IdeWorkbench, perspectives_stack);
+  gtk_widget_class_bind_template_child (widget_class, IdeWorkbench, perspectives_stack_switcher);
+  gtk_widget_class_bind_template_child (widget_class, IdeWorkbench, titlebar_stack);
+  gtk_widget_class_bind_template_child (widget_class, IdeWorkbench, top_stack);
+}
+
+static void
+ide_workbench_init_greeter (IdeWorkbench *self)
+{
+  g_assert (IDE_IS_WORKBENCH (self));
+
+  self->greeter_perspective = g_object_new (IDE_TYPE_GREETER_PERSPECTIVE,
+                                            "visible", TRUE,
+                                            NULL);
+  ide_workbench_add_perspective (self, IDE_PERSPECTIVE (self->greeter_perspective));
+  gtk_container_child_set (GTK_CONTAINER (self->titlebar_stack),
+                           ide_perspective_get_titlebar (IDE_PERSPECTIVE (self->greeter_perspective)),
+                           "position", 0,
+                           NULL);
+  gtk_container_child_set (GTK_CONTAINER (self->top_stack),
+                           GTK_WIDGET (self->greeter_perspective),
+                           "position", 0,
+                           NULL);
+  ide_workbench_set_perspective (self, IDE_PERSPECTIVE (self->greeter_perspective));
+}
+
+static void
+ide_workbench_init_editor (IdeWorkbench *self)
+{
+  g_assert (IDE_IS_WORKBENCH (self));
+
+  self->editor_perspective = g_object_new (IDE_TYPE_EDITOR_PERSPECTIVE,
+                                           "visible", TRUE,
+                                           NULL);
+  ide_workbench_add_perspective (self, IDE_PERSPECTIVE (self->editor_perspective));
+}
+
+static void
+ide_workbench_init (IdeWorkbench *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  ide_workbench_init_greeter (self);
+  ide_workbench_init_editor (self);
+  ide_workbench_init_editor (self);
+  ide_window_settings_register (GTK_WINDOW (self));
+
+  gtk_stack_set_visible_child (self->top_stack, GTK_WIDGET (self->greeter_perspective));
+}
+
+static void
+ide_workbench_views_foreach_cb (GtkWidget *widget,
+                                gpointer   user_data)
+{
+  IdeWorkbenchForeach *foreach_data = user_data;
+
+  g_assert (foreach_data);
+  g_assert (foreach_data->callback);
+
+  foreach_data->callback (widget, foreach_data->user_data);
+}
+
+/**
+ * ide_workbench_views_foreach:
+ * @self: An #IdeWorkbench.
+ * @callback: (scope call): The callback to execute
+ * @user_data: user data for @callback.
+ *
+ * Executes @callback for every #IdeView across all perspectives.
+ */
+void
+ide_workbench_views_foreach (IdeWorkbench *self,
+                             GtkCallback   callback,
+                             gpointer      user_data)
+{
+  IdeWorkbenchForeach foreach = { callback, user_data };
+
+  g_return_if_fail (IDE_IS_WORKBENCH (self));
+  g_return_if_fail (callback != NULL);
+
+  gtk_container_foreach (GTK_CONTAINER (self->perspectives_stack),
+                         ide_workbench_views_foreach_cb,
+                         &foreach);
+}
+
+static void
+ide_workbench_addin_added (PeasExtensionSet *set,
+                           PeasPluginInfo   *plugin_info,
+                           PeasExtension    *extension,
+                           gpointer          user_data)
+{
+  IdeWorkbench *self = user_data;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_WORKBENCH_ADDIN (extension));
+  g_assert (IDE_IS_WORKBENCH (self));
+
+  ide_workbench_addin_load (IDE_WORKBENCH_ADDIN (extension), self);
+}
+
+static void
+ide_workbench_addin_removed (PeasExtensionSet *set,
+                             PeasPluginInfo   *plugin_info,
+                             PeasExtension    *extension,
+                             gpointer          user_data)
+{
+  IdeWorkbench *self = user_data;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_WORKBENCH_ADDIN (extension));
+  g_assert (IDE_IS_WORKBENCH (self));
+
+  ide_workbench_addin_unload (IDE_WORKBENCH_ADDIN (extension), self);
+}
+
+/**
+ * ide_workbench_get_context:
+ * @self: An #IdeWorkbench.
+ *
+ * Gets the context associated with the workbench, or %NULL.
+ *
+ * Returns: (transfer none) (nullable): An #IdeContext or %NULL.
+ */
+IdeContext *
+ide_workbench_get_context (IdeWorkbench *self)
+{
+  g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);
+
+  return self->context;
+}
+
+void
+_ide_workbench_set_context (IdeWorkbench *self,
+                            IdeContext   *context)
+{
+  g_return_if_fail (IDE_IS_WORKBENCH (self));
+  g_return_if_fail (IDE_IS_CONTEXT (context));
+  g_return_if_fail (self->context == NULL);
+
+  g_set_object (&self->context, context);
+
+  self->addins = peas_extension_set_new (peas_engine_get_default (),
+                                         IDE_TYPE_WORKBENCH_ADDIN,
+                                         NULL);
+
+  g_signal_connect (self->addins,
+                    "extension-added",
+                    G_CALLBACK (ide_workbench_addin_added),
+                    self);
+
+  g_signal_connect (self->addins,
+                    "extension-removed",
+                    G_CALLBACK (ide_workbench_addin_removed),
+                    self);
+
+  peas_extension_set_foreach (self->addins, ide_workbench_addin_added, self);
+
+  ide_workbench_set_perspective (self, IDE_PERSPECTIVE (self->editor_perspective));
+
+  gtk_stack_set_visible_child_name (self->top_stack, "perspectives");
+}
+
+void
+ide_workbench_add_perspective (IdeWorkbench   *self,
+                               IdePerspective *perspective)
+{
+  const gchar *id;
+  const gchar *title;
+  const gchar *icon_name;
+  GtkStack *stack;
+  GtkWidget *titlebar;
+
+  g_assert (IDE_IS_WORKBENCH (self));
+  g_assert (IDE_IS_PERSPECTIVE (perspective));
+
+  id = ide_perspective_get_id (perspective);
+  title = ide_perspective_get_title (perspective);
+  icon_name = ide_perspective_get_icon_name (perspective);
+
+  if (IDE_IS_GREETER_PERSPECTIVE (perspective))
+    stack = self->top_stack;
+  else
+    stack = self->perspectives_stack;
+
+  gtk_widget_set_hexpand (GTK_WIDGET (perspective), TRUE);
+
+  gtk_container_add_with_properties (GTK_CONTAINER (stack),
+                                     GTK_WIDGET (perspective),
+                                     "icon-name", icon_name,
+                                     "name", id,
+                                     "needs-attention", FALSE,
+                                     "title", title,
+                                     NULL);
+
+  titlebar = ide_perspective_get_titlebar (perspective);
+  if (titlebar == NULL)
+    titlebar = g_object_new (IDE_TYPE_WORKBENCH_HEADER_BAR,
+                             "visible", TRUE,
+                             NULL);
+
+  gtk_container_add_with_properties (GTK_CONTAINER (self->titlebar_stack), titlebar,
+                                     "name", id,
+                                     NULL);
+}
+
+void
+ide_workbench_remove_perspective (IdeWorkbench   *self,
+                                  IdePerspective *perspective)
+{
+  const gchar *id;
+  GtkWidget *titlebar;
+
+  g_assert (IDE_IS_WORKBENCH (self));
+  g_assert (IDE_IS_PERSPECTIVE (perspective));
+  g_assert (gtk_widget_get_parent (GTK_WIDGET (perspective)) ==
+            GTK_WIDGET (self->perspectives_stack));
+
+  id = ide_perspective_get_id (perspective);
+  titlebar = gtk_stack_get_child_by_name (self->titlebar_stack, id);
+
+  gtk_container_remove (GTK_CONTAINER (self->titlebar_stack), titlebar);
+  gtk_container_remove (GTK_CONTAINER (self->perspectives_stack), GTK_WIDGET (perspective));
+}
+
+/**
+ * ide_workbench_get_perspective:
+ * @self: An #IdeWorkbench.
+ *
+ * Gets the current perspective.
+ *
+ * Returns: (transfer none): An #IdePerspective.
+ */
+IdePerspective *
+ide_workbench_get_perspective (IdeWorkbench *self)
+{
+  GtkWidget *visible_child;
+
+  g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);
+
+  visible_child = gtk_stack_get_visible_child (self->perspectives_stack);
+
+  return IDE_PERSPECTIVE (visible_child);
+}
+
+void
+ide_workbench_set_perspective (IdeWorkbench   *self,
+                               IdePerspective *perspective)
+{
+  GActionGroup *actions;
+  const gchar *id;
+  GtkStack *stack;
+
+  g_return_if_fail (IDE_IS_WORKBENCH (self));
+  g_return_if_fail (IDE_IS_PERSPECTIVE (perspective));
+
+  if (IDE_IS_GREETER_PERSPECTIVE (perspective))
+    stack = self->top_stack;
+  else
+    stack = self->perspectives_stack;
+
+  id = ide_perspective_get_id (perspective);
+  gtk_stack_set_visible_child_name (self->titlebar_stack, id);
+  gtk_stack_set_visible_child_name (stack, id);
+
+  actions = ide_perspective_get_actions (perspective);
+  gtk_widget_insert_action_group (GTK_WIDGET (self), "perspective", actions);
+}
diff --git a/libide/ide-workbench.h b/libide/ide-workbench.h
new file mode 100644
index 0000000..af22553
--- /dev/null
+++ b/libide/ide-workbench.h
@@ -0,0 +1,69 @@
+/* ide-workbench.h
+ *
+ * Copyright (C) 2015 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/>.
+ */
+
+#ifndef IDE_WORKBENCH_H
+#define IDE_WORKBENCH_H
+
+#include <gtk/gtk.h>
+
+#include "ide-context.h"
+#include "ide-perspective.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_WORKBENCH (ide_workbench_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeWorkbench, ide_workbench, IDE, WORKBENCH, GtkApplicationWindow)
+
+void            ide_workbench_open_async         (IdeWorkbench         *self,
+                                                  GFile               **files,
+                                                  gint                  n_files,
+                                                  GCancellable         *cancellable,
+                                                  GAsyncReadyCallback   callback,
+                                                  gpointer              user_data);
+gboolean        ide_workbench_open_finish        (IdeWorkbench         *self,
+                                                  GAsyncResult         *result,
+                                                  GError              **error);
+void            ide_workbench_save_all_async     (IdeWorkbench         *self,
+                                                  GCancellable         *cancellable,
+                                                  GAsyncReadyCallback   callback,
+                                                  gpointer              user_data);
+gboolean        ide_workbench_save_all_finish    (IdeWorkbench         *self,
+                                                  GAsyncResult         *result,
+                                                  GError              **error);
+void            ide_workbench_focus              (IdeWorkbench         *self,
+                                                  GtkWidget            *widget);
+void            ide_workbench_close              (IdeWorkbench         *self);
+IdeContext     *ide_workbench_get_context        (IdeWorkbench         *self);
+void            ide_workbench_add_perspective    (IdeWorkbench         *self,
+                                                  IdePerspective       *perspective);
+void            ide_workbench_remove_perspective (IdeWorkbench         *self,
+                                                  IdePerspective       *perspective);
+IdePerspective *ide_workbench_get_perspective    (IdeWorkbench         *self);
+void            ide_workbench_set_perspective    (IdeWorkbench         *self,
+                                                  IdePerspective       *perspective);
+gboolean        ide_workbench_get_fullscreen     (IdeWorkbench         *self);
+void            ide_workbench_set_fullscreen     (IdeWorkbench         *self,
+                                                  gboolean              fullscreen);
+void            ide_workbench_views_foreach      (IdeWorkbench         *self,
+                                                  GtkCallback           callback,
+                                                  gpointer              user_data);
+
+G_END_DECLS
+
+#endif /* IDE_WORKBENCH_H */
diff --git a/libide/ide.c b/libide/ide.c
index d1ff190..eb84919 100644
--- a/libide/ide.c
+++ b/libide/ide.c
@@ -18,7 +18,6 @@
 
 #include <girepository.h>
 #include <glib/gi18n.h>
-#include <libgit2-glib/ggit.h>
 #include <stdlib.h>
 
 #include "gconstructor.h"
@@ -30,9 +29,6 @@
 #include "ide-git-vcs.h"
 #include "ide-gsettings-file-settings.h"
 #include "ide-modelines-file-settings.h"
-#include "ide-internal.h"
-#include "ide-project-miner.h"
-#include "ide-search-provider.h"
 
 #ifdef ENABLE_GJS_SCRIPTING
 # include "ide-gjs-script.h"
@@ -42,8 +38,6 @@
 # include "ide-pygobject-script.h"
 #endif
 
-#include "modeline-parser.h"
-
 static gboolean     programNameRead;
 static const gchar *programName = "libide";
 
@@ -78,13 +72,6 @@ ide_set_program_name (const gchar *program_name)
 static void
 ide_init_ctor (void)
 {
-  GgitFeatureFlags ggit_flags;
-
-  g_irepository_prepend_search_path (LIBDIR"/gnome-builder/girepository-1.0");
-
-  g_type_ensure (IDE_TYPE_CONTEXT);
-  g_type_ensure (IDE_TYPE_VCS);
-
   g_io_extension_point_register (IDE_FILE_SETTINGS_EXTENSION_POINT);
   g_io_extension_point_register (IDE_SCRIPT_EXTENSION_POINT);
   g_io_extension_point_register (IDE_VCS_EXTENSION_POINT);
@@ -124,27 +111,4 @@ ide_init_ctor (void)
                                   IDE_TYPE_DIRECTORY_VCS,
                                   IDE_VCS_EXTENSION_POINT".directory",
                                   -200);
-
-  modeline_parser_init ();
-
-  ggit_init ();
-
-  ggit_flags = ggit_get_features ();
-
-  if ((ggit_flags & GGIT_FEATURE_THREADS) == 0)
-    {
-      g_error (_("Builder requires libgit2-glib with threading support."));
-      exit (EXIT_FAILURE);
-    }
-
-  if ((ggit_flags & GGIT_FEATURE_SSH) == 0)
-    {
-      g_error (_("Builder requires libgit2-glib with SSH support."));
-      exit (EXIT_FAILURE);
-    }
-
-  /* TODO: tune what we startup here once we have IdeApplication/IdeWorkbench/etc. */
-  _ide_thread_pool_init ();
-
-  _ide_battery_monitor_init ();
 }
diff --git a/libide/ide.h b/libide/ide.h
index b6bd696..f50106c 100644
--- a/libide/ide.h
+++ b/libide/ide.h
@@ -25,6 +25,7 @@ G_BEGIN_DECLS
 
 #define IDE_INSIDE
 
+#include "ide-application.h"
 #include "ide-back-forward-item.h"
 #include "ide-back-forward-list.h"
 #include "ide-build-result.h"
@@ -56,10 +57,14 @@ G_BEGIN_DECLS
 #include "ide-highlight-engine.h"
 #include "ide-highlighter.h"
 #include "ide-indenter.h"
+#include "ide-layout-manager.h"
 #include "ide-log.h"
 #include "ide-macros.h"
 #include "ide-object.h"
 #include "ide-pattern-spec.h"
+#include "ide-perspective.h"
+#include "ide-preferences.h"
+#include "ide-preferences-addin.h"
 #include "ide-process.h"
 #include "ide-progress.h"
 #include "ide-project.h"
@@ -96,6 +101,7 @@ G_BEGIN_DECLS
 #include "ide-unsaved-files.h"
 #include "ide-vcs.h"
 #include "ide-vcs-uri.h"
+#include "ide-workbench.h"
 
 #include "directory/ide-directory-vcs.h"
 #include "doap/ide-doap-person.h"
diff --git a/libide/resources/libide.gresource.xml b/libide/resources/libide.gresource.xml
index 647b0ae..55ee581 100644
--- a/libide/resources/libide.gresource.xml
+++ b/libide/resources/libide.gresource.xml
@@ -18,5 +18,14 @@
 
     <file alias="keybindings/emacs.css">../../data/keybindings/emacs.css</file>
     <file alias="keybindings/vim.css">../../data/keybindings/vim.css</file>
+
+  </gresource>
+
+  <gresource prefix="/org/gnome/builder">
+    <file alias="ui/ide-editor-perspective.ui">../../data/ui/ide-editor-perspective.ui</file>
+    <file alias="ui/ide-greeter-perspective.ui">../../data/ui/ide-greeter-perspective.ui</file>
+    <file alias="ui/ide-greeter-project-row.ui">../../data/ui/ide-greeter-project-row.ui</file>
+    <file alias="ui/ide-workbench.ui">../../data/ui/ide-workbench.ui</file>
+    <file alias="ui/ide-workbench-header-bar.ui">../../data/ui/ide-workbench-header-bar.ui</file>
   </gresource>
 </gresources>
diff --git a/libide/util/ide-window-settings.c b/libide/util/ide-window-settings.c
new file mode 100644
index 0000000..41b8334
--- /dev/null
+++ b/libide/util/ide-window-settings.c
@@ -0,0 +1,154 @@
+/* ide-window-settings.c
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * 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/>.
+ */
+
+#include "ide-window-settings.h"
+
+#define GB_WINDOW_MIN_WIDTH  1280
+#define GB_WINDOW_MIN_HEIGHT 720
+#define SAVE_TIMEOUT_SECS    1
+
+static GSettings *settings;
+
+static gboolean
+ide_window_settings__window_save_settings_cb (gpointer data)
+{
+  GtkWindow *window = data;
+  GdkRectangle geom;
+  gboolean maximized;
+
+  g_assert (GTK_IS_WINDOW (window));
+  g_assert (G_IS_SETTINGS (settings));
+
+  g_object_set_data (G_OBJECT (window), "SETTINGS_HANDLER_ID", NULL);
+
+  gtk_window_get_size (window, &geom.width, &geom.height);
+  gtk_window_get_position (window, &geom.x, &geom.y);
+  maximized = gtk_window_is_maximized (window);
+
+  g_settings_set (settings, "window-size", "(ii)", geom.width, geom.height);
+  g_settings_set (settings, "window-position", "(ii)", geom.x, geom.y);
+  g_settings_set_boolean (settings, "window-maximized", maximized);
+
+  return G_SOURCE_REMOVE;
+}
+
+static gboolean
+ide_window_settings__window_configure_event (GtkWindow         *window,
+                                             GdkEventConfigure *event)
+{
+  guint handler;
+
+  g_assert (GTK_IS_WINDOW (window));
+  g_assert (event != NULL);
+  g_assert (G_IS_SETTINGS (settings));
+
+  handler = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (window), "SETTINGS_HANDLER_ID"));
+
+  if (handler == 0)
+    {
+      handler = g_timeout_add_seconds (SAVE_TIMEOUT_SECS,
+                                       ide_window_settings__window_save_settings_cb,
+                                       window);
+      g_object_set_data (G_OBJECT (window), "SETTINGS_HANDLER_ID", GINT_TO_POINTER (handler));
+    }
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static void
+ide_window_settings__window_realize (GtkWindow *window)
+{
+  GdkRectangle geom = { 0 };
+  gboolean maximized = FALSE;
+
+  g_assert (GTK_IS_WINDOW (window));
+  g_assert (G_IS_SETTINGS (settings));
+
+  g_settings_get (settings, "window-position", "(ii)", &geom.x, &geom.y);
+  g_settings_get (settings, "window-size", "(ii)", &geom.width, &geom.height);
+  g_settings_get (settings, "window-maximized", "b", &maximized);
+
+  geom.width = MAX (geom.width, GB_WINDOW_MIN_WIDTH);
+  geom.height = MAX (geom.height, GB_WINDOW_MIN_HEIGHT);
+  gtk_window_set_default_size (window, geom.width, geom.height);
+
+  gtk_window_move (window, geom.x, geom.y);
+
+  if (maximized)
+    gtk_window_maximize (window);
+}
+
+static void
+ide_window_settings__window_destroy (GtkWindow *window)
+{
+  guint handler;
+
+  g_assert (GTK_IS_WINDOW (window));
+  g_assert (G_IS_SETTINGS (settings));
+
+  handler = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (window), "SETTINGS_HANDLER_ID"));
+
+  if (handler != 0)
+    {
+      g_source_remove (handler);
+      g_object_set_data (G_OBJECT (window), "SETTINGS_HANDLER_ID", NULL);
+    }
+
+  g_signal_handlers_disconnect_by_func (window,
+                                        G_CALLBACK (ide_window_settings__window_configure_event),
+                                        NULL);
+
+  g_signal_handlers_disconnect_by_func (window,
+                                        G_CALLBACK (ide_window_settings__window_destroy),
+                                        NULL);
+
+  g_signal_handlers_disconnect_by_func (window,
+                                        G_CALLBACK (ide_window_settings__window_realize),
+                                        NULL);
+
+  g_object_unref (settings);
+}
+
+void
+ide_window_settings_register (GtkWindow *window)
+{
+  if (settings == NULL)
+    {
+      settings = g_settings_new ("org.gnome.builder");
+      g_object_add_weak_pointer (G_OBJECT (settings), (gpointer *)&settings);
+    }
+  else
+    {
+      g_object_ref (settings);
+    }
+
+  g_signal_connect (window,
+                    "configure-event",
+                    G_CALLBACK (ide_window_settings__window_configure_event),
+                    NULL);
+
+  g_signal_connect (window,
+                    "destroy",
+                    G_CALLBACK (ide_window_settings__window_destroy),
+                    NULL);
+
+  g_signal_connect (window,
+                    "realize",
+                    G_CALLBACK (ide_window_settings__window_realize),
+                    NULL);
+}
diff --git a/src/greeter/gb-greeter-pill-box.h b/libide/util/ide-window-settings.h
similarity index 57%
rename from src/greeter/gb-greeter-pill-box.h
rename to libide/util/ide-window-settings.h
index 394a333..a2b6542 100644
--- a/src/greeter/gb-greeter-pill-box.h
+++ b/libide/util/ide-window-settings.h
@@ -1,4 +1,4 @@
-/* gb-greeter-pill-box.h
+/* ide-window-settings.h
  *
  * Copyright (C) 2015 Christian Hergert <christian hergert me>
  *
@@ -16,22 +16,15 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#ifndef GB_GREETER_PILL_BOX_H
-#define GB_GREETER_PILL_BOX_H
+#ifndef IDE_WINDOW_SETTINGS_H
+#define IDE_WINDOW_SETTINGS_H
 
 #include <gtk/gtk.h>
 
 G_BEGIN_DECLS
 
-#define GB_TYPE_GREETER_PILL_BOX (gb_greeter_pill_box_get_type())
-
-G_DECLARE_FINAL_TYPE (GbGreeterPillBox, gb_greeter_pill_box, GB, GREETER_PILL_BOX, GtkEventBox)
-
-GtkWidget   *gb_greeter_pill_box_new       (const gchar      *label);
-const gchar *gb_greeter_pill_box_get_label (GbGreeterPillBox *self);
-void         gb_greeter_pill_box_set_label (GbGreeterPillBox *self,
-                                            const gchar      *label);
+void ide_window_settings_register (GtkWindow *window);
 
 G_END_DECLS
 
-#endif /* GB_GREETER_PILL_BOX_H */
+#endif /* IDE_WINDOW_SETTINGS_H */
diff --git a/src/Makefile.am b/src/Makefile.am
index 4e90e31..e6bb8e1 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -80,8 +80,6 @@ libgnome_builder_la_SOURCES = \
        editor/gb-editor-view-actions.h \
        editor/gb-editor-view-addin-private.h \
        editor/gb-editor-view-private.h \
-       greeter/gb-greeter-pill-box.c \
-       greeter/gb-greeter-pill-box.h \
        greeter/gb-greeter-project-row.c \
        greeter/gb-greeter-project-row.h \
        greeter/gb-greeter-window.c \
diff --git a/src/greeter/gb-greeter-project-row.c b/src/greeter/gb-greeter-project-row.c
index fe353d8..da4459a 100644
--- a/src/greeter/gb-greeter-project-row.c
+++ b/src/greeter/gb-greeter-project-row.c
@@ -22,10 +22,10 @@
 #include <ide.h>
 
 #include "egg-binding-group.h"
+#include "egg-pill-box.h"
 
 #include "gb-glib.h"
 #include "gb-greeter-project-row.h"
-#include "gb-greeter-pill-box.h"
 
 struct _GbGreeterProjectRow
 {
@@ -135,7 +135,7 @@ gb_greeter_project_row_add_languages (GbGreeterProjectRow *self,
           const gchar *name = languages [i - 1];
           GtkWidget *pill;
 
-          pill = g_object_new (GB_TYPE_GREETER_PILL_BOX,
+          pill = g_object_new (EGG_TYPE_PILL_BOX,
                                "visible", TRUE,
                                "label", name,
                                NULL);
diff --git a/src/main.c b/src/main.c
index fc04523..175851b 100644
--- a/src/main.c
+++ b/src/main.c
@@ -28,7 +28,6 @@
 #include <ide.h>
 #include <locale.h>
 
-#include "gb-application.h"
 #include "gb-icons-resources.h"
 
 int
@@ -58,7 +57,7 @@ main (int   argc,
 
   g_resources_register (gb_icons_get_resource ());
 
-  app = g_object_new (GB_TYPE_APPLICATION,
+  app = g_object_new (IDE_TYPE_APPLICATION,
                       "application-id", "org.gnome.Builder",
                       "flags", G_APPLICATION_HANDLES_OPEN,
                       NULL);
diff --git a/src/resources/gnome-builder.gresource.xml b/src/resources/gnome-builder.gresource.xml
index 18862b5..3cf649e 100644
--- a/src/resources/gnome-builder.gresource.xml
+++ b/src/resources/gnome-builder.gresource.xml
@@ -21,7 +21,6 @@
     <file alias="ui/gb-editor-settings-widget.ui">../../data/ui/gb-editor-settings-widget.ui</file>
     <file alias="ui/gb-editor-tweak-widget.ui">../../data/ui/gb-editor-tweak-widget.ui</file>
     <file alias="ui/gb-editor-view.ui">../../data/ui/gb-editor-view.ui</file>
-    <file alias="ui/gb-greeter-pill-box.ui">../../data/ui/gb-greeter-pill-box.ui</file>
     <file alias="ui/gb-greeter-project-row.ui">../../data/ui/gb-greeter-project-row.ui</file>
     <file alias="ui/gb-greeter-window.ui">../../data/ui/gb-greeter-window.ui</file>
     <file alias="ui/gb-html-view.ui">../../data/ui/gb-html-view.ui</file>
diff --git a/src/util/gb-glib.h b/src/util/gb-glib.h
index 1942a68..c7f9f36 100644
--- a/src/util/gb-glib.h
+++ b/src/util/gb-glib.h
@@ -42,8 +42,6 @@ G_BEGIN_DECLS
       } \
   } G_STMT_END
 
-gchar *gb_date_time_format_for_display (GDateTime *self);
-
 G_END_DECLS
 
 #endif /* GB_GLIB_H */


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