Annotation of gnutrition/main.c, revision 1.5
1.1 asm 1: // SPDX-License-Identifier: GPL-3.0-or-later
2: /*
1.5 ! asm 3: * $Id: main.c,v 1.4 2026/05/12 21:32:44 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"));
1.5 ! asm 92: printf (_(" -G, --gender=NAME neutral, female, or male\n"));
1.1 asm 93: printf (_(" -A, --activity=LEVEL activity level: sedentary, light,\n"));
1.3 asm 94: printf (_(" moderate, very-active, or extra-active\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"));
1.5 ! asm 103: printf (_("\nWhen --age, --height, --weight, --gender and --activity are all\n"));
! 104: printf (_("given, the calorie target is estimated using the Mifflin-St Jeor\n"));
1.1 asm 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;
1.5 ! asm 463: int profile_gender; /* neutral, female, male */
1.1 asm 464: int profile_activity;
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;
1.5 ! asm 497: profile_gender = GENDER_NEUTRAL;
1.1 asm 498: profile_activity = ACTIVITY_SEDENTARY;
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:
1.4 asm 509: while ((c = getopt_long (argc, argv, "hVs:i:l:n:x:e:bc:a:H:w:A:G:d:D:P:",
1.1 asm 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:
1.5 ! asm 613: case 'G':
! 614: profile_gender = parse_gender (optarg);
! 615: if (profile_gender < 0)
! 616: {
! 617: fprintf (stderr, _("%s: unknown gender '%s'\n"),
! 618: PROGRAM_NAME, optarg);
! 619: fprintf (stderr, _("Valid options: neutral, female, male\n"));
! 620: return 1;
! 621: }
! 622: profile_given |= 7;
! 623: break;
! 624:
1.1 asm 625: case 'A':
626: profile_activity = parse_activity (optarg);
627: if (profile_activity < 0)
628: {
629: fprintf (stderr, _("%s: unknown activity level '%s'\n"),
630: PROGRAM_NAME, optarg);
631: fprintf (stderr, _("Valid levels: sedentary, light, moderate, "
632: "very-active, extra-active\n"));
633: return 1;
634: }
635: profile_given |= 8;
636: break;
637:
638: case 'd':
639: date = optarg;
640: edit_date_given = 1;
641: break;
642:
643: case 'D':
644: db_path = optarg;
645: break;
646:
647: case 'P':
648: log_path_explicit = optarg;
649: break;
650:
651: default:
652: fprintf (stderr, _("Try '%s --help' for more information.\n"),
653: PROGRAM_NAME);
654: return 1;
655: }
656: }
657:
658: /* Default date to today if not specified. */
659: if (!date)
660: {
661: date_alloc = get_today ();
662: if (!date_alloc)
663: return 1;
664: date = date_alloc;
665: }
666: else
667: {
668: date_alloc = normalize_date (date);
669: if (!date_alloc)
670: {
671: fprintf (stderr, _("%s: invalid date: %s\n"), PROGRAM_NAME, date);
672: return 1;
673: }
674: date = date_alloc;
675: }
676:
677: /* Suppress unused variable warnings. */
678: (void) do_budget;
679:
680: /* Open the food database. */
681: food_db = db_open (db_path);
682: if (!food_db)
683: {
684: free (date_alloc);
685: return 1;
686: }
687:
688: /* Open the user's food log / profile database. */
689: if (log_path_explicit)
690: {
691: log_path = strdup (log_path_explicit);
692: if (!log_path)
693: {
694: fprintf (stderr, _("%s: memory exhausted\n"), PROGRAM_NAME);
695: db_close (food_db);
696: free (date_alloc);
697: return 1;
698: }
699: }
700: else
701: {
702: log_path = get_log_path ();
703: if (!log_path)
704: {
705: db_close (food_db);
706: free (date_alloc);
707: return 1;
708: }
709: }
710:
711: if (ensure_dir (log_path) < 0)
712: {
713: db_close (food_db);
714: free (log_path);
715: free (date_alloc);
716: return 1;
717: }
718:
719: log_db = log_open (log_path);
720: if (!log_db)
721: {
722: db_close (food_db);
723: free (log_path);
724: free (date_alloc);
725: return 1;
726: }
727:
728: /* If all profile fields were given on the command line, compute the
729: calorie estimate, save the profile, and use the result (unless
730: --calories was also given, which takes precedence). */
731: if (profile_given == 15) /* all four bits set */
732: {
733: struct user_profile prof;
734: prof.age_years = profile_age;
735: prof.height_cm = profile_height;
736: prof.weight_kg = profile_weight;
1.5 ! asm 737: prof.gender = profile_gender;
1.1 asm 738: prof.activity_level = profile_activity;
739: prof.calorie_target = budget_estimate_calories (profile_age,
740: profile_height,
741: profile_weight,
1.5 ! asm 742: profile_gender,
! 743: profile_activity);
1.1 asm 744: if (log_save_profile (log_db, &prof) < 0)
745: fprintf (stderr, _("%s: warning: could not save profile\n"),
746: PROGRAM_NAME);
747: else
748: printf (_("Profile saved (estimated %d kcal/day).\n"),
749: prof.calorie_target);
750:
751: if (!calories_explicit)
752: calories = prof.calorie_target;
753: }
754: else if (profile_given != 0)
755: {
1.5 ! asm 756: fprintf (stderr, _("%s: --age, --height, --weight, --gender, and --activity "
1.1 asm 757: "must all be given together\n"), PROGRAM_NAME);
758: log_close (log_db);
759: db_close (food_db);
760: free (log_path);
761: free (date_alloc);
762: return 1;
763: }
764: else if (!calories_explicit)
765: {
766: /* No profile args and no --calories: load saved profile. */
767: struct user_profile saved;
768: int rc = log_get_profile (log_db, &saved);
769: if (rc == 0 && saved.calorie_target > 0)
770: calories = saved.calorie_target;
771: }
772:
773: switch (mode)
774: {
775: case 0:
776: /* Interactive ncurses mode. */
777: exit_status = ui_run (food_db, log_db, calories) < 0 ? 1 : 0;
778: break;
779:
780: case 1:
781: exit_status = cmd_search (food_db, search_query);
782: break;
783:
784: case 2:
785: exit_status = cmd_info (food_db, info_code, log_servings);
786: break;
787:
788: case 3:
789: {
790: /* Log a food: first look up the description. */
791: struct food_list results;
792: char code_str[32];
793: snprintf (code_str, sizeof code_str, "%d", log_code);
794:
795: /* Search by exact food code - we query for foods matching
796: the code string but we'll match by code. */
797: if (db_search_foods (food_db, "", &results) == 0)
798: {
799: size_t j;
800: const char *desc = _("Unknown food");
801: for (j = 0; j < results.count; j++)
802: {
803: if (results.items[j].food_code == log_code)
804: {
805: desc = results.items[j].description;
806: break;
807: }
808: }
809: if (log_add (log_db, log_code, desc, date,
810: log_servings) < 0)
811: exit_status = 1;
812: else
813: printf (_("Logged food %d (%s) x%.1f for %s.\n"),
814: log_code, desc, log_servings,
815: format_date (date));
816: food_list_free (&results);
817: }
818: else
819: {
820: exit_status = 1;
821: }
822: }
823: break;
824:
825: case 4:
826: exit_status = cmd_budget (food_db, log_db, date, calories);
827: break;
828:
829: case 5:
830: /* Delete a log entry. */
831: if (log_delete (log_db, delete_id) < 0)
832: exit_status = 1;
833: else
834: printf (_("Deleted log entry %d.\n"), delete_id);
835: break;
836:
837: case 6:
838: /* Edit a log entry. At least -n or -d must be given. */
839: if (!edit_quantity_given && !edit_date_given)
840: {
841: fprintf (stderr,
842: _("%s: --edit requires --quantity and/or --date\n"),
843: PROGRAM_NAME);
844: exit_status = 1;
845: }
846: else
847: {
848: /* We need to fetch the current entry to fill in unchanged
849: fields. Use a simple query by iterating the date list
850: and entries. We look up the entry by ID. */
851: struct date_list dl;
852: int found = 0;
853: if (log_get_dates (log_db, &dl) == 0)
854: {
855: size_t di;
856: for (di = 0; di < dl.count && !found; di++)
857: {
858: struct log_list el;
859: if (log_get_day (log_db, dl.dates[di], &el) == 0)
860: {
861: size_t ei;
862: for (ei = 0; ei < el.count; ei++)
863: {
864: if (el.items[ei].id == edit_id)
865: {
866: double new_srv = edit_quantity_given
867: ? log_servings : el.items[ei].servings;
868: const char *new_date = edit_date_given
869: ? date : el.items[ei].date;
870: if (log_update (log_db, edit_id,
871: new_date, new_srv) < 0)
872: exit_status = 1;
873: else
874: printf (_("Updated log entry %d "
875: "(%.1f servings, %s).\n"),
876: edit_id, new_srv,
877: format_date (new_date));
878: found = 1;
879: break;
880: }
881: }
882: log_list_free (&el);
883: }
884: }
885: date_list_free (&dl);
886: }
887: if (!found && exit_status == 0)
888: {
889: fprintf (stderr,
890: _("%s: log entry %d not found\n"),
891: PROGRAM_NAME, edit_id);
892: exit_status = 1;
893: }
894: }
895: break;
896:
897: default:
898: abort ();
899: }
900:
901: log_close (log_db);
902: db_close (food_db);
903: free (log_path);
904: free (date_alloc);
905: return exit_status;
906: }
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>