forked from vercel/hyperpower
-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
251 lines (230 loc) · 9.09 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
const throttle = require('lodash.throttle');
const Color = require('color');
const nameToHex = require('convert-css-color-name-to-hex');
const toHex = (str) => Color(nameToHex(str)).hexString();
const values = require('lodash.values');
// Constants for the particle simulation.
const MAX_PARTICLES = 500;
const PARTICLE_NUM_RANGE = () => 5 + Math.round(Math.random() * 5);
const PARTICLE_GRAVITY = 0.075;
const PARTICLE_ALPHA_FADEOUT = 0.96;
const PARTICLE_VELOCITY_RANGE = {
x: [-1, 1],
y: [-3.5, -1.5]
};
// Our extension's custom redux middleware. Here we can intercept redux actions and respond to them.
exports.middleware = (store) => (next) => (action) => {
// the redux `action` object contains a loose `type` string, the
// 'SESSION_ADD_DATA' type identifier corresponds to an action in which
// the terminal wants to output information to the GUI.
if ('SESSION_ADD_DATA' === action.type) {
// 'SESSION_ADD_DATA' actions hold the output text data in the `data` key.
const { data } = action;
if (detectWowCommand(data)) {
// Here, we are responding to 'wow' being input at the prompt. Since we don't
// want the "unknown command" output being displayed to the user, we don't thunk the next
// middleware by calling `next(action)`. Instead, we dispatch a new action 'WOW_MODE_TOGGLE'.
store.dispatch({
type: 'WOW_MODE_TOGGLE'
});
} else {
next(action);
}
} else {
next(action);
}
};
// This function performs regex matching on expected shell output for 'wow' being input
// at the command line. Currently it supports output from bash, zsh, fish, cmd and powershell.
function detectWowCommand(data) {
const patterns = [
'wow: command not found',
'command not found: wow',
'Unknown command \'wow\'',
'\'wow\' is not recognized.*'
];
return new RegExp('(' + patterns.join(')|(') + ')').test(data)
}
// Our extension's custom ui state reducer. Here we can listen for our 'WOW_MODE_TOGGLE' action
// and modify the state accordingly.
exports.reduceUI = (state, action) => {
switch (action.type) {
case 'WOW_MODE_TOGGLE':
// Toggle wow mode!
return state.set('wowMode', !state.wowMode);
}
return state;
};
// Our extension's state property mapper. Here we can pass the ui's `wowMode` state
// into the terminal component's properties.
exports.mapTermsState = (state, map) => {
return Object.assign(map, {
wowMode: state.ui.wowMode
});
};
// We'll need to handle reflecting the `wowMode` property down through possible nested
// parent/children terminal hierarchies.
const passProps = (uid, parentProps, props) => {
return Object.assign(props, {
wowMode: parentProps.wowMode
});
}
exports.getTermGroupProps = passProps;
exports.getTermProps = passProps;
// The `decorateTerm` hook allows our extension to return a higher order react component.
// It supplies us with:
// - Term: The terminal component.
// - React: The enture React namespace.
// - notify: Helper function for displaying notifications in the operating system.
//
// The portions of this code dealing with the particle simulation are heavily based on:
// - https://atom.io/packages/power-mode
// - https://github.com/itszero/rage-power/blob/master/index.jsx
exports.decorateTerm = (Term, { React, notify }) => {
// Define and return our higher order component.
return class extends React.Component {
constructor (props, context) {
super(props, context);
// Since we'll be passing these functions around, we need to bind this
// to each.
this._drawFrame = this._drawFrame.bind(this);
this._resizeCanvas = this._resizeCanvas.bind(this);
this._onTerminal = this._onTerminal.bind(this);
this._onCursorChange = this._onCursorChange.bind(this);
this._shake = throttle(this._shake.bind(this), 100, { trailing: false });
this._spawnParticles = throttle(this._spawnParticles.bind(this), 25, { trailing: false });
// Initial particle state
this._particles = [];
// We'll set these up when the terminal is available in `_onTerminal`
this._div = null;
this._cursor = null;
this._observer = null;
this._canvas = null;
}
_onTerminal (term) {
if (this.props.onTerminal) this.props.onTerminal(term);
this._div = term.div_;
this._cursor = term.cursorNode_;
this._window = term.document_.defaultView;
// We'll need to observe cursor change events.
this._observer = new MutationObserver(this._onCursorChange);
this._observer.observe(this._cursor, {
attributes: true,
childList: false,
characterData: false
});
this._initCanvas();
}
// Set up our canvas element we'll use to do particle effects on.
_initCanvas () {
this._canvas = document.createElement('canvas');
this._canvas.style.position = 'absolute';
this._canvas.style.top = '0';
this._canvas.style.pointerEvents = 'none';
this._canvasContext = this._canvas.getContext('2d');
this._canvas.width = window.innerWidth;
this._canvas.height = window.innerHeight;
document.body.appendChild(this._canvas);
this._window.requestAnimationFrame(this._drawFrame);
this._window.addEventListener('resize', this._resizeCanvas);
}
_resizeCanvas () {
this._canvas.width = window.innerWidth;
this._canvas.height = window.innerHeight;
}
// Draw the next frame in the particle simulation.
_drawFrame () {
this._canvasContext.clearRect(0, 0, this._canvas.width, this._canvas.height);
this._particles.forEach((particle) => {
particle.velocity.y += PARTICLE_GRAVITY;
particle.x += particle.velocity.x;
particle.y += particle.velocity.y;
particle.alpha *= PARTICLE_ALPHA_FADEOUT;
this._canvasContext.fillStyle = `rgba(${particle.color.join(',')}, ${particle.alpha})`;
this._canvasContext.fillRect(Math.round(particle.x - 1), Math.round(particle.y - 1), 3, 3);
});
this._particles = this._particles
.slice(Math.max(this._particles.length - MAX_PARTICLES, 0))
.filter((particle) => particle.alpha > 0.1);
this._window.requestAnimationFrame(this._drawFrame);
}
// Pushes `PARTICLE_NUM_RANGE` new particles into the simulation.
_spawnParticles (x, y) {
// const { colors } = this.props;
const colors = this.props.wowMode
? values(this.props.colors).map(toHex)
: [toHex(this.props.cursorColor)];
const numParticles = PARTICLE_NUM_RANGE();
for (let i = 0; i < numParticles; i++) {
const colorCode = colors[i % colors.length];
const r = parseInt(colorCode.slice(1, 3), 16);
const g = parseInt(colorCode.slice(3, 5), 16);
const b = parseInt(colorCode.slice(5, 7), 16);
const color = [r, g, b];
this._particles.push(this._createParticle(x, y, color));
}
}
// Returns a particle of a specified color
// at some random offset from the input coordinates.
_createParticle (x, y, color) {
return {
x,
y: y,
alpha: 1,
color,
velocity: {
x: PARTICLE_VELOCITY_RANGE.x[0] + Math.random() *
(PARTICLE_VELOCITY_RANGE.x[1] - PARTICLE_VELOCITY_RANGE.x[0]),
y: PARTICLE_VELOCITY_RANGE.y[0] + Math.random() *
(PARTICLE_VELOCITY_RANGE.y[1] - PARTICLE_VELOCITY_RANGE.y[0])
}
};
}
// 'Shakes' the screen by applying a temporary translation
// to the terminal container.
_shake () {
// TODO: Maybe we should do this check in `_onCursorChange`?
if(!this.props.wowMode) return;
const intensity = 1 + 2 * Math.random();
const x = intensity * (Math.random() > 0.5 ? -1 : 1);
const y = intensity * (Math.random() > 0.5 ? -1 : 1);
this._div.style.transform = `translate3d(${x}px, ${y}px, 0)`;
setTimeout(() => {
if (this._div) this._div.style.transform = '';
}, 75);
}
_onCursorChange () {
this._shake();
// Get current coordinates of the cursor relative the container and
// spawn new articles.
const { top, left } = this._cursor.getBoundingClientRect();
const origin = this._div.getBoundingClientRect();
requestAnimationFrame(() => {
this._spawnParticles(left + origin.left, top + origin.top);
});
}
// Called when the props change, here we'll check if wow mode has gone
// on -> off or off -> on and notify the user accordingly.
componentWillReceiveProps (next) {
if (next.wowMode && !this.props.wowMode) {
notify('WOW such on');
} else if (!next.wowMode && this.props.wowMode) {
notify('WOW such off');
}
}
render () {
// Return the default Term component with our custom onTerminal closure
// setting up and managing the particle effects.
return React.createElement(Term, Object.assign({}, this.props, {
onTerminal: this._onTerminal
}));
}
componentWillUnmount () {
document.body.removeChild(this._canvas);
// Stop observing _onCursorChange
if (this._observer) {
this._observer.disconnect();
}
}
}
};