/[LeafOK_CVS]/lbbs/src/menu.c
ViewVC logotype

Contents of /lbbs/src/menu.c

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1.39 - (show annotations)
Wed May 14 04:22:45 2025 UTC (10 months ago) by sysadm
Branch: MAIN
Changes since 1.38: +17 -6 lines
Content type: text/x-csrc
Refine menu with trie_dict

1 /***************************************************************************
2 menu.c - description
3 -------------------
4 Copyright : (C) 2004-2025 by Leaflet
5 Email : leaflet@leafok.com
6 ***************************************************************************/
7
8 /***************************************************************************
9 * *
10 * This program is free software; you can redistribute it and/or modify *
11 * it under the terms of the GNU General Public License as published by *
12 * the Free Software Foundation; either version 3 of the License, or *
13 * (at your option) any later version. *
14 * *
15 ***************************************************************************/
16
17 #include "bbs.h"
18 #include "bbs_cmd.h"
19 #include "user_priv.h"
20 #include "bbs_cmd.h"
21 #include "menu.h"
22 #include "log.h"
23 #include "io.h"
24 #include "screen.h"
25 #include "common.h"
26 #include <string.h>
27 #include <stdio.h>
28 #include <ctype.h>
29 #include <stdlib.h>
30
31 #define MENU_SCREEN_PATH_PREFIX "var/MENU_SCR_"
32 #define MENU_CONF_DELIM_WITH_SPACE " ,\t\r\n"
33 #define MENU_CONF_DELIM_WITHOUT_SPACE "\r\n"
34
35 MENU_SET bbs_menu;
36
37 int load_menu(MENU_SET *p_menu_set, const char *conf_file)
38 {
39 FILE *fin, *fout;
40 int fin_line = 0;
41 int i = 0;
42 int j = 0;
43 char buffer[LINE_BUFFER_LEN];
44 char temp[LINE_BUFFER_LEN];
45 char screen_filename[FILE_PATH_LEN];
46 char *p = NULL;
47 char *q = NULL;
48 char *saveptr = NULL;
49 MENU *p_menu = NULL;
50 MENU_ITEM *p_item = NULL;
51
52 p_menu_set->menu_count = 0;
53 p_menu_set->p_menu_name_dict = trie_dict_create();
54
55 if ((fin = fopen(conf_file, "r")) == NULL)
56 {
57 log_error("Open %s failed", conf_file);
58 return -2;
59 }
60
61 strncpy(p_menu_set->conf_file, conf_file, sizeof(p_menu_set->conf_file) - 1);
62 p_menu_set->conf_file[sizeof(p_menu_set->conf_file) - 1] = '\0';
63
64 while (fgets(buffer, sizeof(buffer), fin))
65 {
66 fin_line++;
67
68 p = strtok_r(buffer, MENU_CONF_DELIM_WITH_SPACE, &saveptr);
69 if (p == NULL) // Blank line
70 {
71 continue;
72 }
73
74 if (*p == '#' || *p == '\r' || *p == '\n') // Comment or blank line
75 {
76 continue;
77 }
78
79 if (*p == '%')
80 {
81 p++;
82
83 if (strcmp(p, "menu") == 0) // BEGIN of sub-menu
84 {
85 if (p_menu != NULL)
86 {
87 log_error("Incomplete menu definition in menu config line %d\n", fin_line);
88 return -1;
89 }
90 p_menu = (MENU *)malloc(sizeof(MENU));
91 if (p_menu == NULL)
92 {
93 log_error("Unable to allocate memory for menu\n");
94 return -3;
95 }
96 p_menu_set->p_menu[i] = p_menu;
97 i++;
98 p_menu_set->menu_count = i;
99
100 j = 0; // Menu item counter
101 p_menu->item_count = 0;
102 p_menu->item_cur_pos = 0;
103 p_menu->title.show = 0;
104 p_menu->screen.show = 0;
105
106 q = strtok_r(NULL, MENU_CONF_DELIM_WITH_SPACE, &saveptr);
107 if (q == NULL)
108 {
109 log_error("Error menu name in menu config line %d\n", fin_line);
110 return -1;
111 }
112 p = q;
113 while (isalnum(*q) || *q == '_')
114 {
115 q++;
116 }
117 if (*q != '\0')
118 {
119 log_error("Error menu name in menu config line %d\n", fin_line);
120 return -1;
121 }
122
123 if (q - p > sizeof(p_menu->name) - 1)
124 {
125 log_error("Too longer menu name in menu config line %d\n", fin_line);
126 return -1;
127 }
128 strncpy(p_menu->name, p, sizeof(p_menu->name) - 1);
129 p_menu->name[sizeof(p_menu->name) - 1] = '\0';
130 if (trie_dict_set(p_menu_set->p_menu_name_dict, p_menu->name, (int64_t)p_menu) != 1)
131 {
132 log_error("Error set menu dict [%s]\n", p_menu->name);
133 }
134
135 // Check syntax
136 q = strtok_r(NULL, MENU_CONF_DELIM_WITH_SPACE, &saveptr);
137 if (q != NULL)
138 {
139 log_error("Unknown extra content in menu config line %d\n", fin_line);
140 return -1;
141 }
142
143 while (fgets(buffer, sizeof(buffer), fin))
144 {
145 fin_line++;
146
147 p = strtok_r(buffer, MENU_CONF_DELIM_WITH_SPACE, &saveptr);
148 if (p == NULL) // Blank line
149 {
150 continue;
151 }
152
153 if (*p == '#' || *p == '\r' || *p == '\n') // Comment or blank line
154 {
155 continue;
156 }
157
158 if (*p == '%') // END of sub-menu
159 {
160 p_menu = NULL;
161 break;
162 }
163 else if (*p == '!' || *p == '@')
164 {
165 // BEGIN of menu item
166 p_item = (MENU_ITEM *)malloc(sizeof(MENU_ITEM));
167 if (p_item == NULL)
168 {
169 log_error("Unable to allocate memory for menu item\n");
170 return -3;
171 }
172 p_menu->items[j] = p_item;
173 j++;
174 p_menu->item_count = j;
175
176 p_item->submenu = (*p == '!' ? 1 : 0);
177
178 // Menu item action
179 p++;
180 if (strcmp(p, "..") == 0) // Return to parent menu
181 {
182 q = p + 2; // strlen("..")
183 }
184 else
185 {
186 q = p;
187 while (isalnum(*q) || *q == '_')
188 {
189 q++;
190 }
191 if (*q != '\0')
192 {
193 log_error("Error menu item action in menu config line %d\n", fin_line);
194 return -1;
195 }
196 }
197
198 if (q - p > sizeof(p_item->action) - 1)
199 {
200 log_error("Too longer menu action in menu config line %d\n", fin_line);
201 return -1;
202 }
203 strncpy(p_item->action, p, sizeof(p_item->action) - 1);
204 p_item->action[sizeof(p_item->action) - 1] = '\0';
205
206 // Menu item row
207 q = strtok_r(NULL, MENU_CONF_DELIM_WITH_SPACE, &saveptr);
208 if (q == NULL)
209 {
210 log_error("Error menu item row in menu config line %d\n", fin_line);
211 return -1;
212 }
213 p = q;
214 while (isdigit(*q))
215 {
216 q++;
217 }
218 if (*q != '\0')
219 {
220 log_error("Error menu item row in menu config line %d\n", fin_line);
221 return -1;
222 }
223 p_item->row = atoi(p);
224
225 // Menu item col
226 q = strtok_r(NULL, MENU_CONF_DELIM_WITH_SPACE, &saveptr);
227 if (q == NULL)
228 {
229 log_error("Error menu item col in menu config line %d\n", fin_line);
230 return -1;
231 }
232 p = q;
233 while (isdigit(*q))
234 {
235 q++;
236 }
237 if (*q != '\0')
238 {
239 log_error("Error menu item col in menu config line %d\n", fin_line);
240 return -1;
241 }
242 p_item->col = atoi(p);
243
244 // Menu item priv
245 q = strtok_r(NULL, MENU_CONF_DELIM_WITH_SPACE, &saveptr);
246 if (q == NULL)
247 {
248 log_error("Error menu item priv in menu config line %d\n", fin_line);
249 return -1;
250 }
251 p = q;
252 while (isdigit(*q))
253 {
254 q++;
255 }
256 if (*q != '\0')
257 {
258 log_error("Error menu item priv in menu config line %d\n", fin_line);
259 return -1;
260 }
261 p_item->priv = atoi(p);
262
263 // Menu item level
264 q = strtok_r(NULL, MENU_CONF_DELIM_WITH_SPACE, &saveptr);
265 if (q == NULL)
266 {
267 log_error("Error menu item level in menu config line %d\n", fin_line);
268 return -1;
269 }
270 p = q;
271 while (isdigit(*q))
272 {
273 q++;
274 }
275 if (*q != '\0')
276 {
277 log_error("Error menu item level in menu config line %d\n", fin_line);
278 return -1;
279 }
280 p_item->level = atoi(p);
281
282 // Menu item name
283 q = strtok_r(NULL, MENU_CONF_DELIM_WITH_SPACE, &saveptr);
284 if (q == NULL || *q != '\"')
285 {
286 log_error("Error menu item name in menu config line %d\n", fin_line);
287 return -1;
288 }
289 q++;
290 p = q;
291 while (*q != '\0' && *q != '\"')
292 {
293 q++;
294 }
295 if (*q != '\"' || *(q + 1) != '\0')
296 {
297 log_error("Error menu item name in menu config line %d\n", fin_line);
298 return -1;
299 }
300 *q = '\0';
301
302 if (q - p > sizeof(p_item->name) - 1)
303 {
304 log_error("Too longer menu name in menu config line %d\n", fin_line);
305 return -1;
306 }
307 strncpy(p_item->name, p, sizeof(p_item->name) - 1);
308 p_item->name[sizeof(p_item->name) - 1] = '\0';
309
310 // Menu item text
311 q = strtok_r(NULL, MENU_CONF_DELIM_WITHOUT_SPACE, &saveptr);
312 if (q == NULL || (q = strchr(q, '\"')) == NULL)
313 {
314 log_error("Error menu item text in menu config line %d\n", fin_line);
315 return -1;
316 }
317 q++;
318 p = q;
319 while (*q != '\0' && *q != '\"')
320 {
321 q++;
322 }
323 if (*q != '\"')
324 {
325 log_error("Error menu item text in menu config line %d\n", fin_line);
326 return -1;
327 }
328 *q = '\0';
329
330 if (q - p > sizeof(p_item->text) - 1)
331 {
332 log_error("Too longer menu item text in menu config line %d\n", fin_line);
333 return -1;
334 }
335 strncpy(p_item->text, p, sizeof(p_item->text) - 1);
336 p_item->text[sizeof(p_item->text) - 1] = '\0';
337
338 // Check syntax
339 q = strtok_r(q + 1, MENU_CONF_DELIM_WITH_SPACE, &saveptr);
340 if (q != NULL)
341 {
342 log_error("Unknown extra content in menu config line %d\n", fin_line);
343 return -1;
344 }
345 }
346 else if (strcmp(p, "title") == 0)
347 {
348 p_menu->title.show = 1;
349
350 // Menu title row
351 q = strtok_r(NULL, MENU_CONF_DELIM_WITH_SPACE, &saveptr);
352 if (q == NULL)
353 {
354 log_error("Error menu title row in menu config line %d\n", fin_line);
355 return -1;
356 }
357 p = q;
358 while (isdigit(*q))
359 {
360 q++;
361 }
362 if (*q != '\0')
363 {
364 log_error("Error menu title row in menu config line %d\n", fin_line);
365 return -1;
366 }
367 p_menu->title.row = atoi(p);
368
369 // Menu title col
370 q = strtok_r(NULL, MENU_CONF_DELIM_WITH_SPACE, &saveptr);
371 if (q == NULL)
372 {
373 log_error("Error menu title col in menu config line %d\n", fin_line);
374 return -1;
375 }
376 p = q;
377 while (isdigit(*q))
378 {
379 q++;
380 }
381 if (*q != '\0')
382 {
383 log_error("Error menu title col in menu config line %d\n", fin_line);
384 return -1;
385 }
386 p_menu->title.col = atoi(p);
387
388 // Menu title text
389 q = strtok_r(NULL, MENU_CONF_DELIM_WITHOUT_SPACE, &saveptr);
390 if (q == NULL || (q = strchr(q, '\"')) == NULL)
391 {
392 log_error("Error menu title text in menu config line %d\n", fin_line);
393 return -1;
394 }
395 q++;
396 p = q;
397 while (*q != '\0' && *q != '\"')
398 {
399 q++;
400 }
401 if (*q != '\"')
402 {
403 log_error("Error menu title text in menu config line %d\n", fin_line);
404 return -1;
405 }
406 *q = '\0';
407
408 if (q - p > sizeof(p_item->text) - 1)
409 {
410 log_error("Too longer menu title text in menu config line %d\n", fin_line);
411 return -1;
412 }
413 strncpy(p_menu->title.text, p, sizeof(p_menu->title.text) - 1);
414 p_menu->title.text[sizeof(p_menu->title.text) - 1] = '\0';
415
416 // Check syntax
417 q = strtok_r(q + 1, MENU_CONF_DELIM_WITH_SPACE, &saveptr);
418 if (q != NULL)
419 {
420 log_error("Unknown extra content in menu config line %d\n", fin_line);
421 return -1;
422 }
423 }
424 else if (strcmp(p, "screen") == 0)
425 {
426 p_menu->screen.show = 1;
427
428 // Menu screen row
429 q = strtok_r(NULL, MENU_CONF_DELIM_WITH_SPACE, &saveptr);
430 if (q == NULL)
431 {
432 log_error("Error menu screen row in menu config line %d\n", fin_line);
433 return -1;
434 }
435 p = q;
436 while (isdigit(*q))
437 {
438 q++;
439 }
440 if (*q != '\0')
441 {
442 log_error("Error menu screen row in menu config line %d\n", fin_line);
443 return -1;
444 }
445 p_menu->screen.row = atoi(p);
446
447 // Menu screen col
448 q = strtok_r(NULL, MENU_CONF_DELIM_WITH_SPACE, &saveptr);
449 if (q == NULL)
450 {
451 log_error("Error menu screen col in menu config line %d\n", fin_line);
452 return -1;
453 }
454 p = q;
455 while (isdigit(*q))
456 {
457 q++;
458 }
459 if (*q != '\0')
460 {
461 log_error("Error menu screen col in menu config line %d\n", fin_line);
462 return -1;
463 }
464 p_menu->screen.col = atoi(p);
465
466 // Menu screen name
467 q = strtok_r(NULL, MENU_CONF_DELIM_WITH_SPACE, &saveptr);
468 if (q == NULL)
469 {
470 log_error("Error menu screen name in menu config line %d\n", fin_line);
471 return -1;
472 }
473 p = q;
474 while (isalnum(*q) || *q == '_')
475 {
476 q++;
477 }
478 if (*q != '\0')
479 {
480 log_error("Error menu screen name in menu config line %d\n", fin_line);
481 return -1;
482 }
483
484 snprintf(p_menu->screen.filename, sizeof(p_menu->screen.filename), "%s%s", MENU_SCREEN_PATH_PREFIX, p);
485
486 // Check syntax
487 q = strtok_r(NULL, MENU_CONF_DELIM_WITH_SPACE, &saveptr);
488 if (q != NULL)
489 {
490 log_error("Unknown extra content in menu config line %d\n", fin_line);
491 return -1;
492 }
493 }
494 }
495 }
496 else // BEGIN of menu screen
497 {
498 q = p;
499 while (isalnum(*q) || *q == '_')
500 {
501 q++;
502 }
503 if (*q != '\0')
504 {
505 log_error("Error menu screen name in menu config line %d\n", fin_line);
506 return -1;
507 }
508
509 snprintf(screen_filename, sizeof(screen_filename), "%s%s", MENU_SCREEN_PATH_PREFIX, p);
510
511 // Check syntax
512 q = strtok_r(NULL, MENU_CONF_DELIM_WITH_SPACE, &saveptr);
513 if (q != NULL)
514 {
515 log_error("Unknown extra content in menu config line %d\n", fin_line);
516 return -1;
517 }
518
519 if ((fout = fopen(screen_filename, "w")) == NULL)
520 {
521 log_error("Open %s failed", screen_filename);
522 return -2;
523 }
524
525 while (fgets(buffer, sizeof(buffer), fin))
526 {
527 fin_line++;
528
529 strncpy(temp, buffer, sizeof(temp)); // Duplicate line for strtok_r
530 p = strtok_r(temp, MENU_CONF_DELIM_WITH_SPACE, &saveptr);
531 if (p != NULL && *p == '%') // END of menu screen
532 {
533 break;
534 }
535
536 if (fputs(buffer, fout) < 0)
537 {
538 log_error("Write %s failed", screen_filename);
539 return -2;
540 }
541 }
542
543 fclose(fout);
544 }
545 }
546 else // Invalid prefix
547 {
548 log_error("Error in menu config line %d\n", fin_line);
549 return -1;
550 }
551 }
552 fclose(fin);
553
554 p_menu_set->menu_count = i;
555 p_menu_set->menu_select_depth = 0;
556 p_menu_set->p_menu_select[p_menu_set->menu_select_depth] = (i == 0 ? NULL : p_menu_set->p_menu[0]);
557
558 return 0;
559 }
560
561 MENU *get_menu(MENU_SET *p_menu_set, const char *menu_name)
562 {
563 int ret;
564 int64_t value = 0;
565
566 ret = trie_dict_get(p_menu_set->p_menu_name_dict, menu_name, &value);
567
568 if (ret == 1) // found
569 {
570 return ((MENU *)value);
571 }
572
573 return NULL;
574 }
575
576 static void display_menu_cursor(MENU *p_menu, int show)
577 {
578 moveto((p_menu->items[p_menu->item_cur_pos])->r_row,
579 (p_menu->items[p_menu->item_cur_pos])->r_col - 2);
580 outc(show ? '>' : ' ');
581 iflush();
582 }
583
584 int display_menu(MENU *p_menu)
585 {
586 int row = 0;
587 int col = 0;
588 int menu_selectable = 0;
589
590 if (p_menu == NULL)
591 {
592 return -1;
593 }
594
595 if (p_menu->item_cur_pos > 0 &&
596 checkpriv(&BBS_priv, 0, p_menu->items[p_menu->item_cur_pos]->priv) != 0 &&
597 checklevel(&BBS_priv, p_menu->items[p_menu->item_cur_pos]->level) != 0)
598 {
599 menu_selectable = 1;
600 }
601
602 if (p_menu->title.show)
603 {
604 show_top(p_menu->title.text);
605 }
606
607 if (p_menu->screen.show)
608 {
609 moveto(p_menu->screen.row, p_menu->screen.col);
610 if (display_file(p_menu->screen.filename) != 0)
611 {
612 log_error("Display menu screen <%s> failed!\n",
613 p_menu->screen.filename);
614 }
615 }
616
617 for (int i = 0; i < p_menu->item_count; i++)
618 {
619 if (p_menu->items[i]->row != 0)
620 {
621 row = p_menu->items[i]->row;
622 }
623 if (p_menu->items[i]->col != 0)
624 {
625 col = p_menu->items[i]->col;
626 }
627
628 if (checkpriv(&BBS_priv, 0, p_menu->items[i]->priv) == 0 || checklevel(&BBS_priv, p_menu->items[i]->level) == 0)
629 {
630 p_menu->items[i]->display = 0;
631 p_menu->items[i]->r_row = 0;
632 p_menu->items[i]->r_col = 0;
633 }
634 else
635 {
636 p_menu->items[i]->display = 1;
637
638 if (!menu_selectable)
639 {
640 p_menu->item_cur_pos = i;
641 menu_selectable = 1;
642 }
643
644 p_menu->items[i]->r_row = row;
645 p_menu->items[i]->r_col = col;
646
647 moveto(row, col);
648 prints("%s", p_menu->items[i]->text);
649
650 row++;
651 }
652 }
653
654 if (!menu_selectable)
655 {
656 return -1;
657 }
658
659 display_menu_cursor(p_menu, 1);
660
661 return 0;
662 }
663
664 int display_current_menu(MENU_SET *p_menu_set)
665 {
666 MENU *p_menu;
667
668 p_menu = p_menu_set->p_menu_select[p_menu_set->menu_select_depth];
669
670 return display_menu(p_menu);
671 }
672
673 int menu_control(MENU_SET *p_menu_set, int key)
674 {
675 int i;
676 MENU *p_menu;
677
678 if (p_menu_set->menu_count == 0)
679 {
680 return 0;
681 }
682
683 p_menu = p_menu_set->p_menu_select[p_menu_set->menu_select_depth];
684
685 switch (key)
686 {
687 case CR:
688 case KEY_RIGHT:
689 if (p_menu->items[p_menu->item_cur_pos]->submenu)
690 {
691 if (strcmp(p_menu->items[p_menu->item_cur_pos]->action, "..") == 0)
692 {
693 return menu_control(p_menu_set, KEY_LEFT);
694 }
695 p_menu_set->menu_select_depth++;
696 p_menu = get_menu(p_menu_set, p_menu->items[p_menu->item_cur_pos]->action);
697 p_menu_set->p_menu_select[p_menu_set->menu_select_depth] = p_menu;
698
699 if (display_menu(p_menu) != 0)
700 {
701 return menu_control(p_menu_set, KEY_LEFT);
702 }
703 }
704 else
705 {
706 return (exec_cmd(p_menu->items[p_menu->item_cur_pos]->action,
707 p_menu->items[p_menu->item_cur_pos]->name));
708 }
709 break;
710 case KEY_LEFT:
711 if (p_menu_set->menu_select_depth > 0)
712 {
713 p_menu_set->menu_select_depth--;
714 if (display_current_menu(p_menu_set) != 0)
715 {
716 return menu_control(p_menu_set, KEY_LEFT);
717 }
718 }
719 else
720 {
721 display_menu_cursor(p_menu, 0);
722 p_menu->item_cur_pos = p_menu->item_count - 1;
723 while (p_menu->item_cur_pos >= 0 && (!p_menu->items[p_menu->item_cur_pos]->display ||
724 p_menu->items[p_menu->item_cur_pos]->priv != 0 ||
725 p_menu->items[p_menu->item_cur_pos]->level != 0))
726 {
727 p_menu->item_cur_pos--;
728 }
729 display_menu_cursor(p_menu, 1);
730 }
731 break;
732 case KEY_UP:
733 display_menu_cursor(p_menu, 0);
734 do
735 {
736 p_menu->item_cur_pos--;
737 if (p_menu->item_cur_pos < 0)
738 {
739 p_menu->item_cur_pos = p_menu->item_count - 1;
740 }
741 } while (!p_menu->items[p_menu->item_cur_pos]->display);
742 display_menu_cursor(p_menu, 1);
743 break;
744 case KEY_DOWN:
745 display_menu_cursor(p_menu, 0);
746 do
747 {
748 p_menu->item_cur_pos++;
749 if (p_menu->item_cur_pos >= p_menu->item_count)
750 {
751 p_menu->item_cur_pos = 0;
752 }
753 } while (!p_menu->items[p_menu->item_cur_pos]->display);
754 display_menu_cursor(p_menu, 1);
755 break;
756 default:
757 if (isalnum(key))
758 {
759 for (i = 0; i < p_menu->item_count; i++)
760 {
761 if (toupper(key) == toupper(p_menu->items[i]->name[0]) &&
762 p_menu->items[i]->display)
763 {
764 display_menu_cursor(p_menu, 0);
765 p_menu->item_cur_pos = i;
766 display_menu_cursor(p_menu, 1);
767 return 0;
768 }
769 }
770 }
771 break;
772 }
773
774 return 0;
775 }
776
777 void unload_menu(MENU_SET *p_menu_set)
778 {
779 MENU *p_menu;
780 int i, j;
781
782 if (p_menu_set->p_menu_name_dict != NULL)
783 {
784 trie_dict_destroy(p_menu_set->p_menu_name_dict);
785 p_menu_set->p_menu_name_dict = NULL;
786 }
787
788 for (i = 0; i < p_menu_set->menu_count; i++)
789 {
790 p_menu = p_menu_set->p_menu[i];
791 for (j = 0; j < p_menu->item_count; j++)
792 {
793 free(p_menu->items[j]);
794 }
795 free(p_menu);
796 }
797
798 p_menu_set->menu_count = 0;
799 p_menu_set->menu_select_depth = 0;
800 }
801
802 int reload_menu(MENU_SET *p_menu_set)
803 {
804 int result;
805 char conf_file[FILE_PATH_LEN];
806
807 strncpy(conf_file, p_menu_set->conf_file, sizeof(conf_file) - 1);
808 conf_file[sizeof(conf_file) - 1] = '\0';
809
810 unload_menu(p_menu_set);
811 result = load_menu(p_menu_set, conf_file);
812
813 return result;
814 }

webmaster@leafok.com
ViewVC Help
Powered by ViewVC 1.3.0-beta1