@arcblock/terminal 3.1.8 → 3.1.10
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.
- package/LICENSE +1 -1
- package/README.md +130 -5
- package/lib/Player.js +74 -85
- package/lib/Terminal.js +13 -7
- package/lib/styles.js +294 -0
- package/package.json +4 -4
- package/src/Player.jsx +29 -36
- package/src/Player.stories.jsx +4 -0
- package/src/Terminal.jsx +8 -4
- package/src/styles.js +357 -0
- package/lib/player.css +0 -378
- package/lib/xterm.css +0 -171
- package/src/player.css +0 -378
- package/src/xterm.css +0 -171
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,19 +1,144 @@
|
|
|
1
1
|

|
|
2
2
|
|
|
3
|
-
> Terminal Player is a
|
|
3
|
+
> Terminal Player is a React component for playing back terminal recordings in the browser, built on top of `xterm.js`. Perfect for creating interactive terminal demos, tutorials, and documentation.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🎬 **Playback Controls**: Play, pause, seek, and loop terminal recordings
|
|
8
|
+
- 🎨 **Customizable Themes**: Full terminal color theme support
|
|
9
|
+
- 📱 **Responsive Design**: Automatically adapts to container size
|
|
10
|
+
- ⚡ **Performance**: Optimized for smooth playback of long recordings
|
|
11
|
+
- 🔧 **Easy Integration**: Simple React component API
|
|
12
|
+
- 📹 **asciinema Compatible**: Works with standard `.cast` recording format
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
6
15
|
|
|
7
16
|
```shell
|
|
8
17
|
yarn add @arcblock/terminal
|
|
9
18
|
```
|
|
10
19
|
|
|
11
|
-
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
### Basic Usage
|
|
12
23
|
|
|
13
24
|
```javascript
|
|
14
|
-
import
|
|
25
|
+
import { Player } from '@arcblock/terminal';
|
|
26
|
+
import recordingData from './my-recording.json';
|
|
15
27
|
|
|
16
28
|
export default function Demo() {
|
|
17
|
-
return <Player frames={records} options={config} />;
|
|
29
|
+
return <Player frames={recordingData.records} options={recordingData.config} autoPlay={true} loop={true} />;
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Player Props
|
|
34
|
+
|
|
35
|
+
| Prop | Type | Default | Description |
|
|
36
|
+
| ------------ | ---------- | ------------ | -------------------------------- |
|
|
37
|
+
| `frames` | `Array` | **required** | Array of terminal frames to play |
|
|
38
|
+
| `options` | `Object` | **required** | Player configuration and theme |
|
|
39
|
+
| `autoPlay` | `Boolean` | `false` | Auto-start playback when mounted |
|
|
40
|
+
| `loop` | `Boolean` | `false` | Loop playback infinitely |
|
|
41
|
+
| `onStart` | `Function` | - | Callback when playback starts |
|
|
42
|
+
| `onPause` | `Function` | - | Callback when playback pauses |
|
|
43
|
+
| `onComplete` | `Function` | - | Callback when playback completes |
|
|
44
|
+
|
|
45
|
+
## Recording Terminal Sessions
|
|
46
|
+
|
|
47
|
+
### 1. Install asciinema
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# macOS
|
|
51
|
+
brew install asciinema
|
|
52
|
+
|
|
53
|
+
# Ubuntu/Debian
|
|
54
|
+
sudo apt install asciinema
|
|
55
|
+
|
|
56
|
+
# pip
|
|
57
|
+
pipx install asciinema
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 2. Record Your Session
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Start recording
|
|
64
|
+
asciinema rec my-demo.cast
|
|
65
|
+
|
|
66
|
+
# ... perform your terminal commands ...
|
|
67
|
+
|
|
68
|
+
# Stop recording (Ctrl+D or type 'exit')
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 3. Convert to Player Format
|
|
72
|
+
|
|
73
|
+
Visit our **online converter** to transform your `.cast` file:
|
|
74
|
+
|
|
75
|
+
**👉 [https://arcblock.github.io/ux/?path=/story/data-display-terminal-player--recording-guide](https://arcblock.github.io/ux/?path=/story/data-display-terminal-player--recording-guide)**
|
|
76
|
+
|
|
77
|
+
- Drag & drop your `.cast` file
|
|
78
|
+
- Instant conversion and live preview
|
|
79
|
+
- Download the converted JSON file
|
|
80
|
+
- No command-line tools required!
|
|
81
|
+
|
|
82
|
+
## Advanced Configuration
|
|
83
|
+
|
|
84
|
+
### Custom Theme
|
|
85
|
+
|
|
86
|
+
```javascript
|
|
87
|
+
const customOptions = {
|
|
88
|
+
...recordingData.config,
|
|
89
|
+
theme: {
|
|
90
|
+
background: '#1e1e1e',
|
|
91
|
+
foreground: '#d4d4d4',
|
|
92
|
+
cursor: '#ffffff',
|
|
93
|
+
// ... more colors
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
<Player frames={frames} options={customOptions} />;
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Playback Options
|
|
101
|
+
|
|
102
|
+
```javascript
|
|
103
|
+
const playbackOptions = {
|
|
104
|
+
...recordingData.config,
|
|
105
|
+
frameDelay: 'auto', // or number in ms
|
|
106
|
+
maxIdleTime: 2000, // max delay between frames
|
|
107
|
+
repeat: true, // loop playback
|
|
108
|
+
autoplay: true, // start automatically
|
|
109
|
+
};
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Examples
|
|
113
|
+
|
|
114
|
+
Check out our **Storybook demos**:
|
|
115
|
+
|
|
116
|
+
- [Basic Player](https://arcblock.github.io/ux/?path=/story/data-display-terminal-player--player)
|
|
117
|
+
- [Auto Play](https://arcblock.github.io/ux/?path=/story/data-display-terminal-player--auto-play)
|
|
118
|
+
- [Loop Mode](https://arcblock.github.io/ux/?path=/story/data-display-terminal-player--loop)
|
|
119
|
+
- [Recording Guide](https://arcblock.github.io/ux/?path=/story/data-display-terminal-player--recording-guide)
|
|
120
|
+
|
|
121
|
+
## Data Format
|
|
122
|
+
|
|
123
|
+
The Player expects data in this format:
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"config": {
|
|
128
|
+
"cols": 80,
|
|
129
|
+
"rows": 24,
|
|
130
|
+
"frameDelay": "auto",
|
|
131
|
+
"theme": {
|
|
132
|
+
/* terminal colors */
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
"records": [
|
|
136
|
+
{ "delay": 100, "content": "Hello World!\n" },
|
|
137
|
+
{ "delay": 500, "content": "Next command...\n" }
|
|
138
|
+
]
|
|
18
139
|
}
|
|
19
140
|
```
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
Apache 2.0
|
package/lib/Player.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { jsxs as f, jsx as n } from "react/jsx-runtime";
|
|
2
|
-
import { useRef as
|
|
3
|
-
import
|
|
4
|
-
import H from "
|
|
5
|
-
import { useSize as I } from "ahooks";
|
|
2
|
+
import { useRef as b, useState as A, useReducer as O, useEffect as x } from "react";
|
|
3
|
+
import o from "prop-types";
|
|
4
|
+
import { useSize as W, useInterval as H } from "ahooks";
|
|
6
5
|
import y from "lodash/isUndefined";
|
|
7
6
|
import h from "lodash/noop";
|
|
8
|
-
import
|
|
9
|
-
import "./
|
|
10
|
-
import { defaultOptions as
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
import I from "./Terminal.js";
|
|
8
|
+
import { PlayerRoot as L } from "./styles.js";
|
|
9
|
+
import { defaultOptions as _, formatFrames as U, defaultState as Y, isFrameAt as N, findFrameAt as F, getPlayerClass as X, getFrameClass as $, formatTime as G } from "./util.js";
|
|
10
|
+
const K = 8;
|
|
11
|
+
function Q({ ...P }) {
|
|
12
|
+
const i = Object.assign({}, P);
|
|
13
|
+
y(i.onComplete) && (i.onComplete = h), y(i.onStart) && (i.onStart = h), y(i.onStop) && (i.onStop = h), y(i.onPause) && (i.onPause = h), y(i.onTick) && (i.onTick = h), y(i.onJump) && (i.onJump = h);
|
|
14
|
+
const t = Object.assign({}, _, i.options), { frames: u, totalDuration: m } = U(i.frames, t), R = {
|
|
15
15
|
cols: t.cols,
|
|
16
16
|
rows: t.rows,
|
|
17
17
|
cursorStyle: t.cursorStyle,
|
|
@@ -20,101 +20,81 @@ function K({ ...P }) {
|
|
|
20
20
|
lineHeight: t.lineHeight,
|
|
21
21
|
letterSpacing: t.letterSpacing,
|
|
22
22
|
allowTransparency: !0,
|
|
23
|
-
scrollback: 0
|
|
24
|
-
|
|
25
|
-
},
|
|
23
|
+
scrollback: 0,
|
|
24
|
+
theme: t.theme || {}
|
|
25
|
+
}, C = (a, e) => {
|
|
26
26
|
switch (e.type) {
|
|
27
27
|
case "jump":
|
|
28
|
-
return { ...
|
|
28
|
+
return { ...a, isPlaying: !1, ...e.payload };
|
|
29
29
|
case "start":
|
|
30
|
-
return { ...
|
|
30
|
+
return { ...a, isStarted: !0, lastTickTime: Date.now() };
|
|
31
31
|
case "play":
|
|
32
|
-
return {
|
|
33
|
-
...i,
|
|
34
|
-
isPlaying: !0,
|
|
35
|
-
lastTickTime: Date.now(),
|
|
36
|
-
...e.payload
|
|
37
|
-
};
|
|
32
|
+
return { ...a, isPlaying: !0, lastTickTime: Date.now(), ...e.payload };
|
|
38
33
|
case "pause":
|
|
39
|
-
return { ...
|
|
34
|
+
return { ...a, isPlaying: !1, ...e.payload };
|
|
40
35
|
case "tickStart":
|
|
41
|
-
return {
|
|
42
|
-
...i,
|
|
43
|
-
isRendering: !0,
|
|
44
|
-
lastTickTime: Date.now(),
|
|
45
|
-
...e.payload
|
|
46
|
-
};
|
|
36
|
+
return { ...a, isRendering: !0, lastTickTime: Date.now(), ...e.payload };
|
|
47
37
|
case "tickEnd":
|
|
48
|
-
return {
|
|
49
|
-
...i,
|
|
50
|
-
isRendering: !1,
|
|
51
|
-
lastTickTime: Date.now(),
|
|
52
|
-
...e.payload
|
|
53
|
-
};
|
|
38
|
+
return { ...a, isRendering: !1, lastTickTime: Date.now(), ...e.payload };
|
|
54
39
|
case "reset":
|
|
55
|
-
return {
|
|
56
|
-
...i,
|
|
57
|
-
currentFrame: 0,
|
|
58
|
-
currentTime: 0,
|
|
59
|
-
...e.payload
|
|
60
|
-
};
|
|
40
|
+
return { ...a, currentFrame: 0, currentTime: 0, ...e.payload };
|
|
61
41
|
default:
|
|
62
|
-
return { ...
|
|
42
|
+
return { ...a, lastTickTime: Date.now(), ...e.payload };
|
|
63
43
|
}
|
|
64
|
-
},
|
|
44
|
+
}, c = b(null), k = b(null), d = b(null), [B, D] = A(0), M = W(document.body), [r, l] = O(C, Y), j = M?.width || 0;
|
|
65
45
|
x(() => {
|
|
66
|
-
if (
|
|
46
|
+
if (c.current)
|
|
67
47
|
try {
|
|
68
|
-
const
|
|
48
|
+
const a = 8.03305785123967, e = d.current.getBoundingClientRect();
|
|
69
49
|
let s = e.x < 0 ? e.width + e.x : e.width;
|
|
70
50
|
if (d.current.parentNode) {
|
|
71
|
-
const
|
|
72
|
-
e.width >
|
|
51
|
+
const v = d.current.parentNode.getBoundingClientRect();
|
|
52
|
+
e.width > v.width && (s = v.width);
|
|
73
53
|
}
|
|
74
54
|
t.controls && (s -= 12);
|
|
75
|
-
const T = Math.ceil(s /
|
|
76
|
-
|
|
55
|
+
const T = Math.ceil(s / a), J = Math.min(Math.max(T, 40), t.cols);
|
|
56
|
+
c.current.resize(J, t.rows);
|
|
77
57
|
} catch {
|
|
78
58
|
}
|
|
79
59
|
}, [j, B]);
|
|
80
|
-
const
|
|
81
|
-
const s = u[
|
|
82
|
-
s.content && (r.requireReset &&
|
|
60
|
+
const w = (a, e) => {
|
|
61
|
+
const s = u[a];
|
|
62
|
+
s && s.content && (r.requireReset && c.current.reset(), c.current.write(s.content, () => {
|
|
83
63
|
typeof e == "function" && e();
|
|
84
64
|
}));
|
|
85
|
-
}, p = (
|
|
86
|
-
typeof a
|
|
87
|
-
},
|
|
88
|
-
|
|
89
|
-
const e = F(u,
|
|
65
|
+
}, p = (a) => {
|
|
66
|
+
typeof i[a] == "function" && i[a]({ state: r, frames: u, options: t });
|
|
67
|
+
}, S = (a) => {
|
|
68
|
+
c.current.reset();
|
|
69
|
+
const e = F(u, a);
|
|
90
70
|
for (let s = 0; s < e; s++)
|
|
91
|
-
|
|
92
|
-
},
|
|
93
|
-
if (!k.current || !
|
|
71
|
+
w(s);
|
|
72
|
+
}, E = (a) => {
|
|
73
|
+
if (!k.current || !c.current || !r.isStarted)
|
|
94
74
|
return !1;
|
|
95
|
-
const e = k.current.getBoundingClientRect().width, s =
|
|
96
|
-
return
|
|
97
|
-
}, g = () => (r.isStarted === !1 && (
|
|
75
|
+
const e = k.current.getBoundingClientRect().width, s = a.nativeEvent.offsetX, T = Math.floor(m * s / e);
|
|
76
|
+
return l({ type: "jump", payload: { currentTime: T } }), S(T), p("onJump"), !1;
|
|
77
|
+
}, g = () => (r.isStarted === !1 && (l({ type: "start" }), c.current.reset()), l({ type: "play" }), p("onStart"), !1), z = () => (l({ type: "pause" }), p("onPause"), !1), q = () => (r.currentFrame === u.length - 1 && r.currentTime === m && (l({ type: "reset" }), c.current.reset()), p("onPlay"), g());
|
|
98
78
|
return x(() => {
|
|
99
|
-
if (
|
|
79
|
+
if (c.current) {
|
|
100
80
|
if (d.current)
|
|
101
81
|
try {
|
|
102
82
|
D(d.current.getBoundingClientRect().width);
|
|
103
83
|
} catch {
|
|
104
84
|
}
|
|
105
|
-
t.autoplay ? g() :
|
|
85
|
+
t.autoplay ? g() : S(Math.min(Math.abs(t.thumbnailTime), m));
|
|
106
86
|
}
|
|
107
87
|
}, []), H(
|
|
108
88
|
() => {
|
|
109
89
|
if (r.isRendering)
|
|
110
90
|
return !1;
|
|
111
|
-
const
|
|
112
|
-
r.currentTime < m && (e.currentTime = r.currentTime +
|
|
113
|
-
const s =
|
|
114
|
-
return r.currentFrame !== -1 && s ?
|
|
91
|
+
const a = Date.now() - r.lastTickTime, e = {};
|
|
92
|
+
r.currentTime < m && (e.currentTime = r.currentTime + a), r.currentTime > m && (e.currentTime = m);
|
|
93
|
+
const s = N(u, e.currentTime, r.currentFrame);
|
|
94
|
+
return r.currentFrame !== -1 && s ? l({ type: "tick", payload: e }) : r.currentFrame === u.length - 1 ? (p("onComplete"), t.repeat ? l({ type: "reset", payload: e }) : (e.currentTime = 0, e.currentFrame = 0, e.requireReset = !0, e.isStarted = !1, l({ type: "pause", payload: e }))) : (e?.currentTime != null && r?.currentFrame != null && N(e.currentTime, r.currentFrame + 1) ? e.currentFrame = r.currentFrame + 1 : e.currentFrame = F(u, e.currentTime), l({ type: "tickStart", payload: e }), w(e.currentFrame, () => (r.requireReset && (e.requireReset = !1), l({ type: "tickEnd", payload: e }), p("onTick"))));
|
|
115
95
|
},
|
|
116
|
-
r.isPlaying ?
|
|
117
|
-
), t.controls && (t.frameBox.title = null, t.frameBox.type = null, t.frameBox.style = {}, t.theme
|
|
96
|
+
r.isPlaying ? K : null
|
|
97
|
+
), t.controls && (t.frameBox.title = null, t.frameBox.type = null, t.frameBox.style = {}, t.theme?.background === "transparent" ? t.frameBox.style.background = "black" : t.theme?.background && (t.frameBox.style.background = t.theme.background), t.frameBox.style.padding = "10px", t.frameBox.style.paddingBottom = "40px"), /* @__PURE__ */ f(L, { className: X(t, r), ref: d, children: [
|
|
118
98
|
/* @__PURE__ */ n("div", { className: "cover", onClick: g }),
|
|
119
99
|
/* @__PURE__ */ n("div", { className: "start", onClick: g, children: /* @__PURE__ */ f("svg", { style: { enableBackground: "new 0 0 30 30" }, viewBox: "0 0 30 30", children: [
|
|
120
100
|
/* @__PURE__ */ n("polygon", { points: "6.583,3.186 5,4.004 5,15 26,15 26.483,14.128 " }),
|
|
@@ -132,27 +112,36 @@ function K({ ...P }) {
|
|
|
132
112
|
] }),
|
|
133
113
|
/* @__PURE__ */ n("div", { className: "title", children: t.frameBox.title || "" })
|
|
134
114
|
] }),
|
|
135
|
-
/* @__PURE__ */ n("div", { className: "terminal-body", children: /* @__PURE__ */ n(
|
|
115
|
+
/* @__PURE__ */ n("div", { className: "terminal-body", children: /* @__PURE__ */ n(I, { ref: c, options: R }) })
|
|
136
116
|
] }) }),
|
|
137
117
|
/* @__PURE__ */ f("div", { className: "controller", children: [
|
|
138
|
-
r.isPlaying && /* @__PURE__ */ n("div", { className: "pause", onClick:
|
|
139
|
-
!r.isPlaying && r.isStarted && /* @__PURE__ */ n("div", { className: "play", onClick:
|
|
118
|
+
r.isPlaying && /* @__PURE__ */ n("div", { className: "pause", onClick: z, title: "Pause", children: /* @__PURE__ */ n("span", { className: "icon" }) }),
|
|
119
|
+
!r.isPlaying && r.isStarted && /* @__PURE__ */ n("div", { className: "play", onClick: q, title: "Play", children: /* @__PURE__ */ n("span", { className: "icon" }) }),
|
|
140
120
|
!r.isPlaying && !r.isStarted && /* @__PURE__ */ n("div", { className: "play", onClick: g, title: "Start", children: /* @__PURE__ */ n("span", { className: "icon" }) }),
|
|
141
121
|
/* @__PURE__ */ n("div", { className: "timer", children: G(r.currentTime) }),
|
|
142
|
-
/* @__PURE__ */ n("div", { className: "progressbar-wrapper", children: /* @__PURE__ */ n("div", { className: "progressbar", ref: k, onClick:
|
|
122
|
+
/* @__PURE__ */ n("div", { className: "progressbar-wrapper", children: /* @__PURE__ */ n("div", { className: "progressbar", ref: k, onClick: E, children: /* @__PURE__ */ n("div", { className: "progress", style: { width: `${r.currentTime / m * 100}%` } }) }) })
|
|
143
123
|
] })
|
|
144
124
|
] });
|
|
145
125
|
}
|
|
146
|
-
|
|
147
|
-
frames:
|
|
148
|
-
options:
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
126
|
+
Q.propTypes = {
|
|
127
|
+
frames: o.array.isRequired,
|
|
128
|
+
options: o.shape({
|
|
129
|
+
autoplay: o.bool,
|
|
130
|
+
repeat: o.bool,
|
|
131
|
+
controls: o.bool,
|
|
132
|
+
frameBox: o.object,
|
|
133
|
+
theme: o.object,
|
|
134
|
+
cols: o.number,
|
|
135
|
+
rows: o.number
|
|
136
|
+
}).isRequired,
|
|
137
|
+
onComplete: o.func,
|
|
138
|
+
onStart: o.func,
|
|
139
|
+
onStop: o.func,
|
|
140
|
+
onPause: o.func,
|
|
141
|
+
onTick: o.func,
|
|
142
|
+
onJump: o.func
|
|
155
143
|
};
|
|
156
144
|
export {
|
|
157
|
-
K as
|
|
145
|
+
K as PLAYER_FRAME_DELAY,
|
|
146
|
+
Q as default
|
|
158
147
|
};
|
package/lib/Terminal.js
CHANGED
|
@@ -6,8 +6,8 @@ import { WebLinksAddon as a } from "xterm-addon-web-links";
|
|
|
6
6
|
import { FitAddon as d } from "xterm-addon-fit";
|
|
7
7
|
import p from "lodash/debounce";
|
|
8
8
|
import s from "lodash/noop";
|
|
9
|
-
import "./
|
|
10
|
-
class
|
|
9
|
+
import { TerminalRoot as f } from "./styles.js";
|
|
10
|
+
class x extends m.Component {
|
|
11
11
|
xterm = null;
|
|
12
12
|
container = null;
|
|
13
13
|
componentDidMount() {
|
|
@@ -61,13 +61,19 @@ class f extends m.Component {
|
|
|
61
61
|
}
|
|
62
62
|
render() {
|
|
63
63
|
const { className: t = "", style: e = {} } = this.props, r = ["react-xterm", t].filter(Boolean).join(" ");
|
|
64
|
-
return (
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
return /* @__PURE__ */ n(
|
|
65
|
+
f,
|
|
66
|
+
{
|
|
67
|
+
ref: (o) => {
|
|
68
|
+
this.container = o;
|
|
69
|
+
},
|
|
70
|
+
className: r,
|
|
71
|
+
style: e
|
|
72
|
+
}
|
|
67
73
|
);
|
|
68
74
|
}
|
|
69
75
|
}
|
|
70
|
-
|
|
76
|
+
x.propTypes = {
|
|
71
77
|
onData: i.func,
|
|
72
78
|
onRender: i.func,
|
|
73
79
|
options: i.object,
|
|
@@ -76,5 +82,5 @@ f.propTypes = {
|
|
|
76
82
|
style: i.object
|
|
77
83
|
};
|
|
78
84
|
export {
|
|
79
|
-
|
|
85
|
+
x as default
|
|
80
86
|
};
|