mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-10 19:27:14 +01:00
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file-length / file length <= 250 lines (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint) (push) Waiting to run
Tests / test (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
212 lines
7.7 KiB
Python
212 lines
7.7 KiB
Python
"""Render a :class:`~diet_guard._body_view.BodyView` as aligned text blocks.
|
|
|
|
One renderer for both surfaces: the CLI prints the blocks, the gate's Body
|
|
tab shows each in a monospace panel. Numbers go through ``half_up`` so they
|
|
match the phone to the digit.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import date
|
|
import textwrap
|
|
from typing import TYPE_CHECKING
|
|
|
|
from diet_guard._activity_kcal import (
|
|
FORMULAS,
|
|
METHODS_NOTE,
|
|
session_kcal,
|
|
)
|
|
from diet_guard._body_calc import BMI_PRIME_NOTE, TREFETHEN_NOTE, half_up
|
|
from diet_guard._body_energy import KCAL_PER_KG, MIN_TARGET_KCAL, activity_label
|
|
from diet_guard._body_steps import STEP_BASELINE
|
|
from diet_guard._body_store import age_on
|
|
|
|
if TYPE_CHECKING:
|
|
from diet_guard._body_energy import Goal
|
|
from diet_guard._body_view import BodyView
|
|
from diet_guard._week_plan import WeekPlan
|
|
|
|
_NAME_W = 24
|
|
_COL_W = 15
|
|
_SESSIONS_SHOWN = 10
|
|
_WRAP = 88
|
|
_NEEDS = "Set birth date, height, sex and a weight to see the numbers."
|
|
|
|
|
|
def _wrap(text: str, indent: str = "") -> list[str]:
|
|
"""Explanatory prose broken at :data:`_WRAP` columns.
|
|
|
|
One unwrapped note once widened the gate's left panel enough to push the
|
|
whole right column off-screen.
|
|
"""
|
|
return textwrap.wrap(text, _WRAP, initial_indent=indent, subsequent_indent=indent)
|
|
|
|
|
|
def _num(value: float, digits: int = 0) -> str:
|
|
"""``value`` rounded half-up to ``digits`` decimals."""
|
|
return f"{half_up(value, digits):.{digits}f}"
|
|
|
|
|
|
def profile_lines(view: BodyView, today: date) -> list[str]:
|
|
"""Who the numbers are for, and what is still missing."""
|
|
prof = view.profile
|
|
birth = "birth date not set"
|
|
if prof.birth is not None:
|
|
age = age_on(date.fromisoformat(prof.birth), today)
|
|
birth = f"born {prof.birth} ({age} y)"
|
|
height = f"{_num(prof.height_cm)} cm" if prof.height_cm else "height not set"
|
|
sex = {"m": "male", "f": "female"}.get(prof.sex or "", "sex not set")
|
|
newest = max(view.weights) if view.weights else None
|
|
weight = (
|
|
f"{_num(view.weights[newest], 1)} kg on {newest} "
|
|
f"({len(view.weights)} weigh-ins)"
|
|
if newest
|
|
else "no weight logged yet"
|
|
)
|
|
return [f"Profile: {birth}, {height}, {sex}", f"Weight: {weight}"]
|
|
|
|
|
|
def bodyfat_lines(view: BodyView) -> list[str]:
|
|
"""The newest body-fat reading and what it switches on."""
|
|
if not view.body_fats:
|
|
return ["Body fat: not logged (log one to use Katch-McArdle)."]
|
|
day = max(view.body_fats)
|
|
return [
|
|
(
|
|
f"Body fat: {_num(view.body_fats[day], 1)}% on {day} "
|
|
f"({len(view.body_fats)} readings) -- targets use Katch-McArdle."
|
|
)
|
|
]
|
|
|
|
|
|
def bmi_lines(view: BodyView) -> list[str]:
|
|
"""BMI with plain-language readings, and the healthy weight range."""
|
|
if view.numbers is None:
|
|
return [_NEEDS]
|
|
bmi = view.numbers.bmi
|
|
low, high = view.numbers.healthy
|
|
return [
|
|
f"BMI {_num(bmi.bmi, 1)} ({bmi.category})",
|
|
(
|
|
f"Healthy weight for your height (BMI 18.5-24.9): "
|
|
f"{_num(low, 1)}-{_num(high, 1)} kg"
|
|
),
|
|
f"BMI Prime {_num(bmi.prime, 2)} Trefethen {_num(bmi.trefethen, 1)}",
|
|
*_wrap(BMI_PRIME_NOTE, " "),
|
|
*_wrap(TREFETHEN_NOTE, " "),
|
|
]
|
|
|
|
|
|
def ideal_lines(view: BodyView) -> list[str]:
|
|
"""Every ideal-weight formula, then their average and range."""
|
|
if view.numbers is None:
|
|
return [_NEEDS]
|
|
lines = ["Ideal weight:"]
|
|
for formula in view.numbers.ideal:
|
|
value = (
|
|
"n/a (< 152 cm)"
|
|
if formula.value is None
|
|
else f"{_num(formula.value, 1)} kg"
|
|
)
|
|
lines.append(f" {formula.name:<{_NAME_W}}{value}")
|
|
summary = view.numbers.ideal_summary
|
|
if summary is not None:
|
|
avg, low, high = summary
|
|
lines.append(f" {'Average of all':<{_NAME_W}}{_num(avg, 1)} kg")
|
|
lines.append(f" {'Ideal range':<{_NAME_W}}{_num(low, 1)}-{_num(high, 1)} kg")
|
|
return lines
|
|
|
|
|
|
def bmr_lines(view: BodyView) -> list[str]:
|
|
"""Resting metabolism by formula; body-fat ones wait for a reading."""
|
|
if view.numbers is None:
|
|
return [_NEEDS]
|
|
lines = ["Resting metabolism (kcal/day):"]
|
|
for f in view.numbers.bmr:
|
|
value = "n/a -- log a body-fat %" if f.value is None else _num(f.value)
|
|
lines.append(f" {f.name:<{_NAME_W + 8}}{value}")
|
|
return lines
|
|
|
|
|
|
def goal_sentence(goal: Goal) -> str:
|
|
"""``To lose 0.5 kg / week on Moderate`` -- the goal as words."""
|
|
activity = activity_label(goal.activity)
|
|
if goal.direction == "maintain":
|
|
return f"To maintain on {activity}"
|
|
amount = f"{goal.amount:g} {goal.weight_unit} / {goal.time_unit}"
|
|
return f"To {goal.direction} {amount} on {activity}"
|
|
|
|
|
|
def target_lines(view: BodyView) -> list[str]:
|
|
"""The top line: what to eat for the chosen goal, and how it was got."""
|
|
if view.numbers is None:
|
|
return [_NEEDS]
|
|
target = view.numbers.target
|
|
if target.kcal is None or target.tdee is None:
|
|
return [
|
|
(
|
|
f"{goal_sentence(view.goal)}: no workouts published yet "
|
|
"(the PC publishes them on sync)."
|
|
)
|
|
]
|
|
lines = [f"{goal_sentence(view.goal)} eat {target.kcal} kcal a day."]
|
|
if target.below_floor:
|
|
lines.append(f" That is below the {MIN_TARGET_KCAL} kcal floor.")
|
|
lines.append(
|
|
f" Maintenance {_num(target.tdee)} kcal ({target.bmr_name} resting burn "
|
|
f"x activity); 1 kg of fat = {KCAL_PER_KG:g} kcal."
|
|
)
|
|
lines.extend(_wrap("Informational only -- the budget is never changed here.", " "))
|
|
return lines
|
|
|
|
|
|
def activity_lines(view: BodyView) -> list[str]:
|
|
"""Measured exercise averages and the newest published workouts."""
|
|
if view.activity_stamp is None and not view.steps:
|
|
return ["No workouts published yet (the PC publishes them on sync)."]
|
|
stamp = (view.activity_stamp or "")[:16].replace("T", " ") or "not yet"
|
|
lines = [f"Workouts published {stamp}"]
|
|
exercise = view.exercise
|
|
if exercise is not None:
|
|
means = " ".join(
|
|
f"{name} {_num(kcal)}" for name, kcal in exercise.means.items()
|
|
)
|
|
lines.append(f"Last {exercise.days} full days, net exercise kcal/day: {means}")
|
|
if exercise is not None and view.steps:
|
|
newest = max(view.steps)
|
|
lines.append(
|
|
f"Steps outside workouts: {view.steps[newest]} on {newest}; above "
|
|
f"{STEP_BASELINE}/day they add {_num(exercise.steps_kcal)} kcal/day "
|
|
"(included above)."
|
|
)
|
|
weight = view.weight
|
|
for session in view.sessions[-_SESSIONS_SHOWN:][::-1]:
|
|
km = f" {_num(session.km, 1)} km" if session.km else ""
|
|
kcal = ""
|
|
if weight is not None:
|
|
result = session_kcal(session, weight)
|
|
kcal = " " + " ".join(
|
|
f"{name[0]}{_num(result.kcal[name])}" for name in FORMULAS
|
|
)
|
|
lines.append(
|
|
f" {session.day} {session.kind:<8}{_num(session.minutes):>4} min{km}{kcal}"
|
|
)
|
|
lines.extend(_wrap(f"(M = MET, A = ACSM, P = Per-km) {METHODS_NOTE}"))
|
|
return lines
|
|
|
|
|
|
def plan_lines(plan: WeekPlan) -> list[str]:
|
|
"""The week plan, one line per day."""
|
|
lines = [f"Week plan for an average of {plan.target_avg} kcal/day:"]
|
|
for day in plan.days:
|
|
note = {"unlogged": " (no log - type it?)"}.get(day.source, "")
|
|
lines.append(f" {day.weekday} {day.day} {day.kcal:>5} {day.source}{note}")
|
|
if plan.per_remaining is None:
|
|
lines.append(
|
|
f"Every day is known: the week averages {_num(plan.week_avg)} kcal."
|
|
)
|
|
else:
|
|
warn = f" -- below the {MIN_TARGET_KCAL} floor!" if plan.below_floor else ""
|
|
lines.append(f"Eat {plan.per_remaining} kcal on each remaining day{warn}")
|
|
return lines
|