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