preliminary support for ovr edits within mmenu
[pandora-libraries.git] / minimenu / mmcat.c
index de4db3a..b3573f7 100644 (file)
@@ -2,21 +2,44 @@
 #include <string.h>
 #include <strings.h>
 #include <stdlib.h>
+#include <sys/types.h>
+#include <dirent.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <ctype.h>
 
 #include "pnd_conf.h"
 #include "pnd_logger.h"
 #include "pnd_pxml.h"
 #include "pnd_container.h"
 #include "pnd_discovery.h"
+#include "../lib/pnd_pathiter.h"
 
 #include "mmenu.h"
 #include "mmcache.h"
 #include "mmcat.h"
+#include "freedesktop_cats.h"
 
-mm_category_t g_categories [ MAX_CATS ];
-unsigned char g_categorycount = 0;
+// all categories known -- visible, hidden, subcats, whatever.
+pnd_box_handle m_categories = NULL;
+unsigned int m_categorycount = 0;
 
-unsigned char category_push ( char *catname, pnd_disco_t *app ) {
+// all categories being published right now -- a subset copied from m_categories
+mm_category_t *g_categories [ MAX_CATS ];
+unsigned char g_categorycount;
+
+// category mappings
+mm_catmap_t g_catmaps [ MAX_CATS ];
+unsigned char g_catmapcount = 0;
+
+extern pnd_conf_handle g_conf;
+
+void category_init ( void ) {
+  m_categories = pnd_box_new ( "Schrodinger's cat" );
+  return;
+}
+
+unsigned char category_push ( char *catname, char *parentcatname, pnd_disco_t *app, pnd_conf_handle ovrh, char *fspath, unsigned char visiblep ) {
   mm_category_t *c;
 
   // check category list; if found, append app to the end of it.
@@ -28,15 +51,40 @@ unsigned char category_push ( char *catname, pnd_disco_t *app ) {
   // find or create category
   //
 
-  if ( ( c = category_query ( catname ) ) ) {
+  if ( ( c = pnd_box_find_by_key ( m_categories, catname ) ) ) {
     // category was found..
   } else {
     // category wasn't found..
-    pnd_log ( PND_LOG_DEFAULT, "New category '%s'\n", catname );
-    g_categories [ g_categorycount ].catname = strdup ( catname );
-    g_categories [ g_categorycount ].refs = NULL;
-    c = &(g_categories [ g_categorycount ]);
-    g_categorycount++;
+    //pnd_log ( PND_LOG_DEFAULT, "New category '%s'\n", catname );
+    c = pnd_box_allocinsert ( m_categories, catname, sizeof(mm_category_t) );
+    c -> catname = strdup ( catname );
+    if ( parentcatname ) {
+      c -> parent_catname = strdup ( parentcatname );
+    }
+
+    if ( visiblep ) {
+      c -> catflags = CFNORMAL;
+    } else {
+      c -> catflags = CFHIDDEN;
+    }
+
+    // if user prefers subcats-as-folders, lets reflag this sucker
+    if ( parentcatname && pnd_conf_get_as_int_d ( g_conf, "tabs.subcat_as_folders", 1 ) ) {
+      //printf ( "subcat: %s parent: %s\n", catname, parentcatname ? parentcatname : "none" );
+      c -> catflags = CFSUBCAT;
+    }
+
+    c -> refs = NULL;
+
+    if ( fspath ) {
+      c -> fspath = strdup ( fspath );
+    }
+
+    m_categorycount++;
+  }
+
+  if ( ! app ) {
+    return ( 1 ); // create cat, but skip app
   }
 
   // alloc and populate appref
@@ -49,6 +97,7 @@ unsigned char category_push ( char *catname, pnd_disco_t *app ) {
   bzero ( ar, sizeof(mm_appref_t) );
 
   ar -> ref = app;
+  ar -> ovrh = ovrh;
 
   // plug it into category
   //   and sort it on insert!
@@ -64,7 +113,7 @@ unsigned char category_push ( char *catname, pnd_disco_t *app ) {
     while ( iter ) {
 
       if ( iter -> ref -> title_en ) {
-       if ( strcmp ( ar -> ref -> title_en, iter -> ref -> title_en ) < 0 ) {
+       if ( cat_sort_score ( c, ar, iter ) < 0 ) {
          // new guy is smaller than the current guy!
          break;
        }
@@ -101,36 +150,699 @@ unsigned char category_push ( char *catname, pnd_disco_t *app ) {
   return ( 1 );
 }
 
-mm_category_t *category_query ( char *catname ) {
-  unsigned char i;
+int cat_sort_score ( mm_category_t *cat, mm_appref_t *s1, mm_appref_t *s2 ) {
 
-  for ( i = 0; i < g_categorycount; i++ ) {
+  // are we in a directory browser, or looking at pnd-files?
+  if ( cat -> fspath ) {
+    // directory browser mode
 
-    if ( strcasecmp ( g_categories [ i ].catname, catname ) == 0 ) {
-      return ( &(g_categories [ i ]) );
+    if ( s1 == s2 ) {
+      return ( 0 ); // equal
+
+    } else if ( s1 -> ref -> object_type == pnd_object_type_directory &&
+               s2 -> ref -> object_type == pnd_object_type_directory )
+    {
+      // both are directories, be nice
+      return ( strcmp ( s1 -> ref -> title_en, s2 -> ref -> title_en ) );
+    } else if ( s1 -> ref -> object_type == pnd_object_type_directory &&
+               s2 -> ref -> object_type != pnd_object_type_directory )
+    {
+      return ( -1 ); // dir on the left is earlier than file on the right
+    } else if ( s1 -> ref -> object_type != pnd_object_type_directory &&
+               s2 -> ref -> object_type == pnd_object_type_directory )
+    {
+      return ( 1 ); // dir on the right is earlier than file on the left
+    } else {
+      // file on file
+      return ( strcmp ( s1 -> ref -> title_en, s2 -> ref -> title_en ) );
     }
 
   }
 
-  return ( NULL );
+  // pnd tab
+  //
+
+  // if this is comparing subcat folder to subcat folder, or pnd to pnd, or pnd to subcat folder?
+  unsigned char s1sub = 0;
+  unsigned char s2sub = 0;
+  if ( s1 -> ref -> object_type == pnd_object_type_directory ) {
+    s1sub = 1;
+  }
+  if ( s2 -> ref -> object_type == pnd_object_type_directory ) {
+    s2sub = 1;
+  }
+
+  if        ( (   s1sub ) && (   s2sub ) ) {
+    return ( strcasecmp ( s1 -> ref -> title_en, s2 -> ref -> title_en ) );
+  } else if ( (   s1sub ) && ( ! s2sub ) ) {
+    return ( -1 );
+  } else if ( ( ! s1sub ) && (   s2sub ) ) {
+    return ( 1 );
+  } else if ( ( ! s1sub ) && ( ! s2sub ) ) {
+    return ( strcasecmp ( s1 -> ref -> title_en, s2 -> ref -> title_en ) );
+  }
+
+  return ( strcasecmp ( s1 -> ref -> title_en, s2 -> ref -> title_en ) );
 }
 
 void category_dump ( void ) {
-  unsigned int i;
 
   // WHY AREN'T I SORTING ON INSERT?
 
   // dump
-  for ( i = 0; i < g_categorycount; i++ ) {
-    pnd_log ( PND_LOG_DEFAULT, "Category %u: '%s' * %u\n", i, g_categories [ i ].catname, g_categories [ i ].refcount );
-    mm_appref_t *ar = g_categories [ i ].refs;
+  mm_category_t *iter = pnd_box_get_head ( m_categories );
+  unsigned int counter = 0;
+
+  while ( iter ) {
+
+    pnd_log ( PND_LOG_DEFAULT, "Category %u: '%s' * %u\n", counter, iter -> catname, iter -> refcount );
+    mm_appref_t *ar = iter -> refs;
 
     while ( ar ) {
       pnd_log ( PND_LOG_DEFAULT, "  Appref %s\n", IFNULL(ar -> ref -> title_en,"No Name") );
       ar = ar -> next;
     }
 
-  } // for
+    iter = pnd_box_get_next ( iter );
+    counter++;
+  }
+
+  return;
+}
+
+void category_freeall ( void ) {
+  mm_category_t *c, *cnext;
+  mm_appref_t *iter, *next;
+
+  c = pnd_box_get_head ( m_categories );
+
+  while ( c ) {
+    cnext = pnd_box_get_next ( c );
+
+    // wipe 'em
+    iter = c -> refs;
+
+    while ( iter ) {
+      next = iter -> next;
+      free ( iter );
+      iter = next;
+    }
+
+    c -> refs = NULL;
+
+    if ( c -> catname ) {
+      free ( c -> catname );
+      c -> catname = NULL;
+    }
+
+    if ( c -> fspath ) {
+      free ( c -> fspath );
+      c -> fspath = NULL;
+    }
+
+    pnd_box_delete_node ( m_categories, c );
+
+    // next
+    c = cnext;
+  }
+
+  return;
+}
+
+unsigned char category_map_setup ( void ) {
+
+  char *searchpath = pnd_box_get_head ( g_conf );
+
+  if ( ! searchpath ) {
+    return ( 0 );
+  }
+
+  // look through conf for useful keys
+  while ( searchpath ) {
+    char *k = pnd_box_get_key ( searchpath );
+
+    // does this key look like a category mapping key?
+    if ( strncasecmp ( k, "categories.@", 12 ) == 0 ) {
+      k += 12;
+
+      // iterate across 'words' in v, assigning catmaps to them
+      SEARCHCHUNK_PRE
+      {
+       //pnd_log ( pndn_debug, "target(%s) from(%s)\n", k, buffer );
+
+       category_push ( k, NULL /* parent cat */, NULL, 0, NULL /* fspath */, 1 );
+       g_catmaps [ g_catmapcount ].target = pnd_box_find_by_key ( m_categories, k );
+       g_catmaps [ g_catmapcount ].from = strdup ( buffer );
+       g_catmapcount++;
+
+      }
+      SEARCHCHUNK_POST
+
+    } // if key looks like catmap
+
+    searchpath = pnd_box_get_next ( searchpath );
+  } // while each conf key
+
+  return ( 1 );
+}
+
+mm_category_t *category_map_query ( char *cat ) {
+  unsigned char i;
+
+  for ( i = 0; i < g_catmapcount; i++ ) {
+    if ( strcasecmp ( g_catmaps [ i ].from, cat ) == 0 ) {
+      return ( g_catmaps [ i ].target );
+    }
+  }
+
+  return ( NULL );
+}
+
+unsigned char category_meta_push ( char *catname, char *parentcatname, pnd_disco_t *app, pnd_conf_handle ovrh, unsigned char visiblep ) {
+  mm_category_t *cat;
+#if 0 // prepending
+  char catnamebuffer [ 512 ] = "";
+#endif
+
+  //fprintf ( stderr, "meta push: '%s'\n", catname );
+
+  if ( ! catname ) {
+    return ( 1 ); // fine, just nada
+  }
+
+  // we don't screw with "All" category that mmenu.c generates on the fly
+  if ( strncmp ( catname, "All ", 4 ) == 0 ) {
+    goto category_done_audit;
+  }
+
+  // category cleansing; lets..
+  // - ensure we only let good freedesktop categories through
+  // - we fix case.. no more UtIliTy (a good cat, studlycaps)
+  // - no more good cats but swapped ancestry; Utility as child of something?
+  // - if bogus, we just ship it off to BAD_CAT
+
+  unsigned char cat_is_clean = 1;
+  freedesktop_cat_t *fdcat = NULL, *fdpcat = NULL;
+  fdcat = freedesktop_category_query ( catname );
+  if ( parentcatname ) {
+    fdpcat = freedesktop_category_query ( parentcatname );
+  }
+
+  // ensure requested cat is good
+  if ( ! fdcat ) {
+    // requested cat is bad, send it to Other
+    cat_is_clean = 0;
+    printf ( "PXML Fail %s: Cat request %s (parent %s) -> bad cat\n", app -> title_en ? app -> title_en : "no name?", catname, parentcatname ? parentcatname : "n/a" );
+
+    // do the Other substitution right away, so remaining code has something to look at in fdcat
+    fdcat = freedesktop_category_query ( BADCATNAME );
+    catname = fdcat -> cat;
+    fdpcat = NULL;
+    parentcatname = NULL;
+
+  } else {
+    // use canonicle entry, so our Case is now correct!
+    catname = fdcat -> cat;
+  }
+
+  // ensure parent is good, if specified
+  if ( parentcatname ) {
+    if ( ! fdpcat ) {
+      // requested cat is bad, send it to Other
+      cat_is_clean = 0;
+      printf ( "PXML Fail %s: Cat request %s (parent %s) -> parent bad cat\n", app -> title_en ? app -> title_en : "no name?", catname, parentcatname ? parentcatname : "n/a" );
+      // fix immediately so code doesn't explode
+      parentcatname = NULL;
+    } else {
+      // use canonicle entry, so our Case is now correct!
+      parentcatname = fdpcat -> cat;
+    }
+  }
+
+  // ensure ancestry is good
+  // - if cat request is for child, ensure its a child
+  // - if parent specified, ensure its a parent
+  // - if child specified, ensure its parent is the right parent(?!)
+  //
+  if ( parentcatname ) {
+    // implies catname request is for child, with parent parentcatname
+
+    if ( fdcat -> parent_cat == NULL ) {
+      // but wait, catname is actually a parent cat...
+      cat_is_clean = 0;
+      printf ( "PXML Fail %s: Cat request %s (parent %s) -> cat wants to be child, but FD says its a parent\n", app -> title_en ? app -> title_en : "no name?", catname, parentcatname ? parentcatname : "n/a" );
+    }
+    if ( fdpcat -> parent_cat ) {
+      // but wait, parent cat is actually a subcat!
+      cat_is_clean = 0;
+      printf ( "PXML Fail %s: Cat request %s (parent %s) -> parent cat, FD says its a child\n", app -> title_en ? app -> title_en : "no name?", catname, parentcatname ? parentcatname : "n/a" );
+    }
+
+  } else {
+    // implies request is for a parent cat - itself has no parent
+
+    if ( fdcat -> parent_cat ) {
+      // but wait, cat actually has a parent!
+      cat_is_clean = 0;
+      printf ( "PXML Fail %s: Cat request %s (parent %s) -> cat wants to be parent, FD says its a child\n", app -> title_en ? app -> title_en : "no name?", catname, parentcatname ? parentcatname : "n/a" );
+    }
+
+  }
+
+  // ensure that if this is a child cat, its parent is the right parent
+  if ( parentcatname ) {
+    if ( ( ! fdcat -> parent_cat ) ||
+        ( ! fdpcat ) )
+    { 
+      // child cat points to a different parent than requested parent!
+      cat_is_clean = 0;
+      printf ( "PXML Fail %s: Cat request %s (parent %s) -> cat wants to be child of a cat which FD says is the wrong parent (1)\n", app -> title_en ? app -> title_en : "no name?", catname, parentcatname ? parentcatname : "n/a" );
+    } else if ( strcasecmp ( fdcat -> parent_cat, fdpcat -> cat ) != 0 ) {
+      // child cat points to a different parent than requested parent!
+      cat_is_clean = 0;
+      printf ( "PXML Fail %s: Cat request %s (parent %s) -> cat wants to be child of a cat which FD says is the wrong parent (2)\n", app -> title_en ? app -> title_en : "no name?", catname, parentcatname ? parentcatname : "n/a" );
+    }
+  }
+
+  // did testing fail? if so, bump to Other!
+  //
+  if ( ! cat_is_clean ) {
+    // set Other visibility
+    visiblep = cat_is_visible ( g_conf, BADCATNAME );
+    // fix cat request
+    fdcat = freedesktop_category_query ( BADCATNAME );
+    catname = fdcat -> cat;
+    // nullify parent cat request (if any)
+    fdpcat = NULL;
+    parentcatname = NULL;
+  } else {
+    //printf ( "PXML Category Pass: Cat request %s (parent %s)\n", catname, parentcatname ? parentcatname : "n/a" );
+  }
+
+  // push bad categories into Other (if we're not targeting All right now)
+#if 0
+  if ( 1 /*pnd_conf_get_as_int_d ( g_conf, "categories.good_cats_only", 1 )*/ ) {
+
+    // don't audit All
+    if ( strncmp ( catname, "All ", 4 ) != 0 ) {
+
+      // if this is a parent cat..
+      if ( catname && ! parentcatname ) {
+
+       // if bad, shove it to Other
+       if ( ! freedesktop_check_cat ( catname ) ) {
+         parentcatname = NULL;
+         catname = BADCATNAME;
+         visiblep = cat_is_visible ( g_conf, catname );
+       }
+
+      } else if ( catname && parentcatname ) {
+       // this is a subcat
+
+       // if parent is bad, then we probably already pushed it over, so don't do it again.
+       // if its parent is okay, but subcat is bad, push it to other. (ie: lets avoid duplication in Other)
+       if ( ! freedesktop_check_cat ( parentcatname ) ) {
+         // skip
+         return ( 1 );
+
+       } else if ( ! freedesktop_check_cat ( catname ) ) {
+         parentcatname = NULL;
+         catname = BADCATNAME;
+         visiblep = cat_is_visible ( g_conf, catname );
+       }
+
+      } // parent or child cat?
+
+    } // not All
+
+  } // good cats only?
+#endif
+
+ category_done_audit:
+
+  // if invisible, and a parent category name is known, prepend it for ease of use
+#if 0 // prepending
+  if ( ! visiblep && parentcatname ) {
+    snprintf ( catnamebuffer, 500, "%s.%s", parentcatname, catname );
+    catname = catnamebuffer;
+  }
+#endif
+
+  // if this is a subcat push, and its requesting special 'no subcat', then just ditch it
+  if ( parentcatname && strcmp ( catname, "NoSubcategory" ) == 0 ) {
+    return ( 1 );
+  }
+
+  // do we honour cat mapping at all?
+  if ( pnd_conf_get_as_int_d ( g_conf, "categories.map_on", 0 ) ) {
+
+    // is this guy mapped?
+    cat = category_map_query ( catname );
+
+    if ( cat ) {
+      category_push ( cat -> catname, parentcatname /* parent cat */, app, ovrh, NULL /* fspath */, visiblep );
+      goto meta_done;
+    }
+
+    // not mapped.. but default?
+    if ( pnd_conf_get_as_int_d ( g_conf, "categories.map_default_on", 0 ) ) {
+      char *def = pnd_conf_get_as_char ( g_conf, "categories.map_default_cat" );
+      if ( def ) {
+       category_push ( def, parentcatname /* parent cat */, app, ovrh, NULL /* fspath */, visiblep );
+       goto meta_done;
+      }
+    }
+
+  } // cat map is desired?
+
+  // is app already in the target cat? (ie: its being pushed twice due to cat mapping or Other'ing or something..)
+  if ( app ) {
+    if ( category_contains_app ( catname, app -> unique_id ) ) {
+      printf ( "App Fail: app (%s %s) is already in cat %s\n", app -> title_en ? app -> title_en : "no name?", app -> unique_id, catname );
+      return ( 1 ); // success, already there!
+    }
+  }
+
+  // not default, just do it
+  category_push ( catname, parentcatname /* parent cat */, app, ovrh, NULL /* fspath */, visiblep );
+
+  // if subcats as folders, then lets just make up a dummy app that pretends to be a folder,
+  // and stuff it into the parent cat
+  if ( parentcatname && pnd_conf_get_as_int_d ( g_conf, "tabs.subcat_as_folders", 1 ) && cat_is_visible ( g_conf, catname ) ) {
+
+    // it is implicit that since we're talking parentcat, its already been created in a previous call
+    // therefore, we need to..
+    // i) find the parent cat
+    // ii) ensure it already has a faux-disco container
+    // iii) ensure that disco container doesn't already contain a disco-entry for this subcat
+    // iv) create the dummy app folder by pushing the disco into the apprefs as normal
+    // v) create a dummy '..' for going back up, in the child
+
+    mm_category_t *pcat = pnd_box_find_by_key ( m_categories, parentcatname );
+
+    if ( ! pcat -> disco ) {
+      pcat -> disco = pnd_box_new ( pcat -> catname );
+    }
+
+    // if this subcat is already in the faux-disco list, then its probably already
+    // handled so we needn't concern ourselves anymore. If not, then we can
+    // create it and push it into the parent as a new 'app'
+    pnd_disco_t *disco = pnd_box_find_by_key ( pcat -> disco, catname );
+
+    if ( ! disco ) {
+
+      disco = pnd_box_allocinsert ( pcat -> disco, catname, sizeof(pnd_disco_t) );
+      if ( disco ) {
+
+       // create the subcat faux-disco entry, and register into parent cat .. if its visible
+       char uid [ 30 ];
+       sprintf ( uid, "%p", catname );
+
+       disco -> unique_id = strdup ( uid );
+       if ( strchr ( catname, '.' ) ) {
+         disco -> title_en = strdup ( strchr ( catname, '.' ) + 1 );
+       } else {
+         disco -> title_en = strdup ( catname );
+       }
+       disco -> object_flags = PND_DISCO_GENERATED;
+       disco -> object_type = pnd_object_type_directory; // suggest to Grid that its a dir
+       disco -> object_path = strdup ( catname );
+
+       category_push ( parentcatname, NULL /* parent cat */, disco, 0 /*ovrh*/, NULL /* fspath */, 1 /* visible */ );
+
+       // create .. faux-disco entry into child cat
+       disco = pnd_box_allocinsert ( pcat -> disco, catname, sizeof(pnd_disco_t) );
+
+       sprintf ( uid, "%p", uid );
+
+       disco -> unique_id = strdup ( uid );
+       disco -> title_en = strdup ( ".." );
+       disco -> object_flags = PND_DISCO_GENERATED;
+       disco -> object_type = pnd_object_type_directory; // suggest to Grid that its a dir
+
+       category_push ( catname, parentcatname /* parent cat */, disco, 0 /*ovrh*/, NULL /* fspath */, 1 /* visible */ );
+
+      } // making faux disco entries
+
+    } // disco already exist?
+
+  } // subcat as folder?
+
+  // hack :(
+ meta_done:
+
+  //fprintf ( stderr, "cat meta-push : vis[%30s,%d b] : tally; vis %d invis %d\n", catname, visiblep, g_categorycount, _categories_inviscount );
+
+  return ( 1 );
+}
+
+unsigned char category_fs_restock ( mm_category_t *cat ) {
+
+  if ( ! cat -> fspath ) {
+    return ( 1 ); // not a filesystem browser tab
+  }
+
+  // clear any existing baggage
+  //
+
+  // apprefs
+  mm_appref_t *iter = cat -> refs, *next;
+  while ( iter ) {
+    next = iter -> next;
+    free ( iter );
+    iter = next;
+  }
+  cat -> refs = NULL;
+
+  // discos
+  if ( cat -> disco ) {
+    pnd_disco_t *p = pnd_box_get_head ( cat -> disco );
+    pnd_disco_t *n;
+    while ( p ) {
+      n = pnd_box_get_next ( p );
+      pnd_disco_destroy ( p );
+      p = n;
+    }
+    pnd_box_delete ( cat -> disco );
+  }
+
+  // rescan the filesystem
+  //
+
+  //pnd_log ( pndn_debug, "Restocking cat %s with path %s\n", cat -> catname, cat -> fspath );
+  DIR *d;
+
+  if ( ( d = opendir ( cat -> fspath ) ) ) {
+    struct dirent *de = readdir ( d );
+
+    pnd_disco_t *disco;
+    char uid [ 100 ];
+
+    cat -> disco = pnd_box_new ( cat -> catname );
+
+    while ( de ) {
+
+      struct stat buffy;
+      char fullpath [ PATH_MAX ];
+      sprintf ( fullpath, "%s/%s", cat -> fspath, de -> d_name );
+      int statret = stat ( fullpath, &buffy );
+
+      // if file is executable somehow or another
+      if ( statret == 0 &&
+          buffy.st_mode & (S_IXUSR|S_IXGRP|S_IXOTH)
+        )
+      {
+       // determine unique-id
+       sprintf ( uid, "%d", (int) de -> d_ino );
+       disco = NULL;
+
+       switch ( de -> d_type ) {
+
+       case DT_DIR:
+         if ( strcmp ( de -> d_name, "." ) == 0 ) {
+           // ignore ".", but ".." is fine
+         } else if ( strcmp ( de -> d_name, ".." ) == 0 && strcmp ( cat -> fspath, "/" ) == 0 ) {
+           // ignore ".." only if we're at the true root
+         } else {
+           disco = pnd_box_allocinsert ( cat -> disco, uid, sizeof(pnd_disco_t) );
+           disco -> object_type = pnd_object_type_directory; // suggest to Grid that its a dir
+         }
+         break;
+       case DT_UNKNOWN:
+       case DT_REG:
+         disco = pnd_box_allocinsert ( cat -> disco, uid, sizeof(pnd_disco_t) );
+         disco -> object_type = pnd_object_type_unknown; // suggest to Grid that its a file
+         break;
+
+       } // switch
+
+       // found a directory or executable?
+       if ( disco ) {
+         // register with current category
+         disco -> unique_id = strdup ( uid );
+         disco -> title_en = strdup ( de -> d_name );
+         disco -> object_flags = PND_DISCO_GENERATED;
+         disco -> object_path = strdup ( cat -> fspath );
+         disco -> object_filename = strdup ( de -> d_name );
+         category_push ( cat -> catname, NULL /* parent cat */, disco, 0 /* no ovr */, NULL /* fspath already set */, 1 /* visible */ );
+         // if a override icon exists, cache it up
+         cache_icon ( disco, pnd_conf_get_as_int_d ( g_conf, "grid.icon_max_width", 50 ),
+                      pnd_conf_get_as_int_d ( g_conf, "grid.icon_max_height", 50 ) );
+       }
+
+      } // stat
+
+      // next
+      de = readdir ( d );
+    }
+
+    closedir ( d );
+  }
+
+  return ( 1 );
+}
+
+static int catname_cmp ( const void *p1, const void *p2 ) {
+  //mm_category_t *c1 = (mm_category_t*) p1;
+  //mm_category_t *c2 = (mm_category_t*) p2;
+  mm_category_t *c1 = *( (mm_category_t**) p1 );
+  mm_category_t *c2 = *( (mm_category_t**) p2 );
+
+  if ( ( isalnum ( c1 -> catname [ 0 ] ) ) && ( ! isalnum ( c1 -> catname [ 1 ] ) ) ) {
+    return ( -1 );
+  } else if ( ( ! isalnum ( c1 -> catname [ 0 ] ) ) && ( isalnum ( c1 -> catname [ 1 ] ) ) ) {
+    return ( 1 );
+  } else if ( ( ! isalnum ( c1 -> catname [ 0 ] ) ) && ( ! isalnum ( c1 -> catname [ 1 ] ) ) ) {
+    return ( 0 );
+  }
+
+  int i = strcasecmp ( c1 -> catname, c2 -> catname );
+  //printf ( "cat name compare %p %s to %p %s = %d\n", p1, c1 -> catname, p2, c2 -> catname, i );
+
+  return ( i );
+}
+
+void category_sort ( void ) {
+  // we probably don't want to sort tab categories, since the user may have specified an ordering
+  // But we can sort invisi-cats, to make them easier to find, and ordered by parent category
+
+#if 0
+  qsort ( _categories_invis, _categories_inviscount, sizeof(mm_category_t), catname_cmp );
+#endif
+
+#if 1
+  qsort ( g_categories, g_categorycount, sizeof(mm_category_t*), catname_cmp );
+#endif
+
+  return;
+}
+
+void category_publish ( unsigned int filter_mask, char *param ) {
+  unsigned char interested;
+
+  // clear published categories
+  memset ( g_categories, '\0', sizeof(mm_category_t*) * MAX_CATS );
+  g_categorycount = 0;
+
+  // figure out the start
+  mm_category_t *iter = pnd_box_get_head ( m_categories );
+
+  // for each category we know...
+  while ( iter ) {
+
+    interested = 0;
+
+    // is this category desired?
+    if ( filter_mask == CFALL ) {
+      interested = 1;
+    } else if ( filter_mask == CFBYNAME ) {
+      if ( strcasecmp ( iter -> catname, param ) == 0 ) {
+       interested = 1;
+      }
+    } else if ( iter -> catflags == filter_mask ) {
+      interested = 1;
+    } // if
+
+    if ( interested ) {
+      // set us up the bomb; notice that we're just duplicating the pointers, not making
+      // any new data here; none of this should ever be free'd!
+      g_categories [ g_categorycount ] = iter;
+      g_categorycount++;
+    }
+
+    // next
+    iter = pnd_box_get_next ( iter );
+  }
+
+  // dump
+#if 0
+  unsigned int i;
+  for ( i = 0; i < g_categorycount; i++ ) {
+    printf ( "Unsorted cat %d %p: %s\n", i, &(g_categories [ i ]), g_categories [ i ] -> catname );
+  }
+#endif
+
+  // sort published categories
+  category_sort();
 
   return;
 }
+
+unsigned int category_count ( unsigned int filter_mask ) {
+  mm_category_t *iter = pnd_box_get_head ( m_categories );
+  unsigned int count = 0;
+
+  // for each category we know...
+  while ( iter ) {
+
+    // is this category desired?
+    if ( iter -> catflags == filter_mask ) {
+      count++;
+    } // if
+
+    // next
+    iter = pnd_box_get_next ( iter );
+  }
+
+  return ( count );
+}
+
+int category_index ( char *catname ) {
+  unsigned char i;
+  for ( i = 0; i < g_categorycount; i++ ) {
+    if ( strcasecmp ( g_categories [ i ] -> catname, catname ) == 0 ) {
+      return ( i );
+    }
+  }
+  return ( -1 );
+}
+
+unsigned char category_contains_app ( char *catname, char *unique_id ) {
+
+  mm_category_t *c = pnd_box_find_by_key ( m_categories, catname );
+
+  if ( ! c ) {
+    return ( 0 ); // wtf?
+  }
+
+  if ( ! c -> refs ) {
+    return ( 0 ); // no apps at all
+  }
+
+  mm_appref_t *iter = c -> refs;
+
+  while ( iter ) {
+
+    if ( strcmp ( iter -> ref -> unique_id, unique_id ) == 0 ) {
+      return ( 1 );
+    }
+
+    iter = iter -> next;
+  }
+
+  return ( 0 );
+}