1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
|
import os
from datetime import datetime
def clear_screen() -> None:
os.system("cls" if os.name == "nt" else "clear")
def print_header(title: str) -> None:
print(f"\n--- {title} ---\n")
def print_table(headers: list[str], rows: list[list[str]]) -> None:
if not rows:
print(" (empty)")
return
all_rows = [headers] + [[str(c) for c in row] for row in rows]
widths = [max(len(r[i]) for r in all_rows) for i in range(len(headers))]
fmt = " | ".join(f"{{:<{w}}}" for w in widths)
print(fmt.format(*headers))
print("-+-".join("-" * w for w in widths))
for row in all_rows[1:]:
print(fmt.format(*row))
def prompt_int(
prompt: str,
required: bool = True,
min_val: int | None = None,
max_val: int | None = None,
default: int | None = None,
) -> int | None:
while True:
val = input(prompt).strip()
if not val:
if default is not None:
return default
if not required:
return None
print("This field is required.")
continue
try:
n = int(val)
except ValueError:
print("Please enter a valid integer.")
continue
if min_val is not None and n < min_val:
print(f"Must be at least {min_val}.")
continue
if max_val is not None and n > max_val:
print(f"Must be at most {max_val}.")
continue
return n
def prompt_float(
prompt: str,
required: bool = True,
min_val: float | None = None,
max_val: float | None = None,
default: float | None = None,
) -> float | None:
while True:
val = input(prompt).strip()
if not val:
if default is not None:
return default
if not required:
return None
print("This field is required.")
continue
try:
n = float(val)
except ValueError:
print("Please enter a valid number.")
continue
if min_val is not None and n < min_val:
print(f"Must be at least {min_val}.")
continue
if max_val is not None and n > max_val:
print(f"Must be at most {max_val}.")
continue
return round(n, 1)
def prompt_str(
prompt: str, required: bool = True, default: str | None = None
) -> str | None:
while True:
val = input(prompt).strip()
if not val:
if default is not None:
return default
if not required:
return None
print("This field is required.")
continue
return val
def _fmt_kg(val: str) -> str:
"""Format a single weight value, dropping '.0' for whole numbers."""
n = float(val)
return str(int(n)) if n == int(n) else f"{n:.1f}"
def format_weight(weight_str: str, bw_relative: bool) -> str:
"""Format comma-separated weight values for display."""
values = weight_str.split(",")
if bw_relative:
parts = []
for v in values:
n = float(v)
label = _fmt_kg(v)
if n >= 0:
parts.append(f"BW+{label}")
else:
parts.append(f"BW{label}")
return "/".join(parts) + "kg"
return "/".join(_fmt_kg(v) for v in values) + "kg"
def prompt_sets_detail(
sets: int,
default_reps: int | None = None,
default_weight: float | None = None,
default_rest: int | None = None,
bw_relative: bool = False,
) -> tuple[str, str, str]:
"""Prompt for reps, weight (kg), and rest time on each set, return comma-separated strings."""
weight_label = "Weight relative to BW (kg)" if bw_relative else "Weight kg"
weight_min: float | None = None if bw_relative else 0.0
reps_list = []
weight_list = []
rest_list = []
for i in range(1, sets + 1):
print(f" Set {i}:")
if default_reps is not None:
r = prompt_int(
f" Reps [{default_reps}]: ", min_val=1, default=default_reps
)
else:
r = prompt_int(" Reps: ", min_val=1)
assert r is not None
reps_list.append(str(r))
if default_weight is not None:
w = prompt_float(
f" {weight_label} [{_fmt_kg(str(default_weight))}]: ",
min_val=weight_min,
default=default_weight,
)
else:
w = prompt_float(f" {weight_label}: ", min_val=weight_min)
assert w is not None
weight_list.append(f"{w:.1f}")
if default_rest is not None:
rest_val = input(f" Rest seconds [{default_rest}] (- = skip): ").strip()
if rest_val == "-":
rest_list.append("")
elif rest_val == "":
rest_list.append(str(default_rest))
else:
try:
n = int(rest_val)
if n < 0:
print(" Must be at least 0, using default.")
rest_list.append(str(default_rest))
else:
rest_list.append(str(n))
except ValueError:
print(" Invalid input, using default.")
rest_list.append(str(default_rest))
else:
rt = prompt_int(" Rest seconds (optional): ", required=False, min_val=0)
rest_list.append(str(rt) if rt is not None else "")
return ",".join(reps_list), ",".join(weight_list), ",".join(rest_list)
def format_rest_time(rest_str: str | None) -> str:
"""Format comma-separated rest values for display, showing '-' for skipped sets."""
if not rest_str:
return "-"
parts = []
for v in rest_str.split(","):
v = v.strip()
if v:
parts.append(f"{v}s")
else:
parts.append("-")
return "/".join(parts)
def prompt_datetime(prompt: str, default_now: bool = True) -> str:
while True:
default = datetime.now().strftime("%Y-%m-%d %H")
val = input(f"{prompt} [{default}]: ").strip()
if not val:
if default_now:
return default
else:
try:
datetime.strptime(val, "%Y-%m-%d %H")
return val
except ValueError:
print("Invalid format. Use YYYY-MM-DD HH.")
continue
def pause() -> None:
input("\nPress Enter to continue...")
def confirm(prompt: str) -> bool:
val = input(f"{prompt} [y/N]: ").strip().lower()
return val == "y"
|