5efff6f24570a0555c44653df4e8157e8b540cb6
[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 #if 0 // prepending
300   char catnamebuffer [ 512 ] = "";
301 #endif
302
303   if ( ! catname ) {
304     return ( 1 ); // fine, just nada
305   }
306
307   //fprintf ( stderr, "meta push: '%s'\n", catname );
308
309   // push bad categories into Other (if we're not targeting All right now)
310   if ( pnd_conf_get_as_int_d ( g_conf, "categories.good_cats_only", 1 ) ) {
311
312     // don't audit All
313     if ( strncmp ( catname, "All ", 4 ) != 0 ) {
314
315       // if this is a parent cat..
316       if ( catname && ! parentcatname ) {
317
318         // if bad, shove it to Other
319         if ( ! freedesktop_check_cat ( catname ) ) {
320           parentcatname = NULL;
321           catname = BADCATNAME;
322           visiblep = cat_is_visible ( g_conf, catname );
323         }
324
325       } else if ( catname && parentcatname ) {
326         // this is a subcat
327
328         // if parent is bad, then we probably already pushed it over, so don't do it again.
329         // if its parent is okay, but subcat is bad, push it to other. (ie: lets avoid duplication in Other)
330         if ( ! freedesktop_check_cat ( parentcatname ) ) {
331           // skip
332           return ( 1 );
333
334         } else if ( ! freedesktop_check_cat ( catname ) ) {
335           parentcatname = NULL;
336           catname = BADCATNAME;
337           visiblep = cat_is_visible ( g_conf, catname );
338         }
339
340       } // parent or child cat?
341
342     } // not All
343
344   } // good cats only?
345
346   // if invisible, and a parent category name is known, prepend it for ease of use
347 #if 0 // prepending
348   if ( ! visiblep && parentcatname ) {
349     snprintf ( catnamebuffer, 500, "%s.%s", parentcatname, catname );
350     catname = catnamebuffer;
351   }
352 #endif
353
354   // do we honour cat mapping at all?
355   if ( pnd_conf_get_as_int_d ( g_conf, "categories.map_on", 0 ) ) {
356
357     // is this guy mapped?
358     cat = category_map_query ( catname );
359
360     if ( cat ) {
361       category_push ( cat -> catname, parentcatname /* parent cat */, app, ovrh, NULL /* fspath */, visiblep );
362       goto meta_done;
363     }
364
365     // not mapped.. but default?
366     if ( pnd_conf_get_as_int_d ( g_conf, "categories.map_default_on", 0 ) ) {
367       char *def = pnd_conf_get_as_char ( g_conf, "categories.map_default_cat" );
368       if ( def ) {
369         category_push ( def, parentcatname /* parent cat */, app, ovrh, NULL /* fspath */, visiblep );
370         goto meta_done;
371       }
372     }
373
374   } // cat map is desired?
375
376   // not default, just do it
377   category_push ( catname, parentcatname /* parent cat */, app, ovrh, NULL /* fspath */, visiblep );
378
379   // if subcats as folders, then lets just make up a dummy app that pretends to be a folder,
380   // and stuff it into the parent cat
381   if ( parentcatname && pnd_conf_get_as_int_d ( g_conf, "tabs.subcat_as_folders", 1 ) ) {
382
383     // it is implicit that since we're talking parentcat, its already been created in a previous call
384     // therefore, we need to..
385     // i) find the parent cat
386     // ii) ensure it already has a faux-disco container
387     // iii) ensure that disco container doesn't already contain a disco-entry for this subcat
388     // iv) create the dummy app folder by pushing the disco into the apprefs as normal
389     // v) create a dummy '..' for going back up, in the child
390
391     mm_category_t *pcat = pnd_box_find_by_key ( m_categories, parentcatname );
392
393     if ( ! pcat -> disco ) {
394       pcat -> disco = pnd_box_new ( pcat -> catname );
395     }
396
397     // if this subcat is already in the faux-disco list, then its probably already
398     // handled so we needn't concern ourselves anymore. If not, then we can
399     // create it and push it into the parent as a new 'app'
400     pnd_disco_t *disco = pnd_box_find_by_key ( pcat -> disco, catname );
401
402     if ( ! disco ) {
403
404       disco = pnd_box_allocinsert ( pcat -> disco, catname, sizeof(pnd_disco_t) );
405       if ( disco ) {
406
407         // create the subcat faux-disco entry, and register into parent cat
408         char uid [ 30 ];
409         sprintf ( uid, "%p", catname );
410
411         disco -> unique_id = strdup ( uid );
412         if ( strchr ( catname, '.' ) ) {
413           disco -> title_en = strdup ( strchr ( catname, '.' ) + 1 );
414         } else {
415           disco -> title_en = strdup ( catname );
416         }
417         disco -> object_flags = PND_DISCO_GENERATED;
418         disco -> object_type = pnd_object_type_directory; // suggest to Grid that its a dir
419         disco -> object_path = strdup ( catname );
420
421         category_push ( parentcatname, NULL /* parent cat */, disco, 0 /*ovrh*/, NULL /* fspath */, 1 /* visible */ );
422
423         // create .. faux-disco entry into child cat
424         disco = pnd_box_allocinsert ( pcat -> disco, catname, sizeof(pnd_disco_t) );
425
426         sprintf ( uid, "%p", uid );
427
428         disco -> unique_id = strdup ( uid );
429         disco -> title_en = strdup ( ".." );
430         disco -> object_flags = PND_DISCO_GENERATED;
431         disco -> object_type = pnd_object_type_directory; // suggest to Grid that its a dir
432
433         category_push ( catname, parentcatname /* parent cat */, disco, 0 /*ovrh*/, NULL /* fspath */, 1 /* visible */ );
434
435       } // making faux disco entries
436
437     } // disco already exist?
438
439   } // subcat as folder?
440
441   // hack :(
442  meta_done:
443
444   //fprintf ( stderr, "cat meta-push : vis[%30s,%d b] : tally; vis %d invis %d\n", catname, visiblep, g_categorycount, _categories_inviscount );
445
446   return ( 1 );
447 }
448
449 unsigned char category_fs_restock ( mm_category_t *cat ) {
450
451   if ( ! cat -> fspath ) {
452     return ( 1 ); // not a filesystem browser tab
453   }
454
455   // clear any existing baggage
456   //
457
458   // apprefs
459   mm_appref_t *iter = cat -> refs, *next;
460   while ( iter ) {
461     next = iter -> next;
462     free ( iter );
463     iter = next;
464   }
465   cat -> refs = NULL;
466
467   // discos
468   if ( cat -> disco ) {
469     pnd_disco_t *p = pnd_box_get_head ( cat -> disco );
470     pnd_disco_t *n;
471     while ( p ) {
472       n = pnd_box_get_next ( p );
473       pnd_disco_destroy ( p );
474       p = n;
475     }
476     pnd_box_delete ( cat -> disco );
477   }
478
479   // rescan the filesystem
480   //
481
482   //pnd_log ( pndn_debug, "Restocking cat %s with path %s\n", cat -> catname, cat -> fspath );
483   DIR *d;
484
485   if ( ( d = opendir ( cat -> fspath ) ) ) {
486     struct dirent *de = readdir ( d );
487
488     pnd_disco_t *disco;
489     char uid [ 100 ];
490
491     cat -> disco = pnd_box_new ( cat -> catname );
492
493     while ( de ) {
494
495       struct stat buffy;
496       char fullpath [ PATH_MAX ];
497       sprintf ( fullpath, "%s/%s", cat -> fspath, de -> d_name );
498       int statret = stat ( fullpath, &buffy );
499
500       // if file is executable somehow or another
501       if ( statret == 0 &&
502            buffy.st_mode & (S_IXUSR|S_IXGRP|S_IXOTH)
503          )
504       {
505         // determine unique-id
506         sprintf ( uid, "%d", (int) de -> d_ino );
507         disco = NULL;
508
509         switch ( de -> d_type ) {
510
511         case DT_DIR:
512           if ( strcmp ( de -> d_name, "." ) == 0 ) {
513             // ignore ".", but ".." is fine
514           } else if ( strcmp ( de -> d_name, ".." ) == 0 && strcmp ( cat -> fspath, "/" ) == 0 ) {
515             // ignore ".." only if we're at the true root
516           } else {
517             disco = pnd_box_allocinsert ( cat -> disco, uid, sizeof(pnd_disco_t) );
518             disco -> object_type = pnd_object_type_directory; // suggest to Grid that its a dir
519           }
520           break;
521         case DT_UNKNOWN:
522         case DT_REG:
523           disco = pnd_box_allocinsert ( cat -> disco, uid, sizeof(pnd_disco_t) );
524           disco -> object_type = pnd_object_type_unknown; // suggest to Grid that its a file
525           break;
526
527         } // switch
528
529         // found a directory or executable?
530         if ( disco ) {
531           // register with current category
532           disco -> unique_id = strdup ( uid );
533           disco -> title_en = strdup ( de -> d_name );
534           disco -> object_flags = PND_DISCO_GENERATED;
535           disco -> object_path = strdup ( cat -> fspath );
536           disco -> object_filename = strdup ( de -> d_name );
537           category_push ( cat -> catname, NULL /* parent cat */, disco, 0 /* no ovr */, NULL /* fspath already set */, 1 /* visible */ );
538           // if a override icon exists, cache it up
539           cache_icon ( disco, pnd_conf_get_as_int_d ( g_conf, "grid.icon_max_width", 50 ),
540                        pnd_conf_get_as_int_d ( g_conf, "grid.icon_max_height", 50 ) );
541         }
542
543       } // stat
544
545       // next
546       de = readdir ( d );
547     }
548
549     closedir ( d );
550   }
551
552   return ( 1 );
553 }
554
555 static int catname_cmp ( const void *p1, const void *p2 ) {
556   //mm_category_t *c1 = (mm_category_t*) p1;
557   //mm_category_t *c2 = (mm_category_t*) p2;
558   mm_category_t *c1 = *( (mm_category_t**) p1 );
559   mm_category_t *c2 = *( (mm_category_t**) p2 );
560
561   if ( ( isalnum ( c1 -> catname [ 0 ] ) ) && ( ! isalnum ( c1 -> catname [ 1 ] ) ) ) {
562     return ( -1 );
563   } else if ( ( ! isalnum ( c1 -> catname [ 0 ] ) ) && ( isalnum ( c1 -> catname [ 1 ] ) ) ) {
564     return ( 1 );
565   } else if ( ( ! isalnum ( c1 -> catname [ 0 ] ) ) && ( ! isalnum ( c1 -> catname [ 1 ] ) ) ) {
566     return ( 0 );
567   }
568
569   int i = strcasecmp ( c1 -> catname, c2 -> catname );
570   //printf ( "cat name compare %p %s to %p %s = %d\n", p1, c1 -> catname, p2, c2 -> catname, i );
571
572   return ( i );
573 }
574
575 void category_sort ( void ) {
576   // we probably don't want to sort tab categories, since the user may have specified an ordering
577   // But we can sort invisi-cats, to make them easier to find, and ordered by parent category
578
579 #if 0
580   qsort ( _categories_invis, _categories_inviscount, sizeof(mm_category_t), catname_cmp );
581 #endif
582
583 #if 1
584   qsort ( g_categories, g_categorycount, sizeof(mm_category_t*), catname_cmp );
585 #endif
586
587   return;
588 }
589
590 void category_publish ( unsigned int filter_mask, char *param ) {
591   unsigned char interested;
592
593   // clear published categories
594   memset ( g_categories, '\0', sizeof(mm_category_t*) * MAX_CATS );
595   g_categorycount = 0;
596
597   // figure out the start
598   mm_category_t *iter = pnd_box_get_head ( m_categories );
599
600   // for each category we know...
601   while ( iter ) {
602
603     interested = 0;
604
605     // is this category desired?
606     if ( filter_mask == CFALL ) {
607       interested = 1;
608     } else if ( filter_mask == CFBYNAME ) {
609       if ( strcasecmp ( iter -> catname, param ) == 0 ) {
610         interested = 1;
611       }
612     } else if ( iter -> catflags == filter_mask ) {
613       interested = 1;
614     } // if
615
616     if ( interested ) {
617       // set us up the bomb; notice that we're just duplicating the pointers, not making
618       // any new data here; none of this should ever be free'd!
619       g_categories [ g_categorycount ] = iter;
620       g_categorycount++;
621     }
622
623     // next
624     iter = pnd_box_get_next ( iter );
625   }
626
627   // dump
628 #if 0
629   unsigned int i;
630   for ( i = 0; i < g_categorycount; i++ ) {
631     printf ( "Unsorted cat %d %p: %s\n", i, &(g_categories [ i ]), g_categories [ i ] -> catname );
632   }
633 #endif
634
635   // sort published categories
636   category_sort();
637
638   return;
639 }
640
641 unsigned int category_count ( unsigned int filter_mask ) {
642   mm_category_t *iter = pnd_box_get_head ( m_categories );
643   unsigned int count = 0;
644
645   // for each category we know...
646   while ( iter ) {
647
648     // is this category desired?
649     if ( iter -> catflags == filter_mask ) {
650       count++;
651     } // if
652
653     // next
654     iter = pnd_box_get_next ( iter );
655   }
656
657   return ( count );
658 }
659
660 int category_index ( char *catname ) {
661   unsigned char i;
662  
663   for ( i = 0; i < g_categorycount; i++ ) {
664  
665     if ( strcasecmp ( g_categories [ i ] -> catname, catname ) == 0 ) {
666       return ( i );
667     }
668  
669   }
670  
671   return ( -1 );
672 }