You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

671 lines
24 KiB

  1. #include "time.h"
  2. #define DJSTD_BASIC_ENTRY
  3. #include "djstdlib/core.c"
  4. typedef enum {
  5. CmdArgType_BOOL,
  6. CmdArgType_STRING,
  7. CmdArgType_INT,
  8. CmdArgType_FLOAT,
  9. // --
  10. CmdArgType_Count,
  11. } CmdArgType;
  12. string cmd_argTypeFmt(CmdArgType type) {
  13. switch (type) {
  14. case CmdArgType_FLOAT:
  15. return s("float");
  16. case CmdArgType_BOOL:
  17. return s("boolean flag");
  18. case CmdArgType_INT:
  19. return s("integer");
  20. case CmdArgType_STRING:
  21. return s("string");
  22. default:
  23. return s("invalid command argument type");
  24. }
  25. }
  26. typedef struct {
  27. /**
  28. * The zero byte '\0' means no char name.
  29. */
  30. char charName;
  31. string name;
  32. string description;
  33. CmdArgType type;
  34. } CmdOptionArg;
  35. typedef struct {
  36. string name;
  37. string description;
  38. CmdArgType type;
  39. } CmdPositionalArg;
  40. typedef struct {
  41. string name;
  42. string content;
  43. CmdArgType type;
  44. } CmdParsedOptionArg;
  45. typedef struct {
  46. int index;
  47. CmdArgType type;
  48. void *content;
  49. } CmdParsedPositionalArg;
  50. DefineList(CmdParsedPositionalArg, CmdParsedPositionalArg);
  51. DefineList(CmdParsedOptionArg, CmdParsedOptionArg);
  52. typedef struct {
  53. CmdParsedPositionalArgList posArgs;
  54. CmdParsedOptionArgList optArgs;
  55. } ParsedCmd;
  56. DefineList(CmdPositionalArg, CmdPositionalArg);
  57. DefineList(CmdOptionArg, CmdOptionArg);
  58. typedef struct {
  59. string name;
  60. string description;
  61. CmdPositionalArgList posArgs;
  62. CmdOptionArgList optArgs;
  63. /**
  64. * @returns The status code of the command
  65. */
  66. int32 (*command)(Arena *arena, StringList args);
  67. } BasicCommand;
  68. void cmd_printSyntax(BasicCommand *cmd) {
  69. print("%S", cmd->name);
  70. for (EachIn(cmd->posArgs, j)) {
  71. print(" [%S]", cmd->posArgs.data[j].name);
  72. }
  73. }
  74. DefineList(BasicCommand, BasicCommand);
  75. void cmd_printHelp(Arena *arena, BasicCommandList commands, string *helpCmd) {
  76. if (helpCmd) {
  77. for (EachIn(commands, i)) {
  78. BasicCommand *icmd = &commands.data[i];
  79. if (strEql(*helpCmd, icmd->name)) {
  80. print("Syntax: "); cmd_printSyntax(icmd); print("\n\n");
  81. print("%S\n", icmd->description);
  82. print("\n");
  83. if (icmd->posArgs.length > 0) {
  84. print("Arguments:\n");
  85. for (EachIn(icmd->posArgs, j)) {
  86. CmdPositionalArg *posArg = &icmd->posArgs.data[j];
  87. print("%S (%S) - %S\n", posArg->name, cmd_argTypeFmt(posArg->type), posArg->description);
  88. }
  89. }
  90. if (icmd->optArgs.length > 0) {
  91. print("Options:\n");
  92. for (EachIn(icmd->optArgs, j)) {
  93. CmdOptionArg *optArg = &icmd->optArgs.data[j];
  94. string charNameStr = optArg->charName != '\0' ? strPrintf(arena, "-%c, ", optArg->charName) : s("");
  95. print("%S--%S (%S) - %S\n", charNameStr, optArg->name, cmd_argTypeFmt(optArg->type), optArg->description);
  96. }
  97. }
  98. break;
  99. }
  100. }
  101. } else {
  102. print("Available Commands:\n");
  103. for (EachIn(commands, i)) {
  104. print("- "); cmd_printSyntax(&commands.data[i]); print("\n");
  105. }
  106. }
  107. }
  108. const string LOG_FILE_LOCATION = s("./log.gtl");
  109. const string DB_FILE_LOCATION = s("./db.gtd");
  110. typedef struct {
  111. uint32 nextId;
  112. } GymLogDbHeader;
  113. typedef struct {
  114. uint32 id;
  115. uint32 nameLength;
  116. } GymLogDbEntry;
  117. typedef struct {
  118. uint32 id;
  119. string name;
  120. } GymLogDbParsedEntry;
  121. typedef GymLogDbParsedEntry Exercise;
  122. DefineList(GymLogDbParsedEntry, GymLogDbParsedEntry);
  123. typedef struct {
  124. GymLogDbHeader header;
  125. GymLogDbParsedEntryList entries;
  126. } GymLogDbParsed;
  127. typedef struct {
  128. uint8 reps;
  129. real32 weight;
  130. } WeightRepsInfo;
  131. typedef struct {
  132. uint64 timestamp;
  133. uint32 exerciseId;
  134. union {
  135. WeightRepsInfo weightRepsInfo;
  136. };
  137. } GymLogEntry;
  138. typedef struct {
  139. real32 totalWork;
  140. uint32 restTime;
  141. } WorkSummary;
  142. GymLogDbParsed *parseDb(Arena *arena, string database) {
  143. GymLogDbParsed *dbParsed = PushStruct(arena, GymLogDbParsed);
  144. dbParsed->header = *((GymLogDbHeader *)database.str);
  145. size_t head = sizeof(GymLogDbHeader);
  146. uint32 entriesLeft = dbParsed->header.nextId - 1;
  147. dbParsed->entries = PushList(arena, GymLogDbParsedEntryList, entriesLeft);
  148. while (entriesLeft > 0 && head < database.length) {
  149. GymLogDbEntry *currentEntry = (GymLogDbEntry *)((byte *)database.str + head);
  150. GymLogDbParsedEntry parsedEntry = { currentEntry->id, PushString(arena, currentEntry->nameLength) };
  151. head += sizeof(GymLogDbEntry);
  152. memcpy(parsedEntry.name.str, database.str + head, currentEntry->nameLength);
  153. AppendList(&dbParsed->entries, parsedEntry);
  154. head += currentEntry->nameLength;
  155. }
  156. return dbParsed;
  157. }
  158. DefineList(GymLogEntry, GymLogEntry);
  159. GymLogEntryList loadEntryLog(Arena *arena, string fileLocation) {
  160. GymLogEntryList result = {0};
  161. string logfile = os_readEntireFile(arena, LOG_FILE_LOCATION);
  162. if (logfile.length % sizeof(GymLogEntry) != 0) {
  163. print("Log file corrupted.\n");
  164. } else {
  165. size_t entryCount = logfile.length / sizeof(GymLogEntry);
  166. result = (GymLogEntryList){ (GymLogEntry *)logfile.str, entryCount, entryCount };
  167. }
  168. return result;
  169. }
  170. WorkSummary workSummaryForExercise(GymLogEntryList entries, Exercise exercise) {
  171. WorkSummary result = {0};
  172. UnixTimestamp lastTimestamp = 0;
  173. for (EachInReversed(entries, i)) {
  174. if (entries.data[i].exerciseId == exercise.id) {
  175. GymLogEntry logEntry = entries.data[i];
  176. result.totalWork += logEntry.weightRepsInfo.weight * logEntry.weightRepsInfo.reps;
  177. if (lastTimestamp > 0) {
  178. result.restTime += (uint32)(lastTimestamp - logEntry.timestamp);
  179. }
  180. lastTimestamp = logEntry.timestamp;
  181. }
  182. }
  183. return result;
  184. }
  185. int gymTrackerLogWorkToday(Arena *arena, Exercise exercise) {
  186. int statusCode = 0;
  187. string logfile = os_readEntireFile(arena, LOG_FILE_LOCATION);
  188. if (logfile.length % sizeof(GymLogEntry) != 0) {
  189. print("Log file corrupted.\n");
  190. statusCode = 1;
  191. } else {
  192. size_t entryCount = logfile.length / sizeof(GymLogEntry);
  193. GymLogEntryList logEntries = { (GymLogEntry *)logfile.str, entryCount, entryCount };
  194. UnixTimestamp todayUnix = getSystemUnixTime();
  195. Timestamp todayTs = timestampFromUnixTime(&todayUnix);
  196. GymLogEntryList todaysEntries = {0};
  197. todaysEntries.data = logEntries.data;
  198. for (EachInReversed(logEntries, i)) {
  199. GymLogEntry logEntry = logEntries.data[i];
  200. Timestamp logTs = timestampFromUnixTime(&logEntry.timestamp);
  201. if (logTs.tm_yday == todayTs.tm_yday && todayTs.tm_year == logTs.tm_year) {
  202. todaysEntries.length += 1;
  203. todaysEntries.data = &logEntries.data[i];
  204. }
  205. }
  206. if (todaysEntries.data) {
  207. todaysEntries.capacity = todaysEntries.length;
  208. WorkSummary summary = workSummaryForExercise(todaysEntries, exercise);
  209. print("Total work today for %S:\n%.2fkg in ~%.2fmin.\n", exercise.name, summary.totalWork, (real32)summary.restTime / 60.0f);
  210. }
  211. }
  212. return statusCode;
  213. }
  214. DefineList(real32, Real32);
  215. DefineList(uint32, Uint32);
  216. int32 gymTrackerStatus(Arena *arena, StringList args) {
  217. int32 statusCode = 0;
  218. string file = os_readEntireFile(arena, LOG_FILE_LOCATION);
  219. if (file.length % sizeof(GymLogEntry) != 0) {
  220. puts("Log file corrupted.");
  221. statusCode = 1;
  222. } else {
  223. GymLogDbParsed *db = parseDb(arena, os_readEntireFile(arena, DB_FILE_LOCATION));
  224. Timestamp startTs = {0};
  225. ParsePositiveIntResult numDays = {1, true};
  226. bool showAll = args.length == 1 && strEql(args.data[0], s("--all"));
  227. if (!showAll) {
  228. if (args.length == 2 && (strEql(args.data[0], s("--days")) || strEql(args.data[0], s("-d")))) {
  229. size_t l;
  230. numDays = parsePositiveInt(args.data[1], &l);
  231. }
  232. if (!numDays.valid) {
  233. puts("Bad argument for --days (-d) parameter.");
  234. statusCode = 1;
  235. } else {
  236. uint64 todayUnix = getSystemUnixTime();
  237. UnixTimestamp startUnix = todayUnix - numDays.result * 24 * 60 * 60;
  238. startTs = timestampFromUnixTime(&startUnix);
  239. }
  240. }
  241. if (statusCode == 0) {
  242. int lastDay = -1;
  243. int lastYear = -1;
  244. size_t entryCount = file.length / sizeof(GymLogEntry);
  245. GymLogEntryList logEntries = { (GymLogEntry *)file.str, entryCount, entryCount };
  246. StringList nameByExercise = PushFullListZero(arena, StringList, db->header.nextId);
  247. Real32List workPerExerciseByDay = PushFullListZero(arena, Real32List, db->header.nextId);
  248. Real32List workPerExerciseByPrevDay = PushFullListZero(arena, Real32List, db->header.nextId);
  249. Uint32List restPerExerciseByDay = PushFullListZero(arena, Uint32List, db->header.nextId);
  250. Uint32List lastTsPerExerciseByDay = PushFullListZero(arena, Uint32List, db->header.nextId);
  251. int dayCount = 0;
  252. Timestamp timestamp = {0};
  253. GymLogEntry *prevEntry = 0;
  254. GymLogEntry *entry = 0;
  255. for (EachIn(logEntries, i)) {
  256. prevEntry = entry;
  257. entry = &logEntries.data[i];
  258. timestamp = timestampFromUnixTime(&entry->timestamp);
  259. if (timestamp.tm_year < startTs.tm_year || timestamp.tm_yday < startTs.tm_yday) {
  260. continue;
  261. }
  262. if (timestamp.tm_yday != lastDay || timestamp.tm_year != lastYear) {
  263. if (dayCount > 0) {
  264. print("\n");
  265. }
  266. print("================ %S ===================\n", formatTimeYmd(arena, &timestamp));
  267. lastDay = timestamp.tm_yday;
  268. lastYear = timestamp.tm_year;
  269. dayCount++;
  270. }
  271. workPerExerciseByDay.data[entry->exerciseId] += entry->weightRepsInfo.reps * entry->weightRepsInfo.weight;
  272. uint32 lastTsForExercise = lastTsPerExerciseByDay.data[entry->exerciseId];
  273. if (lastTsForExercise > 0) {
  274. restPerExerciseByDay.data[entry->exerciseId] += (uint32)(entry->timestamp - lastTsForExercise);
  275. }
  276. lastTsPerExerciseByDay.data[entry->exerciseId] = (uint32)entry->timestamp;
  277. const char *format;
  278. if (entry->weightRepsInfo.weight == (int32)entry->weightRepsInfo.weight) {
  279. format = "%S: %S %.0fkg X %i\n";
  280. } else {
  281. format = "%S: %S %.2fkg X %i\n";
  282. }
  283. string *exerciseName = &(nameByExercise.data[entry->exerciseId]);
  284. if (exerciseName->str == 0) {
  285. for (EachIn(db->entries, j)) {
  286. GymLogDbParsedEntry dbEntry = db->entries.data[j];
  287. if (dbEntry.id == entry->exerciseId) {
  288. *exerciseName = dbEntry.name;
  289. }
  290. }
  291. }
  292. string nameToPrint = {0};
  293. if (prevEntry && entry->exerciseId == prevEntry->exerciseId) {
  294. nameToPrint = PushStringFill(arena, exerciseName->length, '.');
  295. } else {
  296. nameToPrint = exerciseName->str ? *exerciseName : s("unknown-exercise");
  297. print("\n");
  298. }
  299. print(format,
  300. formatTimeHms(arena, &timestamp),
  301. nameToPrint,
  302. entry->weightRepsInfo.weight,
  303. entry->weightRepsInfo.reps);
  304. Timestamp nextTimestamp = {0};
  305. if (i < logEntries.length - 1) {
  306. nextTimestamp = timestampFromUnixTime(&logEntries.data[i + 1].timestamp);
  307. }
  308. if (i == logEntries.length + 1 || nextTimestamp.tm_yday != lastDay || nextTimestamp.tm_year != lastYear) {
  309. print("\n");
  310. print("Work summary:\n");
  311. for (size_t j = 0; j < workPerExerciseByDay.length; j++) {
  312. if (workPerExerciseByDay.data[j] != 0.0f) {
  313. const char *fmtString;
  314. real32 improvement = 0;
  315. real32 workToday = workPerExerciseByDay.data[j];
  316. real32 workLastTime = workPerExerciseByPrevDay.data[j];
  317. if (workPerExerciseByPrevDay.data[j] == 0) {
  318. fmtString = COLOR_TEXT("%S", ANSI_fg_cyan) ": %.2fkg in %.2fmin\n";
  319. } else {
  320. improvement = workToday - workLastTime;
  321. if (improvement > 0) {
  322. fmtString = COLOR_TEXT("%S", ANSI_fg_cyan) ": %.2fkg in %.2fmin " COLOR_TEXT("+%.2fkg (+%.2f%%)\n", ANSI_fg_green);
  323. } else if (improvement < 0) {
  324. fmtString = COLOR_TEXT("%S", ANSI_fg_cyan) ": %.2fkg in %.2fmin " COLOR_TEXT("%.2fkg (%.2f%%)\n", ANSI_fg_red);
  325. } else {
  326. fmtString = COLOR_TEXT("%S", ANSI_fg_cyan) ": %.2fkg in %.2fmin " COLOR_TEXT("(no change)\n", ANSI_fg_yellow);
  327. }
  328. }
  329. print(fmtString, nameByExercise.data[j], workToday, (real32)restPerExerciseByDay.data[j] / 60.0f, improvement, improvement / workLastTime * 100);
  330. workPerExerciseByPrevDay.data[j] = workToday;
  331. }
  332. }
  333. ZeroListFull(&workPerExerciseByDay);
  334. ZeroListFull(&restPerExerciseByDay);
  335. ZeroListFull(&lastTsPerExerciseByDay);
  336. prevEntry = 0;
  337. entry = 0;
  338. }
  339. }
  340. }
  341. }
  342. return statusCode;
  343. }
  344. int gymTrackerDeleteEntries(Arena *arena, StringList args) {
  345. int statusCode = 0;
  346. if (args.length == 0) {
  347. print("Please pass the number of entries to delete starting from the most recent.");
  348. statusCode = 1;
  349. } else {
  350. size_t position = 0;
  351. ParsePositiveIntResult numToDeleteParsed = parsePositiveInt(args.data[0], &position);
  352. if (numToDeleteParsed.valid) {
  353. GymLogEntryList logEntries = loadEntryLog(arena, LOG_FILE_LOCATION);
  354. if (numToDeleteParsed.result > logEntries.length) {
  355. print("%i is more than the current number of log entries (%i). Aborting.", numToDeleteParsed, logEntries.length);
  356. statusCode = 1;
  357. } else {
  358. os_writeEntireFile(arena, LOG_FILE_LOCATION, (byte *)logEntries.data, (logEntries.length - numToDeleteParsed.result) * sizeof(GymLogEntry));
  359. }
  360. } else {
  361. print("Invalid number to delete.\n");
  362. statusCode = 0;
  363. }
  364. }
  365. return statusCode;
  366. }
  367. // Syntax: do <exercise-name> weightKg reps
  368. int32 gymTrackerDo(Arena *arena, StringList args) {
  369. int32 statusCode = 0;
  370. Exercise exercise = {};
  371. if (args.length < 3 || args.data[0].length == 0) {
  372. print("Invalid exercise name and/or number of arguments.\n");
  373. statusCode = 1;
  374. } else {
  375. exercise.name = args.data[0];
  376. }
  377. GymLogDbParsedEntry *existingEntry = 0;
  378. if (statusCode == 0) {
  379. GymLogDbParsed *db = parseDb(arena, os_readEntireFile(arena, DB_FILE_LOCATION));
  380. for (EachIn(db->entries, i)) {
  381. GymLogDbParsedEntry entry = db->entries.data[i];
  382. if (strStartsWith(entry.name, exercise.name)) {
  383. existingEntry = &entry;
  384. if (entry.name.length != exercise.name.length) {
  385. exercise.name = entry.name;
  386. print("Assuming exercise \"%S\".\n\n", entry.name);
  387. }
  388. break;
  389. }
  390. }
  391. if (!existingEntry) {
  392. print("The exercise \"%S\" hasn't been registered.", exercise.name);
  393. statusCode = 1;
  394. }
  395. }
  396. if (statusCode == 0) {
  397. exercise.id = existingEntry->id;
  398. size_t parsedCount = 0;
  399. ParsePositiveReal32Result kg = parsePositiveReal32(args.data[1], &parsedCount);
  400. ParsePositiveIntResult reps = parsePositiveInt(args.data[2], &parsedCount);
  401. if (!kg.valid || !reps.valid) {
  402. print("%zu, %f, %\n", parsedCount, kg, reps);
  403. print("Invalid reps or weight input.\n");
  404. statusCode = 1;
  405. } else {
  406. GymLogEntry entry = {
  407. .timestamp=getSystemUnixTime(),
  408. .exerciseId=exercise.id,
  409. .weightRepsInfo={
  410. .reps=reps.result,
  411. .weight=kg.result,
  412. },
  413. };
  414. os_fileAppend(arena, LOG_FILE_LOCATION, (byte *)&entry, sizeof(entry));
  415. statusCode = gymTrackerLogWorkToday(arena, exercise);
  416. }
  417. }
  418. return statusCode;
  419. }
  420. int gymTrackerListExercises(Arena *arena, StringList args) {
  421. int statusCode = 0;
  422. GymLogDbParsed *db = parseDb(arena, os_readEntireFile(arena, DB_FILE_LOCATION));
  423. if (db->entries.length == 0) {
  424. print("No entries currently registered in the exercise database.");
  425. } else {
  426. print("%i entries currently registered in the exercise database:\n\n", db->entries.length);
  427. for (EachIn(db->entries, i)) {
  428. print("#%i: %S\n", i + 1, db->entries.data[i].name);
  429. }
  430. }
  431. return statusCode;
  432. }
  433. int gymTrackerAddExercise(Arena *arena, StringList args) {
  434. int statusCode = 0;
  435. string newExerciseName = args.data[0];
  436. if (newExerciseName.length == 0) {
  437. print("No exercise name provided.\n");
  438. statusCode = 1;
  439. }
  440. if (statusCode != 1) {
  441. string database = os_readEntireFile(arena, DB_FILE_LOCATION);
  442. byte *buf = 0;
  443. size_t newEntryStartIndex = 0;
  444. if (database.length == 0) {
  445. // Initialise DB
  446. newEntryStartIndex = sizeof(GymLogDbHeader);
  447. buf = PushArray(arena, byte, sizeof(GymLogDbHeader) + sizeof(GymLogDbEntry) + newExerciseName.length);
  448. GymLogDbHeader *header = (GymLogDbHeader *)buf;
  449. header->nextId = 1;
  450. } else {
  451. // Validate entry not already present
  452. bool invalid = false;
  453. GymLogDbHeader *header = (GymLogDbHeader *)database.str;
  454. size_t head = sizeof(GymLogDbHeader);
  455. uint32 entriesLeft = header->nextId - 1;
  456. while (entriesLeft > 0 && head < database.length) {
  457. GymLogDbEntry *currentEntry = (GymLogDbEntry *)((byte *)database.str + head);
  458. head += sizeof(GymLogDbEntry);
  459. string entryName = {(char *)((byte *)database.str + head), currentEntry->nameLength };
  460. if (strEql(entryName, newExerciseName)) {
  461. invalid = true;
  462. print("Exercise \"%S\" already registered (entry #%i)\n", entryName, currentEntry->id);
  463. break;
  464. }
  465. head += currentEntry->nameLength;
  466. }
  467. if (!invalid) {
  468. newEntryStartIndex = database.length;
  469. buf = PushArray(arena, byte, database.length + sizeof(GymLogDbEntry) + newExerciseName.length);
  470. memcpy(buf, database.str, database.length);
  471. } else {
  472. statusCode = 1;
  473. }
  474. }
  475. if (statusCode != 1) {
  476. // Add entry
  477. GymLogDbHeader *header = (GymLogDbHeader *)buf;
  478. GymLogDbEntry *entry = (GymLogDbEntry *)(buf + newEntryStartIndex);
  479. entry->id = header->nextId;
  480. entry->nameLength = (uint32)newExerciseName.length;
  481. header->nextId++;
  482. byte *newExerciseNameDb = buf + newEntryStartIndex + sizeof(GymLogDbEntry);
  483. memcpy(newExerciseNameDb, newExerciseName.str, newExerciseName.length);
  484. size_t bufSize = newEntryStartIndex + sizeof(GymLogDbEntry) + newExerciseName.length;
  485. os_writeEntireFile(arena, DB_FILE_LOCATION, buf, bufSize);
  486. }
  487. }
  488. return statusCode;
  489. }
  490. int32 cmd_dispatch(Arena *arena, StringList args, BasicCommandList cmds) {
  491. int32 result = 0;
  492. if (args.length < 1) {
  493. print("At least one arg is required.\n");
  494. result = 1;
  495. } else if (strEql(args.data[0], s("--help"))) {
  496. cmd_printHelp(arena, cmds, NULL);
  497. } else if (args.length > 1 && strEql(args.data[1], s("--help"))) {
  498. cmd_printHelp(arena, cmds, &args.data[0]);
  499. } else {
  500. string userCmd = args.data[0];
  501. for (EachIn(cmds, i)) {
  502. if (strEql(cmds.data[i].name, userCmd)) {
  503. StringList argsRest = ListTail(args, 1);
  504. cmds.data[i].command(arena, argsRest);
  505. }
  506. }
  507. }
  508. return result;
  509. }
  510. int djstd_entry(Arena *arena, StringList args) {
  511. BasicCommandList cmds = AsList(BasicCommandList, {
  512. {
  513. .name = s("status"),
  514. .description = s("Shows the currently recorded exercises. Default displays the current day."),
  515. .posArgs = EmptyList(),
  516. .optArgs = AsList(CmdOptionArgList, {
  517. {
  518. .charName = '\0',
  519. .name = s("all"),
  520. .description = s("Displays the full recorded history since day zero."),
  521. .type = CmdArgType_BOOL
  522. },
  523. {
  524. .charName = 'd',
  525. .name = s("days"),
  526. .description = s("Displays the history for a previous number of days."),
  527. .type = CmdArgType_INT,
  528. }
  529. }),
  530. .command = gymTrackerStatus,
  531. },
  532. {
  533. .name = s("do"),
  534. .description = s("Records an exercise with weight and reps"),
  535. .posArgs = AsList(CmdPositionalArgList, {
  536. {
  537. .name = s("exercise"),
  538. .type = CmdArgType_STRING,
  539. },
  540. {
  541. .name = s("weight"),
  542. .description = s("Weight moved for one repetition"),
  543. .type = CmdArgType_FLOAT,
  544. },
  545. {
  546. .name = s("reps"),
  547. .description = s("Number of repetitions performed"),
  548. .type = CmdArgType_INT,
  549. }
  550. }),
  551. .optArgs = EmptyList(),
  552. .command = gymTrackerDo,
  553. },
  554. {
  555. .name = s("delete"),
  556. .description = s("Deletes the last given number of entries."),
  557. .posArgs = AsList(CmdPositionalArgList, {
  558. {
  559. .name = s("count"),
  560. .description = s("The number of entries to pop off the end of the record."),
  561. .type = CmdArgType_INT,
  562. }
  563. }),
  564. .optArgs = EmptyList(),
  565. .command = gymTrackerDeleteEntries,
  566. },
  567. {
  568. .name = s("list"),
  569. .description = s("Lists all available exercises in the database."),
  570. .posArgs = EmptyList(),
  571. .optArgs = EmptyList(),
  572. .command = gymTrackerListExercises,
  573. },
  574. {
  575. .name = s("add"),
  576. .description = s("Adds a new exercise name to the database."),
  577. .posArgs = AsList(CmdPositionalArgList, {
  578. {
  579. .name = s("name"),
  580. .description = s("The name of the exercise to be added."),
  581. .type = CmdArgType_STRING,
  582. },
  583. }),
  584. .optArgs = EmptyList(),
  585. .command = gymTrackerAddExercise,
  586. }
  587. });
  588. return cmd_dispatch(arena, args, cmds);
  589. }