Annotation of gnutrition/ui.c, revision 1.1

1.1     ! asm         1: // SPDX-License-Identifier: GPL-3.0-or-later
        !             2: /*
        !             3:  * $Id$
        !             4:  *
        !             5:  * ui.c - ncurses user interface for GNUtrition
        !             6:  *
        !             7:  * Copyright (C) 2026 Free Software Foundation, Inc.
        !             8:  *
        !             9:  * Author: Jason Self <jself@gnu.org>
        !            10:  *         Anton McClure <asm@gnu.org>
        !            11:  */
        !            12: 
        !            13: #ifdef HAVE_CONFIG_H
        !            14: #include <config.h>
        !            15: #endif
        !            16: 
        !            17: #include "ui.h"
        !            18: #include "budget.h"
        !            19: #include "db.h"
        !            20: #include "log.h"
        !            21: #include "i18n.h"
        !            22: 
        !            23: #include <curses.h>
        !            24: #include <errno.h>
        !            25: #include <stdlib.h>
        !            26: #include <string.h>
        !            27: #include <time.h>
        !            28: 
        !            29: /* Maximum length of the search input buffer.  This is a UI display
        !            30:    limit, not a data limit.  */
        !            31: #define SEARCH_BUF_SIZE 256
        !            32: 
        !            33: /* Get today's date as YYYY-MM-DD.  Returns a pointer to a static
        !            34:    buffer; not reentrant.  */
        !            35: static const char *
        !            36: today_date (void)
        !            37: {
        !            38:   static char buf[11];
        !            39:   time_t now;
        !            40:   struct tm *tm;
        !            41: 
        !            42:   now = time (NULL);
        !            43:   tm = localtime (&now);
        !            44:   strftime (buf, sizeof buf, "%Y-%m-%d", tm);
        !            45:   return buf;
        !            46: }
        !            47: 
        !            48: /* Format an ISO 8601 date (YYYY-MM-DD) for display using the
        !            49:    locale's preferred date representation.  Returns a pointer to a
        !            50:    static buffer; not reentrant.  */
        !            51: static const char *
        !            52: format_date (const char *iso_date)
        !            53: {
        !            54:   static char buf[64];
        !            55:   struct tm tm;
        !            56: 
        !            57:   memset (&tm, 0, sizeof tm);
        !            58:   if (sscanf (iso_date, "%d-%d-%d",
        !            59:               &tm.tm_year, &tm.tm_mon, &tm.tm_mday) != 3)
        !            60:     return iso_date;
        !            61:   tm.tm_year -= 1900;
        !            62:   tm.tm_mon -= 1;
        !            63:   if (strftime (buf, sizeof buf, "%x", &tm) == 0)
        !            64:     return iso_date;
        !            65:   return buf;
        !            66: }
        !            67: 
        !            68: /* Parse a locale-formatted date string back into an ISO 8601 date
        !            69:    (YYYY-MM-DD).  Returns 0 on success, -1 on failure.  */
        !            70: static int
        !            71: parse_locale_date (const char *locale_date, char *iso_buf, int bufsz)
        !            72: {
        !            73:   struct tm tm;
        !            74: 
        !            75:   memset (&tm, 0, sizeof tm);
        !            76:   if (strptime (locale_date, "%x", &tm) == NULL)
        !            77:     return -1;
        !            78:   if (strftime (iso_buf, (size_t) bufsz, "%Y-%m-%d", &tm) == 0)
        !            79:     return -1;
        !            80:   return 0;
        !            81: }
        !            82: 
        !            83: /* Draw the title bar.  */
        !            84: static void
        !            85: draw_title (void)
        !            86: {
        !            87:   attron (A_REVERSE);
        !            88:   mvhline (0, 0, ' ', COLS);
        !            89:   mvprintw (0, 1, _("GNUtrition %s"), PACKAGE_VERSION);
        !            90:   attroff (A_REVERSE);
        !            91: }
        !            92: 
        !            93: /* Draw the status / help bar at the bottom.  */
        !            94: static void
        !            95: draw_status (const char *msg)
        !            96: {
        !            97:   attron (A_REVERSE);
        !            98:   mvhline (LINES - 1, 0, ' ', COLS);
        !            99:   mvprintw (LINES - 1, 1, "%s", msg);
        !           100:   attroff (A_REVERSE);
        !           101: }
        !           102: 
        !           103: /* Draw the daily budget summary for DATE.  Returns the number of
        !           104:    lines used.  */
        !           105: static int
        !           106: draw_budget (sqlite3 *food_db, sqlite3 *log_db, int start_row,
        !           107:              int calories, const char *date)
        !           108: {
        !           109:   struct daily_budget budget;
        !           110:   struct daily_budget consumed;
        !           111:   struct log_list entries;
        !           112:   size_t i;
        !           113:   int row;
        !           114: 
        !           115:   budget = budget_for_calories (calories);
        !           116:   memset (&consumed, 0, sizeof consumed);
        !           117: 
        !           118:   if (log_get_day (log_db, date, &entries) == 0)
        !           119:     {
        !           120:       for (i = 0; i < entries.count; i++)
        !           121:         {
        !           122:           struct fped_entry fped;
        !           123:           if (db_get_fped (food_db, entries.items[i].food_code, &fped) == 0)
        !           124:             {
        !           125:               double s = entries.items[i].servings;
        !           126:               consumed.vegetables += fped.vegetables * s;
        !           127:               consumed.fruits += fped.fruits * s;
        !           128:               consumed.grains += fped.grains * s;
        !           129:               consumed.dairy += fped.dairy * s;
        !           130:               consumed.protein += fped.protein * s;
        !           131:               consumed.oils += fped.oils * s;
        !           132:             }
        !           133:         }
        !           134:       log_list_free (&entries);
        !           135:     }
        !           136: 
        !           137:   row = start_row;
        !           138:   attron (A_BOLD);
        !           139:   mvprintw (row++, 2, _("Daily Budget (%s) - %d kcal USDA Pattern"),
        !           140:             format_date (date), budget.calories);
        !           141:   attroff (A_BOLD);
        !           142:   row++;
        !           143:   mvprintw (row++, 2, _("%-20s %10s %10s %10s"),
        !           144:             _("Food Group"), _("Budget"), _("Consumed"), _("Remaining"));
        !           145:   mvprintw (row++, 2, "%-20s %10s %10s %10s",
        !           146:             "--------------------", "----------",
        !           147:             "----------", "----------");
        !           148:   mvprintw (row++, 2, _("%-20s %7.1f c  %7.1f c  %7.1f c "),
        !           149:             _("Vegetables"), budget.vegetables,
        !           150:             consumed.vegetables, budget.vegetables - consumed.vegetables);
        !           151:   mvprintw (row++, 2, _("%-20s %7.1f c  %7.1f c  %7.1f c "),
        !           152:             _("Fruits"), budget.fruits,
        !           153:             consumed.fruits, budget.fruits - consumed.fruits);
        !           154:   mvprintw (row++, 2, _("%-20s %7.1f oz %7.1f oz %7.1f oz"),
        !           155:             _("Grains"), budget.grains,
        !           156:             consumed.grains, budget.grains - consumed.grains);
        !           157:   mvprintw (row++, 2, _("%-20s %7.1f c  %7.1f c  %7.1f c "),
        !           158:             _("Dairy"), budget.dairy,
        !           159:             consumed.dairy, budget.dairy - consumed.dairy);
        !           160:   mvprintw (row++, 2, _("%-20s %7.1f oz %7.1f oz %7.1f oz"),
        !           161:             _("Protein Foods"), budget.protein,
        !           162:             consumed.protein, budget.protein - consumed.protein);
        !           163:   mvprintw (row++, 2, _("%-20s %7.1f g  %7.1f g  %7.1f g "),
        !           164:             _("Oils"), budget.oils,
        !           165:             consumed.oils, budget.oils - consumed.oils);
        !           166:   return row - start_row;
        !           167: }
        !           168: 
        !           169: /* Read a string from the ncurses screen at ROW, COL.
        !           170:    Edits BUF (of size BUFSZ) in-place.  Returns the key that
        !           171:    ended input (Enter or Escape).  */
        !           172: static int
        !           173: read_field (int row, int col, char *buf, int bufsz)
        !           174: {
        !           175:   int len;
        !           176:   int ch;
        !           177: 
        !           178:   len = (int) strlen (buf);
        !           179:   for (;;)
        !           180:     {
        !           181:       mvprintw (row, col, "%-20s", buf);
        !           182:       mvprintw (row, col + len, "_");
        !           183:       clrtoeol ();
        !           184:       refresh ();
        !           185:       ch = getch ();
        !           186: 
        !           187:       if (ch == '\n' || ch == KEY_ENTER || ch == '\t')
        !           188:         return '\n';
        !           189:       if (ch == 27)
        !           190:         return 27;
        !           191:       if ((ch == KEY_BACKSPACE || ch == 127 || ch == 8) && len > 0)
        !           192:         buf[--len] = '\0';
        !           193:       else if (ch >= 32 && ch < 127 && len < bufsz - 1)
        !           194:         {
        !           195:           buf[len++] = (char) ch;
        !           196:           buf[len] = '\0';
        !           197:         }
        !           198:     }
        !           199: }
        !           200: 
        !           201: /* Show food detail screen.  Display nutrient information and FPED
        !           202:    data, and let the user log the food with a chosen quantity and
        !           203:    date.  */
        !           204: static void
        !           205: food_detail_screen (sqlite3 *food_db, sqlite3 *log_db,
        !           206:                     int food_code, const char *description)
        !           207: {
        !           208:   struct nutrient_list nutrients;
        !           209:   struct fped_entry fped;
        !           210:   int has_fped;
        !           211:   char srv_buf[16];
        !           212:   char date_buf[16];
        !           213:   int scroll;
        !           214:   int ch;
        !           215:   int field;  /* 0 = browsing nutrients, 1 = servings, 2 = date  */
        !           216: 
        !           217:   if (db_get_nutrients (food_db, food_code, &nutrients) < 0)
        !           218:     return;
        !           219:   has_fped = (db_get_fped (food_db, food_code, &fped) == 0);
        !           220: 
        !           221:   snprintf (srv_buf, sizeof srv_buf, "1.0");
        !           222:   strncpy (date_buf, today_date (), sizeof date_buf - 1);
        !           223:   date_buf[sizeof date_buf - 1] = '\0';
        !           224:   scroll = 0;
        !           225:   field = 0;
        !           226: 
        !           227:   for (;;)
        !           228:     {
        !           229:       size_t i;
        !           230:       int row;
        !           231:       int visible;
        !           232:       int total_lines;
        !           233:       int srv_row;
        !           234:       int date_row;
        !           235:       double servings;
        !           236: 
        !           237:       {
        !           238:         char *endp;
        !           239:         errno = 0;
        !           240:         servings = strtod (srv_buf, &endp);
        !           241:         if (errno != 0 || endp == srv_buf || *endp != '\0'
        !           242:             || servings <= 0.0)
        !           243:           servings = 1.0;
        !           244:       }
        !           245: 
        !           246:       clear ();
        !           247:       draw_title ();
        !           248:       if (field == 0)
        !           249:         draw_status (_("Up/Down: scroll | Tab: edit fields | "
        !           250:                      "l: log food | Esc: back"));
        !           251:       else
        !           252:         draw_status (_("Enter/Tab: next field | Esc: cancel edit"));
        !           253: 
        !           254:       attron (A_BOLD);
        !           255:       mvprintw (2, 2, "%d - %-.*s", food_code, COLS - 14, description);
        !           256:       attroff (A_BOLD);
        !           257: 
        !           258:       /* Count total display lines for scroll limit.  */
        !           259:       total_lines = (int) nutrients.count + 1 + (has_fped ? 9 : 0);
        !           260: 
        !           261:       /* Visible area for scrollable nutrient list.  */
        !           262:       visible = LINES - 9;
        !           263:       if (visible < 1)
        !           264:         visible = 1;
        !           265: 
        !           266:       if (scroll > total_lines - visible)
        !           267:         scroll = total_lines - visible;
        !           268:       if (scroll < 0)
        !           269:         scroll = 0;
        !           270: 
        !           271:       row = 4;
        !           272:       {
        !           273:         int line_idx = 0;
        !           274: 
        !           275:         /* Nutrient header.  */
        !           276:         if (nutrients.count > 0 && line_idx >= scroll
        !           277:             && row < LINES - 5)
        !           278:           {
        !           279:             mvprintw (row++, 2, _("%-40s %10s"),
        !           280:                       _("Nutrient"), _("Value"));
        !           281:           }
        !           282:         line_idx++;
        !           283: 
        !           284:         for (i = 0; i < nutrients.count; i++, line_idx++)
        !           285:           {
        !           286:             if (line_idx < scroll)
        !           287:               continue;
        !           288:             if (row >= LINES - 5)
        !           289:               break;
        !           290:             mvprintw (row++, 2, "%-40.40s %10.2f",
        !           291:                       nutrients.items[i].name,
        !           292:                       nutrients.items[i].value * servings);
        !           293:           }
        !           294: 
        !           295:         /* FPED section.  */
        !           296:         if (has_fped)
        !           297:           {
        !           298:             if (line_idx >= scroll && row < LINES - 5)
        !           299:               {
        !           300:                 row++;
        !           301:                 attron (A_BOLD);
        !           302:                 if (servings != 1.0)
        !           303:                   mvprintw (row++, 2,
        !           304:                             _("Food Pattern Equivalents "
        !           305:                               "(per 100g x %.1f):"), servings);
        !           306:                 else
        !           307:                   mvprintw (row++, 2,
        !           308:                             _("Food Pattern Equivalents (per 100g):"));
        !           309:                 attroff (A_BOLD);
        !           310:               }
        !           311:             line_idx += 2;
        !           312: 
        !           313:             if (line_idx >= scroll && row < LINES - 5)
        !           314:               mvprintw (row++, 2,
        !           315:                         _("  Vegetables: %.2f cup-eq"),
        !           316:                         fped.vegetables * servings);
        !           317:             line_idx++;
        !           318:             if (line_idx >= scroll && row < LINES - 5)
        !           319:               mvprintw (row++, 2,
        !           320:                         _("  Fruits:     %.2f cup-eq"),
        !           321:                         fped.fruits * servings);
        !           322:             line_idx++;
        !           323:             if (line_idx >= scroll && row < LINES - 5)
        !           324:               mvprintw (row++, 2,
        !           325:                         _("  Grains:     %.2f oz-eq"),
        !           326:                         fped.grains * servings);
        !           327:             line_idx++;
        !           328:             if (line_idx >= scroll && row < LINES - 5)
        !           329:               mvprintw (row++, 2,
        !           330:                         _("  Dairy:      %.2f cup-eq"),
        !           331:                         fped.dairy * servings);
        !           332:             line_idx++;
        !           333:             if (line_idx >= scroll && row < LINES - 5)
        !           334:               mvprintw (row++, 2,
        !           335:                         _("  Protein:    %.2f oz-eq"),
        !           336:                         fped.protein * servings);
        !           337:             line_idx++;
        !           338:             if (line_idx >= scroll && row < LINES - 5)
        !           339:               mvprintw (row++, 2,
        !           340:                         _("  Oils:       %.2f g"),
        !           341:                         fped.oils * servings);
        !           342:             line_idx++;
        !           343:           }
        !           344:       }
        !           345: 
        !           346:       /* Bottom area: servings and date fields.  */
        !           347:       srv_row = LINES - 4;
        !           348:       date_row = LINES - 3;
        !           349: 
        !           350:       if (field == 1)
        !           351:         attron (A_REVERSE);
        !           352:       mvprintw (srv_row, 2, _("Servings: "));
        !           353:       if (field == 1)
        !           354:         attroff (A_REVERSE);
        !           355:       mvprintw (srv_row, 12, "%-20s", srv_buf);
        !           356: 
        !           357:       if (field == 2)
        !           358:         attron (A_REVERSE);
        !           359:       mvprintw (date_row, 2, _("Date:     "));
        !           360:       if (field == 2)
        !           361:         attroff (A_REVERSE);
        !           362:       mvprintw (date_row, 12, "%-20s", format_date (date_buf));
        !           363: 
        !           364:       refresh ();
        !           365: 
        !           366:       if (field == 1)
        !           367:         {
        !           368:           ch = read_field (srv_row, 12, srv_buf, (int) sizeof srv_buf);
        !           369:           if (ch == 27)
        !           370:             field = 0;
        !           371:           else
        !           372:             field = 2;
        !           373:           continue;
        !           374:         }
        !           375:       else if (field == 2)
        !           376:         {
        !           377:           char disp_buf[64];
        !           378:           strncpy (disp_buf, format_date (date_buf), sizeof disp_buf - 1);
        !           379:           disp_buf[sizeof disp_buf - 1] = '\0';
        !           380:           ch = read_field (date_row, 12, disp_buf, (int) sizeof disp_buf);
        !           381:           if (ch != 27)
        !           382:             {
        !           383:               if (parse_locale_date (disp_buf, date_buf,
        !           384:                                      (int) sizeof date_buf) < 0)
        !           385:                 {
        !           386:                   draw_status (_("Invalid date.  Press any key..."));
        !           387:                   refresh ();
        !           388:                   getch ();
        !           389:                 }
        !           390:             }
        !           391:           field = 0;
        !           392:           continue;
        !           393:         }
        !           394: 
        !           395:       ch = getch ();
        !           396: 
        !           397:       switch (ch)
        !           398:         {
        !           399:         case 27:  /* Escape  */
        !           400:           nutrient_list_free (&nutrients);
        !           401:           return;
        !           402: 
        !           403:         case KEY_UP:
        !           404:           if (scroll > 0)
        !           405:             scroll--;
        !           406:           break;
        !           407: 
        !           408:         case KEY_DOWN:
        !           409:           scroll++;
        !           410:           break;
        !           411: 
        !           412:         case KEY_PPAGE:
        !           413:           scroll -= visible;
        !           414:           break;
        !           415: 
        !           416:         case KEY_NPAGE:
        !           417:           scroll += visible;
        !           418:           break;
        !           419: 
        !           420:         case '\t':
        !           421:           field = 1;
        !           422:           break;
        !           423: 
        !           424:         case 'l':
        !           425:         case 'L':
        !           426:           {
        !           427:             char *endp;
        !           428:             double servings;
        !           429:             errno = 0;
        !           430:             servings = strtod (srv_buf, &endp);
        !           431:             if (errno != 0 || endp == srv_buf || *endp != '\0'
        !           432:                 || servings <= 0.0)
        !           433:               servings = 1.0;
        !           434:             log_add (log_db, food_code, description,
        !           435:                      date_buf, servings);
        !           436:             draw_status (_("Logged! Press any key..."));
        !           437:             refresh ();
        !           438:             getch ();
        !           439:           }
        !           440:           break;
        !           441: 
        !           442:         default:
        !           443:           break;
        !           444:         }
        !           445:     }
        !           446: }
        !           447: 
        !           448: /* Show the food search screen.  Let the user type a query, display
        !           449:    results, and view food details on selection.  */
        !           450: static void
        !           451: search_screen (sqlite3 *food_db, sqlite3 *log_db)
        !           452: {
        !           453:   char query[SEARCH_BUF_SIZE];
        !           454:   int qlen;
        !           455:   struct food_list results;
        !           456:   int selected;
        !           457:   int scroll;
        !           458:   int ch;
        !           459:   int running;
        !           460: 
        !           461:   memset (query, 0, sizeof query);
        !           462:   qlen = 0;
        !           463:   results.items = NULL;
        !           464:   results.count = 0;
        !           465:   results.capacity = 0;
        !           466:   selected = 0;
        !           467:   scroll = 0;
        !           468:   running = 1;
        !           469: 
        !           470:   while (running)
        !           471:     {
        !           472:       size_t i;
        !           473:       int row;
        !           474:       int visible;
        !           475: 
        !           476:       clear ();
        !           477:       draw_title ();
        !           478:       draw_status (_("Type to search | Enter: view details | "
        !           479:                    "Up/Down: select | Esc: back"));
        !           480: 
        !           481:       mvprintw (2, 2, _("Search: %s_"), query);
        !           482:       row = 4;
        !           483: 
        !           484:       if (results.count > 0)
        !           485:         {
        !           486:           visible = LINES - 6;
        !           487:           if (visible < 1)
        !           488:             visible = 1;
        !           489: 
        !           490:           /* Keep selected item visible by adjusting scroll.  */
        !           491:           if (selected < scroll)
        !           492:             scroll = selected;
        !           493:           if (selected >= scroll + visible)
        !           494:             scroll = selected - visible + 1;
        !           495: 
        !           496:           for (i = (size_t) scroll;
        !           497:                i < results.count && row < LINES - 2; i++)
        !           498:             {
        !           499:               if ((int) i == selected)
        !           500:                 attron (A_REVERSE);
        !           501:               mvprintw (row, 2, " %-8d %-.*s",
        !           502:                         results.items[i].food_code,
        !           503:                         COLS - 14,
        !           504:                         results.items[i].description);
        !           505:               if ((int) i == selected)
        !           506:                 attroff (A_REVERSE);
        !           507:               row++;
        !           508:             }
        !           509:         }
        !           510:       else if (qlen > 0)
        !           511:         {
        !           512:           mvprintw (row, 2, _("(no results)"));
        !           513:         }
        !           514: 
        !           515:       refresh ();
        !           516:       ch = getch ();
        !           517: 
        !           518:       switch (ch)
        !           519:         {
        !           520:         case 27:  /* Escape */
        !           521:           running = 0;
        !           522:           break;
        !           523: 
        !           524:         case KEY_UP:
        !           525:           if (selected > 0)
        !           526:             selected--;
        !           527:           break;
        !           528: 
        !           529:         case KEY_DOWN:
        !           530:           if (selected < (int) results.count - 1)
        !           531:             selected++;
        !           532:           break;
        !           533: 
        !           534:         case KEY_BACKSPACE:
        !           535:         case 127:
        !           536:         case 8:
        !           537:           if (qlen > 0)
        !           538:             {
        !           539:               query[--qlen] = '\0';
        !           540:               food_list_free (&results);
        !           541:               selected = 0;
        !           542:               scroll = 0;
        !           543:               if (qlen > 0)
        !           544:                 db_search_foods (food_db, query, &results);
        !           545:             }
        !           546:           break;
        !           547: 
        !           548:         case '\n':
        !           549:         case KEY_ENTER:
        !           550:           if (results.count > 0 && selected < (int) results.count)
        !           551:             {
        !           552:               food_detail_screen (food_db, log_db,
        !           553:                                   results.items[selected].food_code,
        !           554:                                   results.items[selected].description);
        !           555:             }
        !           556:           break;
        !           557: 
        !           558:         default:
        !           559:           if (ch >= 32 && ch < 127
        !           560:               && qlen < (int) sizeof query - 1)
        !           561:             {
        !           562:               query[qlen++] = (char) ch;
        !           563:               query[qlen] = '\0';
        !           564:               food_list_free (&results);
        !           565:               selected = 0;
        !           566:               scroll = 0;
        !           567:               db_search_foods (food_db, query, &results);
        !           568:             }
        !           569:           break;
        !           570:         }
        !           571:     }
        !           572: 
        !           573:   food_list_free (&results);
        !           574: }
        !           575: 
        !           576: /* Edit a log entry.  Let the user adjust the servings and date for
        !           577:    the selected entry.  Returns 1 if the entry was modified.  */
        !           578: static int
        !           579: edit_log_entry (sqlite3 *log_db, struct log_entry *entry)
        !           580: {
        !           581:   char srv_buf[16];
        !           582:   char date_buf[16];
        !           583:   int field;  /* -1 = idle, 0 = editing servings, 1 = editing date  */
        !           584:   int ch;
        !           585: 
        !           586:   snprintf (srv_buf, sizeof srv_buf, "%.1f", entry->servings);
        !           587:   strncpy (date_buf, entry->date, sizeof date_buf - 1);
        !           588:   date_buf[sizeof date_buf - 1] = '\0';
        !           589:   field = -1;
        !           590: 
        !           591:   for (;;)
        !           592:     {
        !           593:       int srv_row;
        !           594:       int date_row;
        !           595: 
        !           596:       clear ();
        !           597:       draw_title ();
        !           598:       if (field < 0)
        !           599:         draw_status (_("Tab: edit fields | s: save changes | "
        !           600:                      "Esc: cancel"));
        !           601:       else
        !           602:         draw_status (_("Enter/Tab: next field | Esc: cancel edit"));
        !           603: 
        !           604:       attron (A_BOLD);
        !           605:       mvprintw (2, 2, _("Edit Log Entry: %s"), entry->description);
        !           606:       attroff (A_BOLD);
        !           607: 
        !           608:       srv_row = 4;
        !           609:       date_row = 5;
        !           610: 
        !           611:       if (field == 0)
        !           612:         attron (A_REVERSE);
        !           613:       mvprintw (srv_row, 2, _("Servings: "));
        !           614:       if (field == 0)
        !           615:         attroff (A_REVERSE);
        !           616:       mvprintw (srv_row, 12, "%-20s", srv_buf);
        !           617: 
        !           618:       if (field == 1)
        !           619:         attron (A_REVERSE);
        !           620:       mvprintw (date_row, 2, _("Date:     "));
        !           621:       if (field == 1)
        !           622:         attroff (A_REVERSE);
        !           623:       mvprintw (date_row, 12, "%-20s", format_date (date_buf));
        !           624: 
        !           625:       refresh ();
        !           626: 
        !           627:       if (field == 0)
        !           628:         {
        !           629:           ch = read_field (srv_row, 12, srv_buf, (int) sizeof srv_buf);
        !           630:           if (ch == 27)
        !           631:             field = -1;
        !           632:           else
        !           633:             field = 1;
        !           634:           continue;
        !           635:         }
        !           636:       else if (field == 1)
        !           637:         {
        !           638:           char disp_buf[64];
        !           639:           strncpy (disp_buf, format_date (date_buf), sizeof disp_buf - 1);
        !           640:           disp_buf[sizeof disp_buf - 1] = '\0';
        !           641:           ch = read_field (date_row, 12, disp_buf, (int) sizeof disp_buf);
        !           642:           if (ch != 27)
        !           643:             {
        !           644:               if (parse_locale_date (disp_buf, date_buf,
        !           645:                                      (int) sizeof date_buf) < 0)
        !           646:                 {
        !           647:                   draw_status (_("Invalid date.  Press any key..."));
        !           648:                   refresh ();
        !           649:                   getch ();
        !           650:                 }
        !           651:             }
        !           652:           field = -1;
        !           653:           continue;
        !           654:         }
        !           655: 
        !           656:       ch = getch ();
        !           657: 
        !           658:       switch (ch)
        !           659:         {
        !           660:         case 27:
        !           661:           return 0;
        !           662: 
        !           663:         case '\t':
        !           664:           field = 0;
        !           665:           break;
        !           666: 
        !           667:         case 's':
        !           668:         case 'S':
        !           669:           {
        !           670:             char *endp;
        !           671:             double servings;
        !           672:             errno = 0;
        !           673:             servings = strtod (srv_buf, &endp);
        !           674:             if (errno != 0 || endp == srv_buf || *endp != '\0'
        !           675:                 || servings <= 0.0)
        !           676:               {
        !           677:                 draw_status (_("Invalid servings.  Press any key..."));
        !           678:                 refresh ();
        !           679:                 getch ();
        !           680:                 break;
        !           681:               }
        !           682:             if (log_update (log_db, entry->id, date_buf, servings) == 0)
        !           683:               {
        !           684:                 draw_status (_("Updated! Press any key..."));
        !           685:                 refresh ();
        !           686:                 getch ();
        !           687:                 return 1;
        !           688:               }
        !           689:             else
        !           690:               {
        !           691:                 draw_status (_("Error updating! Press any key..."));
        !           692:                 refresh ();
        !           693:                 getch ();
        !           694:               }
        !           695:           }
        !           696:           break;
        !           697: 
        !           698:         default:
        !           699:           break;
        !           700:         }
        !           701:     }
        !           702: }
        !           703: 
        !           704: /* Show the food log with date navigation.  */
        !           705: static void
        !           706: log_screen (sqlite3 *food_db, sqlite3 *log_db, int calories)
        !           707: {
        !           708:   struct log_list entries;
        !           709:   struct date_list dates;
        !           710:   char date[16];
        !           711:   int date_idx;
        !           712:   int selected;
        !           713:   int ch;
        !           714:   size_t j;
        !           715: 
        !           716:   strncpy (date, today_date (), sizeof date - 1);
        !           717:   date[sizeof date - 1] = '\0';
        !           718: 
        !           719:   /* Load all dates that have entries.  */
        !           720:   if (log_get_dates (log_db, &dates) < 0)
        !           721:     dates.count = 0;
        !           722: 
        !           723:   /* Find today's position in the date list (or -1).  */
        !           724:   date_idx = -1;
        !           725:   for (j = 0; j < dates.count; j++)
        !           726:     {
        !           727:       if (strcmp (dates.dates[j], date) == 0)
        !           728:         {
        !           729:           date_idx = (int) j;
        !           730:           break;
        !           731:         }
        !           732:     }
        !           733: 
        !           734:   selected = 0;
        !           735: 
        !           736:   while (1)
        !           737:     {
        !           738:       size_t i;
        !           739:       int row;
        !           740:       int budget_lines;
        !           741:       int log_start;
        !           742: 
        !           743:       clear ();
        !           744:       draw_title ();
        !           745:       draw_status (_("Left/Right: change date | Up/Down: select | "
        !           746:                    "d: delete | e: edit | Esc: back"));
        !           747: 
        !           748:       /* Show budget for the currently displayed date.  */
        !           749:       budget_lines = draw_budget (food_db, log_db, 2, calories, date);
        !           750:       log_start = 2 + budget_lines + 1;
        !           751: 
        !           752:       attron (A_BOLD);
        !           753:       if (dates.count > 1)
        !           754:         {
        !           755:           mvprintw (log_start, 2, _("Food Log for %s (%d/%d)"),
        !           756:                     format_date (date),
        !           757:                     date_idx >= 0 ? date_idx + 1 : 0,
        !           758:                     (int) dates.count);
        !           759:         }
        !           760:       else
        !           761:         {
        !           762:           mvprintw (log_start, 2, _("Food Log for %s"),
        !           763:                     format_date (date));
        !           764:         }
        !           765:       attroff (A_BOLD);
        !           766: 
        !           767:       row = log_start + 2;
        !           768:       if (log_get_day (log_db, date, &entries) == 0)
        !           769:         {
        !           770:           if (entries.count == 0)
        !           771:             {
        !           772:               mvprintw (row, 2, _("(no entries yet)"));
        !           773:               selected = 0;
        !           774:             }
        !           775:           else
        !           776:             {
        !           777:               if (selected >= (int) entries.count)
        !           778:                 selected = (int) entries.count - 1;
        !           779:               if (selected < 0)
        !           780:                 selected = 0;
        !           781:               mvprintw (row++, 2, _("%-5s %-8s %-6s %s"),
        !           782:                         _("ID"), _("Code"), _("Srv"), _("Description"));
        !           783:               mvprintw (row++, 2, "%-5s %-8s %-6s %s",
        !           784:                         "-----", "--------", "------",
        !           785:                         "------------------------------------");
        !           786:               for (i = 0; i < entries.count; i++)
        !           787:                 {
        !           788:                   if ((int) i == selected)
        !           789:                     attron (A_REVERSE);
        !           790:                   mvprintw (row, 2, "%-5d %-8d %5.1f  %-.*s",
        !           791:                             entries.items[i].id,
        !           792:                             entries.items[i].food_code,
        !           793:                             entries.items[i].servings,
        !           794:                             COLS - 26,
        !           795:                             entries.items[i].description);
        !           796:                   if ((int) i == selected)
        !           797:                     attroff (A_REVERSE);
        !           798:                   row++;
        !           799:                   if (row >= LINES - 2)
        !           800:                     break;
        !           801:                 }
        !           802:             }
        !           803:           log_list_free (&entries);
        !           804:         }
        !           805: 
        !           806:       refresh ();
        !           807:       ch = getch ();
        !           808: 
        !           809:       switch (ch)
        !           810:         {
        !           811:         case 27:  /* Escape  */
        !           812:           date_list_free (&dates);
        !           813:           return;
        !           814: 
        !           815:         case KEY_UP:
        !           816:           if (selected > 0)
        !           817:             selected--;
        !           818:           break;
        !           819: 
        !           820:         case KEY_DOWN:
        !           821:           selected++;
        !           822:           break;
        !           823: 
        !           824:         case KEY_LEFT:
        !           825:           if (dates.count > 0)
        !           826:             {
        !           827:               if (date_idx > 0)
        !           828:                 date_idx--;
        !           829:               else if (date_idx < 0 && dates.count > 0)
        !           830:                 date_idx = (int) dates.count - 1;
        !           831:               if (date_idx >= 0
        !           832:                   && date_idx < (int) dates.count)
        !           833:                 {
        !           834:                   strncpy (date, dates.dates[date_idx],
        !           835:                            sizeof date - 1);
        !           836:                   date[sizeof date - 1] = '\0';
        !           837:                 }
        !           838:               selected = 0;
        !           839:             }
        !           840:           break;
        !           841: 
        !           842:         case KEY_RIGHT:
        !           843:           if (dates.count > 0)
        !           844:             {
        !           845:               if (date_idx < 0)
        !           846:                 date_idx = 0;
        !           847:               else if (date_idx < (int) dates.count - 1)
        !           848:                 date_idx++;
        !           849:               if (date_idx >= 0
        !           850:                   && date_idx < (int) dates.count)
        !           851:                 {
        !           852:                   strncpy (date, dates.dates[date_idx],
        !           853:                            sizeof date - 1);
        !           854:                   date[sizeof date - 1] = '\0';
        !           855:                 }
        !           856:               selected = 0;
        !           857:             }
        !           858:           break;
        !           859: 
        !           860:         case 'd':
        !           861:         case 'D':
        !           862:           {
        !           863:             /* Delete the selected entry with confirmation.  */
        !           864:             struct log_list del_entries;
        !           865:             if (log_get_day (log_db, date, &del_entries) == 0
        !           866:                 && del_entries.count > 0
        !           867:                 && selected < (int) del_entries.count)
        !           868:               {
        !           869:                 draw_status (_("Delete this entry? (y/n)"));
        !           870:                 refresh ();
        !           871:                 ch = getch ();
        !           872:                 if (ch == 'y' || ch == 'Y')
        !           873:                   {
        !           874:                     log_delete (log_db, del_entries.items[selected].id);
        !           875:                     /* Reload dates.  */
        !           876:                     date_list_free (&dates);
        !           877:                     if (log_get_dates (log_db, &dates) < 0)
        !           878:                       dates.count = 0;
        !           879:                     /* Re-find date index.  */
        !           880:                     date_idx = -1;
        !           881:                     for (j = 0; j < dates.count; j++)
        !           882:                       {
        !           883:                         if (strcmp (dates.dates[j], date) == 0)
        !           884:                           {
        !           885:                             date_idx = (int) j;
        !           886:                             break;
        !           887:                           }
        !           888:                       }
        !           889:                     if (selected > 0)
        !           890:                       selected--;
        !           891:                   }
        !           892:                 log_list_free (&del_entries);
        !           893:               }
        !           894:           }
        !           895:           break;
        !           896: 
        !           897:         case 'e':
        !           898:         case 'E':
        !           899:           {
        !           900:             /* Edit the selected entry.  */
        !           901:             struct log_list ed_entries;
        !           902:             if (log_get_day (log_db, date, &ed_entries) == 0
        !           903:                 && ed_entries.count > 0
        !           904:                 && selected < (int) ed_entries.count)
        !           905:               {
        !           906:                 if (edit_log_entry (log_db, &ed_entries.items[selected]))
        !           907:                   {
        !           908:                     /* Reload dates since date might have changed.  */
        !           909:                     date_list_free (&dates);
        !           910:                     if (log_get_dates (log_db, &dates) < 0)
        !           911:                       dates.count = 0;
        !           912:                     date_idx = -1;
        !           913:                     for (j = 0; j < dates.count; j++)
        !           914:                       {
        !           915:                         if (strcmp (dates.dates[j], date) == 0)
        !           916:                           {
        !           917:                             date_idx = (int) j;
        !           918:                             break;
        !           919:                           }
        !           920:                       }
        !           921:                   }
        !           922:                 log_list_free (&ed_entries);
        !           923:               }
        !           924:           }
        !           925:           break;
        !           926: 
        !           927:         default:
        !           928:           break;
        !           929:         }
        !           930:     }
        !           931: }
        !           932: 
        !           933: /* Gender names. */
        !           934: static const char *gender_names[] =
        !           935: {
        !           936:   N_("Neutral"),
        !           937:   N_("Female"),
        !           938:   N_("Male")
        !           939: };
        !           940: 
        !           941: #define NUM_GENDERS (sizeof (gender_names) / sizeof (gender_names[0]))
        !           942: 
        !           943: /* Activity level names for display.  */
        !           944: static const char *activity_names[] =
        !           945: {
        !           946:   N_("Sedentary"),
        !           947:   N_("Light"),
        !           948:   N_("Moderate"),
        !           949:   N_("Very active"),
        !           950:   N_("Extra active")
        !           951: };
        !           952: 
        !           953: #define NUM_ACTIVITIES (sizeof (activity_names) / sizeof (activity_names[0]))
        !           954: 
        !           955: /* Profile setup screen.  Returns the new calorie target, or the
        !           956:    original CALORIES if the user cancels.  */
        !           957: static int
        !           958: profile_screen (sqlite3 *log_db, int calories)
        !           959: {
        !           960:   struct user_profile prof;
        !           961:   char age_buf[16];
        !           962:   char height_buf[16];
        !           963:   char weight_buf[16];
        !           964:   int activity_sel;
        !           965:   int gender_sel;
        !           966:   int field;  /* 0=age, 1=height, 2=weight, 3=activity, 4=gender  */
        !           967:   int ch;
        !           968:   int rc;
        !           969: 
        !           970:   memset (&prof, 0, sizeof prof);
        !           971:   memset (age_buf, 0, sizeof age_buf);
        !           972:   memset (height_buf, 0, sizeof height_buf);
        !           973:   memset (weight_buf, 0, sizeof weight_buf);
        !           974:   activity_sel = ACTIVITY_SEDENTARY;
        !           975:   gender_sel = GENDER_NEUTRAL;
        !           976: 
        !           977:   /* Load existing profile if any.  */
        !           978:   rc = log_get_profile (log_db, &prof);
        !           979:   if (rc == 0)
        !           980:     {
        !           981:       snprintf (age_buf, sizeof age_buf, "%d", prof.age_years);
        !           982:       snprintf (height_buf, sizeof height_buf, "%.1f", prof.height_cm);
        !           983:       snprintf (weight_buf, sizeof weight_buf, "%.1f", prof.weight_kg);
        !           984:       activity_sel = prof.activity_level;
        !           985:       gender_sel = prof.gender;
        !           986:     }
        !           987: 
        !           988:   field = 0;
        !           989: 
        !           990:   for (;;)
        !           991:     {
        !           992:       int row;
        !           993:       int field_rows[5];
        !           994: 
        !           995:       clear ();
        !           996:       draw_title ();
        !           997:       draw_status (_("Tab/Enter: next field | Up/Down: activity | "
        !           998:                    "Esc: cancel | s: save"));
        !           999: 
        !          1000:       row = 2;
        !          1001:       attron (A_BOLD);
        !          1002:       mvprintw (row++, 2, _("Profile Setup"));
        !          1003:       attroff (A_BOLD);
        !          1004:       row++;
        !          1005:       mvprintw (row++, 2,
        !          1006:                 _("Enter your details to estimate a daily calorie target."));
        !          1007:       mvprintw (row++, 2,
        !          1008:                 _("Uses the Mifflin-St Jeor equation (sex-neutral)."));
        !          1009:       row++;
        !          1010: 
        !          1011:       /* Age field.  */
        !          1012:       field_rows[0] = row;
        !          1013:       if (field == 0)
        !          1014:         attron (A_REVERSE);
        !          1015:       mvprintw (row, 2, _("Age (years):     "));
        !          1016:       if (field == 0)
        !          1017:         attroff (A_REVERSE);
        !          1018:       mvprintw (row++, 20, "%-20s", age_buf);
        !          1019: 
        !          1020:       /* Height field.  */
        !          1021:       field_rows[1] = row;
        !          1022:       if (field == 1)
        !          1023:         attron (A_REVERSE);
        !          1024:       mvprintw (row, 2, _("Height (cm):     "));
        !          1025:       if (field == 1)
        !          1026:         attroff (A_REVERSE);
        !          1027:       mvprintw (row++, 20, "%-20s", height_buf);
        !          1028: 
        !          1029:       /* Weight field.  */
        !          1030:       field_rows[2] = row;
        !          1031:       if (field == 2)
        !          1032:         attron (A_REVERSE);
        !          1033:       mvprintw (row, 2, _("Weight (kg):     "));
        !          1034:       if (field == 2)
        !          1035:         attroff (A_REVERSE);
        !          1036:       mvprintw (row++, 20, "%-20s", weight_buf);
        !          1037: 
        !          1038:       /* Activity field.  */
        !          1039:       field_rows[3] = row;
        !          1040:       if (field == 3)
        !          1041:         attron (A_REVERSE);
        !          1042:       mvprintw (row, 2, _("Activity level:  "));
        !          1043:       if (field == 3)
        !          1044:         attroff (A_REVERSE);
        !          1045:       if (activity_sel >= 0 && activity_sel < (int) NUM_ACTIVITIES)
        !          1046:         mvprintw (row++, 20, "%-20s", _(activity_names[activity_sel]));
        !          1047:       else
        !          1048:         mvprintw (row++, 20, "%-20s", _("Sedentary"));
        !          1049: 
        !          1050:       /* Gender field.  */
        !          1051:       field_rows[4] = row;
        !          1052:       if (field == 4)
        !          1053:         attron (A_REVERSE);
        !          1054:       mvprintw (row, 2, _("Gender:          "));
        !          1055:       if (field == 4)
        !          1056:         attroff (A_REVERSE);
        !          1057:       mvprintw (row++, 20, "%-20s", _(gender_names[gender_sel]));
        !          1058: 
        !          1059:       row++;
        !          1060:       if (prof.calorie_target > 0)
        !          1061:         mvprintw (row++, 2, _("Current saved target: %d kcal/day"),
        !          1062:                   prof.calorie_target);
        !          1063: 
        !          1064:       /* If all fields have values, show preview.  */
        !          1065:       if (age_buf[0] && height_buf[0] && weight_buf[0])
        !          1066:         {
        !          1067:           char *endp;
        !          1068:           double h, w;
        !          1069:           errno = 0;
        !          1070:           h = strtod (height_buf, &endp);
        !          1071:           if (errno != 0 || endp == height_buf || *endp != '\0')
        !          1072:             h = 0.0;
        !          1073:           errno = 0;
        !          1074:           w = strtod (weight_buf, &endp);
        !          1075:           if (errno != 0 || endp == weight_buf || *endp != '\0')
        !          1076:             w = 0.0;
        !          1077:           if (h > 0.0 && w > 0.0)
        !          1078:             {
        !          1079:               int est = budget_estimate_calories (atoi (age_buf),
        !          1080:                                                    h, w,
        !          1081:                                                    activity_sel, gender_sel);
        !          1082:               mvprintw (row++, 2,
        !          1083:                         _("Estimated target:     %d kcal/day"), est);
        !          1084:             }
        !          1085:         }
        !          1086: 
        !          1087:       refresh ();
        !          1088: 
        !          1089:       if (field < 3)
        !          1090:         {
        !          1091:           char *buf;
        !          1092:           int bufsz;
        !          1093: 
        !          1094:           if (field == 0)
        !          1095:             { buf = age_buf; bufsz = (int) sizeof age_buf; }
        !          1096:           else if (field == 1)
        !          1097:             { buf = height_buf; bufsz = (int) sizeof height_buf; }
        !          1098:           else
        !          1099:             { buf = weight_buf; bufsz = (int) sizeof weight_buf; }
        !          1100: 
        !          1101:           ch = read_field (field_rows[field], 20, buf, bufsz);
        !          1102:           if (ch == 27) return calories;
        !          1103:           if (ch == '\t' || ch == KEY_DOWN || ch == '\n' || ch == KEY_ENTER)
        !          1104:             field++;
        !          1105:           else if (ch == KEY_UP && field > 0)
        !          1106:             field--;
        !          1107:         }
        !          1108:       else
        !          1109:         {
        !          1110:           ch = getch ();
        !          1111:           if (ch == 27) return calories;
        !          1112:           if (ch == 's' || ch == 'S')
        !          1113:             goto save_profile;
        !          1114:           if (field == 3)
        !          1115:             {
        !          1116:               if (ch == KEY_UP) activity_sel = (activity_sel > 0) ? activity_sel - 1 : (int)NUM_ACTIVITIES - 1;
        !          1117:               else if (ch == KEY_DOWN) activity_sel = (activity_sel < (int)NUM_ACTIVITIES - 1) ? activity_sel + 1 : 0;
        !          1118:               else if (ch == '\n' || ch == KEY_ENTER || ch == '\t') field = 4;
        !          1119:             }
        !          1120:           else if (field == 4)
        !          1121:             {
        !          1122:               if (ch == KEY_UP) gender_sel = (gender_sel > 0) ? gender_sel - 1 : (int)NUM_GENDERS - 1;
        !          1123:               else if (ch == KEY_DOWN) gender_sel = (gender_sel < (int)NUM_GENDERS - 1) ? gender_sel + 1 : 0;
        !          1124:               else if (ch == '\n' || ch == KEY_ENTER || ch == '\t') field = 0;
        !          1125:             }
        !          1126:         }
        !          1127: 
        !          1128:       continue;
        !          1129: 
        !          1130:   save_profile:
        !          1131:       if (!age_buf[0] || !height_buf[0] || !weight_buf[0])
        !          1132:         {
        !          1133:           draw_status (_("All fields required! Press any key..."));
        !          1134:           refresh ();
        !          1135:           getch ();
        !          1136:         }
        !          1137:       else
        !          1138:         {
        !          1139:           int est;
        !          1140:           char *endp;
        !          1141:           prof.age_years = atoi (age_buf);
        !          1142:           errno = 0;
        !          1143:           prof.height_cm = strtod (height_buf, &endp);
        !          1144:           if (errno != 0 || endp == height_buf
        !          1145:               || *endp != '\0' || prof.height_cm <= 0.0)
        !          1146:             {
        !          1147:               draw_status (_("Invalid height! Press any key..."));
        !          1148:               refresh ();
        !          1149:               getch ();
        !          1150:               continue;
        !          1151:             }
        !          1152:           errno = 0;
        !          1153:           prof.weight_kg = strtod (weight_buf, &endp);
        !          1154:           if (errno != 0 || endp == weight_buf
        !          1155:               || *endp != '\0' || prof.weight_kg <= 0.0)
        !          1156:             {
        !          1157:               draw_status (_("Invalid weight! Press any key..."));
        !          1158:               refresh ();
        !          1159:               getch ();
        !          1160:               continue;
        !          1161:             }
        !          1162: 
        !          1163:           prof.activity_level = activity_sel;
        !          1164:           prof.gender = gender_sel;
        !          1165:           est = budget_estimate_calories (prof.age_years,
        !          1166:                                           prof.height_cm,
        !          1167:                                           prof.weight_kg,
        !          1168:                                           prof.activity_level,
        !          1169:                                           prof.gender);
        !          1170:           prof.calorie_target = est;
        !          1171: 
        !          1172:           if (log_save_profile (log_db, &prof) < 0)
        !          1173:             {
        !          1174:               draw_status (_("Error saving! Press any key..."));
        !          1175:               refresh ();
        !          1176:               getch ();
        !          1177:             }
        !          1178:           else
        !          1179:             {
        !          1180:               draw_status (_("Profile saved! Press any key..."));
        !          1181:               refresh ();
        !          1182:               getch ();
        !          1183:               return est;
        !          1184:             }
        !          1185:         }
        !          1186:     }
        !          1187: }
        !          1188: 
        !          1189: int
        !          1190: ui_run (sqlite3 *food_db, sqlite3 *log_db, int calories)
        !          1191: {
        !          1192:   int ch;
        !          1193:   int running;
        !          1194: 
        !          1195:   initscr ();
        !          1196:   cbreak ();
        !          1197:   noecho ();
        !          1198:   keypad (stdscr, TRUE);
        !          1199:   curs_set (0);
        !          1200: 
        !          1201:   running = 1;
        !          1202:   while (running)
        !          1203:     {
        !          1204:       clear ();
        !          1205:       draw_title ();
        !          1206:       draw_status (_("s: Search foods | l: View log | "
        !          1207:                    "p: Profile | q: Quit"));
        !          1208: 
        !          1209:       draw_budget (food_db, log_db, 2, calories, today_date ());
        !          1210: 
        !          1211:       mvprintw (LINES - 3, 2,
        !          1212:                 _("[s] Search   [l] Log   [p] Profile   [q] Quit"));
        !          1213: 
        !          1214:       refresh ();
        !          1215:       ch = getch ();
        !          1216: 
        !          1217:       switch (ch)
        !          1218:         {
        !          1219:         case 's':
        !          1220:         case 'S':
        !          1221:           search_screen (food_db, log_db);
        !          1222:           break;
        !          1223: 
        !          1224:         case 'l':
        !          1225:         case 'L':
        !          1226:           log_screen (food_db, log_db, calories);
        !          1227:           break;
        !          1228: 
        !          1229:         case 'p':
        !          1230:         case 'P':
        !          1231:           calories = profile_screen (log_db, calories);
        !          1232:           break;
        !          1233: 
        !          1234:         case 'q':
        !          1235:         case 'Q':
        !          1236:           running = 0;
        !          1237:           break;
        !          1238: 
        !          1239:         default:
        !          1240:           break;
        !          1241:         }
        !          1242:     }
        !          1243: 
        !          1244:   endwin ();
        !          1245:   return 0;
        !          1246: }

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>