| @@ -1,6 +1,117 @@ | |||
| #include <math.h> | |||
| #include <memory.h> | |||
| #include <time.h> | |||
| #include "./djstdlib/core.cpp" | |||
| #include "djstdlib/core.h" | |||
| enum CmdArgType { | |||
| CmdArgType_BOOL, | |||
| CmdArgType_STRING, | |||
| CmdArgType_INT, | |||
| CmdArgType_FLOAT, | |||
| // -- | |||
| CmdArgType_Count, | |||
| }; | |||
| string cmd_argTypeFmt(CmdArgType type) { | |||
| switch (type) { | |||
| case CmdArgType_FLOAT: | |||
| return "float"_s; | |||
| case CmdArgType_BOOL: | |||
| return "boolean flag"_s; | |||
| case CmdArgType_INT: | |||
| return "integer"_s; | |||
| case CmdArgType_STRING: | |||
| return "string"_s; | |||
| default: | |||
| return "invalid command argument type"_s; | |||
| } | |||
| } | |||
| struct CmdOptionArg { | |||
| /** | |||
| * The zero byte '\0' means no char name. | |||
| */ | |||
| char charName; | |||
| string name; | |||
| string description; | |||
| CmdArgType type; | |||
| }; | |||
| struct CmdPositionalArg { | |||
| string name; | |||
| string description; | |||
| CmdArgType type; | |||
| }; | |||
| struct CmdParsedOptionArg { | |||
| string name; | |||
| string content; | |||
| CmdArgType type; | |||
| }; | |||
| struct CmdParsedPositionalArg { | |||
| int index; | |||
| CmdArgType type; | |||
| void *content; | |||
| }; | |||
| struct ParsedCmd { | |||
| list<CmdParsedPositionalArg> posArgs; | |||
| list<CmdParsedOptionArg> optArgs; | |||
| }; | |||
| struct BasicCommand { | |||
| string name; | |||
| string description; | |||
| list<CmdPositionalArg> posArgs; | |||
| list<CmdOptionArg> optArgs; | |||
| /** | |||
| * @returns The status code of the command | |||
| */ | |||
| int32 (*command)(Arena *arena, list<string> args); | |||
| }; | |||
| void cmd_printSyntax(BasicCommand *cmd) { | |||
| print("%S", cmd->name); | |||
| for (EachIn(cmd->posArgs, j)) { | |||
| print(" [%S]", cmd->posArgs.data[j].name); | |||
| } | |||
| } | |||
| void cmd_printHelp(Arena *arena, list<BasicCommand> commands, string *helpCmd) { | |||
| if (helpCmd) { | |||
| for (EachIn(commands, i)) { | |||
| BasicCommand *icmd = &commands.data[i]; | |||
| if (strEql(*helpCmd, icmd->name)) { | |||
| print("Syntax: "); cmd_printSyntax(icmd); print("\n\n"); | |||
| print("%S\n", icmd->description); | |||
| print("\n"); | |||
| if (icmd->posArgs.length > 0) { | |||
| print("Arguments:\n"); | |||
| for (EachIn(icmd->posArgs, j)) { | |||
| CmdPositionalArg *posArg = &icmd->posArgs.data[j]; | |||
| print("%S (%S) - %S\n", posArg->name, cmd_argTypeFmt(posArg->type), posArg->description); | |||
| } | |||
| } | |||
| if (icmd->optArgs.length > 0) { | |||
| print("Options:\n"); | |||
| for (EachIn(icmd->optArgs, j)) { | |||
| CmdOptionArg *optArg = &icmd->optArgs.data[j]; | |||
| string charNameStr = optArg->charName != '\0' ? strPrintf(arena, "-%c, ", optArg->charName) : ""_s; | |||
| print("%S--%S (%S) - %S\n", charNameStr, optArg->name, cmd_argTypeFmt(optArg->type), optArg->description); | |||
| } | |||
| } | |||
| break; | |||
| } | |||
| } | |||
| } else { | |||
| print("Available Commands:\n"); | |||
| for (EachIn(commands, i)) { | |||
| print("- "); cmd_printSyntax(&commands.data[i]); print("\n"); | |||
| } | |||
| } | |||
| } | |||
| const string LOG_FILE_LOCATION = "./log.gtl"_s; | |||
| const string DB_FILE_LOCATION = "./db.gtd"_s; | |||
| @@ -70,7 +181,7 @@ list<GymLogEntry> loadEntryLog(Arena *arena, string fileLocation) { | |||
| string logfile = os_readEntireFile(arena, LOG_FILE_LOCATION); | |||
| if (logfile.length % sizeof(GymLogEntry) != 0) { | |||
| log("Log file corrupted.\n"); | |||
| print("Log file corrupted.\n"); | |||
| } else { | |||
| size_t entryCount = logfile.length / sizeof(GymLogEntry); | |||
| result = { (GymLogEntry *)logfile.str, entryCount, entryCount }; | |||
| @@ -101,7 +212,7 @@ int gymTrackerLogWorkToday(Arena *arena, Exercise exercise) { | |||
| string logfile = os_readEntireFile(arena, LOG_FILE_LOCATION); | |||
| if (logfile.length % sizeof(GymLogEntry) != 0) { | |||
| log("Log file corrupted.\n"); | |||
| print("Log file corrupted.\n"); | |||
| statusCode = 1; | |||
| } else { | |||
| size_t entryCount = logfile.length / sizeof(GymLogEntry); | |||
| @@ -123,7 +234,7 @@ int gymTrackerLogWorkToday(Arena *arena, Exercise exercise) { | |||
| if (todaysEntries.data) { | |||
| todaysEntries.length = todaysEntries.head; | |||
| WorkSummary summary = workSummaryForExercise(todaysEntries, exercise); | |||
| log("Total work today for %S:\n%.2fkg in ~%.2fmin.\n", exercise.name, summary.totalWork, (real32)summary.restTime / 60.0f); | |||
| print("Total work today for %S:\n%.2fkg in ~%.2fmin.\n", exercise.name, summary.totalWork, (real32)summary.restTime / 60.0f); | |||
| } | |||
| } | |||
| @@ -131,8 +242,8 @@ int gymTrackerLogWorkToday(Arena *arena, Exercise exercise) { | |||
| return statusCode; | |||
| } | |||
| int gymTrackerStatus(Arena *arena, list<string> args) { | |||
| int statusCode = 0; | |||
| int32 gymTrackerStatus(Arena *arena, list<string> args) { | |||
| int32 statusCode = 0; | |||
| string file = os_readEntireFile(arena, LOG_FILE_LOCATION); | |||
| if (file.length % sizeof(GymLogEntry) != 0) { | |||
| @@ -190,9 +301,9 @@ int gymTrackerStatus(Arena *arena, list<string> args) { | |||
| if (timestamp.tm_yday != lastDay || timestamp.tm_year != lastYear) { | |||
| if (dayCount > 0) { | |||
| log("\n"); | |||
| print("\n"); | |||
| } | |||
| log("================ %S ===================\n", formatTimeYmd(arena, ×tamp)); | |||
| print("================ %S ===================\n", formatTimeYmd(arena, ×tamp)); | |||
| lastDay = timestamp.tm_yday; | |||
| lastYear = timestamp.tm_year; | |||
| dayCount++; | |||
| @@ -227,9 +338,9 @@ int gymTrackerStatus(Arena *arena, list<string> args) { | |||
| nameToPrint = PushStringFill(arena, exerciseName->length, '.'); | |||
| } else { | |||
| nameToPrint = exerciseName->str ? *exerciseName : "unknown-exercise"_s; | |||
| log("\n"); | |||
| print("\n"); | |||
| } | |||
| log(format, | |||
| print(format, | |||
| formatTimeHms(arena, ×tamp), | |||
| nameToPrint, | |||
| entry->weightRepsInfo.weight, | |||
| @@ -241,8 +352,8 @@ int gymTrackerStatus(Arena *arena, list<string> args) { | |||
| } | |||
| if (i == logEntries.head + 1 || nextTimestamp.tm_yday != lastDay || nextTimestamp.tm_year != lastYear) { | |||
| log("\n"); | |||
| log("Work summary:\n"); | |||
| print("\n"); | |||
| print("Work summary:\n"); | |||
| for (size_t j = 0; j < workPerExerciseByDay.length; j++) { | |||
| if (workPerExerciseByDay.data[j] != 0.0f) { | |||
| const char *fmtString; | |||
| @@ -262,7 +373,7 @@ int gymTrackerStatus(Arena *arena, list<string> args) { | |||
| } | |||
| } | |||
| log(fmtString, nameByExercise.data[j], workToday, (real32)restPerExerciseByDay.data[j] / 60.0f, improvement, improvement / workLastTime * 100); | |||
| print(fmtString, nameByExercise.data[j], workToday, (real32)restPerExerciseByDay.data[j] / 60.0f, improvement, improvement / workLastTime * 100); | |||
| workPerExerciseByPrevDay.data[j] = workToday; | |||
| } | |||
| @@ -284,7 +395,7 @@ int gymTrackerDeleteEntries(Arena *arena, list<string> args) { | |||
| int statusCode = 0; | |||
| if (args.length == 0) { | |||
| log("Please pass the number of entries to delete starting from the most recent."); | |||
| print("Please pass the number of entries to delete starting from the most recent."); | |||
| statusCode = 1; | |||
| } else { | |||
| size_t position = 0; | |||
| @@ -292,13 +403,13 @@ int gymTrackerDeleteEntries(Arena *arena, list<string> args) { | |||
| if (numToDeleteParsed.valid) { | |||
| list<GymLogEntry> logEntries = loadEntryLog(arena, LOG_FILE_LOCATION); | |||
| if (numToDeleteParsed.result > logEntries.length) { | |||
| log("%i is more than the current number of log entries (%i). Aborting.", numToDeleteParsed, logEntries.length); | |||
| print("%i is more than the current number of log entries (%i). Aborting.", numToDeleteParsed, logEntries.length); | |||
| statusCode = 1; | |||
| } else { | |||
| os_writeEntireFile(arena, LOG_FILE_LOCATION, (byte *)logEntries.data, (logEntries.length - numToDeleteParsed.result) * sizeof(GymLogEntry)); | |||
| } | |||
| } else { | |||
| log("Invalid number to delete.\n"); | |||
| print("Invalid number to delete.\n"); | |||
| statusCode = 0; | |||
| } | |||
| } | |||
| @@ -307,12 +418,12 @@ int gymTrackerDeleteEntries(Arena *arena, list<string> args) { | |||
| } | |||
| // Syntax: do <exercise-name> weightKg reps | |||
| int gymTrackerDo(Arena *arena, list<string> args) { | |||
| int statusCode = 0; | |||
| int32 gymTrackerDo(Arena *arena, list<string> args) { | |||
| int32 statusCode = 0; | |||
| Exercise exercise = {}; | |||
| if (args.length < 3 || args.data[0].length == 0) { | |||
| log("Invalid exercise name and/or number of arguments.\n"); | |||
| print("Invalid exercise name and/or number of arguments.\n"); | |||
| statusCode = 1; | |||
| } else { | |||
| exercise.name = args.data[0]; | |||
| @@ -328,13 +439,13 @@ int gymTrackerDo(Arena *arena, list<string> args) { | |||
| existingEntry = &entry; | |||
| if (entry.name.length != exercise.name.length) { | |||
| exercise.name = entry.name; | |||
| log("Assuming exercise \"%S\".\n\n", entry.name); | |||
| print("Assuming exercise \"%S\".\n\n", entry.name); | |||
| } | |||
| break; | |||
| } | |||
| } | |||
| if (!existingEntry) { | |||
| log("The exercise \"%S\" hasn't been registered.", exercise.name); | |||
| print("The exercise \"%S\" hasn't been registered.", exercise.name); | |||
| statusCode = 1; | |||
| } | |||
| } | |||
| @@ -345,8 +456,8 @@ int gymTrackerDo(Arena *arena, list<string> args) { | |||
| ParsePositiveReal32Result kg = parsePositiveReal32(args.data[1], &parsedCount); | |||
| ParsePositiveIntResult reps = parsePositiveInt(args.data[2], &parsedCount); | |||
| if (!kg.valid || !reps.valid) { | |||
| log("%zu, %f, %\n", parsedCount, kg, reps); | |||
| log("Invalid reps or weight input.\n"); | |||
| print("%zu, %f, %\n", parsedCount, kg, reps); | |||
| print("Invalid reps or weight input.\n"); | |||
| statusCode = 1; | |||
| } else { | |||
| GymLogEntry entry = { | |||
| @@ -371,11 +482,11 @@ int gymTrackerListExercises(Arena *arena, list<string> args) { | |||
| GymLogDbParsed *db = parseDb(arena, os_readEntireFile(arena, DB_FILE_LOCATION)); | |||
| if (db->entries.length == 0) { | |||
| log("No entries currently registered in the exercise database."); | |||
| print("No entries currently registered in the exercise database."); | |||
| } else { | |||
| log("%i entries currently registered in the exercise database:\n\n", db->entries.length); | |||
| print("%i entries currently registered in the exercise database:\n\n", db->entries.length); | |||
| for (EachIn(db->entries, i)) { | |||
| log("#%i: %S\n", i + 1, db->entries.data[i].name); | |||
| print("#%i: %S\n", i + 1, db->entries.data[i].name); | |||
| } | |||
| } | |||
| @@ -387,7 +498,7 @@ int gymTrackerAddExercise(Arena *arena, list<string> args) { | |||
| string newExerciseName = args.data[0]; | |||
| if (newExerciseName.length == 0) { | |||
| log("No exercise name provided.\n"); | |||
| print("No exercise name provided.\n"); | |||
| statusCode = 1; | |||
| } | |||
| @@ -415,7 +526,7 @@ int gymTrackerAddExercise(Arena *arena, list<string> args) { | |||
| string entryName = {(char *)((byte *)database.str + head), currentEntry->nameLength }; | |||
| if (strEql(entryName, newExerciseName)) { | |||
| invalid = true; | |||
| log("Exercise \"%S\" already registered (entry #%i)\n", entryName, currentEntry->id); | |||
| print("Exercise \"%S\" already registered (entry #%i)\n", entryName, currentEntry->id); | |||
| break; | |||
| } | |||
| head += currentEntry->nameLength; | |||
| @@ -447,16 +558,147 @@ int gymTrackerAddExercise(Arena *arena, list<string> args) { | |||
| return statusCode; | |||
| } | |||
| int32 cmd_dispatch(Arena *arena, list<string> args, list<BasicCommand> cmds) { | |||
| int32 result = 0; | |||
| if (args.length < 1) { | |||
| print("At least one arg is required.\n"); | |||
| result = 1; | |||
| } else if (strEql(args.data[0], "--help"_s)) { | |||
| cmd_printHelp(arena, cmds, NULL); | |||
| } else if (args.length > 1 && strEql(args.data[1], "--help"_s)) { | |||
| cmd_printHelp(arena, cmds, &args.data[0]); | |||
| } else { | |||
| string userCmd = args.data[0]; | |||
| for (EachIn(cmds, i)) { | |||
| if (strEql(cmds.data[i].name, userCmd)) { | |||
| list<string> argsRest = listSlice(args, 1); | |||
| cmds.data[i].command(arena, argsRest); | |||
| } | |||
| } | |||
| } | |||
| return result; | |||
| } | |||
| int main(int argc, char **argv) { | |||
| initialiseCore(); | |||
| initialiseDjStdCore(); | |||
| Arena *arena = arenaAlloc(Megabytes(64)); | |||
| list<string> args = getArgs(arena, argc, argv); | |||
| int statusCode = 0; | |||
| if (args.length < 1) { | |||
| log("At least one arg is required.\n"); | |||
| statusCode = 1; | |||
| } | |||
| CmdOptionArg cmdStatusOptArgs[] = { | |||
| { | |||
| .charName = '\0', | |||
| .name = "all"_s, | |||
| .description = "Displays the full recorded history since day zero."_s, | |||
| .type = CmdArgType_BOOL | |||
| }, | |||
| { | |||
| .charName = 'd', | |||
| .name = "days"_s, | |||
| .description = "Displays the history for a previous number of days."_s, | |||
| .type = CmdArgType_INT, | |||
| } | |||
| }; | |||
| BasicCommand cmdStatus = { | |||
| .name = "status"_s, | |||
| .description = "Shows the currently recorded exercises. Default displays the current day."_s, | |||
| .posArgs = EmptyList(CmdPositionalArg), | |||
| .optArgs = ArrayAsList(CmdOptionArg, cmdStatusOptArgs), | |||
| .command = gymTrackerStatus, | |||
| }; | |||
| CmdPositionalArg cmdDoPosArgs[] = { | |||
| { | |||
| .name = "exercise"_s, | |||
| .type = CmdArgType_STRING, | |||
| }, | |||
| { | |||
| .name = "weight"_s, | |||
| .description = "Weight moved for one repetition"_s, | |||
| .type = CmdArgType_FLOAT, | |||
| }, | |||
| { | |||
| .name = "reps"_s, | |||
| .description = "Number of repetitions performed"_s, | |||
| .type = CmdArgType_INT, | |||
| } | |||
| }; | |||
| BasicCommand cmdDo = { | |||
| .name = "do"_s, | |||
| .description = "Records an exercise with weight and reps"_s, | |||
| .posArgs = ArrayAsList(CmdPositionalArg, cmdDoPosArgs), | |||
| .optArgs = EmptyList(CmdOptionArg), | |||
| .command = gymTrackerDo, | |||
| }; | |||
| CmdPositionalArg cmdDeletePosArgs[] = { | |||
| { | |||
| .name = "count"_s, | |||
| .description = "The number of entries to pop off the end of the record."_s, | |||
| .type = CmdArgType_INT, | |||
| } | |||
| }; | |||
| BasicCommand cmdDelete = { | |||
| .name = "delete"_s, | |||
| .description = "Deletes the last given number of entries."_s, | |||
| .posArgs = ArrayAsList(CmdPositionalArg, cmdDeletePosArgs), | |||
| .optArgs = EmptyList(CmdOptionArg), | |||
| .command = gymTrackerDeleteEntries, | |||
| }; | |||
| BasicCommand cmdList = { | |||
| .name = "list"_s, | |||
| .description = "Lists all available exercises in the database."_s, | |||
| .posArgs = EmptyList(CmdPositionalArg), | |||
| .optArgs = EmptyList(CmdOptionArg), | |||
| .command = gymTrackerListExercises, | |||
| }; | |||
| CmdPositionalArg cmdAddPosArgs[] = { | |||
| { | |||
| .name = "name"_s, | |||
| .description = "The name of the exercise to be added."_s, | |||
| .type = CmdArgType_STRING, | |||
| }, | |||
| }; | |||
| BasicCommand cmdAdd = { | |||
| .name = "add"_s, | |||
| .description = "Adds a new exercise name to the database."_s, | |||
| .posArgs = ArrayAsList(CmdPositionalArg, cmdAddPosArgs), | |||
| .optArgs = EmptyList(CmdOptionArg), | |||
| .command = gymTrackerAddExercise, | |||
| }; | |||
| BasicCommand commands[] = { | |||
| cmdStatus, | |||
| cmdDo, | |||
| cmdDelete, | |||
| cmdList, | |||
| cmdAdd, | |||
| }; | |||
| return cmd_dispatch(arena, args, ArrayAsList(BasicCommand, commands)); | |||
| /* | |||
| if (icmd.posArgs.length > 0) { | |||
| print("\tPositional arguments:\n"); | |||
| for (EachIn(icmd.posArgs, j)) { | |||
| CmdPositionalArg arg = icmd.posArgs.data[j]; | |||
| print("\t- %S: (%S) %S\n", | |||
| arg.name, | |||
| cmdArgTypeFmt(arg.type), | |||
| arg.description | |||
| ); | |||
| } | |||
| } | |||
| if (icmd.optArgs.length > 0) { | |||
| print("\tOptions:\n"); | |||
| } | |||
| */ | |||
| int statusCode = 0; | |||
| if (statusCode == 0) { | |||
| string cmd = args.data[0]; | |||
| @@ -473,7 +715,7 @@ int main(int argc, char **argv) { | |||
| } else if (strEql("add"_s, cmd)) { | |||
| statusCode = gymTrackerAddExercise(arena, argsRest); | |||
| } else { | |||
| log("Unknown command \"%S\"\n", args.data[0]); | |||
| print("Unknown command \"%S\"\n", args.data[0]); | |||
| statusCode = 1; | |||
| } | |||
| } | |||