// SPDX-License-Identifier: GPL-3.0-or-later
/*
* $Id: ui.c,v 1.1 2026/05/08 03:23:59 asm Exp $
*
* ui.c - ncurses user interface 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 "ui.h"
#include "budget.h"
#include "db.h"
#include "log.h"
#include "i18n.h"
#include <curses.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
/* Maximum length of the search input buffer. This is a UI display
limit, not a data limit. */
#define SEARCH_BUF_SIZE 256
/* Get today's date as YYYY-MM-DD. Returns a pointer to a static
buffer; not reentrant. */
static const char *
today_date (void)
{
static char buf[11];
time_t now;
struct tm *tm;
now = time (NULL);
tm = localtime (&now);
strftime (buf, sizeof buf, "%Y-%m-%d", tm);
return buf;
}
/* 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;
}
/* Parse a locale-formatted date string back into an ISO 8601 date
(YYYY-MM-DD). Returns 0 on success, -1 on failure. */
static int
parse_locale_date (const char *locale_date, char *iso_buf, int bufsz)
{
struct tm tm;
memset (&tm, 0, sizeof tm);
if (strptime (locale_date, "%x", &tm) == NULL)
return -1;
if (strftime (iso_buf, (size_t) bufsz, "%Y-%m-%d", &tm) == 0)
return -1;
return 0;
}
/* Draw the title bar. */
static void
draw_title (void)
{
attron (A_REVERSE);
mvhline (0, 0, ' ', COLS);
mvprintw (0, 1, _("GNUtrition %s"), PACKAGE_VERSION);
attroff (A_REVERSE);
}
/* Draw the status / help bar at the bottom. */
static void
draw_status (const char *msg)
{
attron (A_REVERSE);
mvhline (LINES - 1, 0, ' ', COLS);
mvprintw (LINES - 1, 1, "%s", msg);
attroff (A_REVERSE);
}
/* Draw the daily budget summary for DATE. Returns the number of
lines used. */
static int
draw_budget (sqlite3 *food_db, sqlite3 *log_db, int start_row,
int calories, const char *date)
{
struct daily_budget budget;
struct daily_budget consumed;
struct log_list entries;
size_t i;
int row;
budget = budget_for_calories (calories);
memset (&consumed, 0, sizeof consumed);
if (log_get_day (log_db, date, &entries) == 0)
{
for (i = 0; i < entries.count; i++)
{
struct fped_entry fped;
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);
}
row = start_row;
attron (A_BOLD);
mvprintw (row++, 2, _("Daily Budget (%s) - %d kcal USDA Pattern"),
format_date (date), budget.calories);
attroff (A_BOLD);
row++;
mvprintw (row++, 2, _("%-20s %10s %10s %10s"),
_("Food Group"), _("Budget"), _("Consumed"), _("Remaining"));
mvprintw (row++, 2, "%-20s %10s %10s %10s",
"--------------------", "----------",
"----------", "----------");
mvprintw (row++, 2, _("%-20s %7.1f c %7.1f c %7.1f c "),
_("Vegetables"), budget.vegetables,
consumed.vegetables, budget.vegetables - consumed.vegetables);
mvprintw (row++, 2, _("%-20s %7.1f c %7.1f c %7.1f c "),
_("Fruits"), budget.fruits,
consumed.fruits, budget.fruits - consumed.fruits);
mvprintw (row++, 2, _("%-20s %7.1f oz %7.1f oz %7.1f oz"),
_("Grains"), budget.grains,
consumed.grains, budget.grains - consumed.grains);
mvprintw (row++, 2, _("%-20s %7.1f c %7.1f c %7.1f c "),
_("Dairy"), budget.dairy,
consumed.dairy, budget.dairy - consumed.dairy);
mvprintw (row++, 2, _("%-20s %7.1f oz %7.1f oz %7.1f oz"),
_("Protein Foods"), budget.protein,
consumed.protein, budget.protein - consumed.protein);
mvprintw (row++, 2, _("%-20s %7.1f g %7.1f g %7.1f g "),
_("Oils"), budget.oils,
consumed.oils, budget.oils - consumed.oils);
return row - start_row;
}
/* Read a string from the ncurses screen at ROW, COL.
Edits BUF (of size BUFSZ) in-place. Returns the key that
ended input (Enter or Escape). */
static int
read_field (int row, int col, char *buf, int bufsz)
{
int len;
int ch;
len = (int) strlen (buf);
for (;;)
{
mvprintw (row, col, "%-20s", buf);
mvprintw (row, col + len, "_");
clrtoeol ();
refresh ();
ch = getch ();
if (ch == '\n' || ch == KEY_ENTER || ch == '\t')
return '\n';
if (ch == 27)
return 27;
if ((ch == KEY_BACKSPACE || ch == 127 || ch == 8) && len > 0)
buf[--len] = '\0';
else if (ch >= 32 && ch < 127 && len < bufsz - 1)
{
buf[len++] = (char) ch;
buf[len] = '\0';
}
}
}
/* Show food detail screen. Display nutrient information and FPED
data, and let the user log the food with a chosen quantity and
date. */
static void
food_detail_screen (sqlite3 *food_db, sqlite3 *log_db,
int food_code, const char *description)
{
struct nutrient_list nutrients;
struct fped_entry fped;
int has_fped;
char srv_buf[16];
char date_buf[16];
int scroll;
int ch;
int field; /* 0 = browsing nutrients, 1 = servings, 2 = date */
if (db_get_nutrients (food_db, food_code, &nutrients) < 0)
return;
has_fped = (db_get_fped (food_db, food_code, &fped) == 0);
snprintf (srv_buf, sizeof srv_buf, "1.0");
strncpy (date_buf, today_date (), sizeof date_buf - 1);
date_buf[sizeof date_buf - 1] = '\0';
scroll = 0;
field = 0;
for (;;)
{
size_t i;
int row;
int visible;
int total_lines;
int srv_row;
int date_row;
double servings;
{
char *endp;
errno = 0;
servings = strtod (srv_buf, &endp);
if (errno != 0 || endp == srv_buf || *endp != '\0'
|| servings <= 0.0)
servings = 1.0;
}
clear ();
draw_title ();
if (field == 0)
draw_status (_("Up/Down: scroll | Tab: edit fields | "
"l: log food | Esc: back"));
else
draw_status (_("Enter/Tab: next field | Esc: cancel edit"));
attron (A_BOLD);
mvprintw (2, 2, "%d - %-.*s", food_code, COLS - 14, description);
attroff (A_BOLD);
/* Count total display lines for scroll limit. */
total_lines = (int) nutrients.count + 1 + (has_fped ? 9 : 0);
/* Visible area for scrollable nutrient list. */
visible = LINES - 9;
if (visible < 1)
visible = 1;
if (scroll > total_lines - visible)
scroll = total_lines - visible;
if (scroll < 0)
scroll = 0;
row = 4;
{
int line_idx = 0;
/* Nutrient header. */
if (nutrients.count > 0 && line_idx >= scroll
&& row < LINES - 5)
{
mvprintw (row++, 2, _("%-40s %10s"),
_("Nutrient"), _("Value"));
}
line_idx++;
for (i = 0; i < nutrients.count; i++, line_idx++)
{
if (line_idx < scroll)
continue;
if (row >= LINES - 5)
break;
mvprintw (row++, 2, "%-40.40s %10.2f",
nutrients.items[i].name,
nutrients.items[i].value * servings);
}
/* FPED section. */
if (has_fped)
{
if (line_idx >= scroll && row < LINES - 5)
{
row++;
attron (A_BOLD);
if (servings != 1.0)
mvprintw (row++, 2,
_("Food Pattern Equivalents "
"(per 100g x %.1f):"), servings);
else
mvprintw (row++, 2,
_("Food Pattern Equivalents (per 100g):"));
attroff (A_BOLD);
}
line_idx += 2;
if (line_idx >= scroll && row < LINES - 5)
mvprintw (row++, 2,
_(" Vegetables: %.2f cup-eq"),
fped.vegetables * servings);
line_idx++;
if (line_idx >= scroll && row < LINES - 5)
mvprintw (row++, 2,
_(" Fruits: %.2f cup-eq"),
fped.fruits * servings);
line_idx++;
if (line_idx >= scroll && row < LINES - 5)
mvprintw (row++, 2,
_(" Grains: %.2f oz-eq"),
fped.grains * servings);
line_idx++;
if (line_idx >= scroll && row < LINES - 5)
mvprintw (row++, 2,
_(" Dairy: %.2f cup-eq"),
fped.dairy * servings);
line_idx++;
if (line_idx >= scroll && row < LINES - 5)
mvprintw (row++, 2,
_(" Protein: %.2f oz-eq"),
fped.protein * servings);
line_idx++;
if (line_idx >= scroll && row < LINES - 5)
mvprintw (row++, 2,
_(" Oils: %.2f g"),
fped.oils * servings);
line_idx++;
}
}
/* Bottom area: servings and date fields. */
srv_row = LINES - 4;
date_row = LINES - 3;
if (field == 1)
attron (A_REVERSE);
mvprintw (srv_row, 2, _("Servings: "));
if (field == 1)
attroff (A_REVERSE);
mvprintw (srv_row, 12, "%-20s", srv_buf);
if (field == 2)
attron (A_REVERSE);
mvprintw (date_row, 2, _("Date: "));
if (field == 2)
attroff (A_REVERSE);
mvprintw (date_row, 12, "%-20s", format_date (date_buf));
refresh ();
if (field == 1)
{
ch = read_field (srv_row, 12, srv_buf, (int) sizeof srv_buf);
if (ch == 27)
field = 0;
else
field = 2;
continue;
}
else if (field == 2)
{
char disp_buf[64];
strncpy (disp_buf, format_date (date_buf), sizeof disp_buf - 1);
disp_buf[sizeof disp_buf - 1] = '\0';
ch = read_field (date_row, 12, disp_buf, (int) sizeof disp_buf);
if (ch != 27)
{
if (parse_locale_date (disp_buf, date_buf,
(int) sizeof date_buf) < 0)
{
draw_status (_("Invalid date. Press any key..."));
refresh ();
getch ();
}
}
field = 0;
continue;
}
ch = getch ();
switch (ch)
{
case 27: /* Escape */
nutrient_list_free (&nutrients);
return;
case KEY_UP:
if (scroll > 0)
scroll--;
break;
case KEY_DOWN:
scroll++;
break;
case KEY_PPAGE:
scroll -= visible;
break;
case KEY_NPAGE:
scroll += visible;
break;
case '\t':
field = 1;
break;
case 'l':
case 'L':
{
char *endp;
double servings;
errno = 0;
servings = strtod (srv_buf, &endp);
if (errno != 0 || endp == srv_buf || *endp != '\0'
|| servings <= 0.0)
servings = 1.0;
log_add (log_db, food_code, description,
date_buf, servings);
draw_status (_("Logged! Press any key..."));
refresh ();
getch ();
}
break;
default:
break;
}
}
}
/* Show the food search screen. Let the user type a query, display
results, and view food details on selection. */
static void
search_screen (sqlite3 *food_db, sqlite3 *log_db)
{
char query[SEARCH_BUF_SIZE];
int qlen;
struct food_list results;
int selected;
int scroll;
int ch;
int running;
memset (query, 0, sizeof query);
qlen = 0;
results.items = NULL;
results.count = 0;
results.capacity = 0;
selected = 0;
scroll = 0;
running = 1;
while (running)
{
size_t i;
int row;
int visible;
clear ();
draw_title ();
draw_status (_("Type to search | Enter: view details | "
"Up/Down: select | Esc: back"));
mvprintw (2, 2, _("Search: %s_"), query);
row = 4;
if (results.count > 0)
{
visible = LINES - 6;
if (visible < 1)
visible = 1;
/* Keep selected item visible by adjusting scroll. */
if (selected < scroll)
scroll = selected;
if (selected >= scroll + visible)
scroll = selected - visible + 1;
for (i = (size_t) scroll;
i < results.count && row < LINES - 2; i++)
{
if ((int) i == selected)
attron (A_REVERSE);
mvprintw (row, 2, " %-8d %-.*s",
results.items[i].food_code,
COLS - 14,
results.items[i].description);
if ((int) i == selected)
attroff (A_REVERSE);
row++;
}
}
else if (qlen > 0)
{
mvprintw (row, 2, _("(no results)"));
}
refresh ();
ch = getch ();
switch (ch)
{
case 27: /* Escape */
running = 0;
break;
case KEY_UP:
if (selected > 0)
selected--;
break;
case KEY_DOWN:
if (selected < (int) results.count - 1)
selected++;
break;
case KEY_BACKSPACE:
case 127:
case 8:
if (qlen > 0)
{
query[--qlen] = '\0';
food_list_free (&results);
selected = 0;
scroll = 0;
if (qlen > 0)
db_search_foods (food_db, query, &results);
}
break;
case '\n':
case KEY_ENTER:
if (results.count > 0 && selected < (int) results.count)
{
food_detail_screen (food_db, log_db,
results.items[selected].food_code,
results.items[selected].description);
}
break;
default:
if (ch >= 32 && ch < 127
&& qlen < (int) sizeof query - 1)
{
query[qlen++] = (char) ch;
query[qlen] = '\0';
food_list_free (&results);
selected = 0;
scroll = 0;
db_search_foods (food_db, query, &results);
}
break;
}
}
food_list_free (&results);
}
/* Edit a log entry. Let the user adjust the servings and date for
the selected entry. Returns 1 if the entry was modified. */
static int
edit_log_entry (sqlite3 *log_db, struct log_entry *entry)
{
char srv_buf[16];
char date_buf[16];
int field; /* -1 = idle, 0 = editing servings, 1 = editing date */
int ch;
snprintf (srv_buf, sizeof srv_buf, "%.1f", entry->servings);
strncpy (date_buf, entry->date, sizeof date_buf - 1);
date_buf[sizeof date_buf - 1] = '\0';
field = -1;
for (;;)
{
int srv_row;
int date_row;
clear ();
draw_title ();
if (field < 0)
draw_status (_("Tab: edit fields | s: save changes | "
"Esc: cancel"));
else
draw_status (_("Enter/Tab: next field | Esc: cancel edit"));
attron (A_BOLD);
mvprintw (2, 2, _("Edit Log Entry: %s"), entry->description);
attroff (A_BOLD);
srv_row = 4;
date_row = 5;
if (field == 0)
attron (A_REVERSE);
mvprintw (srv_row, 2, _("Servings: "));
if (field == 0)
attroff (A_REVERSE);
mvprintw (srv_row, 12, "%-20s", srv_buf);
if (field == 1)
attron (A_REVERSE);
mvprintw (date_row, 2, _("Date: "));
if (field == 1)
attroff (A_REVERSE);
mvprintw (date_row, 12, "%-20s", format_date (date_buf));
refresh ();
if (field == 0)
{
ch = read_field (srv_row, 12, srv_buf, (int) sizeof srv_buf);
if (ch == 27)
field = -1;
else
field = 1;
continue;
}
else if (field == 1)
{
char disp_buf[64];
strncpy (disp_buf, format_date (date_buf), sizeof disp_buf - 1);
disp_buf[sizeof disp_buf - 1] = '\0';
ch = read_field (date_row, 12, disp_buf, (int) sizeof disp_buf);
if (ch != 27)
{
if (parse_locale_date (disp_buf, date_buf,
(int) sizeof date_buf) < 0)
{
draw_status (_("Invalid date. Press any key..."));
refresh ();
getch ();
}
}
field = -1;
continue;
}
ch = getch ();
switch (ch)
{
case 27:
return 0;
case '\t':
field = 0;
break;
case 's':
case 'S':
{
char *endp;
double servings;
errno = 0;
servings = strtod (srv_buf, &endp);
if (errno != 0 || endp == srv_buf || *endp != '\0'
|| servings <= 0.0)
{
draw_status (_("Invalid servings. Press any key..."));
refresh ();
getch ();
break;
}
if (log_update (log_db, entry->id, date_buf, servings) == 0)
{
draw_status (_("Updated! Press any key..."));
refresh ();
getch ();
return 1;
}
else
{
draw_status (_("Error updating! Press any key..."));
refresh ();
getch ();
}
}
break;
default:
break;
}
}
}
/* Show the food log with date navigation. */
static void
log_screen (sqlite3 *food_db, sqlite3 *log_db, int calories)
{
struct log_list entries;
struct date_list dates;
char date[16];
int date_idx;
int selected;
int ch;
size_t j;
strncpy (date, today_date (), sizeof date - 1);
date[sizeof date - 1] = '\0';
/* Load all dates that have entries. */
if (log_get_dates (log_db, &dates) < 0)
dates.count = 0;
/* Find today's position in the date list (or -1). */
date_idx = -1;
for (j = 0; j < dates.count; j++)
{
if (strcmp (dates.dates[j], date) == 0)
{
date_idx = (int) j;
break;
}
}
selected = 0;
while (1)
{
size_t i;
int row;
int budget_lines;
int log_start;
clear ();
draw_title ();
draw_status (_("Left/Right: change date | Up/Down: select | "
"d: delete | e: edit | Esc: back"));
/* Show budget for the currently displayed date. */
budget_lines = draw_budget (food_db, log_db, 2, calories, date);
log_start = 2 + budget_lines + 1;
attron (A_BOLD);
if (dates.count > 1)
{
mvprintw (log_start, 2, _("Food Log for %s (%d/%d)"),
format_date (date),
date_idx >= 0 ? date_idx + 1 : 0,
(int) dates.count);
}
else
{
mvprintw (log_start, 2, _("Food Log for %s"),
format_date (date));
}
attroff (A_BOLD);
row = log_start + 2;
if (log_get_day (log_db, date, &entries) == 0)
{
if (entries.count == 0)
{
mvprintw (row, 2, _("(no entries yet)"));
selected = 0;
}
else
{
if (selected >= (int) entries.count)
selected = (int) entries.count - 1;
if (selected < 0)
selected = 0;
mvprintw (row++, 2, _("%-5s %-8s %-6s %s"),
_("ID"), _("Code"), _("Srv"), _("Description"));
mvprintw (row++, 2, "%-5s %-8s %-6s %s",
"-----", "--------", "------",
"------------------------------------");
for (i = 0; i < entries.count; i++)
{
if ((int) i == selected)
attron (A_REVERSE);
mvprintw (row, 2, "%-5d %-8d %5.1f %-.*s",
entries.items[i].id,
entries.items[i].food_code,
entries.items[i].servings,
COLS - 26,
entries.items[i].description);
if ((int) i == selected)
attroff (A_REVERSE);
row++;
if (row >= LINES - 2)
break;
}
}
log_list_free (&entries);
}
refresh ();
ch = getch ();
switch (ch)
{
case 27: /* Escape */
date_list_free (&dates);
return;
case KEY_UP:
if (selected > 0)
selected--;
break;
case KEY_DOWN:
selected++;
break;
case KEY_LEFT:
if (dates.count > 0)
{
if (date_idx > 0)
date_idx--;
else if (date_idx < 0 && dates.count > 0)
date_idx = (int) dates.count - 1;
if (date_idx >= 0
&& date_idx < (int) dates.count)
{
strncpy (date, dates.dates[date_idx],
sizeof date - 1);
date[sizeof date - 1] = '\0';
}
selected = 0;
}
break;
case KEY_RIGHT:
if (dates.count > 0)
{
if (date_idx < 0)
date_idx = 0;
else if (date_idx < (int) dates.count - 1)
date_idx++;
if (date_idx >= 0
&& date_idx < (int) dates.count)
{
strncpy (date, dates.dates[date_idx],
sizeof date - 1);
date[sizeof date - 1] = '\0';
}
selected = 0;
}
break;
case 'd':
case 'D':
{
/* Delete the selected entry with confirmation. */
struct log_list del_entries;
if (log_get_day (log_db, date, &del_entries) == 0
&& del_entries.count > 0
&& selected < (int) del_entries.count)
{
draw_status (_("Delete this entry? (y/n)"));
refresh ();
ch = getch ();
if (ch == 'y' || ch == 'Y')
{
log_delete (log_db, del_entries.items[selected].id);
/* Reload dates. */
date_list_free (&dates);
if (log_get_dates (log_db, &dates) < 0)
dates.count = 0;
/* Re-find date index. */
date_idx = -1;
for (j = 0; j < dates.count; j++)
{
if (strcmp (dates.dates[j], date) == 0)
{
date_idx = (int) j;
break;
}
}
if (selected > 0)
selected--;
}
log_list_free (&del_entries);
}
}
break;
case 'e':
case 'E':
{
/* Edit the selected entry. */
struct log_list ed_entries;
if (log_get_day (log_db, date, &ed_entries) == 0
&& ed_entries.count > 0
&& selected < (int) ed_entries.count)
{
if (edit_log_entry (log_db, &ed_entries.items[selected]))
{
/* Reload dates since date might have changed. */
date_list_free (&dates);
if (log_get_dates (log_db, &dates) < 0)
dates.count = 0;
date_idx = -1;
for (j = 0; j < dates.count; j++)
{
if (strcmp (dates.dates[j], date) == 0)
{
date_idx = (int) j;
break;
}
}
}
log_list_free (&ed_entries);
}
}
break;
default:
break;
}
}
}
/* Gender names. */
static const char *gender_names[] =
{
N_("Neutral"),
N_("Female"),
N_("Male")
};
#define NUM_GENDERS (sizeof (gender_names) / sizeof (gender_names[0]))
/* Activity level names for display. */
static const char *activity_names[] =
{
N_("Sedentary"),
N_("Light"),
N_("Moderate"),
N_("Very active"),
N_("Extra active")
};
#define NUM_ACTIVITIES (sizeof (activity_names) / sizeof (activity_names[0]))
/* Profile setup screen. Returns the new calorie target, or the
original CALORIES if the user cancels. */
static int
profile_screen (sqlite3 *log_db, int calories)
{
struct user_profile prof;
char age_buf[16];
char height_buf[16];
char weight_buf[16];
int activity_sel;
int gender_sel;
int field; /* 0=age, 1=height, 2=weight, 3=activity, 4=gender */
int ch;
int rc;
memset (&prof, 0, sizeof prof);
memset (age_buf, 0, sizeof age_buf);
memset (height_buf, 0, sizeof height_buf);
memset (weight_buf, 0, sizeof weight_buf);
activity_sel = ACTIVITY_SEDENTARY;
gender_sel = GENDER_NEUTRAL;
/* Load existing profile if any. */
rc = log_get_profile (log_db, &prof);
if (rc == 0)
{
snprintf (age_buf, sizeof age_buf, "%d", prof.age_years);
snprintf (height_buf, sizeof height_buf, "%.1f", prof.height_cm);
snprintf (weight_buf, sizeof weight_buf, "%.1f", prof.weight_kg);
activity_sel = prof.activity_level;
gender_sel = prof.gender;
}
field = 0;
for (;;)
{
int row;
int field_rows[5];
clear ();
draw_title ();
draw_status (_("Tab/Enter: next field | Up/Down: activity | "
"Esc: cancel | s: save"));
row = 2;
attron (A_BOLD);
mvprintw (row++, 2, _("Profile Setup"));
attroff (A_BOLD);
row++;
mvprintw (row++, 2,
_("Enter your details to estimate a daily calorie target."));
mvprintw (row++, 2,
_("Uses the Mifflin-St Jeor equation (sex-neutral)."));
row++;
/* Age field. */
field_rows[0] = row;
if (field == 0)
attron (A_REVERSE);
mvprintw (row, 2, _("Age (years): "));
if (field == 0)
attroff (A_REVERSE);
mvprintw (row++, 20, "%-20s", age_buf);
/* Height field. */
field_rows[1] = row;
if (field == 1)
attron (A_REVERSE);
mvprintw (row, 2, _("Height (cm): "));
if (field == 1)
attroff (A_REVERSE);
mvprintw (row++, 20, "%-20s", height_buf);
/* Weight field. */
field_rows[2] = row;
if (field == 2)
attron (A_REVERSE);
mvprintw (row, 2, _("Weight (kg): "));
if (field == 2)
attroff (A_REVERSE);
mvprintw (row++, 20, "%-20s", weight_buf);
/* Activity field. */
field_rows[3] = row;
if (field == 3)
attron (A_REVERSE);
mvprintw (row, 2, _("Activity level: "));
if (field == 3)
attroff (A_REVERSE);
if (activity_sel >= 0 && activity_sel < (int) NUM_ACTIVITIES)
mvprintw (row++, 20, "%-20s", _(activity_names[activity_sel]));
else
mvprintw (row++, 20, "%-20s", _("Sedentary"));
/* Gender field. */
field_rows[4] = row;
if (field == 4)
attron (A_REVERSE);
mvprintw (row, 2, _("Gender: "));
if (field == 4)
attroff (A_REVERSE);
mvprintw (row++, 20, "%-20s", _(gender_names[gender_sel]));
row++;
if (prof.calorie_target > 0)
mvprintw (row++, 2, _("Current saved target: %d kcal/day"),
prof.calorie_target);
/* If all fields have values, show preview. */
if (age_buf[0] && height_buf[0] && weight_buf[0])
{
char *endp;
double h, w;
errno = 0;
h = strtod (height_buf, &endp);
if (errno != 0 || endp == height_buf || *endp != '\0')
h = 0.0;
errno = 0;
w = strtod (weight_buf, &endp);
if (errno != 0 || endp == weight_buf || *endp != '\0')
w = 0.0;
if (h > 0.0 && w > 0.0)
{
int est = budget_estimate_calories (atoi (age_buf),
h, w,
activity_sel, gender_sel);
mvprintw (row++, 2,
_("Estimated target: %d kcal/day"), est);
}
}
refresh ();
if (field < 3)
{
char *buf;
int bufsz;
if (field == 0)
{ buf = age_buf; bufsz = (int) sizeof age_buf; }
else if (field == 1)
{ buf = height_buf; bufsz = (int) sizeof height_buf; }
else
{ buf = weight_buf; bufsz = (int) sizeof weight_buf; }
ch = read_field (field_rows[field], 20, buf, bufsz);
if (ch == 27) return calories;
if (ch == '\t' || ch == KEY_DOWN || ch == '\n' || ch == KEY_ENTER)
field++;
else if (ch == KEY_UP && field > 0)
field--;
}
else
{
ch = getch ();
if (ch == 27) return calories;
if (ch == 's' || ch == 'S')
goto save_profile;
if (field == 3)
{
if (ch == KEY_UP) activity_sel = (activity_sel > 0) ? activity_sel - 1 : (int)NUM_ACTIVITIES - 1;
else if (ch == KEY_DOWN) activity_sel = (activity_sel < (int)NUM_ACTIVITIES - 1) ? activity_sel + 1 : 0;
else if (ch == '\n' || ch == KEY_ENTER || ch == '\t') field = 4;
}
else if (field == 4)
{
if (ch == KEY_UP) gender_sel = (gender_sel > 0) ? gender_sel - 1 : (int)NUM_GENDERS - 1;
else if (ch == KEY_DOWN) gender_sel = (gender_sel < (int)NUM_GENDERS - 1) ? gender_sel + 1 : 0;
else if (ch == '\n' || ch == KEY_ENTER || ch == '\t') field = 0;
}
}
continue;
save_profile:
if (!age_buf[0] || !height_buf[0] || !weight_buf[0])
{
draw_status (_("All fields required! Press any key..."));
refresh ();
getch ();
}
else
{
int est;
char *endp;
prof.age_years = atoi (age_buf);
errno = 0;
prof.height_cm = strtod (height_buf, &endp);
if (errno != 0 || endp == height_buf
|| *endp != '\0' || prof.height_cm <= 0.0)
{
draw_status (_("Invalid height! Press any key..."));
refresh ();
getch ();
continue;
}
errno = 0;
prof.weight_kg = strtod (weight_buf, &endp);
if (errno != 0 || endp == weight_buf
|| *endp != '\0' || prof.weight_kg <= 0.0)
{
draw_status (_("Invalid weight! Press any key..."));
refresh ();
getch ();
continue;
}
prof.activity_level = activity_sel;
prof.gender = gender_sel;
est = budget_estimate_calories (prof.age_years,
prof.height_cm,
prof.weight_kg,
prof.activity_level,
prof.gender);
prof.calorie_target = est;
if (log_save_profile (log_db, &prof) < 0)
{
draw_status (_("Error saving! Press any key..."));
refresh ();
getch ();
}
else
{
draw_status (_("Profile saved! Press any key..."));
refresh ();
getch ();
return est;
}
}
}
}
int
ui_run (sqlite3 *food_db, sqlite3 *log_db, int calories)
{
int ch;
int running;
initscr ();
cbreak ();
noecho ();
keypad (stdscr, TRUE);
curs_set (0);
running = 1;
while (running)
{
clear ();
draw_title ();
draw_status (_("s: Search foods | l: View log | "
"p: Profile | q: Quit"));
draw_budget (food_db, log_db, 2, calories, today_date ());
mvprintw (LINES - 3, 2,
_("[s] Search [l] Log [p] Profile [q] Quit"));
refresh ();
ch = getch ();
switch (ch)
{
case 's':
case 'S':
search_screen (food_db, log_db);
break;
case 'l':
case 'L':
log_screen (food_db, log_db, calories);
break;
case 'p':
case 'P':
calories = profile_screen (log_db, calories);
break;
case 'q':
case 'Q':
running = 0;
break;
default:
break;
}
}
endwin ();
return 0;
}
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>