asciinema-rails 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +9 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +107 -0
  7. data/Rakefile +2 -0
  8. data/asciinema-rails.gemspec +27 -0
  9. data/lib/asciinema-rails.rb +1 -0
  10. data/lib/asciinema/asciicast.rb +44 -0
  11. data/lib/asciinema/asciicast_frames_file_updater.rb +24 -0
  12. data/lib/asciinema/asciicast_snapshot_updater.rb +20 -0
  13. data/lib/asciinema/brush.rb +78 -0
  14. data/lib/asciinema/cell.rb +33 -0
  15. data/lib/asciinema/cursor.rb +18 -0
  16. data/lib/asciinema/film.rb +40 -0
  17. data/lib/asciinema/frame.rb +26 -0
  18. data/lib/asciinema/frame_diff.rb +20 -0
  19. data/lib/asciinema/frame_diff_list.rb +26 -0
  20. data/lib/asciinema/grid.rb +51 -0
  21. data/lib/asciinema/json_file_writer.rb +21 -0
  22. data/lib/asciinema/rails.rb +8 -0
  23. data/lib/asciinema/rails/convertor.rb +97 -0
  24. data/lib/asciinema/rails/engine.rb +6 -0
  25. data/lib/asciinema/rails/version.rb +5 -0
  26. data/lib/asciinema/snapshot.rb +41 -0
  27. data/lib/asciinema/stdout.rb +101 -0
  28. data/lib/asciinema/terminal.rb +65 -0
  29. data/spec/.rspec +2 -0
  30. data/spec/asciinemosh_spec.rb +63 -0
  31. data/spec/fixtures/sudosh-script +8 -0
  32. data/spec/fixtures/sudosh-time +22 -0
  33. data/spec/spec_helper.rb +50 -0
  34. data/src/terminal +0 -0
  35. data/src/terminal.c +307 -0
  36. data/vendor/assets/javscripts/asciinema-rails.js +4 -0
  37. data/vendor/assets/javscripts/asciinema.org/asciinema-player.js +1149 -0
  38. data/vendor/assets/javscripts/asciinema.org/rAF.js +32 -0
  39. data/vendor/assets/javscripts/asciinema.org/react-0.10.0.js +17228 -0
  40. data/vendor/assets/javscripts/asciinema.org/screenfull.js +143 -0
  41. data/vendor/assets/stylesheets/asciinema-rails.css +4 -0
  42. data/vendor/assets/stylesheets/asciinema.org/asciinema-player.css +1732 -0
  43. data/vendor/assets/stylesheets/asciinema.org/themes/solarized-dark.css +35 -0
  44. data/vendor/assets/stylesheets/asciinema.org/themes/solarized-light.css +35 -0
  45. data/vendor/assets/stylesheets/asciinema.org/themes/tango.css +35 -0
  46. metadata +192 -0
@@ -0,0 +1,4 @@
1
+ //= require asciinema.org/rAF
2
+ //= require asciinema.org/react-0.10.0
3
+ //= require asciinema.org/asciinema-player
4
+ //= require asciinema.org/screenfull
@@ -0,0 +1,1149 @@
1
+ /** @jsx React.DOM */
2
+
3
+ (function(exports) {
4
+ var dom = React.DOM;
5
+
6
+ var PlaybackControlButton = React.createClass({ displayName: 'PlayButton',
7
+ // props: playing, onPauseClick, onResumeClick
8
+
9
+ render: function() {
10
+ var icon;
11
+
12
+ if (this.props.playing) {
13
+ icon = asciinema.PauseIcon();
14
+ } else {
15
+ icon = asciinema.PlayIcon();
16
+ }
17
+
18
+ return dom.span({ className: "playback-button", onClick: this.handleClick }, icon);
19
+ },
20
+
21
+ handleClick: function(event) {
22
+ event.preventDefault();
23
+
24
+ if (this.props.playing) {
25
+ this.props.onPauseClick();
26
+ } else {
27
+ this.props.onResumeClick();
28
+ }
29
+ }
30
+
31
+ });
32
+
33
+ var FullscreenToggleButton = React.createClass({ displayName: 'FullscreenToggleButton',
34
+ // props: fullscreen, onClick
35
+
36
+ render: function() {
37
+ var icon;
38
+
39
+ if (this.props.fullscreen) {
40
+ icon = asciinema.ShrinkIcon();
41
+ } else {
42
+ icon = asciinema.ExpandIcon();
43
+ }
44
+
45
+ return dom.span({ className: "fullscreen-button", onClick: this.handleClick }, icon);
46
+ },
47
+
48
+ handleClick: function(event) {
49
+ event.preventDefault();
50
+ this.props.onClick();
51
+ },
52
+
53
+ });
54
+
55
+ exports.ControlBar = React.createClass({ displayName: 'ControlBar',
56
+ // props: playing, fullscreen, currentTime, totalTime, onPauseClick,
57
+ // onResumeClick, onSeekClick, toggleFullscreen
58
+
59
+ render: function() {
60
+ return (
61
+ dom.div({ className: "control-bar" },
62
+
63
+ PlaybackControlButton({
64
+ playing: this.props.playing,
65
+ onPauseClick: this.props.onPauseClick,
66
+ onResumeClick: this.props.onResumeClick
67
+ }),
68
+
69
+ asciinema.Timer({
70
+ currentTime: this.props.currentTime,
71
+ totalTime: this.props.totalTime
72
+ }),
73
+
74
+ FullscreenToggleButton({
75
+ fullscreen: this.props.fullscreen,
76
+ onClick: this.props.toggleFullscreen,
77
+ }),
78
+
79
+ asciinema.ProgressBar({
80
+ value: this.props.currentTime / this.props.totalTime,
81
+ onClick: this.handleSeek
82
+ })
83
+
84
+ )
85
+ )
86
+ },
87
+
88
+ handleSeek: function(value) {
89
+ this.props.onSeekClick(value * this.props.totalTime);
90
+ },
91
+
92
+ shouldComponentUpdate: function(nextProps, nextState) {
93
+ return nextProps.playing != this.props.playing ||
94
+ nextProps.currentTime != this.props.currentTime ||
95
+ nextProps.totalTime != this.props.totalTime ||
96
+ nextProps.fullscreen != this.props.fullscreen;
97
+ },
98
+
99
+ });
100
+
101
+ })(window.asciinema = window.asciinema || {});
102
+
103
+ (function(exports) {
104
+ var dom = React.DOM;
105
+
106
+ exports.Cursor = React.createClass({ displayName: 'Cursor',
107
+ // props: fg, bg, char, inverse
108
+
109
+ render: function() {
110
+ return dom.span({ className: this.className() }, this.props.char);
111
+ },
112
+
113
+ className: function() {
114
+ if (this.props.inverse) {
115
+ return "cursor fg-" + this.props.fg + " bg-" + this.props.bg;
116
+ } else {
117
+ return "cursor fg-" + this.props.bg + " bg-" + this.props.fg;
118
+ }
119
+ },
120
+ });
121
+
122
+ })(window.asciinema = window.asciinema || {});
123
+
124
+ (function(exports) {
125
+
126
+ function FunnyMovie() {
127
+ // this.onFrame = onFrame;
128
+ this.n = 0;
129
+ this.direction = 1
130
+ }
131
+
132
+ FunnyMovie.prototype.start = function(onFrame) {
133
+ setInterval(function() {
134
+ this.generateFrame(onFrame);
135
+ }.bind(this), 100);
136
+ }
137
+
138
+ FunnyMovie.prototype.generateFrame = function(onFrame) {
139
+ var lines = {};
140
+ lines[this.n] = [[(new Date()).toString(), {}]];
141
+ onFrame({ lines: lines });
142
+
143
+ this.n += this.direction;
144
+ if (this.n < 0 || this.n >= 10) {
145
+ this.direction *= -1;
146
+ }
147
+ }
148
+
149
+ FunnyMovie.prototype.pause = function() {
150
+ return false;
151
+ }
152
+
153
+ FunnyMovie.prototype.resume = function() {
154
+ return false;
155
+ }
156
+
157
+ FunnyMovie.prototype.seek = function(time) {
158
+ return false;
159
+ }
160
+
161
+ exports.FunnyMovie = FunnyMovie;
162
+
163
+ })(window.asciinema = window.asciinema || {});
164
+
165
+ (function(exports) {
166
+
167
+ function HttpArraySource(url, speed) {
168
+ this.url = url;
169
+ this.speed = speed || 1;
170
+ }
171
+
172
+ HttpArraySource.prototype.start = function(onFrame, onFinish, setLoading) {
173
+ var controller;
174
+
175
+ if (this.data) {
176
+ controller = this.createController(onFrame, onFinish);
177
+ } else {
178
+ this.fetchData(setLoading, function() {
179
+ controller = this.createController(onFrame, onFinish);
180
+ }.bind(this));
181
+ }
182
+
183
+ return {
184
+ time: function() {
185
+ if (controller && controller.time) {
186
+ return controller.time();
187
+ } else {
188
+ return 0;
189
+ }
190
+ },
191
+
192
+ pause: function() {
193
+ if (controller && controller.pause) {
194
+ return controller.pause();
195
+ }
196
+ },
197
+
198
+ resume: function() {
199
+ if (controller && controller.resume) {
200
+ return controller.resume();
201
+ }
202
+ },
203
+
204
+ seek: function(time) {
205
+ if (controller && controller.seek) {
206
+ return controller.seek(time);
207
+ }
208
+ }
209
+ }
210
+ }
211
+
212
+ HttpArraySource.prototype.fetchData = function(setLoading, onResult) {
213
+ setLoading(true);
214
+
215
+ var request = $.ajax({ url: this.url, dataType: 'json' });
216
+
217
+ request.done(function(data) {
218
+ setLoading(false);
219
+ this.data = data;
220
+ onResult();
221
+ }.bind(this));
222
+
223
+ request.fail(function(jqXHR, textStatus) {
224
+ setLoading(false);
225
+ console.error(this.url, textStatus);
226
+ });
227
+ }
228
+
229
+ HttpArraySource.prototype.createController = function(onFrame, onFinish) {
230
+ arraySource = new asciinema.NavigableArraySource(this.data, this.speed);
231
+
232
+ return arraySource.start(onFrame, onFinish);
233
+ }
234
+
235
+ exports.HttpArraySource = HttpArraySource;
236
+
237
+ })(window.asciinema = window.asciinema || {});
238
+
239
+ /** @jsx React.DOM */
240
+
241
+ (function(exports) {
242
+ var dom = React.DOM;
243
+
244
+ exports.PlayIcon = React.createClass({ displayName: 'PlayIcon',
245
+
246
+ render: function() {
247
+ return (
248
+ dom.svg({ version: "1.1", xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 12 12", className: "icon" },
249
+ dom.path({ d: "M1,0 L11,6 L1,12 Z" })
250
+ )
251
+ )
252
+ },
253
+
254
+ });
255
+
256
+ exports.PauseIcon = React.createClass({ displayName: 'PauseIcon',
257
+
258
+ render: function() {
259
+ return (
260
+ dom.svg({ version: "1.1", xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 12 12", className: "icon" },
261
+ dom.path({ d: "M1,0 L4,0 L4,12 L1,12 Z" }),
262
+ dom.path({ d: "M8,0 L11,0 L11,12 L8,12 Z" })
263
+ )
264
+ )
265
+ },
266
+
267
+ });
268
+
269
+ exports.ExpandIcon = React.createClass({ displayName: 'ExpandIcon',
270
+
271
+ render: function() {
272
+ return (
273
+ dom.svg({ version: "1.1", xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 12 12", className: "icon" },
274
+ dom.path({ d: "M0,0 L5,0 L3,2 L5,4 L4,5 L2,3 L0,5 Z" }),
275
+ dom.path({ d: "M12,12 L12,7 L10,9 L8,7 L7,8 L9,10 L7,12 Z" })
276
+ )
277
+ )
278
+ },
279
+
280
+ });
281
+
282
+ exports.ShrinkIcon = React.createClass({ displayName: 'ShrinkIcon',
283
+
284
+ render: function() {
285
+ return (
286
+ dom.svg({ version: "1.1", xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 12 12", className: "icon" },
287
+ dom.path({ d: "M5,5 L5,0 L3,2 L1,0 L0,1 L2,3 L0,5 Z" }),
288
+ dom.path({ d: "M7,7 L12,7 L10,9 L12,11 L11,12 L9,10 L7,12 Z" })
289
+ )
290
+ )
291
+ },
292
+
293
+ });
294
+
295
+ })(window.asciinema = window.asciinema || {});
296
+
297
+ (function(exports) {
298
+ var dom = React.DOM;
299
+
300
+ exports.Line = React.createClass({ displayName: 'Line',
301
+ // props: parts, cursorX, cursorInverted
302
+
303
+ render: function() {
304
+ var lineLength = 0;
305
+ var cursorX = this.props.cursorX;
306
+
307
+ var parts = this.props.parts.map(function(part, index) {
308
+ var attrs = {};
309
+ // clone attrs, so we can adjust it below
310
+ for (key in part[1]) {
311
+ attrs[key] = part[1][key];
312
+ }
313
+
314
+ var partProps = { text: part[0], attrs: attrs };
315
+ var partLength = part[0].length;
316
+
317
+ if (cursorX !== null) {
318
+ if (lineLength <= cursorX && cursorX < lineLength + partLength) {
319
+ partProps.cursorX = cursorX - lineLength;
320
+ partProps.cursorInverted = this.props.cursorInverted;
321
+
322
+ // TODO: remove this hack and update terminal.c to do this instead
323
+ if (attrs.inverse) {
324
+ delete attrs.inverse;
325
+ } else {
326
+ attrs.inverse = true;
327
+ }
328
+ }
329
+ }
330
+
331
+ lineLength += partLength;
332
+
333
+ return asciinema.Part(partProps);
334
+ }.bind(this));
335
+
336
+ return dom.span({ className: "line" }, parts);
337
+ },
338
+
339
+ });
340
+ })(window.asciinema = window.asciinema || {});
341
+
342
+ (function(exports) {
343
+
344
+ function Movie(width, height, source, snapshot, totalTime) {
345
+ this.width = width;
346
+ this.height = height;
347
+ this.source = source;
348
+ this.snapshot = snapshot;
349
+ this.totalTime = totalTime;
350
+ }
351
+
352
+ Movie.prototype.start = function(onFrame, onFinish, setTime, setLoading, loop) {
353
+ var timeIntervalId;
354
+
355
+ var source = this.source;
356
+ var controller = {};
357
+
358
+ function onSourceFinish() {
359
+ if (loop) {
360
+ start();
361
+ } else {
362
+ clearInterval(timeIntervalId);
363
+ onFinish();
364
+ }
365
+ }
366
+
367
+ function start() {
368
+ var ctrl = source.start(onFrame, onSourceFinish, setLoading);
369
+
370
+ for (prop in ctrl) {
371
+ controller[prop] = ctrl[prop];
372
+ }
373
+ }
374
+
375
+ start();
376
+
377
+ timeIntervalId = setInterval(function() {
378
+ setTime(controller.time());
379
+ }, 300);
380
+
381
+ return controller;
382
+ }
383
+
384
+ exports.Movie = Movie;
385
+
386
+ })(window.asciinema = window.asciinema || {});
387
+
388
+ // var source = new ArraySource([]);
389
+ // var source = new CamSource(80, 24);
390
+ // var source = new WebsocketSource(url);
391
+
392
+ // var movie = new Movie(80, 24, source, [], 123.456);
393
+
394
+ // var controller = source.start(onFrame, onFinish, setLoading);
395
+ // controller.pause();
396
+
397
+ (function(exports) {
398
+
399
+ function now() {
400
+ return (new Date).getTime() / 1000;
401
+ }
402
+
403
+ function play(frames, speed, onFrame, onFinish) {
404
+ var frameNo = 0;
405
+ var startedAt = new Date;
406
+ var timeoutId;
407
+
408
+ function generate() {
409
+ var frame = frames[frameNo];
410
+
411
+ if (!frame) {
412
+ return;
413
+ }
414
+
415
+ onFrame(frame[0], frame[1]);
416
+
417
+ frameNo += 1;
418
+ scheduleNextFrame();
419
+ }
420
+
421
+ function scheduleNextFrame() {
422
+ var frame = frames[frameNo];
423
+
424
+ if (frame) {
425
+ timeoutId = setTimeout(generate, frames[frameNo][0] * 1000 / speed);
426
+ } else {
427
+ onFinish();
428
+
429
+ if (window.console) {
430
+ window.console.log('finished in ' + ((new Date).getTime() - startedAt.getTime()));
431
+ }
432
+ }
433
+ }
434
+
435
+ function stop() {
436
+ clearTimeout(timeoutId);
437
+ }
438
+
439
+ scheduleNextFrame();
440
+
441
+ return stop;
442
+ }
443
+
444
+ function NavigableArraySource(frames, speed) {
445
+ this.frames = frames;
446
+ this.speed = speed || 1;
447
+ }
448
+
449
+ NavigableArraySource.prototype.start = function(onFrame, onFinish, setLoading) {
450
+ var elapsedTime = 0;
451
+ var currentFramePauseTime;
452
+ var lastFrameTime;
453
+ var paused = false;
454
+ var finished = false;
455
+ var stop;
456
+
457
+ var playFrom = function(time) {
458
+ lastFrameTime = now();
459
+ elapsedTime = time;
460
+
461
+ return play(this.framesFrom(time), this.speed, function(delay, changes) {
462
+ lastFrameTime = now();
463
+ elapsedTime += delay;
464
+ onFrame(changes);
465
+ }, function() {
466
+ finished = true;
467
+ onFinish();
468
+ });
469
+ }.bind(this);
470
+
471
+ var currentFrameTime = function() {
472
+ return (now() - lastFrameTime) * this.speed;
473
+ }.bind(this);
474
+
475
+ stop = playFrom(0);
476
+
477
+ return {
478
+ pause: function() {
479
+ if (finished) {
480
+ return false;
481
+ }
482
+
483
+ paused = true;
484
+ stop();
485
+ currentFramePauseTime = currentFrameTime();
486
+
487
+ return true;
488
+ }.bind(this),
489
+
490
+ resume: function() {
491
+ if (finished) {
492
+ return false;
493
+ }
494
+
495
+ paused = false;
496
+ stop = playFrom(elapsedTime + currentFramePauseTime);
497
+
498
+ return true;
499
+ }.bind(this),
500
+
501
+ seek: function(seconds) {
502
+ if (finished) {
503
+ return false;
504
+ }
505
+
506
+ paused = false;
507
+ stop();
508
+ stop = playFrom(seconds);
509
+
510
+ return true;
511
+ }.bind(this),
512
+
513
+ time: function() {
514
+ if (finished) {
515
+ return elapsedTime;
516
+ } else if (paused) {
517
+ return elapsedTime + currentFramePauseTime;
518
+ } else {
519
+ return elapsedTime + currentFrameTime();
520
+ }
521
+ }.bind(this),
522
+ }
523
+ }
524
+
525
+ NavigableArraySource.prototype.framesFrom = function(fromTime) {
526
+ var frameNo = 0;
527
+ var currentTime = 0;
528
+ var changes = {};
529
+
530
+ while (currentTime + this.frames[frameNo][0] < fromTime) {
531
+ var frame = this.frames[frameNo];
532
+ currentTime += frame[0];
533
+ asciinema.mergeChanges(changes, frame[1]);
534
+ frameNo += 1;
535
+ }
536
+
537
+ var frames = [[0, changes]];
538
+
539
+ var nextFrame = this.frames[frameNo];
540
+ var delay = nextFrame[0] - (fromTime - currentTime);
541
+ frames = frames.concat([[delay, nextFrame[1]]]);
542
+
543
+ frames = frames.concat(this.frames.slice(frameNo + 1));
544
+
545
+ return frames;
546
+ }
547
+
548
+ exports.NavigableArraySource = NavigableArraySource;
549
+
550
+ })(window.asciinema = window.asciinema || {});
551
+
552
+ (function(exports) {
553
+ var dom = React.DOM;
554
+
555
+ exports.LoadingOverlay = React.createClass({ displayName: 'LoadingOverlay',
556
+
557
+ render: function() {
558
+ return (
559
+ dom.div({ className: "loading" },
560
+ dom.div({ className: "loader" })
561
+ )
562
+ );
563
+ }
564
+
565
+ });
566
+
567
+ exports.StartOverlay = React.createClass({ displayName: 'StartOverlay',
568
+ // props: start
569
+
570
+ render: function() {
571
+ return (
572
+ dom.div({ className: "start-prompt", onClick: this.onClick },
573
+ dom.div({ className: "play-button" },
574
+ dom.div(null,
575
+ dom.span(null,
576
+ asciinema.PlayIcon()
577
+ )
578
+ )
579
+ )
580
+ )
581
+ );
582
+ },
583
+
584
+ onClick: function(event) {
585
+ event.preventDefault();
586
+ this.props.start();
587
+ },
588
+
589
+ });
590
+ })(window.asciinema = window.asciinema || {});
591
+
592
+ (function(exports) {
593
+ var dom = React.DOM;
594
+
595
+ exports.Part = React.createClass({ displayName: 'Part',
596
+ // props: text, attrs, cursorX, cursorInverted
597
+
598
+ render: function() {
599
+ return dom.span({ className: this.className() }, this.children());
600
+ },
601
+
602
+ children: function() {
603
+ var text = this.props.text;
604
+ var cursorX = this.props.cursorX;
605
+
606
+ if (cursorX !== undefined) {
607
+ var elements = [];
608
+
609
+ if (cursorX > 0) {
610
+ elements = elements.concat([text.slice(0, cursorX)])
611
+ }
612
+
613
+ var cursor = asciinema.Cursor({
614
+ fg: this.fgColor() || 'fg',
615
+ bg: this.bgColor() || 'bg',
616
+ char: text[cursorX],
617
+ inverse: this.props.cursorInverted,
618
+ });
619
+
620
+ elements = elements.concat([cursor]);
621
+
622
+ if (cursorX + 1 < text.length) {
623
+ elements = elements.concat([text.slice(cursorX + 1)]);
624
+ }
625
+
626
+ return elements;
627
+ } else {
628
+ return this.props.text;
629
+ }
630
+ },
631
+
632
+ fgColor: function() {
633
+ var fg = this.props.attrs.fg;
634
+
635
+ if (this.props.attrs.bold && fg !== undefined && fg < 8) {
636
+ fg += 8;
637
+ }
638
+
639
+ return fg;
640
+ },
641
+
642
+ bgColor: function() {
643
+ var bg = this.props.attrs.bg;
644
+
645
+ if (this.props.attrs.blink && bg !== undefined && bg < 8) {
646
+ bg += 8;
647
+ }
648
+
649
+ return bg;
650
+ },
651
+
652
+ className: function() {
653
+ var classes = [];
654
+ var attrs = this.props.attrs;
655
+
656
+ var fg = this.fgColor();
657
+ var bg = this.bgColor();
658
+
659
+ if (attrs.inverse) {
660
+ var fgClass, bgClass;
661
+
662
+ if (bg !== undefined) {
663
+ fgClass = 'fg-' + bg;
664
+ } else {
665
+ fgClass = 'fg-bg';
666
+ }
667
+
668
+ if (fg !== undefined) {
669
+ bgClass = 'bg-' + fg;
670
+ } else {
671
+ bgClass = 'bg-fg';
672
+ }
673
+
674
+ classes = classes.concat([fgClass, bgClass]);
675
+ } else {
676
+ if (fg !== undefined) {
677
+ classes = classes.concat(['fg-' + fg]);
678
+ }
679
+
680
+ if (bg !== undefined) {
681
+ classes = classes.concat(['bg-' + bg]);
682
+ }
683
+ }
684
+
685
+ if (attrs.bold) {
686
+ classes = classes.concat(['bright']);
687
+ }
688
+
689
+ if (attrs.underline) {
690
+ classes = classes.concat(['underline']);
691
+ }
692
+
693
+ return classes.join(' ');
694
+ }
695
+
696
+ });
697
+ })(window.asciinema = window.asciinema || {});
698
+
699
+ (function(exports) {
700
+ var dom = React.DOM;
701
+
702
+ exports.Player = React.createClass({ displayName: 'Player',
703
+ // props: movie, autoPlay, fontSize, theme, loop
704
+
705
+ getInitialState: function() {
706
+ var lines = this.props.movie.snapshot || [];
707
+ var cursor = { x: 0, y: 0, visible: false };
708
+ var fontSize = this.props.fontSize || 'small';
709
+
710
+ return {
711
+ lines: lines,
712
+ cursor: cursor,
713
+ fontSize: fontSize,
714
+ fullscreen: false,
715
+ loading: false,
716
+ state: 'not-started',
717
+ currentTime: 0,
718
+ totalTime: this.props.movie.totalTime,
719
+ }
720
+ },
721
+
722
+ componentWillMount: function() {
723
+ if (this.props.autoPlay) {
724
+ this.start();
725
+ }
726
+ },
727
+
728
+ componentDidMount: function() {
729
+ if (screenfull.enabled) {
730
+ document.addEventListener(screenfull.raw.fullscreenchange, function() {
731
+ this.setState({ fullscreen: screenfull.isFullscreen });
732
+ }.bind(this));
733
+ }
734
+
735
+ window.addEventListener('resize', function() {
736
+ this.setState({
737
+ windowHeight: window.innerHeight,
738
+ playerHeight: this.refs.player.getDOMNode().offsetHeight
739
+ });
740
+ }.bind(this), true);
741
+
742
+ requestAnimationFrame(this.applyChanges);
743
+ },
744
+
745
+ render: function() {
746
+ var overlay;
747
+
748
+ if (this.state.loading) {
749
+ overlay = asciinema.LoadingOverlay();
750
+ } else if (!this.props.autoPlay && this.isNotStarted()) {
751
+ overlay = asciinema.StartOverlay({ start: this.start });
752
+ }
753
+
754
+ return (
755
+ dom.div({ className: 'asciinema-player-wrapper' },
756
+ dom.div({ ref: 'player', className: this.playerClassName(), style: this.playerStyle() },
757
+
758
+ asciinema.Terminal({
759
+ width: this.props.movie.width,
760
+ height: this.props.movie.height,
761
+ fontSize: this.fontSize(),
762
+ lines: this.state.lines,
763
+ cursor: this.state.cursor,
764
+ cursorBlinking: this.isPlaying(),
765
+ }),
766
+
767
+ asciinema.ControlBar({
768
+ playing: this.isPlaying(),
769
+ onPauseClick: this.pause,
770
+ onResumeClick: this.resume,
771
+ onSeekClick: this.seek,
772
+ currentTime: this.state.currentTime,
773
+ totalTime: this.state.totalTime,
774
+ fullscreen: this.state.fullscreen,
775
+ toggleFullscreen: this.toggleFullscreen,
776
+ }),
777
+
778
+ overlay
779
+ )
780
+ )
781
+ );
782
+ },
783
+
784
+ playerClassName: function() {
785
+ return 'asciinema-player ' + this.themeClassName();
786
+ },
787
+
788
+ themeClassName: function() {
789
+ return 'asciinema-theme-' + (this.props.theme || 'tango');
790
+ },
791
+
792
+ fontSize: function() {
793
+ if (this.state.fullscreen) {
794
+ return 'small';
795
+ } else {
796
+ return this.state.fontSize;
797
+ }
798
+ },
799
+
800
+ playerStyle: function() {
801
+ if (this.state.fullscreen && this.state.windowHeight && this.state.playerHeight) {
802
+ var space = this.state.windowHeight - this.state.playerHeight;
803
+
804
+ if (space > 0) {
805
+ return { marginTop: (space / 2) + 'px' };
806
+ }
807
+ }
808
+
809
+ return {};
810
+ },
811
+
812
+ setLoading: function(loading) {
813
+ this.setState({ loading: loading });
814
+ },
815
+
816
+ start: function() {
817
+ this.setState({ state: 'playing' });
818
+ this.movieController = this.props.movie.start(this.onFrame, this.onFinish, this.setTime, this.setLoading, this.props.loop);
819
+ },
820
+
821
+ onFinish: function() {
822
+ this.setState({ state: 'finished' });
823
+ },
824
+
825
+ setTime: function(time) {
826
+ this.setState({ currentTime: time });
827
+ },
828
+
829
+ pause: function() {
830
+ if (this.movieController.pause && this.movieController.pause()) {
831
+ this.setState({ state: 'paused' });
832
+ }
833
+ },
834
+
835
+ resume: function() {
836
+ if (this.isFinished()) {
837
+ this.start();
838
+ } else {
839
+ if (this.movieController.resume && this.movieController.resume()) {
840
+ this.setState({ state: 'playing' });
841
+ }
842
+ }
843
+ },
844
+
845
+ seek: function(time) {
846
+ if (this.movieController.seek && this.movieController.seek(time)) {
847
+ this.setState({ state: 'playing', currentTime: time });
848
+ }
849
+ },
850
+
851
+ toggleFullscreen: function() {
852
+ if (screenfull.enabled) {
853
+ screenfull.toggle(this.getDOMNode());
854
+ }
855
+ },
856
+
857
+ onFrame: function(changes) {
858
+ this.changes = this.changes || {};
859
+ asciinema.mergeChanges(this.changes, changes);
860
+ },
861
+
862
+ applyChanges: function() {
863
+ requestAnimationFrame(this.applyChanges);
864
+
865
+ // if (!this.dirty) {
866
+ // return;
867
+ // }
868
+
869
+ var changes = this.changes || {};
870
+ var newState = {};
871
+
872
+ if (changes.lines) {
873
+ var lines = [];
874
+
875
+ for (var n in this.state.lines) {
876
+ lines[n] = this.state.lines[n];
877
+ }
878
+
879
+ for (var n in changes.lines) {
880
+ lines[n] = changes.lines[n];
881
+ }
882
+
883
+ newState.lines = lines;
884
+ }
885
+
886
+ if (changes.cursor) {
887
+ var cursor = {
888
+ x: this.state.cursor.x,
889
+ y: this.state.cursor.y,
890
+ visible: this.state.cursor.visible
891
+ };
892
+
893
+ for (var key in changes.cursor) {
894
+ cursor[key] = changes.cursor[key];
895
+ }
896
+
897
+ newState.cursor = cursor;
898
+ }
899
+
900
+ this.setState(newState);
901
+ this.changes = {};
902
+ },
903
+
904
+ isNotStarted: function() {
905
+ return this.state.state === 'not-started';
906
+ },
907
+
908
+ isPlaying: function() {
909
+ return this.state.state === 'playing';
910
+ },
911
+
912
+ isFinished: function() {
913
+ return this.state.state === 'finished';
914
+ },
915
+ });
916
+
917
+ exports.mergeChanges = function(dest, src) {
918
+ if (src.lines) {
919
+ dest.lines = dest.lines || {};
920
+
921
+ for (var n in src.lines) {
922
+ dest.lines[n] = src.lines[n];
923
+ }
924
+ }
925
+
926
+ if (src.cursor) {
927
+ dest.cursor = dest.cursor || {};
928
+
929
+ for (var key in src.cursor) {
930
+ dest.cursor[key] = src.cursor[key];
931
+ }
932
+ }
933
+ }
934
+
935
+ })(window.asciinema = window.asciinema || {});
936
+
937
+ (function(exports) {
938
+ var dom = React.DOM;
939
+
940
+ exports.ProgressBar = React.createClass({ displayName: 'ProgressBar',
941
+ // props.value
942
+ // props.onClick
943
+
944
+ render: function() {
945
+ var width = 100 * this.props.value;
946
+
947
+ return (
948
+ dom.span({ className: "progressbar" },
949
+ dom.span({ className: "bar", ref: "bar", onMouseDown: this.handleClick },
950
+ dom.span({ className: "gutter" },
951
+ dom.span({ style: { width: width + "%" } })
952
+ )
953
+ )
954
+ )
955
+ )
956
+ },
957
+
958
+ handleClick: function(event) {
959
+ event.preventDefault();
960
+
961
+ var target = event.target || event.srcElement,
962
+ style = target.currentStyle || window.getComputedStyle(target, null),
963
+ borderLeftWidth = parseInt(style['borderLeftWidth'], 10),
964
+ borderTopWidth = parseInt(style['borderTopWidth'], 10),
965
+ rect = target.getBoundingClientRect(),
966
+ offsetX = event.clientX - borderLeftWidth - rect.left,
967
+ offsetY = event.clientY - borderTopWidth - rect.top;
968
+
969
+ var barWidth = this.refs.bar.getDOMNode().offsetWidth;
970
+ this.props.onClick(offsetX / barWidth);
971
+ }
972
+
973
+ });
974
+
975
+ })(window.asciinema = window.asciinema || {});
976
+
977
+ (function(exports) {
978
+ var dom = React.DOM;
979
+
980
+ exports.Terminal = React.createClass({ displayName: 'Terminal',
981
+ // props: width, height, fontSize, lines, cursor, cursorBlinking
982
+
983
+ getInitialState: function() {
984
+ return { cursorInverted: false };
985
+ },
986
+
987
+ render: function() {
988
+ var cursor = this.props.cursor;
989
+
990
+ var lines = this.props.lines.map(function(line, index) {
991
+ if (cursor.visible && cursor.y == index) {
992
+ return asciinema.Line({
993
+ parts: line,
994
+ cursorX: cursor.x,
995
+ cursorInverted: this.props.cursorBlinking && this.state.cursorInverted,
996
+ });
997
+ } else {
998
+ return asciinema.Line({ parts: line });
999
+ }
1000
+
1001
+ }.bind(this));
1002
+
1003
+ return dom.pre({ className: this.className(), style: this.style() }, lines);
1004
+ },
1005
+
1006
+ className: function() {
1007
+ return "asciinema-terminal " + this.fontClassName();
1008
+ },
1009
+
1010
+ fontClassName: function() {
1011
+ return 'font-' + this.props.fontSize;
1012
+ },
1013
+
1014
+ style: function() {
1015
+ if (this.state.charDimensions) {
1016
+ var dimensions = this.state.charDimensions[this.props.fontSize];
1017
+ var width = Math.ceil(this.props.width * dimensions.width) + 'px';
1018
+ var height = Math.ceil(this.props.height * dimensions.height) + 'px';
1019
+ return { width: width, height: height };
1020
+ } else {
1021
+ return {};
1022
+ }
1023
+ },
1024
+
1025
+ componentDidMount: function() {
1026
+ this.calculateCharDimensions();
1027
+ this.startBlinking();
1028
+ },
1029
+
1030
+ componentDidUpdate: function(prevProps, prevState) {
1031
+ if (prevProps.lines != this.props.lines || prevProps.cursor != this.props.cursor) {
1032
+ this.restartBlinking();
1033
+ }
1034
+ },
1035
+
1036
+ componentWillUnmount: function() {
1037
+ this.stopBlinking();
1038
+ },
1039
+
1040
+ shouldComponentUpdate: function(nextProps, nextState) {
1041
+ return nextProps.lines != this.props.lines ||
1042
+ nextProps.cursor != this.props.cursor ||
1043
+ nextProps.fontSize != this.props.fontSize ||
1044
+ nextState.cursorInverted != this.state.cursorInverted ||
1045
+ nextState.charDimensions != this.state.charDimensions;
1046
+ },
1047
+
1048
+ calculateCharDimensions: function() {
1049
+ var $tmpChild = $('<span class="font-sample"><span class="line"><span class="char">MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM</span></span></span>');
1050
+ this.getDOMNode().appendChild($tmpChild[0]);
1051
+ var $span = $tmpChild.find('.char');
1052
+
1053
+ var charDimensions = {};
1054
+
1055
+ $tmpChild.addClass('font-small');
1056
+ charDimensions.small = { width: $span.width() / 200, height: $tmpChild.height() };
1057
+
1058
+ $tmpChild.removeClass('font-small');
1059
+ $tmpChild.addClass('font-medium');
1060
+ charDimensions.medium = { width: $span.width() / 200, height: $tmpChild.height() };
1061
+
1062
+ $tmpChild.removeClass('font-medium');
1063
+ $tmpChild.addClass('font-big');
1064
+ charDimensions.big = { width: $span.width() / 200, height: $tmpChild.height() };
1065
+
1066
+ $tmpChild.remove();
1067
+
1068
+ this.setState({ charDimensions: charDimensions });
1069
+ },
1070
+
1071
+ startBlinking: function() {
1072
+ this.cursorBlinkInvervalId = setInterval(this.flip, 500);
1073
+ },
1074
+
1075
+ stopBlinking: function() {
1076
+ clearInterval(this.cursorBlinkInvervalId);
1077
+ },
1078
+
1079
+ restartBlinking: function() {
1080
+ this.stopBlinking();
1081
+ this.reset();
1082
+ this.startBlinking();
1083
+ },
1084
+
1085
+ reset: function() {
1086
+ this.setState({ cursorInverted: false });
1087
+ },
1088
+
1089
+ flip: function() {
1090
+ this.setState({ cursorInverted: !this.state.cursorInverted });
1091
+ },
1092
+
1093
+ });
1094
+ })(typeof exports === 'undefined' ? (this.asciinema = this.asciinema || {}) : exports);
1095
+
1096
+ (function(exports) {
1097
+ var dom = React.DOM;
1098
+
1099
+ exports.Timer = React.createClass({ displayName: 'Timer',
1100
+ // props.currentTime
1101
+ // props.totalTime
1102
+
1103
+ render: function() {
1104
+ return (
1105
+ dom.span({ className: "timer" },
1106
+ dom.span({ className: "time-elapsed" }, this.elapsedTime()),
1107
+ dom.span({ className: "time-remaining" }, this.remainingTime())
1108
+ )
1109
+ )
1110
+ },
1111
+
1112
+ remainingTime: function() {
1113
+ var t = this.props.totalTime - this.props.currentTime;
1114
+ return "-" + this.formatTime(t);
1115
+ },
1116
+
1117
+ elapsedTime: function() {
1118
+ return this.formatTime(this.props.currentTime);
1119
+ },
1120
+
1121
+ formatTime: function(seconds) {
1122
+ if (seconds < 0) {
1123
+ seconds = 0;
1124
+ }
1125
+
1126
+ return "" + this.minutes(seconds) + ":" + this.seconds(seconds);
1127
+ },
1128
+
1129
+ minutes: function(s) {
1130
+ var minutes = Math.floor(s / 60)
1131
+ return this.pad2(minutes);
1132
+ },
1133
+
1134
+ seconds: function(s) {
1135
+ var seconds = Math.floor(s % 60)
1136
+ return this.pad2(seconds);
1137
+ },
1138
+
1139
+ pad2: function(number) {
1140
+ if (number < 10) {
1141
+ return '0' + number;
1142
+ } else {
1143
+ return number;
1144
+ }
1145
+ }
1146
+
1147
+ });
1148
+
1149
+ })(window.asciinema = window.asciinema || {});