From 65414c2d64834b831bf59a13a58295bc7da3547d Mon Sep 17 00:00:00 2001 From: Lexi Quinn Date: Tue, 4 Apr 2023 01:57:29 +1000 Subject: [PATCH] initial commit for new format --- formatspec.md | 187 +++++++++++++++++++++++ src2/client.c | 118 ++++++++++++++ src2/server.c | 415 ++++++++++++++++++++++++++++++++++++++++++++++++++ src2/tui.c | 204 +++++++++++++++++++++++++ 4 files changed, 924 insertions(+) create mode 100644 formatspec.md create mode 100644 src2/client.c create mode 100644 src2/server.c create mode 100644 src2/tui.c diff --git a/formatspec.md b/formatspec.md new file mode 100644 index 0000000..da98e18 --- /dev/null +++ b/formatspec.md @@ -0,0 +1,187 @@ +Introduction +============ +The quest file format is a declarative computer language. It is made up +of statements that when read by the software define how it should behave and +statements that are written by the software to declare what it recorded. +Each statement begins with a directive followed by options or arguments indented +by one tab on the following lines. +The order of the directives is important, they are read by the program +line by line and may inherit data from those placed above. +If a line does not begin with a recognizable directive, it is ignored. +Care should be taken when inserting comments as one program may recognize +directives that another does not, for this reason double backslashes (//) are +reserved for leaving comments + +List of directives +================== +The indented section of the directives reference show example values first, +followed by the range of possible values or brief explaination in brackets + +Option directives +----------------- +Any client is free to define their own directives to look for in this section. +These directives override a programs defaults, they should be placed at the +top of the file alongside MetaData directives + +Foreground-Color + 247, 248, 242 (0-255, 0-255, 0-255) + +Background-Color + 47, 53, 66 (0-255, 0-255, 0-255) + +Best-Color + 249, 255, 79 (0-255, 0-255, 0-255) + +Ahead-Gaining-Color + 24, 240, 31 (0-255, 0-255, 0-255) + +Ahead-Losing-Color + 79, 255, 85 (0-255, 0-255, 0-255) + +Behind-Gaining-Color + 255, 79, 79 (0-255, 0-255, 0-255) + +Behind-Losing-Color + 224, 34, 34 (0-255, 0-255, 0-255) + +Comparison + Personal-Best (Personal-Best/World-Record/Sum-of-Best) + +Show-Delta-Column + True (True/False) + +Show-Segment-Column + True (True/False) + +Show-Time-Column + True (True/False) + +Show-Comparison-Column + True (True/False) + +Toggle-Hotkeys + t (See key name section) + +Start-Key + r (See key name section) + +Stop-Key + f (See key name section) + +Pause-Key + y (See key name section) + +Split-Key + e (See key name section) + +Unsplit-Key + g (See key name section) + +Skip-Key + v (See key name section) + +MetaData directives +--------------- +These directives provide metadata to runs that follow them, +multiple of these can appear in a single file if multiple +games or categories are run after one another + +Title + Elden Ring (Title of the game being run) + +Category + Any% (Name of the category being run) + +Runner + SuperCoolGuy04 (The name you as the runner are known by) + +Segment and Route Directives +---------------------------- +Runs by themselves are simply a list of events that occured +(such as splitting or pausing) in the quest language, in order to give meaning +to this data, these more complicated directives are used to define segments +that are played between splits and routes made up of these segments. +Define all your possible segments first, followed by all routes. +If no segments are defined, a single unnamed segment is assumed. +If no routes are defined, a single unnamed route that passes through all +segments in the order of their definition is assumed. + +Segment + Shortname + Stage One (A name for the segment that should identify it + within the file and refer to the single + objective completed. + This name should not change.) + Longname + Murderize Chad (The display name for the segment if it should + differ from the proper name, this name can be + changed freely and may be used for + inside-jokes without issue.) + Description + Go to the golden palace and kill + the big dude with the ugly sunglasses + (Optional third argument for use as a more + detailed note or reminder of the objective + for the segment that may be displayed + alongside the name) + +Route + Name + Magic Swordless (Name for the route, keeping in mind routes + are not categories, you may simply run + multiple different routes because you're unsure + yet which strategies are the fastest) + Segments + Stage One (The ordered list of segments the route + Stage Two consists of, these names should match the + Stage Four segment Shortnames) + +Run Directives +-------------- +These directives are much more complicated and are not intended to be written +by a human but rather by the timer software, they will make up the majority +of a file as they are the run history which may be quite long. +These data passed by these directives exists agnostic of segments, route, games, +or categories, rather they are either explicitly matched with metadata that is +applicable, or by default is matched with the last set of metadata declared by +the time of the run directive + +Run + Route + Magic Swordless + Start + 2016-10-23 10:03:12.034Z + (The first event in a run must be Start, and it + must be accompanied by an RFC 3339 timestamp) + Split + 120052 (Following events may be accompanied by + RFC 3339 timestamps or simply a millisecond + offset from the previous event) + Skip + 323481 + Split + 3121111 + Pause + 421397 + Unpause + 2016-10-23 11:16:04.175Z + Stop (The last event in a run is always a Stop) + 123111 + +The 'Then' Directive +------------------ +The default behaviour the timer should exhibit when multiple games, catagories, +or routes exist in the same quest file is to allow the user to select +which one should be run, but the 'Then' directive instead allows for multiple +routes or catagories to be run one straight after the other as a single longer +"route". The timer should insert a dummy "segment" between the two routes for +the user to spend resetting the game and record the resulting run in two places: +A run directive should be recorded with every event including the dummy split +that can be exported and shared as one marathon attempt +AND +Individual run directives for each of the individual routes should be created +without the other included routes or the dummy split that can contribute to the +attempt history and stats of those individual catagories +If multiple routes exist on both or either side of a Then directive, the routes +to be stitched together should be selectable out of the options given, just as +they are normally. diff --git a/src2/client.c b/src2/client.c new file mode 100644 index 0000000..30ffbbd --- /dev/null +++ b/src2/client.c @@ -0,0 +1,118 @@ +#include +#include +#include + +#include +#include + +#include + +int main(int argc, char *argv[]) { + int sockfd, portno, n; + struct sockaddr_in serv_addr; + struct hostent *server; + + char buffer[256]; + char commandcode; + + if (argc < 2) { + fprintf(stderr,"usage %s command\n", argv[0]); + exit(0); + } + + portno = 8101; + + /* Create a socket point */ + sockfd = socket(AF_INET, SOCK_STREAM, 0); + + if (sockfd < 0) { + perror("ERROR opening socket"); + exit(1); + } + + server = gethostbyname("localhost"); + + if (server == NULL) { + fprintf(stderr,"ERROR, no such host\n"); + exit(0); + } + + bzero((char *) &serv_addr, sizeof(serv_addr)); + serv_addr.sin_family = AF_INET; + bcopy((char *)server->h_addr, (char *)&serv_addr.sin_addr.s_addr, server->h_length); + serv_addr.sin_port = htons(portno); + + /* Now connect to the server */ + if (connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) { + perror("ERROR connecting"); + exit(1); + } + + if (!strcmp(argv[1], "time")) { + commandcode = 1; + } else if (!strcmp(argv[1], "start")) { + commandcode = 2; + } else if (!strcmp(argv[1], "stop")) { + commandcode = 3; + } else if (!strcmp(argv[1], "kill")) { + commandcode = 4; + } else if (!strcmp(argv[1], "split")) { + commandcode = 5; + } else if (!strcmp(argv[1], "skip")) { + commandcode = 6; + } else if (!strcmp(argv[1], "pause")) { + commandcode = 7; + } else if (!strcmp(argv[1], "resume")) { + commandcode = 8; + } else if (!strcmp(argv[1], "undo")) { + commandcode = 9; + } else if (!strcmp(argv[1], "redo")) { + commandcode = 10; + } else if (!strcmp(argv[1], "foreground")) { + commandcode = 11; + } else if (!strcmp(argv[1], "background")) { + commandcode = 12; + } else { + perror("No valid command given"); + exit(1); + } + + /* Send message to the server */ + n = write(sockfd, &commandcode, 1); + + if (n < 0) { + perror("ERROR writing to socket"); + exit(1); + } + + /* Now read server response */ + //bzero(buffer,256); + + //read an int response + if (commandcode < 11) { + int x = -1; + n = read(sockfd, &x, sizeof(int)); + + if (n < 0) { + perror("ERROR reading from socket"); + exit(1); + } + + if (x != -1) + printf("%d\n",x); + } + //read a string response + else { + bzero(buffer,256); + n = read(sockfd, &buffer, 255); + + if (n < 0) { + perror("ERROR reading from socket"); + exit(1); + } + + if (buffer != NULL) + printf("%s", buffer); + } + return 0; +} diff --git a/src2/server.c b/src2/server.c new file mode 100644 index 0000000..eebaaf3 --- /dev/null +++ b/src2/server.c @@ -0,0 +1,415 @@ +#include +#include +#include + +#include +#include + +#include +#include +#include + +#define NS_PER_S 1000000000 + +struct timespec finish, delta; +int pausedTime = 0; +bool timerActive = false; +bool paused = false; +bool alive = true; +int timerOffset = 0; +enum event_type { + START, + SPLIT, + SKIP, + PAUSE, + RESUME, + STOP +}; +struct run_event { + enum event_type type; + struct timespec time; +}; + +struct run_event *run; +//Enough to hold a sm64 16 star, can realloc later +int runMaxLength = 12; +int runMarker = 0; +int runMarker2 = 0; + +//save file stuff +int files = 0; +char **filePaths = NULL; +char **names, **values; +int valuecount; + +void sub_timespec(struct timespec t1, struct timespec t2, struct timespec* td) +{ + td->tv_nsec = t2.tv_nsec - t1.tv_nsec; + td->tv_sec = t2.tv_sec - t1.tv_sec; + if (td->tv_sec > 0 && td->tv_nsec < 0) { + td->tv_nsec += NS_PER_S; + td->tv_sec--; + } else if (td->tv_sec < 0 && td->tv_nsec > 0) { + td->tv_nsec -= NS_PER_S; + td->tv_sec++; + } +} + +void offset_timespec(int milliseconds, struct timespec* t) +{ + //this should leave milliseconds with just the remainder + int second_offset = milliseconds / 1000; + milliseconds -= second_offset * 1000; + + int nanosecond_offset = milliseconds * 1000000; + + t->tv_nsec -= nanosecond_offset; + if (t->tv_nsec < 0) { + second_offset++; + t->tv_nsec += 1000000000; + } + t->tv_sec -= second_offset; +} + +int timespecToMS(struct timespec t) +{ + return (t.tv_nsec / 1000000) + (t.tv_sec * 1000); +} + +void extend_run() +{ + runMaxLength *= 2; + run = realloc(run, sizeof(struct run_event) * runMaxLength); +} + +void add_event(enum event_type t) +{ + if (runMarker == runMaxLength) + extend_run(); + run[runMarker].type = t; + clock_gettime(CLOCK_REALTIME, &run[runMarker].time); + if (t == START) + offset_timespec(timerOffset, &run[runMarker].time); + runMarker++; + runMarker2 = runMarker; +} + +void start() +{ + //TODO: Save the old run to the file before the new one starts, + //the reason to do this here is it gives the runner a chance to undo + //if they accidentally hit the stop button + //TODO: Clear the run data first + timerActive = true; + add_event(START); +} + +void stop() +{ + timerActive = false; + add_event(STOP); + //this makes sure the time clients recieve from time + //requests match the time on the stop event + finish = run[runMarker - 1].time; +} + +void split() +{ + add_event(SPLIT); +} + +void skip() +{ + add_event(SKIP); +} + +void addPauseTime() +{ + int pauseEvent = 0; + for (int i = runMarker - 2; i >= 1; i--) { + if (run[i].type == PAUSE) { + pauseEvent = i; + break; + } + } + sub_timespec(run[pauseEvent].time, run[runMarker - 1].time, &delta); + pausedTime += timespecToMS(delta); +} + +void subtractPauseTime() +{ + int pauseEvent = 0; + for (int i = runMarker - 1; i >= i; i--) { + if (run[i].type == PAUSE) { + pauseEvent = i; + break; + } + } + sub_timespec(run[pauseEvent].time, run[runMarker].time, &delta); + pausedTime -= timespecToMS(delta); +} + +void undo() +{ + if (runMarker > 0) { + runMarker--; + if (run[runMarker].type == STOP) + timerActive = true; + if (run[runMarker].type == START) + timerActive = false; + if (run[runMarker].type == PAUSE) + paused = false; + if (run[runMarker].type == RESUME) { + paused = true; + subtractPauseTime(); + } + } +} + +void redo() +{ + if (runMarker < runMarker2) { + runMarker++; + if (run[runMarker - 1].type == STOP) + timerActive = false; + if (run[runMarker - 1].type == START) + timerActive = true; + if (run[runMarker - 1].type == PAUSE) + paused = true; + if (run[runMarker - 1].type == RESUME) { + paused = false; + addPauseTime(); + } + } +} + +//this isnt just called pause() because that would overlap with +void pause_timer() +{ + if (!paused) { + add_event(PAUSE); + paused = true; + } +} + +void resume() +{ + if (paused) { + add_event(RESUME); + paused = false; + addPauseTime(); + } +} + +void loadFiles() +{ + FILE* fp; + //TODO: for now we're just looking for the metadata values + char buff[255]; + char buff2[255]; + + for (int i = 0; i < files; i++) { + fp = fopen(filePaths[i], "r"); + + while(1) { + char *x = fgets(buff, 255, fp); + if (buff[0] == '/' && buff[1] == '/' || buff[0] == '\n') + continue; + if (!strcmp(buff, "Segment") || !strcmp(buff, "Route") || x == NULL) + break; + fgets(buff2, 255, fp); + if (buff2[0] == '\t') { + valuecount++; + names = realloc(names, sizeof(char*) * valuecount); + names[valuecount - 1] = malloc(strlen(buff) - 1); + strncpy(names[valuecount - 1], buff, strlen(buff) - 1); + names[valuecount - 1][strlen(buff)] = '\0'; + values = realloc(values, sizeof(char*) * valuecount); + values[valuecount - 1] = malloc(strlen(buff2) - 2); + strncpy(values[valuecount - 1], buff2 + 1, strlen(buff2) - 1); + values[valuecount - 1][strlen(buff2)] = '\0'; + } + } + + fclose(fp); + } + + //Print metadata arrays + for (int i = 0; i < valuecount; i++) { + printf("%s | %s", names[i], values[i]); + } +} + +//TODO: eventually file loading should support loading multiple files +void addFile(char *path) +{ + files++; + filePaths = realloc(filePaths, sizeof(char*) * files); + filePaths[files - 1] = path; + loadFiles(); +} + +void sendTime(int sock) +{ + int n, x; + if (timerActive) + clock_gettime(CLOCK_REALTIME, &finish); + if (paused) { + sub_timespec(run[0].time, run[runMarker - 1].time, &delta); + } else { + sub_timespec(run[0].time, finish, &delta); + } + x = timespecToMS(delta) - pausedTime; + n = write(sock, &x, sizeof(int)); + + if (n < 0) { + perror("ERROR writing to socket"); + exit(1); + } +} + +void sendValue(int sock, char* name) +{ + int n, x; + bool namefound = false; + for(int i = 0; i < valuecount; i++) { + if (!strcmp(names[i], name)) { + x = i; + namefound = true; + } + } + if (namefound) + n = write(sock, values[x], strlen(values[x])); + else + n = write(sock, "DATA NOT PRESENT", 17); + + if (n < 0) { + perror("ERROR writing to socket"); + exit(1); + } +} + +void doprocessing (int sock) +{ + int n; + char commandcode; + n = read(sock,&commandcode,1); + + if (n < 0) { + perror("ERROR reading from socket"); + exit(1); + } + if (commandcode == 1) { + //printf("Recieved time command\n"); + sendTime(sock); + } else if (commandcode == 2) { + printf("Recieved start command\n"); + start(); + } else if (commandcode == 3) { + printf("Recieved stop command\n"); + stop(); + } else if (commandcode == 4) { + printf("Recieved kill command\n"); + alive = false; + } else if (commandcode == 5) { + printf("Recieved split command\n"); + split(); + } else if (commandcode == 6) { + printf("Recieved skip command\n"); + skip(); + } else if (commandcode == 7) { + printf("Recieved pause command\n"); + pause_timer(); + } else if (commandcode == 8) { + printf("Recieved resume command\n"); + resume(); + } else if (commandcode == 9) { + printf("Recieved undo command\n"); + undo(); + } else if (commandcode == 10) { + printf("Recieved redo command\n"); + redo(); + } else if (commandcode == 11) { + printf("Recieved request for foreground color\n"); + sendValue(sock, "Foreground-Color"); + } else if (commandcode == 12) { + printf("Recieved request for background color\n"); + sendValue(sock, "Background-Color"); + } else { + printf("Recieved invalid command code, ignoring...\n"); + } +} + +int main(int argc, char *argv[]) +{ + int sockfd, newsockfd, portno, clilen; + char buffer[256]; + struct sockaddr_in serv_addr, cli_addr; + int n, pid; + run = malloc(sizeof(struct run_event) * runMaxLength); + //TODO: remove this file testing boilerplate + addFile(argv[1]); + + /* First call to socket() function */ + sockfd = socket(AF_INET, SOCK_STREAM, 0); + + if (sockfd < 0) { + perror("ERROR opening socket"); + exit(1); + } + + /* Initialize socket structure */ + bzero((char *) &serv_addr, sizeof(serv_addr)); + portno = 8101; + + serv_addr.sin_family = AF_INET; + serv_addr.sin_addr.s_addr = INADDR_ANY; + serv_addr.sin_port = htons(portno); + + /* Now bind the host address using bind() call.*/ + if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) { + perror("ERROR on binding"); + exit(1); + } + + /* Now start listening for the clients, here + * process will go in sleep mode and will wait + * for the incoming connection + */ + + listen(sockfd,5); + clilen = sizeof(cli_addr); + + while (alive) { + newsockfd = accept(sockfd, (struct sockaddr *) &cli_addr, &clilen); + + if (newsockfd < 0) { + perror("ERROR on accept"); + exit(1); + } + + /* Create child process */ + //pid = fork(); + pid = 1; + + if (pid < 0) { + perror("ERROR on fork"); + exit(1); + } + + if (pid == 0) { + /* This is the child process */ + //close(sockfd); + //doprocessing(newsockfd); + //exit(0); + } + else { + doprocessing(newsockfd); + close(newsockfd); + } + + } /* end of while */ + free(run); + close(sockfd); +} + diff --git a/src2/tui.c b/src2/tui.c new file mode 100644 index 0000000..544a3ea --- /dev/null +++ b/src2/tui.c @@ -0,0 +1,204 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +//Big numbies +char bignumbers[4][126] = { +//char bignumbers[4][55] = { + "▄▀▀▄ ▄█ ▄▀▀▄ ▄▀▀▄ ▄ █ █▀▀▀ ▄▀▀ ▀▀▀█ ▄▀▀▄ ▄▀▀▄ ", + "█ █ █ ▄▀ ▄▀ █▄▄█ █▄▄ █▄▄ ▐▌ ▀▄▄▀ ▀▄▄█ ▀ ", + "█ █ █ ▄▀ ▄ █ █ █ █ █ █ █ █ █ ▀ ", + " ▀▀ ▀▀▀ ▀▀▀▀ ▀▀ ▀ ▀▀▀ ▀▀ ▀ ▀▀ ▀▀ ▀ " +}; +struct termios base; +int fps = 60; +struct color { + int r; + int g; + int b; +}; +struct color b = { 47, 53, 66}; //Background color +struct color f = {247, 248, 242}; //Text foreground color +struct color g = {249, 255, 79}; //Best ever segment time color +struct color ag = { 24, 240, 31}; //Ahead, and gaining time segment color +struct color al = { 79, 255, 85}; //Ahead, but losing time segment color +struct color bg = {255, 79, 79}; //Behind, but gaining time segment color +struct color bl = {224, 34, 34}; //Behind, and losing time segment color + +int w, h; + +int timestringDigits(int ms) +{ + int chars = 4; + if (ms >= 10000) + chars += 1; + if (ms >= 60000) + chars += 2; + if (ms >= 600000) + chars += 1; + if (ms >= 3600000) + chars += 2; + if (ms >= 36000000) + chars += 1; + if (ms >= 360000000) + chars += 1; + return chars; +} + +//This function needs an x and y coordinate because the resulting string +//is supposed to be printed across 4 rows so it cant really just return the +//resulting string +void printbigtimestring(int ms, int x, int y) +{ + //convert the single cell per character string length into the same + //thing for the big time string, it cant just be a simple multiplication + //because the : and . digits arent as wide as the numbers + + //Example string, printing a blank timer with all digits at x=65, y=40. + + //\033[40;65H▄▀▀▄ ▄▀▀▄ ▄▀▀▄ ▄▀▀▄ ▄▀▀▄ ▄▀▀▄ ▄▀▀▄ ▄▀▀▄ ▄▀▀▄ + //\033[41;65H█ █ █ █ █ █ ▀ █ █ █ █ ▀ █ █ █ █ █ █ █ █ + //\033[42;65H█ █ █ █ █ █ ▀ █ █ █ █ ▀ █ █ █ █ █ █ █ █ + //\033[43;65H ▀▀ ▀▀ ▀▀ ▀▀ ▀▀ ▀▀ ▀▀ ▀ ▀▀ ▀▀ + + char buffer[256]; + +} + +void timestring(char *str, int ms) +{ + int msdigits = (ms / 10) % 100; + int seconds = (ms / 1000) % 60; + int minutes = ((ms / 1000) / 60) % 60; + int hours = ((ms / 1000) / 60) / 60; + sprintf(str, "%03d:%02d:%02d.%02d", hours, minutes, seconds, msdigits); +} + +void resize(int i) +{ + struct winsize ws; + ioctl(1, TIOCGWINSZ, &ws); + w = ws.ws_col; + h = ws.ws_row; +} + +void initScreen() +{ + struct termios t; + tcgetattr(1, &base); + t = base; + t.c_lflag &= (~ECHO & ~ICANON); + tcsetattr(1, TCSANOW, &t); + //TODO:Figure out why i did this + dup(0); + fcntl(0, F_SETFL, O_NONBLOCK); + printf("\033[?1049h\n"); //Switch to TUI mode (alternate buffer) + printf("\033[?25l\n"); //Hide text cursor + printf("\033[2J\n"); //Clear screen +} + +void resetScreen() +{ + tcsetattr(1, TCSANOW, &base); + printf("\033[2J\n"); //Clear screen + printf("\033[?1049l\n"); //Switch back to regular mode + printf("\033[?25h\n"); //Show cursor +} + +void die(int i) +{ + exit(1); +} + +void processColorString(struct color *c, char* s) +{ + int i = 0; + int length = strlen(s); + char comp[4]; + int compcount = 0; + int colorcompsdone = 0; + //TODO: if we know the length now that we're not doing fgetc we dont + //need a while loop; convert to for loop + while (1) { + char x = s[i++]; + if (x >= 48 && x <= 57) { + comp[compcount] = x; + compcount++; + } + if (x == 44 || i == length) { + comp[compcount] = '\0'; + switch(colorcompsdone) { + case 0: + c->r = atoi(comp); + break; + case 1: + c->g = atoi(comp); + break; + case 2: + c->b = atoi(comp); + } + colorcompsdone++; + compcount = 0; + } + if (i == length) + break; + } +} + +int main (int argc, char *argv[]) +{ + initScreen(); + atexit(resetScreen); + signal(SIGTERM, die); + signal(SIGINT, die); + signal(SIGWINCH, resize); + resize(0); + + FILE *fp; + char path[1000]; + char ti[13]; + + //Request foreground color from config file + fp = popen("./quest-log foreground", "r"); + fgets(path, sizeof(path), fp); + if (strcmp(path, "DATA NOT PRESENT")) + processColorString(&f, path); + printf("\033[38;2;%d;%d;%dm", f.r, f.g, f.b); + pclose(fp); + + //Request background color from config file + fp = popen("./quest-log background", "r"); + fgets(path, sizeof(path), fp); + if (strcmp(path, "DATA NOT PRESENT")) + processColorString(&b, path); + printf("\033[48;2;%d;%d;%dm", b.r, b.g, b.b); + pclose(fp); + + //Set fps from command line argument + for (int i = 1; i < argc; i++) { + if (!strcmp(argv[i], "-fps")) + fps = atoi(argv[i + 1]); + } + + while (1) { + int time = 0; + fp = popen("./quest-log time", "r"); + if (fp == NULL) { + printf("Failed to run command\n"); + exit(1); + } + while (fgets(path, sizeof(path), fp) != NULL) { + time = atoi(path); + printf("\033[2J\n"); + timestring(&ti, time); + printf("%s\n", ti + (12 - timestringDigits(time))); + } + pclose(fp); + usleep(1000000 / fps); + } +}