/* This is -*- C -*- */
/* $Id: guppi-plug-in-spec.c,v 1.19 2002/01/19 02:45:26 trow Exp $ */

/*
 * guppi-plug-in-spec.c
 *
 * Copyright (C) 2000 EMC Capital Management, Inc.
 *
 * Developed by Jon Trowbridge <trow@gnu.org> and
 * Havoc Pennington <hp@pobox.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 2 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
 * USA
 */

#include <config.h>
#include "guppi-plug-in-spec.h"

#include <stdio.h>
#include <dirent.h>
#include <ctype.h>
#include <string.h>

#include <glib.h>
#include <libgnome/gnome-defs.h>
#include <libgnome/gnome-util.h>
#include <libgnome/gnome-i18n.h>
#include <guppi-useful.h>


static GtkObjectClass *parent_class = NULL;

static void
guppi_plug_in_spec_finalize (GtkObject * obj)
{
  GuppiPlugInSpec *spec = GUPPI_PLUG_IN_SPEC (obj);

  guppi_finalized (obj);

  guppi_free0 (spec->path);

  guppi_free0 (spec->so_filename);

  guppi_free (spec->code);
  guppi_free (spec->type);
  guppi_free (spec->name);

  guppi_strfreev (spec->copyright_vec);
  guppi_strfreev (spec->author_vec);
  guppi_strfreev (spec->keyword_vec);
  guppi_strfreev (spec->depends_vec);
  guppi_strfreev (spec->provides_vec);

  guppi_strfreev (spec->exported_symbol_vec);

  guppi_free (spec->preloaded_scm_filename);
  guppi_free (spec->preloaded_py_filename);
  guppi_free (spec->so_filename);
  guppi_free (spec->icon);

  guppi_unref0 (spec->plug_in);

  if (parent_class->finalize)
    parent_class->finalize (obj);
}

static void
guppi_plug_in_spec_class_init (GuppiPlugInSpecClass * klass)
{
  GtkObjectClass *object_class = (GtkObjectClass *) klass;

  parent_class = gtk_type_class (GTK_TYPE_OBJECT);
  object_class->finalize = guppi_plug_in_spec_finalize;
}

static void
guppi_plug_in_spec_init (GuppiPlugInSpec * obj)
{

}

GtkType
guppi_plug_in_spec_get_type (void)
{
  static GtkType guppi_plug_in_spec_type = 0;
  if (!guppi_plug_in_spec_type) {
    static const GtkTypeInfo guppi_plug_in_spec_info = {
      "GuppiPlugInSpec",
      sizeof (GuppiPlugInSpec),
      sizeof (GuppiPlugInSpecClass),
      (GtkClassInitFunc) guppi_plug_in_spec_class_init,
      (GtkObjectInitFunc) guppi_plug_in_spec_init,
      NULL, NULL, (GtkClassInitFunc) NULL
    };
    guppi_plug_in_spec_type =
      gtk_type_unique (GTK_TYPE_OBJECT, &guppi_plug_in_spec_info);
  }
  return guppi_plug_in_spec_type;
}

static gchar **
list2vec (GList * lst)
{
  gchar **vec = NULL;
  gint i = 0;

  vec = guppi_new0 (gchar *, g_list_length (lst) + 1);
  while (lst != NULL) {
    vec[i] = (gchar *) lst->data;
    lst->data = NULL;
    lst = g_list_next (lst);
    ++i;
  }

  g_list_free (lst);

  return vec;
}

static gboolean
parse_version (const gchar * txt, gint * major, gint * minor, gint * micro)
{
  if (sscanf (txt, "%d.%d.%d", major, minor, micro) == 3)
    return TRUE;
  else if (sscanf (txt, "%d.%d", major, minor) == 2) {
    *micro = 0;
    return TRUE;
  }
  return FALSE;
}

static gchar *
plug_in_filename (const gchar * path, const gchar * name)
{
  gchar *str;
  gchar *path2;

  str = g_concat_dir_and_file (path, name);
  guppi_outside_alloc (str);
  if (g_file_exists (str))
    return str;

  guppi_free (str);

  path2 = guppi_strdup_printf ("%s%c.libs", path, G_DIR_SEPARATOR);
  str = g_concat_dir_and_file (path2, name);
  guppi_outside_alloc (str);
  guppi_free (path2);

  if (g_file_exists (str))
    return str;

  guppi_free (str);

  return NULL;
}

enum {
  SPEC_CODE,
  SPEC_TYPE,
  SPEC_NAME,
  SPEC_COMMENT,
  SPEC_VERSION,
  SPEC_COPYRIGHT,
  SPEC_AUTHOR,
  SPEC_KEYWORD,
  SPEC_DEPENDS,
  SPEC_PROVIDES,
  SPEC_EXPORTED_SYMBOL,
  SPEC_PRELOADED_SCM,
  SPEC_PRELOADED_PYTHON,
  SPEC_PLUGIN,
  SPEC_ICON,
  LAST_SPEC
};

const gchar *field_name[LAST_SPEC] = {
  "Code",
  "Type",
  "Name",
  "Comment",
  "Version",
  "Copyright",
  "Author",
  "Keyword",
  "Depends",
  "Provides",
  "ExportedSymbol",
  "PreloadedScheme",
  "PreloadedPython",
  "PlugIn",
  "Icon"
};

static gboolean
parse_spec_file (GuppiPlugInSpec * spec, const gchar * filename)
{
  FILE *in;
  gchar buffer[512];
  GList *copyright_list = NULL;
  GList *author_list = NULL;
  GList *keyword_list = NULL;
  GList *depends_list = NULL;
  GList *provides_list = NULL;
  GList *export_symb_list = NULL;
  gchar *lingua = NULL;

  /*
   * Lingua currently never gets set to anything non-NULL.
   * This obviously needs to be fixed.
   */

  in = fopen (filename, "r");
  if (in == NULL)
    return FALSE;
  spec->path = g_dirname (filename);
  guppi_outside_alloc (spec->path);

  if (!fgets (buffer, 512, in)) {
    /* empty file? */
    fclose (in);
    return FALSE;
  }

  if (!g_strcasecmp (buffer, "[Guppi Plug-in]")) {
    fclose (in);
    return FALSE;
  }

  while (fgets (buffer, 512, in)) {
    gchar *p;
    gchar *q;
    gchar *key;
    gchar *value;
    gchar *line_lingua;
    gint type;
    gboolean supports_lingua;

    g_strstrip (buffer);

    /* I know, I know... this is almost as bad a using "goto" */
    if (buffer[0] == '\0')
      continue;

    p = strchr (buffer, '=');
    if (p == NULL) {
      g_warning ("Bad line: %s", buffer);
      fclose (in);
      return FALSE;
    }

    *p = '\0';
    value = p + 1;

    key = guppi_strdup (buffer);
    line_lingua = NULL;
    p = strrchr (key, '[');
    q = strrchr (key, ']');
    if (p != NULL && q != NULL && p < q) {
      *p = '\0';
      *q = '\0';
      line_lingua = p + 1;
    }

    for (type = 0; type < LAST_SPEC && g_strcasecmp (key, field_name[type]);
	 ++type);

    if (type == LAST_SPEC) {
      g_warning ("Bad key: %s", key);
      fclose (in);
      return FALSE;
    }

    supports_lingua = (type == SPEC_NAME ||
		       type == SPEC_COMMENT || type == SPEC_KEYWORD);

    if (line_lingua && !supports_lingua) {
      g_warning ("Ignoring language tag [%s] on key \"%s\"", line_lingua,
		 key);
      guppi_free (line_lingua);
      line_lingua = NULL;
    }

    if (!supports_lingua ||
	((line_lingua == NULL && lingua == NULL) ||
	 (line_lingua != NULL && lingua != NULL &&
	  !g_strcasecmp (line_lingua, lingua)))) {

      switch (type) {

      case SPEC_CODE:
	spec->code = guppi_strdup (value);
	break;

      case SPEC_TYPE:
	spec->type = guppi_strdup (value);
	break;

      case SPEC_NAME:
	spec->name = guppi_strdup (value);
	break;

      case SPEC_COMMENT:
	spec->comment = guppi_strdup (value);
	break;

      case SPEC_VERSION:
	g_assert (parse_version (value,
				 &spec->major_version,
				 &spec->minor_version, &spec->micro_version));
	break;

      case SPEC_COPYRIGHT:
	copyright_list = g_list_append (copyright_list, guppi_strdup (value));
	break;

      case SPEC_AUTHOR:
	author_list = g_list_append (author_list, guppi_strdup (value));
	break;

      case SPEC_KEYWORD:
	keyword_list = g_list_append (keyword_list, guppi_strdup (value));
	break;

      case SPEC_DEPENDS:
	depends_list = g_list_append (keyword_list, guppi_strdup (value));
	break;

      case SPEC_PROVIDES:
	provides_list = g_list_append (provides_list, guppi_strdup (value));
	break;

      case SPEC_EXPORTED_SYMBOL:
	export_symb_list = g_list_append (export_symb_list, guppi_strdup (value));
	break;

      case SPEC_PRELOADED_SCM:
	spec->preloaded_scm_filename = guppi_strdup (value);
	break;

      case SPEC_PRELOADED_PYTHON:
	spec->preloaded_py_filename = guppi_strdup (value);
	break;

      case SPEC_PLUGIN:
	spec->so_filename = guppi_strdup (value);
	break;

      case SPEC_ICON:
	spec->icon = guppi_strdup (value);
	break;

      default:
	g_assert_not_reached ();
      }
    }

    guppi_free (key);
  }

  spec->copyright_vec = list2vec (copyright_list);
  spec->author_vec = list2vec (author_list);
  spec->keyword_vec = list2vec (keyword_list);
  spec->depends_vec = list2vec (depends_list);
  spec->provides_vec = list2vec (provides_list);
  spec->exported_symbol_vec = list2vec (export_symb_list);

  fclose (in);
  return TRUE;
}

static gboolean
validate_spec (GuppiPlugInSpec * spec)
{
  gchar *s;

  if (spec->code == NULL) {
    g_print ("No code! (%s)\n", spec->path);
    return FALSE;
  }

  if (spec->type == NULL) {
    g_print ("No type! (%s)\n", spec->path);
    return FALSE;
  }

  if (spec->name == NULL) {
    g_print ("No name! (%s)\n", spec->path);
    return FALSE;
  }

  /* Make sure the plug-in file exists. */
  if (spec->so_filename == NULL) {
    g_print ("No plug-in specified (%s)\n", spec->path);
    return FALSE;
  }

  s = plug_in_filename (spec->path, spec->so_filename);
  if (s == NULL) {
    g_print ("Plug-in %s not found\n", spec->so_filename);
    return FALSE;
  }
  guppi_free (s);


  return TRUE;
}

static void
process_spec_exported_symbols (GuppiPlugInSpec *spec)
{
  if (!spec->exported_symbol_vec)
    return;
#if 0
  for (i=0; spec->exported_symbol_vec[i]; ++i) {
    if (guppi_supports_guile () && guppi_guile_is_active ()) {
      guppi_scm_define_autoloaded_symbol (spec->exported_symbol_vec[i],
					  spec->type, spec->code);
    }
  }
#endif
}

static void
do_spec_preloads (GuppiPlugInSpec * spec)
{
  guppi_pixmap_path_add (spec->path);
  /* guppi_script_path_add (spec->path); */
  guppi_glade_path_add (spec->path);

#if 0
  if (spec->preloaded_scm_filename && guppi_supports_guile ()) {
    gchar *path = g_concat_dir_and_file (spec->path,
					 spec->preloaded_scm_filename);
    guppi_outside_alloc (path);
    if (g_file_exists (path) && guppi_file_is_guile_script (path))
      guppi_execute_script (path);
    else
      g_warning ("Couldn't preload \"%s\" as scheme", path);
    guppi_free (path);
  }

  if (spec->preloaded_py_filename && guppi_supports_python ()) {
    gchar *path = g_concat_dir_and_file (spec->path,
					 spec->preloaded_py_filename);
    guppi_outside_alloc (path);
    if (g_file_exists (path) && guppi_file_is_python_script (path))
      guppi_execute_script (path);
    else
      g_warning ("Couldn't preload \"%s\" as python", path);
    guppi_free (path);
  }
#endif

}

GuppiPlugInSpec *
guppi_plug_in_spec_new (const gchar * filename)
{
  GuppiPlugInSpec *spec;

  g_return_val_if_fail (filename != NULL, NULL);

  spec = GUPPI_PLUG_IN_SPEC (guppi_type_new (guppi_plug_in_spec_get_type ()));

  if (!parse_spec_file (spec, filename) || !validate_spec (spec)) {
    guppi_unref (spec);
    return NULL;
  }

  process_spec_exported_symbols (spec);
  do_spec_preloads (spec);

  return spec;
}

GuppiPlugIn *
guppi_plug_in_spec_plug_in (GuppiPlugInSpec * spec)
{
  g_return_val_if_fail (spec != NULL && GUPPI_IS_PLUG_IN_SPEC (spec), NULL);

  if (spec->plug_in == NULL) {
    gchar *path;

    path = plug_in_filename (spec->path, spec->so_filename);

    if (path == NULL) {
      g_print ("Plug-in %s (%s) not found.\n", spec->so_filename, spec->code);
      return NULL;
    }

    spec->plug_in = guppi_plug_in_load (path);
    spec->plug_in->spec = spec;
    guppi_free (path);
  }

  return spec->plug_in;
}


/**********************************************************************/

static GHashTable *plug_in_table = NULL;

static gint
version_compare (const GuppiPlugInSpec * a, const GuppiPlugInSpec * b)
{
  if (a->major_version < b->major_version)
    return -1;
  else if (a->major_version > b->major_version)
    return +1;
  else if (a->minor_version < b->minor_version)
    return -1;
  else if (a->minor_version > b->minor_version)
    return +1;
  else if (a->micro_version < b->micro_version)
    return -1;
  else if (a->micro_version > b->micro_version)
    return +1;
  else
    return 0;
}

static void
register_plug_in (GuppiPlugInSpec * info)
{
  gpointer data;
  GuppiPlugInSpec *ai;
  GHashTable *type_table;
  gint rv;

  g_return_if_fail (info != NULL);

  g_return_if_fail (info->type != NULL);
  g_return_if_fail (info->code != NULL);
  g_return_if_fail (info->name != NULL);

  if (plug_in_table == NULL)
    plug_in_table = g_hash_table_new (g_str_hash, g_str_equal);

  type_table = (GHashTable *) g_hash_table_lookup (plug_in_table, info->type);
  if (type_table == NULL) {
    type_table = g_hash_table_new (g_str_hash, g_str_equal);
    g_hash_table_insert (plug_in_table, (gchar *) info->type, type_table);
  }

  data = g_hash_table_lookup (type_table, info->code);
  if (data != NULL) {

    ai = GUPPI_PLUG_IN_SPEC (data);
    g_assert (ai != NULL);
    rv = version_compare (ai, info);

    if (rv == -1) {
      /* The already-loaded version is older */
      g_message ("Replacing %s %d.%d.%d with %d.%d.%d",
		 info->name,
		 info->major_version, info->minor_version,
		 info->micro_version, ai->major_version, ai->minor_version,
		 ai->micro_version);

      g_hash_table_remove (type_table, info->code);

    } else {
      /* The already-loaded version is newer or the same */

      g_message ("Skipping %s %d.%d.%d",
		 info->name,
		 info->major_version, info->minor_version,
		 info->micro_version);
      return;
    }
  }

  g_hash_table_insert (type_table, (gchar *) info->code, info);
}

GuppiPlugInSpec *
guppi_plug_in_spec_lookup (const gchar * type, const gchar * code)
{
  gpointer data;

  g_return_val_if_fail (type != NULL, NULL);
  g_return_val_if_fail (code != NULL, NULL);

  if (plug_in_table == NULL)
    return NULL;

  data = g_hash_table_lookup (plug_in_table, type);
  if (data == NULL)
    return NULL;
  data = g_hash_table_lookup ((GHashTable *) data, code);
  if (data == NULL)
    return NULL;

  return GUPPI_PLUG_IN_SPEC (data);
}

GuppiPlugIn *
guppi_plug_in_lookup (const gchar * type, const gchar * code)
{
  GuppiPlugInSpec *spec;

  g_return_val_if_fail (type != NULL, NULL);
  g_return_val_if_fail (code != NULL, NULL);

  spec = guppi_plug_in_spec_lookup (type, code);
  if (spec == NULL)
    return NULL;

  return guppi_plug_in_spec_plug_in (spec);
}

/***********************************************************************/

gboolean
guppi_plug_in_exists (const gchar * type, const gchar * code)
{
  g_return_val_if_fail (type != NULL && code != NULL, FALSE);

  return guppi_plug_in_spec_lookup (type, code) != NULL;
}

gboolean
guppi_plug_in_is_loaded (const gchar * type, const gchar * code)
{
  GuppiPlugInSpec *spec;
  g_return_val_if_fail (type != NULL && code != NULL, FALSE);
  spec = guppi_plug_in_spec_lookup (type, code);
  return spec && spec->plug_in;
}

void
guppi_plug_in_force_load (const gchar * type, const gchar * code)
{
  g_return_if_fail (type != NULL && code != NULL);
  guppi_plug_in_lookup (type, code);
}

/***********************************************************************/

static GList *plug_in_dirs = NULL;

static void
guppi_plug_in_path_clear (void)
{
  GList *iter = plug_in_dirs;
  while (iter != NULL) {
    guppi_free (iter->data);
    iter = g_list_next (iter);
  }
  g_list_free (plug_in_dirs);
  plug_in_dirs = NULL;
}

static GList *
guppi_plug_in_path_split (const gchar * path)
{
  const gchar *start;
  const gchar *curr;
  GList *pathlist = NULL;

  if (path == NULL)
    return NULL;

  start = path;
  while (*start != '\0') {

    while (*start == ':')
      ++start;

    curr = start;
    while (*curr != ':' && *curr != '\0')
      ++curr;

    if (start != curr)
      pathlist = g_list_append (pathlist, guppi_strndup (start, curr - start));

    start = curr;
  }

  return pathlist;
}

void
guppi_plug_in_path_set (const gchar * path)
{
  g_return_if_fail (path != NULL);
  guppi_plug_in_path_clear ();
  plug_in_dirs = guppi_plug_in_path_split (path);
}

void
guppi_plug_in_path_prepend (const gchar * path)
{
  g_return_if_fail (path != NULL);
  plug_in_dirs =
    g_list_concat (guppi_plug_in_path_split (path), plug_in_dirs);
}

void
guppi_plug_in_path_append (const gchar * path)
{
  g_return_if_fail (path != NULL);
  plug_in_dirs =
    g_list_concat (plug_in_dirs, guppi_plug_in_path_split (path));
}

void
guppi_plug_in_path_reset_to_default (void)
{
  gchar *env;

  guppi_plug_in_path_clear ();

  if (development_path_hacks ())
    guppi_plug_in_path_append ("../../plug-ins");

  env = getenv ("GUPPI_PLUGIN_PATH");
  if (env)
    guppi_plug_in_path_append (env);

#ifdef GUPPI_PLUGINS
  if (!development_path_hacks ())
    guppi_plug_in_path_append (GUPPI_PLUGINS);
#endif

}

void
guppi_plug_in_path_dump (void)
{
  GList *iter = plug_in_dirs;

  g_print (_("Plug-in Search Path:"));
  g_print ("\n");
  if (iter == NULL)
    g_print ("    <none>\n");
  while (iter != NULL) {
    g_print ("    %s\n", (gchar *) iter->data);
    iter = g_list_next (iter);
  }
  g_print ("\n");
}

/***********************************************************************/

void
guppi_plug_in_spec_find (const gchar * path, gboolean recurse)
{
  DIR *dir;
  struct dirent *dirent;

  g_return_if_fail (path != NULL);

  if (!g_file_test (path, G_FILE_TEST_ISDIR))
    return;

  dir = opendir (path);
  if (dir == NULL) {
    g_message ("couldn't open %s", path);
    return;
  }

  while ((dirent = readdir (dir)) != NULL) {

    gchar *full_name;

    full_name = g_concat_dir_and_file (path, dirent->d_name);

    if (recurse &&
	strcmp (dirent->d_name, ".") &&
	strcmp (dirent->d_name, "..") &&
	strcmp (dirent->d_name, "CVS") &&
	strcmp (dirent->d_name, ".deps") &&
	strcmp (dirent->d_name, ".libs") &&
	g_file_test (full_name, G_FILE_TEST_ISDIR)) {

      guppi_plug_in_spec_find (full_name, recurse);

    } else if (g_file_test (full_name, G_FILE_TEST_ISFILE) &&
	       !strcmp (g_extension_pointer (dirent->d_name), "plugin")) {
      GuppiPlugInSpec *spec;

      spec = guppi_plug_in_spec_new (full_name);

      if (spec != NULL)
	register_plug_in (spec);
    }

    g_free (full_name);

  }

  closedir (dir);
}

void
guppi_plug_in_spec_find_all (void)
{
  GList *i = plug_in_dirs;
  while (i != NULL) {
    guppi_plug_in_spec_find ((gchar *) (i->data), TRUE);
    i = g_list_next (i);
  }
}

void
guppi_plug_in_load_all (void)
{
  guppi_plug_in_spec_find_all ();
}

static void
load_by_type_fn (GuppiPlugInSpec * spec, gpointer ptr)
{
  guppi_plug_in_spec_plug_in (spec);
}

void
guppi_plug_in_spec_load_by_type (const gchar * type)
{
  g_return_if_fail (type != NULL);
  guppi_plug_in_spec_foreach_of_type (type, load_by_type_fn, NULL);
}

/***********************************************************************/

struct foreach_info {
  GuppiPlugInSpecFn func;
  gpointer data;
};

static void
hfunc_inner (gpointer key, gpointer val, gpointer user_data)
{
  struct foreach_info *fi = (struct foreach_info *) user_data;
  g_return_if_fail (GUPPI_IS_PLUG_IN_SPEC (val));
  (fi->func) (GUPPI_PLUG_IN_SPEC (val), fi->data);
}

static void
hfunc_outer (gpointer key, gpointer val, gpointer user_data)
{
  g_hash_table_foreach ((GHashTable *) val, hfunc_inner, user_data);
}

void
guppi_plug_in_spec_foreach (GuppiPlugInSpecFn func, gpointer data)
{
  struct foreach_info fi;
  fi.func = func;
  fi.data = data;

  g_return_if_fail (func != NULL);

  if (plug_in_table == NULL)	/* no plug-ins available */
    return;

  g_hash_table_foreach (plug_in_table, hfunc_outer, &fi);
}

void
guppi_plug_in_spec_foreach_of_type (const gchar * type,
				    GuppiPlugInSpecFn func, gpointer data)
{
  GHashTable *subtable;
  struct foreach_info fi;
  fi.func = func;
  fi.data = data;

  g_return_if_fail (type != NULL);
  g_return_if_fail (func != NULL);

  if (plug_in_table == NULL)	/* no plug-ins available */
    return;

  subtable = (GHashTable *) g_hash_table_lookup (plug_in_table, type);
  if (subtable != NULL)
    g_hash_table_foreach (subtable, hfunc_inner, &fi);
}

static void
incr (GuppiPlugInSpec * foo, gpointer x)
{
  ++*(gint *) x;
}

gint
guppi_plug_in_count (void)
{
  gint x = 0;
  guppi_plug_in_spec_foreach (incr, &x);
  return x;
}

gint
guppi_plug_in_count_by_type (const gchar * type)
{
  gint x = 0;
  g_return_val_if_fail (type != NULL, 0);
  guppi_plug_in_spec_foreach_of_type (type, incr, &x);
  return x;
}

static void
shutdown_iter_fn (gpointer key, gpointer val, gpointer data)
{
  guppi_unref (val);
}

static void
shutdown_iter_fn_outer (gpointer key, gpointer val, gpointer data)
{
  g_hash_table_foreach ((GHashTable *) val, shutdown_iter_fn, NULL);
  g_hash_table_destroy ((GHashTable *) val);
}

void
guppi_plug_in_spec_shutdown (gpointer ignored)
{
  guppi_plug_in_path_clear ();

  if (plug_in_table != NULL) {
    g_hash_table_foreach (plug_in_table, shutdown_iter_fn_outer, NULL);
    g_hash_table_destroy (plug_in_table);
    plug_in_table = NULL;
  }
}


/* $Id: guppi-plug-in-spec.c,v 1.19 2002/01/19 02:45:26 trow Exp $ */


syntax highlighted by Code2HTML, v. 0.9.1