mmenu; better handling where a suncategory name is available to multiple parent categ...
[pandora-libraries.git] / minimenu / mmcat.c
1
2 #include <string.h>
3 #include <strings.h>
4 #include <stdlib.h>
5 #include <sys/types.h>
6 #include <dirent.h>
7 #include <sys/stat.h>
8 #include <unistd.h>
9 #include <ctype.h>
10
11 #include "pnd_conf.h"
12 #include "pnd_logger.h"
13 #include "pnd_pxml.h"
14 #include "pnd_container.h"
15 #include "pnd_discovery.h"
16 #include "../lib/pnd_pathiter.h"
17
18 #include "mmenu.h"
19 #include "mmcache.h"
20 #include "mmcat.h"
21 #include "freedesktop_cats.h"
22
23 // all categories known -- visible, hidden, subcats, whatever.
24 pnd_box_handle m_categories = NULL;
25 unsigned int m_categorycount = 0;
26
27 // all categories being published right now -- a subset copied from m_categories
28 mm_category_t *g_categories [ MAX_CATS ];
29 unsigned char g_categorycount;
30
31 // category mappings
32 mm_catmap_t g_catmaps [ MAX_CATS ];
33 unsigned char g_catmapcount = 0;
34
35 extern pnd_conf_handle g_conf;
36
37 void category_init ( void ) {
38   m_categories = pnd_box_new ( "Schrodinger's cat" );
39   return;
40 }
41
42 static char *_normalize ( char *catname, char *parentcatname ) {
43   static char buffer [ 101 ];
44   snprintf ( buffer, 100, "%s*%s", catname, parentcatname ? parentcatname : "NoParent" );
45   return ( buffer );
46 }
47
48 unsigned char category_push ( char *catname, char *parentcatname, pnd_disco_t *app, pnd_conf_handle ovrh, char *fspath, unsigned char visiblep ) {
49   mm_category_t *c;
50
51   // check category list; if found, append app to the end of it.
52   // if not found, add it to the category list and plop the app in there.
53   // app's are just app-refs, which contain links to the disco-t list -- thus removal is only in one place, and
54   // an app can be in multiple categories if we like..
55   //
56
57   // find or create category
58   //
59
60   if ( ( c = pnd_box_find_by_key ( m_categories, _normalize ( catname, parentcatname ) ) ) ) {
61     // category was found..
62   } else {
63     // category wasn't found..
64     //pnd_log ( PND_LOG_DEFAULT, "New category '%s'\n", catname );
65     c = pnd_box_allocinsert ( m_categories, _normalize ( catname, parentcatname ), sizeof(mm_category_t) );
66     c -> catname = strdup ( catname );
67     if ( parentcatname ) {
68       c -> parent_catname = strdup ( parentcatname );
69     }
70
71     if ( visiblep ) {
72       c -> catflags = CFNORMAL;
73     } else {
74       c -> catflags = CFHIDDEN;
75     }
76
77     // if user prefers subcats-as-folders, lets reflag this sucker
78     if ( parentcatname && pnd_conf_get_as_int_d ( g_conf, "tabs.subcat_as_folders", 1 ) ) {
79       //printf ( "subcat: %s parent: %s\n", catname, parentcatname ? parentcatname : "none" );
80       c -> catflags = CFSUBCAT;
81     }
82
83     c -> refs = NULL;
84
85     if ( fspath ) {
86       c -> fspath = strdup ( fspath );
87     }
88
89     m_categorycount++;
90   }
91
92   if ( ! app ) {
93     return ( 1 ); // create cat, but skip app
94   }
95
96   // alloc and populate appref
97   //
98   mm_appref_t *ar = malloc ( sizeof(mm_appref_t) );
99   if ( ! ar ) {
100     return ( 0 );
101   }
102
103   bzero ( ar, sizeof(mm_appref_t) );
104
105   ar -> ref = app;
106   ar -> ovrh = ovrh;
107
108   // plug it into category
109   //   and sort it on insert!
110 #if 0 // no sorting
111   ar -> next = c -> refs;
112   c -> refs = ar;
113 #else // with sorting
114   // if no refs at all, or new guy has no title, just stick it in at head
115   if ( c -> refs && ar -> ref -> title_en ) {
116     mm_appref_t *iter = c -> refs;
117     mm_appref_t *last = NULL;
118
119     while ( iter ) {
120
121       if ( iter -> ref -> title_en ) {
122         if ( cat_sort_score ( c, ar, iter ) < 0 ) {
123           // new guy is smaller than the current guy!
124           break;
125         }
126       } else {
127         // since new guy must have a name by here, we're bigger than any guy who does not have a name
128         // --> continue
129       }
130
131       last = iter;
132       iter = iter -> next;
133     }
134
135     if ( iter ) {
136       // smaller than the current guy, so stitch in
137       if ( last ) {
138         ar -> next = iter;
139         last -> next = ar;
140       } else {
141         ar -> next = c -> refs;
142         c -> refs = ar;
143       }
144     } else {
145       // we're the biggest, just append to last
146       last -> next = ar;
147     }
148
149   } else {
150     ar -> next = c -> refs;
151     c -> refs = ar;
152   }
153 #endif
154   c -> refcount++;
155
156   return ( 1 );
157 }
158
159 int cat_sort_score ( mm_category_t *cat, mm_appref_t *s1, mm_appref_t *s2 ) {
160
161   // are we in a directory browser, or looking at pnd-files?
162   if ( cat -> fspath ) {
163     // directory browser mode
164
165     if ( s1 == s2 ) {
166       return ( 0 ); // equal
167
168     } else if ( s1 -> ref -> object_type == pnd_object_type_directory &&
169                 s2 -> ref -> object_type == pnd_object_type_directory )
170     {
171       // both are directories, be nice
172       return ( strcmp ( s1 -> ref -> title_en, s2 -> ref -> title_en ) );
173     } else if ( s1 -> ref -> object_type == pnd_object_type_directory &&
174                 s2 -> ref -> object_type != pnd_object_type_directory )
175     {
176       return ( -1 ); // dir on the left is earlier than file on the right
177     } else if ( s1 -> ref -> object_type != pnd_object_type_directory &&
178                 s2 -> ref -> object_type == pnd_object_type_directory )
179     {
180       return ( 1 ); // dir on the right is earlier than file on the left
181     } else {
182       // file on file
183       return ( strcmp ( s1 -> ref -> title_en, s2 -> ref -> title_en ) );
184     }
185
186   }
187
188   // pnd tab
189   //
190
191   // if this is comparing subcat folder to subcat folder, or pnd to pnd, or pnd to subcat folder?
192   unsigned char s1sub = 0;
193   unsigned char s2sub = 0;
194   if ( s1 -> ref -> object_type == pnd_object_type_directory ) {
195     s1sub = 1;
196   }
197   if ( s2 -> ref -> object_type == pnd_object_type_directory ) {
198     s2sub = 1;
199   }
200
201   if        ( (   s1sub ) && (   s2sub ) ) {
202     return ( strcasecmp ( s1 -> ref -> title_en, s2 -> ref -> title_en ) );
203   } else if ( (   s1sub ) && ( ! s2sub ) ) {
204     return ( -1 );
205   } else if ( ( ! s1sub ) && (   s2sub ) ) {
206     return ( 1 );
207   } else if ( ( ! s1sub ) && ( ! s2sub ) ) {
208     return ( strcasecmp ( s1 -> ref -> title_en, s2 -> ref -> title_en ) );
209   }
210
211   return ( strcasecmp ( s1 -> ref -> title_en, s2 -> ref -> title_en ) );
212 }
213
214 void category_dump ( void ) {
215
216   // WHY AREN'T I SORTING ON INSERT?
217
218   // dump
219   mm_category_t *iter = pnd_box_get_head ( m_categories );
220   unsigned int counter = 0;
221
222   while ( iter ) {
223
224     pnd_log ( PND_LOG_DEFAULT, "Category %u: '%s' * %u\n", counter, iter -> catname, iter -> refcount );
225     mm_appref_t *ar = iter -> refs;
226
227     while ( ar ) {
228       pnd_log ( PND_LOG_DEFAULT, "  Appref %s\n", IFNULL(ar -> ref -> title_en,"No Name") );
229       ar = ar -> next;
230     }
231
232     iter = pnd_box_get_next ( iter );
233     counter++;
234   }
235
236   return;
237 }
238
239 void category_freeall ( void ) {
240   mm_category_t *c, *cnext;
241   mm_appref_t *iter, *next;
242
243   c = pnd_box_get_head ( m_categories );
244
245   while ( c ) {
246     cnext = pnd_box_get_next ( c );
247
248     // wipe 'em
249     iter = c -> refs;
250
251     while ( iter ) {
252       next = iter -> next;
253       free ( iter );
254       iter = next;
255     }
256
257     c -> refs = NULL;
258
259     if ( c -> catname ) {
260       free ( c -> catname );
261       c -> catname = NULL;
262     }
263
264     if ( c -> fspath ) {
265       free ( c -> fspath );
266       c -> fspath = NULL;
267     }
268
269     pnd_box_delete_node ( m_categories, c );
270
271     // next
272     c = cnext;
273   }
274
275   return;
276 }
277
278 unsigned char category_map_setup ( void ) {
279
280   char *searchpath = pnd_box_get_head ( g_conf );
281
282   if ( ! searchpath ) {
283     return ( 0 );
284   }
285
286   // look through conf for useful keys
287   while ( searchpath ) {
288     char *k = pnd_box_get_key ( searchpath );
289
290     // does this key look like a category mapping key?
291     if ( strncasecmp ( k, "categories.@", 12 ) == 0 ) {
292       k += 12;
293
294       // iterate across 'words' in v, assigning catmaps to them
295       SEARCHCHUNK_PRE
296       {
297         //pnd_log ( pndn_debug, "target(%s) from(%s)\n", k, buffer );
298
299         category_push ( k, NULL /* parent cat */, NULL, 0, NULL /* fspath */, 1 );
300         g_catmaps [ g_catmapcount ].target = pnd_box_find_by_key ( m_categories, _normalize ( k, NULL /* TBD: hack, not sure if this is right default value */ ) );
301         g_catmaps [ g_catmapcount ].from = strdup ( buffer );
302         g_catmapcount++;
303
304       }
305       SEARCHCHUNK_POST
306
307     } // if key looks like catmap
308
309     searchpath = pnd_box_get_next ( searchpath );
310   } // while each conf key
311
312   return ( 1 );
313 }
314
315 mm_category_t *category_map_query ( char *cat ) {
316   unsigned char i;
317
318   for ( i = 0; i < g_catmapcount; i++ ) {
319     if ( strcasecmp ( g_catmaps [ i ].from, cat ) == 0 ) {
320       return ( g_catmaps [ i ].target );
321     }
322   }
323
324   return ( NULL );
325 }
326
327 unsigned char category_meta_push ( char *catname, char *parentcatname, pnd_disco_t *app, pnd_conf_handle ovrh, unsigned char visiblep, unsigned char parentp ) {
328   mm_category_t *cat;
329 #if 0 // prepending
330   char catnamebuffer [ 512 ] = "";
331 #endif
332
333   //fprintf ( stderr, "meta push: '%s'\n", catname );
334
335   if ( ! catname ) {
336     return ( 1 ); // fine, just nada
337   }
338
339   // we don't screw with "All" category that mmenu.c generates on the fly
340   if ( strncmp ( catname, "All ", 4 ) == 0 ) {
341     goto category_done_audit;
342   }
343
344   // check if category came from an ovr-file; if so, we implicitly trust it instead of enforcing rules
345   if ( app -> object_flags & ( PND_DISCO_CUSTOM1 | PND_DISCO_CUSTOM1 ) ) {
346     goto category_done_audit;
347   }
348
349   // category cleansing; lets..
350   // - ensure we only let good freedesktop categories through
351   // - we fix case.. no more UtIliTy (a good cat, studlycaps)
352   // - no more good cats but swapped ancestry; Utility as child of something?
353   // - if bogus, we just ship it off to BAD_CAT
354
355   unsigned char cat_is_clean = 1;
356   freedesktop_cat_t *fdcat = NULL, *fdpcat = NULL;
357   fdcat = freedesktop_category_query ( catname, parentcatname );
358   if ( parentcatname ) {
359     fdpcat = freedesktop_category_query ( parentcatname, NULL );
360   }
361
362   // ensure requested cat is good
363   if ( ! fdcat ) {
364     // requested cat is bad, send it to Other
365     cat_is_clean = 0;
366     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" );
367
368     // do the Other substitution right away, so remaining code has something to look at in fdcat
369     fdcat = freedesktop_category_query ( BADCATNAME, NULL );
370     catname = fdcat -> cat;
371     fdpcat = NULL;
372     parentcatname = NULL;
373
374   } else {
375     // use canonicle entry, so our Case is now correct!
376     catname = fdcat -> cat;
377   }
378
379   // ensure parent is good, if specified
380   if ( parentcatname ) {
381     if ( ! fdpcat ) {
382       // requested cat is bad, send it to Other
383       cat_is_clean = 0;
384       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" );
385       // fix immediately so code doesn't explode
386       parentcatname = NULL;
387     } else {
388       // use canonicle entry, so our Case is now correct!
389       parentcatname = fdpcat -> cat;
390     }
391   }
392
393   // ensure ancestry is good
394   // - if cat request is for child, ensure its a child
395   // - if parent specified, ensure its a parent
396   // - if child specified, ensure its parent is the right parent(?!)
397   //
398   if ( parentcatname ) {
399     // implies catname request is for child, with parent parentcatname
400
401     if ( fdcat -> parent_cat == NULL ) {
402       // but wait, catname is actually a parent cat...
403       cat_is_clean = 0;
404       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" );
405     }
406     if ( fdpcat -> parent_cat ) {
407       // but wait, parent cat is actually a subcat!
408       cat_is_clean = 0;
409       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" );
410     }
411
412   } else {
413     // implies request is for a parent cat - itself has no parent
414
415     if ( fdcat -> parent_cat ) {
416       // but wait, cat actually has a parent!
417       cat_is_clean = 0;
418       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" );
419     }
420
421   }
422
423   // ensure that if this is a child cat, its parent is the right parent
424   if ( parentcatname ) {
425     if ( ( ! fdcat -> parent_cat ) ||
426          ( ! fdpcat ) )
427     { 
428       // child cat points to a different parent than requested parent!
429       cat_is_clean = 0;
430       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" );
431     } else if ( strcasecmp ( fdcat -> parent_cat, fdpcat -> cat ) != 0 ) {
432       // child cat points to a different parent than requested parent!
433       cat_is_clean = 0;
434       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" );
435     }
436   }
437
438   // did testing fail? if so, bump to Other!
439   //
440   if ( ! cat_is_clean ) {
441     // set Other visibility
442     visiblep = cat_is_visible ( g_conf, BADCATNAME );
443     // fix cat request
444     fdcat = freedesktop_category_query ( BADCATNAME, NULL );
445     catname = fdcat -> cat;
446     // nullify parent cat request (if any)
447     fdpcat = NULL;
448     parentcatname = NULL;
449   } else {
450     //printf ( "PXML Category Pass: Cat request %s (parent %s)\n", catname, parentcatname ? parentcatname : "n/a" );
451   }
452
453   // push bad categories into Other (if we're not targeting All right now)
454 #if 0
455   if ( 1 /*pnd_conf_get_as_int_d ( g_conf, "categories.good_cats_only", 1 )*/ ) {
456
457     // don't audit All
458     if ( strncmp ( catname, "All ", 4 ) != 0 ) {
459
460       // if this is a parent cat..
461       if ( catname && ! parentcatname ) {
462
463         // if bad, shove it to Other
464         if ( ! freedesktop_check_cat ( catname ) ) {
465           parentcatname = NULL;
466           catname = BADCATNAME;
467           visiblep = cat_is_visible ( g_conf, catname );
468         }
469
470       } else if ( catname && parentcatname ) {
471         // this is a subcat
472
473         // if parent is bad, then we probably already pushed it over, so don't do it again.
474         // if its parent is okay, but subcat is bad, push it to other. (ie: lets avoid duplication in Other)
475         if ( ! freedesktop_check_cat ( parentcatname ) ) {
476           // skip
477           return ( 1 );
478
479         } else if ( ! freedesktop_check_cat ( catname ) ) {
480           parentcatname = NULL;
481           catname = BADCATNAME;
482           visiblep = cat_is_visible ( g_conf, catname );
483         }
484
485       } // parent or child cat?
486
487     } // not All
488
489   } // good cats only?
490 #endif
491
492  category_done_audit:
493
494   // if invisible, and a parent category name is known, prepend it for ease of use
495 #if 0 // prepending
496   if ( ! visiblep && parentcatname ) {
497     snprintf ( catnamebuffer, 500, "%s.%s", parentcatname, catname );
498     catname = catnamebuffer;
499   }
500 #endif
501
502   // if this is a subcat push, and its requesting special 'no subcat', then just ditch it
503   if ( parentcatname && strcmp ( catname, "NoSubcategory" ) == 0 ) {
504     return ( 1 );
505   }
506
507   // do we honour cat mapping at all?
508   if ( pnd_conf_get_as_int_d ( g_conf, "categories.map_on", 0 ) ) {
509
510     // is this guy mapped?
511     cat = category_map_query ( catname );
512
513     if ( cat ) {
514       category_push ( cat -> catname, parentcatname /* parent cat */, app, ovrh, NULL /* fspath */, visiblep );
515       goto meta_done;
516     }
517
518     // not mapped.. but default?
519     if ( pnd_conf_get_as_int_d ( g_conf, "categories.map_default_on", 0 ) ) {
520       char *def = pnd_conf_get_as_char ( g_conf, "categories.map_default_cat" );
521       if ( def ) {
522         category_push ( def, parentcatname /* parent cat */, app, ovrh, NULL /* fspath */, visiblep );
523         goto meta_done;
524       }
525     }
526
527   } // cat map is desired?
528
529   // is app already in the target cat? (ie: its being pushed twice due to cat mapping or Other'ing or something..)
530   if ( app ) {
531     if ( category_contains_app ( catname, parentcatname, app -> unique_id ) ) {
532       printf ( "App Fail: app (%s %s) is already in cat %s\n", app -> title_en ? app -> title_en : "no name?", app -> unique_id, catname );
533       return ( 1 ); // success, already there!
534     }
535   }
536
537   // are we not putting apps into parent cat, when subcat is present? if so..
538   // if the cat we're looking at now is the main (or main alt-cat), and we also have a subcat, then ditch it.
539   // so, is the cat we're looking at right now the apps main (or main alt) cat?
540   if ( parentp ) {
541     // and does this app have sub/altsub cats?
542     if ( app -> main_category1 || app -> main_category2 ||
543          app -> alt_category1 || app -> alt_category2 )
544     {
545       // and we're only desiring the subcat version of the app?
546       if ( pnd_conf_get_as_int_d ( g_conf, "tabs.subcat_to_parent", 1 ) == 0 ) {
547         // create the parent category, since we need to be able to place a folder here maybe
548         category_push ( catname, parentcatname /* parent cat */, NULL /* app */, NULL /* ovrh */, NULL /* fspath */, visiblep );
549         // bail
550         return ( 1 );
551       } // subcat to parent?
552     } // app has subcat?
553   } // tab we're looking at now is the main tab?
554
555   // not default, just do it
556   category_push ( catname, parentcatname /* parent cat */, app, ovrh, NULL /* fspath */, visiblep );
557
558   // if subcats as folders, then lets just make up a dummy app that pretends to be a folder,
559   // and stuff it into the parent cat
560   if ( parentcatname && pnd_conf_get_as_int_d ( g_conf, "tabs.subcat_as_folders", 1 ) && cat_is_visible ( g_conf, catname ) ) {
561
562     // it is implicit that since we're talking parentcat, its already been created in a previous call
563     // therefore, we need to..
564     // i) find the parent cat
565     // ii) ensure it already has a faux-disco container
566     // iii) ensure that disco container doesn't already contain a disco-entry for this subcat
567     // iv) create the dummy app folder by pushing the disco into the apprefs as normal
568     // v) create a dummy '..' for going back up, in the child
569
570     mm_category_t *pcat = pnd_box_find_by_key ( m_categories, _normalize ( parentcatname, NULL ) );
571
572     if ( ! pcat -> disco ) {
573       pcat -> disco = pnd_box_new ( pcat -> catname );
574     }
575
576     // if this subcat is already in the faux-disco list, then its probably already
577     // handled so we needn't concern ourselves anymore. If not, then we can
578     // create it and push it into the parent as a new 'app'
579     pnd_disco_t *disco = pnd_box_find_by_key ( pcat -> disco, catname );
580
581     if ( ! disco ) {
582
583       disco = pnd_box_allocinsert ( pcat -> disco, catname, sizeof(pnd_disco_t) );
584       if ( disco ) {
585
586         // create the subcat faux-disco entry, and register into parent cat .. if its visible
587         char uid [ 30 ];
588         sprintf ( uid, "%p", catname );
589
590         disco -> unique_id = strdup ( uid );
591         if ( strchr ( catname, '.' ) ) {
592           disco -> title_en = strdup ( strchr ( catname, '.' ) + 1 );
593         } else {
594           disco -> title_en = strdup ( catname );
595         }
596         disco -> object_flags = PND_DISCO_GENERATED;
597         disco -> object_type = pnd_object_type_directory; // suggest to Grid that its a dir
598         disco -> object_path = strdup ( _normalize ( catname, parentcatname ) );
599
600         category_push ( parentcatname, NULL /* parent cat */, disco, 0 /*ovrh*/, NULL /* fspath */, 1 /* visible */ );
601
602         // create .. faux-disco entry into child cat
603         disco = pnd_box_allocinsert ( pcat -> disco, catname, sizeof(pnd_disco_t) );
604
605         sprintf ( uid, "%p", uid );
606
607         disco -> unique_id = strdup ( uid );
608         disco -> title_en = strdup ( ".." );
609         disco -> object_flags = PND_DISCO_GENERATED;
610         disco -> object_type = pnd_object_type_directory; // suggest to Grid that its a dir
611
612         category_push ( catname, parentcatname /* parent cat */, disco, 0 /*ovrh*/, NULL /* fspath */, 1 /* visible */ );
613
614       } // making faux disco entries
615
616     } // disco already exist?
617
618   } // subcat as folder?
619
620   // hack :(
621  meta_done:
622
623   //fprintf ( stderr, "cat meta-push : vis[%30s,%d b] : tally; vis %d invis %d\n", catname, visiblep, g_categorycount, _categories_inviscount );
624
625   return ( 1 );
626 }
627
628 unsigned char category_fs_restock ( mm_category_t *cat ) {
629
630   if ( ! cat -> fspath ) {
631     return ( 1 ); // not a filesystem browser tab
632   }
633
634   // clear any existing baggage
635   //
636
637   // apprefs
638   mm_appref_t *iter = cat -> refs, *next;
639   while ( iter ) {
640     next = iter -> next;
641     free ( iter );
642     iter = next;
643   }
644   cat -> refs = NULL;
645
646   // discos
647   if ( cat -> disco ) {
648     pnd_disco_t *p = pnd_box_get_head ( cat -> disco );
649     pnd_disco_t *n;
650     while ( p ) {
651       n = pnd_box_get_next ( p );
652       pnd_disco_destroy ( p );
653       p = n;
654     }
655     pnd_box_delete ( cat -> disco );
656   }
657
658   // rescan the filesystem
659   //
660
661   //pnd_log ( pndn_debug, "Restocking cat %s with path %s\n", cat -> catname, cat -> fspath );
662   DIR *d;
663
664   if ( ( d = opendir ( cat -> fspath ) ) ) {
665     struct dirent *de = readdir ( d );
666
667     pnd_disco_t *disco;
668     char uid [ 100 ];
669
670     cat -> disco = pnd_box_new ( cat -> catname );
671
672     while ( de ) {
673
674       struct stat buffy;
675       char fullpath [ PATH_MAX ];
676       sprintf ( fullpath, "%s/%s", cat -> fspath, de -> d_name );
677       int statret = stat ( fullpath, &buffy );
678
679       // if file is executable somehow or another
680       if ( statret == 0 &&
681            buffy.st_mode & (S_IXUSR|S_IXGRP|S_IXOTH)
682          )
683       {
684         // determine unique-id
685         sprintf ( uid, "%d", (int) de -> d_ino );
686         disco = NULL;
687
688         switch ( de -> d_type ) {
689
690         case DT_DIR:
691           if ( strcmp ( de -> d_name, "." ) == 0 ) {
692             // ignore ".", but ".." is fine
693           } else if ( strcmp ( de -> d_name, ".." ) == 0 && strcmp ( cat -> fspath, "/" ) == 0 ) {
694             // ignore ".." only if we're at the true root
695           } else {
696             disco = pnd_box_allocinsert ( cat -> disco, uid, sizeof(pnd_disco_t) );
697             disco -> object_type = pnd_object_type_directory; // suggest to Grid that its a dir
698           }
699           break;
700         case DT_UNKNOWN:
701         case DT_REG:
702           disco = pnd_box_allocinsert ( cat -> disco, uid, sizeof(pnd_disco_t) );
703           disco -> object_type = pnd_object_type_unknown; // suggest to Grid that its a file
704           break;
705
706         } // switch
707
708         // found a directory or executable?
709         if ( disco ) {
710           // register with current category
711           disco -> unique_id = strdup ( uid );
712           disco -> title_en = strdup ( de -> d_name );
713           disco -> object_flags = PND_DISCO_GENERATED;
714           disco -> object_path = strdup ( cat -> fspath );
715           disco -> object_filename = strdup ( de -> d_name );
716           category_push ( cat -> catname, NULL /* parent cat */, disco, 0 /* no ovr */, NULL /* fspath already set */, 1 /* visible */ );
717           // if a override icon exists, cache it up
718           cache_icon ( disco, pnd_conf_get_as_int_d ( g_conf, "grid.icon_max_width", 50 ),
719                        pnd_conf_get_as_int_d ( g_conf, "grid.icon_max_height", 50 ) );
720         }
721
722       } // stat
723
724       // next
725       de = readdir ( d );
726     }
727
728     closedir ( d );
729   }
730
731   return ( 1 );
732 }
733
734 static int catname_cmp ( const void *p1, const void *p2 ) {
735   //mm_category_t *c1 = (mm_category_t*) p1;
736   //mm_category_t *c2 = (mm_category_t*) p2;
737   mm_category_t *c1 = *( (mm_category_t**) p1 );
738   mm_category_t *c2 = *( (mm_category_t**) p2 );
739
740   if ( ( isalnum ( c1 -> catname [ 0 ] ) ) && ( ! isalnum ( c1 -> catname [ 1 ] ) ) ) {
741     return ( -1 );
742   } else if ( ( ! isalnum ( c1 -> catname [ 0 ] ) ) && ( isalnum ( c1 -> catname [ 1 ] ) ) ) {
743     return ( 1 );
744   } else if ( ( ! isalnum ( c1 -> catname [ 0 ] ) ) && ( ! isalnum ( c1 -> catname [ 1 ] ) ) ) {
745     return ( 0 );
746   }
747
748   int i = strcasecmp ( c1 -> catname, c2 -> catname );
749   //printf ( "cat name compare %p %s to %p %s = %d\n", p1, c1 -> catname, p2, c2 -> catname, i );
750
751   return ( i );
752 }
753
754 void category_sort ( void ) {
755   // we probably don't want to sort tab categories, since the user may have specified an ordering
756   // But we can sort invisi-cats, to make them easier to find, and ordered by parent category
757
758 #if 0
759   qsort ( _categories_invis, _categories_inviscount, sizeof(mm_category_t), catname_cmp );
760 #endif
761
762 #if 1
763   qsort ( g_categories, g_categorycount, sizeof(mm_category_t*), catname_cmp );
764 #endif
765
766   return;
767 }
768
769 void category_publish ( unsigned int filter_mask, char *param ) {
770   unsigned char interested;
771
772   // clear published categories
773   memset ( g_categories, '\0', sizeof(mm_category_t*) * MAX_CATS );
774   g_categorycount = 0;
775
776   // figure out the start
777   mm_category_t *iter = pnd_box_get_head ( m_categories );
778
779   // for each category we know...
780   while ( iter ) {
781
782     interested = 0;
783
784     // is this category desired?
785     if ( filter_mask == CFALL ) {
786       interested = 1;
787     } else if ( filter_mask == CFBYNAME ) {
788       char *foo = strchr ( param, '*' ) + 1;
789       if ( strncasecmp ( iter -> catname, param, strlen ( iter -> catname ) ) == 0 &&
790            strcasecmp ( iter -> parent_catname, foo ) == 0 )
791       {
792         interested = 1;
793       }
794     } else if ( iter -> catflags == filter_mask ) {
795       interested = 1;
796     } // if
797
798     // desired tab?
799     if ( interested ) {
800
801       // lets only publish tabs that actually have an app in them .. just in case we've
802       // pruned out all the apps (sent them to Other or are suppressing apps in parent cats or
803       // something) at some point.
804       if ( iter -> fspath || iter -> refs ) {
805
806         // set us up the bomb; notice that we're just duplicating the pointers, not making
807         // any new data here; none of this should ever be free'd!
808         g_categories [ g_categorycount ] = iter;
809         g_categorycount++;
810
811       } // has apps?
812
813     } // interested?
814
815     // next
816     iter = pnd_box_get_next ( iter );
817   }
818
819   // dump
820 #if 0
821   unsigned int i;
822   for ( i = 0; i < g_categorycount; i++ ) {
823     printf ( "Unsorted cat %d %p: %s\n", i, &(g_categories [ i ]), g_categories [ i ] -> catname );
824   }
825 #endif
826
827   // sort published categories
828   category_sort();
829
830   return;
831 }
832
833 unsigned int category_count ( unsigned int filter_mask ) {
834   mm_category_t *iter = pnd_box_get_head ( m_categories );
835   unsigned int count = 0;
836
837   // for each category we know...
838   while ( iter ) {
839
840     // is this category desired?
841     if ( iter -> catflags == filter_mask ) {
842       count++;
843     } // if
844
845     // next
846     iter = pnd_box_get_next ( iter );
847   }
848
849   return ( count );
850 }
851
852 int category_index ( char *catname ) {
853   unsigned char i;
854  
855   for ( i = 0; i < g_categorycount; i++ ) {
856  
857     if ( strcasecmp ( g_categories [ i ] -> catname, catname ) == 0 ) {
858       return ( i );
859     }
860  
861   }
862  
863   return ( -1 );
864 }
865
866 unsigned char category_contains_app ( char *catname, char *parentcatname, char *unique_id ) {
867
868   mm_category_t *c = pnd_box_find_by_key ( m_categories, _normalize ( catname, parentcatname ) );
869
870   if ( ! c ) {
871     return ( 0 ); // wtf?
872   }
873
874   if ( ! c -> refs ) {
875     return ( 0 ); // no apps at all
876   }
877
878   mm_appref_t *iter = c -> refs;
879
880   while ( iter ) {
881
882     if ( strcmp ( iter -> ref -> unique_id, unique_id ) == 0 ) {
883       return ( 1 );
884     }
885
886     iter = iter -> next;
887   }
888
889   return ( 0 );
890 }