diff --git a/README.md b/README.md index 55bda66..6937967 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ pronounced *turmeric*, Termrec is a fast & tiny terminal session recorder writte - Extremely Fast & Small in Size (~52kb) - 0 External Libraries (Except Posix standard stuff) - UTF-8 Support -- Asciinema V1 & V2 File Format Support +- ONLY Asciinema V1 Format Support - Runs On Any Unix-Based System --- diff --git a/src/main.c b/src/main.c index 3da297d..51850a5 100644 --- a/src/main.c +++ b/src/main.c @@ -13,9 +13,8 @@ int masterfd; pid_t child; void PrintUsage(const char* name) { - printf("Usage: %s [command] [session-path] [options]\n\n", name); - printf("[command]\n - play: play a session\n - rec: record a session\n - help: shows help message\n\n"); - printf("[options]\n -f, --format: file-format of the session being played (default asciinema_v1)\n valid formats are: asciinema_v1, asciinema_v2\n"); + printf("Usage: %s [command] [session-path]\n\n", name); + printf("[command]\n - play: play a session\n - rec: record a session\n - help: shows help message\n"); } int main(int argc, char** argv) { @@ -31,11 +30,11 @@ int main(int argc, char** argv) { if (strcmp(argv[i], "rec") == 0) { if (i == argc - 1) { printf("No session name specified!\n"); exit(EXIT_FAILURE); } oa.mode = TERMREC_RECORD; - oa.fileName = argv[i + 1]; + oa.rec.filePath = argv[i + 1]; } else if (strcmp(argv[i], "play") == 0) { if (i == argc - 1) { printf("No session name specified!\n"); exit(EXIT_FAILURE); } oa.mode = TERMREC_PLAY; - oa.fileName = argv[i + 1]; + oa.rec.filePath = argv[i + 1]; } else if (strcmp(argv[i], "help") == 0) { PrintUsage(argv[0]); return 0; @@ -44,16 +43,6 @@ int main(int argc, char** argv) { exit(EXIT_FAILURE); } - for (i = 2; i < argc; i++) { - if (strcmp(argv[i], "-f") == 0 || strcmp(argv[i], "--format") == 0) { - if (i == argc - 1) { printf("No file-format name specified!\n"); exit(EXIT_FAILURE); } - const char* format = argv[i + 1]; - if (strcmp(format, "asciinema_v1") == 0) oa.format = ASCIINEMA_V1; - else if (strcmp(format, "asciinema_v2") == 0) oa.format = ASCIINEMA_V2; - else { printf("Invalid file-format specified!\n"); exit(EXIT_FAILURE); } - } - } - if (oa.mode == TERMREC_RECORD) { RecordSession(oa); } else if (oa.mode == TERMREC_PLAY) { diff --git a/src/record.h b/src/record.h index e085adc..33b3d75 100644 --- a/src/record.h +++ b/src/record.h @@ -1,6 +1,8 @@ #ifndef RECORD_H #define RECORD_H 1 +#include + enum control_command { CMD_NONE, CMD_CTRL_A, @@ -12,26 +14,20 @@ typedef enum { TERMREC_RECORD } AppMode; -typedef enum { - ASCIINEMA_V1 = 1, - ASCIINEMA_V2 = 2 -} FileFormat; +struct Recording { + uint32_t width; // cols + uint32_t height; // rows + uint64_t timestamp; + const char* env; + const char* filePath; +}; struct outargs { int start_paused; int controlfd; int masterfd; - - // Terminal Rows/Columns - int rows, cols; - AppMode mode; - FileFormat format; - - const char* cmd; - const char* env; - const char* title; - const char* fileName; + struct Recording rec; }; #endif diff --git a/src/recorder.c b/src/recorder.c index b83631c..8a50a8a 100644 --- a/src/recorder.c +++ b/src/recorder.c @@ -45,18 +45,17 @@ void RecordSession(struct outargs oa) { char *exec_cmd; char cwd[PATH_MAX]; - oa.env = SerializeEnv(); + oa.rec.env = SerializeEnv(); exec_cmd = NULL; - if (!oa.format) oa.format = ASCIINEMA_V1; - if (!oa.fileName) oa.fileName = "events.cast"; + if (!oa.rec.filePath) oa.rec.filePath = "events.cast"; if (pipe(controlfd) != 0) die("pipe"); oa.controlfd = controlfd[0]; TermGetWinSize(&rows, &cols); - oa.rows = rows; - oa.cols = cols; + oa.rec.width = cols; + oa.rec.height = rows; if (getcwd(cwd, sizeof(cwd)) != NULL) { printf("CWD: %s\n", cwd); @@ -71,11 +70,11 @@ void RecordSession(struct outargs oa) { if (ioctl(STDIN_FILENO, TIOCGWINSZ, &win) == -1) die("ioctl(TIOCGWINSZ)"); owin = rwin = win; - if (!oa.rows || oa.rows > win.ws_row) oa.rows = win.ws_row; - if (!oa.cols || oa.cols > win.ws_col) oa.cols = win.ws_col; + if (!oa.rec.width || oa.rec.width > win.ws_col) oa.rec.width = win.ws_col; + if (!oa.rec.height || oa.rec.height > win.ws_row) oa.rec.height = win.ws_row; - win.ws_row = oa.rows; - win.ws_col = oa.cols; + win.ws_col = oa.rec.width; + win.ws_row = oa.rec.height; TermEnableRawMode(); @@ -278,8 +277,7 @@ static void handle_command(enum control_command cmd) { } // This Function Is Responsible For Writing The Data To The File -static inline void handle_input(unsigned char *buf, size_t buflen, FileFormat format) { - assert(format >= ASCIINEMA_V1 && format <= ASCIINEMA_V2); +static inline void handle_input(FILE* writerFile, unsigned char *buf, size_t buflen) { static int first = 1; double delta; @@ -300,7 +298,7 @@ static inline void handle_input(unsigned char *buf, size_t buflen, FileFormat fo dur += delta; - WriteStdoutStart((format == ASCIINEMA_V1 ? delta : dur) / 1000); + Writer_OnBeforeStdoutData(writerFile, delta / 1000.0f); uint32_t state, cp; state = 0; @@ -313,29 +311,29 @@ static inline void handle_input(unsigned char *buf, size_t buflen, FileFormat fo uint32_t h, l; h = ((cp - 0x10000) >> 10) + 0xd800; l = ((cp - 0x10000) & 0x3ff) + 0xdc00; - WriteStdout_fprintf("\\u%04" PRIx32 "\\u%04" PRIx32, h, l); + Writer_OnStdoutData(writerFile, "\\u%04" PRIx32 "\\u%04" PRIx32, h, l); } else { - WriteStdout_fprintf("\\u%04" PRIx32, cp); + Writer_OnStdoutData(writerFile, "\\u%04" PRIx32, cp); } } else { - WriteStdout_fputs("\\ud83d\\udca9"); + Writer_OnStdoutData(writerFile, "\\ud83d\\udca9"); } } else { switch (buf[j]) { case '"': case '\\': - WriteStdout_fputc('\\'); // output backslash for escaping - WriteStdout_fputc(buf[j]); // print the character itself + Writer_OnStdoutData(writerFile, "\\"); // output backslash for escaping + Writer_OnStdoutData(writerFile, "%c", buf[j]); // print the character itself break; default: - WriteStdout_fputc(buf[j]); + Writer_OnStdoutData(writerFile, "%c", buf[j]); break; } } } } - WriteStdoutEnd(); + Writer_OnAfterStdoutData(writerFile); } /* @@ -350,13 +348,11 @@ void StartOutputProcess(struct outargs *oa) { status = EXIT_SUCCESS; master = oa->masterfd; - assert(oa->format >= ASCIINEMA_V1 && oa->format <= ASCIINEMA_V2); - start_paused = paused = oa->start_paused; setbuf(stdout, NULL); // Set The Stream To Be Un-Buffered - WriterInit(oa->fileName); - WriteHeader(oa); + FILE* writerFile = Writer_Init(oa->rec.filePath); + Writer_WriteHeader(writerFile, &oa->rec); if (close(STDIN_FILENO) == -1) { die("close"); @@ -428,15 +424,17 @@ void StartOutputProcess(struct outargs *oa) { } if (!paused) { - handle_input(obuf, nread, oa->format); + handle_input(writerFile, obuf, nread); } } } } end: - WriteDuration(dur / 1000); - WriterClose(); + Writer_OnStdoutAllEnd(writerFile); + Writer_WriteDuration(writerFile, dur / 1000.0f); + Writer_Close(writerFile); + if (close(oa->masterfd) == -1) { die("close"); } diff --git a/src/terminal.c b/src/terminal.c index bd8efe7..d333dca 100644 --- a/src/terminal.c +++ b/src/terminal.c @@ -2,12 +2,12 @@ #include "terminal.h" #include "main.h" -struct termios OriginalTermIOS; +static struct termios InitialAttribs; void TermEnableRawMode() { - if (tcgetattr(STDIN_FILENO, &OriginalTermIOS) == -1) die("tcgetattr"); + if (tcgetattr(STDIN_FILENO, &InitialAttribs) == -1) die("tcgetattr"); - struct termios raw = OriginalTermIOS; + struct termios raw = InitialAttribs; raw.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|INLCR|IGNCR|ICRNL|IXON); raw.c_oflag &= ~OPOST; @@ -21,7 +21,7 @@ void TermEnableRawMode() { } void TermDisableRawMode() { - if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &OriginalTermIOS) == -1) + if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &InitialAttribs) == -1) die("tcsetattr"); } diff --git a/src/writer.c b/src/writer.c index 71c92cf..d5aeaed 100644 --- a/src/writer.c +++ b/src/writer.c @@ -3,137 +3,81 @@ #include #include -FILE* outfile = NULL; -struct outargs OA; - -FILE* WriterInit(const char* fileName) { - memset(&OA, 0, sizeof(struct outargs)); - outfile = fopen(fileName, "wb"); - if (outfile == NULL) { +FILE* Writer_Init(const char* filePath) { + FILE* f = fopen(filePath, "wb"); + if (f == NULL) { die("fopen"); } - setbuf(outfile, NULL); // Set The Stream To Be Un-Buffered - return outfile; + setbuf(f, NULL); // Set The Stream To Be Un-Buffered + return f; } -/* - Function: WriteHeader() - Description: Inits The Data Writer Which Is A Wrapper To Write Files in Multiple Formats - Remarks: Returns Zero On Success & Negative One On Errors -*/ -int WriteHeader(struct outargs* oa) { - if (oa == NULL || outfile == NULL) return -1; - - memcpy(&OA, oa, sizeof(struct outargs)); - if (oa->format == ASCIINEMA_V1 || oa->format == ASCIINEMA_V2) { - /* Write asciicast header and append events. Format defined at - * v1 https://github.com/asciinema/asciinema/blob/master/doc/asciicast-v1.md - * v2 https://github.com/asciinema/asciinema/blob/master/doc/asciicast-v2.md - * - * With v1, we insert an empty first record to avoid the hassle of dealing with - * ES (still) not supporting trailing commas. - */ - - struct timeval tv; - gettimeofday(&tv, NULL); - - #define _CONDITION_TAB oa->format == ASCIINEMA_V1 ? '\t' : ' ' - #define _CONDITION_NEWL oa->format == ASCIINEMA_V1 ? '\n' : ' ' - - WriteStdout_fprintf("{ "); // Have Room For Putting Duration - WriteStdout_fprintf("%c\"version\": %d,%c", _CONDITION_TAB, oa->format, _CONDITION_NEWL); - WriteStdout_fprintf("%c\"timestamp\": %ld,%c", _CONDITION_TAB, tv.tv_sec, _CONDITION_NEWL); - WriteStdout_fprintf("%c\"width\": %d,%c", _CONDITION_TAB, oa->cols, _CONDITION_NEWL); - WriteStdout_fprintf("%c\"height\": %d,%c", _CONDITION_TAB, oa->rows, _CONDITION_NEWL); - WriteStdout_fprintf("%c\"command\": %s,%c", _CONDITION_TAB, oa->cmd ? oa->cmd : "\"\"", _CONDITION_NEWL); - WriteStdout_fprintf("%c\"title\": %s,%c", _CONDITION_TAB, oa->title ? oa->title : "\"\"", _CONDITION_NEWL); - WriteStdout_fprintf("%c\"env\": %s%c%c", _CONDITION_TAB, oa->env, oa->format == ASCIINEMA_V1 ? ',' : ' ', _CONDITION_NEWL); - - #undef _CONDITION_TAB - #undef _CONDITION_NEWL - - if (oa->format == ASCIINEMA_V1) - WriteStdout_fprintf("\t\"stdout\": [\n\t\t[ 0, \"\" ],\n"); // v1 header finished, console data is appended in structure - else if (oa->format == ASCIINEMA_V2) - WriteStdout_fprintf("}\n[ 0, \"o\", \"\" ]\n"); - } +int Writer_WriteHeader(FILE* file, const struct Recording* rec) { + if (file == NULL || rec == NULL) return -1; + + struct timeval tv; + gettimeofday(&tv, NULL); + + fprintf( + file, + "{\n" + "\t\"version\": 1,\n" + "\t\"timestamp\": %ld,\n" + "\t\"width\": %d,\n" + "\t\"height\": %d,\n" + "\t\"command\": \"\",\n" + "\t\"title\": \"\",\n" + "\t\"env\": %s,\n" + "\t\"stdout\": [\n\t\t[ 0, \"\" ],\n", // v1 header finished, console data is appended in structure + tv.tv_sec, + rec->width, + rec->height, + rec->env + ); return 0; } -int WriteDuration(float duration) { - if (outfile == NULL) return -1; +int Writer_WriteDuration(FILE* file, float duration) { + if (file == NULL) return -1; - if (OA.format == ASCIINEMA_V1 || OA.format == ASCIINEMA_V2) { - // seeks to header, overwriting spaces with duration - size_t currPos = ftell(outfile); - fseek(outfile, 2L, SEEK_SET); + // Go back 6 characters and replace the ',' at that + // position to a ' ', as the item before it would be + // the end of the JSON Array, so extraneous ',' would + // cause syntax error + size_t curr = ftell(file); + fseek(file, curr - 6, SEEK_SET); + fputc(' ', file); + fseek(file, curr, SEEK_SET); - #define _CONDITION_NEWL OA.format == ASCIINEMA_V1 ? '\n' : ' ' - fprintf(outfile, "%c\t\"duration\": %.9g,%c", _CONDITION_NEWL, duration, _CONDITION_NEWL); - #undef _CONDITION_NEWL - - fflush(outfile); - fseek(outfile, currPos, SEEK_SET); - } + fprintf(file, "\t\"duration\": %.9g\n", duration); return 0; } -void WriterClose() { - if (OA.format == ASCIINEMA_V1) { - // Delete The Comma From Last Entry in "stdout" array - size_t currPos = ftell(outfile); - fseek(outfile, currPos - 2, SEEK_SET); - fputc(' ', outfile); - fseek(outfile, currPos, SEEK_SET); - - WriteStdout_fprintf("\t]\n}\n"); // closes stdout segment - } - - if (fclose(outfile) == -1) { +void Writer_Close(FILE* file) { + fputs("}\n", file); + if (fclose(file) == -1) { die("fclose"); } - outfile = NULL; -} - -void WriteStdoutStart(float duration) { - if (outfile) { - if (OA.format == ASCIINEMA_V1) { - WriteStdout_fprintf("\t\t[ %0.4f, \"", duration); - } else if (OA.format == ASCIINEMA_V2) { - WriteStdout_fprintf("[ %0.4f, \"o\", \"", duration); - } - } } -void WriteStdout_fprintf(const char* fmt, ...) { - if (outfile) { - va_list args; - va_start(args, fmt); - vfprintf(outfile, fmt, args); - va_end(args); - } +void Writer_OnBeforeStdoutData(FILE* file, float duration) { + fprintf(file, "\t\t[ %0.4f, \"", duration); } -void WriteStdout_fputs(const char* str) { - if (outfile) { - fputs(str, outfile); - } +void Writer_OnStdoutData(FILE* file, const char* fmt, ...) { + va_list args; + va_start(args, fmt); + vfprintf(file, fmt, args); + va_end(args); } -void WriteStdout_fputc(char character) { - if (outfile) { - fputc(character, outfile); - } +void Writer_OnAfterStdoutData(FILE* file) { + fputs("\" ],\n", file); } -void WriteStdoutEnd() { - if (outfile) { - if (OA.format == ASCIINEMA_V1) { - WriteStdout_fputs("\" ],\n"); - } else if (OA.format == ASCIINEMA_V2) { - WriteStdout_fputs("\" ]\n"); - } - } +void Writer_OnStdoutAllEnd(FILE* file) { + // closes stdout segment + fprintf(file, "\t],\n"); } diff --git a/src/writer.h b/src/writer.h index 4a40299..e041b57 100644 --- a/src/writer.h +++ b/src/writer.h @@ -6,31 +6,30 @@ #include #include "record.h" -// Initializes Writer, And Returns The FILE* in which all the data is written -FILE* WriterInit(const char* fileName); +/* This is the Asciinema v1 Writer, It's Format defined at + * v1 https://github.com/asciinema/asciinema/blob/master/doc/asciicast-v1.md */ -// Write The Header Like Columns & Rows, Title Etc... -int WriteHeader(struct outargs* oa); +// Initializes Writer +FILE* Writer_Init(const char* filePath); -// Write The Duration Of The Recording -int WriteDuration(float duration); +int Writer_WriteHeader(FILE* file, const struct Recording* rec); -// Call To Close The Writer -void WriterClose(); +// Call before any stdout data arrives +void Writer_OnBeforeStdoutData(FILE* file, float duration); -/* - * Wrappers Around fprintf, fputs, fputc to write data depending on the file format - */ +// Call for any stdout data that arrives +void Writer_OnStdoutData(FILE* file, const char* fmt, ...); -// Call Before Writing A New STDOUT Data -void WriteStdoutStart(float duration); +// Call after there is no more stdout data +void Writer_OnAfterStdoutData(FILE* file); -// Call To Write STDOUT Data -void WriteStdout_fprintf(const char* fmt, ...); -void WriteStdout_fputs(const char* str); -void WriteStdout_fputc(char character); +// Call after ALL of the stdout data has been finally written +void Writer_OnStdoutAllEnd(FILE* file); -// Call After All The STDOUT Data is Written -void WriteStdoutEnd(); +// Call after `Writer_OnStdoutAllEnd`, Writes The Duration Of The Recording +int Writer_WriteDuration(FILE* file, float duration); + +// Call after `WriteDuration`, Finalizes the Recording +void Writer_Close(FILE* file); #endif