Annotation of gnutrition/main.c, revision 1.2

1.1       asm         1: // SPDX-License-Identifier: GPL-3.0-or-later
                      2: /*
1.2     ! asm         3:  * $Id: main.c,v 1.1 2026/05/08 03:23:59 asm Exp $
1.1       asm         4:  *
                      5:  * main.c - Entry point 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 "budget.h"
                     18: #include "db.h"
                     19: #include "log.h"
                     20: #include "ui.h"
                     21: #include "i18n.h"
                     22: 
                     23: #include <errno.h>
                     24: #include <getopt.h>
                     25: #include <locale.h>
                     26: #include <stdio.h>
                     27: #include <stdlib.h>
                     28: #include <string.h>
                     29: #include <sys/stat.h>
                     30: #include <time.h>
                     31: 
                     32: #define PROGRAM_NAME "gnutrition"
                     33: 
                     34: static const struct option long_options[] =
                     35: {
                     36:   {"help",       no_argument,       NULL, 'h'},
                     37:   {"version",    no_argument,       NULL, 'V'},
                     38:   {"search",     required_argument, NULL, 's'},
                     39:   {"info",       required_argument, NULL, 'i'},
                     40:   {"log",        required_argument, NULL, 'l'},
                     41:   {"quantity",   required_argument, NULL, 'n'},
                     42:   {"delete",     required_argument, NULL, 'x'},
                     43:   {"edit",       required_argument, NULL, 'e'},
                     44:   {"budget",     no_argument,       NULL, 'b'},
                     45:   {"calories",   required_argument, NULL, 'c'},
                     46:   {"age",        required_argument, NULL, 'a'},
                     47:   {"height",     required_argument, NULL, 'H'},
                     48:   {"weight",     required_argument, NULL, 'w'},
1.2     ! asm        49:   {"gender",     required_argument, NULL, 'G'},
1.1       asm        50:   {"activity",   required_argument, NULL, 'A'},
                     51:   {"date",       required_argument, NULL, 'd'},
                     52:   {"db",         required_argument, NULL, 'D'},
                     53:   {"profile-db", required_argument, NULL, 'P'},
                     54:   {NULL, 0, NULL, 0}
                     55: };
                     56: 
                     57: static void
                     58: print_version (void)
                     59: {
                     60:   printf (_("GNUtrition %s\n"), PACKAGE_VERSION);
                     61:   printf (_("Copyright (C) 2026 Free Software Foundation, Inc.\n"));
                     62:   printf (_("License GPLv3+: GNU GPL version 3 or later "
                     63:           "<http://gnu.org/licenses/gpl.html>\n"));
                     64:   printf (_("This is free software: you are free to change "
                     65:           "and redistribute it.\n"));
                     66:   printf (_("There is NO WARRANTY, to the extent permitted by law.\n"));
                     67: }
                     68: 
                     69: static void
                     70: print_help (void)
                     71: {
                     72:   printf (_("Usage: %s [OPTION]...\n"), PROGRAM_NAME);
                     73:   printf (_("Track your daily food intake using the USDA Food Pattern "
                     74:           "budget system.\n\n"));
                     75:   printf (_("With no options, starts the interactive ncurses interface.\n\n"));
                     76:   printf (_("Options:\n"));
                     77:   printf (_("  -s, --search=QUERY   search for foods matching QUERY\n"));
                     78:   printf (_("  -i, --info=CODE      show nutrient info for food CODE\n"));
                     79:   printf (_("  -l, --log=CODE       log a food by its code\n"));
                     80:   printf (_("  -n, --quantity=NUM   number of servings to log "
                     81:           "(default: 1.0)\n"));
                     82:   printf (_("  -x, --delete=ID      delete a food log entry by its ID\n"));
                     83:   printf (_("  -e, --edit=ID        edit a food log entry (use with "
                     84:           "-n and/or -d)\n"));
                     85:   printf (_("  -b, --budget         show today's budget summary\n"));
                     86:   printf (_("  -c, --calories=KCAL  set daily calorie target "
                     87:           "(default: 2000)\n"));
                     88:   printf (_("  -a, --age=YEARS      your age in years "
                     89:           "(for calorie estimation)\n"));
                     90:   printf (_("  -H, --height=CM      your height in centimeters\n"));
                     91:   printf (_("  -w, --weight=KG      your weight in kilograms\n"));
                     92:   printf (_("  -A, --activity=LEVEL activity level: sedentary, light,\n"));
1.2     ! asm        93:   printf (_("                         moderate, very-active, or extra-active"));
        !            94:   printf (_("  -G, --gender=NAME    neutral, female, or male\n"));
1.1       asm        95:   printf (_("  -d, --date=DATE      date for log/budget "
                     96:           "(default: today)\n"));
1.2     ! asm        97:   printf (_("  -D, --db=PATH        path to food database (default: food.db)\n"));
1.1       asm        98:   printf (_("  -P, --profile-db=PATH\n"));
                     99:   printf (_("                       path to profile/log database\n"));
1.2     ! asm       100:   printf (_("                         (default: $XDG_DATA_HOME/gnutrition/log.db)\n"));
1.1       asm       101:   printf (_("  -h, --help           display this help and exit\n"));
                    102:   printf (_("  -V, --version        output version information and exit\n"));
                    103:   printf (_("\nWhen --age, --height, --weight, and --activity are all given,\n"));
                    104:   printf (_("the calorie target is estimated using the Mifflin-St Jeor\n"));
                    105:   printf (_("equation and saved to your profile for future sessions.\n"));
                    106:   printf (_("Use --calories to override the computed estimate.\n"));
                    107:   printf (_("\nThe calorie level determines your daily food-group budget\n"));
                    108:   printf (_("using the USDA Healthy US-Style Eating Pattern table\n"));
                    109:   printf (_("(Dietary Guidelines for Americans, 2020-2025, Appendix 3).\n"));
                    110:   printf (_("Values are rounded to the nearest 200 kcal pattern level\n"));
                    111:   printf (_("(range: 1000-3200).\n"));
                    112:   printf (_("\nYour profile and food log are stored in a separate database\n"));
                    113:   printf (_("at $XDG_DATA_HOME/gnutrition/log.db (the USDA food database\n"));
                    114:   printf (_("is never modified).\n"));
                    115:   printf (_("\nReport bugs to: bug-gnutrition@gnu.org\n"));
                    116:   printf (_("GNUtrition home page: "
                    117:           "<http://www.gnu.org/software/gnutrition/>\n"));
                    118:   printf (_("General help using GNU software: "
                    119:           "<http://www.gnu.org/gethelp/>\n"));
                    120: }
                    121: 
                    122: /* Parse an activity-level name.  Returns -1 if NAME is not
                    123:    recognized.  */
                    124: static int
                    125: parse_activity (const char *name)
                    126: {
                    127:   if (strcmp (name, "sedentary") == 0)
                    128:     return ACTIVITY_SEDENTARY;
                    129:   if (strcmp (name, "light") == 0)
                    130:     return ACTIVITY_LIGHT;
                    131:   if (strcmp (name, "moderate") == 0)
                    132:     return ACTIVITY_MODERATE;
                    133:   if (strcmp (name, "very-active") == 0)
                    134:     return ACTIVITY_VERY_ACTIVE;
                    135:   if (strcmp (name, "extra-active") == 0)
                    136:     return ACTIVITY_EXTRA_ACTIVE;
                    137:   return -1;
                    138: }
                    139: 
                    140: /* Return today's date as a dynamically-allocated string.  The caller
                    141:    must free the result.  Returns NULL on failure.  */
                    142: static char *
                    143: get_today (void)
                    144: {
                    145:   char *buf;
                    146:   time_t now;
                    147:   struct tm *tm;
                    148: 
                    149:   buf = malloc (11);
                    150:   if (!buf)
                    151:     {
                    152:       fprintf (stderr, _("%s: memory exhausted\n"), PROGRAM_NAME);
                    153:       return NULL;
                    154:     }
                    155:   now = time (NULL);
                    156:   tm = localtime (&now);
                    157:   strftime (buf, 11, "%Y-%m-%d", tm);
                    158:   return buf;
                    159: }
                    160: 
                    161: /* Normalize a date string to ISO 8601 format (YYYY-MM-DD).  Accepts
                    162:    the locale's preferred date representation (%x).  Returns a
                    163:    dynamically-allocated string on success, or NULL on failure.  */
                    164: static char *
                    165: normalize_date (const char *input)
                    166: {
                    167:   struct tm tm;
                    168:   char *buf;
                    169: 
                    170:   buf = malloc (11);
                    171:   if (!buf)
                    172:     {
                    173:       fprintf (stderr, _("%s: memory exhausted\n"), PROGRAM_NAME);
                    174:       return NULL;
                    175:     }
                    176: 
                    177:   memset (&tm, 0, sizeof tm);
                    178:   if (strptime (input, "%x", &tm) != NULL)
                    179:     {
                    180:       if (strftime (buf, 11, "%Y-%m-%d", &tm) > 0)
                    181:         return buf;
                    182:     }
                    183: 
                    184:   free (buf);
                    185:   return NULL;
                    186: }
                    187: 
                    188: /* Format an ISO 8601 date (YYYY-MM-DD) for display using the
                    189:    locale's preferred date representation.  Returns a pointer to a
                    190:    static buffer; not reentrant.  */
                    191: static const char *
                    192: format_date (const char *iso_date)
                    193: {
                    194:   static char buf[64];
                    195:   struct tm tm;
                    196: 
                    197:   memset (&tm, 0, sizeof tm);
                    198:   if (sscanf (iso_date, "%d-%d-%d",
                    199:               &tm.tm_year, &tm.tm_mon, &tm.tm_mday) != 3)
                    200:     return iso_date;
                    201:   tm.tm_year -= 1900;
                    202:   tm.tm_mon -= 1;
                    203:   if (strftime (buf, sizeof buf, "%x", &tm) == 0)
                    204:     return iso_date;
                    205:   return buf;
                    206: }
                    207: 
                    208: /* Get the path to the user's log database.  Uses
                    209:    $XDG_DATA_HOME/gnutrition/log.db or ~/.local/share/gnutrition/log.db.
                    210:    Returns a dynamically-allocated string.  */
                    211: static char *
                    212: get_log_path (void)
                    213: {
                    214:   const char *data_home;
                    215:   char *path;
                    216:   size_t len;
                    217: 
                    218:   data_home = getenv ("XDG_DATA_HOME");
                    219:   if (data_home && data_home[0] != '\0')
                    220:     {
                    221:       len = strlen (data_home) + strlen ("/gnutrition/log.db") + 1;
                    222:       path = malloc (len);
                    223:       if (!path)
                    224:         {
                    225:           fprintf (stderr, _("%s: memory exhausted\n"), PROGRAM_NAME);
                    226:           return NULL;
                    227:         }
                    228:       snprintf (path, len, "%s/gnutrition/log.db", data_home);
                    229:     }
                    230:   else
                    231:     {
                    232:       const char *home = getenv ("HOME");
                    233:       if (!home)
                    234:         {
                    235:           fprintf (stderr, _("%s: HOME is not set\n"), PROGRAM_NAME);
                    236:           return NULL;
                    237:         }
                    238:       len = strlen (home)
                    239:             + strlen ("/.local/share/gnutrition/log.db") + 1;
                    240:       path = malloc (len);
                    241:       if (!path)
                    242:         {
                    243:           fprintf (stderr, _("%s: memory exhausted\n"), PROGRAM_NAME);
                    244:           return NULL;
                    245:         }
                    246:       snprintf (path, len, "%s/.local/share/gnutrition/log.db", home);
                    247:     }
                    248: 
                    249:   return path;
                    250: }
                    251: 
                    252: /* Ensure the directory containing PATH exists.  */
                    253: static int
                    254: ensure_dir (const char *path)
                    255: {
                    256:   char *dir;
                    257:   char *slash;
                    258:   char *copy;
                    259: 
                    260:   copy = strdup (path);
                    261:   if (!copy)
                    262:     {
                    263:       fprintf (stderr, _("%s: memory exhausted\n"), PROGRAM_NAME);
                    264:       return -1;
                    265:     }
                    266: 
                    267:   /* Find the last slash to get the directory part.  */
                    268:   slash = strrchr (copy, '/');
                    269:   if (slash)
                    270:     {
                    271:       *slash = '\0';
                    272:       dir = copy;
                    273: 
                    274:       /* Build the directory path component by component.  */
                    275:       {
                    276:         char *p;
                    277:         for (p = dir + 1; *p; p++)
                    278:           {
                    279:             if (*p == '/')
                    280:               {
                    281:                 *p = '\0';
                    282:                 if (mkdir (dir, 0755) < 0 && errno != EEXIST)
                    283:                   {
                    284:                     fprintf (stderr, _("%s: cannot create directory '%s': %s\n"),
                    285:                              PROGRAM_NAME, dir, strerror (errno));
                    286:                     free (copy);
                    287:                     return -1;
                    288:                   }
                    289:                 *p = '/';
                    290:               }
                    291:           }
                    292:         if (mkdir (dir, 0755) < 0 && errno != EEXIST)
                    293:           {
                    294:             fprintf (stderr, _("%s: cannot create directory '%s': %s\n"),
                    295:                      PROGRAM_NAME, dir, strerror (errno));
                    296:             free (copy);
                    297:             return -1;
                    298:           }
                    299:       }
                    300:     }
                    301: 
                    302:   free (copy);
                    303:   return 0;
                    304: }
                    305: 
                    306: /* Perform a CLI food search.  */
                    307: static int
                    308: cmd_search (sqlite3 *db, const char *query)
                    309: {
                    310:   struct food_list results;
                    311:   size_t i;
                    312: 
                    313:   if (db_search_foods (db, query, &results) < 0)
                    314:     return 1;
                    315: 
                    316:   if (results.count == 0)
                    317:     {
                    318:       printf (_("No foods found matching '%s'.\n"), query);
                    319:     }
                    320:   else
                    321:     {
                    322:       printf ("%-10s %s\n", _("Code"), _("Description"));
                    323:       printf ("%-10s %s\n", "----------",
                    324:               "--------------------------------------------");
                    325:       for (i = 0; i < results.count; i++)
                    326:         printf ("%-10d %s\n", results.items[i].food_code,
                    327:                 results.items[i].description);
                    328:     }
                    329: 
                    330:   food_list_free (&results);
                    331:   return 0;
                    332: }
                    333: 
                    334: /* Show nutrient information for a food code, scaled by SERVINGS.  */
                    335: static int
                    336: cmd_info (sqlite3 *db, int food_code, double servings)
                    337: {
                    338:   struct nutrient_list nutrients;
                    339:   struct fped_entry fped;
                    340:   size_t i;
                    341: 
                    342:   if (db_get_nutrients (db, food_code, &nutrients) < 0)
                    343:     return 1;
                    344: 
                    345:   if (nutrients.count == 0)
                    346:     {
                    347:       printf (_("No nutrient data found for food code %d.\n"), food_code);
                    348:     }
                    349:   else
                    350:     {
                    351:       printf (_("Nutrient information for food code %d:\n\n"), food_code);
                    352:       printf ("%-40s %10s\n", _("Nutrient"), _("Value"));
                    353:       printf ("%-40s %10s\n",
                    354:               "----------------------------------------",
                    355:               "----------");
                    356:       for (i = 0; i < nutrients.count; i++)
                    357:         printf ("%-40s %10.2f\n", nutrients.items[i].name,
                    358:                 nutrients.items[i].value * servings);
                    359:     }
                    360:   nutrient_list_free (&nutrients);
                    361: 
                    362:   /* Also show FPED budget cost if available.  */
                    363:   if (db_get_fped (db, food_code, &fped) == 0)
                    364:     {
                    365:       if (servings != 1.0)
                    366:         printf (_("\nFood Pattern Equivalents (per 100g x %.1f):\n"),
                    367:                 servings);
                    368:       else
                    369:         printf (_("\nFood Pattern Equivalents (per 100g):\n"));
                    370:       printf (_("  Vegetables: %.2f cup-eq\n"), fped.vegetables * servings);
                    371:       printf (_("  Fruits:     %.2f cup-eq\n"), fped.fruits * servings);
                    372:       printf (_("  Grains:     %.2f oz-eq\n"), fped.grains * servings);
                    373:       printf (_("  Dairy:      %.2f cup-eq\n"), fped.dairy * servings);
                    374:       printf (_("  Protein:    %.2f oz-eq\n"), fped.protein * servings);
                    375:       printf (_("  Oils:       %.2f g\n"), fped.oils * servings);
                    376:     }
                    377: 
                    378:   return 0;
                    379: }
                    380: 
                    381: /* Show today's budget summary using the food log.  */
                    382: static int
                    383: cmd_budget (sqlite3 *food_db, sqlite3 *log_db, const char *date,
                    384:             int calories)
                    385: {
                    386:   struct daily_budget budget;
                    387:   struct daily_budget consumed;
                    388:   struct log_list entries;
                    389:   size_t i;
                    390: 
                    391:   budget = budget_for_calories (calories);
                    392:   memset (&consumed, 0, sizeof consumed);
                    393: 
                    394:   if (log_get_day (log_db, date, &entries) < 0)
                    395:     return 1;
                    396: 
                    397:   printf (_("Food log for %s:\n"), format_date (date));
                    398:   if (entries.count == 0)
                    399:     {
                    400:       printf (_("  (no entries)\n"));
                    401:     }
                    402:   else
                    403:     {
                    404:       for (i = 0; i < entries.count; i++)
                    405:         {
                    406:           struct fped_entry fped;
                    407:           printf (_("  %d - %s (%.1f servings)\n"),
                    408:                   entries.items[i].food_code,
                    409:                   entries.items[i].description,
                    410:                   entries.items[i].servings);
                    411:           if (db_get_fped (food_db, entries.items[i].food_code,
                    412:                            &fped) == 0)
                    413:             {
                    414:               double s = entries.items[i].servings;
                    415:               consumed.vegetables += fped.vegetables * s;
                    416:               consumed.fruits += fped.fruits * s;
                    417:               consumed.grains += fped.grains * s;
                    418:               consumed.dairy += fped.dairy * s;
                    419:               consumed.protein += fped.protein * s;
                    420:               consumed.oils += fped.oils * s;
                    421:             }
                    422:         }
                    423:     }
                    424: 
                    425:   log_list_free (&entries);
                    426: 
                    427:   printf ("\n");
                    428:   budget_print (&budget, &consumed);
                    429:   return 0;
                    430: }
                    431: 
                    432: static int
                    433: parse_gender (const char *name)
                    434: {
                    435:   if (strcmp (name, "neutral") == 0)
                    436:     return GENDER_NEUTRAL;
                    437:   if (strcmp (name, "female") == 0)
                    438:     return GENDER_FEMALE;
                    439:   if (strcmp (name, "male") == 0)
                    440:     return GENDER_MALE;
                    441:   return -1;
                    442: }
                    443: 
                    444: int
                    445: main (int argc, char **argv)
                    446: {
                    447:   int c;
                    448:   const char *db_path;
                    449:   const char *search_query;
                    450:   const char *date;
                    451:   char *date_alloc;
                    452:   char *log_path;
                    453:   const char *log_path_explicit;
                    454:   int info_code;
                    455:   int log_code;
                    456:   double log_servings;
                    457:   int do_budget;
                    458:   int calories;
                    459:   int calories_explicit;  /* 1 if --calories was given  */
                    460:   int profile_age;
                    461:   double profile_height;
                    462:   double profile_weight;
                    463:   int profile_activity;
                    464:   int profile_gender;
                    465:   int profile_given;  /* bitmask: 1=age, 2=height, 4=weight, 8=activity  */
                    466:   int mode;  /* 0 = interactive, 1 = search, 2 = info, 3 = log,
                    467:                 4 = budget, 5 = delete, 6 = edit  */
                    468:   int delete_id;
                    469:   int edit_id;
                    470:   int edit_quantity_given;
                    471:   int edit_date_given;
                    472:   sqlite3 *food_db;
                    473:   sqlite3 *log_db;
                    474:   int exit_status;
                    475: 
                    476:   /* Initialize variables explicitly (GNU Coding Standards).  */
                    477:   setlocale (LC_ALL, "");
                    478:   bindtextdomain ("gnutrition", LOCALEDIR);
                    479:   textdomain ("gnutrition");
                    480:   static char food_db_full_path[1024];
                    481:   snprintf(food_db_full_path, sizeof(food_db_full_path), "%s/food.db", GNUTRITION_DATADIR);
                    482:   db_path = food_db_full_path;
                    483:   search_query = NULL;
                    484:   date = NULL;
                    485:   date_alloc = NULL;
                    486:   log_path = NULL;
                    487:   log_path_explicit = NULL;
                    488:   info_code = 0;
                    489:   log_code = 0;
                    490:   log_servings = 1.0;
                    491:   do_budget = 0;
                    492:   calories = 2000;
                    493:   calories_explicit = 0;
                    494:   profile_age = 0;
                    495:   profile_height = 0.0;
                    496:   profile_weight = 0.0;
                    497:   profile_activity = ACTIVITY_SEDENTARY;
                    498:   profile_gender = GENDER_NEUTRAL;
                    499:   profile_given = 0;
                    500:   mode = 0;
                    501:   delete_id = 0;
                    502:   edit_id = 0;
                    503:   edit_quantity_given = 0;
                    504:   edit_date_given = 0;
                    505:   food_db = NULL;
                    506:   log_db = NULL;
                    507:   exit_status = 0;
                    508: 
                    509:   while ((c = getopt_long (argc, argv, "hVs:i:l:n:x:e:bc:a:H:w:A:d:D:P:",
                    510:                            long_options, NULL)) != -1)
                    511:     {
                    512:       switch (c)
                    513:         {
                    514:         case 'h':
                    515:           print_help ();
                    516:           return 0;
                    517: 
                    518:         case 'V':
                    519:           print_version ();
                    520:           return 0;
                    521: 
                    522:         case 's':
                    523:           search_query = optarg;
                    524:           mode = 1;
                    525:           break;
                    526: 
                    527:         case 'i':
                    528:           info_code = atoi (optarg);
                    529:           mode = 2;
                    530:           break;
                    531: 
                    532:         case 'l':
                    533:           log_code = atoi (optarg);
                    534:           mode = 3;
                    535:           break;
                    536: 
                    537:         case 'n':
                    538:           {
                    539:             char *endp;
                    540:             errno = 0;
                    541:             log_servings = strtod (optarg, &endp);
                    542:             if (errno != 0 || endp == optarg || *endp != '\0'
                    543:                 || log_servings <= 0.0)
                    544:               {
                    545:                 fprintf (stderr,
                    546:                          _("%s: invalid quantity '%s'\n"),
                    547:                          PROGRAM_NAME, optarg);
                    548:                 return 1;
                    549:               }
                    550:           }
                    551:           edit_quantity_given = 1;
                    552:           break;
                    553: 
                    554:         case 'x':
                    555:           delete_id = atoi (optarg);
                    556:           mode = 5;
                    557:           break;
                    558: 
                    559:         case 'e':
                    560:           edit_id = atoi (optarg);
                    561:           mode = 6;
                    562:           break;
                    563: 
                    564:         case 'b':
                    565:           do_budget = 1;
                    566:           mode = 4;
                    567:           break;
                    568: 
                    569:         case 'c':
                    570:           calories = budget_round_to_pattern (atoi (optarg));
                    571:           calories_explicit = 1;
                    572:           break;
                    573: 
                    574:         case 'a':
                    575:           profile_age = atoi (optarg);
                    576:           profile_given |= 1;
                    577:           break;
                    578: 
                    579:         case 'H':
                    580:           {
                    581:             char *endp;
                    582:             errno = 0;
                    583:             profile_height = strtod (optarg, &endp);
                    584:             if (errno != 0 || endp == optarg || *endp != '\0'
                    585:                 || profile_height <= 0.0)
                    586:               {
                    587:                 fprintf (stderr,
                    588:                          _("%s: invalid height '%s'\n"),
                    589:                          PROGRAM_NAME, optarg);
                    590:                 return 1;
                    591:               }
                    592:           }
                    593:           profile_given |= 2;
                    594:           break;
                    595: 
                    596:         case 'w':
                    597:           {
                    598:             char *endp;
                    599:             errno = 0;
                    600:             profile_weight = strtod (optarg, &endp);
                    601:             if (errno != 0 || endp == optarg || *endp != '\0'
                    602:                 || profile_weight <= 0.0)
                    603:               {
                    604:                 fprintf (stderr,
                    605:                          _("%s: invalid weight '%s'\n"),
                    606:                          PROGRAM_NAME, optarg);
                    607:                 return 1;
                    608:               }
                    609:           }
                    610:           profile_given |= 4;
                    611:           break;
                    612: 
                    613:         case 'A':
                    614:           profile_activity = parse_activity (optarg);
                    615:           if (profile_activity < 0)
                    616:             {
                    617:               fprintf (stderr, _("%s: unknown activity level '%s'\n"),
                    618:                        PROGRAM_NAME, optarg);
                    619:               fprintf (stderr, _("Valid levels: sedentary, light, moderate, "
                    620:                        "very-active, extra-active\n"));
                    621:               return 1;
                    622:             }
                    623:           profile_given |= 8;
                    624:           break;
                    625: 
                    626:         case 'G':
                    627:           profile_gender = parse_gender (optarg);
                    628:           if (profile_gender < 0)
                    629:             {
                    630:               fprintf (stderr, _("%s: unknown gender '%s'\n"),
                    631:                        PROGRAM_NAME, optarg);
                    632:               fprintf (stderr, _("Valid options: neutral, female, male\n"));
                    633:               return 1;
                    634:             }
                    635:           break;
                    636: 
                    637:         case 'd':
                    638:           date = optarg;
                    639:           edit_date_given = 1;
                    640:           break;
                    641: 
                    642:         case 'D':
                    643:           db_path = optarg;
                    644:           break;
                    645: 
                    646:         case 'P':
                    647:           log_path_explicit = optarg;
                    648:           break;
                    649: 
                    650:         default:
                    651:           fprintf (stderr, _("Try '%s --help' for more information.\n"),
                    652:                    PROGRAM_NAME);
                    653:           return 1;
                    654:         }
                    655:     }
                    656: 
                    657:   /* Default date to today if not specified.  */
                    658:   if (!date)
                    659:     {
                    660:       date_alloc = get_today ();
                    661:       if (!date_alloc)
                    662:         return 1;
                    663:       date = date_alloc;
                    664:     }
                    665:   else
                    666:     {
                    667:       date_alloc = normalize_date (date);
                    668:       if (!date_alloc)
                    669:         {
                    670:           fprintf (stderr, _("%s: invalid date: %s\n"), PROGRAM_NAME, date);
                    671:           return 1;
                    672:         }
                    673:       date = date_alloc;
                    674:     }
                    675: 
                    676:   /* Suppress unused variable warnings.  */
                    677:   (void) do_budget;
                    678: 
                    679:   /* Open the food database.  */
                    680:   food_db = db_open (db_path);
                    681:   if (!food_db)
                    682:     {
                    683:       free (date_alloc);
                    684:       return 1;
                    685:     }
                    686: 
                    687:   /* Open the user's food log / profile database.  */
                    688:   if (log_path_explicit)
                    689:     {
                    690:       log_path = strdup (log_path_explicit);
                    691:       if (!log_path)
                    692:         {
                    693:           fprintf (stderr, _("%s: memory exhausted\n"), PROGRAM_NAME);
                    694:           db_close (food_db);
                    695:           free (date_alloc);
                    696:           return 1;
                    697:         }
                    698:     }
                    699:   else
                    700:     {
                    701:       log_path = get_log_path ();
                    702:       if (!log_path)
                    703:         {
                    704:           db_close (food_db);
                    705:           free (date_alloc);
                    706:           return 1;
                    707:         }
                    708:     }
                    709: 
                    710:   if (ensure_dir (log_path) < 0)
                    711:     {
                    712:       db_close (food_db);
                    713:       free (log_path);
                    714:       free (date_alloc);
                    715:       return 1;
                    716:     }
                    717: 
                    718:   log_db = log_open (log_path);
                    719:   if (!log_db)
                    720:     {
                    721:       db_close (food_db);
                    722:       free (log_path);
                    723:       free (date_alloc);
                    724:       return 1;
                    725:     }
                    726: 
                    727:   /* If all profile fields were given on the command line, compute the
                    728:      calorie estimate, save the profile, and use the result (unless
                    729:      --calories was also given, which takes precedence).  */
                    730:   if (profile_given == 15)  /* all four bits set  */
                    731:     {
                    732:       struct user_profile prof;
                    733:       prof.age_years = profile_age;
                    734:       prof.height_cm = profile_height;
                    735:       prof.weight_kg = profile_weight;
                    736:       prof.activity_level = profile_activity;
                    737:       prof.gender = profile_gender;
                    738:       prof.calorie_target = budget_estimate_calories (profile_age,
                    739:                                                       profile_height,
                    740:                                                       profile_weight,
                    741:                                                       profile_activity,
                    742:                                                       profile_gender);
                    743:       if (log_save_profile (log_db, &prof) < 0)
                    744:         fprintf (stderr, _("%s: warning: could not save profile\n"),
                    745:                  PROGRAM_NAME);
                    746:       else
                    747:         printf (_("Profile saved (estimated %d kcal/day).\n"),
                    748:                 prof.calorie_target);
                    749: 
                    750:       if (!calories_explicit)
                    751:         calories = prof.calorie_target;
                    752:     }
                    753:   else if (profile_given != 0)
                    754:     {
                    755:       fprintf (stderr, _("%s: --age, --height, --weight, and --activity "
                    756:                "must all be given together\n"), PROGRAM_NAME);
                    757:       log_close (log_db);
                    758:       db_close (food_db);
                    759:       free (log_path);
                    760:       free (date_alloc);
                    761:       return 1;
                    762:     }
                    763:   else if (!calories_explicit)
                    764:     {
                    765:       /* No profile args and no --calories: load saved profile.  */
                    766:       struct user_profile saved;
                    767:       int rc = log_get_profile (log_db, &saved);
                    768:       if (rc == 0 && saved.calorie_target > 0)
                    769:         calories = saved.calorie_target;
                    770:     }
                    771: 
                    772:   switch (mode)
                    773:     {
                    774:     case 0:
                    775:       /* Interactive ncurses mode.  */
                    776:       exit_status = ui_run (food_db, log_db, calories) < 0 ? 1 : 0;
                    777:       break;
                    778: 
                    779:     case 1:
                    780:       exit_status = cmd_search (food_db, search_query);
                    781:       break;
                    782: 
                    783:     case 2:
                    784:       exit_status = cmd_info (food_db, info_code, log_servings);
                    785:       break;
                    786: 
                    787:     case 3:
                    788:       {
                    789:         /* Log a food: first look up the description.  */
                    790:         struct food_list results;
                    791:         char code_str[32];
                    792:         snprintf (code_str, sizeof code_str, "%d", log_code);
                    793: 
                    794:         /* Search by exact food code - we query for foods matching
                    795:            the code string but we'll match by code.  */
                    796:         if (db_search_foods (food_db, "", &results) == 0)
                    797:           {
                    798:             size_t j;
                    799:             const char *desc = _("Unknown food");
                    800:             for (j = 0; j < results.count; j++)
                    801:               {
                    802:                 if (results.items[j].food_code == log_code)
                    803:                   {
                    804:                     desc = results.items[j].description;
                    805:                     break;
                    806:                   }
                    807:               }
                    808:             if (log_add (log_db, log_code, desc, date,
                    809:                         log_servings) < 0)
                    810:               exit_status = 1;
                    811:             else
                    812:               printf (_("Logged food %d (%s) x%.1f for %s.\n"),
                    813:                       log_code, desc, log_servings,
                    814:                       format_date (date));
                    815:             food_list_free (&results);
                    816:           }
                    817:         else
                    818:           {
                    819:             exit_status = 1;
                    820:           }
                    821:       }
                    822:       break;
                    823: 
                    824:     case 4:
                    825:       exit_status = cmd_budget (food_db, log_db, date, calories);
                    826:       break;
                    827: 
                    828:     case 5:
                    829:       /* Delete a log entry.  */
                    830:       if (log_delete (log_db, delete_id) < 0)
                    831:         exit_status = 1;
                    832:       else
                    833:         printf (_("Deleted log entry %d.\n"), delete_id);
                    834:       break;
                    835: 
                    836:     case 6:
                    837:       /* Edit a log entry.  At least -n or -d must be given.  */
                    838:       if (!edit_quantity_given && !edit_date_given)
                    839:         {
                    840:           fprintf (stderr,
                    841:                    _("%s: --edit requires --quantity and/or --date\n"),
                    842:                    PROGRAM_NAME);
                    843:           exit_status = 1;
                    844:         }
                    845:       else
                    846:         {
                    847:           /* We need to fetch the current entry to fill in unchanged
                    848:              fields.  Use a simple query by iterating the date list
                    849:              and entries.  We look up the entry by ID.  */
                    850:           struct date_list dl;
                    851:           int found = 0;
                    852:           if (log_get_dates (log_db, &dl) == 0)
                    853:             {
                    854:               size_t di;
                    855:               for (di = 0; di < dl.count && !found; di++)
                    856:                 {
                    857:                   struct log_list el;
                    858:                   if (log_get_day (log_db, dl.dates[di], &el) == 0)
                    859:                     {
                    860:                       size_t ei;
                    861:                       for (ei = 0; ei < el.count; ei++)
                    862:                         {
                    863:                           if (el.items[ei].id == edit_id)
                    864:                             {
                    865:                               double new_srv = edit_quantity_given
                    866:                                 ? log_servings : el.items[ei].servings;
                    867:                               const char *new_date = edit_date_given
                    868:                                 ? date : el.items[ei].date;
                    869:                               if (log_update (log_db, edit_id,
                    870:                                               new_date, new_srv) < 0)
                    871:                                 exit_status = 1;
                    872:                               else
                    873:                                 printf (_("Updated log entry %d "
                    874:                                           "(%.1f servings, %s).\n"),
                    875:                                         edit_id, new_srv,
                    876:                                         format_date (new_date));
                    877:                               found = 1;
                    878:                               break;
                    879:                             }
                    880:                         }
                    881:                       log_list_free (&el);
                    882:                     }
                    883:                 }
                    884:               date_list_free (&dl);
                    885:             }
                    886:           if (!found && exit_status == 0)
                    887:             {
                    888:               fprintf (stderr,
                    889:                        _("%s: log entry %d not found\n"),
                    890:                        PROGRAM_NAME, edit_id);
                    891:               exit_status = 1;
                    892:             }
                    893:         }
                    894:       break;
                    895: 
                    896:     default:
                    897:       abort ();
                    898:     }
                    899: 
                    900:   log_close (log_db);
                    901:   db_close (food_db);
                    902:   free (log_path);
                    903:   free (date_alloc);
                    904:   return exit_status;
                    905: }

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