Saturday, February 16, 2013

Simple interactive menu

 it turned into something a bit more serious and may be useful as a code snippet.
The code shows three files:
  • menu.h: The header file for the menu library.
  • menu.c: Implementation of the menu library functions.
  • main.c: A sample driver that uses the library.
The menu library is fairly generic in that you can provide a format string of menu options and possible matching input values. See main.c for an example of how to use it.

   /* menu.h */  
   #ifndef MENU_H  
   #define MENU_H  
   #define PROMPT_SIZ 128  
   #define ALT_SIZ 128  
   #define MENU_ALTS 11  
   typedef struct menuitem {  
   char alt[MENU_ALTS][ALT_SIZ];  
   char prompt[PROMPT_SIZ];  
   } menuitem_t;  
   #ifdef __cplusplus  
   extern "C" {  
   #endif  
   extern int menu(menuitem_t **items, char *selection, size_t limit);  
   extern menuitem_t **extract_menu_items(const char *options);  
   extern void destroy_menu_items(menuitem_t **items);  
   #ifdef __cplusplus  
   }  
   #endif  
   #endif  
   /* menu.c */  
   #include <stdio.h>  
   #include <stdlib.h>  
   #include <string.h>  
   #include "menu.h"  
   /*  
   @description:  
   Displays the prompts for a collection of menu items and gathers an interactive selection.  
   @returns: True if a matching selection was detected, false otherwise.  
   */  
   extern int menu(menuitem_t **items, char *selection, size_t limit)  
   {  
   size_t i, j;  
   if (items) {  
   /* Display the prompts */  
   for (i = 0; items[i]; i++)  
   puts(items[i]->prompt);  
   fputs("> ", stdout);  
   fflush(stdout);  
   /* Process an interactive selection */  
   if (fgets(selection, limit, stdin)) {  
   char *newline = strchr(selection, '\n');  
   if (newline)  
   *newline = '\0'; /* Trim the newline for convenience */  
   else {  
   int ch;  
   /* Discard extraneous line characters */  
   do  
   ch = fgetc(stdin);  
   while (ch != '\n' && ch != EOF);  
   clearerr(stdin);  
   }  
   for (i = 0; items[i]; i++) {  
   /* Check all of the alternatives for a matching prefix */  
   for (j = 0; j < MENU_ALTS && items[i]->alt[j][0]; j++) {  
   size_t alt_len = strlen(items[i]->alt[j]);  
   /* If the first part of the string matches an alternative, it's valid */  
   if (strncmp(selection, items[i]->alt[j], alt_len) == 0)  
   return 1;  
   }  
   }  
   }  
   }  
   return 0; /* No matches, end-of-file, or stream error */  
   }  
   /*  
   @description:  
   Parses and extracts a collection of menu items from a formatted options string.  
   The options string shall be in the following format:  
   <prompt1>;<prompt1.match1>|<prompt1.match2>;<prompt2>;<prompt2.match1>|<prompt2.match2>...  
   The prompt may not be an empty field, but the match strings may be empty.  
   Example 1 (prompts and matches): "A) Option A;A|a;B) Option B;B|b;Q) Quit;Q|q"  
   Example 2 (prompts and no matches): "A) Option A;;B) Option B;;Q) Quit;"  
   @remarks:  
   Memory is dynamically allocated for the item collection. destroy_menu_items  
   is provided as a convenience function for releasing this memory.  
   */  
   extern menuitem_t **extract_menu_items(const char *options)  
   {  
   menuitem_t **mem;  
   /* Always allocate at least one spot for a trailing null pointer */  
   mem = (menuitem_t**)malloc(sizeof *mem);  
   if (mem) {  
   char copy[BUFSIZ] = "";  
   size_t args = 0;  
   char *tok;  
   strcat(copy, options);  
   tok = strtok(copy, ";");  
   while (tok) {  
   size_t tok_len = strlen(tok);  
   menuitem_t **temp;  
   mem[args] = (menuitem_t*)malloc(sizeof *mem[args]);  
   if (!mem[args])  
   break;  
   /* Assume the first hit is a prompt */  
   strcpy(mem[args]->prompt, tok);  
   /* Assume the second hit is the alt list */  
   if (tok[tok_len + 1] == ';') {  
   /*  
   Kind of tricky. Introspectively looked ahead because strtok won't,  
   and handle an empty alt list field by setting the first alt to "".  
   */  
   mem[args]->alt[0][0] = '\0';  
   }  
   else {  
   char *alt_tok;  
   size_t n = 0;  
   /* Not an empty alt list, so we can grab the token with strtok */  
   tok = strtok(NULL, ";");  
   tok_len = strlen(tok);  
   /*  
   Now reuse strtok to process the alt list (awkward).  
   */  
   alt_tok = strtok(tok, "|");  
   while (n < MENU_ALTS - 1 && alt_tok) {  
   strcpy(mem[args]->alt[n++], alt_tok);  
   alt_tok = strtok(NULL, "|");  
   }  
   /* Make sure to terminate the alt list with a blank string */  
   mem[args]->alt[n][0] = '\0';  
   }  
   /* Make room for the next menu item (or null pointer if we're done) */  
   temp = (menuitem_t**)realloc(mem, (++args + 1) * sizeof *temp);  
   if (!temp)  
   break;  
   mem = temp;  
   /*  
   Restart strtok at the next option token because we  
   may have used strtok to handle a non-empty alt list.  
   */  
   tok = strtok(tok + tok_len + 1, ";");  
   }  
   mem[args] = NULL;  
   }  
   return mem;  
   }  
   /*  
   @description:  
   Releases memory for a collection of menu items returned by extract_menu_items.  
   */  
   extern void destroy_menu_items(menuitem_t **items)  
   {  
   size_t i;  
   for (i = 0; items[i]; i++)  
   free(items[i]);  
   free(items);  
   }  
   /* main.c (sample usage) */  
   #include <ctype.h>  
   #include <stdio.h>  
   #include "menu.h"  
   int main(void)  
   {  
   menuitem_t **items = extract_menu_items("A) Option A;A|a;B) Option B;B|b;Q) Quit;Q|q");  
   int done = 0;  
   while (!done) {  
   char selection[BUFSIZ];  
   int rc = menu(items, selection, sizeof selection);  
   printf("Full selection: '%s'\n", selection);  
   if (!rc)  
   puts("Invalid selection");  
   else {  
   switch (toupper(selection[0])) {  
   case 'A':  
   puts("You chose option A");  
   break;  
   case 'B':  
   puts("You chose option B");  
   break;  
   case 'Q':  
   puts("You chose to quit");  
   done = 1;  
   break;  
   }  
   }  
   }  
   destroy_menu_items(items);  
   return 0;  
   }  

No comments:

Post a Comment