// SPDX-License-Identifier: GPL-3.0-or-later
/*
* $Id: main.c,v 1.1 2026/05/08 03:23:59 asm Exp $
*
* main.c - Entry point for GNUtrition
*
* Copyright (C) 2026 Free Software Foundation, Inc.
*
* Author: Jason Self <jself@gnu.org>
* Anton McClure <asm@gnu.org>
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include "budget.h"
#include "db.h"
#include "log.h"
#include "ui.h"
#include "i18n.h"
#include <errno.h>
#include <getopt.h>
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#define PROGRAM_NAME "gnutrition"
static const struct option long_options[] =
{
{"help", no_argument, NULL, 'h'},
{"version", no_argument, NULL, 'V'},
{"search", required_argument, NULL, 's'},
{"info", required_argument, NULL, 'i'},
{"log", required_argument, NULL, 'l'},
{"quantity", required_argument, NULL, 'n'},
{"delete", required_argument, NULL, 'x'},
{"edit", required_argument, NULL, 'e'},
{"budget", no_argument, NULL, 'b'},
{"calories", required_argument, NULL, 'c'},
{"age", required_argument, NULL, 'a'},
{"height", required_argument, NULL, 'H'},
{"weight", required_argument, NULL, 'w'},
{"activity", required_argument, NULL, 'A'},
{"gender", required_argument, NULL, 'G'},
{"date", required_argument, NULL, 'd'},
{"db", required_argument, NULL, 'D'},
{"profile-db", required_argument, NULL, 'P'},
{NULL, 0, NULL, 0}
};
static void
print_version (void)
{
printf (_("GNUtrition %s\n"), PACKAGE_VERSION);
printf (_("Copyright (C) 2026 Free Software Foundation, Inc.\n"));
printf (_("License GPLv3+: GNU GPL version 3 or later "
"<http://gnu.org/licenses/gpl.html>\n"));
printf (_("This is free software: you are free to change "
"and redistribute it.\n"));
printf (_("There is NO WARRANTY, to the extent permitted by law.\n"));
}
static void
print_help (void)
{
printf (_("Usage: %s [OPTION]...\n"), PROGRAM_NAME);
printf (_("Track your daily food intake using the USDA Food Pattern "
"budget system.\n\n"));
printf (_("With no options, starts the interactive ncurses interface.\n\n"));
printf (_("Options:\n"));
printf (_(" -s, --search=QUERY search for foods matching QUERY\n"));
printf (_(" -i, --info=CODE show nutrient info for food CODE\n"));
printf (_(" -l, --log=CODE log a food by its code\n"));
printf (_(" -n, --quantity=NUM number of servings to log "
"(default: 1.0)\n"));
printf (_(" -x, --delete=ID delete a food log entry by its ID\n"));
printf (_(" -e, --edit=ID edit a food log entry (use with "
"-n and/or -d)\n"));
printf (_(" -b, --budget show today's budget summary\n"));
printf (_(" -c, --calories=KCAL set daily calorie target "
"(default: 2000)\n"));
printf (_(" -a, --age=YEARS your age in years "
"(for calorie estimation)\n"));
printf (_(" -H, --height=CM your height in centimeters\n"));
printf (_(" -w, --weight=KG your weight in kilograms\n"));
printf (_(" -A, --activity=LEVEL activity level: sedentary, light,\n"));
printf (_(" moderate, very-active, "
"or extra-active\n"));
printf (_(" -d, --date=DATE date for log/budget "
"(default: today)\n"));
printf (_(" -D, --db=PATH path to food database "
"(default: food.db)\n"));
printf (_(" -P, --profile-db=PATH\n"));
printf (_(" path to profile/log database\n"));
printf (_(" (default: "
"$XDG_DATA_HOME/gnutrition/log.db)\n"));
printf (_(" -h, --help display this help and exit\n"));
printf (_(" -V, --version output version information and exit\n"));
printf (_("\nWhen --age, --height, --weight, and --activity are all given,\n"));
printf (_("the calorie target is estimated using the Mifflin-St Jeor\n"));
printf (_("equation and saved to your profile for future sessions.\n"));
printf (_("Use --calories to override the computed estimate.\n"));
printf (_("\nThe calorie level determines your daily food-group budget\n"));
printf (_("using the USDA Healthy US-Style Eating Pattern table\n"));
printf (_("(Dietary Guidelines for Americans, 2020-2025, Appendix 3).\n"));
printf (_("Values are rounded to the nearest 200 kcal pattern level\n"));
printf (_("(range: 1000-3200).\n"));
printf (_("\nYour profile and food log are stored in a separate database\n"));
printf (_("at $XDG_DATA_HOME/gnutrition/log.db (the USDA food database\n"));
printf (_("is never modified).\n"));
printf (_("\nReport bugs to: bug-gnutrition@gnu.org\n"));
printf (_("GNUtrition home page: "
"<http://www.gnu.org/software/gnutrition/>\n"));
printf (_("General help using GNU software: "
"<http://www.gnu.org/gethelp/>\n"));
}
/* Parse an activity-level name. Returns -1 if NAME is not
recognized. */
static int
parse_activity (const char *name)
{
if (strcmp (name, "sedentary") == 0)
return ACTIVITY_SEDENTARY;
if (strcmp (name, "light") == 0)
return ACTIVITY_LIGHT;
if (strcmp (name, "moderate") == 0)
return ACTIVITY_MODERATE;
if (strcmp (name, "very-active") == 0)
return ACTIVITY_VERY_ACTIVE;
if (strcmp (name, "extra-active") == 0)
return ACTIVITY_EXTRA_ACTIVE;
return -1;
}
/* Return today's date as a dynamically-allocated string. The caller
must free the result. Returns NULL on failure. */
static char *
get_today (void)
{
char *buf;
time_t now;
struct tm *tm;
buf = malloc (11);
if (!buf)
{
fprintf (stderr, _("%s: memory exhausted\n"), PROGRAM_NAME);
return NULL;
}
now = time (NULL);
tm = localtime (&now);
strftime (buf, 11, "%Y-%m-%d", tm);
return buf;
}
/* Normalize a date string to ISO 8601 format (YYYY-MM-DD). Accepts
the locale's preferred date representation (%x). Returns a
dynamically-allocated string on success, or NULL on failure. */
static char *
normalize_date (const char *input)
{
struct tm tm;
char *buf;
buf = malloc (11);
if (!buf)
{
fprintf (stderr, _("%s: memory exhausted\n"), PROGRAM_NAME);
return NULL;
}
memset (&tm, 0, sizeof tm);
if (strptime (input, "%x", &tm) != NULL)
{
if (strftime (buf, 11, "%Y-%m-%d", &tm) > 0)
return buf;
}
free (buf);
return NULL;
}
/* Format an ISO 8601 date (YYYY-MM-DD) for display using the
locale's preferred date representation. Returns a pointer to a
static buffer; not reentrant. */
static const char *
format_date (const char *iso_date)
{
static char buf[64];
struct tm tm;
memset (&tm, 0, sizeof tm);
if (sscanf (iso_date, "%d-%d-%d",
&tm.tm_year, &tm.tm_mon, &tm.tm_mday) != 3)
return iso_date;
tm.tm_year -= 1900;
tm.tm_mon -= 1;
if (strftime (buf, sizeof buf, "%x", &tm) == 0)
return iso_date;
return buf;
}
/* Get the path to the user's log database. Uses
$XDG_DATA_HOME/gnutrition/log.db or ~/.local/share/gnutrition/log.db.
Returns a dynamically-allocated string. */
static char *
get_log_path (void)
{
const char *data_home;
char *path;
size_t len;
data_home = getenv ("XDG_DATA_HOME");
if (data_home && data_home[0] != '\0')
{
len = strlen (data_home) + strlen ("/gnutrition/log.db") + 1;
path = malloc (len);
if (!path)
{
fprintf (stderr, _("%s: memory exhausted\n"), PROGRAM_NAME);
return NULL;
}
snprintf (path, len, "%s/gnutrition/log.db", data_home);
}
else
{
const char *home = getenv ("HOME");
if (!home)
{
fprintf (stderr, _("%s: HOME is not set\n"), PROGRAM_NAME);
return NULL;
}
len = strlen (home)
+ strlen ("/.local/share/gnutrition/log.db") + 1;
path = malloc (len);
if (!path)
{
fprintf (stderr, _("%s: memory exhausted\n"), PROGRAM_NAME);
return NULL;
}
snprintf (path, len, "%s/.local/share/gnutrition/log.db", home);
}
return path;
}
/* Ensure the directory containing PATH exists. */
static int
ensure_dir (const char *path)
{
char *dir;
char *slash;
char *copy;
copy = strdup (path);
if (!copy)
{
fprintf (stderr, _("%s: memory exhausted\n"), PROGRAM_NAME);
return -1;
}
/* Find the last slash to get the directory part. */
slash = strrchr (copy, '/');
if (slash)
{
*slash = '\0';
dir = copy;
/* Build the directory path component by component. */
{
char *p;
for (p = dir + 1; *p; p++)
{
if (*p == '/')
{
*p = '\0';
if (mkdir (dir, 0755) < 0 && errno != EEXIST)
{
fprintf (stderr, _("%s: cannot create directory '%s': %s\n"),
PROGRAM_NAME, dir, strerror (errno));
free (copy);
return -1;
}
*p = '/';
}
}
if (mkdir (dir, 0755) < 0 && errno != EEXIST)
{
fprintf (stderr, _("%s: cannot create directory '%s': %s\n"),
PROGRAM_NAME, dir, strerror (errno));
free (copy);
return -1;
}
}
}
free (copy);
return 0;
}
/* Perform a CLI food search. */
static int
cmd_search (sqlite3 *db, const char *query)
{
struct food_list results;
size_t i;
if (db_search_foods (db, query, &results) < 0)
return 1;
if (results.count == 0)
{
printf (_("No foods found matching '%s'.\n"), query);
}
else
{
printf ("%-10s %s\n", _("Code"), _("Description"));
printf ("%-10s %s\n", "----------",
"--------------------------------------------");
for (i = 0; i < results.count; i++)
printf ("%-10d %s\n", results.items[i].food_code,
results.items[i].description);
}
food_list_free (&results);
return 0;
}
/* Show nutrient information for a food code, scaled by SERVINGS. */
static int
cmd_info (sqlite3 *db, int food_code, double servings)
{
struct nutrient_list nutrients;
struct fped_entry fped;
size_t i;
if (db_get_nutrients (db, food_code, &nutrients) < 0)
return 1;
if (nutrients.count == 0)
{
printf (_("No nutrient data found for food code %d.\n"), food_code);
}
else
{
printf (_("Nutrient information for food code %d:\n\n"), food_code);
printf ("%-40s %10s\n", _("Nutrient"), _("Value"));
printf ("%-40s %10s\n",
"----------------------------------------",
"----------");
for (i = 0; i < nutrients.count; i++)
printf ("%-40s %10.2f\n", nutrients.items[i].name,
nutrients.items[i].value * servings);
}
nutrient_list_free (&nutrients);
/* Also show FPED budget cost if available. */
if (db_get_fped (db, food_code, &fped) == 0)
{
if (servings != 1.0)
printf (_("\nFood Pattern Equivalents (per 100g x %.1f):\n"),
servings);
else
printf (_("\nFood Pattern Equivalents (per 100g):\n"));
printf (_(" Vegetables: %.2f cup-eq\n"), fped.vegetables * servings);
printf (_(" Fruits: %.2f cup-eq\n"), fped.fruits * servings);
printf (_(" Grains: %.2f oz-eq\n"), fped.grains * servings);
printf (_(" Dairy: %.2f cup-eq\n"), fped.dairy * servings);
printf (_(" Protein: %.2f oz-eq\n"), fped.protein * servings);
printf (_(" Oils: %.2f g\n"), fped.oils * servings);
}
return 0;
}
/* Show today's budget summary using the food log. */
static int
cmd_budget (sqlite3 *food_db, sqlite3 *log_db, const char *date,
int calories)
{
struct daily_budget budget;
struct daily_budget consumed;
struct log_list entries;
size_t i;
budget = budget_for_calories (calories);
memset (&consumed, 0, sizeof consumed);
if (log_get_day (log_db, date, &entries) < 0)
return 1;
printf (_("Food log for %s:\n"), format_date (date));
if (entries.count == 0)
{
printf (_(" (no entries)\n"));
}
else
{
for (i = 0; i < entries.count; i++)
{
struct fped_entry fped;
printf (_(" %d - %s (%.1f servings)\n"),
entries.items[i].food_code,
entries.items[i].description,
entries.items[i].servings);
if (db_get_fped (food_db, entries.items[i].food_code,
&fped) == 0)
{
double s = entries.items[i].servings;
consumed.vegetables += fped.vegetables * s;
consumed.fruits += fped.fruits * s;
consumed.grains += fped.grains * s;
consumed.dairy += fped.dairy * s;
consumed.protein += fped.protein * s;
consumed.oils += fped.oils * s;
}
}
}
log_list_free (&entries);
printf ("\n");
budget_print (&budget, &consumed);
return 0;
}
static int
parse_gender (const char *name)
{
if (strcmp (name, "neutral") == 0)
return GENDER_NEUTRAL;
if (strcmp (name, "female") == 0)
return GENDER_FEMALE;
if (strcmp (name, "male") == 0)
return GENDER_MALE;
return -1;
}
int
main (int argc, char **argv)
{
int c;
const char *db_path;
const char *search_query;
const char *date;
char *date_alloc;
char *log_path;
const char *log_path_explicit;
int info_code;
int log_code;
double log_servings;
int do_budget;
int calories;
int calories_explicit; /* 1 if --calories was given */
int profile_age;
double profile_height;
double profile_weight;
int profile_activity;
int profile_gender;
int profile_given; /* bitmask: 1=age, 2=height, 4=weight, 8=activity */
int mode; /* 0 = interactive, 1 = search, 2 = info, 3 = log,
4 = budget, 5 = delete, 6 = edit */
int delete_id;
int edit_id;
int edit_quantity_given;
int edit_date_given;
sqlite3 *food_db;
sqlite3 *log_db;
int exit_status;
/* Initialize variables explicitly (GNU Coding Standards). */
setlocale (LC_ALL, "");
bindtextdomain ("gnutrition", LOCALEDIR);
textdomain ("gnutrition");
static char food_db_full_path[1024];
snprintf(food_db_full_path, sizeof(food_db_full_path), "%s/food.db", GNUTRITION_DATADIR);
db_path = food_db_full_path;
search_query = NULL;
date = NULL;
date_alloc = NULL;
log_path = NULL;
log_path_explicit = NULL;
info_code = 0;
log_code = 0;
log_servings = 1.0;
do_budget = 0;
calories = 2000;
calories_explicit = 0;
profile_age = 0;
profile_height = 0.0;
profile_weight = 0.0;
profile_activity = ACTIVITY_SEDENTARY;
profile_gender = GENDER_NEUTRAL;
profile_given = 0;
mode = 0;
delete_id = 0;
edit_id = 0;
edit_quantity_given = 0;
edit_date_given = 0;
food_db = NULL;
log_db = NULL;
exit_status = 0;
while ((c = getopt_long (argc, argv, "hVs:i:l:n:x:e:bc:a:H:w:A:d:D:P:",
long_options, NULL)) != -1)
{
switch (c)
{
case 'h':
print_help ();
return 0;
case 'V':
print_version ();
return 0;
case 's':
search_query = optarg;
mode = 1;
break;
case 'i':
info_code = atoi (optarg);
mode = 2;
break;
case 'l':
log_code = atoi (optarg);
mode = 3;
break;
case 'n':
{
char *endp;
errno = 0;
log_servings = strtod (optarg, &endp);
if (errno != 0 || endp == optarg || *endp != '\0'
|| log_servings <= 0.0)
{
fprintf (stderr,
_("%s: invalid quantity '%s'\n"),
PROGRAM_NAME, optarg);
return 1;
}
}
edit_quantity_given = 1;
break;
case 'x':
delete_id = atoi (optarg);
mode = 5;
break;
case 'e':
edit_id = atoi (optarg);
mode = 6;
break;
case 'b':
do_budget = 1;
mode = 4;
break;
case 'c':
calories = budget_round_to_pattern (atoi (optarg));
calories_explicit = 1;
break;
case 'a':
profile_age = atoi (optarg);
profile_given |= 1;
break;
case 'H':
{
char *endp;
errno = 0;
profile_height = strtod (optarg, &endp);
if (errno != 0 || endp == optarg || *endp != '\0'
|| profile_height <= 0.0)
{
fprintf (stderr,
_("%s: invalid height '%s'\n"),
PROGRAM_NAME, optarg);
return 1;
}
}
profile_given |= 2;
break;
case 'w':
{
char *endp;
errno = 0;
profile_weight = strtod (optarg, &endp);
if (errno != 0 || endp == optarg || *endp != '\0'
|| profile_weight <= 0.0)
{
fprintf (stderr,
_("%s: invalid weight '%s'\n"),
PROGRAM_NAME, optarg);
return 1;
}
}
profile_given |= 4;
break;
case 'A':
profile_activity = parse_activity (optarg);
if (profile_activity < 0)
{
fprintf (stderr, _("%s: unknown activity level '%s'\n"),
PROGRAM_NAME, optarg);
fprintf (stderr, _("Valid levels: sedentary, light, moderate, "
"very-active, extra-active\n"));
return 1;
}
profile_given |= 8;
break;
case 'G':
profile_gender = parse_gender (optarg);
if (profile_gender < 0)
{
fprintf (stderr, _("%s: unknown gender '%s'\n"),
PROGRAM_NAME, optarg);
fprintf (stderr, _("Valid options: neutral, female, male\n"));
return 1;
}
break;
case 'd':
date = optarg;
edit_date_given = 1;
break;
case 'D':
db_path = optarg;
break;
case 'P':
log_path_explicit = optarg;
break;
default:
fprintf (stderr, _("Try '%s --help' for more information.\n"),
PROGRAM_NAME);
return 1;
}
}
/* Default date to today if not specified. */
if (!date)
{
date_alloc = get_today ();
if (!date_alloc)
return 1;
date = date_alloc;
}
else
{
date_alloc = normalize_date (date);
if (!date_alloc)
{
fprintf (stderr, _("%s: invalid date: %s\n"), PROGRAM_NAME, date);
return 1;
}
date = date_alloc;
}
/* Suppress unused variable warnings. */
(void) do_budget;
/* Open the food database. */
food_db = db_open (db_path);
if (!food_db)
{
free (date_alloc);
return 1;
}
/* Open the user's food log / profile database. */
if (log_path_explicit)
{
log_path = strdup (log_path_explicit);
if (!log_path)
{
fprintf (stderr, _("%s: memory exhausted\n"), PROGRAM_NAME);
db_close (food_db);
free (date_alloc);
return 1;
}
}
else
{
log_path = get_log_path ();
if (!log_path)
{
db_close (food_db);
free (date_alloc);
return 1;
}
}
if (ensure_dir (log_path) < 0)
{
db_close (food_db);
free (log_path);
free (date_alloc);
return 1;
}
log_db = log_open (log_path);
if (!log_db)
{
db_close (food_db);
free (log_path);
free (date_alloc);
return 1;
}
/* If all profile fields were given on the command line, compute the
calorie estimate, save the profile, and use the result (unless
--calories was also given, which takes precedence). */
if (profile_given == 15) /* all four bits set */
{
struct user_profile prof;
prof.age_years = profile_age;
prof.height_cm = profile_height;
prof.weight_kg = profile_weight;
prof.activity_level = profile_activity;
prof.gender = profile_gender;
prof.calorie_target = budget_estimate_calories (profile_age,
profile_height,
profile_weight,
profile_activity,
profile_gender);
if (log_save_profile (log_db, &prof) < 0)
fprintf (stderr, _("%s: warning: could not save profile\n"),
PROGRAM_NAME);
else
printf (_("Profile saved (estimated %d kcal/day).\n"),
prof.calorie_target);
if (!calories_explicit)
calories = prof.calorie_target;
}
else if (profile_given != 0)
{
fprintf (stderr, _("%s: --age, --height, --weight, and --activity "
"must all be given together\n"), PROGRAM_NAME);
log_close (log_db);
db_close (food_db);
free (log_path);
free (date_alloc);
return 1;
}
else if (!calories_explicit)
{
/* No profile args and no --calories: load saved profile. */
struct user_profile saved;
int rc = log_get_profile (log_db, &saved);
if (rc == 0 && saved.calorie_target > 0)
calories = saved.calorie_target;
}
switch (mode)
{
case 0:
/* Interactive ncurses mode. */
exit_status = ui_run (food_db, log_db, calories) < 0 ? 1 : 0;
break;
case 1:
exit_status = cmd_search (food_db, search_query);
break;
case 2:
exit_status = cmd_info (food_db, info_code, log_servings);
break;
case 3:
{
/* Log a food: first look up the description. */
struct food_list results;
char code_str[32];
snprintf (code_str, sizeof code_str, "%d", log_code);
/* Search by exact food code - we query for foods matching
the code string but we'll match by code. */
if (db_search_foods (food_db, "", &results) == 0)
{
size_t j;
const char *desc = _("Unknown food");
for (j = 0; j < results.count; j++)
{
if (results.items[j].food_code == log_code)
{
desc = results.items[j].description;
break;
}
}
if (log_add (log_db, log_code, desc, date,
log_servings) < 0)
exit_status = 1;
else
printf (_("Logged food %d (%s) x%.1f for %s.\n"),
log_code, desc, log_servings,
format_date (date));
food_list_free (&results);
}
else
{
exit_status = 1;
}
}
break;
case 4:
exit_status = cmd_budget (food_db, log_db, date, calories);
break;
case 5:
/* Delete a log entry. */
if (log_delete (log_db, delete_id) < 0)
exit_status = 1;
else
printf (_("Deleted log entry %d.\n"), delete_id);
break;
case 6:
/* Edit a log entry. At least -n or -d must be given. */
if (!edit_quantity_given && !edit_date_given)
{
fprintf (stderr,
_("%s: --edit requires --quantity and/or --date\n"),
PROGRAM_NAME);
exit_status = 1;
}
else
{
/* We need to fetch the current entry to fill in unchanged
fields. Use a simple query by iterating the date list
and entries. We look up the entry by ID. */
struct date_list dl;
int found = 0;
if (log_get_dates (log_db, &dl) == 0)
{
size_t di;
for (di = 0; di < dl.count && !found; di++)
{
struct log_list el;
if (log_get_day (log_db, dl.dates[di], &el) == 0)
{
size_t ei;
for (ei = 0; ei < el.count; ei++)
{
if (el.items[ei].id == edit_id)
{
double new_srv = edit_quantity_given
? log_servings : el.items[ei].servings;
const char *new_date = edit_date_given
? date : el.items[ei].date;
if (log_update (log_db, edit_id,
new_date, new_srv) < 0)
exit_status = 1;
else
printf (_("Updated log entry %d "
"(%.1f servings, %s).\n"),
edit_id, new_srv,
format_date (new_date));
found = 1;
break;
}
}
log_list_free (&el);
}
}
date_list_free (&dl);
}
if (!found && exit_status == 0)
{
fprintf (stderr,
_("%s: log entry %d not found\n"),
PROGRAM_NAME, edit_id);
exit_status = 1;
}
}
break;
default:
abort ();
}
log_close (log_db);
db_close (food_db);
free (log_path);
free (date_alloc);
return exit_status;
}
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>