├── screenshot.gif ├── .gitignore ├── Makefile ├── README.md └── main.c /screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattn/ttyrec2gif/HEAD/screenshot.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vterm.h 2 | vterm_keycodes.h 3 | *.exe 4 | *.o 5 | animated.gif 6 | ttyrecord 7 | libvterm.a 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SRCS = \ 2 | main.c 3 | 4 | OBJS = $(subst .c,.o,$(SRCS)) 5 | 6 | CFLAGS = -I. -Ilibvterm/include -Wall 7 | LIBS = -L. -Llibvterm/.libs -lgd -lvterm 8 | TARGET = ttyrec2gif 9 | ifeq ($(OS),Windows_NT) 10 | TARGET := $(TARGET).exe 11 | endif 12 | 13 | .SUFFIXES: .c .o 14 | 15 | all : $(TARGET) 16 | 17 | $(TARGET) : $(OBJS) 18 | gcc -o $@ $(OBJS) $(LIBS) 19 | 20 | .c.o : 21 | gcc -c $(CFLAGS) -I. $< -o $@ 22 | 23 | clean : 24 | rm -f *.o $(TARGET) 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ttyrec2gif 2 | 3 | ![ttyrec2gif](https://raw.githubusercontent.com/mattn/ttyrec2gif/master/screenshot.gif) 4 | 5 | create animated gif from ttyrecord. 6 | 7 | ## Usage 8 | 9 | ```console 10 | $ ttyrec2gif ttyrecord 11 | ``` 12 | 13 | ## Requirements 14 | 15 | * libgd 16 | * libvterm 17 | 18 | ## Installation 19 | 20 | ```console 21 | $ make 22 | ``` 23 | 24 | ## License 25 | 26 | MIT 27 | 28 | This is based on ttyplay. 29 | 30 | ## Author 31 | 32 | Yasuhiro Matsumoto (a.k.a. mattn) 33 | -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "vterm.h" 8 | 9 | #define SWAP_ENDIAN(val) ((unsigned int) ( \ 10 | (((unsigned int) (val) & (unsigned int) 0x000000ffU) << 24) | \ 11 | (((unsigned int) (val) & (unsigned int) 0x0000ff00U) << 8) | \ 12 | (((unsigned int) (val) & (unsigned int) 0x00ff0000U) >> 8) | \ 13 | (((unsigned int) (val) & (unsigned int) 0xff000000U) >> 24))) 14 | 15 | static char *f = "VL-Gothic-Regular"; 16 | static double fs = 8.0; 17 | static int brect[8] = {0}; 18 | static int w = 80, h = 24, dx, dy; 19 | 20 | typedef struct header { 21 | int diff; 22 | int len; 23 | } Header; 24 | 25 | static int 26 | is_little_endian() { 27 | static int retval = -1; 28 | if (retval == -1) { 29 | int n = 1; 30 | char *p = (char *)&n; 31 | char x[] = {1, 0, 0, 0}; 32 | retval = memcmp(p, x, 4) == 0 ? 1 : 0; 33 | } 34 | return retval; 35 | } 36 | 37 | static int 38 | convert_to_little_endian(int x) { 39 | if (is_little_endian()) 40 | return x; 41 | return SWAP_ENDIAN(x); 42 | } 43 | 44 | static int 45 | read_header(FILE *fp, Header *h) { 46 | static unsigned long old = 0; 47 | unsigned long cur; 48 | int buf[3]; 49 | if (fread(buf, sizeof(int), 3, fp) == 0) { 50 | return 0; 51 | } 52 | cur = convert_to_little_endian(buf[0]) * 1000000 53 | + convert_to_little_endian(buf[1]); 54 | 55 | h->diff = old == 0 ? 0 : cur - old; 56 | h->len = convert_to_little_endian(buf[2]); 57 | old = cur; 58 | return 1; 59 | } 60 | 61 | static int 62 | ttyread(FILE *fp, Header *h, char **buf) { 63 | if (read_header(fp, h) == 0) 64 | return 0; 65 | *buf = malloc(h->len); 66 | if (*buf == NULL) 67 | perror("malloc"); 68 | if (fread(*buf, 1, h->len, fp) == 0) 69 | perror("fread"); 70 | return 1; 71 | } 72 | 73 | static int 74 | utf_char2bytes(int c, char *buf) { 75 | if (c < 0x80) { 76 | buf[0] = c; 77 | return 1; 78 | } 79 | if (c < 0x800) { 80 | buf[0] = 0xc0 + ((unsigned)c >> 6); 81 | buf[1] = 0x80 + (c & 0x3f); 82 | return 2; 83 | } 84 | if (c < 0x10000) { 85 | buf[0] = 0xe0 + ((unsigned)c >> 12); 86 | buf[1] = 0x80 + (((unsigned)c >> 6) & 0x3f); 87 | buf[2] = 0x80 + (c & 0x3f); 88 | return 3; 89 | } 90 | if (c < 0x200000) { 91 | buf[0] = 0xf0 + ((unsigned)c >> 18); 92 | buf[1] = 0x80 + (((unsigned)c >> 12) & 0x3f); 93 | buf[2] = 0x80 + (((unsigned)c >> 6) & 0x3f); 94 | buf[3] = 0x80 + (c & 0x3f); 95 | return 4; 96 | } 97 | if (c < 0x4000000) { 98 | buf[0] = 0xf8 + ((unsigned)c >> 24); 99 | buf[1] = 0x80 + (((unsigned)c >> 18) & 0x3f); 100 | buf[2] = 0x80 + (((unsigned)c >> 12) & 0x3f); 101 | buf[3] = 0x80 + (((unsigned)c >> 6) & 0x3f); 102 | buf[4] = 0x80 + (c & 0x3f); 103 | return 5; 104 | } 105 | buf[0] = 0xfc + ((unsigned)c >> 30); 106 | buf[1] = 0x80 + (((unsigned)c >> 24) & 0x3f); 107 | buf[2] = 0x80 + (((unsigned)c >> 18) & 0x3f); 108 | buf[3] = 0x80 + (((unsigned)c >> 12) & 0x3f); 109 | buf[4] = 0x80 + (((unsigned)c >> 6) & 0x3f); 110 | buf[5] = 0x80 + (c & 0x3f); 111 | return 6; 112 | } 113 | 114 | static void 115 | usage(void) { 116 | printf("Usage: ttyrec2gif [OPTION] [FILE]\n"); 117 | printf(" -o FILE Set output file \n"); 118 | printf(" -f FONT Set font file \n"); 119 | exit(EXIT_FAILURE); 120 | } 121 | 122 | static void 123 | write_schene(FILE* out, VTerm* vt, int duration) { 124 | int color; 125 | VTermScreen* screen; 126 | VTermPos pos; 127 | VTermScreenCell cell; 128 | gdImagePtr fimg; 129 | 130 | screen = vterm_obtain_screen(vt); 131 | 132 | /* render terminal */ 133 | fimg = gdImageCreateTrueColor(w * dx, h * dy); 134 | for (pos.row = 0; pos.row < 24; pos.row++) { 135 | for (pos.col = 0; pos.col < 80; pos.col++) { 136 | char b[7] = {0}; 137 | vterm_screen_get_cell(screen, pos, &cell); 138 | if (cell.chars[0] == 0) continue; 139 | b[utf_char2bytes((int) *cell.chars, b)] = 0; 140 | 141 | color = gdImageColorResolve( 142 | fimg, 143 | cell.bg.red, 144 | cell.bg.green, 145 | cell.bg.blue); 146 | gdImageFilledRectangle( 147 | fimg, 148 | pos.col * dx, 149 | pos.row * dy, 150 | (pos.col + cell.width) * dx, 151 | (pos.row + cell.width) * dy, 152 | color); 153 | color = gdImageColorResolve( 154 | fimg, 155 | cell.fg.red, 156 | cell.fg.green, 157 | cell.fg.blue); 158 | puts(gdImageStringFT( 159 | fimg, NULL, color, f, 160 | fs, 0.0, 161 | pos.col * dx - brect[6], 162 | pos.row * dy - brect[7], 163 | b)); 164 | pos.col += cell.width - 1; 165 | } 166 | } 167 | 168 | /* render cursor */ 169 | vterm_state_get_cursorpos(vterm_obtain_state(vt), &pos); 170 | vterm_screen_get_cell(screen, pos, &cell); 171 | color = gdImageColorResolve(fimg, 255, 255, 255); 172 | gdImageFilledRectangle( 173 | fimg, 174 | pos.col * dx, 175 | pos.row * dy, 176 | (pos.col + cell.width) * dx, 177 | (pos.row + 1) * dy, 178 | color); 179 | 180 | /* add frame with delay */ 181 | gdImageTrueColorToPalette(fimg, 1, gdMaxColors); 182 | gdImageGifAnimAdd(fimg, out, 1, 0, 0, duration, gdDisposalNone, NULL); 183 | gdImageDestroy(fimg); 184 | } 185 | 186 | int 187 | main(int argc, char* argv[]) { 188 | char *buf; 189 | Header header; 190 | VTerm* vt; 191 | VTermScreen* screen; 192 | char* fname = "animated.gif"; 193 | FILE* in = NULL; 194 | FILE* out = NULL; 195 | 196 | gdImagePtr img; 197 | 198 | while (1) { 199 | int ch = getopt(argc, argv, "o:f:w:h:"); 200 | if (ch == EOF) break; 201 | switch (ch) { 202 | case 'o': 203 | if (optarg == NULL) usage(); 204 | fname = optarg; 205 | break; 206 | case 'f': 207 | if (optarg == NULL) usage(); 208 | f = optarg; 209 | break; 210 | case 'w': 211 | if (optarg == NULL) usage(); 212 | w = atoi(optarg); 213 | break; 214 | case 'h': 215 | if (optarg == NULL) usage(); 216 | h = atoi(optarg); 217 | break; 218 | default: 219 | usage(); 220 | } 221 | } 222 | 223 | if (optind >= argc) usage(); 224 | 225 | in = fopen(argv[optind], "rb"); 226 | if (!in) { 227 | perror("fopen"); 228 | return EXIT_FAILURE; 229 | } 230 | out = fopen(fname, "wb"); 231 | if (!out) { 232 | perror("fopen"); 233 | fclose(in); 234 | return EXIT_FAILURE; 235 | } 236 | 237 | /* calculate cell size */ 238 | gdImageStringFT(NULL, brect, 0, f, fs, 0.0, 0, 0, "\u25a0"); 239 | dx = (brect[4] - brect[6]) / 2; 240 | dy = brect[1] - brect[7]; 241 | 242 | img = gdImageCreate(w * dx, h * dy); 243 | if (!img) { 244 | perror("gdImageCreate"); 245 | fclose(in); 246 | fclose(out); 247 | return EXIT_FAILURE; 248 | } 249 | 250 | /* setup terminal */ 251 | vt = vterm_new(24, 80); 252 | vterm_set_utf8(vt, 1); 253 | screen = vterm_obtain_screen(vt); 254 | vterm_screen_enable_altscreen(screen, 1); 255 | vterm_screen_reset(screen, 1); 256 | 257 | gdImageGifAnimBegin(img, out, 0, -1); 258 | while (ttyread(in, &header, &buf) != 0) { 259 | /* write screen */ 260 | write_schene(out, vt, header.diff/10000); 261 | 262 | /* write to terminal */ 263 | vterm_input_write(vt, buf, header.len); 264 | free(buf); 265 | } 266 | gdImageGifAnimEnd(out); 267 | gdImageDestroy(img); 268 | 269 | vterm_free(vt); 270 | 271 | fclose(in); 272 | fclose(out); 273 | return EXIT_SUCCESS; 274 | } 275 | 276 | /* vim:set et: */ 277 | --------------------------------------------------------------------------------