[gnome-builder/wip/chergert/dspy: 2/2] dspy: add Dspy plugin using new libdspy code



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

    dspy: add Dspy plugin using new libdspy code

 data/org.gnome.Builder.desktop.in.in               |   4 +
 meson_options.txt                                  |   1 +
 src/plugins/dspy/dspy-plugin.c                     |  40 +
 src/plugins/dspy/dspy.gresource.xml                |   9 +
 src/plugins/dspy/dspy.plugin                       |  10 +
 src/plugins/dspy/gbp-dspy-application-addin.c      | 163 ++++
 src/plugins/dspy/gbp-dspy-application-addin.h      |  31 +
 src/plugins/dspy/gbp-dspy-surface.c                |  60 ++
 src/plugins/dspy/gbp-dspy-surface.h                |  33 +
 src/plugins/dspy/gbp-dspy-surface.ui               |  10 +
 src/plugins/dspy/gbp-dspy-workspace.c              |  64 ++
 src/plugins/dspy/gbp-dspy-workspace.h              |  33 +
 src/plugins/dspy/gbp-dspy-workspace.ui             |  36 +
 src/plugins/dspy/gtk/menus.ui                      |  25 +
 src/plugins/dspy/libdspy/dspy-connection-button.c  | 263 ++++++
 src/plugins/dspy/libdspy/dspy-connection-button.h  |  46 ++
 src/plugins/dspy/libdspy/dspy-connection.c         | 491 +++++++++++
 src/plugins/dspy/libdspy/dspy-connection.h         |  56 ++
 .../dspy/libdspy/dspy-introspection-model.c        | 919 +++++++++++++++++++++
 .../dspy/libdspy/dspy-introspection-model.h        |  35 +
 src/plugins/dspy/libdspy/dspy-method-invocation.c  | 585 +++++++++++++
 src/plugins/dspy/libdspy/dspy-method-invocation.h  |  74 ++
 src/plugins/dspy/libdspy/dspy-method-view.c        | 471 +++++++++++
 src/plugins/dspy/libdspy/dspy-method-view.h        |  46 ++
 src/plugins/dspy/libdspy/dspy-method-view.ui       | 282 +++++++
 src/plugins/dspy/libdspy/dspy-name-marquee.c       | 186 +++++
 src/plugins/dspy/libdspy/dspy-name-marquee.h       |  38 +
 src/plugins/dspy/libdspy/dspy-name-marquee.ui      | 115 +++
 src/plugins/dspy/libdspy/dspy-name-row.c           | 214 +++++
 src/plugins/dspy/libdspy/dspy-name-row.h           |  36 +
 src/plugins/dspy/libdspy/dspy-name-row.ui          |  52 ++
 src/plugins/dspy/libdspy/dspy-name.c               | 486 +++++++++++
 src/plugins/dspy/libdspy/dspy-name.h               |  53 ++
 src/plugins/dspy/libdspy/dspy-names-model.c        | 532 ++++++++++++
 src/plugins/dspy/libdspy/dspy-names-model.h        |  39 +
 src/plugins/dspy/libdspy/dspy-node.c               | 598 ++++++++++++++
 src/plugins/dspy/libdspy/dspy-private.h            | 202 +++++
 src/plugins/dspy/libdspy/dspy-signature.c          |  82 ++
 src/plugins/dspy/libdspy/dspy-tree-view.c          | 311 +++++++
 src/plugins/dspy/libdspy/dspy-tree-view.h          |  46 ++
 src/plugins/dspy/libdspy/dspy-view.c               | 610 ++++++++++++++
 src/plugins/dspy/libdspy/dspy-view.h               |  41 +
 src/plugins/dspy/libdspy/dspy-view.ui              | 218 +++++
 src/plugins/dspy/libdspy/dspy.h                    |  37 +
 src/plugins/dspy/libdspy/gtk/menus.ui              |   9 +
 .../symbolic/apps/org.gnome.dfeet-symbolic.svg     |  81 ++
 src/plugins/dspy/libdspy/libdspy.gresource.xml     |  12 +
 src/plugins/dspy/libdspy/meson.build               |  35 +
 src/plugins/dspy/libdspy/themes/shared.css         |  16 +
 src/plugins/dspy/meson.build                       |  21 +
 src/plugins/meson.build                            |   2 +
 51 files changed, 7859 insertions(+)
---
diff --git a/data/org.gnome.Builder.desktop.in.in b/data/org.gnome.Builder.desktop.in.in
index 87a592a31..caa25919c 100644
--- a/data/org.gnome.Builder.desktop.in.in
+++ b/data/org.gnome.Builder.desktop.in.in
@@ -30,3 +30,7 @@ Exec=gnome-builder --clone
 [Desktop Action new-editor]
 Name=New Editor Workspace
 Exec=gnome-builder --editor
+
+[Desktop Action dspy]
+Name=DBus Inspector
+Exec=gnome-builder --dspy
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-plugin.c b/src/plugins/dspy/dspy-plugin.c
new file mode 100644
index 000000000..781afd2b4
--- /dev/null
+++ b/src/plugins/dspy/dspy-plugin.c
@@ -0,0 +1,40 @@
+/* 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-gui.h>
+#include <libide-editor.h>
+#include <libpeas/peas.h>
+
+#include <libdspy-resources.h>
+
+#include "gbp-dspy-application-addin.h"
+
+_IDE_EXTERN void
+_gbp_dspy_register_types (PeasObjectModule *module)
+{
+  g_resources_register (libdspy_get_resource ());
+  dzl_application_add_resources (DZL_APPLICATION (IDE_APPLICATION_DEFAULT), "resource:///org/gnome/dspy");
+
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_APPLICATION_ADDIN,
+                                              GBP_TYPE_DSPY_APPLICATION_ADDIN);
+}
diff --git a/src/plugins/dspy/dspy.gresource.xml b/src/plugins/dspy/dspy.gresource.xml
new file mode 100644
index 000000000..bb738591b
--- /dev/null
+++ b/src/plugins/dspy/dspy.gresource.xml
@@ -0,0 +1,9 @@
+<?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">gbp-dspy-surface.ui</file>
+    <file preprocess="xml-stripblanks">gbp-dspy-workspace.ui</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/dspy/dspy.plugin b/src/plugins/dspy/dspy.plugin
new file mode 100644
index 000000000..5f341dbe9
--- /dev/null
+++ b/src/plugins/dspy/dspy.plugin
@@ -0,0 +1,10 @@
+[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;
+X-At-Startup=true
diff --git a/src/plugins/dspy/gbp-dspy-application-addin.c b/src/plugins/dspy/gbp-dspy-application-addin.c
new file mode 100644
index 000000000..0f274e707
--- /dev/null
+++ b/src/plugins/dspy/gbp-dspy-application-addin.c
@@ -0,0 +1,163 @@
+/* gbp-dspy-application-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-application-addin"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "gbp-dspy-application-addin.h"
+#include "gbp-dspy-workspace.h"
+
+struct _GbpDspyApplicationAddin
+{
+  GObject parent_instance;
+};
+
+static void
+gbp_dspy_application_addin_add_option_entries (IdeApplicationAddin *addin,
+                                               IdeApplication      *app)
+{
+  g_assert (GBP_IS_DSPY_APPLICATION_ADDIN (addin));
+  g_assert (G_IS_APPLICATION (app));
+
+  g_application_add_main_option (G_APPLICATION (app),
+                                 "dspy",
+                                 0,
+                                 G_OPTION_FLAG_IN_MAIN,
+                                 G_OPTION_ARG_NONE,
+                                 _("Display DBus inspector"),
+                                 NULL);
+}
+
+static void
+gbp_dspy_application_addin_handle_command_line (IdeApplicationAddin     *addin,
+                                                IdeApplication          *application,
+                                                GApplicationCommandLine *cmdline)
+{
+  IdeApplication *app = (IdeApplication *)application;
+  GVariantDict *options;
+
+  g_assert (IDE_IS_APPLICATION_ADDIN (addin));
+  g_assert (IDE_IS_APPLICATION (app));
+  g_assert (G_IS_APPLICATION_COMMAND_LINE (cmdline));
+
+  if ((options = g_application_command_line_get_options_dict (cmdline)) &&
+      g_variant_dict_contains (options, "dspy"))
+    {
+      g_autoptr(IdeWorkbench) workbench = NULL;
+      g_autoptr(GFile) workdir = NULL;
+      GbpDspyWorkspace *workspace;
+      IdeContext *context;
+
+      workbench = ide_workbench_new ();
+      ide_application_add_workbench (app, workbench);
+
+      context = ide_workbench_get_context (workbench);
+
+      workdir = g_application_command_line_create_file_for_arg (cmdline, ".");
+      ide_context_set_workdir (context, workdir);
+
+      workspace = gbp_dspy_workspace_new (application);
+      ide_workbench_add_workspace (workbench, IDE_WORKSPACE (workspace));
+      ide_workbench_focus_workspace (workbench, IDE_WORKSPACE (workspace));
+
+      ide_application_set_command_line_handled (application, cmdline, TRUE);
+    }
+}
+
+static void
+dspy_action_cb (GSimpleAction *action,
+                GVariant      *param,
+                gpointer       user_data)
+{
+  g_autoptr(IdeWorkbench) workbench = NULL;
+  g_autoptr(GFile) workdir = NULL;
+  GbpDspyWorkspace *workspace;
+  IdeContext *context;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_DSPY_APPLICATION_ADDIN (user_data));
+
+  workbench = ide_workbench_new ();
+  ide_application_add_workbench (IDE_APPLICATION_DEFAULT, workbench);
+
+  context = ide_workbench_get_context (workbench);
+
+  workdir = g_file_new_for_path (ide_get_projects_dir ());
+  ide_context_set_workdir (context, workdir);
+
+  workspace = gbp_dspy_workspace_new (IDE_APPLICATION_DEFAULT);
+  ide_workbench_add_workspace (workbench, IDE_WORKSPACE (workspace));
+  ide_workbench_focus_workspace (workbench, IDE_WORKSPACE (workspace));
+}
+
+static GActionEntry actions[] = {
+  { "dspy", dspy_action_cb },
+};
+
+static void
+gbp_dspy_application_addin_load (IdeApplicationAddin *addin,
+                                 IdeApplication      *application)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_APPLICATION_ADDIN (addin));
+  g_assert (IDE_IS_APPLICATION (application));
+
+  g_action_map_add_action_entries (G_ACTION_MAP (application),
+                                   actions,
+                                   G_N_ELEMENTS (actions),
+                                   addin);
+}
+
+static void
+gbp_dspy_application_addin_unload (IdeApplicationAddin *addin,
+                                   IdeApplication      *application)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_APPLICATION_ADDIN (addin));
+  g_assert (IDE_IS_APPLICATION (application));
+
+  for (guint i = 0; i < G_N_ELEMENTS (actions); i++)
+    g_action_map_remove_action (G_ACTION_MAP (application), actions[i].name);
+}
+
+static void
+app_addin_iface_init (IdeApplicationAddinInterface *iface)
+{
+  iface->load = gbp_dspy_application_addin_load;
+  iface->unload = gbp_dspy_application_addin_unload;
+  iface->add_option_entries = gbp_dspy_application_addin_add_option_entries;
+  iface->handle_command_line = gbp_dspy_application_addin_handle_command_line;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpDspyApplicationAddin, gbp_dspy_application_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_APPLICATION_ADDIN, app_addin_iface_init))
+
+static void
+gbp_dspy_application_addin_class_init (GbpDspyApplicationAddinClass *klass)
+{
+}
+
+static void
+gbp_dspy_application_addin_init (GbpDspyApplicationAddin *self)
+{
+}
diff --git a/src/plugins/dspy/gbp-dspy-application-addin.h b/src/plugins/dspy/gbp-dspy-application-addin.h
new file mode 100644
index 000000000..6086bcfff
--- /dev/null
+++ b/src/plugins/dspy/gbp-dspy-application-addin.h
@@ -0,0 +1,31 @@
+/* gbp-dspy-application-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_APPLICATION_ADDIN (gbp_dspy_application_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpDspyApplicationAddin, gbp_dspy_application_addin, GBP, DSPY_APPLICATION_ADDIN, 
GObject)
+
+G_END_DECLS
diff --git a/src/plugins/dspy/gbp-dspy-surface.c b/src/plugins/dspy/gbp-dspy-surface.c
new file mode 100644
index 000000000..483eb9a25
--- /dev/null
+++ b/src/plugins/dspy/gbp-dspy-surface.c
@@ -0,0 +1,60 @@
+/* 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 <dspy.h>
+#include <glib/gi18n.h>
+
+#include "gbp-dspy-surface.h"
+
+struct _GbpDspySurface
+{
+  IdeSurface  parent_instance;
+  DspyView   *view;
+};
+
+G_DEFINE_TYPE (GbpDspySurface, gbp_dspy_surface, IDE_TYPE_SURFACE)
+
+static void
+gbp_dspy_surface_class_init (GbpDspySurfaceClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/plugins/dspy/gbp-dspy-surface.ui");
+  gtk_widget_class_bind_template_child (widget_class, GbpDspySurface, view);
+
+  g_type_ensure (DSPY_TYPE_VIEW);
+}
+
+static void
+gbp_dspy_surface_init (GbpDspySurface *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+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..e6537eb80
--- /dev/null
+++ b/src/plugins/dspy/gbp-dspy-surface.ui
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GbpDspySurface" parent="IdeSurface">
+    <child>
+      <object class="DspyView" id="view">
+        <property name="visible">true</property>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/plugins/dspy/gbp-dspy-workspace.c b/src/plugins/dspy/gbp-dspy-workspace.c
new file mode 100644
index 000000000..5be67a886
--- /dev/null
+++ b/src/plugins/dspy/gbp-dspy-workspace.c
@@ -0,0 +1,64 @@
+/* gbp-dspy-workspace.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"
+
+#include "config.h"
+
+#include "gbp-dspy-surface.h"
+#include "gbp-dspy-workspace.h"
+
+struct _GbpDspyWorkspace
+{
+  IdeWorkspace    parent_instance;
+  IdeHeaderBar   *header_bar;
+  GbpDspySurface *surface;
+};
+
+G_DEFINE_TYPE (GbpDspyWorkspace, gbp_dspy_workspace, IDE_TYPE_WORKSPACE)
+
+static void
+gbp_dspy_workspace_class_init (GbpDspyWorkspaceClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  IdeWorkspaceClass *workspace_class = IDE_WORKSPACE_CLASS (klass);
+
+  ide_workspace_class_set_kind (workspace_class, "dspy");
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/plugins/dspy/gbp-dspy-workspace.ui");
+  gtk_widget_class_bind_template_child (widget_class, GbpDspyWorkspace, header_bar);
+  gtk_widget_class_bind_template_child (widget_class, GbpDspyWorkspace, surface);
+}
+
+static void
+gbp_dspy_workspace_init (GbpDspyWorkspace *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+GbpDspyWorkspace *
+gbp_dspy_workspace_new (IdeApplication *application)
+{
+  g_return_val_if_fail (IDE_IS_APPLICATION (application), NULL);
+
+  return g_object_new (GBP_TYPE_DSPY_WORKSPACE,
+                       "application", application,
+                       NULL);
+}
diff --git a/src/plugins/dspy/gbp-dspy-workspace.h b/src/plugins/dspy/gbp-dspy-workspace.h
new file mode 100644
index 000000000..ddf1e16a3
--- /dev/null
+++ b/src/plugins/dspy/gbp-dspy-workspace.h
@@ -0,0 +1,33 @@
+/* gbp-dspy-workspace.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 (gbp_dspy_workspace_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpDspyWorkspace, gbp_dspy_workspace, GBP, DSPY_WORKSPACE, IdeWorkspace)
+
+GbpDspyWorkspace *gbp_dspy_workspace_new (IdeApplication *application);
+
+G_END_DECLS
diff --git a/src/plugins/dspy/gbp-dspy-workspace.ui b/src/plugins/dspy/gbp-dspy-workspace.ui
new file mode 100644
index 000000000..3560e9fde
--- /dev/null
+++ b/src/plugins/dspy/gbp-dspy-workspace.ui
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GbpDspyWorkspace" parent="IdeWorkspace">
+    <property name="default-width">1000</property>
+    <property name="default-height">700</property>
+    <child type="titlebar">
+      <object class="IdeHeaderBar" id="header_bar">
+        <property name="show-close-button">true</property>
+        <property name="show-fullscreen-button">true</property>
+        <property name="visible">true</property>
+        <child type="title">
+          <object class="GtkLabel">
+            <property name="label" translatable="yes">DBus Inspector</property>
+            <property name="visible">true</property>
+            <style>
+              <class name="title"/>
+            </style>
+          </object>
+        </child>
+      </object>
+    </child>
+    <child internal-child="surfaces">
+      <object class="GtkStack" id="surfaces">
+        <property name="visible">true</property>
+        <child>
+          <object class="GbpDspySurface" id="surface">
+            <property name="visible">true</property>
+          </object>
+          <packing>
+            <property name="name">dspy</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/plugins/dspy/gtk/menus.ui b/src/plugins/dspy/gtk/menus.ui
new file mode 100644
index 000000000..d39fe7b95
--- /dev/null
+++ b/src/plugins/dspy/gtk/menus.ui
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <menu id="ide-primary-workspace-surfaces-menu">
+    <section id="ide-primary-workspace-surfaces-menu-utils-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">app.dspy</attribute>
+        <attribute name="verb-icon-name">org.gnome.dfeet-symbolic</attribute>
+      </item>
+    </section>
+  </menu>
+  <menu id="ide-editor-workspace-surfaces-menu">
+    <section id="ide-editor-workspace-surfaces-menu-utils-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">app.dspy</attribute>
+        <attribute name="verb-icon-name">org.gnome.dfeet-symbolic</attribute>
+      </item>
+    </section>
+  </menu>
+</interface>
diff --git a/src/plugins/dspy/libdspy/dspy-connection-button.c 
b/src/plugins/dspy/libdspy/dspy-connection-button.c
new file mode 100644
index 000000000..1f6b44450
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-connection-button.c
@@ -0,0 +1,263 @@
+/* dspy-connection-button.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "dspy-connection-button"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "dspy-connection-button.h"
+
+typedef struct
+{
+  DspyConnection *connection;
+
+  GtkImage *image;
+  GtkLabel *label;
+} DspyConnectionButtonPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (DspyConnectionButton, dspy_connection_button, GTK_TYPE_RADIO_BUTTON)
+
+enum {
+  PROP_0,
+  PROP_BUS_TYPE,
+  PROP_CONNECTION,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+/**
+ * dspy_connection_button_new:
+ *
+ * Create a new #DspyConnectionButton.
+ *
+ * Returns: (transfer full): a newly created #DspyConnectionButton
+ */
+GtkWidget *
+dspy_connection_button_new (void)
+{
+  return g_object_new (DSPY_TYPE_CONNECTION_BUTTON, NULL);
+}
+
+static gboolean
+dspy_connection_button_query_tooltip (GtkWidget  *widget,
+                                      gint        x,
+                                      gint        y,
+                                      gboolean    keyboard,
+                                      GtkTooltip *tooltip)
+{
+  DspyConnectionButton *self = (DspyConnectionButton *)widget;
+  DspyConnection *connection;
+
+  g_assert (DSPY_IS_CONNECTION_BUTTON (self));
+
+  if ((connection = dspy_connection_button_get_connection (self)))
+    {
+      GDBusConnection *bus = dspy_connection_get_connection (connection);
+      const gchar *address = dspy_connection_get_address (connection);
+
+      if (bus != NULL && address != NULL)
+        {
+          /* translators: %s is replaced with the address of the DBus */
+          g_autofree gchar *text = g_strdup_printf (_("Connected to “%s”"), address);
+          gtk_tooltip_set_text (tooltip, text);
+          return TRUE;
+        }
+    }
+
+  return FALSE;
+}
+
+static void
+dspy_connection_button_finalize (GObject *object)
+{
+  DspyConnectionButton *self = (DspyConnectionButton *)object;
+  DspyConnectionButtonPrivate *priv = dspy_connection_button_get_instance_private (self);
+
+  g_clear_object (&priv->connection);
+
+  G_OBJECT_CLASS (dspy_connection_button_parent_class)->finalize (object);
+}
+
+static void
+dspy_connection_button_get_property (GObject    *object,
+                                     guint       prop_id,
+                                     GValue     *value,
+                                     GParamSpec *pspec)
+{
+  DspyConnectionButton *self = DSPY_CONNECTION_BUTTON (object);
+
+  switch (prop_id)
+    {
+    case PROP_BUS_TYPE:
+        {
+          DspyConnection *conn = dspy_connection_button_get_connection (self);
+
+          if (conn != NULL)
+            g_value_set_enum (value, dspy_connection_get_bus_type (conn));
+        }
+      break;
+
+    case PROP_CONNECTION:
+      g_value_set_object (value, dspy_connection_button_get_connection (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dspy_connection_button_set_property (GObject      *object,
+                                     guint         prop_id,
+                                     const GValue *value,
+                                     GParamSpec   *pspec)
+{
+  DspyConnectionButton *self = DSPY_CONNECTION_BUTTON (object);
+
+  switch (prop_id)
+    {
+    case PROP_BUS_TYPE:
+        {
+          GBusType bus_type = g_value_get_enum (value);
+
+          if (bus_type == G_BUS_TYPE_SESSION || bus_type == G_BUS_TYPE_SYSTEM)
+            {
+              g_autoptr(DspyConnection) conn = dspy_connection_new_for_bus (bus_type);
+              dspy_connection_button_set_connection (self, conn);
+            }
+        }
+      break;
+
+    case PROP_CONNECTION:
+      dspy_connection_button_set_connection (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dspy_connection_button_class_init (DspyConnectionButtonClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = dspy_connection_button_finalize;
+  object_class->get_property = dspy_connection_button_get_property;
+  object_class->set_property = dspy_connection_button_set_property;
+
+  widget_class->query_tooltip = dspy_connection_button_query_tooltip;
+
+  properties [PROP_BUS_TYPE] =
+    g_param_spec_enum ("bus-type",
+                       "Bus Type",
+                       "Bus Type",
+                       G_TYPE_BUS_TYPE,
+                       G_BUS_TYPE_SESSION,
+                       (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_CONNECTION] =
+    g_param_spec_object ("connection",
+                         "Connection",
+                         "The connection underlying the button",
+                         DSPY_TYPE_CONNECTION,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+dspy_connection_button_init (DspyConnectionButton *self)
+{
+  DspyConnectionButtonPrivate *priv = dspy_connection_button_get_instance_private (self);
+  GtkBox *box;
+
+  g_object_set (self,
+                "has-tooltip", TRUE,
+                "draw-indicator", FALSE,
+                NULL);
+
+  box = g_object_new (GTK_TYPE_BOX,
+                      "halign", GTK_ALIGN_CENTER,
+                      "orientation", GTK_ORIENTATION_HORIZONTAL,
+                      "visible", TRUE,
+                      NULL);
+  gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (box));
+
+  priv->image = g_object_new (GTK_TYPE_IMAGE,
+                              "icon-name", "dialog-warning-symbolic",
+                              "valign", GTK_ALIGN_CENTER,
+                              "pixel-size", 16,
+                              "margin-end", 6,
+                              "visible", FALSE,
+                              NULL);
+  gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (priv->image));
+
+  priv->label = g_object_new (GTK_TYPE_LABEL,
+                              "visible", TRUE,
+                              NULL);
+  gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (priv->label));
+}
+
+/**
+ * dspy_connection_button_get_connection:
+ * @self: a #DspyConnection
+ *
+ * Returns: (transfer none) (nullable): a #DspyConnection or %NULL
+ */
+DspyConnection *
+dspy_connection_button_get_connection (DspyConnectionButton *self)
+{
+  DspyConnectionButtonPrivate *priv = dspy_connection_button_get_instance_private (self);
+
+  g_return_val_if_fail (DSPY_IS_CONNECTION_BUTTON (self), NULL);
+
+  return priv->connection;
+}
+
+void
+dspy_connection_button_set_connection (DspyConnectionButton *self,
+                                       DspyConnection       *connection)
+{
+  DspyConnectionButtonPrivate *priv = dspy_connection_button_get_instance_private (self);
+
+  g_return_if_fail (DSPY_IS_CONNECTION_BUTTON (self));
+  g_return_if_fail (DSPY_IS_CONNECTION (connection));
+
+  if (g_set_object (&priv->connection, connection))
+    {
+      GBusType bus_type = dspy_connection_get_bus_type (connection);
+
+      if (bus_type == G_BUS_TYPE_SYSTEM)
+        gtk_label_set_label (priv->label, _("System"));
+      else if (bus_type == G_BUS_TYPE_SESSION)
+        gtk_label_set_label (priv->label, _("Session"));
+      else
+        gtk_label_set_label (priv->label, _("Other"));
+
+      g_object_bind_property (connection, "has-error", priv->image, "visible", G_BINDING_SYNC_CREATE);
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CONNECTION]);
+    }
+}
diff --git a/src/plugins/dspy/libdspy/dspy-connection-button.h 
b/src/plugins/dspy/libdspy/dspy-connection-button.h
new file mode 100644
index 000000000..c6632c04f
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-connection-button.h
@@ -0,0 +1,46 @@
+/* dspy-connection-button.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+#include "dspy-connection.h"
+
+G_BEGIN_DECLS
+
+#define DSPY_TYPE_CONNECTION_BUTTON (dspy_connection_button_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (DspyConnectionButton, dspy_connection_button, DSPY, CONNECTION_BUTTON, 
GtkRadioButton)
+
+struct _DspyConnectionButtonClass
+{
+  GtkRadioButtonClass parent_class;
+  
+  /*< private >*/
+  gpointer _reserved[8];
+};
+
+GtkWidget      *dspy_connection_button_new            (void);
+DspyConnection *dspy_connection_button_get_connection (DspyConnectionButton *self);
+void            dspy_connection_button_set_connection (DspyConnectionButton *self,
+                                                       DspyConnection       *connection);
+
+G_END_DECLS
diff --git a/src/plugins/dspy/libdspy/dspy-connection.c b/src/plugins/dspy/libdspy/dspy-connection.c
new file mode 100644
index 000000000..4684e4f6d
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-connection.c
@@ -0,0 +1,491 @@
+/* dspy-connection.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "dspy-connection"
+
+#include "config.h"
+
+#include "dspy-connection.h"
+#include "dspy-names-model.h"
+
+struct _DspyConnection
+{
+  GObject          parent_instance;
+  GCancellable    *cancellable;
+  GDBusConnection *connection;
+  gchar           *address;
+  gchar           *connected_address;
+  GPtrArray       *errors;
+  GBusType         bus_type;
+};
+
+G_DEFINE_TYPE (DspyConnection, dspy_connection, G_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_ADDRESS,
+  PROP_BUS_TYPE,
+  PROP_CONNECTION,
+  PROP_HAS_ERROR,
+  N_PROPS
+};
+
+enum {
+  ERROR,
+  N_SIGNALS
+};
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+/**
+ * dspy_connection_new_for_address:
+ * @address: an address to connect to the bus
+ *
+ * Create a new #DspyConnection.
+ *
+ * Returns: (transfer full): a newly created #DspyConnection
+ */
+DspyConnection *
+dspy_connection_new_for_address (const gchar *address)
+{
+  return g_object_new (DSPY_TYPE_CONNECTION,
+                       "address", address,
+                       NULL);
+}
+
+/**
+ * dspy_connection_new_for_bus:
+ * @bus_type: the type of bus connection
+ *
+ * Create a new #DspyConnection.
+ *
+ * Returns: (transfer full): a newly created #DspyConnection
+ */
+DspyConnection *
+dspy_connection_new_for_bus (GBusType bus_type)
+{
+  return g_object_new (DSPY_TYPE_CONNECTION,
+                       "bus-type", bus_type,
+                       NULL);
+}
+
+static void
+dspy_connection_dispose (GObject *object)
+{
+  DspyConnection *self = (DspyConnection *)object;
+
+  g_assert (DSPY_IS_CONNECTION (self));
+
+  g_cancellable_cancel (self->cancellable);
+  g_clear_object (&self->cancellable);
+
+  if (self->connection != NULL)
+    {
+      if (!g_dbus_connection_is_closed (self->connection))
+        g_dbus_connection_close (self->connection, NULL, NULL, NULL);
+      g_clear_object (&self->connection);
+    }
+
+  G_OBJECT_CLASS (dspy_connection_parent_class)->dispose (object);
+}
+
+static void
+dspy_connection_finalize (GObject *object)
+{
+  DspyConnection *self = (DspyConnection *)object;
+
+  g_clear_pointer (&self->address, g_free);
+  g_clear_pointer (&self->connected_address, g_free);
+
+  G_OBJECT_CLASS (dspy_connection_parent_class)->finalize (object);
+}
+
+static void
+dspy_connection_get_property (GObject    *object,
+                              guint       prop_id,
+                              GValue     *value,
+                              GParamSpec *pspec)
+{
+  DspyConnection *self = DSPY_CONNECTION (object);
+
+  switch (prop_id)
+    {
+    case PROP_ADDRESS:
+      g_value_set_string (value, dspy_connection_get_address (self));
+      break;
+
+    case PROP_BUS_TYPE:
+      g_value_set_enum (value, dspy_connection_get_bus_type (self));
+      break;
+
+    case PROP_CONNECTION:
+      g_value_set_object (value, dspy_connection_get_connection (self));
+      break;
+
+    case PROP_HAS_ERROR:
+      g_value_set_boolean (value, dspy_connection_get_has_error (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dspy_connection_set_property (GObject      *object,
+                              guint         prop_id,
+                              const GValue *value,
+                              GParamSpec   *pspec)
+{
+  DspyConnection *self = DSPY_CONNECTION (object);
+
+  switch (prop_id)
+    {
+    case PROP_ADDRESS:
+      if (g_value_get_string (value))
+        {
+          self->address = g_value_dup_string (value);
+          self->bus_type = G_BUS_TYPE_NONE;
+        }
+      break;
+
+    case PROP_BUS_TYPE:
+      if (g_value_get_enum (value))
+        {
+          self->bus_type = g_value_get_enum (value);
+          g_clear_pointer (&self->address, g_free);
+        }
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dspy_connection_class_init (DspyConnectionClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = dspy_connection_dispose;
+  object_class->finalize = dspy_connection_finalize;
+  object_class->get_property = dspy_connection_get_property;
+  object_class->set_property = dspy_connection_set_property;
+
+  properties [PROP_ADDRESS] =
+    g_param_spec_string ("address",
+                         "Address",
+                         "The bus address to connect",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_BUS_TYPE] =
+    g_param_spec_enum ("bus-type",
+                       "Bus Type",
+                       "The bus type to connect to, if no address is specified",
+                       G_TYPE_BUS_TYPE,
+                       G_BUS_TYPE_NONE,
+                       (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_CONNECTION] =
+    g_param_spec_object ("connection",
+                         "Connection",
+                         "The underlying GDBus connection",
+                         G_TYPE_DBUS_CONNECTION,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_HAS_ERROR] =
+    g_param_spec_boolean ("has-error",
+                          "Has Error",
+                          "Has Error",
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  signals [ERROR] =
+    g_signal_new ("error",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL,
+                  g_cclosure_marshal_VOID__BOXED,
+                  G_TYPE_NONE, 1, G_TYPE_ERROR | G_SIGNAL_TYPE_STATIC_SCOPE);
+}
+
+static void
+dspy_connection_init (DspyConnection *self)
+{
+}
+
+/**
+ * dspy_connection_get_connection:
+ *
+ * Gets the #GDBusConnection, if one has been opened.
+ *
+ * Returns: (transfer none) (nullable): a #GDBusConnection or %NULL
+ */
+GDBusConnection *
+dspy_connection_get_connection (DspyConnection *self)
+{
+  g_return_val_if_fail (DSPY_IS_CONNECTION (self), NULL);
+
+  return self->connection;
+}
+
+const gchar *
+dspy_connection_get_address (DspyConnection *self)
+{
+  g_return_val_if_fail (DSPY_IS_CONNECTION (self), NULL);
+
+  if (self->address)
+    return self->address;
+
+  if (self->connected_address)
+    return self->connected_address;
+
+  return NULL;
+}
+
+GBusType
+dspy_connection_get_bus_type (DspyConnection *self)
+{
+  g_return_val_if_fail (DSPY_IS_CONNECTION (self), G_BUS_TYPE_NONE);
+
+  return self->bus_type;
+}
+
+static void
+dspy_connection_open_address_cb (GObject      *object,
+                                 GAsyncResult *result,
+                                 gpointer      user_data)
+{
+  g_autoptr(GDBusConnection) bus = NULL;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (G_IS_TASK (task));
+
+  if (!(bus = g_dbus_connection_new_for_address_finish (result, &error)))
+    g_task_return_error (task, g_steal_pointer (&error));
+  else
+    g_task_return_pointer (task, g_steal_pointer (&bus), g_object_unref);
+}
+
+void
+dspy_connection_open_async (DspyConnection      *self,
+                            GCancellable        *cancellable,
+                            GAsyncReadyCallback  callback,
+                            gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+  g_autoptr(GError) error = NULL;
+
+  g_return_if_fail (DSPY_IS_CONNECTION (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, dspy_connection_open_async);
+
+  if (self->connection != NULL)
+    {
+      g_task_return_pointer (task, g_object_ref (self->connection), g_object_unref);
+      return;
+    }
+
+  g_clear_pointer (&self->connected_address, g_free);
+
+  if (self->address != NULL)
+    self->connected_address = g_strdup (self->address);
+  else
+    self->connected_address = g_dbus_address_get_for_bus_sync (self->bus_type,
+                                                               cancellable,
+                                                               &error);
+
+  if (error != NULL)
+    g_task_return_error (task, g_steal_pointer (&error));
+  else
+    g_dbus_connection_new_for_address (self->connected_address,
+                                       (G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION |
+                                        G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT),
+                                       NULL,
+                                       cancellable,
+                                       dspy_connection_open_address_cb,
+                                       g_steal_pointer (&task));
+}
+
+/**
+ * dspy_connection_open_finish:
+ *
+ * Completes an asynchronous request to dspy_connection_open_async().
+ *
+ * Returns: (transfer full): a #GDBusConnection if successful; otherwise
+ *   %NULL and @error is set.
+ */
+GDBusConnection *
+dspy_connection_open_finish (DspyConnection  *self,
+                             GAsyncResult    *result,
+                             GError         **error)
+{
+  GDBusConnection *bus;
+
+  g_return_val_if_fail (DSPY_IS_CONNECTION (self), NULL);
+  g_return_val_if_fail (G_IS_TASK (result), NULL);
+
+  if ((bus = g_task_propagate_pointer (G_TASK (result), error)))
+    {
+      g_dbus_connection_set_exit_on_close (bus, FALSE);
+
+      if (g_set_object (&self->connection, bus))
+        g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CONNECTION]);
+    }
+
+  return g_steal_pointer (&bus);
+}
+
+void
+dspy_connection_close (DspyConnection *self)
+{
+  g_return_if_fail (DSPY_IS_CONNECTION (self));
+
+  g_cancellable_cancel (self->cancellable);
+  g_dbus_connection_close (self->connection, NULL, NULL, NULL);
+
+  g_clear_object (&self->connection);
+  g_clear_object (&self->cancellable);
+}
+
+static void
+dspy_connection_list_names_cb (GObject      *object,
+                               GAsyncResult *result,
+                               gpointer      user_data)
+{
+  GAsyncInitable *initable = (GAsyncInitable *)object;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GTask) task = user_data;
+  DspyConnection *self;
+
+  g_assert (G_IS_ASYNC_INITABLE (initable));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+
+  if (!g_async_initable_init_finish (initable, result, &error))
+    {
+      dspy_connection_add_error (self, error);
+      g_task_return_error (task, g_steal_pointer (&error));
+    }
+  else
+    {
+      dspy_connection_clear_errors (self);
+      g_task_return_pointer (task, g_object_ref (initable), g_object_unref);
+    }
+}
+
+void
+dspy_connection_list_names_async (DspyConnection      *self,
+                                  GCancellable        *cancellable,
+                                  GAsyncReadyCallback  callback,
+                                  gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+  g_autoptr(DspyNamesModel) model = NULL;
+
+  g_return_if_fail (DSPY_IS_CONNECTION (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, dspy_connection_list_names_async);
+
+  model = dspy_names_model_new (self);
+
+  g_async_initable_init_async (G_ASYNC_INITABLE (model),
+                               G_PRIORITY_DEFAULT,
+                               cancellable,
+                               dspy_connection_list_names_cb,
+                               g_steal_pointer (&task));
+}
+
+GListModel *
+dspy_connection_list_names_finish (DspyConnection  *self,
+                                   GAsyncResult    *result,
+                                   GError         **error)
+{
+  g_return_val_if_fail (DSPY_IS_CONNECTION (self), NULL);
+  g_return_val_if_fail (G_IS_TASK (result), NULL);
+
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+/**
+ * dspy_connection_get_has_error:
+ *
+ * Checks if any errors have been registered with the connection, such
+ * as when listing peer names.
+ *
+ * This can be used to show extra information to the user about the
+ * connection issues.
+ *
+ * Returns: %TRUE if there are any errors
+ */
+gboolean
+dspy_connection_get_has_error (DspyConnection *self)
+{
+  g_return_val_if_fail (DSPY_IS_CONNECTION (self), FALSE);
+
+  return self->errors != NULL && self->errors->len > 0;
+}
+
+void
+dspy_connection_add_error (DspyConnection *self,
+                           const GError   *error)
+{
+  gboolean notify;
+
+  g_return_if_fail (DSPY_IS_CONNECTION (self));
+  g_return_if_fail (error != NULL);
+
+  if (self->errors == NULL)
+    self->errors = g_ptr_array_new_with_free_func ((GDestroyNotify)g_error_free);
+
+  notify = self->errors->len == 0;
+
+  g_ptr_array_add (self->errors, g_error_copy (error));
+
+  g_signal_emit (self, signals [ERROR], 0, error);
+
+  if (notify)
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_ERROR]);
+}
+
+void
+dspy_connection_clear_errors (DspyConnection *self)
+{
+  g_return_if_fail (DSPY_IS_CONNECTION (self));
+
+  if (self->errors != NULL && self->errors->len > 0)
+    {
+      g_ptr_array_remove_range (self->errors, 0, self->errors->len);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_ERROR]);
+    }
+}
diff --git a/src/plugins/dspy/libdspy/dspy-connection.h b/src/plugins/dspy/libdspy/dspy-connection.h
new file mode 100644
index 000000000..f39e7a60b
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-connection.h
@@ -0,0 +1,56 @@
+/* dspy-connection.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define DSPY_TYPE_CONNECTION (dspy_connection_get_type())
+
+G_DECLARE_FINAL_TYPE (DspyConnection, dspy_connection, DSPY, CONNECTION, GObject)
+
+DspyConnection  *dspy_connection_new_for_address   (const gchar          *address);
+DspyConnection  *dspy_connection_new_for_bus       (GBusType              bus_type);
+void             dspy_connection_add_error         (DspyConnection       *self,
+                                                    const GError         *error);
+void             dspy_connection_clear_errors      (DspyConnection       *self);
+GDBusConnection *dspy_connection_get_connection    (DspyConnection       *self);
+const gchar     *dspy_connection_get_address       (DspyConnection       *self);
+GBusType         dspy_connection_get_bus_type      (DspyConnection       *self);
+gboolean         dspy_connection_get_has_error     (DspyConnection       *self);
+void             dspy_connection_open_async        (DspyConnection       *self,
+                                                    GCancellable         *cancellable,
+                                                    GAsyncReadyCallback   callback,
+                                                    gpointer              user_data);
+GDBusConnection *dspy_connection_open_finish       (DspyConnection       *self,
+                                                    GAsyncResult         *result,
+                                                    GError              **error);
+void             dspy_connection_close             (DspyConnection       *self);
+void             dspy_connection_list_names_async  (DspyConnection       *self,
+                                                    GCancellable         *cancellable,
+                                                    GAsyncReadyCallback   callback,
+                                                    gpointer              user_data);
+GListModel      *dspy_connection_list_names_finish (DspyConnection       *self,
+                                                    GAsyncResult         *result,
+                                                    GError              **error);
+
+G_END_DECLS
diff --git a/src/plugins/dspy/libdspy/dspy-introspection-model.c 
b/src/plugins/dspy/libdspy/dspy-introspection-model.c
new file mode 100644
index 000000000..b2e749cb1
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-introspection-model.c
@@ -0,0 +1,919 @@
+/* dspy-introspection-model.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "dspy-introspection-model"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <string.h>
+
+#include "dspy-introspection-model.h"
+#include "dspy-private.h"
+
+#if 0
+# define LOG_DEBUG(str) g_printerr ("%s\n", str);
+#else
+# define LOG_DEBUG(str)
+#endif
+
+struct _DspyIntrospectionModel
+{
+  GObject       parent_instance;
+
+  GCancellable *cancellable;
+  DspyName     *name;
+  DspyNodeInfo *root;
+
+  /* Synchronize chunks access in threaded workers */
+  GMutex        chunks_mutex;
+  GStringChunk *chunks;
+};
+
+typedef struct
+{
+  GTask           *task;
+  GDBusConnection *connection;
+  gchar           *path;
+} Introspect;
+
+static void
+introspect_free (Introspect *state)
+{
+  g_clear_object (&state->task);
+  g_clear_object (&state->connection);
+  g_clear_pointer (&state->path, g_free);
+  g_slice_free (Introspect, state);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (Introspect, introspect_free)
+
+static void dspy_introspection_model_introspect (GTask           *task,
+                                                 GDBusConnection *connection,
+                                                 const gchar     *path);
+
+static void
+parse_xml_worker (GTask        *task,
+                  gpointer      source_object,
+                  gpointer      task_data,
+                  GCancellable *cancellable)
+{
+  DspyIntrospectionModel *self = source_object;
+  GBytes *bytes = task_data;
+  g_autoptr(GError) error = NULL;
+  DspyNodeInfo *info;
+  const gchar *xml;
+
+  g_assert (G_IS_TASK (task));
+  g_assert (DSPY_IS_INTROSPECTION_MODEL (source_object));
+  g_assert (bytes != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  xml = (const gchar *)g_bytes_get_data (bytes, NULL);
+
+  g_mutex_lock (&self->chunks_mutex);
+  info = _dspy_node_parse (xml, self->chunks, &error);
+  g_mutex_unlock (&self->chunks_mutex);
+
+  if (info != NULL)
+    g_task_return_pointer (task, info, (GDestroyNotify) _dspy_node_free);
+  else
+    g_task_return_error (task, g_steal_pointer (&error));
+}
+
+static void
+parse_xml_async (DspyIntrospectionModel *self,
+                 GBytes                 *bytes,
+                 GCancellable           *cancellable,
+                 GAsyncReadyCallback     callback,
+                 gpointer                user_data)
+{
+  g_autoptr(GTask) task = NULL;
+
+  g_assert (DSPY_IS_INTROSPECTION_MODEL (self));
+  g_assert (bytes != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, parse_xml_async);
+  g_task_set_task_data (task, g_bytes_ref (bytes), (GDestroyNotify) g_bytes_unref);
+  g_task_run_in_thread (task, parse_xml_worker);
+}
+
+static DspyNodeInfo *
+parse_xml_finish (DspyIntrospectionModel  *self,
+                  GAsyncResult            *result,
+                  GError                 **error)
+{
+  g_assert (DSPY_IS_INTROSPECTION_MODEL (self));
+  g_assert (G_IS_TASK (result));
+
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static void
+emit_row_inserted_for_tree_cb (gpointer item,
+                               gpointer user_data)
+{
+  g_autoptr(GtkTreePath) path = NULL;
+  DspyIntrospectionModel *self = user_data;
+  GtkTreeIter iter = { .user_data = item, };
+
+  path = gtk_tree_model_get_path (GTK_TREE_MODEL (self), &iter);
+  gtk_tree_model_row_inserted (GTK_TREE_MODEL (self), path, &iter);
+}
+
+static void
+emit_row_inserted_for_tree (DspyIntrospectionModel *self,
+                            DspyNode               *tree)
+{
+  g_assert (DSPY_IS_INTROSPECTION_MODEL (self));
+  g_assert (tree != NULL);
+
+  _dspy_node_walk (tree, emit_row_inserted_for_tree_cb, self);
+}
+
+static void
+dspy_introspection_model_init_parse_cb (GObject      *object,
+                                        GAsyncResult *result,
+                                        gpointer      user_data)
+{
+  DspyIntrospectionModel *self = (DspyIntrospectionModel *)object;
+  g_autoptr(Introspect) state = user_data;
+  g_autoptr(GError) error = NULL;
+  DspyNodeInfo *info = NULL;
+  GCancellable *cancellable;
+  gint *n_active;
+
+  g_assert (DSPY_IS_INTROSPECTION_MODEL (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (state != NULL);
+  g_assert (G_IS_TASK (state->task));
+  g_assert (state->path != NULL);
+
+  self = g_task_get_source_object (state->task);
+  n_active = g_task_get_task_data (state->task);
+  cancellable = g_task_get_cancellable (state->task);
+
+  g_assert (self != NULL);
+  g_assert (DSPY_IS_INTROSPECTION_MODEL (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_assert (n_active != NULL);
+  g_assert (*n_active > 0);
+
+  if ((info = parse_xml_finish (self, result, &error)))
+    {
+      g_assert (DSPY_IS_NODE (info));
+      g_assert (info->kind == DSPY_NODE_KIND_NODE);
+
+      /* First, queue a bunch of sub-path reads based on any discovered
+       * nodes from querying this specific node.
+       */
+      for (const GList *iter = info->nodes.head; iter; iter = iter->next)
+        {
+          DspyNodeInfo *child = iter->data;
+          g_autofree gchar *child_path = NULL;
+
+          g_assert (child != NULL);
+          g_assert (DSPY_IS_NODE (child));
+          g_assert (child->kind == DSPY_NODE_KIND_NODE);
+
+          child_path = g_build_path ("/", state->path, child->path, NULL);
+          dspy_introspection_model_introspect (state->task, state->connection, child_path);
+        }
+
+      /* Now add this node to our root if it contains any intefaces. */
+      if (info->interfaces->interfaces.length > 0)
+        {
+          g_autofree gchar *abs_path = g_build_path ("/", state->path, info->path, NULL);
+
+          g_mutex_lock (&self->chunks_mutex);
+          info->path = g_string_chunk_insert_const (self->chunks, abs_path);
+          g_mutex_unlock (&self->chunks_mutex);
+
+          g_queue_push_tail_link (&self->root->nodes, &info->link);
+          info->parent = (DspyNode *)self->root;
+
+          emit_row_inserted_for_tree (self, (DspyNode *)info);
+
+          /* Stolen */
+          info = NULL;
+        }
+
+      g_clear_pointer (&info, _dspy_node_free);
+    }
+
+  if (--(*n_active) == 0)
+    g_task_return_boolean (state->task, TRUE);
+}
+
+static void
+dspy_introspection_model_init_introspect_cb (GObject      *object,
+                                             GAsyncResult *result,
+                                             gpointer      user_data)
+{
+  GDBusConnection *bus = (GDBusConnection *)object;
+  DspyIntrospectionModel *self;
+  g_autoptr(Introspect) state = user_data;
+  g_autoptr(GVariant) reply = NULL;
+  g_autoptr(GError) error = NULL;
+  GCancellable *cancellable;
+  gint *n_active;
+
+  g_assert (G_IS_DBUS_CONNECTION (bus));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (state != NULL);
+  g_assert (G_IS_TASK (state->task));
+  g_assert (state->path != NULL);
+
+  self = g_task_get_source_object (state->task);
+  n_active = g_task_get_task_data (state->task);
+  cancellable = g_task_get_cancellable (state->task);
+
+  g_assert (self != NULL);
+  g_assert (DSPY_IS_INTROSPECTION_MODEL (self));
+  g_assert (n_active != NULL);
+  g_assert (*n_active > 0);
+
+  if ((reply = g_dbus_connection_call_finish (bus, result, &error)))
+    {
+      g_autoptr(GBytes) bytes = NULL;
+      const gchar *str = NULL;
+
+      /* Get the XML contents, and wrap it in a new GBytes that will
+       * reference the original GVariant to avoid a copy as this might
+       * contain a large amount of text.
+       */
+      g_variant_get (reply, "(&s)", &str);
+
+      if (str[0] != 0)
+        {
+          bytes = g_bytes_new_with_free_func (str,
+                                              strlen (str),
+                                              (GDestroyNotify) g_variant_unref,
+                                              g_variant_ref (reply));
+          parse_xml_async (self,
+                           bytes,
+                           cancellable,
+                           dspy_introspection_model_init_parse_cb,
+                           g_steal_pointer (&state));
+          return;
+        }
+    }
+  else
+    {
+      DspyConnection *connection = dspy_name_get_connection (self->name);
+
+      dspy_connection_add_error (connection, error);
+    }
+
+  if (--(*n_active) == 0)
+    g_task_return_boolean (state->task, TRUE);
+}
+
+static gboolean
+has_node_with_path (DspyIntrospectionModel *self,
+                    const gchar            *path)
+{
+  g_assert (DSPY_IS_INTROSPECTION_MODEL (self));
+  g_assert (path != NULL);
+
+  for (const GList *iter = self->root->nodes.head; iter; iter = iter->next)
+    {
+      const DspyNode *node = iter->data;
+
+      g_assert (node != NULL);
+      g_assert (DSPY_IS_NODE (node));
+      g_assert (node->any.kind == DSPY_NODE_KIND_NODE);
+
+      if (g_strcmp0 (path, node->node.path) == 0)
+        return TRUE;
+    }
+
+  return FALSE;
+}
+
+static void
+dspy_introspection_model_introspect (GTask           *task,
+                                     GDBusConnection *connection,
+                                     const gchar     *path)
+{
+  DspyIntrospectionModel *self;
+  Introspect *state;
+  gint *n_active;
+
+  g_assert (G_IS_TASK (task));
+  g_assert (G_IS_DBUS_CONNECTION (connection));
+  g_assert (path != NULL);
+
+  self = g_task_get_source_object (task);
+  n_active = g_task_get_task_data (task);
+
+  g_assert (G_IS_TASK (task));
+  g_assert (n_active != NULL);
+
+  /* If we already have this path, then ignore the suplimental query */
+  if (has_node_with_path (self, path))
+    return;
+
+  (*n_active)++;
+
+  state = g_slice_new0 (Introspect);
+  state->task = g_object_ref (task);
+  state->connection = g_object_ref (connection);
+  state->path = g_strdup (path);
+
+  g_dbus_connection_call (connection,
+                          dspy_name_get_owner (self->name),
+                          path,
+                          "org.freedesktop.DBus.Introspectable",
+                          "Introspect",
+                          NULL, /* Params */
+                          G_VARIANT_TYPE ("(s)"),
+                          G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION,
+                          -1,
+                          self->cancellable,
+                          dspy_introspection_model_init_introspect_cb,
+                          state);
+}
+
+static void
+dspy_introspection_model_init_async (GAsyncInitable      *initiable,
+                                     gint                 io_priority,
+                                     GCancellable        *cancellable,
+                                     GAsyncReadyCallback  callback,
+                                     gpointer             user_data)
+{
+  DspyIntrospectionModel *self = (DspyIntrospectionModel *)initiable;
+  GDBusConnection *bus = NULL;
+  DspyConnection *connection = NULL;
+  g_autoptr(GTask) task = NULL;
+
+  g_assert (DSPY_IS_INTROSPECTION_MODEL (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, dspy_introspection_model_init_async);
+  g_task_set_task_data (task, g_new0 (gint, 1), g_free);
+  g_task_set_priority (task, io_priority);
+
+  if (self->name == NULL ||
+      !(connection = dspy_name_get_connection (self->name)) ||
+      !(bus = dspy_connection_get_connection (connection)))
+    {
+      g_task_return_new_error (task,
+                               G_IO_ERROR,
+                               G_IO_ERROR_NOT_INITIALIZED,
+                               "%s has not been intialized with a name",
+                               G_OBJECT_TYPE_NAME (self));
+      return;
+    }
+
+  dspy_introspection_model_introspect (task, bus, "/");
+}
+
+static gboolean
+dspy_introspection_model_init_finish (GAsyncInitable  *initable,
+                                      GAsyncResult    *result,
+                                      GError         **error)
+{
+  g_assert (DSPY_IS_INTROSPECTION_MODEL (initable));
+  g_assert (G_IS_TASK (result));
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+async_initable_iface_init (GAsyncInitableIface *iface)
+{
+  iface->init_async = dspy_introspection_model_init_async;
+  iface->init_finish = dspy_introspection_model_init_finish;
+}
+
+static gboolean
+dspy_introspection_model_iter_children (GtkTreeModel *model,
+                                        GtkTreeIter  *iter,
+                                        GtkTreeIter  *parent)
+{
+  LOG_DEBUG (G_STRFUNC);
+
+  g_assert (DSPY_IS_INTROSPECTION_MODEL (model));
+  g_assert (iter != NULL);
+
+  return gtk_tree_model_iter_nth_child (model, iter, parent, 0);
+}
+
+static gboolean
+dspy_introspection_model_iter_next (GtkTreeModel *model,
+                                    GtkTreeIter  *iter)
+{
+  DspyIntrospectionModel *self = (DspyIntrospectionModel *)model;
+  DspyNode *node;
+
+  LOG_DEBUG (G_STRFUNC);
+
+  g_assert (DSPY_IS_INTROSPECTION_MODEL (self));
+  g_assert (iter != NULL);
+
+  node = iter->user_data;
+
+  g_assert (node != NULL);
+  g_assert (node->any.kind > 0);
+  g_assert (node->any.kind < DSPY_NODE_KIND_LAST);
+
+  switch (node->any.kind)
+    {
+    case DSPY_NODE_KIND_NODE:
+    case DSPY_NODE_KIND_METHOD:
+    case DSPY_NODE_KIND_SIGNAL:
+    case DSPY_NODE_KIND_PROPERTY:
+    case DSPY_NODE_KIND_INTERFACE:
+      if (node->any.link.next != NULL)
+        {
+          iter->user_data = node->any.link.next->data;
+          return TRUE;
+        }
+      else
+        {
+          node->any.link.next = NULL;
+          return FALSE;
+        }
+
+    case DSPY_NODE_KIND_PROPERTIES:
+      iter->user_data = node->any.parent->interface.signals;
+      return TRUE;
+
+    case DSPY_NODE_KIND_SIGNALS:
+      iter->user_data = node->any.parent->interface.methods;
+      return TRUE;
+
+    case DSPY_NODE_KIND_INTERFACES:
+    case DSPY_NODE_KIND_METHODS:
+    case DSPY_NODE_KIND_ARG:
+    case DSPY_NODE_KIND_LAST:
+    default:
+      return FALSE;
+    }
+}
+
+static gint
+dspy_introspection_model_get_n_columns (GtkTreeModel *model)
+{
+  return 1;
+}
+
+static GtkTreePath *
+dspy_introspection_model_get_path (GtkTreeModel *model,
+                                   GtkTreeIter  *iter)
+{
+  DspyIntrospectionModel *self = (DspyIntrospectionModel *)model;
+  GtkTreePath *path;
+  DspyNode *node;
+
+  LOG_DEBUG (G_STRFUNC);
+
+  g_assert (DSPY_IS_INTROSPECTION_MODEL (self));
+  g_assert (iter != NULL);
+
+  node = iter->user_data;
+
+  g_assert (node != NULL);
+  g_assert (node->any.parent != NULL);
+
+  path = gtk_tree_path_new_first ();
+
+  g_assert (gtk_tree_path_get_depth (path) == 1);
+
+  for (; node->any.parent != NULL; node = node->any.parent)
+    {
+      gint pos = 0;
+
+      for (const GList *list = &node->any.link; list->prev; list = list->prev)
+        pos++;
+
+      gtk_tree_path_prepend_index (path, pos);
+    }
+
+  gtk_tree_path_up (path);
+
+  return g_steal_pointer (&path);
+}
+
+static gboolean
+dspy_introspection_model_iter_parent (GtkTreeModel *model,
+                                      GtkTreeIter  *iter,
+                                      GtkTreeIter  *child)
+{
+  DspyNode *node;
+
+  LOG_DEBUG (G_STRFUNC);
+
+  g_assert (DSPY_IS_INTROSPECTION_MODEL (model));
+  g_assert (iter != NULL);
+  g_assert (child != NULL);
+
+  memset (iter, 0, sizeof *iter);
+
+  node = child->user_data;
+
+  g_assert (node != NULL);
+  g_assert (DSPY_IS_NODE (node));
+  g_assert (node->any.parent != NULL);
+
+  /* Ignore root, we don't have a visual node for that */
+  if (node->any.parent->any.parent != NULL)
+    iter->user_data = node->node.parent;
+
+  return iter->user_data != NULL;
+}
+
+static GType
+dspy_introspection_model_get_column_type (GtkTreeModel *model,
+                                          gint          column)
+{
+  if (column == 0)
+    return G_TYPE_STRING;
+
+  return G_TYPE_INVALID;
+}
+
+static gboolean
+dspy_introspection_model_iter_has_child (GtkTreeModel *model,
+                                         GtkTreeIter  *iter)
+{
+  GtkTreeIter child;
+
+  LOG_DEBUG (G_STRFUNC);
+
+  g_assert (DSPY_IS_INTROSPECTION_MODEL (model));
+  g_assert (iter != NULL);
+
+  return gtk_tree_model_iter_nth_child (model, &child, iter, 0);
+}
+
+static GtkTreeModelFlags
+dspy_introspection_model_get_flags (GtkTreeModel *model)
+{
+  return 0;
+}
+
+static void
+dspy_introspection_model_get_value (GtkTreeModel *model,
+                                    GtkTreeIter  *iter,
+                                    gint          column,
+                                    GValue       *value)
+{
+  LOG_DEBUG (G_STRFUNC);
+
+  if (column == 0)
+    {
+      DspyNode *node = iter->user_data;
+      g_autofree gchar *str = NULL;
+
+      g_assert (node != NULL);
+      g_assert (DSPY_IS_NODE (node));
+
+      g_value_init (value, G_TYPE_STRING);
+
+      str = _dspy_node_get_text (node);
+
+      if (_dspy_node_is_group (node))
+        {
+          if (gtk_tree_model_iter_has_child (model, iter))
+            g_value_take_string (value, g_strdup_printf ("<b>%s</b>", str));
+          else
+            g_value_take_string (value, g_strdup_printf ("<span fgalpha='25000' weight='bold'>%s</span>", 
str));
+        }
+      else
+        g_value_take_string (value, g_steal_pointer (&str));
+    }
+}
+
+static gboolean
+dspy_introspection_model_get_iter (GtkTreeModel *model,
+                                   GtkTreeIter  *iter,
+                                   GtkTreePath  *tree_path)
+{
+  DspyIntrospectionModel *self = (DspyIntrospectionModel *)model;
+  DspyNode *cur;
+  gint *indices;
+  gint depth;
+
+  LOG_DEBUG (G_STRFUNC);
+
+  g_assert (DSPY_IS_INTROSPECTION_MODEL (self));
+  g_assert (iter != NULL);
+  g_assert (tree_path != NULL);
+
+  memset (iter, 0, sizeof *iter);
+
+  cur = (DspyNode *)self->root;
+  indices = gtk_tree_path_get_indices_with_depth (tree_path, &depth);
+
+  for (guint i = 0; cur != NULL && i < depth; i++)
+    {
+      gint pos = indices[i];
+
+      if (cur->any.parent == NULL)
+        cur = g_queue_peek_nth (&cur->node.nodes, pos);
+      else if (cur->any.kind == DSPY_NODE_KIND_NODE)
+        cur = (DspyNode *)cur->node.interfaces;
+      else if (cur->any.kind == DSPY_NODE_KIND_INTERFACES)
+        cur = g_queue_peek_nth (&cur->interfaces.interfaces, pos);
+      else if (cur->any.kind == DSPY_NODE_KIND_INTERFACE)
+        {
+          if (pos == 0)
+            cur = (DspyNode *)cur->interface.properties;
+          else if (pos == 1)
+            cur = (DspyNode *)cur->interface.signals;
+          else if (pos == 2)
+            cur = (DspyNode *)cur->interface.methods;
+          else
+            cur = NULL;
+        }
+      else if (cur->any.kind == DSPY_NODE_KIND_PROPERTIES)
+        cur = g_queue_peek_nth (&cur->properties.properties, pos);
+      else if (cur->any.kind == DSPY_NODE_KIND_SIGNALS)
+        cur = g_queue_peek_nth (&cur->signals.signals, pos);
+      else if (cur->any.kind == DSPY_NODE_KIND_METHODS)
+        cur = g_queue_peek_nth (&cur->methods.methods, pos);
+      else
+        cur = NULL;
+    }
+
+  if (cur != NULL)
+    {
+      iter->user_data = cur;
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+static gint
+dspy_introspection_model_iter_n_children (GtkTreeModel *model,
+                                          GtkTreeIter  *iter)
+{
+  DspyIntrospectionModel *self = (DspyIntrospectionModel *)model;
+  DspyNode *node;
+
+  LOG_DEBUG (G_STRFUNC);
+
+  g_assert (DSPY_IS_INTROSPECTION_MODEL (self));
+  g_assert (iter != NULL);
+
+  node = iter ? iter->user_data : self->root;
+
+  if (node->any.kind == DSPY_NODE_KIND_NODE)
+    {
+      /* Root item is the list of paths */
+      if (node->any.parent == NULL)
+        return node->node.nodes.length;
+      else
+        return 1;
+    }
+
+  if (node->any.kind == DSPY_NODE_KIND_INTERFACES)
+    return node->interfaces.interfaces.length;
+
+  if (node->any.kind == DSPY_NODE_KIND_INTERFACE)
+    return 3;
+
+  if (node->any.kind == DSPY_NODE_KIND_METHODS)
+    return node->methods.methods.length;
+
+  if (node->any.kind == DSPY_NODE_KIND_SIGNALS)
+    return node->signals.signals.length;
+
+  if (node->any.kind == DSPY_NODE_KIND_PROPERTIES)
+    return node->properties.properties.length;
+
+  return 0;
+}
+
+static gboolean
+dspy_introspection_model_iter_nth_child (GtkTreeModel *model,
+                                         GtkTreeIter  *iter,
+                                         GtkTreeIter  *parent,
+                                         gint          nth)
+{
+  DspyIntrospectionModel *self = (DspyIntrospectionModel *)model;
+  DspyNode *cur;
+
+  LOG_DEBUG (G_STRFUNC);
+
+  g_assert (DSPY_IS_INTROSPECTION_MODEL (self));
+  g_assert (iter != NULL);
+  g_assert (nth >= 0);
+
+  cur = parent ? parent->user_data : self->root;
+
+  g_assert (DSPY_IS_NODE (cur));
+
+  switch (cur->any.kind)
+    {
+    case DSPY_NODE_KIND_NODE:
+      if (cur->any.parent == NULL)
+        iter->user_data = g_queue_peek_nth (&cur->node.nodes, nth);
+      else
+        iter->user_data = cur->node.interfaces;
+      break;
+
+    case DSPY_NODE_KIND_METHODS:
+      iter->user_data = g_queue_peek_nth (&cur->methods.methods, nth);
+      break;
+
+    case DSPY_NODE_KIND_SIGNALS:
+      iter->user_data = g_queue_peek_nth (&cur->signals.signals, nth);
+      break;
+
+    case DSPY_NODE_KIND_PROPERTIES:
+      iter->user_data = g_queue_peek_nth (&cur->properties.properties, nth);
+      break;
+
+    case DSPY_NODE_KIND_INTERFACES:
+      iter->user_data = g_queue_peek_nth (&cur->interfaces.interfaces, nth);
+      break;
+
+    case DSPY_NODE_KIND_INTERFACE:
+      if (nth == 0)
+        iter->user_data = cur->interface.properties;
+      else if (nth == 1)
+        iter->user_data = cur->interface.signals;
+      else if (nth == 2)
+        iter->user_data = cur->interface.methods;
+      break;
+
+    case DSPY_NODE_KIND_ARG:
+    case DSPY_NODE_KIND_METHOD:
+    case DSPY_NODE_KIND_SIGNAL:
+    case DSPY_NODE_KIND_PROPERTY:
+    case DSPY_NODE_KIND_LAST:
+    default:
+      return FALSE;
+    }
+
+  return iter->user_data != NULL;
+}
+
+static void
+tree_model_iface_init (GtkTreeModelIface *iface)
+{
+  iface->get_column_type = dspy_introspection_model_get_column_type;
+  iface->get_iter = dspy_introspection_model_get_iter;
+  iface->get_flags = dspy_introspection_model_get_flags;
+  iface->get_n_columns = dspy_introspection_model_get_n_columns;
+  iface->get_path = dspy_introspection_model_get_path;
+  iface->get_value = dspy_introspection_model_get_value;
+  iface->iter_children = dspy_introspection_model_iter_children;
+  iface->iter_has_child = dspy_introspection_model_iter_has_child;
+  iface->iter_n_children = dspy_introspection_model_iter_n_children;
+  iface->iter_nth_child = dspy_introspection_model_iter_nth_child;
+  iface->iter_next = dspy_introspection_model_iter_next;
+  iface->iter_parent = dspy_introspection_model_iter_parent;
+}
+
+G_DEFINE_TYPE_WITH_CODE (DspyIntrospectionModel, dspy_introspection_model, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE, async_initable_iface_init)
+                         G_IMPLEMENT_INTERFACE (GTK_TYPE_TREE_MODEL, tree_model_iface_init))
+
+enum {
+  PROP_0,
+  PROP_NAME,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+DspyIntrospectionModel *
+_dspy_introspection_model_new (DspyName *name)
+{
+  g_return_val_if_fail (DSPY_IS_NAME (name), NULL);
+
+  return g_object_new (DSPY_TYPE_INTROSPECTION_MODEL,
+                       "name", name,
+                       NULL);
+}
+
+static void
+dspy_introspection_model_finalize (GObject *object)
+{
+  DspyIntrospectionModel *self = (DspyIntrospectionModel *)object;
+
+  g_clear_object (&self->cancellable);
+  g_clear_object (&self->name);
+  g_clear_pointer (&self->chunks, g_string_chunk_free);
+  g_clear_pointer (&self->root, _dspy_node_free);
+  g_mutex_clear (&self->chunks_mutex);
+
+  G_OBJECT_CLASS (dspy_introspection_model_parent_class)->finalize (object);
+}
+
+static void
+dspy_introspection_model_dispose (GObject *object)
+{
+  DspyIntrospectionModel *self = (DspyIntrospectionModel *)object;
+
+  g_cancellable_cancel (self->cancellable);
+
+  G_OBJECT_CLASS (dspy_introspection_model_parent_class)->dispose (object);
+}
+
+static void
+dspy_introspection_model_get_property (GObject    *object,
+                                       guint       prop_id,
+                                       GValue     *value,
+                                       GParamSpec *pspec)
+{
+  DspyIntrospectionModel *self = DSPY_INTROSPECTION_MODEL (object);
+
+  switch (prop_id)
+    {
+    case PROP_NAME:
+      g_value_set_object (value, dspy_introspection_model_get_name (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dspy_introspection_model_set_property (GObject      *object,
+                                       guint         prop_id,
+                                       const GValue *value,
+                                       GParamSpec   *pspec)
+{
+  DspyIntrospectionModel *self = DSPY_INTROSPECTION_MODEL (object);
+
+  switch (prop_id)
+    {
+    case PROP_NAME:
+      self->name = g_value_dup_object (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dspy_introspection_model_class_init (DspyIntrospectionModelClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = dspy_introspection_model_dispose;
+  object_class->finalize = dspy_introspection_model_finalize;
+  object_class->get_property = dspy_introspection_model_get_property;
+  object_class->set_property = dspy_introspection_model_set_property;
+
+  properties [PROP_NAME] =
+    g_param_spec_object ("name",
+                         "Name",
+                         "The DspyName to introspect",
+                         DSPY_TYPE_NAME,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+dspy_introspection_model_init (DspyIntrospectionModel *self)
+{
+  self->cancellable = g_cancellable_new ();
+  self->chunks = g_string_chunk_new (4096L * 4);
+  self->root = _dspy_node_new_root ();
+  g_mutex_init (&self->chunks_mutex);
+}
+
+/**
+ * dspy_introspection_model_get_name:
+ *
+ * Gets the #DspyName that is being introspected.
+ *
+ * Returns: (transfer none): a #DspyName
+ */
+DspyName *
+dspy_introspection_model_get_name (DspyIntrospectionModel *self)
+{
+  g_return_val_if_fail (DSPY_IS_INTROSPECTION_MODEL (self), NULL);
+
+  return self->name;
+}
diff --git a/src/plugins/dspy/libdspy/dspy-introspection-model.h 
b/src/plugins/dspy/libdspy/dspy-introspection-model.h
new file mode 100644
index 000000000..0ffd00029
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-introspection-model.h
@@ -0,0 +1,35 @@
+/* dspy-introspection-model.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+
+#include "dspy-name.h"
+
+G_BEGIN_DECLS
+
+#define DSPY_TYPE_INTROSPECTION_MODEL (dspy_introspection_model_get_type())
+
+G_DECLARE_FINAL_TYPE (DspyIntrospectionModel, dspy_introspection_model, DSPY, INTROSPECTION_MODEL, GObject)
+
+DspyName *dspy_introspection_model_get_name (DspyIntrospectionModel *self);
+
+G_END_DECLS
diff --git a/src/plugins/dspy/libdspy/dspy-method-invocation.c 
b/src/plugins/dspy/libdspy/dspy-method-invocation.c
new file mode 100644
index 000000000..034cde20f
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-method-invocation.c
@@ -0,0 +1,585 @@
+/* dspy-method-invocation.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "dspy-method-invocation"
+
+#include "config.h"
+
+#include "dspy-method-invocation.h"
+
+typedef struct
+{
+  gchar    *interface;
+  gchar    *signature;
+  gchar    *object_path;
+  gchar    *method;
+  gchar    *reply_signature;
+  DspyName *name;
+  GVariant *parameters;
+  gint      timeout_msec;
+} DspyMethodInvocationPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (DspyMethodInvocation, dspy_method_invocation, G_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_INTERFACE,
+  PROP_METHOD,
+  PROP_NAME,
+  PROP_OBJECT_PATH,
+  PROP_PARAMETERS,
+  PROP_REPLY_SIGNATURE,
+  PROP_SIGNATURE,
+  PROP_TIMEOUT,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+/**
+ * dspy_method_invocation_new:
+ *
+ * Create a new #DspyMethodInvocation.
+ *
+ * Returns: (transfer full): a newly created #DspyMethodInvocation
+ */
+DspyMethodInvocation *
+dspy_method_invocation_new (void)
+{
+  return g_object_new (DSPY_TYPE_METHOD_INVOCATION, NULL);
+}
+
+static void
+dspy_method_invocation_finalize (GObject *object)
+{
+  DspyMethodInvocation *self = (DspyMethodInvocation *)object;
+  DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+
+  g_clear_pointer (&priv->interface, g_free);
+  g_clear_pointer (&priv->signature, g_free);
+  g_clear_pointer (&priv->object_path, g_free);
+  g_clear_pointer (&priv->method, g_free);
+  g_clear_pointer (&priv->reply_signature, g_free);
+  g_clear_object (&priv->name);
+  g_clear_pointer (&priv->parameters, g_variant_unref);
+
+  G_OBJECT_CLASS (dspy_method_invocation_parent_class)->finalize (object);
+}
+
+static void
+dspy_method_invocation_get_property (GObject    *object,
+                                     guint       prop_id,
+                                     GValue     *value,
+                                     GParamSpec *pspec)
+{
+  DspyMethodInvocation *self = DSPY_METHOD_INVOCATION (object);
+
+  switch (prop_id)
+    {
+    case PROP_INTERFACE:
+      g_value_set_string (value, dspy_method_invocation_get_interface (self));
+      break;
+
+    case PROP_OBJECT_PATH:
+      g_value_set_string (value, dspy_method_invocation_get_object_path (self));
+      break;
+
+    case PROP_METHOD:
+      g_value_set_string (value, dspy_method_invocation_get_method (self));
+      break;
+
+    case PROP_SIGNATURE:
+      g_value_set_string (value, dspy_method_invocation_get_signature (self));
+      break;
+
+    case PROP_REPLY_SIGNATURE:
+      g_value_set_string (value, dspy_method_invocation_get_reply_signature (self));
+      break;
+
+    case PROP_NAME:
+      g_value_set_object (value, dspy_method_invocation_get_name (self));
+      break;
+
+    case PROP_PARAMETERS:
+      g_value_set_variant (value, dspy_method_invocation_get_parameters (self));
+      break;
+
+    case PROP_TIMEOUT:
+      g_value_set_int (value, dspy_method_invocation_get_timeout (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dspy_method_invocation_set_property (GObject      *object,
+                                     guint         prop_id,
+                                     const GValue *value,
+                                     GParamSpec   *pspec)
+{
+  DspyMethodInvocation *self = DSPY_METHOD_INVOCATION (object);
+
+  switch (prop_id)
+    {
+    case PROP_INTERFACE:
+      dspy_method_invocation_set_interface (self, g_value_get_string (value));
+      break;
+
+    case PROP_OBJECT_PATH:
+      dspy_method_invocation_set_object_path (self, g_value_get_string (value));
+      break;
+
+    case PROP_METHOD:
+      dspy_method_invocation_set_method (self, g_value_get_string (value));
+      break;
+
+    case PROP_SIGNATURE:
+      dspy_method_invocation_set_signature (self, g_value_get_string (value));
+      break;
+
+    case PROP_REPLY_SIGNATURE:
+      dspy_method_invocation_set_reply_signature (self, g_value_get_string (value));
+      break;
+
+    case PROP_NAME:
+      dspy_method_invocation_set_name (self, g_value_get_object (value));
+      break;
+
+    case PROP_PARAMETERS:
+      dspy_method_invocation_set_parameters (self, g_value_get_variant (value));
+      break;
+
+    case PROP_TIMEOUT:
+      dspy_method_invocation_set_timeout (self, g_value_get_int (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dspy_method_invocation_class_init (DspyMethodInvocationClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = dspy_method_invocation_finalize;
+  object_class->get_property = dspy_method_invocation_get_property;
+  object_class->set_property = dspy_method_invocation_set_property;
+
+  properties [PROP_INTERFACE] =
+    g_param_spec_string ("interface",
+                         "Interface",
+                         "The interface containing the method",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_OBJECT_PATH] =
+    g_param_spec_string ("object-path",
+                         "Object Path",
+                         "The path containing the interface",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_METHOD] =
+    g_param_spec_string ("method",
+                         "Method",
+                         "The method of the interface to execute",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_SIGNATURE] =
+    g_param_spec_string ("signature",
+                         "Signature",
+                         "The signature of the method, used for display purposes",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_REPLY_SIGNATURE] =
+    g_param_spec_string ("reply-signature",
+                         "Reply Signature",
+                         "The reply signature of the method, used for display purposes",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_NAME] =
+    g_param_spec_object ("name",
+                         "Name",
+                         "The DspyName to communicate with",
+                         DSPY_TYPE_NAME,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_PARAMETERS] =
+    g_param_spec_variant ("parameters",
+                          "Parameters",
+                          "The parameters for the invocation",
+                          G_VARIANT_TYPE_ANY,
+                          NULL,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TIMEOUT] =
+    g_param_spec_int ("timeout",
+                      "Timeout",
+                      "The timeout for the operation",
+                      -1, G_MAXINT, -1,
+                      (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+dspy_method_invocation_init (DspyMethodInvocation *self)
+{
+  DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+
+  priv->timeout_msec = -1;
+}
+
+static void
+dspy_method_invocation_execute_call_cb (GObject      *object,
+                                        GAsyncResult *result,
+                                        gpointer      user_data)
+{
+  GDBusConnection *bus = (GDBusConnection *)object;
+  g_autoptr(GVariant) reply = NULL;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (G_IS_DBUS_CONNECTION (bus));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (G_IS_TASK (task));
+
+  if (!(reply = g_dbus_connection_call_finish (bus, result, &error)))
+    g_task_return_error (task, g_steal_pointer (&error));
+  else
+    g_task_return_pointer (task, g_steal_pointer (&reply), (GDestroyNotify)g_variant_unref);
+}
+
+static void
+dspy_method_invocation_execute_open_cb (GObject      *object,
+                                        GAsyncResult *result,
+                                        gpointer      user_data)
+{
+  DspyMethodInvocationPrivate *priv;
+  DspyMethodInvocation *self;
+  DspyConnection *connection = (DspyConnection *)object;
+  g_autoptr(GDBusConnection) bus = NULL;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  GCancellable *cancellable;
+
+  g_assert (DSPY_IS_CONNECTION (connection));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (G_IS_TASK (task));
+
+  if (!(bus = dspy_connection_open_finish (connection, result, &error)))
+    {
+      g_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  self = g_task_get_source_object (task);
+  priv = dspy_method_invocation_get_instance_private (self);
+  cancellable = g_task_get_cancellable (task);
+
+  if (priv->name == NULL ||
+      priv->object_path == NULL ||
+      priv->interface == NULL ||
+      priv->method == NULL ||
+      priv->parameters == NULL)
+    {
+      g_task_return_new_error (task,
+                               G_IO_ERROR,
+                               G_IO_ERROR_NOT_INITIALIZED,
+                               "Method invocation contains uninitialized parameters");
+      return;
+    }
+
+  g_dbus_connection_call (bus,
+                          dspy_name_get_owner (priv->name),
+                          priv->object_path,
+                          priv->interface,
+                          priv->method,
+                          priv->parameters,
+                          NULL, /* Allow any reply type (even if invalid) */
+                          G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION,
+                          priv->timeout_msec,
+                          cancellable,
+                          dspy_method_invocation_execute_call_cb,
+                          g_steal_pointer (&task));
+}
+
+void
+dspy_method_invocation_execute_async (DspyMethodInvocation *self,
+                                      GCancellable         *cancellable,
+                                      GAsyncReadyCallback   callback,
+                                      gpointer              user_data)
+{
+  DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+  g_autoptr(GTask) task = NULL;
+  DspyConnection *connection;
+
+  g_assert (DSPY_IS_METHOD_INVOCATION (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, dspy_method_invocation_execute_async);
+
+  if (priv->name == NULL)
+    {
+      g_task_return_new_error (task,
+                               G_IO_ERROR,
+                               G_IO_ERROR_NOT_INITIALIZED,
+                               "No name set to communicate with");
+      return;
+    }
+
+  connection = dspy_name_get_connection (priv->name);
+
+  dspy_connection_open_async (connection,
+                              cancellable,
+                              dspy_method_invocation_execute_open_cb,
+                              g_steal_pointer (&task));
+}
+
+/**
+ * dspy_method_invocation_execute_finish:
+ *
+ * Completes an asynchronous call to dspy_method_invocation_execute_async()
+ *
+ * Returns: (transfer full): a #GVariant if successful; otherwise %FALSE and
+ *   @error is set.
+ */
+GVariant *
+dspy_method_invocation_execute_finish (DspyMethodInvocation  *self,
+                                       GAsyncResult          *result,
+                                       GError               **error)
+{
+  g_return_val_if_fail (DSPY_IS_METHOD_INVOCATION (self), NULL);
+  g_return_val_if_fail (G_IS_TASK (result), NULL);
+
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+const gchar *
+dspy_method_invocation_get_interface (DspyMethodInvocation *self)
+{
+  DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+  g_return_val_if_fail (DSPY_IS_METHOD_INVOCATION (self), NULL);
+  return priv->interface;
+}
+
+const gchar *
+dspy_method_invocation_get_object_path (DspyMethodInvocation *self)
+{
+  DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+  g_return_val_if_fail (DSPY_IS_METHOD_INVOCATION (self), NULL);
+  return priv->object_path;
+}
+
+const gchar *
+dspy_method_invocation_get_method (DspyMethodInvocation *self)
+{
+  DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+  g_return_val_if_fail (DSPY_IS_METHOD_INVOCATION (self), NULL);
+  return priv->method;
+}
+
+const gchar *
+dspy_method_invocation_get_signature (DspyMethodInvocation *self)
+{
+  DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+  g_return_val_if_fail (DSPY_IS_METHOD_INVOCATION (self), NULL);
+  return priv->signature;
+}
+
+const gchar *
+dspy_method_invocation_get_reply_signature (DspyMethodInvocation *self)
+{
+  DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+  g_return_val_if_fail (DSPY_IS_METHOD_INVOCATION (self), NULL);
+  return priv->reply_signature;
+}
+
+/**
+ * dspy_method_invocation_get_parameters:
+ *
+ * Returns: (transfer none): a #GVariant if set; otherwise %NULL
+ */
+GVariant *
+dspy_method_invocation_get_parameters (DspyMethodInvocation *self)
+{
+  DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+
+  g_return_val_if_fail (DSPY_IS_METHOD_INVOCATION (self), NULL);
+
+  return priv->parameters;
+}
+
+/**
+ * dspy_method_invocation_get_name:
+ *
+ * Returns: (transfer none) (nullable): a #DspyName or %NULL if unset
+ */
+DspyName *
+dspy_method_invocation_get_name (DspyMethodInvocation *self)
+{
+  DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+
+  g_return_val_if_fail (DSPY_IS_METHOD_INVOCATION (self), NULL);
+
+  return priv->name;
+}
+
+void
+dspy_method_invocation_set_interface (DspyMethodInvocation *self,
+                                      const gchar          *interface)
+{
+  DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+
+  g_return_if_fail (DSPY_IS_METHOD_INVOCATION (self));
+
+  if (g_strcmp0 (priv->interface, interface) != 0)
+    {
+      g_free (priv->interface);
+      priv->interface = g_strdup (interface);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_INTERFACE]);
+    }
+}
+
+void
+dspy_method_invocation_set_method (DspyMethodInvocation *self,
+                                   const gchar          *method)
+{
+  DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+
+  g_return_if_fail (DSPY_IS_METHOD_INVOCATION (self));
+
+  if (g_strcmp0 (priv->method, method) != 0)
+    {
+      g_free (priv->method);
+      priv->method = g_strdup (method);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_METHOD]);
+    }
+}
+
+void
+dspy_method_invocation_set_object_path (DspyMethodInvocation *self,
+                                        const gchar          *object_path)
+{
+  DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+
+  g_return_if_fail (DSPY_IS_METHOD_INVOCATION (self));
+
+  if (g_strcmp0 (priv->object_path, object_path) != 0)
+    {
+      g_free (priv->object_path);
+      priv->object_path = g_strdup (object_path);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_OBJECT_PATH]);
+    }
+}
+
+void
+dspy_method_invocation_set_signature (DspyMethodInvocation *self,
+                                      const gchar          *signature)
+{
+  DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+
+  g_return_if_fail (DSPY_IS_METHOD_INVOCATION (self));
+
+  if (g_strcmp0 (priv->signature, signature) != 0)
+    {
+      g_free (priv->signature);
+      priv->signature = g_strdup (signature);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SIGNATURE]);
+    }
+}
+
+void
+dspy_method_invocation_set_reply_signature (DspyMethodInvocation *self,
+                                            const gchar          *reply_signature)
+{
+  DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+
+  g_return_if_fail (DSPY_IS_METHOD_INVOCATION (self));
+
+  if (g_strcmp0 (priv->reply_signature, reply_signature) != 0)
+    {
+      g_free (priv->reply_signature);
+      priv->reply_signature = g_strdup (reply_signature);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_REPLY_SIGNATURE]);
+    }
+}
+
+void
+dspy_method_invocation_set_name (DspyMethodInvocation *self,
+                                 DspyName             *name)
+{
+  DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+
+  g_return_if_fail (DSPY_IS_METHOD_INVOCATION (self));
+
+  if (g_set_object (&priv->name, name))
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_NAME]);
+}
+
+void
+dspy_method_invocation_set_parameters (DspyMethodInvocation *self,
+                                       GVariant             *parameters)
+{
+  DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+
+  g_return_if_fail (DSPY_IS_METHOD_INVOCATION (self));
+
+  if (parameters != priv->parameters)
+    {
+      g_clear_pointer (&priv->parameters, g_variant_unref);
+      priv->parameters = parameters ? g_variant_ref_sink (parameters) : NULL;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PARAMETERS]);
+    }
+}
+
+gint
+dspy_method_invocation_get_timeout (DspyMethodInvocation *self)
+{
+  DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+
+  g_return_val_if_fail (DSPY_IS_METHOD_INVOCATION (self), -1);
+
+  return priv->timeout_msec;
+}
+
+void
+dspy_method_invocation_set_timeout (DspyMethodInvocation *self,
+                                    gint                  timeout)
+{
+  DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+
+  g_return_if_fail (DSPY_IS_METHOD_INVOCATION (self));
+  g_return_if_fail (timeout >= -1);
+
+  if (priv->timeout_msec != timeout)
+    {
+      priv->timeout_msec = timeout;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TIMEOUT]);
+    }
+}
diff --git a/src/plugins/dspy/libdspy/dspy-method-invocation.h 
b/src/plugins/dspy/libdspy/dspy-method-invocation.h
new file mode 100644
index 000000000..e327a4b8f
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-method-invocation.h
@@ -0,0 +1,74 @@
+/* dspy-method-invocation.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+
+#include "dspy-name.h"
+
+G_BEGIN_DECLS
+
+#define DSPY_TYPE_METHOD_INVOCATION (dspy_method_invocation_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (DspyMethodInvocation, dspy_method_invocation, DSPY, METHOD_INVOCATION, GObject)
+
+struct _DspyMethodInvocationClass
+{
+  GObjectClass parent_class;
+
+  /*< private >*/
+  gpointer _reserved[8];
+};
+
+DspyMethodInvocation *dspy_method_invocation_new                 (void);
+const gchar          *dspy_method_invocation_get_interface       (DspyMethodInvocation  *self);
+const gchar          *dspy_method_invocation_get_object_path     (DspyMethodInvocation  *self);
+const gchar          *dspy_method_invocation_get_method          (DspyMethodInvocation  *self);
+const gchar          *dspy_method_invocation_get_signature       (DspyMethodInvocation  *self);
+const gchar          *dspy_method_invocation_get_reply_signature (DspyMethodInvocation  *self);
+GVariant             *dspy_method_invocation_get_parameters      (DspyMethodInvocation  *self);
+DspyName             *dspy_method_invocation_get_name            (DspyMethodInvocation  *self);
+void                  dspy_method_invocation_set_interface       (DspyMethodInvocation  *self,
+                                                                  const gchar           *interface);
+void                  dspy_method_invocation_set_method          (DspyMethodInvocation  *self,
+                                                                  const gchar           *method);
+void                  dspy_method_invocation_set_object_path     (DspyMethodInvocation  *self,
+                                                                  const gchar           *object_path);
+void                  dspy_method_invocation_set_signature       (DspyMethodInvocation  *self,
+                                                                  const gchar           *signature);
+void                  dspy_method_invocation_set_reply_signature (DspyMethodInvocation  *self,
+                                                                  const gchar           *reply_signature);
+void                  dspy_method_invocation_set_name            (DspyMethodInvocation  *self,
+                                                                  DspyName              *name);
+void                  dspy_method_invocation_set_parameters      (DspyMethodInvocation  *self,
+                                                                  GVariant              *parameters);
+void                  dspy_method_invocation_execute_async       (DspyMethodInvocation  *self,
+                                                                  GCancellable          *cancellable,
+                                                                  GAsyncReadyCallback    callback,
+                                                                  gpointer               user_data);
+GVariant             *dspy_method_invocation_execute_finish      (DspyMethodInvocation  *self,
+                                                                  GAsyncResult          *result,
+                                                                  GError               **error);
+gint                  dspy_method_invocation_get_timeout         (DspyMethodInvocation  *self);
+void                  dspy_method_invocation_set_timeout         (DspyMethodInvocation  *self,
+                                                                  gint                   timout);
+
+G_END_DECLS
diff --git a/src/plugins/dspy/libdspy/dspy-method-view.c b/src/plugins/dspy/libdspy/dspy-method-view.c
new file mode 100644
index 000000000..64e90fb23
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-method-view.c
@@ -0,0 +1,471 @@
+/* dspy-method-view.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "dspy-method-view"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+
+#include "dspy-method-view.h"
+
+typedef struct
+{
+  DspyMethodInvocation *invocation;
+  DzlBindingGroup      *bindings;
+  GCancellable         *cancellable;
+  GArray               *durations;
+
+  GtkLabel             *label_interface;
+  GtkLabel             *label_object_path;
+  GtkLabel             *label_method;
+  GtkLabel             *label_avg;
+  GtkLabel             *label_min;
+  GtkLabel             *label_max;
+  GtkButton            *button;
+  GtkButton            *copy_button;
+  GtkTextBuffer        *buffer_params;
+  GtkTextBuffer        *buffer_reply;
+  GtkTextView          *textview_params;
+
+  guint                 busy : 1;
+} DspyMethodViewPrivate;
+
+typedef struct
+{
+  DspyMethodView *self;
+  GTimer         *timer;
+} Execute;
+
+G_DEFINE_TYPE_WITH_PRIVATE (DspyMethodView, dspy_method_view, DZL_TYPE_BIN)
+
+enum {
+  PROP_0,
+  PROP_INVOCATION,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+execute_free (Execute *state)
+{
+  if (state != NULL)
+    {
+      g_clear_pointer (&state->timer, g_timer_destroy);
+      g_clear_object (&state->self);
+      g_slice_free (Execute, state);
+    }
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (Execute, execute_free)
+
+/**
+ * dspy_method_view_new:
+ *
+ * Create a new #DspyMethodView.
+ *
+ * Returns: (transfer full): a newly created #DspyMethodView
+ */
+GtkWidget *
+dspy_method_view_new (void)
+{
+  return g_object_new (DSPY_TYPE_METHOD_VIEW, NULL);
+}
+
+static void
+update_timings (DspyMethodView *self)
+{
+  DspyMethodViewPrivate *priv = dspy_method_view_get_instance_private (self);
+  g_autofree gchar *mean_str = NULL;
+  g_autofree gchar *min_str = NULL;
+  g_autofree gchar *max_str = NULL;
+  gdouble min = G_MAXDOUBLE;
+  gdouble max = -G_MAXDOUBLE;
+  gdouble total = 0;
+  gdouble mean = 0;
+
+  g_assert (DSPY_IS_METHOD_VIEW (self));
+  g_assert (priv->durations != NULL);
+
+  if (priv->durations->len == 0)
+    {
+      gtk_label_set_label (priv->label_avg, NULL);
+      gtk_label_set_label (priv->label_min, NULL);
+      gtk_label_set_label (priv->label_max, NULL);
+      return;
+    }
+
+  for (guint i = 0; i < priv->durations->len; i++)
+    {
+      gdouble val = g_array_index (priv->durations, gdouble, i);
+
+      total += val;
+      min = MIN (min, val);
+      max = MAX (max, val);
+    }
+
+  mean = total / (gdouble)priv->durations->len;
+
+  mean_str = g_strdup_printf ("%0.4lf", mean);
+  min_str = g_strdup_printf ("%0.4lf", min);
+  max_str = g_strdup_printf ("%0.4lf", max);
+
+  gtk_label_set_label (priv->label_avg, mean_str);
+  gtk_label_set_label (priv->label_min, min_str);
+  gtk_label_set_label (priv->label_max, max_str);
+}
+
+static gboolean
+variant_to_string_transform (GBinding     *binding,
+                             const GValue *from_value,
+                             GValue       *to_value,
+                             gpointer      user_data)
+{
+  GVariant *v = g_value_get_variant (from_value);
+  if (v != NULL)
+    g_value_take_string (to_value, g_variant_print (v, FALSE));
+  else
+    g_value_set_string (to_value, "");
+  return TRUE;
+}
+
+static void
+dspy_method_view_execute_cb (GObject      *object,
+                             GAsyncResult *result,
+                             gpointer      user_data)
+{
+  DspyMethodInvocation *invocation = (DspyMethodInvocation *)object;
+  g_autoptr(Execute) state = user_data;
+  DspyMethodViewPrivate *priv;
+  g_autoptr(GVariant) reply = NULL;
+  g_autoptr(GError) error = NULL;
+  gdouble elapsed;
+
+  g_assert (DSPY_IS_METHOD_INVOCATION (invocation));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (state != NULL);
+  g_assert (state->timer != NULL);
+  g_assert (DSPY_IS_METHOD_VIEW (state->self));
+
+  priv = dspy_method_view_get_instance_private (state->self);
+  priv->busy = FALSE;
+
+  g_timer_stop (state->timer);
+  elapsed = g_timer_elapsed (state->timer, NULL);
+  g_array_append_val (priv->durations, elapsed);
+
+  if (!(reply = dspy_method_invocation_execute_finish (invocation, result, &error)))
+    {
+      if (priv->invocation == invocation)
+        gtk_text_buffer_set_text (priv->buffer_reply, error->message, -1);
+    }
+  else
+    {
+      if (priv->invocation == invocation)
+        {
+          g_autofree gchar *replystr = g_variant_print (reply, TRUE);
+          gtk_text_buffer_set_text (priv->buffer_reply, replystr, -1);
+        }
+    }
+
+  update_timings (state->self);
+
+  gtk_button_set_label (priv->button, _("Execute"));
+}
+
+static GVariant *
+get_variant_for_text_buffer (GtkTextBuffer       *buffer,
+                             const GVariantType  *type,
+                             GError             **error)
+{
+  g_autofree gchar *text = NULL;
+  GtkTextIter begin, end;
+
+  g_assert (GTK_IS_TEXT_BUFFER (buffer));
+
+  gtk_text_buffer_get_bounds (buffer, &begin, &end);
+
+  text = g_strstrip (gtk_text_buffer_get_text (buffer, &begin, &end, TRUE));
+
+  if (text[0] != '(')
+    {
+      g_autofree gchar *tmp = text;
+      text = g_strdup_printf ("(%s,)", tmp);
+      gtk_text_buffer_set_text (buffer, text, -1);
+    }
+
+  return g_variant_parse (type, text, NULL, NULL, error);
+}
+
+static void
+dspy_method_view_button_clicked_cb (DspyMethodView *self,
+                                    GtkButton      *button)
+{
+  DspyMethodViewPrivate *priv = dspy_method_view_get_instance_private (self);
+  g_autoptr(GVariant) params = NULL;
+  g_autoptr(GError) error = NULL;
+  const gchar *signature;
+  Execute *state;
+
+  g_assert (DSPY_IS_METHOD_VIEW (self));
+  g_assert (GTK_IS_BUTTON (button));
+
+  /* Always cancel anything in flight */
+  g_cancellable_cancel (priv->cancellable);
+  g_clear_object (&priv->cancellable);
+
+  if (priv->busy)
+    return;
+
+  if (priv->invocation == NULL)
+    return;
+
+  g_assert (priv->busy == FALSE);
+  g_assert (priv->cancellable == NULL);
+
+  signature = dspy_method_invocation_get_signature (priv->invocation);
+
+  if (!signature || !signature[0])
+    signature = NULL;
+
+  if (!(params = get_variant_for_text_buffer (priv->buffer_params,
+                                              (const GVariantType *)signature,
+                                              &error)))
+    {
+      gtk_text_buffer_set_text (priv->buffer_reply, error->message, -1);
+      return;
+    }
+
+  dspy_method_invocation_set_parameters (priv->invocation, params);
+
+  priv->busy = TRUE;
+  priv->cancellable = g_cancellable_new ();
+
+  gtk_text_buffer_set_text (priv->buffer_reply, "", -1);
+
+  state = g_slice_new0 (Execute);
+  state->self = g_object_ref (self);
+  state->timer = g_timer_new ();
+
+  dspy_method_invocation_execute_async (priv->invocation,
+                                        priv->cancellable,
+                                        dspy_method_view_execute_cb,
+                                        state);
+
+  gtk_button_set_label (priv->button, _("Cancel"));
+}
+
+static void
+dspy_method_view_invoke_method (GtkWidget *widget,
+                                gpointer   user_data)
+{
+  DspyMethodView *self = user_data;
+  DspyMethodViewPrivate *priv = dspy_method_view_get_instance_private (self);
+
+  g_assert (DSPY_IS_METHOD_VIEW (self));
+
+  gtk_widget_activate (GTK_WIDGET (priv->button));
+}
+
+static void
+copy_button_clicked_cb (DspyMethodView *self,
+                        GtkButton      *button)
+{
+  DspyMethodViewPrivate *priv = dspy_method_view_get_instance_private (self);
+  g_autofree gchar *text = NULL;
+  GtkClipboard *clipboard;
+  GtkTextIter begin;
+  GtkTextIter end;
+
+  g_assert (DSPY_IS_METHOD_VIEW (self));
+  g_assert (GTK_IS_BUTTON (button));
+
+  if (!gtk_text_buffer_get_selection_bounds (priv->buffer_reply, &begin, &end))
+    gtk_text_buffer_get_bounds (priv->buffer_reply, &begin, &end);
+
+  text = gtk_text_iter_get_slice (&begin, &end);
+  clipboard = gtk_widget_get_clipboard (GTK_WIDGET (self), GDK_SELECTION_CLIPBOARD);
+  gtk_clipboard_set_text (clipboard, text, -1);
+}
+
+static void
+dspy_method_view_finalize (GObject *object)
+{
+  DspyMethodView *self = (DspyMethodView *)object;
+  DspyMethodViewPrivate *priv = dspy_method_view_get_instance_private (self);
+
+  dzl_binding_group_set_source (priv->bindings, NULL);
+
+  g_clear_object (&priv->invocation);
+  g_clear_object (&priv->bindings);
+  g_clear_object (&priv->cancellable);
+  g_clear_pointer (&priv->durations, g_array_unref);
+
+  G_OBJECT_CLASS (dspy_method_view_parent_class)->finalize (object);
+}
+
+static void
+dspy_method_view_get_property (GObject    *object,
+                               guint       prop_id,
+                               GValue     *value,
+                               GParamSpec *pspec)
+{
+  DspyMethodView *self = DSPY_METHOD_VIEW (object);
+
+  switch (prop_id)
+    {
+    case PROP_INVOCATION:
+      g_value_set_object (value, dspy_method_view_get_invocation (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dspy_method_view_set_property (GObject      *object,
+                               guint         prop_id,
+                               const GValue *value,
+                               GParamSpec   *pspec)
+{
+  DspyMethodView *self = DSPY_METHOD_VIEW (object);
+
+  switch (prop_id)
+    {
+    case PROP_INVOCATION:
+      dspy_method_view_set_invocation (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dspy_method_view_class_init (DspyMethodViewClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = dspy_method_view_finalize;
+  object_class->get_property = dspy_method_view_get_property;
+  object_class->set_property = dspy_method_view_set_property;
+
+  properties [PROP_INVOCATION] =
+    g_param_spec_object ("invocation",
+                         "Invocation",
+                         "The method invocation to view",
+                         DSPY_TYPE_METHOD_INVOCATION,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/dspy/dspy-method-view.ui");
+  gtk_widget_class_bind_template_child_private (widget_class, DspyMethodView, buffer_params);
+  gtk_widget_class_bind_template_child_private (widget_class, DspyMethodView, buffer_reply);
+  gtk_widget_class_bind_template_child_private (widget_class, DspyMethodView, button);
+  gtk_widget_class_bind_template_child_private (widget_class, DspyMethodView, copy_button);
+  gtk_widget_class_bind_template_child_private (widget_class, DspyMethodView, label_avg);
+  gtk_widget_class_bind_template_child_private (widget_class, DspyMethodView, label_interface);
+  gtk_widget_class_bind_template_child_private (widget_class, DspyMethodView, label_max);
+  gtk_widget_class_bind_template_child_private (widget_class, DspyMethodView, label_method);
+  gtk_widget_class_bind_template_child_private (widget_class, DspyMethodView, label_min);
+  gtk_widget_class_bind_template_child_private (widget_class, DspyMethodView, label_object_path);
+  gtk_widget_class_bind_template_child_private (widget_class, DspyMethodView, textview_params);
+}
+
+static void
+dspy_method_view_init (DspyMethodView *self)
+{
+  DspyMethodViewPrivate *priv = dspy_method_view_get_instance_private (self);
+  DzlShortcutController *controller;
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  priv->durations = g_array_new (FALSE, FALSE, sizeof (gdouble));
+
+  priv->bindings = dzl_binding_group_new ();
+  dzl_binding_group_bind (priv->bindings, "interface", priv->label_interface, "label", 0);
+  dzl_binding_group_bind (priv->bindings, "method", priv->label_method, "label", 0);
+  dzl_binding_group_bind (priv->bindings, "object-path", priv->label_object_path, "label", 0);
+  dzl_binding_group_bind_full (priv->bindings, "parameters", priv->buffer_params, "text", 0,
+                               variant_to_string_transform, NULL, NULL, NULL);
+
+  g_signal_connect_object (priv->button,
+                           "clicked",
+                           G_CALLBACK (dspy_method_view_button_clicked_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (priv->copy_button,
+                           "clicked",
+                           G_CALLBACK (copy_button_clicked_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  controller = dzl_shortcut_controller_find (GTK_WIDGET (priv->textview_params));
+
+  dzl_shortcut_controller_add_command_callback (controller,
+                                                "org.gnome.dspy.invoke-method",
+                                                "<Primary>Return",
+                                                DZL_SHORTCUT_PHASE_DISPATCH,
+                                                dspy_method_view_invoke_method,
+                                                self,
+                                                NULL);
+}
+
+void
+dspy_method_view_set_invocation (DspyMethodView       *self,
+                                 DspyMethodInvocation *invocation)
+{
+  DspyMethodViewPrivate *priv = dspy_method_view_get_instance_private (self);
+
+  g_return_if_fail (DSPY_IS_METHOD_VIEW (self));
+  g_return_if_fail (!invocation || DSPY_IS_METHOD_INVOCATION (invocation));
+
+  if (g_set_object (&priv->invocation, invocation))
+    {
+      g_cancellable_cancel (priv->cancellable);
+      g_clear_object (&priv->cancellable);
+
+      dzl_binding_group_set_source (priv->bindings, invocation);
+      gtk_text_buffer_set_text (priv->buffer_reply, "", -1);
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_INVOCATION]);
+    }
+}
+
+/**
+ * dspy_method_view_get_invocation:
+ *
+ * Returns: (transfer none) (nullable): a #DspyMethodInvocation or %NULL
+ */
+DspyMethodInvocation *
+dspy_method_view_get_invocation (DspyMethodView *self)
+{
+  DspyMethodViewPrivate *priv = dspy_method_view_get_instance_private (self);
+
+  g_return_val_if_fail (DSPY_IS_METHOD_VIEW (self), NULL);
+
+  return priv->invocation;
+}
diff --git a/src/plugins/dspy/libdspy/dspy-method-view.h b/src/plugins/dspy/libdspy/dspy-method-view.h
new file mode 100644
index 000000000..8729b448c
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-method-view.h
@@ -0,0 +1,46 @@
+/* dspy-method-view.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <dazzle.h>
+
+#include "dspy-method-invocation.h"
+
+G_BEGIN_DECLS
+
+#define DSPY_TYPE_METHOD_VIEW (dspy_method_view_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (DspyMethodView, dspy_method_view, DSPY, METHOD_VIEW, DzlBin)
+
+struct _DspyMethodViewClass
+{
+  DzlBinClass parent_class;
+
+  /*< private >*/
+  gpointer _reserved[8];
+};
+
+GtkWidget            *dspy_method_view_new            (void);
+DspyMethodInvocation *dspy_method_view_get_invocation (DspyMethodView       *self);
+void                  dspy_method_view_set_invocation (DspyMethodView       *self,
+                                                       DspyMethodInvocation *invocation);
+
+G_END_DECLS
diff --git a/src/plugins/dspy/libdspy/dspy-method-view.ui b/src/plugins/dspy/libdspy/dspy-method-view.ui
new file mode 100644
index 000000000..d5272ce51
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-method-view.ui
@@ -0,0 +1,282 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk+" version="3.22"/>
+  <template class="DspyMethodView" parent="DzlBin">
+    <child>
+      <object class="GtkGrid">
+        <property name="column-spacing">12</property>
+        <property name="row-spacing">3</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkLabel">
+            <property name="label" translatable="yes">Object Path</property>
+            <property name="visible">true</property>
+            <property name="xalign">1.0</property>
+            <style>
+              <class name="dim-label"/>
+            </style>
+          </object>
+          <packing>
+            <property name="top-attach">0</property>
+            <property name="left-attach">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="label_object_path">
+            <property name="hexpand">true</property>
+            <property name="visible">true</property>
+            <property name="ellipsize">end</property>
+            <property name="xalign">0.0</property>
+          </object>
+          <packing>
+            <property name="top-attach">0</property>
+            <property name="left-attach">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel">
+            <property name="label" translatable="yes">Interface</property>
+            <property name="visible">true</property>
+            <property name="xalign">1.0</property>
+            <style>
+              <class name="dim-label"/>
+            </style>
+          </object>
+          <packing>
+            <property name="top-attach">1</property>
+            <property name="left-attach">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="label_interface">
+            <property name="hexpand">true</property>
+            <property name="visible">true</property>
+            <property name="ellipsize">end</property>
+            <property name="xalign">0.0</property>
+          </object>
+          <packing>
+            <property name="top-attach">1</property>
+            <property name="left-attach">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel">
+            <property name="label" translatable="yes">Method</property>
+            <property name="visible">true</property>
+            <property name="xalign">1.0</property>
+            <style>
+              <class name="dim-label"/>
+            </style>
+          </object>
+          <packing>
+            <property name="top-attach">2</property>
+            <property name="left-attach">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="label_method">
+            <property name="hexpand">true</property>
+            <property name="visible">true</property>
+            <property name="ellipsize">end</property>
+            <property name="xalign">0.0</property>
+          </object>
+          <packing>
+            <property name="top-attach">2</property>
+            <property name="left-attach">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel">
+            <property name="label" translatable="yes">Parameters</property>
+            <property name="visible">true</property>
+            <property name="xalign">1.0</property>
+            <property name="valign">start</property>
+            <style>
+              <class name="dim-label"/>
+            </style>
+          </object>
+          <packing>
+            <property name="top-attach">3</property>
+            <property name="left-attach">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkBox">
+            <property name="orientation">horizontal</property>
+            <property name="hexpand">true</property>
+            <property name="visible">true</property>
+            <style>
+              <class name="linked"/>
+            </style>
+            <child>
+              <object class="GtkScrolledWindow">
+                <property name="hexpand">true</property>
+                <property name="shadow-type">in</property>
+                <property name="hscrollbar-policy">never</property>
+                <property name="propagate-natural-height">true</property>
+                <property name="max-content-height">100</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="GtkTextView" id="textview_params">
+                    <property name="buffer">buffer_params</property>
+                    <property name="wrap-mode">char</property>
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkButton" id="button">
+                <property name="label" translatable="yes">Execute</property>
+                <property name="visible">true</property>
+                <style>
+                  <class name="suggested-action"/>
+                </style>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="top-attach">3</property>
+            <property name="left-attach">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel">
+            <property name="label" translatable="yes">Result</property>
+            <property name="visible">true</property>
+            <property name="xalign">1.0</property>
+            <property name="valign">start</property>
+            <style>
+              <class name="dim-label"/>
+            </style>
+          </object>
+          <packing>
+            <property name="top-attach">4</property>
+            <property name="left-attach">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkBox">
+            <property name="orientation">horizontal</property>
+            <property name="hexpand">true</property>
+            <property name="visible">true</property>
+            <style>
+              <class name="linked"/>
+            </style>
+            <child>
+              <object class="GtkScrolledWindow">
+                <property name="hexpand">true</property>
+                <property name="shadow-type">in</property>
+                <property name="hscrollbar-policy">never</property>
+                <property name="propagate-natural-height">true</property>
+                <property name="max-content-height">100</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="GtkTextView">
+                    <property name="buffer">buffer_reply</property>
+                    <property name="wrap-mode">char</property>
+                    <property name="editable">false</property>
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkButton" id="copy_button">
+                <property name="label" translatable="yes">Copy</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="top-attach">4</property>
+            <property name="left-attach">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel">
+            <property name="label" translatable="yes">Elapsed Time</property>
+            <property name="halign">end</property>
+            <property name="visible">true</property>
+            <style>
+              <class name="dim-label"/>
+            </style>
+          </object>
+          <packing>
+            <property name="top-attach">5</property>
+            <property name="left-attach">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkBox">
+            <property name="orientation">horizontal</property>
+            <property name="homogeneous">true</property>
+            <property name="spacing">6</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkLabel">
+                <property name="label">Ø:</property>
+                <property name="halign">end</property>
+                <property name="visible">true</property>
+                <style>
+                  <class name="dim-label"/>
+                </style>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel" id="label_avg">
+                <property name="halign">start</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel">
+                <property name="label">Min:</property>
+                <property name="halign">end</property>
+                <property name="visible">true</property>
+                <style>
+                  <class name="dim-label"/>
+                </style>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel" id="label_min">
+                <property name="halign">start</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel">
+                <property name="label">Max:</property>
+                <property name="halign">end</property>
+                <property name="visible">true</property>
+                <style>
+                  <class name="dim-label"/>
+                </style>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel" id="label_max">
+                <property name="halign">start</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="top-attach">5</property>
+            <property name="left-attach">1</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </template>
+  <object class="GtkTextBuffer" id="buffer_params"/>
+  <object class="GtkTextBuffer" id="buffer_reply"/>
+  <object class="GtkSizeGroup">
+    <property name="mode">horizontal</property>
+    <widgets>
+      <widget name="button"/>
+      <widget name="copy_button"/>
+    </widgets>
+  </object>
+</interface>
diff --git a/src/plugins/dspy/libdspy/dspy-name-marquee.c b/src/plugins/dspy/libdspy/dspy-name-marquee.c
new file mode 100644
index 000000000..324877268
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-name-marquee.c
@@ -0,0 +1,186 @@
+/* dspy-name-marquee.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "dspy-name-marquee"
+
+#include "config.h"
+
+#include <dazzle.h>
+
+#include "dspy-name-marquee.h"
+
+struct _DspyNameMarquee
+{
+  GtkBin           parent_instance;
+
+  DspyName        *name;
+  DzlBindingGroup *name_bindings;
+
+  GtkLabel        *label_bus;
+  GtkLabel        *label_name;
+  GtkLabel        *label_owner;
+  GtkLabel        *label_pid;
+};
+
+G_DEFINE_TYPE (DspyNameMarquee, dspy_name_marquee, GTK_TYPE_BIN)
+
+enum {
+  PROP_0,
+  PROP_NAME,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+/**
+ * dspy_name_marquee_new:
+ *
+ * Create a new #DspyNameMarquee.
+ *
+ * Returns: (transfer full): a newly created #DspyNameMarquee
+ */
+GtkWidget *
+dspy_name_marquee_new (void)
+{
+  return g_object_new (DSPY_TYPE_NAME_MARQUEE, NULL);
+}
+
+static void
+dspy_name_marquee_finalize (GObject *object)
+{
+  DspyNameMarquee *self = (DspyNameMarquee *)object;
+
+  dzl_binding_group_set_source (self->name_bindings, NULL);
+  g_clear_object (&self->name_bindings);
+  g_clear_object (&self->name);
+
+  G_OBJECT_CLASS (dspy_name_marquee_parent_class)->finalize (object);
+}
+
+static void
+dspy_name_marquee_get_property (GObject    *object,
+                                guint       prop_id,
+                                GValue     *value,
+                                GParamSpec *pspec)
+{
+  DspyNameMarquee *self = DSPY_NAME_MARQUEE (object);
+
+  switch (prop_id)
+    {
+    case PROP_NAME:
+      g_value_set_object (value, dspy_name_marquee_get_name (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dspy_name_marquee_set_property (GObject      *object,
+                                guint         prop_id,
+                                const GValue *value,
+                                GParamSpec   *pspec)
+{
+  DspyNameMarquee *self = DSPY_NAME_MARQUEE (object);
+
+  switch (prop_id)
+    {
+    case PROP_NAME:
+      dspy_name_marquee_set_name (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dspy_name_marquee_class_init (DspyNameMarqueeClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = dspy_name_marquee_finalize;
+  object_class->get_property = dspy_name_marquee_get_property;
+  object_class->set_property = dspy_name_marquee_set_property;
+
+  properties [PROP_NAME] =
+    g_param_spec_object ("name",
+                         "Name",
+                         "The DspyName to display on the marquee",
+                         DSPY_TYPE_NAME,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/dspy/dspy-name-marquee.ui");
+  gtk_widget_class_bind_template_child (widget_class, DspyNameMarquee, label_bus);
+  gtk_widget_class_bind_template_child (widget_class, DspyNameMarquee, label_name);
+  gtk_widget_class_bind_template_child (widget_class, DspyNameMarquee, label_owner);
+  gtk_widget_class_bind_template_child (widget_class, DspyNameMarquee, label_pid);
+}
+
+static void
+dspy_name_marquee_init (DspyNameMarquee *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  self->name_bindings = dzl_binding_group_new ();
+
+  dzl_binding_group_bind (self->name_bindings, "pid", self->label_pid, "label", 0);
+  dzl_binding_group_bind (self->name_bindings, "name", self->label_name, "label", 0);
+  dzl_binding_group_bind (self->name_bindings, "owner", self->label_owner, "label", 0);
+}
+
+/**
+ * dspy_name_marquee_get_name:
+ *
+ * Gets the name on the marquee
+ *
+ * Returns: (nullable) (transfer none): a #DspyName or %NULL
+ */
+DspyName *
+dspy_name_marquee_get_name (DspyNameMarquee *self)
+{
+  g_return_val_if_fail (DSPY_IS_NAME_MARQUEE (self), NULL);
+
+  return self->name;
+}
+
+void
+dspy_name_marquee_set_name (DspyNameMarquee *self,
+                            DspyName        *name)
+{
+  g_return_if_fail (DSPY_IS_NAME_MARQUEE (self));
+  g_return_if_fail (!name || DSPY_IS_NAME (name));
+
+  if (g_set_object (&self->name, name))
+    {
+      const gchar *address = NULL;
+
+      if (name != NULL)
+        address = dspy_connection_get_address (dspy_name_get_connection (name));
+
+      dzl_binding_group_set_source (self->name_bindings, name);
+      gtk_label_set_label (self->label_bus, address);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_NAME]);
+    }
+}
diff --git a/src/plugins/dspy/libdspy/dspy-name-marquee.h b/src/plugins/dspy/libdspy/dspy-name-marquee.h
new file mode 100644
index 000000000..14cfa859b
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-name-marquee.h
@@ -0,0 +1,38 @@
+/* dspy-name-marquee.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+#include "dspy-name.h"
+
+G_BEGIN_DECLS
+
+#define DSPY_TYPE_NAME_MARQUEE (dspy_name_marquee_get_type())
+
+G_DECLARE_FINAL_TYPE (DspyNameMarquee, dspy_name_marquee, DSPY, NAME_MARQUEE, GtkBin)
+
+GtkWidget *dspy_name_marquee_new      (void);
+DspyName  *dspy_name_marquee_get_name (DspyNameMarquee *self);
+void       dspy_name_marquee_set_name (DspyNameMarquee *self,
+                                       DspyName        *name);
+
+G_END_DECLS
diff --git a/src/plugins/dspy/libdspy/dspy-name-marquee.ui b/src/plugins/dspy/libdspy/dspy-name-marquee.ui
new file mode 100644
index 000000000..0c2087d12
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-name-marquee.ui
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.1 -->
+<interface>
+  <requires lib="gtk+" version="3.10"/>
+  <template class="DspyNameMarquee" parent="GtkBin">
+    <child>
+      <object class="GtkGrid">
+        <property name="margin-start">12</property>
+        <property name="column-spacing">12</property>
+        <property name="row-spacing">3</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkLabel">
+            <property name="label" translatable="yes">Bus Address</property>
+            <property name="xalign">1.0</property>
+            <property name="visible">true</property>
+            <style>
+              <class name="dim-label"/>
+            </style>
+          </object>
+          <packing>
+            <property name="top-attach">0</property>
+            <property name="left-attach">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="label_bus">
+            <property name="xalign">0.0</property>
+            <property name="selectable">true</property>
+            <property name="visible">true</property>
+          </object>
+          <packing>
+            <property name="top-attach">0</property>
+            <property name="left-attach">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel">
+            <property name="label" translatable="yes">Name</property>
+            <property name="xalign">1.0</property>
+            <property name="visible">true</property>
+            <style>
+              <class name="dim-label"/>
+            </style>
+          </object>
+          <packing>
+            <property name="top-attach">1</property>
+            <property name="left-attach">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="label_name">
+            <property name="xalign">0.0</property>
+            <property name="selectable">true</property>
+            <property name="visible">true</property>
+          </object>
+          <packing>
+            <property name="top-attach">1</property>
+            <property name="left-attach">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel">
+            <property name="label" translatable="yes">Owner</property>
+            <property name="xalign">1.0</property>
+            <property name="visible">true</property>
+            <style>
+              <class name="dim-label"/>
+            </style>
+          </object>
+          <packing>
+            <property name="top-attach">2</property>
+            <property name="left-attach">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="label_owner">
+            <property name="xalign">0.0</property>
+            <property name="selectable">true</property>
+            <property name="visible">true</property>
+          </object>
+          <packing>
+            <property name="top-attach">2</property>
+            <property name="left-attach">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel">
+            <property name="label" translatable="yes">Process ID</property>
+            <property name="xalign">1.0</property>
+            <property name="visible">true</property>
+            <style>
+              <class name="dim-label"/>
+            </style>
+          </object>
+          <packing>
+            <property name="top-attach">3</property>
+            <property name="left-attach">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="label_pid">
+            <property name="xalign">0.0</property>
+            <property name="selectable">true</property>
+            <property name="visible">true</property>
+          </object>
+          <packing>
+            <property name="top-attach">3</property>
+            <property name="left-attach">1</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/plugins/dspy/libdspy/dspy-name-row.c b/src/plugins/dspy/libdspy/dspy-name-row.c
new file mode 100644
index 000000000..987e9ec0b
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-name-row.c
@@ -0,0 +1,214 @@
+/* dspy-name-row.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-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_instance;
+
+  DspyName      *name;
+
+  GtkLabel      *title;
+  GtkLabel      *subtitle;
+};
+
+G_DEFINE_TYPE (DspyNameRow, dspy_name_row, GTK_TYPE_LIST_BOX_ROW)
+
+enum {
+  PROP_0,
+  PROP_NAME,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+/**
+ * dspy_name_row_new:
+ * @name: a #DspyName
+ *
+ * Create a new #DspyNameRow.
+ *
+ * Returns: (transfer full): a newly created #DspyNameRow
+ */
+GtkWidget *
+dspy_name_row_new (DspyName *name)
+{
+  g_return_val_if_fail (DSPY_IS_NAME (name), NULL);
+
+  return g_object_new (DSPY_TYPE_NAME_ROW,
+                       "name", name,
+                       "visible", TRUE,
+                       NULL);
+}
+
+static void
+dspy_name_row_update (DspyNameRow *self)
+{
+  g_autoptr(GString) str = NULL;
+  GPid pid;
+
+  g_assert (DSPY_IS_NAME_ROW (self));
+
+  pid = dspy_name_get_pid (self->name);
+  str = g_string_new (NULL);
+
+  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 > -1)
+    {
+      g_string_append (str, ", ");
+      g_string_append_printf (str, _("%s: %u"), _("Pid"), pid);
+    }
+
+  gtk_label_set_label (self->subtitle, str->str);
+
+  gtk_widget_set_tooltip_text (GTK_WIDGET (self),
+                               dspy_name_get_owner (self->name));
+}
+
+static void
+dspy_name_row_set_name (DspyNameRow *self,
+                        DspyName    *name)
+{
+  g_assert (DSPY_IS_NAME_ROW (self));
+  g_assert (DSPY_IS_NAME (name));
+  g_assert (self->name == NULL);
+
+  g_set_object (&self->name, name);
+
+  g_signal_connect_object (self->name,
+                           "notify::pid",
+                           G_CALLBACK (dspy_name_row_update),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->name,
+                           "notify::activatable",
+                           G_CALLBACK (dspy_name_row_update),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  gtk_label_set_label (self->title, dspy_name_get_name (self->name));
+
+  dspy_name_row_update (self);
+}
+
+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_get_property (GObject    *object,
+                            guint       prop_id,
+                            GValue     *value,
+                            GParamSpec *pspec)
+{
+  DspyNameRow *self = DSPY_NAME_ROW (object);
+
+  switch (prop_id)
+    {
+    case PROP_NAME:
+      g_value_set_object (value, dspy_name_row_get_name (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dspy_name_row_set_property (GObject      *object,
+                            guint         prop_id,
+                            const GValue *value,
+                            GParamSpec   *pspec)
+{
+  DspyNameRow *self = DSPY_NAME_ROW (object);
+
+  switch (prop_id)
+    {
+    case PROP_NAME:
+      dspy_name_row_set_name (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+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;
+  object_class->get_property = dspy_name_row_get_property;
+  object_class->set_property = dspy_name_row_set_property;
+
+  properties [PROP_NAME] =
+    g_param_spec_object ("name",
+                         "Name",
+                         "The DspyName for the row",
+                         DSPY_TYPE_NAME,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/dspy/dspy-name-row.ui");
+  gtk_widget_class_bind_template_child (widget_class, DspyNameRow, subtitle);
+  gtk_widget_class_bind_template_child (widget_class, DspyNameRow, title);
+}
+
+static void
+dspy_name_row_init (DspyNameRow *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+/**
+ * dspy_name_row_get_name:
+ *
+ * Gets the #DspyName for the row.
+ *
+ * Returns: (transfer none): a #DspyName
+ */
+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/libdspy/dspy-name-row.h b/src/plugins/dspy/libdspy/dspy-name-row.h
new file mode 100644
index 000000000..8b48f6ee4
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-name-row.h
@@ -0,0 +1,36 @@
+/* dspy-name-row.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-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)
+
+GtkWidget *dspy_name_row_new      (DspyName    *name);
+DspyName  *dspy_name_row_get_name (DspyNameRow *self);
+
+G_END_DECLS
diff --git a/src/plugins/dspy/libdspy/dspy-name-row.ui b/src/plugins/dspy/libdspy/dspy-name-row.ui
new file mode 100644
index 000000000..eaa869d93
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-name-row.ui
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.1 -->
+<interface>
+  <requires lib="gtk+" version="3.10"/>
+  <template class="DspyNameRow" parent="GtkListBoxRow">
+    <property name="can_focus">False</property>
+    <child>
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="margin_left">6</property>
+        <property name="margin_right">6</property>
+        <property name="margin_top">6</property>
+        <property name="margin_bottom">6</property>
+        <property name="orientation">vertical</property>
+        <property name="spacing">3</property>
+        <child>
+          <object class="GtkLabel" id="title">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="ellipsize">end</property>
+            <property name="xalign">0</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="subtitle">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="ellipsize">end</property>
+            <property name="xalign">0</property>
+            <attributes>
+              <attribute name="scale" value="0.83330000000000004"/>
+            </attributes>
+            <style>
+              <class name="dim-label"/>
+            </style>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/plugins/dspy/libdspy/dspy-name.c b/src/plugins/dspy/libdspy/dspy-name.c
new file mode 100644
index 000000000..2b0489dc6
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-name.c
@@ -0,0 +1,486 @@
+/* dspy-name.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "dspy-name"
+
+#include "config.h"
+
+#include "dspy-introspection-model.h"
+#include "dspy-name.h"
+#include "dspy-private.h"
+
+struct _DspyName
+{
+  GObject         parent_instance;
+  DspyConnection *connection;
+  gchar          *name;
+  gchar          *owner;
+  gchar          *search_text;
+  GPid            pid;
+  guint           activatable : 1;
+};
+
+enum {
+  PROP_0,
+  PROP_ACTIVATABLE,
+  PROP_CONNECTION,
+  PROP_NAME,
+  PROP_OWNER,
+  PROP_PID,
+  N_PROPS
+};
+
+G_DEFINE_TYPE (DspyName, dspy_name, G_TYPE_OBJECT)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+dspy_name_finalize (GObject *object)
+{
+  DspyName *self = (DspyName *)object;
+
+  g_clear_object (&self->connection);
+  g_clear_pointer (&self->name, g_free);
+  g_clear_pointer (&self->owner, g_free);
+  g_clear_pointer (&self->search_text, 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_CONNECTION:
+      g_value_set_object (value, dspy_name_get_connection (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_int (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);
+
+  switch (prop_id)
+    {
+    case PROP_ACTIVATABLE:
+      self->activatable = g_value_get_boolean (value);
+      break;
+
+    case PROP_CONNECTION:
+      self->connection = g_value_dup_object (value);
+      break;
+
+    case PROP_NAME:
+      self->name = g_value_dup_string (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_CONNECTION] =
+    g_param_spec_object ("connection",
+                         "Connection",
+                         "The connection where the name can be found",
+                         DSPY_TYPE_CONNECTION,
+                         (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_READABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_PID] =
+    g_param_spec_int ("pid",
+                      "Pid",
+                      "The pid of the peer",
+                      -1, G_MAXINT, -1,
+                      (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+dspy_name_init (DspyName *self)
+{
+  self->pid = -1;
+}
+
+DspyName *
+dspy_name_new (DspyConnection *connection,
+               const gchar    *name,
+               gboolean        activatable)
+{
+  return g_object_new (DSPY_TYPE_NAME,
+                       "activatable", activatable,
+                       "connection", connection,
+                       "name", name,
+                       NULL);
+}
+
+gboolean
+dspy_name_get_activatable (DspyName *self)
+{
+  g_return_val_if_fail (DSPY_IS_NAME (self), FALSE);
+
+  return self->activatable;
+}
+
+void
+_dspy_name_set_activatable (DspyName *self,
+                            gboolean  activatable)
+{
+  g_return_if_fail (DSPY_IS_NAME (self));
+
+  activatable = !!activatable;
+
+  if (self->activatable != activatable)
+    {
+      self->activatable = activatable;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ACTIVATABLE]);
+    }
+}
+
+const gchar *
+dspy_name_get_name (DspyName *self)
+{
+  g_return_val_if_fail (DSPY_IS_NAME (self), NULL);
+
+  return self->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)
+{
+  g_return_val_if_fail (DSPY_IS_NAME (self), 0);
+
+  return self->pid;
+}
+
+const gchar *
+dspy_name_get_owner (DspyName *self)
+{
+  g_return_val_if_fail (DSPY_IS_NAME (self), NULL);
+
+  return self->owner ? self->owner : self->name;
+}
+
+void
+_dspy_name_set_owner (DspyName    *self,
+                      const gchar *owner)
+{
+  g_return_if_fail (DSPY_IS_NAME (self));
+
+  if (g_strcmp0 (owner, self->owner) != 0)
+    {
+      g_free (self->owner);
+      self->owner = g_strdup (owner);
+      g_clear_pointer (&self->search_text, g_free);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_OWNER]);
+    }
+}
+
+void
+_dspy_name_clear_pid (DspyName *self)
+{
+  g_return_if_fail (DSPY_IS_NAME (self));
+
+  if (self->pid != -1)
+    {
+      self->pid = -1;
+      g_clear_pointer (&self->search_text, g_free);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PID]);
+    }
+}
+
+static void
+dspy_name_get_pid_cb (GObject      *object,
+                      GAsyncResult *result,
+                      gpointer      user_data)
+{
+  GDBusConnection *connection = (GDBusConnection *)object;
+  g_autoptr(DspyName) self = user_data;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GVariant) reply = NULL;
+  guint pid;
+
+  g_assert (G_IS_DBUS_CONNECTION (connection));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (DSPY_IS_NAME (self));
+
+  if (!(reply = g_dbus_connection_call_finish (connection, result, &error)))
+    return;
+
+  g_variant_get (reply, "(u)", &pid);
+
+  if (self->pid != pid)
+    {
+      self->pid = pid;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PID]);
+    }
+}
+
+void
+_dspy_name_refresh_pid (DspyName        *self,
+                        GDBusConnection *connection)
+{
+  g_return_if_fail (DSPY_IS_NAME (self));
+  g_return_if_fail (G_IS_DBUS_CONNECTION (connection));
+
+  g_dbus_connection_call (connection,
+                          "org.freedesktop.DBus",
+                          "/org/freedesktop/DBus",
+                          "org.freedesktop.DBus",
+                          "GetConnectionUnixProcessID",
+                          g_variant_new ("(s)", self->name),
+                          G_VARIANT_TYPE ("(u)"),
+                          G_DBUS_CALL_FLAGS_NONE,
+                          -1,
+                          NULL,
+                          dspy_name_get_pid_cb,
+                          g_object_ref (self));
+}
+
+static void
+dspy_name_get_owner_cb (GObject      *object,
+                        GAsyncResult *result,
+                        gpointer      user_data)
+{
+  GDBusConnection *connection = (GDBusConnection *)object;
+  g_autoptr(DspyName) self = user_data;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GVariant) reply = NULL;
+  const gchar *owner = NULL;
+
+  g_assert (G_IS_DBUS_CONNECTION (connection));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (DSPY_IS_NAME (self));
+
+  if (!(reply = g_dbus_connection_call_finish (connection, result, &error)))
+    return;
+
+  g_variant_get (reply, "(&s)", &owner);
+  _dspy_name_set_owner (self, owner);
+}
+
+void
+_dspy_name_refresh_owner (DspyName        *self,
+                          GDBusConnection *connection)
+{
+  g_return_if_fail (DSPY_IS_NAME (self));
+  g_return_if_fail (G_IS_DBUS_CONNECTION (connection));
+
+  g_clear_pointer (&self->owner, g_free);
+
+  /* If the name is already a :0.123 style name, that's the owner */
+  if (self->name[0] == ':')
+    return;
+
+  g_dbus_connection_call (connection,
+                          "org.freedesktop.DBus",
+                          "/org/freedesktop/DBus",
+                          "org.freedesktop.DBus",
+                          "GetNameOwner",
+                          g_variant_new ("(s)", self->name),
+                          G_VARIANT_TYPE ("(s)"),
+                          G_DBUS_CALL_FLAGS_NONE,
+                          -1,
+                          NULL,
+                          dspy_name_get_owner_cb,
+                          g_object_ref (self));
+}
+
+/**
+ * dspy_name_get_connection:
+ *
+ * Gets the connection that is to be used.
+ *
+ * Returns: (transer none): a #DspyConnection or %NULL
+ */
+DspyConnection *
+dspy_name_get_connection (DspyName *self)
+{
+  g_return_val_if_fail (DSPY_IS_NAME (self), NULL);
+
+  return self->connection;
+}
+
+static void
+dspy_name_introspection_cb (GObject      *object,
+                            GAsyncResult *result,
+                            gpointer      user_data)
+{
+  GAsyncInitable *initable = (GAsyncInitable *)object;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GTask) task = user_data;
+
+  g_assert (G_IS_ASYNC_INITABLE (initable));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (G_IS_TASK (task));
+
+  if (!g_async_initable_init_finish (initable, result, &error))
+    g_task_return_error (task, g_steal_pointer (&error));
+  else
+    g_task_return_pointer (task, g_object_ref (initable), g_object_unref);
+}
+
+void
+dspy_name_introspect_async (DspyName            *self,
+                            GCancellable        *cancellable,
+                            GAsyncReadyCallback  callback,
+                            gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+  g_autoptr(DspyIntrospectionModel) model = NULL;
+
+  g_return_if_fail (DSPY_IS_NAME (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, dspy_name_introspect_async);
+
+  model = _dspy_introspection_model_new (self);
+
+  g_async_initable_init_async (G_ASYNC_INITABLE (model),
+                               G_PRIORITY_DEFAULT,
+                               cancellable,
+                               dspy_name_introspection_cb,
+                               g_steal_pointer (&task));
+
+}
+
+/**
+ * dspy_name_introspect_finish:
+ *
+ * Completes a request to dspy_name_introspect_async().
+ *
+ * Returns: (transfer full): a #GtkTreeModel if successful; otherwise
+ *   %NULL and @error is set.
+ */
+GtkTreeModel *
+dspy_name_introspect_finish (DspyName      *self,
+                             GAsyncResult  *result,
+                             GError       **error)
+{
+  g_return_val_if_fail (DSPY_IS_NAME (self), NULL);
+  g_return_val_if_fail (G_IS_TASK (result), NULL);
+
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+const gchar *
+dspy_name_get_search_text (DspyName *self)
+{
+  g_return_val_if_fail (DSPY_IS_NAME (self), FALSE);
+
+  if (self->search_text == NULL)
+    {
+      const gchar *owner = dspy_name_get_owner (self);
+      self->search_text = g_strdup_printf ("%s %s %d", self->name, owner, self->pid);
+    }
+
+  return self->search_text;
+}
diff --git a/src/plugins/dspy/libdspy/dspy-name.h b/src/plugins/dspy/libdspy/dspy-name.h
new file mode 100644
index 000000000..c62ad9f73
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-name.h
@@ -0,0 +1,53 @@
+/* dspy-name.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+#include <gtk/gtk.h>
+
+#include "dspy-connection.h"
+
+G_BEGIN_DECLS
+
+#define DSPY_TYPE_NAME (dspy_name_get_type())
+
+G_DECLARE_FINAL_TYPE (DspyName, dspy_name, DSPY, NAME, GObject)
+
+DspyName       *dspy_name_new               (DspyConnection       *connection,
+                                             const gchar          *name,
+                                             gboolean              activatable);
+DspyConnection *dspy_name_get_connection    (DspyName             *self);
+gboolean        dspy_name_get_activatable   (DspyName             *self);
+GPid            dspy_name_get_pid           (DspyName             *self);
+const gchar    *dspy_name_get_name          (DspyName             *self);
+const gchar    *dspy_name_get_owner         (DspyName             *self);
+const gchar    *dspy_name_get_search_text   (DspyName             *self);
+gint            dspy_name_compare           (gconstpointer         a,
+                                             gconstpointer         b);
+void            dspy_name_introspect_async  (DspyName             *self,
+                                             GCancellable         *cancellable,
+                                             GAsyncReadyCallback   callback,
+                                             gpointer              user_data);
+GtkTreeModel   *dspy_name_introspect_finish (DspyName             *self,
+                                             GAsyncResult         *result,
+                                             GError              **error);
+
+G_END_DECLS
diff --git a/src/plugins/dspy/libdspy/dspy-names-model.c b/src/plugins/dspy/libdspy/dspy-names-model.c
new file mode 100644
index 000000000..d1ea5605a
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-names-model.c
@@ -0,0 +1,532 @@
+/* dspy-names-model.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "dspy-names-model"
+
+#include "config.h"
+
+#include "dspy-names-model.h"
+#include "dspy-private.h"
+
+struct _DspyNamesModel
+{
+  GObject          parent_instance;
+  DspyConnection  *connection;
+  GSequence       *items;
+  GDBusConnection *bus;
+  guint            name_owner_changed_handler;
+};
+
+enum {
+  PROP_0,
+  PROP_CONNECTION,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+_g_weak_ref_free (GWeakRef *wr)
+{
+  g_weak_ref_clear (wr);
+  g_slice_free (GWeakRef, wr);
+}
+
+static GType
+dspy_names_model_get_item_type (GListModel *model)
+{
+  return DSPY_TYPE_NAME;
+}
+
+static guint
+dspy_names_model_get_n_items (GListModel *model)
+{
+  return g_sequence_get_length (DSPY_NAMES_MODEL (model)->items);
+}
+
+static gpointer
+dspy_names_model_get_item (GListModel *model,
+                           guint       position)
+{
+  DspyNamesModel *self = DSPY_NAMES_MODEL (model);
+  GSequenceIter *iter;
+
+  g_assert (DSPY_IS_NAMES_MODEL (self));
+
+  if ((iter = g_sequence_get_iter_at_pos (self->items, position)) &&
+      !g_sequence_iter_is_end (iter))
+    return g_object_ref (g_sequence_get (iter));
+
+  return NULL;
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+  iface->get_item_type = dspy_names_model_get_item_type;
+  iface->get_n_items = dspy_names_model_get_n_items;
+  iface->get_item = dspy_names_model_get_item;
+}
+
+/**
+ * dspy_names_model_get_by_name:
+ * @self: a #DspyNamesModel
+ * @name: the name to lookup, such as ":1.0" or "org.freedesktop.DBus"
+ *
+ * Looks for a #DspyName that matches @name.
+ *
+ * Returns: (transfer full) (nullable): a #DspyName or %NULL
+ */
+DspyName *
+dspy_names_model_get_by_name (DspyNamesModel *self,
+                              const gchar    *name)
+{
+  g_autoptr(DspyName) tmp = NULL;
+  GSequenceIter *iter;
+
+  g_assert (DSPY_IS_NAMES_MODEL (self));
+  g_assert (name != NULL);
+
+  tmp = dspy_name_new (self->connection, name, FALSE);
+  iter = g_sequence_lookup (self->items, tmp, (GCompareDataFunc) dspy_name_compare, NULL);
+
+  if (!iter || g_sequence_iter_is_end (iter))
+    return NULL;
+
+  return g_object_ref (g_sequence_get (iter));
+}
+
+static void
+dspy_names_model_add_names (DspyNamesModel      *self,
+                            GDBusConnection     *bus,
+                            const gchar * const *names,
+                            gboolean             is_activatable)
+{
+  g_assert (DSPY_IS_NAMES_MODEL (self));
+  g_assert (names != NULL);
+
+  for (guint i = 0; names[i] != NULL; i++)
+    {
+      g_autoptr(DspyName) name = NULL;
+      GSequenceIter *iter;
+
+      if ((name = dspy_names_model_get_by_name (self, names[i])))
+        {
+          if (is_activatable && !dspy_name_get_activatable (name))
+            _dspy_name_set_activatable (name, TRUE);
+          continue;
+        }
+
+      name = dspy_name_new (self->connection, names[i], is_activatable);
+
+      _dspy_name_refresh_pid (name, bus);
+      _dspy_name_refresh_owner (name, bus);
+
+      iter = g_sequence_insert_sorted (self->items,
+                                       g_steal_pointer (&name),
+                                       (GCompareDataFunc) dspy_name_compare,
+                                       NULL);
+      g_list_model_items_changed (G_LIST_MODEL (self),
+                                  g_sequence_iter_get_position (iter),
+                                  0, 1);
+    }
+}
+
+static void
+dspy_names_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)
+{
+  GWeakRef *wr = user_data;
+  g_autoptr(DspyNamesModel) self = NULL;
+  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 (params != NULL);
+  g_assert (g_variant_is_of_type (params, G_VARIANT_TYPE ("(sss)")));
+  g_assert (wr != NULL);
+
+  if (!(self = g_weak_ref_get (wr)))
+    return;
+
+  g_variant_get (params, "(&s&s&s)", &vname, &vold_name, &vnew_name);
+
+  name = dspy_name_new (self->connection, vname, FALSE);
+  seq = g_sequence_lookup (self->items,
+                           name,
+                           (GCompareDataFunc) dspy_name_compare,
+                           NULL);
+
+  if (seq == NULL)
+    {
+      if (vnew_name[0])
+        {
+          const gchar *names[] = { vname, NULL };
+          dspy_names_model_add_names (self, connection, names, FALSE);
+        }
+    }
+  else if (!vnew_name[0])
+    {
+      DspyName *item = g_sequence_get (seq);
+
+      if (dspy_name_get_activatable (item) &&
+          dspy_name_get_name (item)[0] != ':')
+        {
+          _dspy_name_clear_pid (item);
+        }
+      else
+        {
+          guint position = g_sequence_iter_get_position (seq);
+          g_sequence_remove (seq);
+          g_list_model_items_changed (G_LIST_MODEL (self), position, 1, 0);
+        }
+    }
+  else
+    {
+      DspyName *item = g_sequence_get (seq);
+
+      if (vnew_name[0] == ':')
+        _dspy_name_set_owner (item, vnew_name);
+
+      _dspy_name_refresh_pid (item, connection);
+    }
+}
+
+static void
+dspy_names_model_init_list_activatable_names_cb (GObject      *object,
+                                                 GAsyncResult *result,
+                                                 gpointer      user_data)
+{
+  GDBusConnection *bus = (GDBusConnection *)object;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GVariant) reply = NULL;
+  gint *n_active;
+
+  g_assert (G_IS_DBUS_CONNECTION (bus));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (G_IS_TASK (task));
+
+  if ((reply = g_dbus_connection_call_finish (bus, result, &error)))
+    {
+      g_autofree const gchar **names = NULL;
+      DspyNamesModel *self;
+
+      g_assert (reply != NULL);
+      g_assert (g_variant_is_of_type (reply, G_VARIANT_TYPE ("(as)")));
+
+      self = g_task_get_source_object (task);
+      g_variant_get (reply, "(^as)", &names);
+      dspy_names_model_add_names (self, bus, (const gchar * const *)names, TRUE);
+    }
+
+  n_active = g_task_get_task_data (task);
+  g_assert (n_active != NULL);
+  g_assert (*n_active > 0);
+
+  if (--(*n_active) == 0)
+    g_task_return_boolean (task, TRUE);
+}
+
+static void
+dspy_names_model_init_list_names_cb (GObject      *object,
+                                     GAsyncResult *result,
+                                     gpointer      user_data)
+{
+  GDBusConnection *bus = (GDBusConnection *)object;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GVariant) reply = NULL;
+  gint *n_active;
+
+  g_assert (G_IS_DBUS_CONNECTION (bus));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (G_IS_TASK (task));
+
+  if ((reply = g_dbus_connection_call_finish (bus, result, &error)))
+    {
+      g_autofree const gchar **names = NULL;
+      DspyNamesModel *self;
+
+      g_assert (reply != NULL);
+      g_assert (g_variant_is_of_type (reply, G_VARIANT_TYPE ("(as)")));
+
+      self = g_task_get_source_object (task);
+      g_variant_get (reply, "(^as)", &names);
+      dspy_names_model_add_names (self, bus, (const gchar * const *)names, FALSE);
+    }
+
+  n_active = g_task_get_task_data (task);
+  g_assert (n_active != NULL);
+  g_assert (*n_active > 0);
+
+  if (--(*n_active) == 0)
+    g_task_return_boolean (task, TRUE);
+}
+
+static void
+dspy_names_model_init_open_cb (GObject      *object,
+                               GAsyncResult *result,
+                               gpointer      user_data)
+{
+  DspyConnection *connection = (DspyConnection *)object;
+  g_autoptr(GDBusConnection) bus = NULL;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  DspyNamesModel *self;
+  GWeakRef *wr;
+
+  g_assert (DSPY_IS_CONNECTION (connection));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (G_IS_TASK (task));
+
+  if (!(bus = dspy_connection_open_finish (connection, result, &error)))
+    {
+      g_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  self = g_task_get_source_object (task);
+
+  g_assert (self != NULL);
+  g_assert (DSPY_IS_NAMES_MODEL (self));
+
+  self->bus = g_object_ref (bus);
+
+  /* Because g_dbus_connection_signal_subscribe() is not guaranteed to
+   * call the cleanup function synchronously when unsubscribed, we need to
+   * use a weak ref in allocated state to ensure that we do not have a
+   * reference cycle. Otherwise, calling unsubscribe() from our finalize
+   * handler could result in a use-after-free. And we can't use a full
+   * reference because we'd never dispose/finalize without external
+   * intervention.
+   */
+  wr = g_slice_new0 (GWeakRef);
+  g_weak_ref_init (wr, self);
+  self->name_owner_changed_handler =
+    g_dbus_connection_signal_subscribe (bus,
+                                        NULL,
+                                        "org.freedesktop.DBus",
+                                        "NameOwnerChanged",
+                                        NULL,
+                                        NULL,
+                                        0,
+                                        dspy_names_model_name_owner_changed_cb,
+                                        g_steal_pointer (&wr),
+                                        (GDestroyNotify)_g_weak_ref_free);
+
+  g_dbus_connection_call (bus,
+                          "org.freedesktop.DBus",
+                          "/org/freedesktop/DBus",
+                          "org.freedesktop.DBus",
+                          "ListActivatableNames",
+                          g_variant_new ("()"),
+                          G_VARIANT_TYPE ("(as)"),
+                          G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION,
+                          G_MAXINT,
+                          g_task_get_cancellable (task),
+                          dspy_names_model_init_list_activatable_names_cb,
+                          g_object_ref (task));
+
+  g_dbus_connection_call (bus,
+                          "org.freedesktop.DBus",
+                          "/org/freedesktop/DBus",
+                          "org.freedesktop.DBus",
+                          "ListNames",
+                          g_variant_new ("()"),
+                          G_VARIANT_TYPE ("(as)"),
+                          G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION,
+                          G_MAXINT,
+                          g_task_get_cancellable (task),
+                          dspy_names_model_init_list_names_cb,
+                          g_object_ref (task));
+}
+
+static void
+dspy_names_model_init_async (GAsyncInitable      *initable,
+                             gint                 io_priority,
+                             GCancellable        *cancellable,
+                             GAsyncReadyCallback  callback,
+                             gpointer             user_data)
+{
+  DspyNamesModel *self = (DspyNamesModel *)initable;
+  g_autoptr(GTask) task = NULL;
+  gint n_active = 2;
+
+  g_assert (DSPY_IS_NAMES_MODEL (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_priority (task, io_priority);
+  g_task_set_source_tag (task, dspy_names_model_init_async);
+  g_task_set_task_data (task, g_memdup (&n_active, sizeof n_active), g_free);
+
+  if (self->connection == NULL)
+    g_task_return_new_error (task,
+                             G_IO_ERROR,
+                             G_IO_ERROR_NOT_INITIALIZED,
+                             "No connection to introspect");
+  else
+    dspy_connection_open_async (self->connection,
+                                cancellable,
+                                dspy_names_model_init_open_cb,
+                                g_steal_pointer (&task));
+}
+
+static gboolean
+dspy_names_model_init_finish (GAsyncInitable  *initable,
+                              GAsyncResult    *result,
+                              GError         **error)
+{
+  g_assert (DSPY_IS_NAMES_MODEL (initable));
+  g_assert (G_IS_TASK (result));
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+async_initable_iface_init (GAsyncInitableIface *iface)
+{
+  iface->init_async = dspy_names_model_init_async;
+  iface->init_finish = dspy_names_model_init_finish;
+}
+
+G_DEFINE_TYPE_WITH_CODE (DspyNamesModel, dspy_names_model, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init)
+                         G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE, async_initable_iface_init))
+
+/**
+ * dspy_names_model_new:
+ * @connection: a #DspyConnection
+ *
+ * Create a new #DspyNamesModel.
+ *
+ * Returns: (transfer full): a newly created #DspyNamesModel
+ */
+DspyNamesModel *
+dspy_names_model_new (DspyConnection *connection)
+{
+  return g_object_new (DSPY_TYPE_NAMES_MODEL,
+                       "connection", connection,
+                       NULL);
+}
+
+static void
+dspy_names_model_dispose (GObject *object)
+{
+  DspyNamesModel *self = (DspyNamesModel *)object;
+
+  g_assert (DSPY_IS_NAMES_MODEL (self));
+  g_assert (self->name_owner_changed_handler == 0 || self->bus != NULL);
+
+  if (self->name_owner_changed_handler > 0)
+    {
+      guint handler_id = self->name_owner_changed_handler;
+      self->name_owner_changed_handler = 0;
+      g_dbus_connection_signal_unsubscribe (self->bus, handler_id);
+    }
+
+  g_clear_object (&self->bus);
+
+  G_OBJECT_CLASS (dspy_names_model_parent_class)->dispose (object);
+}
+
+static void
+dspy_names_model_finalize (GObject *object)
+{
+  DspyNamesModel *self = (DspyNamesModel *)object;
+
+  g_clear_pointer (&self->items, g_sequence_free);
+  g_clear_object (&self->connection);
+
+  G_OBJECT_CLASS (dspy_names_model_parent_class)->finalize (object);
+}
+
+static void
+dspy_names_model_get_property (GObject    *object,
+                               guint       prop_id,
+                               GValue     *value,
+                               GParamSpec *pspec)
+{
+  DspyNamesModel *self = DSPY_NAMES_MODEL (object);
+
+  switch (prop_id)
+    {
+    case PROP_CONNECTION:
+      g_value_set_object (value, self->connection);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dspy_names_model_set_property (GObject      *object,
+                               guint         prop_id,
+                               const GValue *value,
+                               GParamSpec   *pspec)
+{
+  DspyNamesModel *self = DSPY_NAMES_MODEL (object);
+
+  switch (prop_id)
+    {
+    case PROP_CONNECTION:
+      self->connection = g_value_dup_object (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dspy_names_model_class_init (DspyNamesModelClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = dspy_names_model_dispose;
+  object_class->finalize = dspy_names_model_finalize;
+  object_class->get_property = dspy_names_model_get_property;
+  object_class->set_property = dspy_names_model_set_property;
+
+  properties [PROP_CONNECTION] =
+    g_param_spec_object ("connection",
+                         "Connection",
+                         "The connection to introspect",
+                         DSPY_TYPE_CONNECTION,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+dspy_names_model_init (DspyNamesModel *self)
+{
+  self->items = g_sequence_new (g_object_unref);
+}
diff --git a/src/plugins/dspy/libdspy/dspy-names-model.h b/src/plugins/dspy/libdspy/dspy-names-model.h
new file mode 100644
index 000000000..70af82207
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-names-model.h
@@ -0,0 +1,39 @@
+/* dspy-names-model.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+
+#include "dspy-connection.h"
+#include "dspy-name.h"
+
+G_BEGIN_DECLS
+
+#define DSPY_TYPE_NAMES_MODEL (dspy_names_model_get_type())
+
+G_DECLARE_FINAL_TYPE (DspyNamesModel, dspy_names_model, DSPY, NAMES_MODEL, GObject)
+
+DspyNamesModel *dspy_names_model_new            (DspyConnection *connection);
+DspyConnection *dspy_names_model_get_connection (DspyNamesModel *self);
+DspyName       *dspy_names_model_get_by_name    (DspyNamesModel *self,
+                                                 const gchar    *name);
+
+G_END_DECLS
diff --git a/src/plugins/dspy/libdspy/dspy-node.c b/src/plugins/dspy/libdspy/dspy-node.c
new file mode 100644
index 000000000..e8389b8e0
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-node.c
@@ -0,0 +1,598 @@
+/* dspy-node.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "dspy-node"
+
+#include <errno.h>
+#include <glib/gi18n.h>
+
+#include "dspy-private.h"
+
+#define LPAREN  "<span fgalpha='30000'>(</span>"
+#define RPAREN  "<span fgalpha='30000'>)</span>"
+#define ARROW   "<span fgalpha='20000'>↦</span>"
+#define BOLD(s) "<span weight='bold'>" s "</span>"
+#define DIM(s)  "<span fgalpha='40000'>" s "</span>"
+
+/*
+ * This file contains an alternate GDBusNodeInfo hierarchy that we can use
+ * for a couple benefits over GDBusNodeInfo.
+ *
+ * First, it provides parent pointers so that we can navigate the structure
+ * like a tree. This is very useful when used as a GtkTreeModel.
+ *
+ * Second, we can use a GStringChunk and reduce a lot of duplicate strings.
+ */
+
+static gpointer
+dspy_node_new (DspyNodeKind  kind,
+               DspyNode     *parent)
+{
+  DspyNode *node;
+
+  g_assert (kind > 0);
+  g_assert (kind < DSPY_NODE_KIND_LAST);
+
+  node = g_slice_new0 (DspyNode);
+  node->any.kind = kind;
+  node->any.parent = parent;
+  node->any.link.data = node;
+
+  g_assert (DSPY_IS_NODE (node));
+
+  return g_steal_pointer (&node);
+}
+
+static void
+push_tail (GQueue   *queue,
+           gpointer  node)
+{
+  DspyNodeAny *any = node;
+
+  g_assert (DSPY_IS_NODE (any));
+
+  g_queue_push_tail_link (queue, &any->link);
+}
+
+static void
+clear_full (GQueue *queue)
+{
+  g_queue_foreach (queue, (GFunc) _dspy_node_free, NULL);
+  queue->length = 0;
+  queue->head = NULL;
+  queue->tail = NULL;
+}
+
+static DspyArgInfo *
+_dspy_arg_info_new (DspyNode     *parent,
+                    GDBusArgInfo *info,
+                    GStringChunk *chunks)
+{
+  DspyArgInfo *ret;
+
+  g_assert (!parent || DSPY_IS_NODE (parent));
+  g_assert (info != NULL);
+  g_assert (chunks != NULL);
+
+  ret = dspy_node_new (DSPY_NODE_KIND_ARG, parent);
+  ret->name = g_string_chunk_insert_const (chunks, info->name);
+  ret->signature = g_string_chunk_insert_const (chunks, info->signature);
+
+  return ret;
+}
+
+static DspyMethodInfo *
+_dspy_method_info_new (DspyNode        *parent,
+                       GDBusMethodInfo *info,
+                       GStringChunk    *chunks)
+{
+  DspyMethodInfo *ret;
+
+  g_assert (!parent || DSPY_IS_NODE (parent));
+  g_assert (info != NULL);
+  g_assert (chunks != NULL);
+
+  ret = dspy_node_new (DSPY_NODE_KIND_METHOD, parent);
+  ret->name = g_string_chunk_insert_const (chunks, info->name);
+
+  for (guint i = 0; info->in_args[i] != NULL; i++)
+    push_tail (&ret->in_args,
+               _dspy_arg_info_new ((DspyNode *)ret, info->in_args[i], chunks));
+
+  for (guint i = 0; info->out_args[i] != NULL; i++)
+    push_tail (&ret->out_args,
+               _dspy_arg_info_new ((DspyNode *)ret, info->out_args[i], chunks));
+
+  return ret;
+}
+
+static DspySignalInfo *
+_dspy_signal_info_new (DspyNode        *parent,
+                       GDBusSignalInfo *info,
+                       GStringChunk    *chunks)
+{
+  DspySignalInfo *ret;
+
+  g_assert (!parent || DSPY_IS_NODE (parent));
+  g_assert (info != NULL);
+  g_assert (chunks != NULL);
+
+  ret = dspy_node_new (DSPY_NODE_KIND_SIGNAL, parent);
+  ret->name = g_string_chunk_insert_const (chunks, info->name);
+
+  for (guint i = 0; info->args[i] != NULL; i++)
+    push_tail (&ret->args,
+               _dspy_arg_info_new ((DspyNode *)ret, info->args[i], chunks));
+
+  return ret;
+}
+
+static DspyPropertyInfo *
+_dspy_property_info_new (DspyNode          *parent,
+                         GDBusPropertyInfo *info,
+                         GStringChunk      *chunks)
+{
+  DspyPropertyInfo *ret;
+
+  g_assert (!parent || DSPY_IS_NODE (parent));
+  g_assert (info != NULL);
+  g_assert (chunks != NULL);
+
+  ret = dspy_node_new (DSPY_NODE_KIND_PROPERTY, parent);
+  ret->name = g_string_chunk_insert_const (chunks, info->name);
+  ret->signature = g_string_chunk_insert_const (chunks, info->signature);
+  ret->flags = info->flags;
+
+  return ret;
+}
+
+static DspyInterfaceInfo *
+_dspy_interface_info_new (DspyNode           *parent,
+                          GDBusInterfaceInfo *info,
+                          GStringChunk       *chunks)
+{
+  DspyInterfaceInfo *ret;
+
+  g_assert (!parent || DSPY_IS_NODE (parent));
+  g_assert (info != NULL);
+  g_assert (chunks != NULL);
+
+  ret = dspy_node_new (DSPY_NODE_KIND_INTERFACE, parent);
+  ret->name = g_string_chunk_insert_const (chunks, info->name);
+  ret->properties = dspy_node_new (DSPY_NODE_KIND_PROPERTIES, (DspyNode *)ret);
+  ret->signals = dspy_node_new (DSPY_NODE_KIND_SIGNALS, (DspyNode *)ret);
+  ret->methods = dspy_node_new (DSPY_NODE_KIND_METHODS, (DspyNode *)ret);
+
+  for (guint i = 0; info->signals[i] != NULL; i++)
+    push_tail (&ret->signals->signals,
+               _dspy_signal_info_new ((DspyNode *)ret->signals, info->signals[i], chunks));
+
+  for (guint i = 0; info->methods[i] != NULL; i++)
+    push_tail (&ret->methods->methods,
+               _dspy_method_info_new ((DspyNode *)ret->methods, info->methods[i], chunks));
+
+  for (guint i = 0; info->properties[i] != NULL; i++)
+    push_tail (&ret->properties->properties,
+               _dspy_property_info_new ((DspyNode *)ret->properties,
+                                        info->properties[i],
+                                        chunks));
+
+  return ret;
+}
+
+static DspyNodeInfo *
+_dspy_node_info_new (DspyNode      *parent,
+                     GDBusNodeInfo *info,
+                     GStringChunk  *chunks)
+{
+  DspyNodeInfo *ret;
+
+  g_assert (!parent || DSPY_IS_NODE (parent));
+  g_assert (info != NULL);
+  g_assert (chunks != NULL);
+
+  ret = dspy_node_new (DSPY_NODE_KIND_NODE, parent);
+  ret->interfaces = dspy_node_new (DSPY_NODE_KIND_INTERFACES, (DspyNode *)ret);
+  ret->path = info->path ? g_string_chunk_insert_const (chunks, info->path) : NULL;
+
+  for (guint i = 0; info->nodes[i] != NULL; i++)
+    push_tail (&ret->nodes,
+               _dspy_node_info_new ((DspyNode *)ret, info->nodes[i], chunks));
+
+  if (info->interfaces[0])
+    {
+      for (guint i = 0; info->interfaces[i] != NULL; i++)
+        push_tail (&ret->interfaces->interfaces,
+                   _dspy_interface_info_new ((DspyNode *)ret->interfaces,
+                                             info->interfaces[i],
+                                             chunks));
+    }
+
+  return ret;
+}
+
+DspyNodeInfo *
+_dspy_node_parse (const gchar   *xml,
+                  GStringChunk  *chunks,
+                  GError       **error)
+{
+  g_autoptr(GDBusNodeInfo) info = NULL;
+
+  g_assert (xml != NULL);
+  g_assert (chunks != NULL);
+
+  if ((info = g_dbus_node_info_new_for_xml (xml, error)))
+    return _dspy_node_info_new (NULL, info, chunks);
+
+  return NULL;
+}
+
+void
+_dspy_node_free (gpointer data)
+{
+  DspyNode *node = data;
+
+  g_assert (!node || DSPY_IS_NODE (node));
+
+  if (node == NULL)
+    return;
+
+  node->any.parent = NULL;
+
+  switch (node->any.kind)
+    {
+    case DSPY_NODE_KIND_ARG:
+      break;
+
+    case DSPY_NODE_KIND_NODE:
+      _dspy_node_free ((DspyNode *)node->node.interfaces);
+      clear_full (&node->node.nodes);
+      break;
+
+    case DSPY_NODE_KIND_INTERFACE:
+      _dspy_node_free ((DspyNode *)node->interface.properties);
+      _dspy_node_free ((DspyNode *)node->interface.signals);
+      _dspy_node_free ((DspyNode *)node->interface.methods);
+      break;
+
+    case DSPY_NODE_KIND_INTERFACES:
+      clear_full (&node->interfaces.interfaces);
+      break;
+
+    case DSPY_NODE_KIND_METHODS:
+      clear_full (&node->methods.methods);
+      break;
+
+    case DSPY_NODE_KIND_METHOD:
+      clear_full (&node->method.in_args);
+      clear_full (&node->method.out_args);
+      break;
+
+    case DSPY_NODE_KIND_PROPERTIES:
+      clear_full (&node->properties.properties);
+      break;
+
+    case DSPY_NODE_KIND_PROPERTY:
+      g_clear_pointer (&node->property.value, g_free);
+      break;
+
+    case DSPY_NODE_KIND_SIGNALS:
+      clear_full (&node->signals.signals);
+      break;
+
+    case DSPY_NODE_KIND_SIGNAL:
+      clear_full (&node->signal.args);
+      break;
+
+    case DSPY_NODE_KIND_LAST:
+    default:
+      g_assert_not_reached ();
+    }
+
+  node->any.kind = 0;
+  node->any.parent = NULL;
+  node->any.link.prev = NULL;
+  node->any.link.next = NULL;
+  node->any.link.data = NULL;
+
+  g_slice_free (DspyNode, node);
+}
+
+gint
+_dspy_node_info_compare (const DspyNodeInfo  *a,
+                         const DspyNodeInfo  *b)
+{
+  return g_strcmp0 (a->path, b->path);
+}
+
+gint
+_dspy_interface_info_compare (const DspyInterfaceInfo *a,
+                              const DspyInterfaceInfo *b)
+{
+  return g_strcmp0 (a->name, b->name);
+}
+
+DspyNodeInfo *
+_dspy_node_new_root (void)
+{
+  return dspy_node_new (DSPY_NODE_KIND_NODE, NULL);
+}
+
+void
+_dspy_node_walk (DspyNode *node,
+                 GFunc     func,
+                 gpointer  user_data)
+{
+  g_assert (DSPY_IS_NODE (node));
+  g_assert (func != NULL);
+
+  func (node, user_data);
+
+  switch (node->any.kind)
+    {
+    case DSPY_NODE_KIND_ARG:
+      break;
+
+    case DSPY_NODE_KIND_NODE:
+      if (node->node.interfaces != NULL)
+        _dspy_node_walk ((DspyNode *)node->node.interfaces, func, user_data);
+      for (const GList *iter = node->node.nodes.head; iter; iter = iter->next)
+        _dspy_node_walk (iter->data, func, user_data);
+      break;
+
+    case DSPY_NODE_KIND_INTERFACE:
+      _dspy_node_walk ((DspyNode *)node->interface.properties, func, user_data);
+      _dspy_node_walk ((DspyNode *)node->interface.signals, func, user_data);
+      _dspy_node_walk ((DspyNode *)node->interface.methods, func, user_data);
+      break;
+
+    case DSPY_NODE_KIND_INTERFACES:
+      for (const GList *iter = node->interfaces.interfaces.head; iter; iter = iter->next)
+        _dspy_node_walk (iter->data, func, user_data);
+      break;
+
+    case DSPY_NODE_KIND_METHODS:
+      for (const GList *iter = node->methods.methods.head; iter; iter = iter->next)
+        _dspy_node_walk (iter->data, func, user_data);
+      break;
+
+    case DSPY_NODE_KIND_METHOD:
+    case DSPY_NODE_KIND_PROPERTY:
+    case DSPY_NODE_KIND_SIGNAL:
+      break;
+
+    case DSPY_NODE_KIND_PROPERTIES:
+      for (const GList *iter = node->properties.properties.head; iter; iter = iter->next)
+        _dspy_node_walk (iter->data, func, user_data);
+      break;
+
+    case DSPY_NODE_KIND_SIGNALS:
+      for (const GList *iter = node->signals.signals.head; iter; iter = iter->next)
+        _dspy_node_walk (iter->data, func, user_data);
+      break;
+
+    case DSPY_NODE_KIND_LAST:
+    default:
+      g_assert_not_reached ();
+    }
+}
+
+static gchar *
+_dspy_property_info_to_string (DspyPropertyInfo *info)
+{
+  g_autofree gchar *sig = NULL;
+  const gchar *rw;
+
+  g_assert (DSPY_IS_NODE (info));
+  g_assert (info->kind == DSPY_NODE_KIND_PROPERTY);
+
+  sig = _dspy_signature_humanize (info->signature);
+
+  if (info->flags == (G_DBUS_PROPERTY_INFO_FLAGS_WRITABLE | G_DBUS_PROPERTY_INFO_FLAGS_WRITABLE))
+    rw = _("read/write");
+  else if (info->flags  & G_DBUS_PROPERTY_INFO_FLAGS_WRITABLE)
+    rw = _("write-only");
+  else if (info->flags  & G_DBUS_PROPERTY_INFO_FLAGS_READABLE)
+    rw = _("read-only");
+  else
+    rw = "";
+
+  return g_strdup_printf ("%s "ARROW" "BOLD(DIM("%s"))" "LPAREN DIM("%s") RPAREN,
+                          info->name, sig, rw);
+}
+
+static gboolean
+arg_name_is_generated (const gchar *str)
+{
+  if (str == NULL)
+    return TRUE;
+
+  if (g_str_has_prefix (str, "arg_"))
+    {
+      gchar *endptr = NULL;
+      gint64 val;
+
+      str += strlen ("arg_");
+      errno = 0;
+      val = g_ascii_strtoll (str, &endptr, 10);
+
+      if (val >= 0 && errno == 0 && *endptr == 0)
+        return TRUE;
+    }
+
+  return FALSE;
+}
+
+static gchar *
+_dspy_method_info_to_string (DspyMethodInfo *info)
+{
+  GString *str;
+
+  g_assert (DSPY_IS_NODE (info));
+  g_assert (info->kind == DSPY_NODE_KIND_METHOD);
+
+  str = g_string_new (info->name);
+  g_string_append (str, " "LPAREN);
+
+  for (const GList *iter = info->in_args.head; iter; iter = iter->next)
+    {
+      DspyArgInfo *arg = iter->data;
+      g_autofree gchar *sig = _dspy_signature_humanize (arg->signature);
+
+      if (iter->prev != NULL)
+        g_string_append (str, ", ");
+      g_string_append_printf (str, BOLD(DIM("%s")), sig);
+      if (!arg_name_is_generated (arg->name))
+        g_string_append_printf (str, DIM(" %s"), arg->name);
+    }
+
+  g_string_append (str, RPAREN" "ARROW" "LPAREN);
+
+  for (const GList *iter = info->out_args.head; iter; iter = iter->next)
+    {
+      DspyArgInfo *arg = iter->data;
+      g_autofree gchar *sig = _dspy_signature_humanize (arg->signature);
+
+      if (iter->prev != NULL)
+        g_string_append (str, ", ");
+      g_string_append_printf (str, BOLD(DIM("%s")), sig);
+      if (!arg_name_is_generated (arg->name))
+        g_string_append_printf (str, DIM(" %s"), arg->name);
+    }
+
+  g_string_append (str, RPAREN);
+
+  return g_string_free (str, FALSE);
+}
+
+static gchar *
+_dspy_signal_info_to_string (DspySignalInfo *info)
+{
+  GString *str;
+
+  g_assert (DSPY_IS_NODE (info));
+  g_assert (info->kind == DSPY_NODE_KIND_SIGNAL);
+
+  str = g_string_new (info->name);
+  g_string_append (str, " "LPAREN);
+
+  for (const GList *iter = info->args.head; iter; iter = iter->next)
+    {
+      DspyArgInfo *arg = iter->data;
+      g_autofree gchar *sig = _dspy_signature_humanize (arg->signature);
+
+      if (iter->prev != NULL)
+        g_string_append (str, ", ");
+      g_string_append_printf (str, BOLD(DIM("%s")), sig);
+      if (!arg_name_is_generated (arg->name))
+        g_string_append_printf (str, DIM(" %s"), arg->name);
+    }
+
+  g_string_append (str, RPAREN);
+
+  return g_string_free (str, FALSE);
+}
+
+gchar *
+_dspy_node_get_text (DspyNode *node)
+{
+  switch (node->any.kind)
+    {
+    case DSPY_NODE_KIND_ARG:
+      return g_strdup (node->arg.name);
+
+    case DSPY_NODE_KIND_NODE:
+      return g_strdup (node->node.path);
+
+    case DSPY_NODE_KIND_INTERFACE:
+      return g_strdup (node->interface.name);
+
+    case DSPY_NODE_KIND_INTERFACES:
+      return g_strdup (_("Interfaces"));
+
+    case DSPY_NODE_KIND_METHODS:
+      return g_strdup (_("Methods"));
+
+    case DSPY_NODE_KIND_METHOD:
+      return _dspy_method_info_to_string (&node->method);
+
+    case DSPY_NODE_KIND_PROPERTIES:
+      return g_strdup (_("Properties"));
+
+    case DSPY_NODE_KIND_PROPERTY:
+        {
+          g_autofree gchar *str = _dspy_property_info_to_string (&node->property);
+
+          if (node->property.value != NULL)
+            {
+              g_autofree gchar *escaped = g_markup_escape_text (node->property.value, -1);
+              return g_strdup_printf ("%s = %s", str, escaped);
+            }
+
+          return g_steal_pointer (&str);
+        }
+
+    case DSPY_NODE_KIND_SIGNALS:
+      return g_strdup (_("Signals"));
+
+    case DSPY_NODE_KIND_SIGNAL:
+      return _dspy_signal_info_to_string (&node->signal);
+
+    case DSPY_NODE_KIND_LAST:
+    default:
+      g_return_val_if_reached (NULL);
+    }
+}
+
+gboolean
+_dspy_node_is_group (DspyNode *node)
+{
+  g_assert (node != NULL);
+  g_assert (DSPY_IS_NODE (node));
+
+  return node->any.kind == DSPY_NODE_KIND_INTERFACES ||
+         node->any.kind == DSPY_NODE_KIND_PROPERTIES ||
+         node->any.kind == DSPY_NODE_KIND_SIGNALS ||
+         node->any.kind == DSPY_NODE_KIND_METHODS;
+}
+
+const gchar *
+_dspy_node_get_object_path (DspyNode *node)
+{
+  if (node == NULL)
+    return NULL;
+
+  if (node->any.kind == DSPY_NODE_KIND_NODE)
+    return node->node.path;
+
+  return _dspy_node_get_object_path (node->any.parent);
+}
+
+const gchar *
+_dspy_node_get_interface (DspyNode *node)
+{
+  if (node == NULL)
+    return NULL;
+
+  if (node->any.kind == DSPY_NODE_KIND_INTERFACE)
+    return node->interface.name;
+
+  return _dspy_node_get_interface (node->any.parent);
+}
diff --git a/src/plugins/dspy/libdspy/dspy-private.h b/src/plugins/dspy/libdspy/dspy-private.h
new file mode 100644
index 000000000..0094a0ad6
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-private.h
@@ -0,0 +1,202 @@
+/* dspy-private.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#pragma once
+
+#include "dspy-name.h"
+#include "dspy-introspection-model.h"
+
+G_BEGIN_DECLS
+
+typedef enum
+{
+  DSPY_NODE_KIND_NODE = 1,
+  DSPY_NODE_KIND_INTERFACES,
+  DSPY_NODE_KIND_INTERFACE,
+  DSPY_NODE_KIND_METHOD,
+  DSPY_NODE_KIND_METHODS,
+  DSPY_NODE_KIND_SIGNAL,
+  DSPY_NODE_KIND_SIGNALS,
+  DSPY_NODE_KIND_PROPERTY,
+  DSPY_NODE_KIND_PROPERTIES,
+  DSPY_NODE_KIND_ARG,
+  DSPY_NODE_KIND_LAST
+} DspyNodeKind;
+
+typedef union  _DspyNode          DspyNode;
+typedef struct _DspyArgInfo       DspyArgInfo;
+typedef struct _DspyInterfaces    DspyInterfaces;
+typedef struct _DspyInterfaceInfo DspyInterfaceInfo;
+typedef struct _DspyMethodInfo    DspyMethodInfo;
+typedef struct _DspyMethods       DspyMethods;
+typedef struct _DspyNodeAny       DspyNodeAny;
+typedef struct _DspyNodeInfo      DspyNodeInfo;
+typedef struct _DspyProperties    DspyProperties;
+typedef struct _DspyPropertyInfo  DspyPropertyInfo;
+typedef struct _DspySignalInfo    DspySignalInfo;
+typedef struct _DspySignals       DspySignals;
+
+struct _DspyNodeAny
+{
+  DspyNodeKind  kind;
+  DspyNode     *parent;
+  GList         link;
+};
+
+struct _DspyNodeInfo
+{
+  DspyNodeKind     kind;
+  DspyNode        *parent;
+  GList            link;
+  const gchar     *path;
+  GQueue           nodes;
+  DspyInterfaces  *interfaces;
+};
+
+struct _DspyInterfaceInfo
+{
+  DspyNodeKind    kind;
+  DspyNode       *parent;
+  GList           link;
+  const gchar    *name;
+  DspyProperties *properties;
+  DspySignals    *signals;
+  DspyMethods    *methods;
+};
+
+struct _DspyMethodInfo
+{
+  DspyNodeKind   kind;
+  DspyNode      *parent;
+  GList          link;
+  const gchar   *name;
+  GQueue         in_args;
+  GQueue         out_args;
+};
+
+struct _DspySignalInfo
+{
+  DspyNodeKind   kind;
+  DspyNode      *parent;
+  GList          link;
+  const gchar   *name;
+  const gchar   *signature;
+  GQueue         args;
+};
+
+struct _DspyPropertyInfo
+{
+  DspyNodeKind            kind;
+  DspyNode               *parent;
+  GList                   link;
+  const gchar            *name;
+  const gchar            *signature;
+  GDBusPropertyInfoFlags  flags;
+  gchar                  *value;
+};
+
+struct _DspyArgInfo
+{
+  DspyNodeKind  kind;
+  DspyNode     *parent;
+  GList         link;
+  const gchar  *name;
+  const gchar  *signature;
+};
+
+struct _DspyMethods
+{
+  DspyNodeKind  kind;
+  DspyNode     *parent;
+  GList         link;
+  GQueue        methods;
+};
+
+struct _DspySignals
+{
+  DspyNodeKind  kind;
+  DspyNode     *parent;
+  GList         link;
+  GQueue        signals;
+};
+
+struct _DspyProperties
+{
+  DspyNodeKind  kind;
+  DspyNode     *parent;
+  GList         link;
+  GQueue        properties;
+};
+
+struct _DspyInterfaces
+{
+  DspyNodeKind  kind;
+  DspyNode     *parent;
+  GList         link;
+  GQueue        interfaces;
+};
+
+union _DspyNode
+{
+  DspyNodeAny       any;
+  DspyNodeInfo      node;
+  DspyInterfaceInfo interface;
+  DspyInterfaces    interfaces;
+  DspyMethodInfo    method;
+  DspyMethods       methods;
+  DspySignalInfo    signal;
+  DspySignals       signals;
+  DspyPropertyInfo  property;
+  DspyProperties    properties;
+  DspyArgInfo       arg;
+};
+
+#define DSPY_IS_NODE(n) \
+  (((DspyNode*)n)->any.kind > 0 && ((DspyNode*)n)->any.kind < DSPY_NODE_KIND_LAST)
+
+DspyNodeInfo           *_dspy_node_parse              (const gchar              *xml,
+                                                       GStringChunk             *chunks,
+                                                       GError                  **error);
+void                    _dspy_node_free               (gpointer                  data);
+void                    _dspy_node_walk               (DspyNode                 *node,
+                                                       GFunc                     func,
+                                                       gpointer                  user_data);
+gchar                  *_dspy_node_get_text           (DspyNode                 *node);
+DspyNodeInfo           *_dspy_node_new_root           (void);
+gboolean                _dspy_node_is_group           (DspyNode                 *node);
+gint                    _dspy_node_info_compare       (const DspyNodeInfo       *a,
+                                                       const DspyNodeInfo       *b);
+const gchar            *_dspy_node_get_object_path    (DspyNode                 *node);
+const gchar            *_dspy_node_get_interface      (DspyNode                 *node);
+gint                    _dspy_interface_info_compare  (const DspyInterfaceInfo  *a,
+                                                       const DspyInterfaceInfo  *b);
+void                    _dspy_name_clear_pid          (DspyName                 *name);
+void                    _dspy_name_refresh_pid        (DspyName                 *name,
+                                                       GDBusConnection          *connection);
+void                    _dspy_name_refresh_owner      (DspyName                 *name,
+                                                       GDBusConnection          *connection);
+void                    _dspy_name_set_owner          (DspyName                 *self,
+                                                       const gchar              *owner);
+void                    _dspy_name_set_activatable    (DspyName                 *name,
+                                                       gboolean                  is_activatable);
+DspyIntrospectionModel *_dspy_introspection_model_new (DspyName                 *name);
+gchar                  *_dspy_signature_humanize      (const gchar              *signature);
+
+G_END_DECLS
diff --git a/src/plugins/dspy/libdspy/dspy-signature.c b/src/plugins/dspy/libdspy/dspy-signature.c
new file mode 100644
index 000000000..174a1b998
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-signature.c
@@ -0,0 +1,82 @@
+/* dspy-signature.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "dspy-signature"
+
+#include <glib/gi18n.h>
+
+#include "dspy-private.h"
+
+static GHashTable *
+get_common_signatures (void)
+{
+  static GHashTable *common;
+
+  if (g_once_init_enter (&common))
+    {
+      GHashTable *ht = g_hash_table_new (g_str_hash, g_str_equal);
+
+#define INSERT_COMMON(type,word) g_hash_table_insert(ht, (gchar *)type, (gchar *)word)
+      INSERT_COMMON ("n",     "int16");
+      INSERT_COMMON ("q",     "uint16");
+      INSERT_COMMON ("i",     "int32");
+      INSERT_COMMON ("u",     "uint32");
+      INSERT_COMMON ("x",     "int64");
+      INSERT_COMMON ("t",     "uint64");
+      INSERT_COMMON ("s",     "string");
+      INSERT_COMMON ("b",     "boolean");
+      INSERT_COMMON ("y",     "byte");
+      INSERT_COMMON ("o",     "Object Path");
+      INSERT_COMMON ("g",     "Signature");
+      INSERT_COMMON ("d",     "double");
+      INSERT_COMMON ("v",     "Variant");
+      INSERT_COMMON ("h",     "File Descriptor");
+      INSERT_COMMON ("as",    "string[]");
+      INSERT_COMMON ("a{sv}", "Vardict");
+      INSERT_COMMON ("ay",    "Byte Array");
+#undef INSERT_COMMON
+
+      g_once_init_leave (&common, ht);
+    }
+
+  return common;
+}
+
+gchar *
+_dspy_signature_humanize (const gchar *signature)
+{
+  GHashTable *common;
+  const gchar *found;
+
+  if (signature == NULL)
+    return NULL;
+
+  common = get_common_signatures ();
+
+  if ((found = g_hash_table_lookup (common, signature)))
+    return g_strdup (found);
+
+  /* If this is a simple array of something else ... */
+  if ((found = g_hash_table_lookup (common, signature + 1)))
+    /* translators: %s is replaced with the simple DBus type string */
+    return g_strdup_printf (_("Array of [%s]"), found);
+
+  return g_strdup (signature);
+}
diff --git a/src/plugins/dspy/libdspy/dspy-tree-view.c b/src/plugins/dspy/libdspy/dspy-tree-view.c
new file mode 100644
index 000000000..5db96b566
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-tree-view.c
@@ -0,0 +1,311 @@
+/* dspy-tree-view.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "dspy-tree-view"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "dspy-private.h"
+#include "dspy-tree-view.h"
+
+G_DEFINE_TYPE (DspyTreeView, dspy_tree_view, GTK_TYPE_TREE_VIEW)
+
+typedef struct
+{
+  DspyTreeView *self;
+  GtkTreePath  *path;
+} GetProperty;
+
+enum {
+  METHOD_ACTIVATED,
+  N_SIGNALS
+};
+
+static guint signals [N_SIGNALS];
+
+static void
+get_property_free (GetProperty *state)
+{
+  g_clear_object (&state->self);
+  g_clear_pointer (&state->path, gtk_tree_path_free);
+  g_slice_free (GetProperty, state);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (GetProperty, get_property_free)
+
+GtkWidget *
+dspy_tree_view_new (void)
+{
+  return g_object_new (DSPY_TYPE_TREE_VIEW, NULL);
+}
+
+static void
+dspy_tree_view_selection_changed (DspyTreeView     *self,
+                                  GtkTreeSelection *selection)
+{
+  GtkTreeModel *model;
+  GtkTreeIter iter;
+
+  g_assert (DSPY_IS_TREE_VIEW (self));
+  g_assert (GTK_IS_TREE_SELECTION (selection));
+
+  if (gtk_tree_selection_get_selected (selection, &model, &iter) &&
+      DSPY_IS_INTROSPECTION_MODEL (model))
+    {
+      DspyName *name = dspy_introspection_model_get_name (DSPY_INTROSPECTION_MODEL (model));
+      g_autoptr(DspyMethodInvocation) invocation = NULL;
+      DspyNode *node = iter.user_data;
+
+      g_assert (node != NULL);
+      g_assert (DSPY_IS_NODE (node));
+
+      if (node->any.kind == DSPY_NODE_KIND_METHOD)
+        {
+          invocation = dspy_method_invocation_new ();
+          dspy_method_invocation_set_interface (invocation, _dspy_node_get_interface (node));
+          dspy_method_invocation_set_method (invocation, node->method.name);
+
+          if (node->method.in_args.length == 0)
+            dspy_method_invocation_set_parameters (invocation, g_variant_new ("()"));
+        }
+      else if (node->any.kind == DSPY_NODE_KIND_PROPERTY)
+        {
+          const gchar *iface = _dspy_node_get_interface (node);
+
+          invocation = dspy_method_invocation_new ();
+          dspy_method_invocation_set_interface (invocation, "org.freedesktop.DBus.Properties");
+          dspy_method_invocation_set_method (invocation, "Get");
+          dspy_method_invocation_set_signature (invocation, "(ss)");
+          dspy_method_invocation_set_reply_signature (invocation, "v");
+          dspy_method_invocation_set_parameters (invocation,
+                                                 g_variant_new ("(ss)", iface, node->property.name));
+        }
+
+      if (invocation != NULL)
+        {
+          dspy_method_invocation_set_object_path (invocation, _dspy_node_get_object_path (node));
+          dspy_method_invocation_set_name (invocation, name);
+          g_signal_emit (self, signals [METHOD_ACTIVATED], 0, invocation);
+        }
+    }
+
+}
+
+static void
+dspy_tree_view_get_property_cb (GObject      *object,
+                                GAsyncResult *result,
+                                gpointer      user_data)
+{
+  GDBusConnection *bus = (GDBusConnection *)object;
+  g_autoptr(GVariant) reply = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GetProperty) state = user_data;
+
+  g_assert (G_IS_DBUS_CONNECTION (bus));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (state != NULL);
+
+  if ((reply = g_dbus_connection_call_finish (bus, result, &error)))
+    {
+      GtkTreeModel *model = gtk_tree_view_get_model (GTK_TREE_VIEW (state->self));
+      DspyNode *node;
+      GtkTreeIter iter;
+
+      if (gtk_tree_model_get_iter (model, &iter, state->path) &&
+          (node = iter.user_data) &&
+          DSPY_IS_NODE (node) &&
+          node->any.kind == DSPY_NODE_KIND_PROPERTY)
+        {
+          g_autoptr(GVariant) box = g_variant_get_child_value (reply, 0);
+          g_autoptr(GVariant) child = g_variant_get_child_value (box, 0);
+
+          g_clear_pointer (&node->property.value, g_free);
+
+          if (g_variant_is_of_type (child, G_VARIANT_TYPE_STRING) ||
+              g_variant_is_of_type (child, G_VARIANT_TYPE_OBJECT_PATH))
+            node->property.value = g_strdup (g_variant_get_string (child, NULL));
+          else if (g_variant_is_of_type (child, G_VARIANT_TYPE_BYTESTRING))
+            node->property.value = g_strdup (g_variant_get_bytestring (child));
+          else
+            node->property.value = g_variant_print (child, FALSE);
+
+          if (strlen (node->property.value) > 64)
+            {
+              g_autofree gchar *tmp = g_steal_pointer (&node->property.value);
+              tmp[64] = 0;
+              node->property.value = g_strdup_printf ("%s…", tmp);
+            }
+
+          gtk_tree_model_row_changed (model, state->path, &iter);
+        }
+    }
+  else
+    {
+      g_warning ("Failed to get property: %s", error->message);
+    }
+}
+
+static void
+dspy_tree_view_row_activated (GtkTreeView       *view,
+                              GtkTreePath       *path,
+                              GtkTreeViewColumn *column)
+{
+  GtkTreeModel *model;
+  GtkTreeIter iter;
+
+  g_assert (DSPY_IS_TREE_VIEW (view));
+  g_assert (path != NULL);
+  g_assert (!column || GTK_IS_TREE_VIEW_COLUMN (column));
+
+  model = gtk_tree_view_get_model (view);
+
+  if (DSPY_IS_INTROSPECTION_MODEL (model) &&
+      gtk_tree_model_get_iter (model, &iter, path))
+    {
+      DspyName *name = dspy_introspection_model_get_name (DSPY_INTROSPECTION_MODEL (model));
+      DspyConnection *connection = dspy_name_get_connection (name);
+      GDBusConnection *bus = dspy_connection_get_connection (connection);
+      DspyNode *node = iter.user_data;
+
+      g_assert (!node || DSPY_IS_NODE (node));
+
+      if (node != NULL &&
+          node->any.kind == DSPY_NODE_KIND_PROPERTY &&
+          node->property.flags & G_DBUS_PROPERTY_INFO_FLAGS_READABLE)
+        {
+          GetProperty *state;
+
+          state = g_slice_new0 (GetProperty);
+          state->path = gtk_tree_path_copy (path);
+          state->self = g_object_ref (DSPY_TREE_VIEW (view));
+
+          g_dbus_connection_call (bus,
+                                  dspy_name_get_owner (name),
+                                  _dspy_node_get_object_path (node),
+                                  "org.freedesktop.DBus.Properties",
+                                  "Get",
+                                  g_variant_new ("(ss)",
+                                                 _dspy_node_get_interface (node),
+                                                 node->property.name),
+                                  G_VARIANT_TYPE ("(v)"),
+                                  G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION,
+                                  -1,
+                                  NULL,
+                                  dspy_tree_view_get_property_cb,
+                                  state);
+          return;
+        }
+    }
+
+  if (gtk_tree_view_row_expanded (view, path))
+    gtk_tree_view_collapse_row (view, path);
+  else
+    gtk_tree_view_expand_row (view, path, FALSE);
+}
+
+static void
+dspy_tree_view_row_expanded (GtkTreeView *view,
+                             GtkTreeIter *iter,
+                             GtkTreePath *path)
+{
+  DspyNode *node = NULL;
+  GtkTreeModel *model;
+
+  g_assert (GTK_IS_TREE_VIEW (view));
+  g_assert (iter != NULL);
+  g_assert (path != NULL);
+
+  if (GTK_TREE_VIEW_CLASS (dspy_tree_view_parent_class)->row_expanded)
+    GTK_TREE_VIEW_CLASS (dspy_tree_view_parent_class)->row_expanded (view, iter, path);
+
+  if (!(model = gtk_tree_view_get_model (view)) ||
+      !DSPY_IS_INTROSPECTION_MODEL (model))
+    return;
+
+  node = iter->user_data;
+
+  g_assert (node != NULL);
+  g_assert (DSPY_IS_NODE (node));
+
+  /* Expand children too if there are fixed number of children */
+  if (node->any.kind == DSPY_NODE_KIND_NODE ||    /* path node */
+      node->any.kind == DSPY_NODE_KIND_INTERFACE) /* iface node */
+    {
+      GtkTreeIter child;
+
+      if (gtk_tree_model_iter_children (model, &child, iter))
+        {
+          g_autoptr(GtkTreePath) copy = gtk_tree_path_copy (path);
+
+          gtk_tree_path_down (copy);
+
+          do
+            {
+              gtk_tree_view_expand_row (view, copy, FALSE);
+              gtk_tree_path_next (copy);
+            }
+          while (gtk_tree_model_iter_next (model, &child));
+        }
+    }
+}
+
+static void
+dspy_tree_view_class_init (DspyTreeViewClass *klass)
+{
+  GtkTreeViewClass *tree_view_class = GTK_TREE_VIEW_CLASS (klass);
+
+  tree_view_class->row_activated = dspy_tree_view_row_activated;
+  tree_view_class->row_expanded = dspy_tree_view_row_expanded;
+
+  signals [METHOD_ACTIVATED] =
+    g_signal_new ("method-activated",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (DspyTreeViewClass, method_activated),
+                  NULL, NULL,
+                  g_cclosure_marshal_VOID__OBJECT,
+                  G_TYPE_NONE, 1, DSPY_TYPE_METHOD_INVOCATION);
+}
+
+static void
+dspy_tree_view_init (DspyTreeView *self)
+{
+  GtkTreeViewColumn *column;
+  GtkCellRenderer *cell;
+
+  gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (self), TRUE);
+
+  column = gtk_tree_view_column_new ();
+  gtk_tree_view_column_set_title (column, _("Object Path"));
+  gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_AUTOSIZE);
+  gtk_tree_view_append_column (GTK_TREE_VIEW (self), column);
+
+  cell = gtk_cell_renderer_text_new ();
+  gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (column), cell, TRUE);
+  gtk_cell_layout_add_attribute (GTK_CELL_LAYOUT (column), cell, "markup", 0);
+
+  g_signal_connect_object (gtk_tree_view_get_selection (GTK_TREE_VIEW (self)),
+                           "changed",
+                           G_CALLBACK (dspy_tree_view_selection_changed),
+                           self,
+                           G_CONNECT_SWAPPED);
+}
diff --git a/src/plugins/dspy/libdspy/dspy-tree-view.h b/src/plugins/dspy/libdspy/dspy-tree-view.h
new file mode 100644
index 000000000..db4fc9a1d
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-tree-view.h
@@ -0,0 +1,46 @@
+/* dspy-tree-view.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+#include "dspy-method-invocation.h"
+
+G_BEGIN_DECLS
+
+#define DSPY_TYPE_TREE_VIEW (dspy_tree_view_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (DspyTreeView, dspy_tree_view, DSPY, TREE_VIEW, GtkTreeView)
+
+struct _DspyTreeViewClass
+{
+  GtkTreeViewClass parent_class;
+
+  void (*method_activated) (DspyTreeView         *self,
+                            DspyMethodInvocation *invocation);
+
+  /*< private >*/
+  gpointer _reserved[8];
+};
+
+GtkWidget *dspy_tree_view_new (void);
+
+G_END_DECLS
diff --git a/src/plugins/dspy/libdspy/dspy-view.c b/src/plugins/dspy/libdspy/dspy-view.c
new file mode 100644
index 000000000..c420d7ec2
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-view.c
@@ -0,0 +1,610 @@
+/* dspy-view.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "dspy-view"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+
+#include "dspy-connection-button.h"
+#include "dspy-name-marquee.h"
+#include "dspy-method-view.h"
+#include "dspy-name-row.h"
+#include "dspy-tree-view.h"
+#include "dspy-view.h"
+
+#include "libdspy-resources.h"
+
+typedef struct
+{
+  GCancellable          *cancellable;
+  DzlListModelFilter    *filter_model;
+  GListModel            *model;
+
+  /* Template widgets */
+  GtkTreeView           *introspection_tree_view;
+  GtkListBox            *names_list_box;
+  GtkButton             *refresh_button;
+  DspyNameMarquee       *name_marquee;
+  GtkScrolledWindow     *names_scroller;
+  DspyMethodView        *method_view;
+  GtkRevealer           *method_revealer;
+  DspyConnectionButton  *session_button;
+  DspyConnectionButton  *system_button;
+  GtkSearchEntry        *search_entry;
+  GtkMenuButton         *menu_button;
+  GtkBox                *radio_buttons;
+  GtkStack              *stack;
+
+  guint                  destroyed : 1;
+} DspyViewPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (DspyView, dspy_view, GTK_TYPE_BIN)
+
+static void dspy_view_set_model (DspyView   *self,
+                                 GListModel *model);
+
+/**
+ * dspy_view_new:
+ *
+ * Create a new #DspyView.
+ *
+ * This widget contains the window contents beneath the headerbar.
+ *
+ * Returns: (transfer full): a newly created #DspyView
+ */
+GtkWidget *
+dspy_view_new (void)
+{
+  return g_object_new (DSPY_TYPE_VIEW, NULL);
+}
+
+static void
+dspy_view_list_names_cb (GObject      *object,
+                         GAsyncResult *result,
+                         gpointer      user_data)
+{
+  DspyConnection *conn = (DspyConnection *)object;
+  g_autoptr(DspyView) self = user_data;
+  g_autoptr(GListModel) model = NULL;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (DSPY_IS_VIEW (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (DSPY_IS_CONNECTION (conn));
+
+  if (!(model = dspy_connection_list_names_finish (conn, result, &error)))
+    g_warning ("Failed to list names: %s", error->message);
+
+  dspy_view_set_model (self, model);
+}
+
+static void
+radio_button_toggled_cb (DspyView             *self,
+                         DspyConnectionButton *button)
+{
+  DspyViewPrivate *priv = dspy_view_get_instance_private (self);
+  DspyConnection *connection;
+
+  g_assert (DSPY_IS_VIEW (self));
+  g_assert (DSPY_IS_CONNECTION_BUTTON (button));
+
+  if (!gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button)))
+    return;
+
+  gtk_stack_set_visible_child_name (priv->stack, "empty-state");
+
+  connection = dspy_connection_button_get_connection (button);
+  dspy_connection_list_names_async (connection,
+                                    NULL,
+                                    dspy_view_list_names_cb,
+                                    g_object_ref (self));
+}
+
+static void
+connect_address_changed_cb (DspyView       *self,
+                            DzlSimplePopover *popover)
+{
+  const gchar *text;
+
+  g_assert (DSPY_IS_VIEW (self));
+  g_assert (DZL_IS_SIMPLE_POPOVER (popover));
+
+  text = dzl_simple_popover_get_text (popover);
+  dzl_simple_popover_set_ready (popover, text && *text);
+}
+
+static void
+connection_got_error_cb (DspyView       *self,
+                         const GError   *error,
+                         DspyConnection *connection)
+{
+  const gchar *title;
+  GtkWidget *dialog;
+
+  g_assert (DSPY_IS_VIEW (self));
+  g_assert (error != NULL);
+  g_assert (DSPY_IS_CONNECTION (connection));
+
+  if (g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_ACCESS_DENIED))
+    title = _("Access Denied by Peer");
+  else if (g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_AUTH_FAILED))
+    title = _("Authentication Failed");
+  else if (g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_TIMEOUT))
+    title = _("Operation Timed Out");
+  else if (g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_DISCONNECTED))
+    title = _("Lost Connection to Bus");
+  else
+    title = _("DBus Connection Failed");
+
+  dialog = gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (self))),
+                                   GTK_DIALOG_MODAL | GTK_DIALOG_USE_HEADER_BAR,
+                                   GTK_MESSAGE_WARNING,
+                                   GTK_BUTTONS_CLOSE,
+                                   "%s", title);
+  gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), "%s", error->message);
+  g_signal_connect (dialog, "response", G_CALLBACK (gtk_widget_destroy), NULL);
+  gtk_window_present (GTK_WINDOW (dialog));
+}
+
+static void
+connect_address_activate_cb (DspyView         *self,
+                             const gchar      *text,
+                             DzlSimplePopover *popover)
+{
+  DspyViewPrivate *priv = dspy_view_get_instance_private (self);
+  g_autoptr(DspyConnection) connection = NULL;
+  DspyConnectionButton *button;
+
+  g_assert (DSPY_IS_VIEW (self));
+  g_assert (DZL_IS_SIMPLE_POPOVER (popover));
+
+  connection = dspy_connection_new_for_address (text);
+
+  button = g_object_new (DSPY_TYPE_CONNECTION_BUTTON,
+                         "group", priv->session_button,
+                         "connection", connection,
+                         "visible", TRUE,
+                         NULL);
+  g_signal_connect_object (button,
+                           "toggled",
+                           G_CALLBACK (radio_button_toggled_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+  g_signal_connect_object (dspy_connection_button_get_connection (button),
+                           "error",
+                           G_CALLBACK (connection_got_error_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+  gtk_container_add (GTK_CONTAINER (priv->radio_buttons), GTK_WIDGET (button));
+
+  gtk_widget_activate (GTK_WIDGET (button));
+}
+
+static void
+clear_search (DspyView *self)
+{
+  DspyViewPrivate *priv = dspy_view_get_instance_private (self);
+
+  g_assert (DSPY_IS_VIEW (self));
+
+  if (priv->filter_model != NULL)
+    dzl_list_model_filter_set_filter_func (priv->filter_model, NULL, NULL, NULL);
+}
+
+static gboolean
+search_filter_func (DspyName       *name,
+                    DzlPatternSpec *spec)
+{
+  g_assert (DSPY_IS_NAME (name));
+  g_assert (spec != NULL);
+
+  return dzl_pattern_spec_match (spec, dspy_name_get_search_text (name));
+}
+
+static void
+apply_search (DspyView    *self,
+              const gchar *text)
+{
+  DspyViewPrivate *priv = dspy_view_get_instance_private (self);
+
+  g_assert (DSPY_IS_VIEW (self));
+  g_assert (text != NULL);
+  g_assert (text[0] != 0);
+
+  if (priv->filter_model != NULL)
+    dzl_list_model_filter_set_filter_func (priv->filter_model,
+                                           (DzlListModelFilterFunc) search_filter_func,
+                                           dzl_pattern_spec_new (text),
+                                           (GDestroyNotify) dzl_pattern_spec_unref);
+}
+
+static GtkWidget *
+create_name_row_cb (gpointer item,
+                    gpointer user_data)
+{
+  DspyName *name = item;
+
+  g_assert (DSPY_IS_NAME (name));
+  g_assert (user_data == NULL);
+
+  return dspy_name_row_new (name);
+}
+
+static void
+dspy_view_set_model (DspyView   *self,
+                     GListModel *model)
+{
+  DspyViewPrivate *priv = dspy_view_get_instance_private (self);
+  const gchar *text;
+  GtkAdjustment *adj;
+
+  g_assert (DSPY_IS_VIEW (self));
+  g_assert (!model || G_IS_LIST_MODEL (model));
+
+  /* Asynchronous completion implies that we might get here after
+   * the widget has been destroyed.
+   */
+  if (priv->destroyed)
+    return;
+
+  gtk_list_box_bind_model (priv->names_list_box, NULL, NULL, NULL, NULL);
+
+  g_clear_object (&priv->filter_model);
+  g_clear_object (&priv->model);
+
+  if (model != NULL)
+    {
+      priv->model = g_object_ref (model);
+      priv->filter_model = dzl_list_model_filter_new (model);
+    }
+
+  text = gtk_entry_get_text (GTK_ENTRY (priv->search_entry));
+
+  if (text && *text)
+    apply_search (self, text);
+  else
+    clear_search (self);
+
+  gtk_list_box_bind_model (priv->names_list_box,
+                           G_LIST_MODEL (priv->filter_model),
+                           create_name_row_cb,
+                           NULL,
+                           NULL);
+
+  adj = gtk_scrolled_window_get_vadjustment (priv->names_scroller);
+  gtk_adjustment_set_value (adj, 0.0);
+}
+
+static void
+dspy_view_introspect_cb (GObject      *object,
+                         GAsyncResult *result,
+                         gpointer      user_data)
+{
+  DspyName *name = (DspyName *)object;
+  g_autoptr(GtkTreeModel) model = NULL;
+  g_autoptr(DspyView) self = user_data;
+  DspyViewPrivate *priv = dspy_view_get_instance_private (self);
+  g_autoptr(GError) error = NULL;
+
+  g_assert (DSPY_IS_NAME (name));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (DSPY_IS_VIEW (self));
+
+  if (!(model = dspy_name_introspect_finish (name, result, &error)))
+    {
+      DspyConnection *connection = dspy_name_get_connection (name);
+      dspy_connection_add_error (connection, error);
+    }
+
+  gtk_tree_view_set_model (priv->introspection_tree_view, model);
+}
+
+static void
+name_row_activated_cb (DspyView    *self,
+                       DspyNameRow *row,
+                       GtkListBox  *list_box)
+{
+  DspyViewPrivate *priv = dspy_view_get_instance_private (self);
+  DspyName *name;
+
+  g_assert (DSPY_IS_VIEW (self));
+  g_assert (DSPY_IS_NAME_ROW (row));
+  g_assert (GTK_IS_LIST_BOX (list_box));
+
+  name = dspy_name_row_get_name (row);
+
+  g_cancellable_cancel (priv->cancellable);
+  g_clear_object (&priv->cancellable);
+  priv->cancellable = g_cancellable_new ();
+
+  gtk_tree_view_set_model (priv->introspection_tree_view, NULL);
+  dspy_name_marquee_set_name (priv->name_marquee, name);
+
+  gtk_revealer_set_reveal_child (priv->method_revealer, FALSE);
+
+  dspy_name_introspect_async (name,
+                              priv->cancellable,
+                              dspy_view_introspect_cb,
+                              g_object_ref (self));
+
+  gtk_stack_set_visible_child_name (priv->stack, "introspect");
+}
+
+static void
+refresh_button_clicked_cb (DspyView  *self,
+                           GtkButton *button)
+{
+  DspyViewPrivate *priv = dspy_view_get_instance_private (self);
+  GtkListBoxRow *row;
+
+  g_assert (DSPY_IS_VIEW (self));
+  g_assert (GTK_IS_BUTTON (button));
+
+  if ((row = gtk_list_box_get_selected_row (priv->names_list_box)))
+    name_row_activated_cb (self, DSPY_NAME_ROW (row), priv->names_list_box);
+}
+
+static void
+method_activated_cb (DspyView             *self,
+                     DspyMethodInvocation *invocation,
+                     DspyTreeView         *tree_view)
+{
+  DspyViewPrivate *priv = dspy_view_get_instance_private (self);
+
+  g_assert (DSPY_IS_VIEW (self));
+  g_assert (!invocation || DSPY_IS_METHOD_INVOCATION (invocation));
+  g_assert (DSPY_IS_TREE_VIEW (tree_view));
+
+  if (DSPY_IS_METHOD_INVOCATION (invocation))
+    {
+      dspy_method_view_set_invocation (priv->method_view, invocation);
+      gtk_revealer_set_reveal_child (priv->method_revealer, TRUE);
+    }
+}
+
+static void
+notify_child_revealed_cb (DspyView    *self,
+                          GParamSpec  *pspec,
+                          GtkRevealer *revealer)
+{
+  DspyViewPrivate *priv = dspy_view_get_instance_private (self);
+
+  g_assert (DSPY_IS_VIEW (self));
+  g_assert (GTK_IS_REVEALER (revealer));
+
+  if (!gtk_revealer_get_child_revealed (revealer))
+    {
+      dspy_method_view_set_invocation (priv->method_view, NULL);
+    }
+  else
+    {
+      GtkTreeSelection *selection;
+      GtkTreeModel *model = NULL;
+      GtkTreeIter iter;
+
+      selection = gtk_tree_view_get_selection (priv->introspection_tree_view);
+
+      if (gtk_tree_selection_get_selected (selection, &model, &iter))
+        {
+          g_autoptr(GtkTreePath) path = gtk_tree_model_get_path (model, &iter);
+          GtkTreeViewColumn *column = gtk_tree_view_get_column (priv->introspection_tree_view, 0);
+
+          /* Move the selected row as far up as we can so that the revealer
+           * for the method invocation does not cover the selected area.
+           */
+          gtk_tree_view_scroll_to_cell (priv->introspection_tree_view,
+                                        path,
+                                        column,
+                                        TRUE,
+                                        0.0,
+                                        0.0);
+        }
+    }
+}
+
+static void
+search_entry_changed_cb (DspyView       *self,
+                         GtkSearchEntry *search_entry)
+{
+  const gchar *text;
+
+  g_assert (DSPY_IS_VIEW (self));
+  g_assert (GTK_IS_SEARCH_ENTRY (search_entry));
+
+  text = gtk_entry_get_text (GTK_ENTRY (search_entry));
+
+  if (text == NULL || *text == 0)
+    clear_search (self);
+  else
+    apply_search (self, text);
+}
+
+static void
+connect_to_bus_action (GSimpleAction *action,
+                       GVariant      *params,
+                       gpointer       user_data)
+{
+  DspyView *self = user_data;
+  DspyViewPrivate *priv = dspy_view_get_instance_private (self);
+  GtkPopover *popover;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (DSPY_IS_VIEW (self));
+
+  popover = g_object_new (DZL_TYPE_SIMPLE_POPOVER,
+                          "button-text", _("Connect"),
+                          "message", _("Provide the address of the message bus"),
+                          "position", GTK_POS_RIGHT,
+                          "title", _("Connect to Other Bus"),
+                          "relative-to", priv->system_button,
+                          NULL);
+
+  g_signal_connect_object (popover,
+                           "changed",
+                           G_CALLBACK (connect_address_changed_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (popover,
+                           "activate",
+                           G_CALLBACK (connect_address_activate_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect (popover,
+                    "closed",
+                    G_CALLBACK (gtk_widget_destroy),
+                    NULL);
+
+  gtk_popover_popup (popover);
+}
+
+static GActionEntry action_entries[] = {
+  { "connect-to-bus", connect_to_bus_action },
+};
+
+static void
+dspy_view_destroy (GtkWidget *widget)
+{
+  DspyView *self = (DspyView *)widget;
+  DspyViewPrivate *priv = dspy_view_get_instance_private (self);
+
+  priv->destroyed = TRUE;
+
+  g_cancellable_cancel (priv->cancellable);
+  g_clear_object (&priv->cancellable);
+  g_clear_object (&priv->filter_model);
+  g_clear_object (&priv->model);
+
+  GTK_WIDGET_CLASS (dspy_view_parent_class)->destroy (widget);
+}
+
+static void
+dspy_view_class_init (DspyViewClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  widget_class->destroy = dspy_view_destroy;
+
+  gtk_widget_class_set_css_name (widget_class, "dspyview");
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/dspy/dspy-view.ui");
+  gtk_widget_class_bind_template_child_private (widget_class, DspyView, introspection_tree_view);
+  gtk_widget_class_bind_template_child_private (widget_class, DspyView, menu_button);
+  gtk_widget_class_bind_template_child_private (widget_class, DspyView, method_revealer);
+  gtk_widget_class_bind_template_child_private (widget_class, DspyView, method_view);
+  gtk_widget_class_bind_template_child_private (widget_class, DspyView, name_marquee);
+  gtk_widget_class_bind_template_child_private (widget_class, DspyView, names_list_box);
+  gtk_widget_class_bind_template_child_private (widget_class, DspyView, names_scroller);
+  gtk_widget_class_bind_template_child_private (widget_class, DspyView, radio_buttons);
+  gtk_widget_class_bind_template_child_private (widget_class, DspyView, refresh_button);
+  gtk_widget_class_bind_template_child_private (widget_class, DspyView, search_entry);
+  gtk_widget_class_bind_template_child_private (widget_class, DspyView, session_button);
+  gtk_widget_class_bind_template_child_private (widget_class, DspyView, stack);
+  gtk_widget_class_bind_template_child_private (widget_class, DspyView, system_button);
+
+  g_type_ensure (DSPY_TYPE_METHOD_VIEW);
+  g_type_ensure (DSPY_TYPE_NAME_MARQUEE);
+  g_type_ensure (DSPY_TYPE_TREE_VIEW);
+}
+
+static void
+dspy_view_init (DspyView *self)
+{
+  DspyViewPrivate *priv = dspy_view_get_instance_private (self);
+  g_autoptr(GSimpleActionGroup) actions = g_simple_action_group_new ();
+  GMenu *menu;
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  g_action_map_add_action_entries (G_ACTION_MAP (actions),
+                                   action_entries,
+                                   G_N_ELEMENTS (action_entries),
+                                   self);
+  gtk_widget_insert_action_group (GTK_WIDGET (self), "dspy", G_ACTION_GROUP (actions));
+
+  menu = dzl_application_get_menu_by_id (DZL_APPLICATION (g_application_get_default ()),
+                                         "dspy-connections-menu");
+  gtk_menu_button_set_menu_model (priv->menu_button, G_MENU_MODEL (menu));
+
+  g_signal_connect_object (self,
+                           "key-press-event",
+                           G_CALLBACK (dzl_shortcut_manager_handle_event),
+                           NULL,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (priv->names_list_box,
+                           "row-activated",
+                           G_CALLBACK (name_row_activated_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (priv->refresh_button,
+                           "clicked",
+                           G_CALLBACK (refresh_button_clicked_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (priv->method_revealer,
+                           "notify::child-revealed",
+                           G_CALLBACK (notify_child_revealed_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (priv->introspection_tree_view,
+                           "method-activated",
+                           G_CALLBACK (method_activated_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (priv->session_button,
+                           "toggled",
+                           G_CALLBACK (radio_button_toggled_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (dspy_connection_button_get_connection (priv->session_button),
+                           "error",
+                           G_CALLBACK (connection_got_error_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (priv->system_button,
+                           "toggled",
+                           G_CALLBACK (radio_button_toggled_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (dspy_connection_button_get_connection (priv->system_button),
+                           "error",
+                           G_CALLBACK (connection_got_error_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (priv->search_entry,
+                           "changed",
+                           G_CALLBACK (search_entry_changed_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  radio_button_toggled_cb (self, priv->session_button);
+}
diff --git a/src/plugins/dspy/libdspy/dspy-view.h b/src/plugins/dspy/libdspy/dspy-view.h
new file mode 100644
index 000000000..4b05ce9f2
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-view.h
@@ -0,0 +1,41 @@
+/* dspy-view.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define DSPY_TYPE_VIEW (dspy_view_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (DspyView, dspy_view, DSPY, VIEW, GtkBin)
+
+struct _DspyViewClass
+{
+  GtkBinClass parent_class;
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+GtkWidget *dspy_view_new (void);
+
+G_END_DECLS
diff --git a/src/plugins/dspy/libdspy/dspy-view.ui b/src/plugins/dspy/libdspy/dspy-view.ui
new file mode 100644
index 000000000..f720ab976
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-view.ui
@@ -0,0 +1,218 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.1 -->
+<interface>
+  <requires lib="gtk+" version="3.24"/>
+  <template class="DspyView" parent="GtkBin">
+    <property name="can_focus">False</property>
+    <child>
+      <object class="DzlMultiPaned">
+        <property name="orientation">horizontal</property>
+        <property name="visible">True</property>
+        <child>
+          <object class="GtkBox">
+            <property name="orientation">vertical</property>
+            <property name="visible">true</property>
+            <style>
+              <class name="sidebar"/>
+              <class name="view"/>
+            </style>
+            <child>
+              <object class="GtkBox" id="radio_buttons">
+                <property name="margin">6</property>
+                <property name="homogeneous">true</property>
+                <property name="orientation">horizontal</property>
+                <property name="visible">true</property>
+                <style>
+                  <class name="linked"/>
+                </style>
+                <child>
+                  <object class="DspyConnectionButton" id="session_button">
+                    <property name="bus-type">session</property>
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="DspyConnectionButton" id="system_button">
+                    <property name="bus-type">system</property>
+                    <property name="group">session_button</property>
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkSearchEntry" id="search_entry">
+                <property name="placeholder-text" translatable="yes">Search Bus Names</property>
+                <property name="margin-top">6</property>
+                <property name="margin-start">6</property>
+                <property name="margin-end">6</property>
+                <property name="margin-bottom">12</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkBox">
+                <property name="orientation">horizontal</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="GtkLabel">
+                    <property name="label" translatable="yes">Bus Names</property>
+                    <property name="hexpand">true</property>
+                    <property name="visible">true</property>
+                    <property name="xalign">0.0</property>
+                    <property name="margin-start">6</property>
+                    <property name="margin-bottom">1</property>
+                    <style>
+                      <class name="dim-label"/>
+                    </style>
+                    <attributes>
+                      <attribute name="weight" value="bold"/>
+                      <attribute name="scale" value=".833333"/>
+                    </attributes>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkMenuButton" id="menu_button">
+                    <property name="focus-on-click">false</property>
+                    <property name="visible">true</property>
+                    <child>
+                      <object class="GtkImage">
+                        <property name="icon-name">pan-down-symbolic</property>
+                        <property name="visible">true</property>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkScrolledWindow" id="names_scroller">
+                <property name="propagate-natural-width">True</property>
+                <property name="max-content-width">300</property>
+                <property name="vexpand">True</property>
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <child>
+                  <object class="GtkViewport">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <child>
+                      <object class="GtkListBox" id="names_list_box">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="position">300</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkStack" id="stack">
+            <property name="transition-duration">300</property>
+            <property name="transition-type">crossfade</property>
+            <property name="hexpand">true</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="DzlEmptyState">
+                <property name="icon-name">org.gnome.dfeet-symbolic</property>
+                <property name="title" translatable="yes">Select a Bus Name</property>
+                <property name="subtitle" translatable="yes">Select a bus name to introspect the 
peer.</property>
+                <property name="visible">true</property>
+              </object>
+              <packing>
+                <property name="name">empty-state</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkBox">
+                <property name="margin">12</property>
+                <property name="orientation">vertical</property>
+                <property name="spacing">12</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="DspyNameMarquee" id="name_marquee">
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkBox">
+                    <property name="orientation">vertical</property>
+                    <property name="visible">true</property>
+                    <style>
+                      <class name="linked"/>
+                    </style>
+                    <child>
+                      <object class="GtkScrolledWindow">
+                        <property name="shadow-type">in</property>
+                        <property name="vexpand">True</property>
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <child>
+                          <object class="DspyTreeView" id="introspection_tree_view">
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <child internal-child="selection">
+                              <object class="GtkTreeSelection"/>
+                            </child>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="DzlBin">
+                        <property name="visible">true</property>
+                        <style>
+                          <class name="inline-toolbar"/>
+                        </style>
+                        <child>
+                          <object class="GtkBox">
+                            <property name="orientation">horizontal</property>
+                            <property name="visible">true</property>
+                            <child>
+                              <object class="GtkButton" id="refresh_button">
+                                <property name="visible">true</property>
+                                <style>
+                                  <class name="image-button"/>
+                                </style>
+                                <child>
+                                  <object class="GtkImage">
+                                    <property name="icon-name">view-refresh-symbolic</property>
+                                    <property name="visible">true</property>
+                                  </object>
+                                </child>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkRevealer" id="method_revealer">
+                    <property name="reveal-child">false</property>
+                    <property name="visible">true</property>
+                    <child>
+                      <object class="DspyMethodView" id="method_view">
+                        <property name="margin-start">12</property>
+                        <property name="visible">true</property>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="name">introspect</property>
+              </packing>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/plugins/dspy/libdspy/dspy.h b/src/plugins/dspy/libdspy/dspy.h
new file mode 100644
index 000000000..7f5a06247
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy.h
@@ -0,0 +1,37 @@
+/* dspy.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#pragma once
+
+#define DSPY_INSIDE
+
+# include "dspy-connection.h"
+# include "dspy-connection-button.h"
+# include "dspy-introspection-model.h"
+# include "dspy-method-invocation.h"
+# include "dspy-method-view.h"
+# include "dspy-name.h"
+# include "dspy-name-marquee.h"
+# include "dspy-name-row.h"
+# include "dspy-names-model.h"
+# include "dspy-tree-view.h"
+# include "dspy-view.h"
+
+#undef DSPY_INSIDE
diff --git a/src/plugins/dspy/libdspy/gtk/menus.ui b/src/plugins/dspy/libdspy/gtk/menus.ui
new file mode 100644
index 000000000..9b844307c
--- /dev/null
+++ b/src/plugins/dspy/libdspy/gtk/menus.ui
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <menu id="dspy-connections-menu">
+    <item>
+      <attribute name="label" translatable="yes">Connect to Other Bus</attribute>
+      <attribute name="action">dspy.connect-to-bus</attribute>
+    </item>
+  </menu>
+</interface>
diff --git a/src/plugins/dspy/libdspy/icons/symbolic/apps/org.gnome.dfeet-symbolic.svg 
b/src/plugins/dspy/libdspy/icons/symbolic/apps/org.gnome.dfeet-symbolic.svg
new file mode 100644
index 000000000..71fc85382
--- /dev/null
+++ b/src/plugins/dspy/libdspy/icons/symbolic/apps/org.gnome.dfeet-symbolic.svg
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/";
+   xmlns:cc="http://creativecommons.org/ns#";
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+   xmlns:svg="http://www.w3.org/2000/svg";
+   xmlns="http://www.w3.org/2000/svg";
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd";
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape";
+   width="16"
+   height="16"
+   viewBox="0 0 16 16.000001"
+   version="1.1"
+   id="svg2008"
+   inkscape:version="0.92.4 5da689c313, 2019-01-14"
+   sodipodi:docname="org.gnome.dfeet-symbolic.svg">
+  <defs
+     id="defs2002" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="45.254834"
+     inkscape:cx="4.6852043"
+     inkscape:cy="0.94029949"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="true"
+     units="px"
+     borderlayer="true"
+     inkscape:showpageshadow="false">
+    <inkscape:grid
+       type="xygrid"
+       id="grid2553" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata2005">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage"; />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1106.5197)">
+    <path
+       
style="display:inline;opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;stroke:none;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new"
+       d="m 10,1109.5197 c -1.108,0 -2,0.892 -2,2 v 3 h 4 v -3 c 0,-1.108 -0.892,-2 -2,-2 z"
+       id="rect1969"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ssccss" />
+    <path
+       sodipodi:nodetypes="ssccss"
+       inkscape:connector-curvature="0"
+       id="path1972"
+       d="m 10,1118.5197 c -1.108,0 -2,-0.892 -2,-2 v -1 h 4 v 1 c 0,1.108 -0.892,2 -2,2 z"
+       
style="display:inline;opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;stroke:none;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new"
 />
+    <path
+       
style="display:inline;opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;stroke:none;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new"
+       d="m 4,1112.5197 c 1.108,0 2,0.892 2,2 v 3 H 5.27734 c -0.94474,-0.8696 -1.54492,-2.1073 
-1.54492,-3.5 0,-0.5283 0.10678,-1.026 0.26367,-1.5 10e-4,0 0.003,0 0.004,0 z m -1.98242,1.834 c 
0.0597,1.1613 0.42327,2.2424 1.01758,3.166 H 2 v -3 c 0,-0.057 0.013,-0.1103 0.0176,-0.166 z"
+       id="path1974"
+       inkscape:connector-curvature="0" />
+    <path
+       
style="display:inline;opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new"
+       d="M 8.5 0 C 4.369709 0 1 3.3697 1 7.5 C 1 9.0628162 1.4830998 10.515066 2.3066406 11.71875 L 0 14 L 
0 16 L 2 16 L 4.296875 13.703125 C 5.4974125 14.519964 6.9441527 15 8.5 15 C 12.630291 15 16 11.6303 16 7.5 C 
16 3.3697 12.630291 0 8.5 0 z M 8.5 2 C 11.549411 2 14 4.4506 14 7.5 C 14 10.5494 11.549411 13 8.5 13 C 
5.4505892 13 3 10.5494 3 7.5 C 3 4.4506 5.4505892 2 8.5 2 z "
+       transform="translate(0,1106.5197)"
+       id="path1987" />
+  </g>
+</svg>
diff --git a/src/plugins/dspy/libdspy/libdspy.gresource.xml b/src/plugins/dspy/libdspy/libdspy.gresource.xml
new file mode 100644
index 000000000..d6ecfa90e
--- /dev/null
+++ b/src/plugins/dspy/libdspy/libdspy.gresource.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/org/gnome/dspy">
+    <file preprocess="xml-stripblanks">dspy-method-view.ui</file>
+    <file preprocess="xml-stripblanks">dspy-name-marquee.ui</file>
+    <file preprocess="xml-stripblanks">dspy-name-row.ui</file>
+    <file preprocess="xml-stripblanks">dspy-view.ui</file>
+    <file preprocess="xml-stripblanks">gtk/menus.ui</file>
+    <file preprocess="xml-stripblanks">icons/symbolic/apps/org.gnome.dfeet-symbolic.svg</file>
+    <file>themes/shared.css</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/dspy/libdspy/meson.build b/src/plugins/dspy/libdspy/meson.build
new file mode 100644
index 000000000..e87b2bab6
--- /dev/null
+++ b/src/plugins/dspy/libdspy/meson.build
@@ -0,0 +1,35 @@
+libdspy_sources = [
+  'dspy-connection.c',
+  'dspy-connection-button.c',
+  'dspy-introspection-model.c',
+  'dspy-method-invocation.c',
+  'dspy-method-view.c',
+  'dspy-name.c',
+  'dspy-name-marquee.c',
+  'dspy-name-row.c',
+  'dspy-names-model.c',
+  'dspy-node.c',
+  'dspy-signature.c',
+  'dspy-tree-view.c',
+  'dspy-view.c',
+]
+
+libdspy_deps = [
+  libgio_dep,
+  libgtk_dep,
+  libdazzle_dep,
+]
+
+libdspy_sources += gnome.compile_resources('libdspy-resources', 'libdspy.gresource.xml',
+  c_name: 'libdspy'
+)
+
+libdspy = static_library('libdspy', libdspy_sources,
+         dependencies: libdspy_deps,
+)
+
+libdspy_dep = declare_dependency(
+         dependencies: libdspy_deps,
+            link_with: [ libdspy ],
+  include_directories: include_directories('.'),
+)
diff --git a/src/plugins/dspy/libdspy/themes/shared.css b/src/plugins/dspy/libdspy/themes/shared.css
new file mode 100644
index 000000000..7498c3cd9
--- /dev/null
+++ b/src/plugins/dspy/libdspy/themes/shared.css
@@ -0,0 +1,16 @@
+dspyview dzlmultipaned > :first-child {
+  border-right: 1px solid @borders;
+}
+dspyview box.linked.horizontal > scrolledwindow:first-child {
+  border-right: none;
+}
+dspyview .sidebar button.popup.toggle {
+  border: none;
+  border-radius: 0px;
+  background: none;
+  box-shadow: none;
+  padding: 0px 6px;
+}
+dspyview .sidebar button:not(:hover) image {
+  color: alpha(currentColor, 0.6);
+}
diff --git a/src/plugins/dspy/meson.build b/src/plugins/dspy/meson.build
new file mode 100644
index 000000000..d238594d8
--- /dev/null
+++ b/src/plugins/dspy/meson.build
@@ -0,0 +1,21 @@
+if get_option('plugin_dspy')
+
+subdir('libdspy')
+
+plugins_sources += files([
+  'dspy-plugin.c',
+  'gbp-dspy-application-addin.c',
+  'gbp-dspy-surface.c',
+  'gbp-dspy-workspace.c',
+])
+
+plugin_dspy_resources = gnome.compile_resources(
+  'dspy-resources',
+  'dspy.gresource.xml',
+  c_name: 'gbp_dspy',
+)
+
+plugins_sources += plugin_dspy_resources
+plugins_deps += libdspy_dep
+
+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]