diff --git a/app.cpp b/app.cpp index 31f3b45..5718447 100644 --- a/app.cpp +++ b/app.cpp @@ -1,6 +1,117 @@ +#include #include #include #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 posArgs; + list optArgs; +}; + +struct BasicCommand { + string name; + string description; + list posArgs; + list optArgs; + /** + * @returns The status code of the command + */ + int32 (*command)(Arena *arena, list 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 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 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 args) { - int statusCode = 0; +int32 gymTrackerStatus(Arena *arena, list 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 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 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 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 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 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 args) { if (numToDeleteParsed.valid) { list 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 args) { } // Syntax: do weightKg reps -int gymTrackerDo(Arena *arena, list args) { - int statusCode = 0; +int32 gymTrackerDo(Arena *arena, list 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 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 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 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 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 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,17 +558,148 @@ int gymTrackerAddExercise(Arena *arena, list args) { return statusCode; } -int main(int argc, char **argv) { - initialiseCore(); - Arena *arena = arenaAlloc(Megabytes(64)); - list args = getArgs(arena, argc, argv); - int statusCode = 0; +int32 cmd_dispatch(Arena *arena, list args, list cmds) { + int32 result = 0; if (args.length < 1) { - log("At least one arg is required.\n"); - statusCode = 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 argsRest = listSlice(args, 1); + cmds.data[i].command(arena, argsRest); + } + } } + return result; +} + +int main(int argc, char **argv) { + initialiseDjStdCore(); + Arena *arena = arenaAlloc(Megabytes(64)); + list args = getArgs(arena, argc, argv); + + 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]; list argsRest = listSlice(args, 1); @@ -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; } }