]> andersk Git - moira.git/blame - clients/moira/menu.c
forgot needed include file...
[moira.git] / clients / moira / menu.c
CommitLineData
07c56447 1/*
2aaf83c2 2 * Copyright 1987 by the Massachusetts Institute of Technology.
3 * For copying and distribution information, see the file
4 * "mit-copyright.h".
5 *
6 * $Source$
7 * $Author$
8 * $Header$
2aaf83c2 9 *
07c56447 10 * Generic menu system module.
11 *
12 * Basically, we define an enormous tree structure which represents the
13 * menu. Some extra pieces (ml_command, ma_doc) get thrown in so we can
14 * also use the structure for a command-based system.
15 *
16 * By making the menu descriptions so general, we can ease porting to just
17 * about anything.
18 */
19
2aaf83c2 20#ifndef lint
21static char rcsid_menu_c[] = "$Header$";
e9db439c 22
2aaf83c2 23#endif lint
24
34f47457 25#include <sys/types.h>
07c56447 26#include <stdio.h>
967b7c08 27#include <signal.h>
07c56447 28#include <curses.h>
2aaf83c2 29#include <ctype.h>
34f47457 30#include <strings.h>
93b28238 31#include <varargs.h>
07c56447 32#include "menu.h"
33
967b7c08 34#define MAX(A,B) ((A) > (B) ? (A) : (B))
35#define MIN(A,B) ((A) < (B) ? (A) : (B))
36#define CTL(ch) ((ch) & 037)
07c56447 37
38#define MIN_INPUT 2 /* Minimum number of lines for input window */
39
34f47457 40extern char *calloc();
e9db439c 41int more_flg = 1;
2aaf83c2 42
07c56447 43/* Structure for holding current displayed menu */
44struct menu_screen {
45 WINDOW *ms_screen; /* Window for this menu */
46 WINDOW *ms_title; /* Title subwindow */
47 WINDOW *ms_menu; /* Menu subwindow */
48 WINDOW *ms_input; /* Input subwindow */
49 int ms_input_y; /* Input subwindow reference coordinate */
e9db439c 50} *cur_ms;
07c56447 51
2aaf83c2 52#define NULLMS ((struct menu_screen *) 0)
53
54Menu *top_menu; /* Root for command search */
55
07c56447 56/*
57 * Start_menu takes a menu as an argument. It initializes curses u.s.w.,
58 * and a quit in any submenu should unwind back to here. (it might not,
59 * if user functions which run their own menus don't cooperate.)
60 * Start_menu should only be called once, at the start of the program.
61 */
62Start_menu(m)
e9db439c 63 Menu *m;
07c56447 64{
2aaf83c2 65 struct menu_screen *make_ms();
66
e9db439c 67 if (initscr() == ERR) {
07c56447 68 fputs("Can't initialize curses!\n", stderr);
2aaf83c2 69 Start_no_menu(m);
07c56447 70 }
34f47457 71 (void) raw(); /* We parse & print everything ourselves */
72 (void) noecho();
967b7c08 73 cur_ms = make_ms(0); /* So we always have some current */
74 /* menu_screen */
2aaf83c2 75 top_menu = m;
74e7b641 76 /* Run the menu */
967b7c08 77 (void) Do_menu(m, -1, (char **) NULL);
78 Cleanup_menu();
79}
80
81Cleanup_menu()
82{
83 if (cur_ms) {
34f47457 84 (void) wclear(cur_ms->ms_screen);
85 (void) wrefresh(cur_ms->ms_screen);
967b7c08 86 }
07c56447 87 endwin();
88}
967b7c08 89
07c56447 90
2aaf83c2 91/* Like Start_menu, except it doesn't print menus and doesn't use curses */
92Start_no_menu(m)
e9db439c 93 Menu *m;
2aaf83c2 94{
95 cur_ms = NULLMS;
96 top_menu = m;
74e7b641 97 /* Run the menu */
967b7c08 98 (void) Do_menu(m, -1, (char **) NULL);
2aaf83c2 99}
100
07c56447 101/*
102 * Create a new menu screen template with the specified menu length
103 * and return it.
104 */
e9db439c 105struct menu_screen *
106make_ms(length)
107 int length;
07c56447 108{
109 struct menu_screen *ms;
110 char *malloc();
111
e9db439c 112 if (MAX_TITLE + length + MIN_INPUT > LINES) {
07c56447 113 fputs("Menu too big!\n", stderr);
114 exit(2);
115 }
116
117 ms = (struct menu_screen *) malloc(sizeof(struct menu_screen));
118
119 ms->ms_screen = newwin(0, 0, 0, 0);
120 ms->ms_title = subwin(ms->ms_screen, MAX_TITLE, 0, 0, 0);
121 ms->ms_menu = subwin(ms->ms_screen,
122 length, 0, MAX_TITLE, 0);
123 ms->ms_input = subwin(ms->ms_screen, 0, 0,
124 ms->ms_input_y = MAX_TITLE + length,
125 0);
126
127 scrollok(ms->ms_input, TRUE);
2aaf83c2 128 (void) wmove(ms->ms_input, 0, 0);
129 (void) wclear(ms->ms_screen);
07c56447 130
e9db439c 131 return (ms);
07c56447 132}
133
134/*
135 * This routine destroys a menu_screen.
136 */
137destroy_ms(ms)
e9db439c 138 struct menu_screen *ms;
07c56447 139{
140 delwin(ms->ms_title);
141 delwin(ms->ms_menu);
142 delwin(ms->ms_input);
143 delwin(ms->ms_screen);
e9db439c 144 free((char *) ms);
07c56447 145}
146
147/*
148 * This guy actually puts up the menu
967b7c08 149 * Note: if margc < 0, no 'r' option will be displayed (i.e., on the
150 * top level menu)
07c56447 151 */
e9db439c 152int
74e7b641 153Do_menu(m, margc, margv)
e9db439c 154 Menu *m;
74e7b641 155 int margc;
156 char *margv[];
07c56447 157{
158 struct menu_screen *my_ms, *old_cur_ms;
159 char argvals[MAX_ARGC][MAX_ARGLEN]; /* This is where args are stored */
2aaf83c2 160 char buf[MAX_ARGC * MAX_ARGLEN];
07c56447 161 char *argv[MAX_ARGC];
162 int line;
2aaf83c2 163 int i;
164 struct menu_line *command, *Find_command();
07c56447 165 int argc;
967b7c08 166 int quitflag, is_topmenu = (margc < 0);
167
07c56447 168 /* Entry function gets called with old menu_screen still current */
e9db439c 169 if (m->m_entry != NULLFUNC)
74e7b641 170 if (m->m_entry(m, margc, margv) == DM_QUIT)
171 return DM_NORMAL;
07c56447 172
2aaf83c2 173 /* The following get run only in curses mode */
e9db439c 174 if (cur_ms != NULLMS) {
2aaf83c2 175 /* Get a menu_screen */
176 old_cur_ms = cur_ms;
967b7c08 177 cur_ms = my_ms = make_ms(m->m_length + 1 + (is_topmenu?0:1));
07c56447 178
2aaf83c2 179 /* Now print the title and the menu */
e9db439c 180 (void) wclear(my_ms->ms_menu);
181 (void) wmove(my_ms->ms_title, 0, MAX(0, (COLS -
182 strlen(m->m_title)) >> 1));
2aaf83c2 183 (void) wstandout(my_ms->ms_title);
184 (void) waddstr(my_ms->ms_title, m->m_title);
185 (void) wstandend(my_ms->ms_title);
186
e9db439c 187 for (line = 0; line < m->m_length; line++) {
2aaf83c2 188 (void) wmove(my_ms->ms_menu, line, 0);
e9db439c 189 (void) wprintw(my_ms->ms_menu, "%2d. (%-12s) %s.", line + 1,
190 m->m_lines[line].ml_command,
191 m->m_lines[line].ml_doc);
2aaf83c2 192 }
193 (void) wmove(my_ms->ms_menu, line++, 0);
967b7c08 194 if (!is_topmenu) {
195 (void) waddstr(my_ms->ms_menu,
196 " r. (return ) Return to previous menu.");
197 (void) wmove(my_ms->ms_menu, line, 0);
198 }
2aaf83c2 199 (void) waddstr(my_ms->ms_menu, " q. (quit ) Quit.");
07c56447 200
07c56447 201 }
07c56447 202
e9db439c 203 for (;;) {
07c56447 204 /* This will be set by a return val from func or submenu */
205 quitflag = DM_NORMAL;
2aaf83c2 206 /* This is here because we may be coming from another menu */
e9db439c 207 if (cur_ms != NULL)
208 touchwin(my_ms->ms_screen);
07c56447 209 /* Get a command */
967b7c08 210 if (!Prompt_input("Command: ", buf, sizeof(buf)))
211 continue;
2aaf83c2 212 /* Parse it into the argument list */
213 /* If there's nothing there, try again */
214 /* Initialize argv */
e9db439c 215 for (argc = 0; argc < MAX_ARGC; argc++)
216 argv[argc] = argvals[argc];
217
218 if ((argc = Parse_words(buf, argv, MAX_ARGLEN)) == 0)
219 continue;
220 if ((line = atoi(argv[0])) > 0 && line <= m->m_length) {
221 command = &m->m_lines[line - 1];
222 }
967b7c08 223 else if ((!is_topmenu &&
224 (!strcmp(argv[0], "r")
225 || !strcmp(argv[0], "return")))
e9db439c 226 || !strcmp(argv[0], "q")
e9db439c 227 || !strcmp(argv[0], "quit")) {
07c56447 228 /* here if it's either return or quit */
e9db439c 229 if (cur_ms != NULLMS) {
2aaf83c2 230 cur_ms = old_cur_ms;
231 destroy_ms(my_ms);
232 }
e9db439c 233 if (m->m_exit != NULLFUNC)
234 m->m_exit(m);
235 return (*argv[0] == 'r' ? DM_NORMAL : DM_QUIT);
236 /* finally, try to find it using Find_command */
237 }
238 else if ((command = Find_command(argvals[0])) ==
239 (struct menu_line *) 0) {
240 Put_message("Command not recognized");
241 continue;
242 }
2aaf83c2 243 /* If we got to here, command is a valid menu_line */
244 /* Send the offical command name into the argv */
245 (void) strcpy(argvals[0], command->ml_command);
246 /* Show that we're working on it */
247 Put_message(command->ml_doc);
248 /* Print args that we've already got */
e9db439c 249 for (i = 1; i < argc; i++) {
250 if (command->ml_args[i].ma_prompt == NULL)
251 break;
2aaf83c2 252 (void) sprintf(buf, "%s%s", command->ml_args[i].ma_prompt,
253 argv[i]);
254 Put_message(buf);
255 }
256 /* Get remaining arguments, if any */
e9db439c 257 for (; argc < command->ml_argc; argc++) {
967b7c08 258 if (!Prompt_input(command->ml_args[argc].ma_prompt,
259 argvals[argc], sizeof(argvals[argc])))
260 goto punt_command;
2aaf83c2 261 }
e9db439c 262 if (command->ml_function != NULLFUNC) {
2aaf83c2 263 /* If it's got a function, call it */
264 quitflag = command->ml_function(argc, argv);
e9db439c 265 }
266 else if (command->ml_submenu != NULLMENU) {
2aaf83c2 267 /* Else see if it is a submenu */
74e7b641 268 quitflag = Do_menu(command->ml_submenu, argc, argv);
e9db439c 269 }
270 else {
2aaf83c2 271 /* If it's got neither, something is wrong */
272 Put_message("*INTERNAL ERROR: NO FUNCTION OR MENU FOR LINE*");
07c56447 273 }
e9db439c 274 if (quitflag == DM_QUIT) {
275 if (cur_ms != NULLMS) {
2aaf83c2 276 cur_ms = old_cur_ms;
277 destroy_ms(my_ms);
278 }
e9db439c 279 if (m->m_exit != NULLFUNC)
280 m->m_exit(m);
281 return (DM_QUIT);
07c56447 282 }
967b7c08 283 punt_command:
284 ;
07c56447 285 }
286}
287
288/* Prompt the user for input in the input window of cur_ms */
967b7c08 289int Prompt_input(prompt, buf, buflen)
e9db439c 290 char *prompt;
291 char *buf;
292 int buflen;
07c56447 293{
294 int c;
295 char *p;
296 int y, x, oldx;
297
e9db439c 298 if (cur_ms != NULLMS) {
299 more_flg = 1;
300 getyx(cur_ms->ms_input, y, x);
301 (void) wmove(cur_ms->ms_input, y, 0);
302
303 touchwin(cur_ms->ms_screen);
304 refresh_ms(cur_ms);
2aaf83c2 305 (void) waddstr(cur_ms->ms_input, prompt);
306 getyx(cur_ms->ms_input, y, x);
e9db439c 307
2aaf83c2 308 oldx = x;
f659480d 309 p = buf;
310 while(1) {
311/* for (p = buf; p - buf < buflen;) { */
2aaf83c2 312 (void) wmove(cur_ms->ms_input, y, x);
313 (void) wclrtoeol(cur_ms->ms_input);
314 refresh_ms(cur_ms);
315 c = getchar();
316 switch (c) {
967b7c08 317 case CTL('C'):
318 return 0;
319 case CTL('Z'):
34f47457 320 (void) kill(getpid(), SIGTSTP);
967b7c08 321 touchwin(curscr);
322 break;
323 case CTL('L'):
e9db439c 324 (void) wclear(cur_ms->ms_input);
325 (void) waddstr(cur_ms->ms_input, prompt);
34f47457 326 (void) wrefresh(curscr);
e9db439c 327 getyx(cur_ms->ms_input, y, x);
2aaf83c2 328 break;
e9db439c 329 case '\n':
330 case '\r':
f659480d 331 goto end_input;
2aaf83c2 332 case '\b':
333 case '\177':
334 if (p > buf) {
335 p--;
336 x--;
337 }
338 break;
967b7c08 339 case CTL('U'):
340 case CTL('G'):
341 case CTL('['):
2aaf83c2 342 x = oldx;
e9db439c 343 p = buf;
2aaf83c2 344 break;
345 default:
f659480d 346 if (isprint(c) && (p - buf < buflen)) {
967b7c08 347 (void) waddch(cur_ms->ms_input, c);
348 *p++ = c;
349 x++;
350 } else
351 putchar(CTL('G'));
2aaf83c2 352 break;
07c56447 353 }
07c56447 354 }
f659480d 355 end_input:
356 (void) waddch(cur_ms->ms_input, '\n');
357 (void) waddch(cur_ms->ms_input, '\r');
358
359 (void) wclrtoeol(cur_ms->ms_input);
360 refresh_ms(cur_ms);
361 *p = '\0';
362 Start_paging();
363 return 1;
364 } else {
2aaf83c2 365 printf("%s", prompt);
967b7c08 366 if (gets(buf) == NULL)
367 return 0;
2aaf83c2 368 Start_paging();
967b7c08 369 return 1;
07c56447 370 }
371}
372
27de3061 373/* Prompt the user for input in the input window of cur_ms, but don't echo
374 and allow some control characters */
375int Password_input(prompt, buf, buflen)
376 char *prompt;
377 char *buf;
378 int buflen;
379{
380 int c;
381 char *p;
382 int y, x, oldx;
383
384 if (cur_ms != NULLMS) {
385 more_flg = 1;
386 getyx(cur_ms->ms_input, y, x);
387 (void) wmove(cur_ms->ms_input, y, 0);
388
389 touchwin(cur_ms->ms_screen);
390 refresh_ms(cur_ms);
391 (void) waddstr(cur_ms->ms_input, prompt);
392 getyx(cur_ms->ms_input, y, x);
393
394 oldx = x;
395 for (p = buf; p - buf < buflen;) {
396 (void) wmove(cur_ms->ms_input, y, x);
397 (void) wclrtoeol(cur_ms->ms_input);
398 refresh_ms(cur_ms);
399 c = getchar();
400 switch (c) {
401 case CTL('C'):
402 return 0;
403 case CTL('Z'):
34f47457 404 (void) kill(getpid(), SIGTSTP);
27de3061 405 touchwin(curscr);
406 break;
407 case CTL('L'):
408 (void) wclear(cur_ms->ms_input);
409 (void) waddstr(cur_ms->ms_input, prompt);
34f47457 410 (void) wrefresh(curscr);
27de3061 411 getyx(cur_ms->ms_input, y, x);
412 break;
413 case '\n':
414 case '\r':
415 (void) waddch(cur_ms->ms_input, '\n');
416 (void) waddch(cur_ms->ms_input, '\r');
417
418 (void) wclrtoeol(cur_ms->ms_input);
419 refresh_ms(cur_ms);
420 *p = '\0';
421 Start_paging();
422 return 1;
423 case '\b':
424 case '\177':
425 if (p > buf) {
426 p--;
427 x--;
428 }
429 break;
430 case CTL('U'):
431 x = oldx;
432 p = buf;
433 break;
434 default:
435 *p++ = c;
436 break;
437 }
438 }
439 }
440 else {
441 struct sgttyb ttybuf, nttybuf;
442 printf("%s", prompt);
443 /* turn off echoing */
34f47457 444 (void) ioctl(0, TIOCGETP, (char *)&ttybuf);
27de3061 445 nttybuf = ttybuf;
446 nttybuf.sg_flags &= ~ECHO;
34f47457 447 (void)ioctl(0, TIOCSETP, (char *)&nttybuf);
27de3061 448 if (gets(buf) == NULL) {
34f47457 449 (void) ioctl(0, TIOCSETP, (char *)&ttybuf);
27de3061 450 putchar('\n');
451 return 0;
452 }
453 putchar('\n');
34f47457 454 (void) ioctl(0, TIOCSETP, (char *)&ttybuf);
27de3061 455 Start_paging();
456 return 1;
457 }
34f47457 458 return 0;
27de3061 459}
460
2aaf83c2 461int lines_left;
462
463/* Start paging */
464/* This routine will cause the most recently put message to be the
465 one at the top of the screen when a ---More--- prompt is displayed */
466Start_paging()
467{
e9db439c 468 if (cur_ms != NULLMS) {
2aaf83c2 469 lines_left = LINES - cur_ms->ms_input_y - 1;
e9db439c 470 }
471 else {
2aaf83c2 472 lines_left = 23;
473 }
474}
475
476/* Turn off paging */
477Stop_paging()
478{
479 lines_left = -1;
480}
481
f659480d 482/* Print a message in the input window of cur_ms. */
07c56447 483Put_message(msg)
f659480d 484char *msg;
485{
486 char *copy, *line, *s;
487
34f47457 488 copy = (char *) malloc((u_int)COLS);
f659480d 489 s = line = msg;
490 while(*s++) {
491 if (s - line >= COLS-1) {
34f47457 492 (void) strncpy(copy, line, COLS-1);
f659480d 493 line += COLS-1;
494 } else if (*s == '\n') {
495 *s = '\0';
34f47457 496 (void) strcpy(copy, line);
f659480d 497 line = ++s;
498 } else
499 continue;
500 Put_line(copy);
501 }
502 Put_line(line);
503 free(copy);
504}
505
506/* Will be truncated to COLS characters. */
507Put_line(msg)
508char *msg;
07c56447 509{
e9db439c 510 int y, x, i;
511 char *msg1, chr;
512
513 if (!more_flg)
514 return;
2aaf83c2 515
e9db439c 516 if (lines_left >= 0) {
517 if (--lines_left == 0) {
2aaf83c2 518 /* Give the user a more prompt */
e9db439c 519 if (cur_ms != NULLMS) {
2aaf83c2 520 (void) wstandout(cur_ms->ms_input);
521 (void) wprintw(cur_ms->ms_input, "---More---");
522 (void) wstandend(cur_ms->ms_input);
523 refresh_ms(cur_ms);
e9db439c 524 chr = getchar();/* We do care what it is */
525 if (chr == 'q' || chr == 'Q') {
526 more_flg = 0;
527 return;
528 }
2aaf83c2 529 getyx(cur_ms->ms_input, y, x);
34f47457 530 /* x is a bitbucket; avoid lint problems */
531 x=x;
2aaf83c2 532 (void) wmove(cur_ms->ms_input, y, 0);
533 (void) wclrtoeol(cur_ms->ms_input);
e9db439c 534 }
535 else {
2aaf83c2 536 printf("---More (hit return)---");
537 (void) getchar();
538 }
539 Start_paging(); /* Reset lines_left */
540 }
541 }
e9db439c 542
543 if (cur_ms != NULLMS) {
34f47457 544 msg1 = (char *) calloc((u_int)COLS, 1);
f659480d 545 (void) strncpy(msg1, msg, COLS-1);
546 for (i = strlen(msg1); i < COLS - 1; i++)
e9db439c 547 msg1[i] = ' ';
548 (void) wprintw(cur_ms->ms_input, "%s\n", msg1);
2aaf83c2 549/* refresh_ms(cur_ms); */
e9db439c 550 }
551 else {
2aaf83c2 552 puts(msg);
553 }
07c56447 554}
555
556/* Refresh a menu_screen onto the real screen */
557refresh_ms(ms)
e9db439c 558 struct menu_screen *ms;
07c56447 559{
560 int y, x;
561
562 getyx(ms->ms_input, y, x);
2aaf83c2 563 (void) wmove(ms->ms_screen, y + ms->ms_input_y, x);
564 (void) wrefresh(ms->ms_screen);
565}
566
567/* Parse buf into a list of words, which will be placed in strings specified by
568 argv. Space for these strings must have already been allocated.
569 Only the first n characters of each word will be copied */
570Parse_words(buf, argv, n)
e9db439c 571 char *buf;
572 char *argv[];
2aaf83c2 573int n;
e9db439c 574
2aaf83c2 575{
576 char *start, *end; /* For sausage machine */
577 int argc;
578
579 start = buf;
e9db439c 580 for (argc = 0; argc < MAX_ARGC; argc++) {
581 while (isspace(*start))
582 start++; /* Kill whitespace */
583 if (*start == '\0')
584 break; /* Nothing left */
2aaf83c2 585 /* Now find the end of the word */
e9db439c 586 for (end = start; *end != '\0' && !isspace(*end); end++);
587 (void) strncpy(argv[argc], start, MIN(end - start, n)); /* Copy it */
588 argv[argc][MIN(end - start, n - 1)] = '\0'; /* Terminate */
2aaf83c2 589 start = end;
590 }
e9db439c 591 return (argc);
2aaf83c2 592}
593
594/* This is the internal form of Find_command, which recursively searches
595 for a menu_line with command command in the specified menu */
596/* It will search to a maximum depth of d */
e9db439c 597struct menu_line *
598find_command_from(c, m, d)
599 char *c;
600 struct menu *m;
601 int d;
2aaf83c2 602{
603 int line;
604 struct menu_line *maybe;
605
e9db439c 606 if (d < 0)
607 return ((struct menu_line *) 0); /* Too deep! */
608 for (line = 0; line < m->m_length; line++) {
609 if (!strcmp(c, m->m_lines[line].ml_command)) {
610 return (&m->m_lines[line]);
611 }
612 else if (m->m_lines[line].ml_submenu != NULLMENU
613 && (maybe =
614 find_command_from(c, m->m_lines[line].ml_submenu, d - 1))
615 != (struct menu_line *) 0) {
616 return (maybe);
617 }
2aaf83c2 618 }
619 /* If we got to here, nothing works */
e9db439c 620 return ((struct menu_line *) 0);
07c56447 621}
2aaf83c2 622
623/* Find_command searches down the current menu tree */
624/* And returns a pointer to a menu_line with the specified command name */
625/* It returns (struct menu_line *) 0 if none is found */
e9db439c 626struct menu_line *
627Find_command(command)
628 char *command;
2aaf83c2 629{
e9db439c 630 if (top_menu == NULLMENU) {
631 return ((struct menu_line *) 0);
632 }
633 else {
634 return (find_command_from(command, top_menu, MAX_MENU_DEPTH));
2aaf83c2 635 }
636}
22705d2b 637
638void
639sms_com_err_hook(who, code, fmt, args)
640 char *who;
641 int code;
642 char *fmt;
643 va_list args;
644{
645 char buf[BUFSIZ], *cp;
646
647 FILE _strbuf;
648
649 (void) strcpy(buf, who);
650 for (cp = buf; *cp; cp++);
651 *cp++ = ':';
652 *cp++ = ' ';
653 if (code) {
654 (void) strcpy(cp, error_message(code));
655 while (*cp)
656 cp++;
657 }
658 _strbuf._flag = _IOWRT + _IOSTRG;
659 _strbuf._ptr = cp;
660 _strbuf._cnt = BUFSIZ - (cp - buf);
661 _doprnt(fmt, args, &_strbuf);
662 (void) putc('\0', &_strbuf);
663 Put_message(buf);
664}
665
666/*
667 * Local Variables:
668 * mode: c
669 * c-indent-level: 4
670 * c-continued-statement-offset: 4
671 * c-brace-offset: -4
672 * c-argdecl-indent: 4
673 * c-label-offset: -4
674 * End:
675 */
This page took 0.163227 seconds and 5 git commands to generate.