added 'subcats as folders' option and support, set default ON.
[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 unsigned char category_push ( char *catname, char *parentcatname, pnd_disco_t *app, pnd_conf_handle ovrh, char *fspath, unsigned char visiblep ) {
43   mm_category_t *c;
44
45   // check category list; if found, append app to the end of it.
46   // if not found, add it to the category list and plop the app in there.
47   // app's are just app-refs, which contain links to the disco-t list -- thus removal is only in one place, and
48   // an app can be in multiple categories if we like..
49   //
50
51   // find or create category
52   //
53
54   if ( ( c = pnd_box_find_by_key ( m_categories, catname ) ) ) {
55     // category was found..
56   } else {
57     // category wasn't found..
58     //pnd_log ( PND_LOG_DEFAULT, "New category '%s'\n", catname );
59     c = pnd_box_allocinsert ( m_categories, catname, sizeof(mm_category_t) );
60     c -> catname = strdup ( catname );
61     if ( parentcatname ) {
62       c -> parent_catname = strdup ( parentcatname );
63     }
64
65     if ( visiblep ) {
66       c -> catflags = CFNORMAL;
67     } else {
68       c -> catflags = CFHIDDEN;
69     }
70
71     // if user prefers subcats-as-folders, lets reflag this sucker
72     if ( parentcatname && pnd_conf_get_as_int_d ( g_conf, "tabs.subcat_as_folders", 1 ) ) {
73       //printf ( "subcat: %s parent: %s\n", catname, parentcatname ? parentcatname : "none" );
74       c -> catflags = CFSUBCAT;
75     }
76
77     c -> refs = NULL;
78
79     if ( fspath ) {
80       c -> fspath = strdup ( fspath );
81     }
82
83     m_categorycount++;
84   }
85
86   if ( ! app ) {
87     return ( 1 ); // create cat, but skip app
88   }
89
90   // alloc and populate appref
91   //
92   mm_appref_t *ar = malloc ( sizeof(mm_appref_t) );
93   if ( ! ar ) {
94     return ( 0 );
95   }
96
97   bzero ( ar, sizeof(mm_appref_t) );
98
99   ar -> ref = app;
100   ar -> ovrh = ovrh;
101
102   // plug it into category
103   //   and sort it on insert!
104 #if 0 // no sorting
105   ar -> next = c -> refs;
106   c -> refs = ar;
107 #else // with sorting
108   // if no refs at all, or new guy has no title, just stick it in at head
109   if ( c -> refs && ar -> ref -> title_en ) {
110     mm_appref_t *iter = c -> refs;
111     mm_appref_t *last = NULL;
112
113     while ( iter ) {
114
115       if ( iter -> ref -> title_en ) {
116         if ( cat_sort_score ( c, ar, iter ) < 0 ) {
117           // new guy is smaller than the current guy!
118           break;
119         }
120       } else {
121         // since new guy must have a name by here, we're bigger than any guy who does not have a name
122         // --> continue
123       }
124
125       last = iter;
126       iter = iter -> next;
127     }
128
129     if ( iter ) {
130       // smaller than the current guy, so stitch in
131       if ( last ) {
132         ar -> next = iter;
133         last -> next = ar;
134       } else {
135         ar -> next = c -> refs;
136         c -> refs = ar;
137       }
138     } else {
139       // we're the biggest, just append to last
140       last -> next = ar;
141     }
142
143   } else {
144     ar -> next = c -> refs;
145     c -> refs = ar;
146   }
147 #endif
148   c -> refcount++;
149
150   return ( 1 );
151 }
152
153 int cat_sort_score ( mm_category_t *cat, mm_appref_t *s1, mm_appref_t *s2 ) {
154
155   // are we in a directory browser, or looking at pnd-files?
156   if ( cat -> fspath ) {
157
158     if ( s1 == s2 ) {
159       return ( 0 ); // equal
160
161     } else if ( s1 -> ref -> object_type == pnd_object_type_directory &&
162                 s2 -> ref -> object_type == pnd_object_type_directory )
163     {
164       // both are directories, be nice
165       return ( strcmp ( s1 -> ref -> title_en, s2 -> ref -> title_en ) );
166     } else if ( s1 -> ref -> object_type == pnd_object_type_directory &&
167                 s2 -> ref -> object_type != pnd_object_type_directory )
168     {
169       return ( -1 ); // dir on the left is earlier than file on the right
170     } else if ( s1 -> ref -> object_type != pnd_object_type_directory &&
171                 s2 -> ref -> object_type == pnd_object_type_directory )
172     {
173       return ( 1 ); // dir on the right is earlier than file on the left
174     } else {
175       // file on file
176       return ( strcmp ( s1 -> ref -> title_en, s2 -> ref -> title_en ) );
177     }
178
179   }
180
181   return ( strcasecmp ( s1 -> ref -> title_en, s2 -> ref -> title_en ) );
182 }
183
184 void category_dump ( void ) {
185
186   // WHY AREN'T I SORTING ON INSERT?
187
188   // dump
189   mm_category_t *iter = pnd_box_get_head ( m_categories );
190   unsigned int counter = 0;
191
192   while ( iter ) {
193
194     pnd_log ( PND_LOG_DEFAULT, "Category %u: '%s' * %u\n", counter, iter -> catname, iter -> refcount );
195     mm_appref_t *ar = iter -> refs;
196
197     while ( ar ) {
198       pnd_log ( PND_LOG_DEFAULT, "  Appref %s\n", IFNULL(ar -> ref -> title_en,"No Name") );
199       ar = ar -> next;
200     }
201
202     iter = pnd_box_get_next ( iter );
203     counter++;
204   }
205
206   return;
207 }
208
209 void category_freeall ( void ) {
210   mm_category_t *c, *cnext;
211   mm_appref_t *iter, *next;
212
213   c = pnd_box_get_head ( m_categories );
214
215   while ( c ) {
216     cnext = pnd_box_get_next ( c );
217
218     // wipe 'em
219     iter = c -> refs;
220
221     while ( iter ) {
222       next = iter -> next;
223       free ( iter );
224       iter = next;
225     }
226
227     c -> refs = NULL;
228
229     if ( c -> catname ) {
230       free ( c -> catname );
231       c -> catname = NULL;
232     }
233
234     if ( c -> fspath ) {
235       free ( c -> fspath );
236       c -> fspath = NULL;
237     }
238
239     pnd_box_delete_node ( m_categories, c );
240
241     // next
242     c = cnext;
243   }
244
245   return;
246 }
247
248 unsigned char category_map_setup ( void ) {
249
250   char *searchpath = pnd_box_get_head ( g_conf );
251
252   if ( ! searchpath ) {
253     return ( 0 );
254   }
255
256   // look through conf for useful keys
257   while ( searchpath ) {
258     char *k = pnd_box_get_key ( searchpath );
259
260     // does this key look like a category mapping key?
261     if ( strncasecmp ( k, "categories.@", 12 ) == 0 ) {
262       k += 12;
263
264       // iterate across 'words' in v, assigning catmaps to them
265       SEARCHCHUNK_PRE
266       {
267         //pnd_log ( pndn_debug, "target(%s) from(%s)\n", k, buffer );
268
269         category_push ( k, NULL /* parent cat */, NULL, 0, NULL /* fspath */, 1 );
270         g_catmaps [ g_catmapcount ].target = pnd_box_find_by_key ( m_categories, k );
271         g_catmaps [ g_catmapcount ].from = strdup ( buffer );
272         g_catmapcount++;
273
274       }
275       SEARCHCHUNK_POST
276
277     } // if key looks like catmap
278
279     searchpath = pnd_box_get_next ( searchpath );
280   } // while each conf key
281
282   return ( 1 );
283 }
284
285 mm_category_t *category_map_query ( char *cat ) {
286   unsigned char i;
287
288   for ( i = 0; i < g_catmapcount; i++ ) {
289     if ( strcasecmp ( g_catmaps [ i ].from, cat ) == 0 ) {
290       return ( g_catmaps [ i ].target );
291     }
292   }
293
294   return ( NULL );
295 }
296
297 unsigned char category_meta_push ( char *catname, char *parentcatname, pnd_disco_t *app, pnd_conf_handle ovrh, unsigned char visiblep ) {
298   mm_category_t *cat;
299   char catnamebuffer [ 512 ] = "";
300
301   if ( ! catname ) {
302     return ( 1 ); // fine, just nada
303   }
304
305   //fprintf ( stderr, "meta push: '%s'\n", catname );
306
307   // push bad categories into Other (if we're not targeting All right now)
308   if ( pnd_conf_get_as_int_d ( g_conf, "categories.good_cats_only", 1 ) ) {
309
310     // don't audit All
311     if ( strncmp ( catname, "All ", 4 ) != 0 ) {
312
313       // if this is a parent cat..
314       if ( catname && ! parentcatname ) {
315
316         // if bad, shove it to Other
317         if ( ! freedesktop_check_cat ( catname ) ) {
318           parentcatname = NULL;
319           catname = BADCATNAME;
320           visiblep = cat_is_visible ( g_conf, catname );
321         }
322
323       } else if ( catname && parentcatname ) {
324         // this is a subcat
325
326         // if parent is bad, then we probably already pushed it over, so don't do it again.
327         // if its parent is okay, but subcat is bad, push it to other. (ie: lets avoid duplication in Other)
328         if ( ! freedesktop_check_cat ( parentcatname ) ) {
329           // skip
330           return ( 1 );
331
332         } else if ( ! freedesktop_check_cat ( catname ) ) {
333           parentcatname = NULL;
334           catname = BADCATNAME;
335           visiblep = cat_is_visible ( g_conf, catname );
336         }
337
338       } // parent or child cat?
339
340     } // not All
341
342   } // good cats only?
343
344   // if invisible, and a parent category name is known, prepend it for ease of use
345   if ( ! visiblep && parentcatname ) {
346     snprintf ( catnamebuffer, 500, "%s.%s", parentcatname, catname );
347     catname = catnamebuffer;
348   }
349
350   // do we honour cat mapping at all?
351   if ( pnd_conf_get_as_int_d ( g_conf, "categories.map_on", 0 ) ) {
352
353     // is this guy mapped?
354     cat = category_map_query ( catname );
355
356     if ( cat ) {
357       category_push ( cat -> catname, parentcatname /* parent cat */, app, ovrh, NULL /* fspath */, visiblep );
358       goto meta_done;
359     }
360
361     // not mapped.. but default?
362     if ( pnd_conf_get_as_int_d ( g_conf, "categories.map_default_on", 0 ) ) {
363       char *def = pnd_conf_get_as_char ( g_conf, "categories.map_default_cat" );
364       if ( def ) {
365         category_push ( def, parentcatname /* parent cat */, app, ovrh, NULL /* fspath */, visiblep );
366         goto meta_done;
367       }
368     }
369
370   } // cat map is desired?
371
372   // not default, just do it
373   category_push ( catname, parentcatname /* parent cat */, app, ovrh, NULL /* fspath */, visiblep );
374
375   // if subcats as folders, then lets just make up a dummy app that pretends to be a folder,
376   // and stuff it into the parent cat
377   if ( parentcatname && pnd_conf_get_as_int_d ( g_conf, "tabs.subcat_as_folders", 1 ) ) {
378
379     // it is implicit that since we're talking parentcat, its already been created in a previous call
380     // therefore, we need to..
381     // i) find the parent cat
382     // ii) ensure it already has a faux-disco container
383     // iii) ensure that disco container doesn't already contain a disco-entry for this subcat
384     // iv) create the dummy app folder by pushing the disco into the apprefs as normal
385     // v) create a dummy '..' for going back up, in the child
386
387     mm_category_t *pcat = pnd_box_find_by_key ( m_categories, parentcatname );
388
389     if ( ! pcat -> disco ) {
390       pcat -> disco = pnd_box_new ( pcat -> catname );
391     }
392
393     // if this subcat is already in the faux-disco list, then its probably already
394     // handled so we needn't concern ourselves anymore. If not, then we can
395     // create it and push it into the parent as a new 'app'
396     pnd_disco_t *disco = pnd_box_find_by_key ( pcat -> disco, catname );
397
398     if ( ! disco ) {
399
400       disco = pnd_box_allocinsert ( pcat -> disco, catname, sizeof(pnd_disco_t) );
401       if ( disco ) {
402
403         // create the subcat faux-disco entry, and register into parent cat
404         char uid [ 30 ];
405         sprintf ( uid, "%p", catname );
406
407         disco -> unique_id = strdup ( uid );
408         if ( strchr ( catname, '.' ) ) {
409           disco -> title_en = strdup ( strchr ( catname, '.' ) + 1 );
410         } else {
411           disco -> title_en = strdup ( catname );
412         }
413         disco -> object_flags = PND_DISCO_GENERATED;
414         disco -> object_type = pnd_object_type_directory; // suggest to Grid that its a dir
415         disco -> object_path = strdup ( catname );
416
417         category_push ( parentcatname, NULL /* parent cat */, disco, 0 /*ovrh*/, NULL /* fspath */, 1 /* visible */ );
418
419         // create .. faux-disco entry into child cat
420         disco = pnd_box_allocinsert ( pcat -> disco, catname, sizeof(pnd_disco_t) );
421
422         sprintf ( uid, "%p", uid );
423
424         disco -> unique_id = strdup ( uid );
425         disco -> title_en = strdup ( ".." );
426         disco -> object_flags = PND_DISCO_GENERATED;
427         disco -> object_type = pnd_object_type_directory; // suggest to Grid that its a dir
428
429         category_push ( catname, parentcatname /* parent cat */, disco, 0 /*ovrh*/, NULL /* fspath */, 1 /* visible */ );
430
431       } // making faux disco entries
432
433     } // disco already exist?
434
435   } // subcat as folder?
436
437   // hack :(
438  meta_done:
439
440   //fprintf ( stderr, "cat meta-push : vis[%30s,%d b] : tally; vis %d invis %d\n", catname, visiblep, g_categorycount, _categories_inviscount );
441
442   return ( 1 );
443 }
444
445 unsigned char category_fs_restock ( mm_category_t *cat ) {
446
447   if ( ! cat -> fspath ) {
448     return ( 1 ); // not a filesystem browser tab
449   }
450
451   // clear any existing baggage
452   //
453
454   // apprefs
455   mm_appref_t *iter = cat -> refs, *next;
456   while ( iter ) {
457     next = iter -> next;
458     free ( iter );
459     iter = next;
460   }
461   cat -> refs = NULL;
462
463   // discos
464   if ( cat -> disco ) {
465     pnd_disco_t *p = pnd_box_get_head ( cat -> disco );
466     pnd_disco_t *n;
467     while ( p ) {
468       n = pnd_box_get_next ( p );
469       pnd_disco_destroy ( p );
470       p = n;
471     }
472     pnd_box_delete ( cat -> disco );
473   }
474
475   // rescan the filesystem
476   //
477
478   //pnd_log ( pndn_debug, "Restocking cat %s with path %s\n", cat -> catname, cat -> fspath );
479   DIR *d;
480
481   if ( ( d = opendir ( cat -> fspath ) ) ) {
482     struct dirent *de = readdir ( d );
483
484     pnd_disco_t *disco;
485     char uid [ 100 ];
486
487     cat -> disco = pnd_box_new ( cat -> catname );
488
489     while ( de ) {
490
491       struct stat buffy;
492       char fullpath [ PATH_MAX ];
493       sprintf ( fullpath, "%s/%s", cat -> fspath, de -> d_name );
494       int statret = stat ( fullpath, &buffy );
495
496       // if file is executable somehow or another
497       if ( statret == 0 &&
498            buffy.st_mode & (S_IXUSR|S_IXGRP|S_IXOTH)
499          )
500       {
501         // determine unique-id
502         sprintf ( uid, "%d", (int) de -> d_ino );
503         disco = NULL;
504
505         switch ( de -> d_type ) {
506
507         case DT_DIR:
508           if ( strcmp ( de -> d_name, "." ) == 0 ) {
509             // ignore ".", but ".." is fine
510           } else {
511             disco = pnd_box_allocinsert ( cat -> disco, uid, sizeof(pnd_disco_t) );
512             disco -> object_type = pnd_object_type_directory; // suggest to Grid that its a dir
513           }
514           break;
515         case DT_UNKNOWN:
516         case DT_REG:
517           disco = pnd_box_allocinsert ( cat -> disco, uid, sizeof(pnd_disco_t) );
518           disco -> object_type = pnd_object_type_unknown; // suggest to Grid that its a file
519           break;
520
521         } // switch
522
523         // found a directory or executable?
524         if ( disco ) {
525           // register with current category
526           disco -> unique_id = strdup ( uid );
527           disco -> title_en = strdup ( de -> d_name );
528           disco -> object_flags = PND_DISCO_GENERATED;
529           disco -> object_path = strdup ( cat -> fspath );
530           disco -> object_filename = strdup ( de -> d_name );
531           category_push ( cat -> catname, NULL /* parent cat */, disco, 0 /* no ovr */, NULL /* fspath already set */, 1 /* visible */ );
532           // if a override icon exists, cache it up
533           cache_icon ( disco, pnd_conf_get_as_int_d ( g_conf, "grid.icon_max_width", 50 ),
534                        pnd_conf_get_as_int_d ( g_conf, "grid.icon_max_height", 50 ) );
535         }
536
537       } // stat
538
539       // next
540       de = readdir ( d );
541     }
542
543     closedir ( d );
544   }
545
546   return ( 1 );
547 }
548
549 static int catname_cmp ( const void *p1, const void *p2 ) {
550   //mm_category_t *c1 = (mm_category_t*) p1;
551   //mm_category_t *c2 = (mm_category_t*) p2;
552   mm_category_t *c1 = *( (mm_category_t**) p1 );
553   mm_category_t *c2 = *( (mm_category_t**) p2 );
554
555   if ( ( isalnum ( c1 -> catname [ 0 ] ) ) && ( ! isalnum ( c1 -> catname [ 1 ] ) ) ) {
556     return ( -1 );
557   } else if ( ( ! isalnum ( c1 -> catname [ 0 ] ) ) && ( isalnum ( c1 -> catname [ 1 ] ) ) ) {
558     return ( 1 );
559   } else if ( ( ! isalnum ( c1 -> catname [ 0 ] ) ) && ( ! isalnum ( c1 -> catname [ 1 ] ) ) ) {
560     return ( 0 );
561   }
562
563   int i = strcasecmp ( c1 -> catname, c2 -> catname );
564   //printf ( "cat name compare %p %s to %p %s = %d\n", p1, c1 -> catname, p2, c2 -> catname, i );
565
566   return ( i );
567 }
568
569 void category_sort ( void ) {
570   // we probably don't want to sort tab categories, since the user may have specified an ordering
571   // But we can sort invisi-cats, to make them easier to find, and ordered by parent category
572
573 #if 0
574   qsort ( _categories_invis, _categories_inviscount, sizeof(mm_category_t), catname_cmp );
575 #endif
576
577 #if 1
578   qsort ( g_categories, g_categorycount, sizeof(mm_category_t*), catname_cmp );
579 #endif
580
581   return;
582 }
583
584 void category_publish ( unsigned int filter_mask, char *param ) {
585   unsigned char interested;
586
587   // clear published categories
588   memset ( g_categories, '\0', sizeof(mm_category_t*) * MAX_CATS );
589   g_categorycount = 0;
590
591   // figure out the start
592   mm_category_t *iter = pnd_box_get_head ( m_categories );
593
594   // for each category we know...
595   while ( iter ) {
596
597     interested = 0;
598
599     // is this category desired?
600     if ( filter_mask == CFALL ) {
601       interested = 1;
602     } else if ( filter_mask == CFBYNAME ) {
603       if ( strcasecmp ( iter -> catname, param ) == 0 ) {
604         interested = 1;
605       }
606     } else if ( iter -> catflags == filter_mask ) {
607       interested = 1;
608     } // if
609
610     if ( interested ) {
611       // set us up the bomb; notice that we're just duplicating the pointers, not making
612       // any new data here; none of this should ever be free'd!
613       g_categories [ g_categorycount ] = iter;
614       g_categorycount++;
615     }
616
617     // next
618     iter = pnd_box_get_next ( iter );
619   }
620
621   // dump
622 #if 0
623   unsigned int i;
624   for ( i = 0; i < g_categorycount; i++ ) {
625     printf ( "Unsorted cat %d %p: %s\n", i, &(g_categories [ i ]), g_categories [ i ] -> catname );
626   }
627 #endif
628
629   // sort published categories
630   category_sort();
631
632   return;
633 }
634
635 unsigned int category_count ( unsigned int filter_mask ) {
636   mm_category_t *iter = pnd_box_get_head ( m_categories );
637   unsigned int count = 0;
638
639   // for each category we know...
640   while ( iter ) {
641
642     // is this category desired?
643     if ( iter -> catflags == filter_mask ) {
644       count++;
645     } // if
646
647     // next
648     iter = pnd_box_get_next ( iter );
649   }
650
651   return ( count );
652 }
653
654 int category_index ( char *catname ) {
655   unsigned char i;
656  
657   for ( i = 0; i < g_categorycount; i++ ) {
658  
659     if ( strcasecmp ( g_categories [ i ] -> catname, catname ) == 0 ) {
660       return ( i );
661     }
662  
663   }
664  
665   return ( -1 );
666 }