[gnome-builder/wip/chergert/dspy: 2/11] dspy: start on dspy plugin



commit 24f4354098e91aeff5f5827f66fa235e231c93af
Author: Christian Hergert <chergert redhat com>
Date:   Thu Apr 11 14:41:12 2019 -0700

    dspy: start on dspy plugin

 meson_options.txt                           |   1 +
 src/plugins/dspy/dspy-connection-model.c    | 521 ++++++++++++++++++++++++++++
 src/plugins/dspy/dspy-connection-model.h    |  42 +++
 src/plugins/dspy/dspy-connection-row.c      | 116 +++++++
 src/plugins/dspy/dspy-connection-row.h      |  41 +++
 src/plugins/dspy/dspy-connection-row.ui     |  19 +
 src/plugins/dspy/dspy-name-row.c            | 125 +++++++
 src/plugins/dspy/dspy-name-row.h            |  36 ++
 src/plugins/dspy/dspy-name-row.ui           |  33 ++
 src/plugins/dspy/dspy-name-view.c           | 205 +++++++++++
 src/plugins/dspy/dspy-name-view.h           |  40 +++
 src/plugins/dspy/dspy-name-view.ui          | 154 ++++++++
 src/plugins/dspy/dspy-name.c                | 289 +++++++++++++++
 src/plugins/dspy/dspy-name.h                |  49 +++
 src/plugins/dspy/dspy-path-model.c          | 472 +++++++++++++++++++++++++
 src/plugins/dspy/dspy-path-model.h          |  36 ++
 src/plugins/dspy/dspy-plugin.c              |  34 ++
 src/plugins/dspy/dspy.gresource.xml         |  11 +
 src/plugins/dspy/dspy.plugin                |   9 +
 src/plugins/dspy/gbp-dspy-surface.c         | 208 +++++++++++
 src/plugins/dspy/gbp-dspy-surface.h         |  33 ++
 src/plugins/dspy/gbp-dspy-surface.ui        |  79 +++++
 src/plugins/dspy/gbp-dspy-workspace-addin.c |  92 +++++
 src/plugins/dspy/gbp-dspy-workspace-addin.h |  31 ++
 src/plugins/dspy/gtk/menus.ui               |  15 +
 src/plugins/dspy/meson.build                |  23 ++
 src/plugins/meson.build                     |   2 +
 27 files changed, 2716 insertions(+)
---
diff --git a/meson_options.txt b/meson_options.txt
index 57fe9301c..3d6fc2e40 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -31,6 +31,7 @@ option('plugin_color_picker', type: 'boolean')
 option('plugin_ctags', type: 'boolean')
 option('plugin_devhelp', type: 'boolean')
 option('plugin_deviced', type: 'boolean', value: false)
+option('plugin_dspy', type: 'boolean')
 option('plugin_editorconfig', type: 'boolean')
 option('plugin_eslint', type: 'boolean')
 option('plugin_file_search', type: 'boolean')
diff --git a/src/plugins/dspy/dspy-connection-model.c b/src/plugins/dspy/dspy-connection-model.c
new file mode 100644
index 000000000..72a5f5412
--- /dev/null
+++ b/src/plugins/dspy/dspy-connection-model.c
@@ -0,0 +1,521 @@
+/* dspy-connection-model.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "dspy-connection-model"
+
+#include "config.h"
+
+#include "dspy-connection-model.h"
+#include "dspy-name.h"
+
+struct _DspyConnectionModel
+{
+  GObject          parent;
+
+  GDBusConnection *connection;
+  GCancellable    *cancellable;
+  GSequence       *names;
+  GDBusProxy      *bus_proxy;
+  gchar           *address;
+  GBusType         bus_type;
+
+  guint            name_owner_changed_handler;
+};
+
+enum {
+  PROP_0,
+  PROP_CONNECTION,
+  N_PROPS
+};
+
+static guint
+dspy_connection_model_get_n_items (GListModel *model)
+{
+  DspyConnectionModel *self = DSPY_CONNECTION_MODEL (model);
+  return g_sequence_get_length (self->names);
+}
+
+static GType
+dspy_connection_model_get_item_type (GListModel *model)
+{
+  /* XXX: switch to type */
+  return G_TYPE_OBJECT;
+}
+
+static gpointer
+dspy_connection_model_get_item (GListModel *model,
+                                guint       position)
+{
+  DspyConnectionModel *self = DSPY_CONNECTION_MODEL (model);
+  GSequenceIter *iter = g_sequence_get_iter_at_pos (self->names, position);
+
+  if (g_sequence_iter_is_end (iter))
+    return NULL;
+
+  return g_object_ref (g_sequence_get (iter));
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+  iface->get_item_type = dspy_connection_model_get_item_type;
+  iface->get_n_items = dspy_connection_model_get_n_items;
+  iface->get_item = dspy_connection_model_get_item;
+}
+
+G_DEFINE_TYPE_WITH_CODE (DspyConnectionModel, dspy_connection_model, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+dspy_connection_model_finalize (GObject *object)
+{
+  DspyConnectionModel *self = (DspyConnectionModel *)object;
+
+  g_assert (self->connection == NULL);
+  g_assert (self->cancellable == NULL);
+
+  g_clear_object (&self->connection);
+  g_clear_object (&self->cancellable);
+  g_clear_object (&self->bus_proxy);
+  g_clear_pointer (&self->names, g_sequence_free);
+  g_clear_pointer (&self->address, g_free);
+
+  G_OBJECT_CLASS (dspy_connection_model_parent_class)->finalize (object);
+}
+
+static void
+dspy_connection_model_dispose (GObject *object)
+{
+  DspyConnectionModel *self = (DspyConnectionModel *)object;
+
+  dspy_connection_model_set_connection (self, NULL);
+
+  G_OBJECT_CLASS (dspy_connection_model_parent_class)->dispose (object);
+}
+
+static void
+dspy_connection_model_get_property (GObject    *object,
+                                    guint       prop_id,
+                                    GValue     *value,
+                                    GParamSpec *pspec)
+{
+  DspyConnectionModel *self = DSPY_CONNECTION_MODEL (object);
+
+  switch (prop_id)
+    {
+    case PROP_CONNECTION:
+      g_value_set_object (value, dspy_connection_model_get_connection (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dspy_connection_model_set_property (GObject      *object,
+                                    guint         prop_id,
+                                    const GValue *value,
+                                    GParamSpec   *pspec)
+{
+  DspyConnectionModel *self = DSPY_CONNECTION_MODEL (object);
+
+  switch (prop_id)
+    {
+    case PROP_CONNECTION:
+      dspy_connection_model_set_connection (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dspy_connection_model_class_init (DspyConnectionModelClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = dspy_connection_model_dispose;
+  object_class->finalize = dspy_connection_model_finalize;
+  object_class->get_property = dspy_connection_model_get_property;
+  object_class->set_property = dspy_connection_model_set_property;
+
+  /**
+   * DspyConnectionModel:connection:
+   *
+   * The "connection" property contains the #GDBusConnection that will be monitored
+   * for changes to the bus.
+   */
+  properties [PROP_CONNECTION] =
+    g_param_spec_object ("connection",
+                         "Connection",
+                         "A GDBus connection for the source of bus changes",
+                         G_TYPE_DBUS_CONNECTION,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+dspy_connection_model_init (DspyConnectionModel *self)
+{
+  self->names = g_sequence_new (g_object_unref);
+}
+
+static void
+dspy_connection_model_get_name_owner_cb (GObject      *object,
+                                         GAsyncResult *result,
+                                         gpointer      user_data)
+{
+  GDBusProxy *proxy = (GDBusProxy *)object;
+  g_autoptr(GVariant) ret = NULL;
+  g_autoptr(DspyName) name = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (DSPY_IS_NAME (name));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (DSPY_IS_NAME (name));
+
+  if ((ret = g_dbus_proxy_call_finish (proxy, result, &error)))
+    {
+      const gchar *owner = NULL;
+
+      g_variant_get (ret, "(&s)", &owner);
+      dspy_name_set_owner (name, owner);
+    }
+}
+
+static void
+dspy_connection_model_get_process_id_cb (GObject      *object,
+                                         GAsyncResult *result,
+                                         gpointer      user_data)
+{
+  GDBusProxy *proxy = (GDBusProxy *)object;
+  g_autoptr(GVariant) ret = NULL;
+  g_autoptr(DspyName) name = user_data;
+  g_autoptr(GError) error = NULL;
+  GPid pid = 0;
+
+  g_assert (DSPY_IS_NAME (name));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (DSPY_IS_NAME (name));
+
+  if ((ret = g_dbus_proxy_call_finish (proxy, result, &error)))
+    g_variant_get (ret, "(u)", &pid);
+
+  dspy_name_set_pid (name, pid);
+}
+
+static void
+dspy_connection_model_add_names (DspyConnectionModel *self,
+                                 const gchar * const *names,
+                                 gboolean             activatable)
+{
+  g_assert (DSPY_IS_CONNECTION_MODEL (self));
+  g_assert (names != NULL);
+
+  for (guint i = 0; names[i]; i++)
+    {
+      g_autoptr(DspyName) name = dspy_name_new (names[i], activatable);
+      GSequenceIter *seq;
+      guint removed = 0;
+
+      /* Skip if we already know about the name, but replace
+       * the item if we found something activatable.
+       */
+      if ((seq = g_sequence_lookup (self->names,
+                                    name,
+                                    (GCompareDataFunc) dspy_name_compare,
+                                    NULL)))
+        {
+          if (!activatable)
+            continue;
+          g_sequence_remove (seq);
+          removed++;
+        }
+
+      seq = g_sequence_insert_sorted (self->names,
+                                      g_object_ref (name),
+                                      (GCompareDataFunc) dspy_name_compare,
+                                      NULL);
+
+      g_list_model_items_changed (G_LIST_MODEL (self),
+                                  g_sequence_iter_get_position (seq),
+                                  removed, 1);
+
+      g_dbus_proxy_call (self->bus_proxy,
+                         "GetConnectionUnixProcessID",
+                         g_variant_new ("(s)", names[i]),
+                         G_DBUS_CALL_FLAGS_NONE,
+                         -1,
+                         self->cancellable,
+                         dspy_connection_model_get_process_id_cb,
+                         g_object_ref (name));
+
+      if (names[i][0] != ':')
+        g_dbus_proxy_call (self->bus_proxy,
+                           "GetNameOwner",
+                           g_variant_new ("(s)", names[i]),
+                           G_DBUS_CALL_FLAGS_NONE,
+                           -1,
+                           self->cancellable,
+                           dspy_connection_model_get_name_owner_cb,
+                           g_object_ref (name));
+    }
+}
+
+static void
+dspy_connection_model_name_owner_changed_cb (GDBusConnection *connection,
+                                             const gchar     *sender_name,
+                                             const gchar     *object_path,
+                                             const gchar     *interface_name,
+                                             const gchar     *signal_name,
+                                             GVariant        *params,
+                                             gpointer         user_data)
+{
+  DspyConnectionModel *self = user_data;
+  g_autoptr(DspyName) name = NULL;
+  GSequenceIter *seq;
+  const gchar *vname;
+  const gchar *vold_name;
+  const gchar *vnew_name;
+
+  g_assert (G_IS_DBUS_CONNECTION (connection));
+  g_assert (DSPY_IS_CONNECTION_MODEL (self));
+
+  g_variant_get (params, "(&s&s&s)", &vname, &vold_name, &vnew_name);
+
+  name = dspy_name_new (vname, FALSE);
+  seq = g_sequence_lookup (self->names,
+                           name,
+                           (GCompareDataFunc) dspy_name_compare,
+                           NULL);
+
+  if (seq == NULL && vnew_name[0])
+    {
+      const gchar *names[] = { vname, NULL };
+      dspy_connection_model_add_names (self, names, FALSE);
+    }
+  else if (!vnew_name[0])
+    {
+      guint position = g_sequence_iter_get_position (seq);
+      g_sequence_remove (seq);
+      g_list_model_items_changed (G_LIST_MODEL (self), position, 1, 0);
+    }
+
+#if 0
+  g_print ("%s %s %s %s %s\n",
+           sender_name, object_path, interface_name, signal_name,
+           g_variant_print (params, TRUE));
+#endif
+}
+
+static void
+dspy_connection_model_list_activatable_names_cb (GObject      *object,
+                                                 GAsyncResult *result,
+                                                 gpointer      user_data)
+{
+  GDBusProxy *proxy = (GDBusProxy *)object;
+  g_autoptr(DspyConnectionModel) self = user_data;
+  g_autoptr(GVariant) ret = NULL;
+  g_autoptr(GError) error = NULL;
+  g_auto(GStrv) strv = NULL;
+
+  g_assert (G_IS_DBUS_PROXY (proxy));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (DSPY_IS_CONNECTION_MODEL (self));
+
+  if (!(ret = g_dbus_proxy_call_finish (proxy, result, &error)))
+    {
+      g_warning ("Failed to list activatable names: %s", error->message);
+      return;
+    }
+
+  g_variant_get (ret, "(^as)", &strv);
+  dspy_connection_model_add_names (self, (const gchar * const *)strv, TRUE);
+}
+
+static void
+dspy_connection_model_list_names_cb (GObject      *object,
+                                     GAsyncResult *result,
+                                     gpointer      user_data)
+{
+  GDBusProxy *proxy = (GDBusProxy *)object;
+  g_autoptr(DspyConnectionModel) self = user_data;
+  g_autoptr(GVariant) ret = NULL;
+  g_autoptr(GError) error = NULL;
+  g_auto(GStrv) strv = NULL;
+
+  g_assert (G_IS_DBUS_PROXY (proxy));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (DSPY_IS_CONNECTION_MODEL (self));
+
+  if (!(ret = g_dbus_proxy_call_finish (proxy, result, &error)))
+    {
+      g_warning ("Failed to list names: %s", error->message);
+      return;
+    }
+
+  g_variant_get (ret, "(^as)", &strv);
+  dspy_connection_model_add_names (self, (const gchar * const *)strv, FALSE);
+}
+
+DspyConnectionModel *
+dspy_connection_model_new (void)
+{
+  return g_object_new (DSPY_TYPE_CONNECTION_MODEL, NULL);
+}
+
+/**
+ * dspy_connection_model_get_connection:
+ * @self: a #DspyConnectionModel
+ *
+ * Gets the #GDBusConnection used for the model.
+ *
+ * Returns: (transfer none) (nullable): a #GDBusConnection or %NULL
+ */
+GDBusConnection *
+dspy_connection_model_get_connection (DspyConnectionModel *self)
+{
+  g_return_val_if_fail (DSPY_IS_CONNECTION_MODEL (self), NULL);
+
+  return self->connection;
+}
+
+void
+dspy_connection_model_set_connection (DspyConnectionModel *self,
+                                      GDBusConnection     *connection)
+{
+  g_return_if_fail (DSPY_IS_CONNECTION_MODEL (self));
+
+  if (self->connection == connection)
+    return;
+
+  if (self->connection != NULL)
+    {
+      g_cancellable_cancel (self->cancellable);
+      g_dbus_connection_signal_unsubscribe (self->connection,
+                                            self->name_owner_changed_handler);
+      g_clear_object (&self->cancellable);
+      g_clear_object (&self->connection);
+      g_clear_object (&self->bus_proxy);
+      g_clear_pointer (&self->names, g_sequence_free);
+      self->names = g_sequence_new (g_object_unref);
+    }
+
+  g_assert (self->cancellable == NULL);
+  g_assert (self->connection == NULL);
+  g_assert (self->bus_proxy == NULL);
+  g_assert (g_sequence_is_empty (self->names));
+
+  if (connection != NULL)
+    {
+      g_autoptr(GError) error = NULL;
+
+      self->connection = g_object_ref (connection);
+      self->cancellable = g_cancellable_new ();
+      self->name_owner_changed_handler =
+        g_dbus_connection_signal_subscribe (self->connection,
+                                            NULL,
+                                            "org.freedesktop.DBus",
+                                            "NameOwnerChanged",
+                                            NULL,
+                                            NULL,
+                                            0,
+                                            dspy_connection_model_name_owner_changed_cb,
+                                            self,
+                                            NULL);
+      self->bus_proxy = g_dbus_proxy_new_sync (self->connection,
+                                               G_DBUS_PROXY_FLAGS_NONE,
+                                               NULL,
+                                               "org.freedesktop.DBus",
+                                               "/org/freedesktop/DBus",
+                                               "org.freedesktop.DBus",
+                                               self->cancellable, &error);
+
+      if (self->bus_proxy == NULL)
+        {
+          g_warning ("Failed to create DBus proxy: %s", error->message);
+          goto notify;
+        }
+
+      g_dbus_proxy_call (self->bus_proxy,
+                         "ListActivatableNames",
+                         g_variant_new ("()"),
+                         G_DBUS_CALL_FLAGS_NONE,
+                         -1,
+                         self->cancellable,
+                         dspy_connection_model_list_activatable_names_cb,
+                         g_object_ref (self));
+
+      g_dbus_proxy_call (self->bus_proxy,
+                         "ListNames",
+                         g_variant_new ("()"),
+                         G_DBUS_CALL_FLAGS_NONE,
+                         -1,
+                         self->cancellable,
+                         dspy_connection_model_list_names_cb,
+                         g_object_ref (self));
+    }
+
+notify:
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CONNECTION]);
+}
+
+GBusType
+dspy_connection_model_get_bus_type (DspyConnectionModel *self)
+{
+  g_return_val_if_fail (DSPY_IS_CONNECTION_MODEL (self), G_BUS_TYPE_NONE);
+
+  return self->bus_type;
+}
+
+void
+dspy_connection_model_set_bus_type (DspyConnectionModel *self,
+                                    GBusType             bus_type)
+{
+  g_return_if_fail (DSPY_IS_CONNECTION_MODEL (self));
+
+  self->bus_type = bus_type;
+}
+
+const gchar *
+dspy_connection_model_get_address (DspyConnectionModel *self)
+{
+  g_return_val_if_fail (DSPY_IS_CONNECTION_MODEL (self), NULL);
+
+  return self->address;
+}
+
+void
+dspy_connection_model_set_address (DspyConnectionModel *self,
+                                   const gchar         *address)
+{
+  g_return_if_fail (DSPY_IS_CONNECTION_MODEL (self));
+
+  if (g_strcmp0 (self->address, address) != 0)
+    {
+      g_free (self->address);
+      self->address = g_strdup (address);
+    }
+}
diff --git a/src/plugins/dspy/dspy-connection-model.h b/src/plugins/dspy/dspy-connection-model.h
new file mode 100644
index 000000000..0e442ecbc
--- /dev/null
+++ b/src/plugins/dspy/dspy-connection-model.h
@@ -0,0 +1,42 @@
+/* dspy-connection-model.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define DSPY_TYPE_CONNECTION_MODEL (dspy_connection_model_get_type())
+
+G_DECLARE_FINAL_TYPE (DspyConnectionModel, dspy_connection_model, DSPY, CONNECTION_MODEL, GObject)
+
+DspyConnectionModel *dspy_connection_model_new            (void);
+GBusType             dspy_connection_model_get_bus_type   (DspyConnectionModel *self);
+void                 dspy_connection_model_set_bus_type   (DspyConnectionModel *self,
+                                                           GBusType             bus_type);
+const gchar         *dspy_connection_model_get_address    (DspyConnectionModel *self);
+void                 dspy_connection_model_set_address    (DspyConnectionModel *self,
+                                                           const gchar         *address);
+GDBusConnection     *dspy_connection_model_get_connection (DspyConnectionModel *self);
+void                 dspy_connection_model_set_connection (DspyConnectionModel *self,
+                                                           GDBusConnection     *connection);
+
+G_END_DECLS
diff --git a/src/plugins/dspy/dspy-connection-row.c b/src/plugins/dspy/dspy-connection-row.c
new file mode 100644
index 000000000..c06dc4b7d
--- /dev/null
+++ b/src/plugins/dspy/dspy-connection-row.c
@@ -0,0 +1,116 @@
+/* dspy-connection-row.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "dspy-connection-row"
+
+#include "config.h"
+
+#include "dspy-connection-row.h"
+
+struct _DspyConnectionRow
+{
+  GtkListBoxRow  parent;
+  GtkLabel      *label;
+  gchar         *address;
+  GBusType       bus_type;
+};
+
+G_DEFINE_TYPE (DspyConnectionRow, dspy_connection_row, GTK_TYPE_LIST_BOX_ROW)
+
+static void
+dspy_connection_row_finalize (GObject *object)
+{
+  DspyConnectionRow *self = (DspyConnectionRow *)object;
+
+  g_clear_pointer (&self->address, g_free);
+
+  G_OBJECT_CLASS (dspy_connection_row_parent_class)->finalize (object);
+}
+
+static void
+dspy_connection_row_class_init (DspyConnectionRowClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = dspy_connection_row_finalize;
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/plugins/dspy/dspy-connection-row.ui");
+  gtk_widget_class_bind_template_child (widget_class, DspyConnectionRow, label);
+}
+
+static void
+dspy_connection_row_init (DspyConnectionRow *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+DspyConnectionRow *
+dspy_connection_row_new (void)
+{
+  return g_object_new (DSPY_TYPE_CONNECTION_ROW, NULL);
+}
+
+const gchar *
+dspy_connection_row_get_address (DspyConnectionRow *self)
+{
+  g_return_val_if_fail (DSPY_IS_CONNECTION_ROW (self), NULL);
+
+  return self->address;
+}
+
+void
+dspy_connection_row_set_address (DspyConnectionRow *self,
+                                 const gchar       *address)
+{
+  g_return_if_fail (DSPY_IS_CONNECTION_ROW (self));
+
+  if (g_strcmp0 (address, self->address) != 0)
+    {
+      g_free (self->address);
+      self->address = g_strdup (address);
+    }
+}
+
+GBusType
+dspy_connection_row_get_bus_type (DspyConnectionRow *self)
+{
+  g_return_val_if_fail (DSPY_IS_CONNECTION_ROW (self), 0);
+
+  return self->bus_type;
+}
+
+void
+dspy_connection_row_set_bus_type (DspyConnectionRow *self,
+                                  GBusType           bus_type)
+{
+  g_return_if_fail (DSPY_IS_CONNECTION_ROW (self));
+
+  self->bus_type = bus_type;
+}
+
+void
+dspy_connection_row_set_title (DspyConnectionRow *self,
+                               const gchar       *title)
+{
+  g_return_if_fail (DSPY_IS_CONNECTION_ROW (self));
+
+  gtk_label_set_label (self->label, title);
+}
diff --git a/src/plugins/dspy/dspy-connection-row.h b/src/plugins/dspy/dspy-connection-row.h
new file mode 100644
index 000000000..3b5ca7a4a
--- /dev/null
+++ b/src/plugins/dspy/dspy-connection-row.h
@@ -0,0 +1,41 @@
+/* dspy-connection-row.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define DSPY_TYPE_CONNECTION_ROW (dspy_connection_row_get_type())
+
+G_DECLARE_FINAL_TYPE (DspyConnectionRow, dspy_connection_row, DSPY, CONNECTION_ROW, GtkListBoxRow)
+
+DspyConnectionRow *dspy_connection_row_new          (void);
+const gchar       *dspy_connection_row_get_address  (DspyConnectionRow *self);
+void               dspy_connection_row_set_address  (DspyConnectionRow *self,
+                                                     const gchar       *address);
+GBusType           dspy_connection_row_get_bus_type (DspyConnectionRow *self);
+void               dspy_connection_row_set_bus_type (DspyConnectionRow *self,
+                                                     GBusType           bus_type);
+void               dspy_connection_row_set_title    (DspyConnectionRow *self,
+                                                     const gchar       *title);
+
+G_END_DECLS
diff --git a/src/plugins/dspy/dspy-connection-row.ui b/src/plugins/dspy/dspy-connection-row.ui
new file mode 100644
index 000000000..06428c2b1
--- /dev/null
+++ b/src/plugins/dspy/dspy-connection-row.ui
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="DspyConnectionRow" parent="GtkListBoxRow">
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">horizontal</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkLabel" id="label">
+            <property name="margin">6</property>
+            <property name="hexpand">true</property>
+            <property name="visible">true</property>
+            <property name="xalign">0.0</property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/plugins/dspy/dspy-name-row.c b/src/plugins/dspy/dspy-name-row.c
new file mode 100644
index 000000000..054e78acb
--- /dev/null
+++ b/src/plugins/dspy/dspy-name-row.c
@@ -0,0 +1,125 @@
+/* dspy-name-row.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "dspy-name-row"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "dspy-name-row.h"
+
+struct _DspyNameRow
+{
+  GtkListBoxRow  parent;
+
+  DspyName      *name;
+
+  GtkLabel      *label;
+  GtkLabel      *subtitle;
+};
+
+G_DEFINE_TYPE (DspyNameRow, dspy_name_row, GTK_TYPE_LIST_BOX_ROW)
+
+static void
+update_subtitle (DspyNameRow *self)
+{
+  g_autoptr(GString) str = g_string_new (NULL);
+  GPid pid = dspy_name_get_pid (self->name);
+
+  if (dspy_name_get_activatable (self->name))
+    g_string_append_printf (str, _("%s: %s"), _("Activatable"), _("Yes"));
+  else
+    g_string_append_printf (str, _("%s: %s"), _("Activatable"), _("No"));
+
+  if (pid != 0)
+    {
+      g_string_append (str, ", ");
+      g_string_append_printf (str, _("%s: %u"), _("Pid"), pid);
+    }
+
+  gtk_label_set_label (self->subtitle, str->str);
+}
+
+static void
+dspy_name_row_finalize (GObject *object)
+{
+  DspyNameRow *self = (DspyNameRow *)object;
+
+  g_clear_object (&self->name);
+
+  G_OBJECT_CLASS (dspy_name_row_parent_class)->finalize (object);
+}
+
+static void
+dspy_name_row_class_init (DspyNameRowClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = dspy_name_row_finalize;
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/plugins/dspy/dspy-name-row.ui");
+  gtk_widget_class_bind_template_child (widget_class, DspyNameRow, label);
+  gtk_widget_class_bind_template_child (widget_class, DspyNameRow, subtitle);
+}
+
+static void
+dspy_name_row_init (DspyNameRow *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+DspyNameRow *
+dspy_name_row_new (DspyName *name)
+{
+  DspyNameRow *self;
+
+  g_return_val_if_fail (DSPY_IS_NAME (name), NULL);
+
+  self = g_object_new (DSPY_TYPE_NAME_ROW, NULL);
+  self->name = g_object_ref (name);
+
+  gtk_label_set_label (self->label, dspy_name_get_name (name));
+
+  g_signal_connect_object (name,
+                           "notify::pid",
+                           G_CALLBACK (update_subtitle),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  update_subtitle (self);
+
+  return g_steal_pointer (&self);
+}
+
+/**
+ * dspy_name_row_get_name:
+ * @self: a #DspyNameRow
+ *
+ * Returns: (transfer none): a #DspyNameRow or %NULL
+ */
+DspyName *
+dspy_name_row_get_name (DspyNameRow *self)
+{
+  g_return_val_if_fail (DSPY_IS_NAME_ROW (self), NULL);
+
+  return self->name;
+}
diff --git a/src/plugins/dspy/dspy-name-row.h b/src/plugins/dspy/dspy-name-row.h
new file mode 100644
index 000000000..39d6c4ce8
--- /dev/null
+++ b/src/plugins/dspy/dspy-name-row.h
@@ -0,0 +1,36 @@
+/* dspy-name-row.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+#include "dspy-name.h"
+
+G_BEGIN_DECLS
+
+#define DSPY_TYPE_NAME_ROW (dspy_name_row_get_type())
+
+G_DECLARE_FINAL_TYPE (DspyNameRow, dspy_name_row, DSPY, NAME_ROW, GtkListBoxRow)
+
+DspyNameRow *dspy_name_row_new      (DspyName    *name);
+DspyName    *dspy_name_row_get_name (DspyNameRow *self);
+
+G_END_DECLS
diff --git a/src/plugins/dspy/dspy-name-row.ui b/src/plugins/dspy/dspy-name-row.ui
new file mode 100644
index 000000000..6fa701b5d
--- /dev/null
+++ b/src/plugins/dspy/dspy-name-row.ui
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="DspyNameRow" parent="GtkListBoxRow">
+    <child>
+      <object class="GtkBox">
+        <property name="margin">6</property>
+        <property name="spacing">3</property>
+        <property name="orientation">vertical</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkLabel" id="label">
+            <property name="hexpand">true</property>
+            <property name="visible">true</property>
+            <property name="xalign">0.0</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel" id="subtitle">
+            <property name="hexpand">true</property>
+            <property name="visible">true</property>
+            <property name="xalign">0.0</property>
+            <property name="visible">true</property>
+            <property name="ellipsize">end</property>
+            <attributes>
+              <attribute name="foreground-alpha" value="39321"/>
+              <attribute name="scale" value="0.75"/>
+            </attributes>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/plugins/dspy/dspy-name-view.c b/src/plugins/dspy/dspy-name-view.c
new file mode 100644
index 000000000..e71d45c22
--- /dev/null
+++ b/src/plugins/dspy/dspy-name-view.c
@@ -0,0 +1,205 @@
+/* dspy-name-view.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "dspy-name-view"
+
+#include "config.h"
+
+#include <dazzle.h>
+
+#include "dspy-name-view.h"
+#include "dspy-path-model.h"
+
+struct _DspyNameView
+{
+  GtkBin           parent;
+
+  GDBusConnection *connection;
+  DspyName        *name;
+
+  GtkLabel        *address_label;
+  GtkLabel        *name_label;
+  GtkLabel        *unique_label;
+  GtkTreeView     *tree_view;
+  GtkButton       *refresh_button;
+} DspyNameViewPrivate;
+
+G_DEFINE_TYPE (DspyNameView, dspy_name_view, GTK_TYPE_BIN)
+
+static void
+on_refresh_button_clicked_cb (DspyNameView *self,
+                              GtkButton    *button)
+{
+  g_autoptr(DspyPathModel) path_model = NULL;
+
+  g_assert (DSPY_IS_NAME_VIEW (self));
+  g_assert (GTK_IS_BUTTON (button));
+
+  if (self->name == NULL)
+    return;
+
+  gtk_label_set_label (self->name_label, dspy_name_get_name (self->name));
+  gtk_label_set_label (self->unique_label, dspy_name_get_owner (self->name));
+
+  path_model = dspy_path_model_new (self->connection, self->name);
+  gtk_tree_view_set_model (self->tree_view, GTK_TREE_MODEL (path_model));
+}
+
+static void
+on_tree_view_row_activated_cb (DspyNameView      *self,
+                               GtkTreePath       *tree_path,
+                               GtkTreeViewColumn *column,
+                               GtkTreeView       *tree_view)
+{
+  GtkTreeModel *model;
+  GtkTreeIter iter, children;
+
+  g_assert (DSPY_IS_NAME_VIEW (self));
+  g_assert (tree_path != NULL);
+  g_assert (!column || GTK_IS_TREE_VIEW_COLUMN (column));
+  g_assert (GTK_IS_TREE_VIEW (tree_view));
+
+  if (gtk_tree_view_row_expanded (tree_view, tree_path))
+    {
+      gtk_tree_view_collapse_row (tree_view, tree_path);
+      return;
+    }
+
+  /* Quick cheat to always expand two-levels, so we can see things easier.
+   * Once we have a "type" of row to deal with, we can be more selective.
+   */
+
+  gtk_tree_view_expand_row (tree_view, tree_path, FALSE);
+
+  model = gtk_tree_view_get_model (tree_view);
+
+  if (gtk_tree_model_get_iter (model, &iter, tree_path) &&
+      gtk_tree_model_iter_children (model, &children, &iter))
+    {
+      g_autoptr(GtkTreePath) copy = gtk_tree_path_copy (tree_path);
+
+      gtk_tree_path_down (copy);
+
+      do
+        {
+          gtk_tree_view_expand_row (tree_view, copy, FALSE);
+          gtk_tree_path_next (copy);
+        }
+      while (gtk_tree_model_iter_next (model, &children));
+    }
+}
+
+static void
+dspy_name_view_finalize (GObject *object)
+{
+  DspyNameView *self = (DspyNameView *)object;
+
+  g_clear_object (&self->connection);
+  g_clear_object (&self->name);
+
+  G_OBJECT_CLASS (dspy_name_view_parent_class)->finalize (object);
+}
+
+static void
+dspy_name_view_class_init (DspyNameViewClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = dspy_name_view_finalize;
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/plugins/dspy/dspy-name-view.ui");
+  gtk_widget_class_bind_template_child (widget_class, DspyNameView, address_label);
+  gtk_widget_class_bind_template_child (widget_class, DspyNameView, name_label);
+  gtk_widget_class_bind_template_child (widget_class, DspyNameView, refresh_button);
+  gtk_widget_class_bind_template_child (widget_class, DspyNameView, tree_view);
+  gtk_widget_class_bind_template_child (widget_class, DspyNameView, unique_label);
+
+  g_type_ensure (DZL_TYPE_THREE_GRID);
+}
+
+static void
+dspy_name_view_init (DspyNameView *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  g_signal_connect_object (self->refresh_button,
+                           "clicked",
+                           G_CALLBACK (on_refresh_button_clicked_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->tree_view,
+                           "row-activated",
+                           G_CALLBACK (on_tree_view_row_activated_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+}
+
+DspyNameView *
+dspy_name_view_new (void)
+{
+  return g_object_new (DSPY_TYPE_NAME_VIEW, NULL);
+}
+
+static void
+dspy_name_view_clear (DspyNameView *self)
+{
+  g_return_if_fail (DSPY_IS_NAME_VIEW (self));
+
+  gtk_label_set_label (self->address_label, NULL);
+  gtk_label_set_label (self->name_label, NULL);
+  gtk_label_set_label (self->unique_label, NULL);
+  gtk_tree_view_set_model (self->tree_view, NULL);
+}
+
+void
+dspy_name_view_set_name (DspyNameView    *self,
+                         GDBusConnection *connection,
+                         GBusType         bus_type,
+                         const gchar     *address,
+                         DspyName        *name)
+{
+  g_autofree gchar *bus_address = NULL;
+  g_autoptr(DspyPathModel) path_model = NULL;
+
+  g_return_if_fail (DSPY_IS_NAME_VIEW (self));
+
+  if (self->connection == connection && self->name == name)
+    return;
+
+  dspy_name_view_clear (self);
+
+  if (name == NULL)
+    return;
+
+  g_set_object (&self->connection, connection);
+  g_set_object (&self->name, name);
+
+  if (bus_type != G_BUS_TYPE_NONE)
+    address = bus_address = g_dbus_address_get_for_bus_sync (bus_type, NULL, NULL);
+
+  gtk_label_set_label (self->address_label, address);
+  gtk_label_set_label (self->name_label, dspy_name_get_name (name));
+  gtk_label_set_label (self->unique_label, dspy_name_get_owner (name));
+
+  path_model = dspy_path_model_new (self->connection, self->name);
+  gtk_tree_view_set_model (self->tree_view, GTK_TREE_MODEL (path_model));
+}
diff --git a/src/plugins/dspy/dspy-name-view.h b/src/plugins/dspy/dspy-name-view.h
new file mode 100644
index 000000000..05d3eabf7
--- /dev/null
+++ b/src/plugins/dspy/dspy-name-view.h
@@ -0,0 +1,40 @@
+/* dspy-name-view.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+#include "dspy-name.h"
+
+G_BEGIN_DECLS
+
+#define DSPY_TYPE_NAME_VIEW (dspy_name_view_get_type())
+
+G_DECLARE_FINAL_TYPE (DspyNameView, dspy_name_view, DSPY, NAME_VIEW, GtkBin)
+
+DspyNameView *dspy_name_view_new      (void);
+void          dspy_name_view_set_name (DspyNameView    *self,
+                                       GDBusConnection *connection,
+                                       GBusType         bus_type,
+                                       const gchar     *address,
+                                       DspyName        *name);
+
+G_END_DECLS
diff --git a/src/plugins/dspy/dspy-name-view.ui b/src/plugins/dspy/dspy-name-view.ui
new file mode 100644
index 000000000..800fe94fc
--- /dev/null
+++ b/src/plugins/dspy/dspy-name-view.ui
@@ -0,0 +1,154 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="DspyNameView" parent="GtkBin">
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">vertical</property>
+        <property name="spacing">12</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="DzlThreeGrid">
+            <property name="column-spacing">12</property>
+            <property name="row-spacing">0</property>
+            <property name="halign">center</property>
+            <property name="margin-top">18</property>
+            <property name="margin-start">18</property>
+            <property name="margin-end">18</property>
+            <property name="margin-bottom">6</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkLabel">
+                <property name="xalign">1.0</property>
+                <property name="label" translatable="yes">Bus Address</property>
+                <property name="visible">true</property>
+                <style>
+                  <class name="dim-label"/>
+                </style>
+              </object>
+              <packing>
+                <property name="column">left</property>
+                <property name="row">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel">
+                <property name="xalign">1.0</property>
+                <property name="label" translatable="yes">Name</property>
+                <property name="visible">true</property>
+                <style>
+                  <class name="dim-label"/>
+                </style>
+              </object>
+              <packing>
+                <property name="column">left</property>
+                <property name="row">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel">
+                <property name="xalign">1.0</property>
+                <property name="label" translatable="yes">Unique Name</property>
+                <property name="visible">true</property>
+                <style>
+                  <class name="dim-label"/>
+                </style>
+              </object>
+              <packing>
+                <property name="column">left</property>
+                <property name="row">2</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel" id="address_label">
+                <property name="hexpand">true</property>
+                <property name="xalign">0.0</property>
+                <property name="visible">true</property>
+                <property name="width-chars">50</property>
+                <property name="tooltip-text" translatable="yes">See 
http://dbus.freedesktop.org/doc/dbus-specification.html#addresses</property>
+              </object>
+              <packing>
+                <property name="column">center</property>
+                <property name="row">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel" id="name_label">
+                <property name="hexpand">true</property>
+                <property name="xalign">0.0</property>
+                <property name="visible">true</property>
+                <property name="width-chars">50</property>
+              </object>
+              <packing>
+                <property name="column">center</property>
+                <property name="row">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel" id="unique_label">
+                <property name="hexpand">true</property>
+                <property name="xalign">0.0</property>
+                <property name="visible">true</property>
+                <property name="width-chars">50</property>
+              </object>
+              <packing>
+                <property name="column">center</property>
+                <property name="row">2</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkButton" id="refresh_button">
+                <property name="halign">start</property>
+                <property name="label" translatable="yes">Reload</property>
+                <property name="visible">true</property>
+                <style>
+                  <class name="text-button"/>
+                </style>
+              </object>
+              <packing>
+                <property name="column">right</property>
+                <property name="row">0</property>
+              </packing>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkScrolledWindow">
+            <property name="expand">true</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkTreeView" id="tree_view">
+                <property name="headers-visible">true</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="GtkTreeViewColumn">
+                    <property name="expand">true</property>
+                    <property name="title" translatable="yes">Object Path</property>
+                    <child>
+                      <object class="GtkCellRendererText" id="text_cell">
+                        <property name="xalign">0.0</property>
+                        <property name="ypad">3</property>
+                        <property name="xpad">3</property>
+                      </object>
+                      <attributes>
+                        <attribute name="markup">0</attribute>
+                      </attributes>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+  <object class="GtkSizeGroup">
+    <property name="mode">vertical</property>
+    <widgets>
+      <widget name="name_label"/>
+      <widget name="unique_label"/>
+      <widget name="address_label"/>
+      <widget name="refresh_button"/>
+    </widgets>
+  </object>
+</interface>
diff --git a/src/plugins/dspy/dspy-name.c b/src/plugins/dspy/dspy-name.c
new file mode 100644
index 000000000..7473473bb
--- /dev/null
+++ b/src/plugins/dspy/dspy-name.c
@@ -0,0 +1,289 @@
+/* dspy-name.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "dspy-name"
+
+#include "dspy-name.h"
+
+typedef struct
+{
+  gchar *name;
+  gchar *owner;
+  GPid pid;
+  guint activatable : 1;
+} DspyNamePrivate;
+
+enum {
+  PROP_0,
+  PROP_ACTIVATABLE,
+  PROP_NAME,
+  PROP_OWNER,
+  PROP_PID,
+  N_PROPS
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (DspyName, dspy_name, G_TYPE_OBJECT)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+dspy_name_finalize (GObject *object)
+{
+  DspyName *self = (DspyName *)object;
+  DspyNamePrivate *priv = dspy_name_get_instance_private (self);
+
+  g_clear_pointer (&priv->name, g_free);
+  g_clear_pointer (&priv->owner, g_free);
+
+  G_OBJECT_CLASS (dspy_name_parent_class)->finalize (object);
+}
+
+static void
+dspy_name_get_property (GObject    *object,
+                        guint       prop_id,
+                        GValue     *value,
+                        GParamSpec *pspec)
+{
+  DspyName *self = DSPY_NAME (object);
+
+  switch (prop_id)
+    {
+    case PROP_ACTIVATABLE:
+      g_value_set_boolean (value, dspy_name_get_activatable (self));
+      break;
+
+    case PROP_NAME:
+      g_value_set_string (value, dspy_name_get_name (self));
+      break;
+
+    case PROP_OWNER:
+      g_value_set_string (value, dspy_name_get_owner (self));
+      break;
+
+    case PROP_PID:
+      g_value_set_uint (value, dspy_name_get_pid (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dspy_name_set_property (GObject      *object,
+                        guint         prop_id,
+                        const GValue *value,
+                        GParamSpec   *pspec)
+{
+  DspyName *self = DSPY_NAME (object);
+  DspyNamePrivate *priv = dspy_name_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_ACTIVATABLE:
+      priv->activatable = g_value_get_boolean (value);
+      break;
+
+    case PROP_NAME:
+      priv->name = g_value_dup_string (value);
+      break;
+
+    case PROP_OWNER:
+      dspy_name_set_owner (self, g_value_get_string (value));
+      break;
+
+    case PROP_PID:
+      dspy_name_set_pid (self, g_value_get_uint (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dspy_name_class_init (DspyNameClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = dspy_name_finalize;
+  object_class->get_property = dspy_name_get_property;
+  object_class->set_property = dspy_name_set_property;
+
+  properties [PROP_ACTIVATABLE] =
+    g_param_spec_boolean ("activatable",
+                          "Activatable",
+                          "Activatable",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_NAME] =
+    g_param_spec_string ("name",
+                         "Name",
+                         "The peer name",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_OWNER] =
+    g_param_spec_string ("owner",
+                         "Owner",
+                         "The owner of the DBus name",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_PID] =
+    g_param_spec_uint ("pid",
+                       "Pid",
+                       "The pid of the peer",
+                       0, G_MAXUINT, 0,
+                       (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+dspy_name_init (DspyName *self)
+{
+}
+
+DspyName *
+dspy_name_new (const gchar *name,
+               gboolean     activatable)
+{
+  return g_object_new (DSPY_TYPE_NAME,
+                       "activatable", activatable,
+                       "name", name,
+                       NULL);
+}
+
+gboolean
+dspy_name_get_activatable (DspyName *self)
+{
+  DspyNamePrivate *priv = dspy_name_get_instance_private (self);
+
+  g_return_val_if_fail (DSPY_IS_NAME (self), FALSE);
+
+  return priv->activatable;
+}
+
+const gchar *
+dspy_name_get_name (DspyName *self)
+{
+  DspyNamePrivate *priv = dspy_name_get_instance_private (self);
+
+  return priv->name;
+}
+
+void
+dspy_name_set_name (DspyName    *self,
+                    const gchar *name)
+{
+  DspyNamePrivate *priv = dspy_name_get_instance_private (self);
+
+  g_return_if_fail (DSPY_IS_NAME (self));
+
+  if (g_strcmp0 (name, priv->name) != 0)
+    {
+      g_free (priv->name);
+      priv->name = g_strdup (name);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_NAME]);
+    }
+}
+
+gint
+dspy_name_compare (gconstpointer a,
+                   gconstpointer b)
+{
+  DspyName *item1 = DSPY_NAME ((gpointer)a);
+  DspyName *item2 = DSPY_NAME ((gpointer)b);
+  const gchar *name1 = dspy_name_get_name (item1);
+  const gchar *name2 = dspy_name_get_name (item2);
+
+  if (name1[0] != name2[0])
+    {
+      if (name1[0] == ':')
+        return 1;
+      if (name2[0] == ':')
+        return -1;
+    }
+
+  /* Sort numbers like :1.300 better */
+  if (g_str_has_prefix (name1, ":1.") &&
+      g_str_has_prefix (name2, ":1."))
+    {
+      gint i1 = g_ascii_strtoll (name1 + 3, NULL, 10);
+      gint i2 = g_ascii_strtoll (name2 + 3, NULL, 10);
+
+      return i1 - i2;
+    }
+
+  return g_strcmp0 (name1, name2);
+}
+
+GPid
+dspy_name_get_pid (DspyName *self)
+{
+  DspyNamePrivate *priv = dspy_name_get_instance_private (self);
+
+  g_return_val_if_fail (DSPY_IS_NAME (self), 0);
+
+  return priv->pid;
+}
+
+void
+dspy_name_set_pid (DspyName *self,
+                   GPid      pid)
+{
+  DspyNamePrivate *priv = dspy_name_get_instance_private (self);
+
+  g_return_if_fail (DSPY_IS_NAME (self));
+
+  if (priv->pid != pid)
+    {
+      priv->pid = pid;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PID]);
+    }
+}
+
+const gchar *
+dspy_name_get_owner (DspyName *self)
+{
+  DspyNamePrivate *priv = dspy_name_get_instance_private (self);
+
+  g_return_val_if_fail (DSPY_IS_NAME (self), NULL);
+
+  return priv->owner ? priv->owner : priv->name;
+}
+
+void
+dspy_name_set_owner (DspyName    *self,
+                     const gchar *owner)
+{
+  DspyNamePrivate *priv = dspy_name_get_instance_private (self);
+
+  g_return_if_fail (DSPY_IS_NAME (self));
+
+  if (g_strcmp0 (owner, priv->owner) != 0)
+    {
+      g_free (priv->owner);
+      priv->owner = g_strdup (owner);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_OWNER]);
+    }
+}
diff --git a/src/plugins/dspy/dspy-name.h b/src/plugins/dspy/dspy-name.h
new file mode 100644
index 000000000..eca03b177
--- /dev/null
+++ b/src/plugins/dspy/dspy-name.h
@@ -0,0 +1,49 @@
+/* dspy-name.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define DSPY_TYPE_NAME (dspy_name_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (DspyName, dspy_name, DSPY, NAME, GObject)
+
+struct _DspyNameClass
+{
+  GObjectClass parent_class;
+};
+
+DspyName    *dspy_name_new             (const gchar   *name,
+                                        gboolean       activatable);
+gboolean     dspy_name_get_activatable (DspyName      *self);
+GPid         dspy_name_get_pid         (DspyName      *self);
+void         dspy_name_set_pid         (DspyName      *self,
+                                        GPid           pid);
+const gchar *dspy_name_get_name        (DspyName      *self);
+const gchar *dspy_name_get_owner       (DspyName      *self);
+void         dspy_name_set_owner       (DspyName      *self,
+                                        const gchar   *owner);
+gint         dspy_name_compare         (gconstpointer  a,
+                                        gconstpointer  b);
+
+G_END_DECLS
diff --git a/src/plugins/dspy/dspy-path-model.c b/src/plugins/dspy/dspy-path-model.c
new file mode 100644
index 000000000..db629f9ed
--- /dev/null
+++ b/src/plugins/dspy/dspy-path-model.c
@@ -0,0 +1,472 @@
+/* dspy-path-model.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "dspy-path-model"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <stdlib.h>
+
+#include "dspy-path-model.h"
+
+struct _DspyPathModel
+{
+  GtkTreeStore     store;
+  GCancellable    *cancellable;
+  GDBusConnection *connection;
+  DspyName        *name;
+};
+
+G_DEFINE_TYPE (DspyPathModel, dspy_path_model, GTK_TYPE_TREE_STORE)
+
+static GHashTable *simple_types;
+
+static void dspy_path_model_introspect (DspyPathModel *self,
+                                        const gchar   *path);
+
+static gint
+compare_iface (gconstpointer a,
+               gconstpointer b)
+{
+  const GDBusInterfaceInfo * const *info1 = a;
+  const GDBusInterfaceInfo * const *info2 = b;
+
+  return g_strcmp0 ((*info1)->name, (*info2)->name);
+}
+
+static gint
+compare_method (gconstpointer a,
+                gconstpointer b)
+{
+  const GDBusMethodInfo * const *info1 = a;
+  const GDBusMethodInfo * const *info2 = b;
+
+  return g_strcmp0 ((*info1)->name, (*info2)->name);
+}
+
+static gint
+compare_property (gconstpointer a,
+                  gconstpointer b)
+{
+  const GDBusPropertyInfo * const *info1 = a;
+  const GDBusPropertyInfo * const *info2 = b;
+
+  return g_strcmp0 ((*info1)->name, (*info2)->name);
+}
+
+static gint
+compare_signal (gconstpointer a,
+                gconstpointer b)
+{
+  const GDBusSignalInfo * const *info1 = a;
+  const GDBusSignalInfo * const *info2 = b;
+
+  return g_strcmp0 ((*info1)->name, (*info2)->name);
+}
+
+static void
+dspy_path_model_finalize (GObject *object)
+{
+  DspyPathModel *self = (DspyPathModel *)object;
+
+  g_clear_object (&self->cancellable);
+  g_clear_object (&self->connection);
+  g_clear_object (&self->name);
+
+  G_OBJECT_CLASS (dspy_path_model_parent_class)->finalize (object);
+}
+
+static void
+add_signature (GString     *str,
+               const gchar *signature)
+{
+  const gchar *tmp;
+
+  /* TODO: decode signature into human text */
+
+  if ((tmp = g_hash_table_lookup (simple_types, signature)))
+    signature = tmp;
+
+  g_string_append_printf (str, "<span weight='bold' fgalpha='32767'>%s</span>", signature);
+}
+
+static gchar *
+prop_to_string (GDBusPropertyInfo *prop)
+{
+  GString *str = g_string_new (NULL);
+
+  g_string_append (str, prop->name);
+
+  g_string_append_c (str, ' ');
+  add_signature (str, prop->signature);
+
+  g_string_append_c (str, ' ');
+  g_string_append (str, "<span size='smaller' fgalpha='32767'>(");
+  if (prop->flags == (G_DBUS_PROPERTY_INFO_FLAGS_READABLE | G_DBUS_PROPERTY_INFO_FLAGS_WRITABLE))
+    g_string_append (str, _("read/write"));
+  else if (prop->flags == G_DBUS_PROPERTY_INFO_FLAGS_READABLE)
+    g_string_append (str, _("read-only"));
+  else if (prop->flags == G_DBUS_PROPERTY_INFO_FLAGS_WRITABLE)
+    g_string_append (str, _("write-only"));
+  g_string_append (str, ")</span>");
+
+  return g_string_free (str, FALSE);
+}
+
+static void
+add_arg_name (GString     *str,
+              const gchar *name)
+{
+  g_string_append_printf (str, "<span fgalpha='32767'>%s</span>", name);
+}
+
+static gchar *
+method_to_string (GDBusMethodInfo *method)
+{
+  GString *str = g_string_new (NULL);
+
+  g_string_append (str, method->name);
+
+  g_string_append (str, " (");
+
+  for (guint i = 0; method->in_args[i] != NULL; i++)
+    {
+      GDBusArgInfo *arg = method->in_args[i];
+
+      if (i > 0)
+        g_string_append (str, ", ");
+
+      add_signature (str, arg->signature);
+      g_string_append_c (str, ' ');
+      add_arg_name (str, arg->name);
+    }
+
+  g_string_append (str, ") ↦ (");
+
+  for (guint i = 0; method->out_args[i] != NULL; i++)
+    {
+      GDBusArgInfo *arg = method->out_args[i];
+
+      if (i > 0)
+        g_string_append (str, ", ");
+
+      add_signature (str, arg->signature);
+      g_string_append_c (str, ' ');
+      add_arg_name (str, arg->name);
+    }
+
+  g_string_append_c (str, ')');
+
+  return g_string_free (str, FALSE);
+}
+
+static gchar *
+signal_to_string (GDBusSignalInfo *sig)
+{
+  GString *str = g_string_new (NULL);
+
+  g_string_append (str, sig->name);
+  g_string_append (str, " (");
+
+  for (guint i = 0; sig->args[i] != NULL; i++)
+    {
+      GDBusArgInfo *arg = sig->args[i];
+
+      if (i > 0)
+        g_string_append (str, ", ");
+
+      add_signature (str, arg->signature);
+      g_string_append_c (str, ' ');
+      add_arg_name (str, arg->name);
+    }
+
+  g_string_append_c (str, ')');
+
+  return g_string_free (str, FALSE);
+}
+
+static void
+dspy_path_model_dispose (GObject *object)
+{
+  DspyPathModel *self = (DspyPathModel *)object;
+
+  g_cancellable_cancel (self->cancellable);
+
+  G_OBJECT_CLASS (dspy_path_model_parent_class)->dispose (object);
+}
+
+static void
+dspy_path_model_class_init (DspyPathModelClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = dspy_path_model_dispose;
+  object_class->finalize = dspy_path_model_finalize;
+
+  simple_types = g_hash_table_new (g_str_hash, g_str_equal);
+
+#define INSERT(k,v) \
+  g_hash_table_insert (simple_types, (gchar *)k, (gchar *)v)
+  INSERT ("n",     "int16");
+  INSERT ("q",     "uint16");
+  INSERT ("i",     "int32");
+  INSERT ("u",     "uint32");
+  INSERT ("x",     "int64");
+  INSERT ("t",     "uint64");
+  INSERT ("s",     "string");
+  INSERT ("b",     "boolean");
+  INSERT ("y",     "byte");
+  INSERT ("o",     "Object Path");
+  INSERT ("g",     "Signature");
+  INSERT ("d",     "double");
+  INSERT ("v",     "Variant");
+  INSERT ("h",     "File Descriptor");
+  INSERT ("as",    "string[]");
+  INSERT ("a{sv}", "Vardict");
+  INSERT ("ay",    "Byte Array");
+#undef INSERT
+}
+
+static void
+dspy_path_model_init (DspyPathModel *self)
+{
+  GType types[] = { G_TYPE_STRING };
+
+  gtk_tree_store_set_column_types (GTK_TREE_STORE (self), G_N_ELEMENTS (types), types);
+
+  self->cancellable = g_cancellable_new ();
+}
+
+static void
+dspy_path_model_introspect_cb (GObject      *object,
+                               GAsyncResult *result,
+                               gpointer      user_data)
+{
+  GDBusConnection *connection = (GDBusConnection *)object;
+  g_autoptr(GDBusNodeInfo) node_info = NULL;
+  g_autoptr(GVariant) ret = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GTask) task = user_data;
+  DspyPathModel *self;
+  const gchar *path;
+  const gchar *xml;
+
+  g_assert (G_IS_DBUS_CONNECTION (connection));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (G_IS_TASK (task));
+
+  if (!(ret = g_dbus_connection_call_finish (connection, result, &error)))
+    {
+      /* XXX: We might not be authorized, we should propagate that to user */
+      g_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  self = g_task_get_source_object (task);
+  path = g_task_get_task_data (task);
+  xml = NULL;
+
+  g_variant_get (ret, "(&s)", &xml);
+
+  if (!(node_info = g_dbus_node_info_new_for_xml (xml, &error)))
+    {
+      g_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  /* First, queue up all the paths to load while we keep ourselves
+   * busy filling out this part of the tree.
+   */
+  for (guint i = 0; node_info->nodes[i] != NULL; i++)
+    {
+      g_autofree gchar *subpath = NULL;
+
+      subpath = g_strdup_printf ("%s/%s",
+                                 g_str_equal (path, "/") ? "" : path,
+                                 node_info->nodes[i]->path);
+      dspy_path_model_introspect (self, subpath);
+    }
+
+  if (node_info->interfaces[0] != NULL)
+    {
+      GtkTreeIter iter;
+      GtkTreeIter ifaceiter;
+      GtkTreeIter groupiter;
+
+      qsort (node_info->interfaces,
+             g_strv_length ((gchar **)node_info->interfaces),
+             sizeof (gpointer),
+             compare_iface);
+
+      gtk_tree_store_append (GTK_TREE_STORE (self), &iter, NULL);
+      gtk_tree_store_set (GTK_TREE_STORE (self), &iter,
+                          0, path,
+                          -1);
+
+      gtk_tree_store_append (GTK_TREE_STORE (self), &groupiter, &iter);
+      gtk_tree_store_set (GTK_TREE_STORE (self), &groupiter,
+                          0, _("<b>Interfaces</b>"),
+                          -1);
+
+      for (guint i = 0; node_info->interfaces[i] != NULL; i++)
+        {
+          GDBusInterfaceInfo *iface = node_info->interfaces[i];
+
+          gtk_tree_store_append (GTK_TREE_STORE (self), &ifaceiter, &groupiter);
+          gtk_tree_store_set (GTK_TREE_STORE (self), &ifaceiter,
+                              0, iface->name,
+                              -1);
+
+          if (iface->properties[0] != NULL)
+            {
+              GtkTreeIter propsiter;
+
+              gtk_tree_store_append (GTK_TREE_STORE (self), &propsiter, &ifaceiter);
+              gtk_tree_store_set (GTK_TREE_STORE (self), &propsiter,
+                                  0, _("<b>Properties</b>"),
+                                  -1);
+
+              qsort (iface->properties,
+                     g_strv_length ((gchar **)iface->properties),
+                     sizeof (gpointer),
+                     compare_property);
+
+              for (guint j = 0; iface->properties[j] != NULL; j++)
+                {
+                  GDBusPropertyInfo *prop = iface->properties[j];
+                  g_autofree gchar *propstr = prop_to_string (prop);
+                  GtkTreeIter propiter;
+
+                  gtk_tree_store_append (GTK_TREE_STORE (self), &propiter, &propsiter);
+                  gtk_tree_store_set (GTK_TREE_STORE (self), &propiter,
+                                      0, propstr,
+                                      -1);
+                }
+            }
+
+          if (iface->signals[0] != NULL)
+            {
+              GtkTreeIter signalsiter;
+
+              gtk_tree_store_append (GTK_TREE_STORE (self), &signalsiter, &ifaceiter);
+              gtk_tree_store_set (GTK_TREE_STORE (self), &signalsiter,
+                                  0, _("<b>Signals</b>"),
+                                  -1);
+
+              qsort (iface->signals,
+                     g_strv_length ((gchar **)iface->signals),
+                     sizeof (gpointer),
+                     compare_signal);
+
+              for (guint j = 0; iface->signals[j] != NULL; j++)
+                {
+                  GDBusSignalInfo *sig = iface->signals[j];
+                  g_autofree gchar *signalstr = signal_to_string (sig);
+                  GtkTreeIter signaliter;
+
+                  gtk_tree_store_append (GTK_TREE_STORE (self), &signaliter, &signalsiter);
+                  gtk_tree_store_set (GTK_TREE_STORE (self), &signaliter,
+                                      0, signalstr,
+                                      -1);
+                }
+            }
+
+          if (iface->methods[0] != NULL)
+            {
+              GtkTreeIter methodsiter;
+
+              gtk_tree_store_append (GTK_TREE_STORE (self), &methodsiter, &ifaceiter);
+              gtk_tree_store_set (GTK_TREE_STORE (self), &methodsiter,
+                                  0, _("<b>Methods</b>"),
+                                  -1);
+
+              qsort (iface->methods,
+                     g_strv_length ((gchar **)iface->methods),
+                     sizeof (gpointer),
+                     compare_method);
+
+              for (guint j = 0; iface->methods[j] != NULL; j++)
+                {
+                  GDBusMethodInfo *method = iface->methods[j];
+                  g_autofree gchar *methodstr = method_to_string (method);
+                  GtkTreeIter methoditer;
+
+                  gtk_tree_store_append (GTK_TREE_STORE (self), &methoditer, &methodsiter);
+                  gtk_tree_store_set (GTK_TREE_STORE (self), &methoditer,
+                                      0, methodstr,
+                                      -1);
+                }
+            }
+        }
+    }
+
+  g_task_return_boolean (task, TRUE);
+}
+
+static void
+dspy_path_model_introspect (DspyPathModel *self,
+                            const gchar   *object_path)
+{
+  g_autoptr(GTask) task = NULL;
+
+  g_assert (DSPY_IS_PATH_MODEL (self));
+  g_assert (G_IS_DBUS_CONNECTION (self->connection));
+  g_assert (DSPY_IS_NAME (self->name));
+  g_assert (object_path != NULL);
+
+  g_debug ("Introspecting DBus XML of peer %s path %s",
+           dspy_name_get_owner (self->name),
+           object_path);
+
+  task = g_task_new (self, self->cancellable, NULL, NULL);
+  g_task_set_task_data (task, g_strdup (object_path), g_free);
+
+  g_dbus_connection_call (self->connection,
+                          dspy_name_get_owner (self->name),
+                          object_path,
+                          "org.freedesktop.DBus.Introspectable",
+                          "Introspect",
+                          NULL, /* params */
+                          G_VARIANT_TYPE ("(s)"),
+                          G_DBUS_CALL_FLAGS_NONE,
+                          -1,
+                          self->cancellable,
+                          dspy_path_model_introspect_cb,
+                          g_steal_pointer (&task));
+}
+
+DspyPathModel *
+dspy_path_model_new (GDBusConnection *connection,
+                     DspyName        *name)
+{
+  DspyPathModel *self;
+
+  g_return_val_if_fail (G_IS_DBUS_CONNECTION (connection), NULL);
+  g_return_val_if_fail (DSPY_IS_NAME (name), NULL);
+
+  self = g_object_new (DSPY_TYPE_PATH_MODEL, NULL);
+  self->connection = g_object_ref (connection);
+  self->name = g_object_ref (name);
+
+  dspy_path_model_introspect (self, "/");
+
+  return g_steal_pointer (&self);
+}
diff --git a/src/plugins/dspy/dspy-path-model.h b/src/plugins/dspy/dspy-path-model.h
new file mode 100644
index 000000000..471d0dc48
--- /dev/null
+++ b/src/plugins/dspy/dspy-path-model.h
@@ -0,0 +1,36 @@
+/* dspy-path-model.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+#include "dspy-name.h"
+
+G_BEGIN_DECLS
+
+#define DSPY_TYPE_PATH_MODEL (dspy_path_model_get_type())
+
+G_DECLARE_FINAL_TYPE (DspyPathModel, dspy_path_model, DSPY, PATH_MODEL, GtkTreeStore)
+
+DspyPathModel *dspy_path_model_new (GDBusConnection *connection,
+                                    DspyName        *name);
+
+G_END_DECLS
diff --git a/src/plugins/dspy/dspy-plugin.c b/src/plugins/dspy/dspy-plugin.c
new file mode 100644
index 000000000..85bd90efc
--- /dev/null
+++ b/src/plugins/dspy/dspy-plugin.c
@@ -0,0 +1,34 @@
+/* dspy-plugin.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#include "config.h"
+
+#include <libide-editor.h>
+#include <libpeas/peas.h>
+
+#include "gbp-dspy-workspace-addin.h"
+
+_IDE_EXTERN void
+_gbp_dspy_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_WORKSPACE_ADDIN,
+                                              GBP_TYPE_DSPY_WORKSPACE_ADDIN);
+}
diff --git a/src/plugins/dspy/dspy.gresource.xml b/src/plugins/dspy/dspy.gresource.xml
new file mode 100644
index 000000000..c2063d1fc
--- /dev/null
+++ b/src/plugins/dspy/dspy.gresource.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/plugins/dspy">
+    <file>dspy.plugin</file>
+    <file>gtk/menus.ui</file>
+    <file preprocess="xml-stripblanks">dspy-connection-row.ui</file>
+    <file preprocess="xml-stripblanks">dspy-name-row.ui</file>
+    <file preprocess="xml-stripblanks">dspy-name-view.ui</file>
+    <file preprocess="xml-stripblanks">gbp-dspy-surface.ui</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/dspy/dspy.plugin b/src/plugins/dspy/dspy.plugin
new file mode 100644
index 000000000..b98a209a9
--- /dev/null
+++ b/src/plugins/dspy/dspy.plugin
@@ -0,0 +1,9 @@
+[Plugin]
+Authors=Christian Hergert <christian hergert me>
+Builtin=true
+Copyright=Copyright © 2019 Christian Hergert
+Description=Explore DBus session and system connections
+Embedded=_gbp_dspy_register_types
+Module=dspy
+Name=DBus Connection Explorer
+X-Workspace-Kind=primary;editor;
diff --git a/src/plugins/dspy/gbp-dspy-surface.c b/src/plugins/dspy/gbp-dspy-surface.c
new file mode 100644
index 000000000..46d63dbc0
--- /dev/null
+++ b/src/plugins/dspy/gbp-dspy-surface.c
@@ -0,0 +1,208 @@
+/* gbp-dspy-surface.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "gbp-dspy-surface"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+
+#include "dspy-connection-model.h"
+#include "dspy-connection-row.h"
+#include "dspy-name.h"
+#include "dspy-name-row.h"
+#include "dspy-name-view.h"
+
+#include "gbp-dspy-surface.h"
+
+struct _GbpDspySurface
+{
+  IdeSurface           parent_instance;
+
+  GtkListBox          *connections_list_box;
+  GtkScrolledWindow   *connections_scroller;
+  GtkPaned            *paned;
+  GtkScrolledWindow   *names_scroller;
+  GtkListBox          *names_list_box;
+  GtkStack            *view_stack;
+  DspyNameView        *name_view;
+
+  DspyConnectionModel *model;
+};
+
+G_DEFINE_TYPE (GbpDspySurface, gbp_dspy_surface, IDE_TYPE_SURFACE)
+
+static GtkWidget *
+create_names_row (gpointer item,
+                  gpointer user_data)
+{
+  DspyNameRow *row;
+  DspyName *name = item;
+
+  g_assert (DSPY_IS_NAME (name));
+
+  row = dspy_name_row_new (name);
+  gtk_widget_show (GTK_WIDGET (row));
+
+  return GTK_WIDGET (row);
+}
+
+static void
+name_row_activated_cb (GbpDspySurface *self,
+                       DspyNameRow    *row,
+                       GtkListBox     *list_box)
+{
+  g_assert (GBP_IS_DSPY_SURFACE (self));
+  g_assert (DSPY_IS_NAME_ROW (row));
+  g_assert (GTK_IS_LIST_BOX (list_box));
+
+  dspy_name_view_set_name (self->name_view,
+                           dspy_connection_model_get_connection (self->model),
+                           dspy_connection_model_get_bus_type (self->model),
+                           dspy_connection_model_get_address (self->model),
+                           dspy_name_row_get_name (row));
+  gtk_stack_set_visible_child_name (self->view_stack, "name");
+}
+
+static void
+connection_row_activated_cb (GbpDspySurface *self,
+                             GtkListBoxRow  *row,
+                             GtkListBox     *list_box)
+{
+  g_autoptr(GDBusConnection) bus = NULL;
+  g_autoptr(DspyConnectionModel) model = NULL;
+  g_autoptr(GError) error = NULL;
+  const gchar *addr = NULL;
+  GBusType bus_type;
+
+  g_assert (GBP_IS_DSPY_SURFACE (self));
+  g_assert (DSPY_IS_CONNECTION_ROW (row));
+  g_assert (GTK_IS_LIST_BOX (list_box));
+
+  if ((bus_type = dspy_connection_row_get_bus_type (DSPY_CONNECTION_ROW (row))))
+    bus = g_bus_get_sync (bus_type, NULL, &error);
+  else if ((addr = dspy_connection_row_get_address (DSPY_CONNECTION_ROW (row))))
+    bus = g_dbus_connection_new_for_address_sync (addr,
+                                                  (G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION |
+                                                   G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT),
+                                                  NULL, NULL, &error);
+  else
+    g_return_if_reached ();
+
+  if (error != NULL)
+    {
+      g_critical ("Failed to connect to bus: %s", error->message);
+      return;
+    }
+
+  model = dspy_connection_model_new ();
+  dspy_connection_model_set_connection (model, bus);
+  dspy_connection_model_set_bus_type (model, bus_type);
+  dspy_connection_model_set_address (model, addr);
+  gtk_list_box_bind_model (self->names_list_box, G_LIST_MODEL (model), create_names_row, NULL, NULL);
+  g_set_object (&self->model, model);
+}
+
+static void
+add_connection (GbpDspySurface *self,
+                const gchar    *name,
+                GBusType        bus_type,
+                const gchar    *addr)
+{
+  DspyConnectionRow *row;
+
+  g_assert (GBP_IS_DSPY_SURFACE (self));
+
+  row = dspy_connection_row_new ();
+  dspy_connection_row_set_title (row, name);
+
+  if (bus_type != G_BUS_TYPE_NONE)
+    dspy_connection_row_set_bus_type (row, bus_type);
+  else if (addr)
+    dspy_connection_row_set_address (row, addr);
+  else
+    g_return_if_reached ();
+
+  gtk_container_add (GTK_CONTAINER (self->connections_list_box), GTK_WIDGET (row));
+
+  gtk_widget_show (GTK_WIDGET (row));
+}
+
+static void
+gbp_dspy_surface_finalize (GObject *object)
+{
+  GbpDspySurface *self = (GbpDspySurface *)object;
+
+  g_clear_object (&self->model);
+
+  G_OBJECT_CLASS (gbp_dspy_surface_parent_class)->finalize (object);
+}
+
+static void
+gbp_dspy_surface_class_init (GbpDspySurfaceClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = gbp_dspy_surface_finalize;
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/plugins/dspy/gbp-dspy-surface.ui");
+  gtk_widget_class_bind_template_child (widget_class, GbpDspySurface, connections_list_box);
+  gtk_widget_class_bind_template_child (widget_class, GbpDspySurface, connections_scroller);
+  gtk_widget_class_bind_template_child (widget_class, GbpDspySurface, paned);
+  gtk_widget_class_bind_template_child (widget_class, GbpDspySurface, names_list_box);
+  gtk_widget_class_bind_template_child (widget_class, GbpDspySurface, names_scroller);
+  gtk_widget_class_bind_template_child (widget_class, GbpDspySurface, name_view);
+  gtk_widget_class_bind_template_child (widget_class, GbpDspySurface, view_stack);
+
+  g_type_ensure (DSPY_TYPE_NAME_VIEW);
+}
+
+static void
+gbp_dspy_surface_init (GbpDspySurface *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  gtk_widget_set_name (GTK_WIDGET (self), "dspy");
+  ide_surface_set_icon_name (IDE_SURFACE (self), "edit-find-symbolic");
+  ide_surface_set_title (IDE_SURFACE (self), _("DBus Inspector"));
+
+  g_signal_connect_object (self->connections_list_box,
+                           "row-activated",
+                           G_CALLBACK (connection_row_activated_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->names_list_box,
+                           "row-activated",
+                           G_CALLBACK (name_row_activated_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  add_connection (self, _("System Bus"), G_BUS_TYPE_SYSTEM, NULL);
+  add_connection (self, _("Session Bus"), G_BUS_TYPE_SESSION, NULL);
+}
+
+GbpDspySurface *
+gbp_dspy_surface_new (void)
+{
+  return g_object_new (GBP_TYPE_DSPY_SURFACE, NULL);
+}
diff --git a/src/plugins/dspy/gbp-dspy-surface.h b/src/plugins/dspy/gbp-dspy-surface.h
new file mode 100644
index 000000000..2ec6a35f7
--- /dev/null
+++ b/src/plugins/dspy/gbp-dspy-surface.h
@@ -0,0 +1,33 @@
+/* gbp-dspy-surface.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <libide-gui.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_DSPY_SURFACE (gbp_dspy_surface_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpDspySurface, gbp_dspy_surface, GBP, DSPY_SURFACE, IdeSurface)
+
+GbpDspySurface *gbp_dspy_surface_new (void);
+
+G_END_DECLS
diff --git a/src/plugins/dspy/gbp-dspy-surface.ui b/src/plugins/dspy/gbp-dspy-surface.ui
new file mode 100644
index 000000000..294fefef0
--- /dev/null
+++ b/src/plugins/dspy/gbp-dspy-surface.ui
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GbpDspySurface" parent="IdeSurface">
+    <child>
+      <object class="GtkPaned" id="paned">
+        <property name="orientation">horizontal</property>
+        <property name="position">300</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkPaned" id="left_paned">
+            <property name="orientation">vertical</property>
+            <property name="position">75</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkScrolledWindow" id="connections_scroller">
+                <property name="propagate-natural-height">true</property>
+                <property name="propagate-natural-width">true</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="GtkListBox" id="connections_list_box">
+                    <property name="selection-mode">browse</property>
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="shrink">false</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkScrolledWindow" id="names_scroller">
+                <property name="propagate-natural-height">true</property>
+                <property name="propagate-natural-width">true</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="GtkListBox" id="names_list_box">
+                    <property name="selection-mode">browse</property>
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="shrink">false</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="shrink">false</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkStack" id="view_stack">
+            <property name="expand">true</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="DzlEmptyState" id="empty_state">
+                <property name="icon-name">edit-find-symbolic</property>
+                <property name="title" translatable="yes">Select a Connection</property>
+                <property name="subtitle" translatable="yes">Select a connection and name on the bus to view 
details</property>
+                <property name="visible">true</property>
+              </object>
+              <packing>
+                <property name="name">empty</property>
+              </packing>
+            </child>
+            <child>
+              <object class="DspyNameView" id="name_view">
+                <property name="visible">true</property>
+              </object>
+              <packing>
+                <property name="name">name</property>
+              </packing>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/plugins/dspy/gbp-dspy-workspace-addin.c b/src/plugins/dspy/gbp-dspy-workspace-addin.c
new file mode 100644
index 000000000..4ddc43e18
--- /dev/null
+++ b/src/plugins/dspy/gbp-dspy-workspace-addin.c
@@ -0,0 +1,92 @@
+/* gbp-dspy-workspace-addin.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "gbp-dspy-workspace-addin"
+
+#include <libide-editor.h>
+
+#include "dspy-connection-model.h"
+
+#include "gbp-dspy-surface.h"
+#include "gbp-dspy-workspace-addin.h"
+
+struct _GbpDspyWorkspaceAddin
+{
+  GObject              parent_instance;
+  DspyConnectionModel *model;
+  GbpDspySurface      *surface;
+};
+
+static void
+gbp_dspy_workspace_addin_load (IdeWorkspaceAddin *addin,
+                               IdeWorkspace      *workspace)
+{
+  GbpDspyWorkspaceAddin *self = (GbpDspyWorkspaceAddin *)addin;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_DSPY_WORKSPACE_ADDIN (addin));
+  g_assert (IDE_IS_PRIMARY_WORKSPACE (workspace) || IDE_IS_EDITOR_WORKSPACE (workspace));
+
+  self->surface = gbp_dspy_surface_new ();
+  g_signal_connect (self->surface,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &self->surface);
+  ide_workspace_add_surface (workspace, IDE_SURFACE (self->surface));
+  gtk_widget_show (GTK_WIDGET (self->surface));
+
+  //self->model = dspy_connection_model_new ();
+  //dspy_connection_model_set_connection (self->model, g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL));
+}
+
+static void
+gbp_dspy_workspace_addin_unload (IdeWorkspaceAddin *addin,
+                                 IdeWorkspace      *workspace)
+{
+  GbpDspyWorkspaceAddin *self = (GbpDspyWorkspaceAddin *)addin;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_DSPY_WORKSPACE_ADDIN (addin));
+  g_assert (IDE_IS_PRIMARY_WORKSPACE (workspace) || IDE_IS_EDITOR_WORKSPACE (workspace));
+
+  if (self->surface != NULL)
+    gtk_widget_destroy (GTK_WIDGET (self->surface));
+
+}
+
+static void
+workspace_addin_iface_init (IdeWorkspaceAddinInterface *iface)
+{
+  iface->load = gbp_dspy_workspace_addin_load;
+  iface->unload = gbp_dspy_workspace_addin_unload;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpDspyWorkspaceAddin, gbp_dspy_workspace_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_WORKSPACE_ADDIN, workspace_addin_iface_init))
+
+static void
+gbp_dspy_workspace_addin_class_init (GbpDspyWorkspaceAddinClass *klass)
+{
+}
+
+static void
+gbp_dspy_workspace_addin_init (GbpDspyWorkspaceAddin *self)
+{
+}
diff --git a/src/plugins/dspy/gbp-dspy-workspace-addin.h b/src/plugins/dspy/gbp-dspy-workspace-addin.h
new file mode 100644
index 000000000..e4cfec50e
--- /dev/null
+++ b/src/plugins/dspy/gbp-dspy-workspace-addin.h
@@ -0,0 +1,31 @@
+/* gbp-dspy-workspace-addin.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <libide-gui.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_DSPY_WORKSPACE_ADDIN (gbp_dspy_workspace_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpDspyWorkspaceAddin, gbp_dspy_workspace_addin, GBP, DSPY_WORKSPACE_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/dspy/gtk/menus.ui b/src/plugins/dspy/gtk/menus.ui
new file mode 100644
index 000000000..03ae767e4
--- /dev/null
+++ b/src/plugins/dspy/gtk/menus.ui
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <menu id="ide-primary-workspace-surfaces-menu">
+    <section id="ide-primary-workspace-surfaces-menu-section">
+      <item>
+        <attribute name="id">surface-menu-dspy</attribute>
+        <attribute name="label" translatable="yes">DBus Inspector</attribute>
+        <attribute name="role">normal</attribute>
+        <attribute name="action">win.surface</attribute>
+        <attribute name="target">dspy</attribute>
+        <attribute name="verb-icon-name">edit-find-symbolic</attribute>
+      </item>
+    </section>
+  </menu>
+</interface>
diff --git a/src/plugins/dspy/meson.build b/src/plugins/dspy/meson.build
new file mode 100644
index 000000000..8b84d5638
--- /dev/null
+++ b/src/plugins/dspy/meson.build
@@ -0,0 +1,23 @@
+if get_option('plugin_dspy')
+
+plugins_sources += files([
+  'dspy-plugin.c',
+  'dspy-connection-model.c',
+  'dspy-connection-row.c',
+  'dspy-name.c',
+  'dspy-name-row.c',
+  'dspy-name-view.c',
+  'dspy-path-model.c',
+  'gbp-dspy-surface.c',
+  'gbp-dspy-workspace-addin.c',
+])
+
+plugin_dspy_resources = gnome.compile_resources(
+  'dspy-resources',
+  'dspy.gresource.xml',
+  c_name: 'gbp_dspy',
+)
+
+plugins_sources += plugin_dspy_resources
+
+endif
diff --git a/src/plugins/meson.build b/src/plugins/meson.build
index 0c5fd6aa4..4feeb4a41 100644
--- a/src/plugins/meson.build
+++ b/src/plugins/meson.build
@@ -56,6 +56,7 @@ subdir('devhelp')
 subdir('deviceui')
 subdir('deviced')
 subdir('doap')
+subdir('dspy')
 subdir('editor')
 subdir('editorconfig')
 subdir('emacs')
@@ -143,6 +144,7 @@ status += [
   'CTags ................. : @0@'.format(get_option('plugin_ctags')),
   'Devhelp ............... : @0@'.format(get_option('plugin_devhelp')),
   'Deviced ............... : @0@'.format(get_option('plugin_deviced')),
+  'DBus Spy .............. : @0@'.format(get_option('plugin_dspy')),
   'Editorconfig .......... : @0@'.format(get_option('plugin_editorconfig')),
   'ESLint ................ : @0@'.format(get_option('plugin_eslint')),
   'File Search ........... : @0@'.format(get_option('plugin_file_search')),



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