[folks] Bug 629075 — Add folks command line application



commit 96b1a7b28989b50cc0bb2241ece5889a2b7fb0cf
Author: Philip Withnall <philip withnall collabora co uk>
Date:   Mon Sep 6 19:38:21 2010 +0100

    Bug 629075 â?? Add folks command line application
    
    Add an interactive command line inspection utility for libfolks.
    Closes: bgo#629075.

 NEWS                                      |    4 +
 configure.ac                              |   21 ++
 tools/Makefile.am                         |    8 +
 tools/inspect/Makefile.am                 |   40 +++
 tools/inspect/command-backends.vala       |  102 ++++++
 tools/inspect/command-help.vala           |   89 +++++
 tools/inspect/command-individuals.vala    |   90 +++++
 tools/inspect/command-persona-stores.vala |  107 ++++++
 tools/inspect/command-personas.vala       |   81 +++++
 tools/inspect/command-quit.vala           |   57 ++++
 tools/inspect/command-signals.vala        |  250 ++++++++++++++
 tools/inspect/inspect.vala                |  223 +++++++++++++
 tools/inspect/signal-manager.vala         |  502 +++++++++++++++++++++++++++++
 tools/inspect/utils.vala                  |  477 +++++++++++++++++++++++++++
 14 files changed, 2051 insertions(+), 0 deletions(-)
---
diff --git a/NEWS b/NEWS
index 3cd95df..6f03ea9 100644
--- a/NEWS
+++ b/NEWS
@@ -1,11 +1,15 @@
 Overview of changes from libfolks 0.3.3 to libfolks 0.3.4
 =========================================================
 
+Major changes:
+* Add folks-inspect tool
+
 Bugs fixed:
 * Bug 637240 â?? libfolks-telepathy.so exports private symbols
 * Bug 638311 â?? Add a HACKING file that outlines development policies and
   coding style
 * Bug 629083 â?? Review coding conventions in folks
+* Bug 629075 â?? Add folks command line application
 
 Overview of changes from libfolks 0.3.2 to libfolks 0.3.3
 =========================================================
diff --git a/configure.ac b/configure.ac
index 4c2ace4..662980d 100644
--- a/configure.ac
+++ b/configure.ac
@@ -193,6 +193,25 @@ AS_IF([test "$enable_import_tool" = "yes" -a "$with_import_tool" = "no"],
       [AC_MSG_ERROR([Import tool explicitly enabled, but libxml2 not found])])
 AM_CONDITIONAL([ENABLE_IMPORT_TOOL], [test "$with_import_tool" = "yes"])
 
+# Readline's required for the folks-inspect program
+AC_ARG_ENABLE([inspect-tool],
+              AS_HELP_STRING([--enable-inspect-tool],
+                             [Enable building the data inspection tool]),
+              [enable_inspect_tool=$enableval with_inspect_tool=$enableval],
+              [enable_inspect_tool=maybe with_inspect_tool=no])
+AS_IF([test "$enable_inspect_tool" != "no"],
+      [AC_CHECK_LIB([readline], [main],
+                    [with_inspect_tool=yes], [with_inspect_tool=no],
+                    [-lncurses])])
+AS_IF([test "$enable_inspect_tool" != "no" -a "$with_inspect_tool" = "yes"],
+      [VALA_CHECK_PACKAGES([readline])
+       LIBREADLINE="-lreadline -lncurses"
+       AC_DEFINE([HAVE_LIBREADLINE], [1], [Define if you have libreadline])
+       AC_SUBST([LIBREADLINE])])
+AS_IF([test "$enable_inspect_tool" = "yes" -a "$with_inspect_tool" = "no"],
+      [AC_MSG_ERROR([Inspect tool explicitly enabled, but readline not found])])
+AM_CONDITIONAL([ENABLE_INSPECT_TOOL], [test "$with_inspect_tool" = "yes"])
+
 # -----------------------------------------------------------
 # Documentation
 # -----------------------------------------------------------
@@ -304,6 +323,7 @@ AC_CONFIG_FILES([
     tests/lib/telepathy/contactlist/session.conf
     tests/tools/Makefile
     tools/Makefile
+    tools/inspect/Makefile
 ])
 
 AC_OUTPUT
@@ -319,6 +339,7 @@ Configure summary:
         Documentation...............:  ${HAVE_VALADOC}
         Tests.......................:  ${have_tp_glib_for_tests}
         Import tool.................:  ${with_import_tool}
+        Inspector tool..............:  ${with_inspect_tool}
 
 
 "
diff --git a/tools/Makefile.am b/tools/Makefile.am
index 17ab8e7..9bcce9c 100644
--- a/tools/Makefile.am
+++ b/tools/Makefile.am
@@ -1,3 +1,11 @@
+# Inspector
+DIST_SUBDIRS = inspect
+
+if ENABLE_INSPECT_TOOL
+SUBDIRS = inspect
+endif
+
+# Importer
 if ENABLE_IMPORT_TOOL
 bin_PROGRAMS = folks-import
 endif
diff --git a/tools/inspect/Makefile.am b/tools/inspect/Makefile.am
new file mode 100644
index 0000000..715574a
--- /dev/null
+++ b/tools/inspect/Makefile.am
@@ -0,0 +1,40 @@
+VALAFLAGS = \
+	--vapidir=$(top_srcdir)/folks \
+	--pkg=readline \
+	--pkg=gobject-2.0 \
+	--pkg=gee-1.0 \
+	--pkg=folks \
+	$(NULL)
+
+bin_PROGRAMS = folks-inspect
+
+folks_inspect_SOURCES = \
+	command-backends.vala \
+	command-help.vala \
+	command-individuals.vala \
+	command-persona-stores.vala \
+	command-personas.vala \
+	command-quit.vala \
+	command-signals.vala \
+	signal-manager.vala \
+	utils.vala \
+	inspect.vala \
+	$(NULL)
+folks_inspect_LDADD = \
+	$(LIBREADLINE) \
+	$(GLIB_LIBS) \
+	$(GEE_LIBS) \
+	$(top_builddir)/folks/libfolks.la \
+	$(NULL)
+folks_inspect_CFLAGS = \
+	-I$(top_srcdir)/folks \
+	$(GLIB_CFLAGS) \
+	$(GEE_CFLAGS) \
+	$(NULL)
+
+GITIGNOREFILES = \
+	folks_inspect_vala.stamp \
+	$(folks_inspect_SOURCES:.vala=.c) \
+	$(NULL)
+
+-include $(top_srcdir)/git.mk
diff --git a/tools/inspect/command-backends.vala b/tools/inspect/command-backends.vala
new file mode 100644
index 0000000..d3a5d18
--- /dev/null
+++ b/tools/inspect/command-backends.vala
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2010 Collabora Ltd.
+ *
+ * This library is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This library 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 library.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ *       Philip Withnall <philip withnall collabora co uk>
+ */
+
+using Folks;
+using Gee;
+using GLib;
+
+private class Folks.Inspect.Commands.Backends : Folks.Inspect.Command
+{
+  public override string name
+    {
+      get { return "backends"; }
+    }
+
+  public override string description
+    {
+      get { return "Inspect the backends loaded by the aggregator."; }
+    }
+
+  public override string help
+    {
+      get
+        {
+          return "backends                   List all known backends.\n" +
+              "backends [backend name]    Display the details of the " +
+              "specified backend and list its persona stores.";
+        }
+    }
+
+  public Backends (Client client)
+    {
+      base (client);
+    }
+
+  public override void run (string? command_string)
+    {
+      if (command_string == null)
+        {
+          /* List all the backends */
+          Collection<Backend> backends =
+              this.client.backend_store.list_backends ();
+
+          Utils.print_line ("%u backends:", backends.size);
+
+          Utils.indent ();
+          foreach (Backend backend in backends)
+            Utils.print_line ("%s", backend.name);
+          Utils.unindent ();
+        }
+      else
+        {
+          /* Show the details of a particular backend */
+          Backend backend =
+              this.client.backend_store.get_backend_by_name (command_string);
+
+          if (backend == null)
+            {
+              Utils.print_line ("Unrecognised backend name '%s'.",
+                  command_string);
+              return;
+            }
+
+          Utils.print_line ("Backend '%s' with %u persona stores " +
+              "(type ID, ID ('display name')):",
+              backend.name, backend.persona_stores.size ());
+
+          /* List the backend's persona stores */
+          Utils.indent ();
+          backend.persona_stores.foreach ((k, v) =>
+            {
+              PersonaStore store = (PersonaStore) v;
+              Utils.print_line ("%s, %s ('%s')", store.type_id, store.id,
+                  store.display_name);
+            });
+          Utils.unindent ();
+        }
+    }
+
+  public override string[]? complete_subcommand (string subcommand)
+    {
+      /* @subcommand should be a backend name */
+      return Readline.completion_matches (subcommand,
+          Utils.backend_name_completion_cb);
+    }
+}
diff --git a/tools/inspect/command-help.vala b/tools/inspect/command-help.vala
new file mode 100644
index 0000000..21d3b03
--- /dev/null
+++ b/tools/inspect/command-help.vala
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2010 Collabora Ltd.
+ *
+ * This library is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This library 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 library.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ *       Philip Withnall <philip withnall collabora co uk>
+ */
+
+using Folks;
+using Gee;
+using GLib;
+
+private class Folks.Inspect.Commands.Help : Folks.Inspect.Command
+{
+  public override string name
+    {
+      get { return "help"; }
+    }
+
+  public override string description
+    {
+      get { return "Get help on using the program."; }
+    }
+
+  public override string help
+    {
+      get
+        {
+          return "help                   Describe all the available " +
+              "commands.\n" +
+              "help [command name]    Give more detailed help on the " +
+              "specified command.";
+        }
+    }
+
+  public Help (Client client)
+    {
+      base (client);
+    }
+
+  public override void run (string? command_string)
+    {
+      if (command_string == null)
+        {
+          /* Help index */
+          Utils.print_line ("Type 'help <command>' for more information " +
+              "about a particular command.");
+
+          MapIterator<string, Command> iter =
+              this.client.commands.map_iterator ();
+
+          Utils.indent ();
+          while (iter.next () == true)
+            {
+              Utils.print_line ("%-20s  %s", iter.get_key (),
+                  iter.get_value ().description);
+            }
+          Utils.unindent ();
+        }
+      else
+        {
+          /* Help for a given command */
+          Command command = this.client.commands.get (command_string);
+          if (command == null)
+            Utils.print_line ("Unrecognised command '%s'.", command_string);
+          else
+            Utils.print_line ("%s", command.help);
+        }
+    }
+
+  public override string[]? complete_subcommand (string subcommand)
+    {
+      /* @subcommand should be a command name */
+      return Readline.completion_matches (subcommand,
+          Utils.command_name_completion_cb);
+    }
+}
diff --git a/tools/inspect/command-individuals.vala b/tools/inspect/command-individuals.vala
new file mode 100644
index 0000000..e3ce6fd
--- /dev/null
+++ b/tools/inspect/command-individuals.vala
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2010 Collabora Ltd.
+ *
+ * This library is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This library 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 library.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ *       Philip Withnall <philip withnall collabora co uk>
+ */
+
+using Folks;
+using Gee;
+using GLib;
+
+private class Folks.Inspect.Commands.Individuals : Folks.Inspect.Command
+{
+  public override string name
+    {
+      get { return "individuals"; }
+    }
+
+  public override string description
+    {
+      get
+        {
+          return "Inspect the individuals currently present in the aggregator";
+        }
+    }
+
+  public override string help
+    {
+      get
+        {
+          return "individuals                    List all known " +
+              "individuals.\n" +
+              "individuals [individual ID]    Display the details of the " +
+              "specified individual and list its personas.";
+        }
+    }
+
+  public Individuals (Client client)
+    {
+      base (client);
+    }
+
+  public override void run (string? command_string)
+    {
+      if (command_string == null)
+        {
+          /* List all the individuals */
+          this.client.aggregator.individuals.foreach ((k, v) =>
+            {
+              Utils.print_individual ((Individual) v, false);
+              Utils.print_line ("");
+            });
+        }
+      else
+        {
+          /* Display the details of a single individual */
+          Individual individual =
+              this.client.aggregator.individuals.lookup (command_string);
+
+          if (individual == null)
+            {
+              Utils.print_line ("Unrecognised individual ID '%s'.",
+                  command_string);
+              return;
+            }
+
+          Utils.print_individual (individual, true);
+        }
+    }
+
+  public override string[]? complete_subcommand (string subcommand)
+    {
+      /* @subcommand should be an individual ID */
+      return Readline.completion_matches (subcommand,
+          Utils.individual_id_completion_cb);
+    }
+}
diff --git a/tools/inspect/command-persona-stores.vala b/tools/inspect/command-persona-stores.vala
new file mode 100644
index 0000000..445e75f
--- /dev/null
+++ b/tools/inspect/command-persona-stores.vala
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2010 Collabora Ltd.
+ *
+ * This library is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This library 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 library.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ *       Philip Withnall <philip withnall collabora co uk>
+ */
+
+using Folks;
+using Gee;
+using GLib;
+
+private class Folks.Inspect.Commands.PersonaStores : Folks.Inspect.Command
+{
+  public override string name
+    {
+      get { return "persona-stores"; }
+    }
+
+  public override string description
+    {
+      get
+        {
+          return "Inspect the persona stores loaded by the aggregator";
+        }
+    }
+
+  public override string help
+    {
+      get
+        {
+          return "persona-stores                       List all known " +
+              "persona stores.\n" +
+              "persona-stores [persona store ID]    Display the details of " +
+              "the specified persona store and list its personas.";
+        }
+    }
+
+  public PersonaStores (Client client)
+    {
+      base (client);
+    }
+
+  public override void run (string? command_string)
+    {
+      if (command_string == null)
+        {
+          /* List all the persona stores */
+          Collection<Backend> backends =
+              this.client.backend_store.list_backends ();
+
+          foreach (Backend backend in backends)
+            {
+              HashTable<string, PersonaStore> stores = backend.persona_stores;
+
+              stores.foreach ((k, v) =>
+                {
+                  Utils.print_persona_store ((PersonaStore) v, false);
+                  Utils.print_line ("");
+                });
+            }
+        }
+      else
+        {
+          /* Show the details of a particular persona store */
+          Collection<Backend> backends =
+              this.client.backend_store.list_backends ();
+          PersonaStore store = null;
+
+          foreach (Backend backend in backends)
+            {
+              HashTable<string, PersonaStore> stores = backend.persona_stores;
+              store = stores.lookup (command_string);
+              if (store != null)
+                break;
+            }
+
+          if (store == null)
+            {
+              Utils.print_line ("Unrecognised persona store ID '%s'.",
+                  command_string);
+              return;
+            }
+
+          Utils.print_persona_store (store, true);
+        }
+    }
+
+  public override string[]? complete_subcommand (string subcommand)
+    {
+      /* @subcommand should be a persona store ID */
+      return Readline.completion_matches (subcommand,
+          Utils.persona_store_id_completion_cb);
+    }
+}
diff --git a/tools/inspect/command-personas.vala b/tools/inspect/command-personas.vala
new file mode 100644
index 0000000..87b953f
--- /dev/null
+++ b/tools/inspect/command-personas.vala
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2010 Collabora Ltd.
+ *
+ * This library is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This library 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 library.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ *       Philip Withnall <philip withnall collabora co uk>
+ */
+
+using Folks;
+using Gee;
+using GLib;
+
+private class Folks.Inspect.Commands.Personas : Folks.Inspect.Command
+{
+  public override string name
+    {
+      get { return "personas"; }
+    }
+
+  public override string description
+    {
+      get
+        {
+          return "Inspect the personas currently present in the aggregator";
+        }
+    }
+
+  public override string help
+    {
+      get
+        {
+          return "personas                  List all known personas.\n" +
+              "personas [persona UID]    Display the details of the " +
+              "specified persona.";
+        }
+    }
+
+  public Personas (Client client)
+    {
+      base (client);
+    }
+
+  public override void run (string? command_string)
+    {
+      this.client.aggregator.individuals.foreach ((k, v) =>
+        {
+          Individual individual = (Individual) v;
+
+          foreach (Persona persona in individual.personas)
+            {
+              /* Either list all personas, or only list the one specified */
+              if (command_string != null && persona.uid != command_string)
+                continue;
+
+              Utils.print_persona (persona);
+
+              if (command_string == null)
+                Utils.print_line ("");
+            }
+        });
+    }
+
+  public override string[]? complete_subcommand (string subcommand)
+    {
+      /* @subcommand should be a persona UID */
+      return Readline.completion_matches (subcommand,
+          Utils.persona_uid_completion_cb);
+    }
+}
diff --git a/tools/inspect/command-quit.vala b/tools/inspect/command-quit.vala
new file mode 100644
index 0000000..7999507
--- /dev/null
+++ b/tools/inspect/command-quit.vala
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2010 Collabora Ltd.
+ *
+ * This library is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This library 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 library.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ *       Philip Withnall <philip withnall collabora co uk>
+ */
+
+using Folks;
+using GLib;
+
+private class Folks.Inspect.Commands.Quit : Folks.Inspect.Command
+{
+  public override string name
+    {
+      get { return "quit"; }
+    }
+
+  public override string description
+    {
+      get
+        {
+          return "Quit the program.";
+        }
+    }
+
+  public override string help
+    {
+      get
+        {
+          return "quit    Quit the program gracefully, like a cow lolloping " +
+              "across a green field.";
+        }
+    }
+
+  public Quit (Client client)
+    {
+      base (client);
+    }
+
+  public override void run (string? command_string)
+    {
+      Process.exit (0);
+    }
+}
diff --git a/tools/inspect/command-signals.vala b/tools/inspect/command-signals.vala
new file mode 100644
index 0000000..7030e46
--- /dev/null
+++ b/tools/inspect/command-signals.vala
@@ -0,0 +1,250 @@
+/*
+ * Copyright (C) 2010 Collabora Ltd.
+ *
+ * This library is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This library 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 library.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ *       Philip Withnall <philip withnall collabora co uk>
+ */
+
+using Folks;
+using Gee;
+using GLib;
+
+/*
+ * signals â?? list signals we're currently connected to
+ * signals connect ClassName â?? connect to all the signals on all the instances
+ *     of that class
+ * signals connect ClassName::signal â?? connect to the given signal on all the
+ *     instances of that class
+ * signals connect 0xdeadbeef â?? connect to all the signals on a particular class
+ *     instance
+ * signals connect 0xdeadbeef::signal â?? connect to the given signal on a
+ *     particular class instance
+ * signals disconnect (as above)
+ * signals disconnect â?? signal handler ID
+ * signals ClassName â?? list all the signals on all the instances of that class,
+ *     highlighting the ones we're currently connected to
+ * signals 0xdeadbeef â?? list all the signals on a particular class instance,
+ *     highlighting the ones we're currently connected to
+ * signals ClassName::signal â?? show the details of this signal
+ * signals 0xdeadbeef::signal â?? show the details of this signal
+ */
+
+private class Folks.Inspect.Commands.Signals : Folks.Inspect.Command
+{
+  public override string name
+    {
+      get { return "signals"; }
+    }
+
+  public override string description
+    {
+      get
+        {
+          return "Allow connection to and display of signals emitted by " +
+              "libfolks.";
+        }
+    }
+
+  public override string help
+    {
+      get
+        {
+          return "signals                                            " +
+              "List signals we're currently connected to.\n" +
+              "signals connect [class name]                       " +
+              "Connect to all the signals on all the instances of that " +
+              "class.\n" +
+              "signals connect [class name]::[signal name]        " +
+              "Connect to the given signal on all the instances of that " +
+              "class.\n" +
+              "signals connect [object pointer]                   " +
+              "Connect to all the signals on a particular class instance.\n" +
+              "signals connect [object pointer]::[signal name]    " +
+              "Connect to the given signal on a particular class instance.\n" +
+              "signals disconnect                                 " +
+              "(As for 'connect'.)\n" +
+              "signals [class name]                               " +
+              "List all the signals on all the instances of that class, " +
+              "highlighting the ones we're currently connected to.\n" +
+              "signals [object pointer]                           " +
+              "List all the signals on a particular class instance, " +
+              "highlighting the ones we're currently connected to.\n" +
+              "signals [class name]::[signal name]                " +
+              "Show the details of this signal.\n" +
+              "signals [object pointer]::[signal name]            " +
+              "Show the details of this signal.";
+        }
+    }
+
+  public Signals (Client client)
+    {
+      base (client);
+    }
+
+  public override void run (string? command_string)
+    {
+      if (command_string == null)
+        {
+          /* List all the signals we're connected to */
+          this.client.signal_manager.list_signals (Type.INVALID, null);
+        }
+      else
+        {
+          /* Parse subcommands */
+          string[] parts = command_string.split (" ", 2);
+
+          if (parts.length < 1)
+            {
+              Utils.print_line ("Unrecognised 'signals' command '%s'.",
+                command_string);
+              return;
+            }
+
+          Type class_type;
+          Object class_instance;
+          string signal_name;
+          string detail_string;
+
+          if (parts[0] == "connect" || parts[0] == "disconnect")
+            {
+              /* Connect to or disconnect from a signal */
+              if (parts[1] == null || parts[1].strip () == "")
+                {
+                  Utils.print_line ("Unrecognised signal identifier '%s'.",
+                      parts[1]);
+                  return;
+                }
+
+              if (this.parse_signal_id (parts[1].strip (), out class_type,
+                  out class_instance, out signal_name,
+                  out detail_string) == false)
+                {
+                  return;
+                }
+
+              /* FIXME: Handle "disconnect <signal ID>" */
+              if (parts[0] == "connect")
+                {
+                  uint signal_count =
+                      this.client.signal_manager.connect_to_signal (class_type,
+                          class_instance, signal_name, detail_string);
+                  Utils.print_line ("Connected to %u signals.", signal_count);
+                }
+              else
+                {
+                  uint signal_count =
+                      this.client.signal_manager.disconnect_from_signal (
+                          class_type, class_instance, signal_name,
+                              detail_string);
+                  Utils.print_line ("Disconnected from %u signals.",
+                      signal_count);
+                }
+            }
+          else
+            {
+              /* List some of the signals we're connected to, or display
+               * their details. */
+              if (this.parse_signal_id (parts[0].strip (), out class_type,
+                  out class_instance, out signal_name,
+                  out detail_string) == false)
+                {
+                  return;
+                }
+
+              if (signal_name == null)
+                {
+                  this.client.signal_manager.list_signals (class_type,
+                      class_instance);
+                }
+              else
+                {
+                  /* Get the class type from the instance */
+                  if (class_type == Type.INVALID)
+                    class_type = class_instance.get_type ();
+
+                  this.client.signal_manager.show_signal_details (class_type,
+                      signal_name, detail_string);
+                }
+            }
+        }
+    }
+
+  public override string[]? complete_subcommand (string subcommand)
+    {
+      /* @subcommand should be a backend name */
+      /* TODO */
+      return Readline.completion_matches (subcommand,
+          Utils.backend_name_completion_cb);
+    }
+
+  private bool parse_signal_id (string input,
+      out Type class_type,
+      out Object? class_instance,
+      out string? signal_name,
+      out string? detail_string)
+    {
+      /* We accept any of the following formats:
+       *  ClassName::signal-name
+       *  ClassName::signal-name::detail
+       *  0xdeadbeef::signal-name
+       *  0xdeadbeef::signal-name::detail
+       *  ClassName
+       *  0xdeadbeef
+       *
+       * We output exactly one of class_type and class_instance, and optionally
+       * output signal_name and/or detail_string as appropriate.
+       */
+      assert (input != null && input != "");
+
+      string[] parts = input.split ("::", 3);
+      string class_name_or_instance = parts[0];
+      string signal_name_inner = (parts.length > 1) ? parts[1] : null;
+      string detail_string_inner = (parts.length > 2) ? parts[2] : null;
+
+      if (signal_name == "" || detail_string == "")
+        {
+          Utils.print_line ("Invalid signal identifier '%s'.", input);
+          return false;
+        }
+
+      if (class_name_or_instance.length > 2 &&
+          class_name_or_instance[0] == '0' && class_name_or_instance[1] == 'x')
+        {
+          /* We have a class instance */
+          ulong address = class_name_or_instance.to_ulong (null, 16);
+          class_instance = (Object) address;
+          assert (class_instance.get_type ().is_object ());
+        }
+      else
+        {
+          /* We have a class name */
+          class_type = Type.from_name (class_name_or_instance);
+          if (class_type == Type.INVALID ||
+              (class_type.is_instantiatable () == false &&
+               class_type.is_interface () == false))
+            {
+              Utils.print_line ("Unrecognised class name '%s'.",
+                  class_name_or_instance);
+              return false;
+            }
+        }
+
+      signal_name = signal_name_inner;
+      detail_string = detail_string_inner;
+
+      return true;
+    }
+}
diff --git a/tools/inspect/inspect.vala b/tools/inspect/inspect.vala
new file mode 100644
index 0000000..57cf571
--- /dev/null
+++ b/tools/inspect/inspect.vala
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2010 Collabora Ltd.
+ *
+ * This library is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This library 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 library.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ *       Philip Withnall <philip withnall collabora co uk>
+ */
+
+using Folks.Inspect.Commands;
+using Folks;
+using Readline;
+using Gee;
+using GLib;
+
+/* We have to have a static global instance so that the readline callbacks can
+ * access its data, since they don't pass closures around. */
+static Inspect.Client main_client = null;
+
+public class Folks.Inspect.Client : Object
+{
+  public HashMap<string, Command> commands;
+  private MainLoop main_loop;
+  private unowned Thread folks_thread;
+  public IndividualAggregator aggregator { get; private set; }
+  public BackendStore backend_store { get; private set; }
+  public SignalManager signal_manager { get; private set; }
+
+  public static int main (string[] args)
+    {
+      main_client = new Client ();
+      main_client.run_interactive ();
+
+      return 0;
+    }
+
+  private void *folks_thread_main ()
+    {
+      this.main_loop = new MainLoop ();
+
+      this.signal_manager = new SignalManager ();
+
+      this.aggregator = new IndividualAggregator ();
+      this.aggregator.prepare ();
+
+      this.backend_store = BackendStore.dup ();
+      this.backend_store.backend_available.connect ((bs, b) => 
+        {
+          Backend backend = (Backend) b;
+
+          backend.prepare.begin ((obj, result) =>
+            {
+              try
+                {
+                  backend.prepare.end (result);
+                }
+              catch (GLib.Error e)
+                {
+                  warning ("Error preparing Backend '%s': %s", backend.name,
+                      e.message);
+                }
+            });
+        });
+
+      this.backend_store.load_backends ();
+
+      this.main_loop.run ();
+
+      return null;
+    }
+
+  public Client ()
+    {
+      Utils.init ();
+
+      this.commands = new HashMap<string, Command> (str_hash, str_equal);
+
+      /* Register the commands we support */
+      /* FIXME: This should be automatic */
+      this.commands.set ("quit", new Commands.Quit (this));
+      this.commands.set ("help", new Commands.Help (this));
+      this.commands.set ("individuals", new Commands.Individuals (this));
+      this.commands.set ("personas", new Commands.Personas (this));
+      this.commands.set ("backends", new Commands.Backends (this));
+      this.commands.set ("persona-stores", new Commands.PersonaStores (this));
+      this.commands.set ("signals", new Commands.Signals (this));
+
+      try
+        {
+          this.folks_thread = Thread<void*>.create<void*> (
+              this.folks_thread_main, true);
+        }
+      catch (ThreadError e)
+        {
+          stdout.printf ("Couldn't create aggregator thread: %s", e.message);
+          Process.exit (1);
+        }
+    }
+
+  public void run_interactive ()
+    {
+      /* Interactive mode: have a little shell which allows the data from
+       * libfolks to be browsed and edited in real time. */
+
+      /* Allow things to be set for folks-inspect in ~/.inputrc, and install our
+       * own completion function. */
+      Readline.readline_name = "folks-inspect";
+      Readline.attempted_completion_function = Client.completion_cb;
+
+      /* Main prompt loop */
+      while (true)
+        {
+          string command_line = Readline.readline ("> ");
+
+          if (command_line == null)
+            continue;
+
+          command_line = command_line.strip ();
+          if (command_line == "")
+            continue;
+
+          string subcommand;
+          string command_name;
+          Command command = this.parse_command_line (command_line,
+              out command_name, out subcommand);
+
+          /* Run the command */
+          if (command != null)
+            command.run (subcommand);
+          else
+            stdout.printf ("Unrecognised command '%s'.\n", command_name);
+
+          /* Store the command in the history, even if it failed */
+          Readline.History.add (command_line);
+        }
+    }
+
+  private static Command? parse_command_line (string command_line,
+      out string command_name,
+      out string? subcommand)
+    {
+      string[] parts = command_line.split (" ", 2);
+
+      if (parts.length < 1)
+        return null;
+
+      command_name = parts[0];
+      if (parts.length == 2 && parts[1] != "")
+        subcommand = parts[1];
+      else
+        subcommand = null;
+
+      /* Extract the first part of the command and see if it matches anything in
+       * this.commands */
+      return main_client.commands.get (parts[0]);
+    }
+
+  [CCode (array_length = false, array_null_terminated = true)]
+  private static string[]? completion_cb (string word,
+      int start,
+      int end)
+    {
+      /* word is the word to complete, and start and end are its bounds inside
+       * Readline.line_buffer, which contains the entire current line. */
+
+      /* Command name completion */
+      if (start == 0)
+        {
+          return Readline.completion_matches (word,
+              Utils.command_name_completion_cb);
+        }
+
+      /* Command parameter completion is passed off to the Command objects */
+      string command_name;
+      string subcommand;
+      Command command = Client.parse_command_line (Readline.line_buffer,
+          out command_name,
+          out subcommand);
+
+      if (command != null)
+        {
+          if (subcommand == null)
+            subcommand = "";
+          return command.complete_subcommand (subcommand);
+        }
+
+      return null;
+    }
+}
+
+private abstract class Folks.Inspect.Command
+{
+  protected Client client;
+
+  public Command (Client client)
+    {
+      this.client = client;
+    }
+
+  public abstract string name { get; }
+  public abstract string description { get; }
+  public abstract string help { get; }
+
+  public abstract void run (string? command_string);
+
+  [CCode (array_length = false, array_null_terminated = true)]
+  public virtual string[]? complete_subcommand (string subcommand)
+    {
+      /* Default implementation */
+      return null;
+    }
+}
diff --git a/tools/inspect/signal-manager.vala b/tools/inspect/signal-manager.vala
new file mode 100644
index 0000000..746e02e
--- /dev/null
+++ b/tools/inspect/signal-manager.vala
@@ -0,0 +1,502 @@
+/*
+ * Copyright (C) 2010 Collabora Ltd.
+ *
+ * This library is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This library 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 library.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ *       Philip Withnall <philip withnall collabora co uk>
+ */
+
+using Folks;
+using Gee;
+using GLib;
+
+public class Folks.Inspect.SignalManager : Object
+{
+  /* Map from class type â?? map from signal ID to hook ID */
+  private HashMap<Type, HashMap<uint, ulong>> signals_by_class_type;
+  /* Map from class instance â?? map from signal ID to hook ID */
+  private HashMap<Object, HashMap<uint, ulong>> signals_by_class_instance;
+
+  public SignalManager ()
+    {
+      this.signals_by_class_type =
+          new HashMap<Type, HashMap<uint, ulong>> ();
+      this.signals_by_class_instance =
+          new HashMap<Object, HashMap<uint, ulong>> ();
+    }
+
+  public void list_signals (Type class_type,
+      Object? class_instance)
+    {
+      if (class_type != Type.INVALID)
+        {
+          /* List the signals we're connected to via emission hooks on this
+           * class type */
+          HashMap<uint, ulong> hook_ids =
+              this.signals_by_class_type.get (class_type);
+
+          Utils.print_line ("Signals on all instances of class type '%s':",
+              class_type.name ());
+          Utils.indent ();
+          this.list_signals_for_type (class_type, hook_ids);
+          Utils.unindent ();
+        }
+      else if (class_instance != null)
+        {
+          /* List the signals we're connected to on this class instance */
+          HashMap<uint, ulong> signal_handler_ids =
+              this.signals_by_class_instance.get (class_instance);
+
+          Utils.print_line ("Signals on instance %p of class type '%s':",
+              class_instance, class_instance.get_type ().name ());
+          Utils.indent ();
+          this.list_signals_for_type (class_instance.get_type (),
+              signal_handler_ids);
+          Utils.unindent ();
+        }
+      else
+        {
+          /* List all the signals we're connected to on everything */
+          MapIterator<Type, HashMap<uint, ulong>> class_type_iter =
+              this.signals_by_class_type.map_iterator ();
+
+          Utils.print_line ("Connected signals on all instances of classes:");
+
+          Utils.indent ();
+          while (class_type_iter.next () == true)
+            {
+              HashMap<uint, ulong> hook_ids = class_type_iter.get_value ();
+              MapIterator<uint, ulong> hook_iter =  hook_ids.map_iterator ();
+
+              string class_name = class_type_iter.get_key ().name ();
+              while (hook_iter.next () == true)
+                {
+                  Utils.print_line ("%s::%s â?? connected", class_name,
+                      Signal.name (hook_iter.get_key ()));
+                }
+            }
+          Utils.unindent ();
+
+          MapIterator<Object, HashMap<uint, ulong>> class_instance_iter =
+              this.signals_by_class_instance.map_iterator ();
+
+          Utils.print_line ("Connected signals on specific instances of " +
+              "classes:");
+
+          Utils.indent ();
+          while (class_instance_iter.next () == true)
+            {
+              HashMap<uint, ulong> signal_handler_ids =
+                  class_instance_iter.get_value ();
+              MapIterator<uint, ulong> signal_handler_iter =
+                  signal_handler_ids.map_iterator ();
+
+              string class_name =
+                  class_instance_iter.get_key ().get_type ().name ();
+              while (signal_handler_iter.next () == true)
+                {
+                  Utils.print_line ("%s::%s â?? connected", class_name,
+                      Signal.name (signal_handler_iter.get_key ()));
+                }
+            }
+          Utils.unindent ();
+        }
+    }
+
+  public void show_signal_details (Type class_type,
+      string? signal_name,
+      string? detail_string)
+    {
+      uint signal_id = Signal.lookup (signal_name, class_type);
+      if (signal_id == 0)
+        {
+          Utils.print_line ("Unrecognised signal name '%s' on class '%s'.",
+              signal_name, class_type.name ());
+          return;
+        }
+
+      /* Query the signal's information */
+      SignalQuery query_info;
+      Signal.query (signal_id, out query_info);
+
+      /* Print the query response */
+      Utils.print_line ("Signal ID        %u", query_info.signal_id);
+      Utils.print_line ("Signal name      %s", query_info.signal_name);
+      Utils.print_line ("Emitting type    %s", query_info.itype.name ());
+      Utils.print_line ("Signal flags     %s",
+          this.signal_flags_to_string (query_info.signal_flags));
+      Utils.print_line ("Return type      %s", query_info.return_type.name ());
+      Utils.print_line ("Parameter types:");
+      Utils.indent ();
+      for (uint i = 0; i < query_info.n_params; i++)
+        Utils.print_line ("%-4u  %s", i, query_info.param_types[i].name ());
+      Utils.unindent ();
+    }
+
+  public uint connect_to_signal (Type class_type,
+      Object? class_instance,
+      string? signal_name,
+      string? detail_string)
+    {
+      /* We return the number of signals we connected to */
+      if (class_type != Type.INVALID && signal_name != null)
+        {
+          /* Connecting to a given signal on all instances of a class */
+          uint signal_id = Signal.lookup (signal_name, class_type);
+          if (signal_id == 0)
+            {
+              Utils.print_line ("Unrecognised signal name '%s' on class '%s'.",
+                  signal_name, class_type.name ());
+              return 0;
+            }
+
+          if (this.add_emission_hook (class_type, signal_id,
+              detail_string) == false)
+            {
+              Utils.print_line ("Not allowed to connect to signal '%s' on " +
+                  "class '%s'.", signal_name, class_type.name ());
+              return 0;
+            }
+
+          return 1;
+        }
+      else if (class_type != Type.INVALID && signal_name == null)
+        {
+          /* Connecting to all signals on all instances of a class */
+          uint[] signal_ids = Signal.list_ids (class_type);
+          uint signal_count = 0;
+
+          foreach (uint signal_id in signal_ids)
+            {
+              if (this.add_emission_hook (class_type, signal_id, null) == true)
+                signal_count++;
+            }
+
+          return signal_count;
+        }
+      else if (class_instance != null && signal_name != null)
+        {
+          /* Connecting to a given signal on a given class instance */
+          uint signal_id =
+              Signal.lookup (signal_name, class_instance.get_type ());
+          if (signal_id == 0)
+            {
+              Utils.print_line ("Unrecognised signal name '%s' on instance " +
+                  "%p of class '%s'.", signal_name, class_instance,
+                  class_instance.get_type ().name ());
+              return 0;
+            }
+
+          this.add_signal_handler (class_instance, signal_id, detail_string);
+
+          return 1;
+        }
+      else if (class_instance != null && signal_name == null)
+        {
+          /* Connecting to all signals on a given class instance */
+          uint[] signal_ids = Signal.list_ids (class_instance.get_type ());
+          uint signal_count = 0;
+
+          foreach (uint signal_id in signal_ids)
+            {
+              signal_count++;
+              this.add_signal_handler (class_instance, signal_id, null);
+            }
+
+          return signal_count;
+        }
+
+      assert_not_reached ();
+    }
+
+  public uint disconnect_from_signal (Type class_type,
+      Object? class_instance,
+      string? signal_name,
+      string? detail_string)
+    {
+      /* We return the number of signals we disconnected from */
+      if (class_type != Type.INVALID && signal_name != null)
+        {
+          /* Disconnecting from a given signal on all instances of a class */
+          uint signal_id = Signal.lookup (signal_name, class_type);
+          if (signal_id == 0)
+            {
+              Utils.print_line ("Unrecognised signal name '%s' on class '%s'.",
+                  signal_name, class_type.name ());
+              return 0;
+            }
+
+          if (this.remove_emission_hook (class_type, signal_id) == false)
+            {
+              Utils.print_line ("Could not remove hook for signal '%s' on " +
+                  "class '%s'.", signal_name, class_type.name ());
+              return 0;
+            }
+
+          return 1;
+        }
+      else if (class_type != Type.INVALID && signal_name == null)
+        {
+          /* Disconnecting from all signals on all instances of a class */
+          uint[] signal_ids = Signal.list_ids (class_type);
+          uint signal_count = 0;
+
+          foreach (uint signal_id in signal_ids)
+            {
+              if (this.remove_emission_hook (class_type, signal_id) == true)
+                signal_count--;
+            }
+
+          return signal_count;
+        }
+      else if (class_instance != null && signal_name != null)
+        {
+          /* Disconnecting from a given signal on a given class instance */
+          uint signal_id =
+              Signal.lookup (signal_name, class_instance.get_type ());
+          if (signal_id == 0)
+            {
+              Utils.print_line ("Unrecognised signal name '%s' on instance " +
+                  "%p of class '%s'.", signal_name, class_instance,
+                  class_instance.get_type ().name ());
+              return 0;
+            }
+
+          this.remove_signal_handler (class_instance, signal_id);
+
+          return 1;
+        }
+      else if (class_instance != null && signal_name == null)
+        {
+          /* Disconnecting from all signals on a given class instance */
+          uint[] signal_ids = Signal.list_ids (class_instance.get_type ());
+          uint signal_count = 0;
+
+          foreach (uint signal_id in signal_ids)
+            {
+              if (this.remove_signal_handler (class_instance, signal_id))
+                signal_count--;
+            }
+
+          return signal_count;
+        }
+
+      assert_not_reached ();
+    }
+
+  private void list_signals_for_type (Type type,
+      HashMap<uint, ulong>? signal_id_map)
+    {
+      uint[] signal_ids = Signal.list_ids (type);
+
+      /* Print information about the signals on this type */
+      if (signal_ids != null)
+        {
+          string type_name = type.name ();
+          foreach (uint signal_id in signal_ids)
+            {
+              unowned string signal_name = Signal.name (signal_id);
+
+              if (signal_id_map != null &&
+                  signal_id_map.has_key (signal_id) == true)
+                {
+                  Utils.print_line ("%s::%s â?? connected",
+                      type_name, signal_name);
+                }
+              else
+                {
+                  Utils.print_line ("%s::%s",
+                      type_name, signal_name);
+                }
+            }
+        }
+
+      /* Recurse to the type's interfaces */
+      Type[] interfaces = type.interfaces ();
+      foreach (Type interface_type in interfaces)
+        this.list_signals_for_type (interface_type, signal_id_map);
+
+      /* Chain up to the type's parent */
+      Type parent_type = type.parent ();
+      if (parent_type != Type.INVALID)
+        this.list_signals_for_type (parent_type, signal_id_map);
+    }
+
+  /* FIXME: This is necessary because if we do sizeof(Closure), Vala will
+   * generate the following C code: sizeof(GClosure*).
+   * This is not what we want. */
+  [CCode (cname = "sizeof (GClosure)")] extern const int CLOSURE_STRUCT_SIZE;
+
+  private void add_signal_handler (Object class_instance,
+      uint signal_id,
+      string? detail_string)
+    {
+      Closure closure = new Closure (this.CLOSURE_STRUCT_SIZE, this);
+      closure.set_meta_marshal (null, this.signal_meta_marshaller);
+
+      Quark detail_quark = 0;
+      if (detail_string != null)
+        detail_quark = Quark.try_string (detail_string);
+
+      ulong signal_handler_id = Signal.connect_closure_by_id (class_instance,
+          signal_id, detail_quark, closure, false);
+
+      /* Store the signal handler ID so we can list or remove it later */
+      HashMap<uint, ulong> signal_handler_ids =
+          this.signals_by_class_instance.get (class_instance);
+      if (signal_handler_ids == null)
+        {
+          signal_handler_ids = new HashMap<uint, ulong> ();
+          this.signals_by_class_instance.set (class_instance,
+              signal_handler_ids);
+        }
+
+      signal_handler_ids.set (signal_id, signal_handler_id);
+    }
+
+  private bool remove_signal_handler (Object class_instance,
+      uint signal_id)
+    {
+      HashMap<uint, ulong> signal_handler_ids =
+          this.signals_by_class_instance.get (class_instance);
+
+      if (signal_handler_ids == null ||
+          signal_handler_ids.has_key (signal_id) == false)
+        {
+          return false;
+        }
+
+      ulong signal_handler_id = signal_handler_ids.get (signal_id);
+      SignalHandler.disconnect (class_instance, signal_handler_id);
+      signal_handler_ids.unset (signal_id);
+
+      return true;
+    }
+
+  private static void signal_meta_marshaller (Closure closure,
+      out Value return_value,
+      Value[] param_values,
+      void *invocation_hint,
+      void *marshal_data)
+    {
+      SignalInvocationHint* hint = (SignalInvocationHint*) invocation_hint;
+
+      SignalQuery query_info;
+      Signal.query (hint->signal_id, out query_info);
+
+      Utils.print_line ("Signal '%s::%s' emitted with parameters:",
+          query_info.itype.name (), query_info.signal_name);
+
+      Utils.indent ();
+      uint i = 0;
+      foreach (Value param_value in param_values)
+        {
+          Utils.print_line ("%-4u  %-10s  %s", i++, param_value.type ().name (),
+              Utils.transform_value_to_string (param_value));
+        }
+      Utils.unindent ();
+    }
+
+  private bool add_emission_hook (Type class_type,
+      uint signal_id,
+      string? detail_string)
+    {
+      Quark detail_quark = 0;
+      if (detail_string != null)
+        detail_quark = Quark.try_string (detail_string);
+
+      /* Query the signal to check it supports emission hooks */
+      SignalQuery query;
+      Signal.query (signal_id, out query);
+
+      /* FIXME: It would be nice if we could find some way to support NO_HOOKS
+       * signals. */
+      if ((query.signal_flags & SignalFlags.NO_HOOKS) != 0)
+        return false;
+
+      ulong hook_id = Signal.add_emission_hook (signal_id,
+          detail_quark, this.emission_hook_cb, null);
+
+      /* Store the hook ID so we can list or remove it later */
+      HashMap<uint, ulong> hook_ids =
+          this.signals_by_class_type.get (class_type);
+      if (hook_ids == null)
+        {
+          hook_ids = new HashMap<uint, ulong> ();
+          this.signals_by_class_type.set (class_type, hook_ids);
+        }
+
+      hook_ids.set (signal_id, hook_id);
+
+      return true;
+    }
+
+  private bool remove_emission_hook (Type class_type,
+      uint signal_id)
+    {
+      HashMap<uint, ulong> hook_ids =
+          this.signals_by_class_type.get (class_type);
+
+      if (hook_ids == null || hook_ids.has_key (signal_id) == false)
+        return false;
+
+      ulong hook_id = hook_ids.get (signal_id);
+      Signal.remove_emission_hook (signal_id, hook_id);
+      hook_ids.unset (signal_id);
+
+      return true;
+    }
+
+  private bool emission_hook_cb (SignalInvocationHint hint,
+      Value[] param_values)
+    {
+      SignalQuery query_info;
+      Signal.query (hint.signal_id, out query_info);
+
+      Utils.print_line ("Signal '%s::%s' emitted with parameters:",
+          query_info.itype.name (), query_info.signal_name);
+
+      Utils.indent ();
+      uint i = 0;
+      foreach (Value param_value in param_values)
+        {
+          Utils.print_line ("%-4u  %-10s  %s", i++, param_value.type ().name (),
+              Utils.transform_value_to_string (param_value));
+        }
+      Utils.unindent ();
+
+      return true;
+    }
+
+  private static string signal_flags_to_string (SignalFlags flags)
+    {
+      string output = "";
+
+      if ((flags & SignalFlags.RUN_FIRST) != 0)
+        output += "G_SIGNAL_RUN_FIRST";
+      if ((flags & SignalFlags.RUN_LAST) != 0)
+        output += ((output != "") ? " | " : "") + "G_SIGNAL_RUN_LAST";
+      if ((flags & SignalFlags.RUN_CLEANUP) != 0)
+        output += ((output != "") ? " | " : "") + "G_SIGNAL_RUN_CLEANUP";
+      if ((flags & SignalFlags.DETAILED) != 0)
+        output += ((output != "") ? " | " : "") + "G_SIGNAL_DETAILED";
+      if ((flags & SignalFlags.ACTION) != 0)
+        output += ((output != "") ? " | " : "") + "G_SIGNAL_ACTION";
+      if ((flags & SignalFlags.NO_HOOKS) != 0)
+        output += ((output != "") ? " | " : "") + "G_SIGNAL_NO_HOOKS";
+
+      return output;
+    }
+}
diff --git a/tools/inspect/utils.vala b/tools/inspect/utils.vala
new file mode 100644
index 0000000..725006d
--- /dev/null
+++ b/tools/inspect/utils.vala
@@ -0,0 +1,477 @@
+/*
+ * Copyright (C) 2010 Collabora Ltd.
+ *
+ * This library is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This library 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 library.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ *       Philip Withnall <philip withnall collabora co uk>
+ */
+
+using Folks;
+using Gee;
+using GLib;
+
+private class Folks.Inspect.Utils
+{
+  /* The current indentation level, in spaces */
+  private static uint indentation = 0;
+  private static string indentation_string = "";
+
+  public static void init ()
+    {
+      Utils.indentation_string = "";
+
+      /* Register some general transformation functions */
+      Value.register_transform_func (typeof (Object), typeof (string),
+          Utils.transform_object_to_string);
+      Value.register_transform_func (typeof (Folks.PersonaStore),
+          typeof (string), Utils.transform_persona_store_to_string);
+      Value.register_transform_func (typeof (string[]), typeof (string),
+          Utils.transform_string_array_to_string);
+    }
+
+  private static void transform_object_to_string (Value src,
+      out Value dest)
+    {
+      /* FIXME: works around bgo#638363 */
+      Value dest_tmp = Value (typeof (string));
+      dest_tmp.take_string ("%p".printf (src.get_object ()));
+      dest = dest_tmp;
+    }
+
+  private static void transform_persona_store_to_string (Value src,
+      out Value dest)
+    {
+      /* FIXME: works around bgo#638363 */
+      Value dest_tmp = Value (typeof (string));
+      Folks.PersonaStore store = (Folks.PersonaStore) src.get_object ();
+      dest_tmp.take_string ("%p: %s, %s (%s)".printf (store, store.type_id,
+          store.id, store.display_name));
+      dest = dest_tmp;
+    }
+
+  private static void transform_string_array_to_string (Value src,
+      out Value dest)
+    {
+      /* FIXME: works around bgo#638363 */
+      Value dest_tmp = Value (typeof (string));
+      unowned string[] array = (string[]) src.get_boxed ();
+      string output = "{ ";
+      bool first = true;
+      foreach (string element in array)
+        {
+          if (first == false)
+            output += ", ";
+          output += "'%s'".printf (element);
+          first = false;
+        }
+      output += " }";
+      dest_tmp.take_string (output);
+      dest = dest_tmp;
+    }
+
+  public static void indent ()
+    {
+      /* We indent in increments of two spaces */
+      Utils.indentation += 2;
+      Utils.indentation_string = string.nfill (Utils.indentation, ' ');
+    }
+
+  public static void unindent ()
+    {
+      Utils.indentation -= 2;
+      Utils.indentation_string = string.nfill (Utils.indentation, ' ');
+    }
+
+  [PrintfFormat ()]
+  public static void print_line (string format, ...)
+    {
+      /* FIXME: store the va_list temporarily to work around bgo#638308 */
+      var valist = va_list ();
+      string output = format.vprintf (valist);
+      stdout.printf ("%s%s\n", Utils.indentation_string, output);
+    }
+
+  public static void print_individual (Individual individual,
+      bool show_personas)
+    {
+      Utils.print_line ("Individual '%s' with %u personas:",
+          individual.id, individual.personas.length ());
+
+      /* List the Individual's properties */
+      unowned ParamSpec[] properties =
+          individual.get_class ().list_properties ();
+
+      Utils.indent ();
+      foreach (unowned ParamSpec pspec in properties)
+        {
+          Value prop_value;
+          string output_string;
+
+          /* Ignore the personas property if we're printing the personas out */
+          if (show_personas == true && pspec.get_name () == "personas")
+            continue;
+
+          prop_value = Value (pspec.value_type);
+          individual.get_property (pspec.get_name (), ref prop_value);
+
+          output_string = Utils.property_to_string (individual.get_type (),
+              pspec.get_name (), prop_value);
+
+          Utils.print_line ("%-20s  %s", pspec.get_nick (), output_string);
+        }
+
+      if (show_personas == true)
+        {
+          Utils.print_line ("");
+          Utils.print_line ("Personas:");
+
+          Utils.indent ();
+          foreach (Persona persona in individual.personas)
+            Utils.print_persona (persona);
+          Utils.unindent ();
+        }
+      Utils.unindent ();
+    }
+
+  public static void print_persona (Persona persona)
+    {
+      Utils.print_line ("Persona '%s':", persona.uid);
+
+      /* List the Persona's properties */
+      unowned ParamSpec[] properties =
+          persona.get_class ().list_properties ();
+
+      Utils.indent ();
+      foreach (unowned ParamSpec pspec in properties)
+        {
+          Value prop_value;
+          string output_string;
+
+          prop_value = Value (pspec.value_type);
+          persona.get_property (pspec.get_name (), ref prop_value);
+
+          output_string = Utils.property_to_string (persona.get_type (),
+              pspec.get_name (), prop_value);
+
+          Utils.print_line ("%-20s  %s", pspec.get_nick (), output_string);
+        }
+      Utils.unindent ();
+    }
+
+  public static void print_persona_store (PersonaStore store,
+      bool show_personas)
+    {
+      Utils.print_line ("Persona store '%s' with %u personas:",
+          store.id, store.personas.size ());
+
+      /* List the store's properties */
+      unowned ParamSpec[] properties =
+          store.get_class ().list_properties ();
+
+      Utils.indent ();
+      foreach (unowned ParamSpec pspec in properties)
+        {
+          Value prop_value;
+          string output_string;
+
+          /* Ignore the personas property if we're printing the personas out */
+          if (show_personas == true && pspec.get_name () == "personas")
+            continue;
+
+          prop_value = Value (pspec.value_type);
+          store.get_property (pspec.get_name (), ref prop_value);
+
+          output_string = Utils.property_to_string (store.get_type (),
+              pspec.get_name (), prop_value);
+
+          Utils.print_line ("%-20s  %s", pspec.get_nick (), output_string);
+        }
+
+      if (show_personas == true)
+        {
+          Utils.print_line ("");
+          Utils.print_line ("Personas:");
+
+          Utils.indent ();
+          store.personas.foreach ((k, v) =>
+            {
+              Utils.print_persona ((Persona) v);
+            });
+          Utils.unindent ();
+        }
+      Utils.unindent ();
+    }
+
+  private static string property_to_string (Type object_type,
+      string prop_name,
+      Value prop_value)
+    {
+      string output_string;
+
+      /* Overrides for various known properties */
+      if (object_type.is_a (typeof (Individual)) && prop_name == "personas")
+        {
+          unowned GLib.List<Persona> personas =
+              (GLib.List<Persona>) prop_value.get_pointer ();
+          return "List of %u personas".printf (personas.length ());
+        }
+      else if (object_type.is_a (typeof (PersonaStore)) &&
+          prop_name == "personas")
+        {
+          unowned HashTable<string, Persona> personas =
+              (HashTable<string, Persona>) prop_value.get_boxed ();
+          return "Set of %u personas".printf (personas.size ());
+        }
+      else if (prop_name == "groups")
+        {
+          HashTable<string, bool> groups =
+              (HashTable<string, bool>) prop_value.get_boxed ();
+          output_string = "{ ";
+          bool first = true;
+
+          /* FIXME: This is rather inefficient */
+          groups.foreach ((k, v) =>
+            {
+              if ((bool) v == true)
+                {
+                  if (first == false)
+                    output_string += ", ";
+                  output_string += "'%s'".printf ((string) k);
+                  first = false;
+                }
+            });
+
+          output_string += " }";
+          return output_string;
+        }
+      else if (prop_name == "avatar")
+        {
+          return "%p".printf (prop_value.get_object ());
+        }
+      else if (prop_name == "im-addresses")
+        {
+          HashTable<string, GenericArray<string>> im_addresses =
+              (HashTable<string, GenericArray<string>>) prop_value.get_boxed ();
+          output_string = "{ ";
+          bool first = true;
+
+          /* FIXME: This is rather inefficient */
+          im_addresses.foreach ((k, v) =>
+            {
+              if (first == false)
+                output_string += ", ";
+              output_string += "'%s' : { ".printf ((string) k);
+              first = false;
+
+              GenericArray<string> addresses = (GenericArray<string>) v;
+              bool _first = true;
+              addresses.foreach ((a) =>
+                {
+                  if (_first == false)
+                    output_string += ", ";
+                  output_string += "'%s'".printf ((string) a);
+                  _first = false;
+                });
+
+              output_string += " }";
+            });
+
+          output_string += " }";
+          return output_string;
+        }
+
+      return Utils.transform_value_to_string (prop_value);
+    }
+
+  public static string transform_value_to_string (Value prop_value)
+    {
+      if (Value.type_transformable (prop_value.type (), typeof (string)))
+        {
+          /* Convert to a string value */
+          Value string_value = Value (typeof (string));
+          prop_value.transform (ref string_value);
+          return string_value.get_string ();
+        }
+      else
+        {
+          /* Can't convert the property value to a string */
+          return "Can't convert from type '%s' to '%s'".printf (
+              prop_value.type ().name (), typeof (string).name ());
+        }
+    }
+
+  /* FIXME: This can't be in the command_completion_cb() function because Vala
+   * doesn't allow static local variables. Erk. */
+  private static MapIterator<string, Command>? command_name_iter = null;
+
+  /* Complete a command name, starting with @word. */
+  public static string? command_name_completion_cb (string word,
+      int state)
+    {
+      /* Initialise state. Whoever wrote the readline API should be shot. */
+      if (state == 0)
+        Utils.command_name_iter = main_client.commands.map_iterator ();
+
+      while (Utils.command_name_iter.next () == true)
+        {
+          string command_name = Utils.command_name_iter.get_key ();
+          if (command_name.has_prefix (word))
+            return command_name;
+        }
+
+      /* Clean up */
+      Utils.command_name_iter = null;
+      return null;
+    }
+
+  /* FIXME: This can't be in the individual_id_completion_cb() function because
+   * Vala doesn't allow static local variables. Erk. */
+  private static HashTableIter<string, Individual>? individual_id_iter = null;
+
+  /* Complete an individual's ID, starting with @word. */
+  public static string? individual_id_completion_cb (string word,
+      int state)
+    {
+      /* Initialise state. Whoever wrote the readline API should be shot. */
+      if (state == 0)
+        {
+          Utils.individual_id_iter = HashTableIter<string, Individual> (
+              main_client.aggregator.individuals);
+        }
+
+      string id;
+      Individual individual;
+      while (Utils.individual_id_iter.next (out id, out individual) == true)
+        {
+          if (id.has_prefix (word))
+            return id;
+        }
+
+      /* Clean up */
+      Utils.individual_id_iter = null;
+      return null;
+    }
+
+  /* FIXME: This can't be in the individual_id_completion_cb() function because
+   * Vala doesn't allow static local variables. Erk. */
+  private static unowned GLib.List<Persona>? persona_uid_iter = null;
+
+  /* Complete an individual's ID, starting with @word. */
+  public static string? persona_uid_completion_cb (string word,
+      int state)
+    {
+      /* Initialise state. Whoever wrote the readline API should be shot. */
+      if (state == 0)
+        {
+          Utils.individual_id_iter = HashTableIter<string, Individual> (
+              main_client.aggregator.individuals);
+          Utils.persona_uid_iter = null;
+        }
+
+      Individual individual = null;
+      while (Utils.persona_uid_iter != null ||
+          Utils.individual_id_iter.next (null, out individual) == true)
+        {
+          if (Utils.persona_uid_iter == null)
+            {
+              assert (individual != null);
+              Utils.persona_uid_iter = individual.personas;
+            }
+
+          while (Utils.persona_uid_iter != null)
+            {
+              unowned Persona persona = (Persona) Utils.persona_uid_iter.data;
+              Utils.persona_uid_iter = Utils.persona_uid_iter.next;
+              if (persona.uid.has_prefix (word))
+                return persona.uid;
+            }
+        }
+
+      /* Clean up */
+      Utils.individual_id_iter = null;
+      return null;
+    }
+
+  /* FIXME: This can't be in the backend_name_completion_cb() function because
+   * Vala doesn't allow static local variables. Erk. */
+  private static Iterator<Backend>? backend_name_iter = null;
+
+  /* Complete an individual's ID, starting with @word. */
+  public static string? backend_name_completion_cb (string word,
+      int state)
+    {
+      /* Initialise state. Whoever wrote the readline API should be shot. */
+      if (state == 0)
+        {
+          Utils.backend_name_iter =
+              main_client.backend_store.list_backends ().iterator ();
+        }
+
+      while (Utils.backend_name_iter.next () == true)
+        {
+          Backend backend = Utils.backend_name_iter.get ();
+          if (backend.name.has_prefix (word))
+            return backend.name;
+        }
+
+      /* Clean up */
+      Utils.backend_name_iter = null;
+      return null;
+    }
+
+  /* FIXME: This can't be in the persona_store_id_completion_cb() function
+   * because Vala doesn't allow static local variables. Erk. */
+  private static HashTableIter<string, PersonaStore>? persona_store_id_iter =
+      null;
+
+  /* Complete a persona store's ID, starting with @word. */
+  public static string? persona_store_id_completion_cb (string word,
+      int state)
+    {
+      /* Initialise state. Whoever wrote the readline API should be shot. */
+      if (state == 0)
+        {
+          Utils.backend_name_iter =
+              main_client.backend_store.list_backends ().iterator ();
+          Utils.persona_store_id_iter = null;
+        }
+
+      while (Utils.persona_store_id_iter != null ||
+          Utils.backend_name_iter.next () == true)
+        {
+          if (Utils.persona_store_id_iter == null)
+            {
+              Backend backend = Utils.backend_name_iter.get ();
+              Utils.persona_store_id_iter =
+                  HashTableIter<string, PersonaStore> (backend.persona_stores);
+            }
+
+          string id;
+          PersonaStore store;
+          while (Utils.persona_store_id_iter.next (out id, out store) == true)
+            {
+              if (id.has_prefix (word))
+                return id;
+            }
+
+          /* Clean up */
+          Utils.persona_store_id_iter = null;
+        }
+
+      /* Clean up */
+      Utils.backend_name_iter = null;
+      return null;
+    }
+}



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