Skip to content

Commit a28af7c

Browse files
committed
terminal timer
1 parent e2dd98d commit a28af7c

File tree

1 file changed

+99
-51
lines changed

1 file changed

+99
-51
lines changed

src/components/AnimatedTerminal.tsx

Lines changed: 99 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useState, useMemo, useRef } from 'react';
1+
import { useEffect, useState, useMemo, useRef } from "react";
22

33
type TerminalExample = {
44
name: string;
@@ -14,16 +14,8 @@ interface AnimatedTerminalProps {
1414
* and split it into lines
1515
*/
1616
function dedentAndSplit(text: string): string[] {
17-
const lines = text.split('\n');
18-
19-
// Trim leading and trailing empty lines
20-
while (lines.length > 0 && lines[0].trim().length === 0) {
21-
lines.shift();
22-
}
23-
while (lines.length > 0 && lines[lines.length - 1].trim().length === 0) {
24-
lines.pop();
25-
}
26-
17+
const lines = text.split("\n");
18+
2719
// Find the minimum indentation (excluding empty lines)
2820
let minIndent = Infinity;
2921
for (const line of lines) {
@@ -32,24 +24,24 @@ function dedentAndSplit(text: string): string[] {
3224
minIndent = Math.min(minIndent, indent);
3325
}
3426
}
35-
27+
3628
// If no indentation found, return lines as-is
3729
if (minIndent === Infinity) {
3830
return lines;
3931
}
40-
32+
4133
// Remove the common indentation from all lines
42-
return lines.map(line => {
34+
return lines.map((line) => {
4335
if (line.trim().length === 0) {
44-
return '';
36+
return "";
4537
}
4638
return line.slice(minIndent);
4739
});
4840
}
4941

50-
const getExamples = (version: string = '9.8.0'): TerminalExample[] => [
42+
const getExamples = (version: string = "9.8.0"): TerminalExample[] => [
5143
{
52-
name: 'NumPy Basics',
44+
name: "NumPy Basics",
5345
lines: dedentAndSplit(`
5446
$ ipython
5547
IPython ${version} -- An enhanced Interactive Python
@@ -64,7 +56,7 @@ const getExamples = (version: string = '9.8.0'): TerminalExample[] => [
6456
`),
6557
},
6658
{
67-
name: 'Performance & Plotting',
59+
name: "Performance & Plotting",
6860
lines: dedentAndSplit(`
6961
$ ipython
7062
IPython ${version} -- An enhanced Interactive Python
@@ -78,7 +70,7 @@ const getExamples = (version: string = '9.8.0'): TerminalExample[] => [
7870
`),
7971
},
8072
{
81-
name: 'Functions',
73+
name: "Functions",
8274
lines: dedentAndSplit(`
8375
$ ipython
8476
IPython ${version} -- An enhanced Interactive Python
@@ -93,6 +85,36 @@ const getExamples = (version: string = '9.8.0'): TerminalExample[] => [
9385
Out[2]: 55
9486
`),
9587
},
88+
{
89+
name: "Async",
90+
lines: dedentAndSplit(`
91+
$ ipython
92+
IPython ${version} -- An enhanced Interactive Python
93+
94+
# we will use await at top level !
95+
96+
In [1]: import asyncio
97+
98+
In [2]: async def fetch_data():
99+
...: await asyncio.sleep(0.1)
100+
...: return "Data fetched!"
101+
...:
102+
103+
In [3]: await fetch_data()
104+
Out[3]: 'Data fetched!'
105+
106+
In [4]: async def process_items(items):
107+
...: results = []
108+
...: for item in items:
109+
...: await asyncio.sleep(0.05)
110+
...: results.append(item * 2)
111+
...: return results
112+
...:
113+
114+
In [5]: await process_items([1, 2, 3])
115+
Out[5]: [2, 4, 6]
116+
`),
117+
},
96118
];
97119

98120
const EXAMPLE_DELAY = 4000; // Delay before starting next example
@@ -123,17 +145,17 @@ export default function AnimatedTerminal({ version }: AnimatedTerminalProps) {
123145
setIsVisible(visible);
124146
};
125147

126-
document.addEventListener('visibilitychange', handleVisibilityChange);
127-
window.addEventListener('blur', handleWindowBlur);
128-
window.addEventListener('focus', handleWindowFocus);
148+
document.addEventListener("visibilitychange", handleVisibilityChange);
149+
window.addEventListener("blur", handleWindowBlur);
150+
window.addEventListener("focus", handleWindowFocus);
129151
const initialVisible = !document.hidden && document.hasFocus();
130152
isVisibleRef.current = initialVisible;
131153
setIsVisible(initialVisible);
132154

133155
return () => {
134-
document.removeEventListener('visibilitychange', handleVisibilityChange);
135-
window.removeEventListener('blur', handleWindowBlur);
136-
window.removeEventListener('focus', handleWindowFocus);
156+
document.removeEventListener("visibilitychange", handleVisibilityChange);
157+
window.removeEventListener("blur", handleWindowBlur);
158+
window.removeEventListener("focus", handleWindowFocus);
137159
};
138160
}, []);
139161

@@ -155,13 +177,22 @@ export default function AnimatedTerminal({ version }: AnimatedTerminalProps) {
155177
const example = examples[currentExample];
156178
if (!example) return;
157179

158-
const lineDelay = 400; // delay between lines appearing
180+
const baseLineDelay = 300; // delay between lines appearing
181+
const currentLine = example.lines[currentLineIndex];
182+
// Add 100ms delay for empty lines
183+
const lineDelay =
184+
currentLine && currentLine.trim().length === 0
185+
? baseLineDelay + 300
186+
: baseLineDelay;
159187

160188
if (currentLineIndex < example.lines.length) {
161189
// Show next line
162190
const timer = setTimeout(() => {
163191
if (isVisibleRef.current) {
164-
setDisplayedLines((prev) => [...prev, example.lines[currentLineIndex]]);
192+
setDisplayedLines((prev) => [
193+
...prev,
194+
example.lines[currentLineIndex],
195+
]);
165196
setCurrentLineIndex((prev) => prev + 1);
166197
}
167198
}, lineDelay);
@@ -181,58 +212,75 @@ export default function AnimatedTerminal({ version }: AnimatedTerminalProps) {
181212
}
182213
}, [currentExample, currentLineIndex, examples, isVisible]);
183214

184-
const getLinePrefix = (line: string): { prefix: string; content: string; prefixColor: string } => {
185-
if (line.startsWith('In [')) {
215+
const getLinePrefix = (
216+
line: string
217+
): { prefix: string; content: string; prefixColor: string } => {
218+
if (line.startsWith("In [")) {
186219
const match = line.match(/^(In \[\d+\]:\s*)(.*)$/);
187220
if (match) {
188-
return { prefix: match[1], content: match[2], prefixColor: 'text-theme-primary' };
221+
return {
222+
prefix: match[1],
223+
content: match[2],
224+
prefixColor: "text-theme-primary",
225+
};
189226
}
190227
}
191-
if (line.startsWith('Out[')) {
228+
if (line.startsWith("Out[")) {
192229
const match = line.match(/^(Out\[\d+\]:\s*)(.*)$/);
193230
if (match) {
194-
return { prefix: match[1], content: match[2], prefixColor: 'text-theme-accent' };
231+
return {
232+
prefix: match[1],
233+
content: match[2],
234+
prefixColor: "text-theme-accent",
235+
};
195236
}
196237
}
197-
if (line.startsWith(' ...:')) {
198-
return { prefix: ' ...: ', content: line.substring(8), prefixColor: 'text-gray-500 dark:text-gray-400' };
238+
if (line.startsWith(" ...:")) {
239+
return {
240+
prefix: " ...: ",
241+
content: line.substring(8),
242+
prefixColor: "text-gray-500 dark:text-gray-400",
243+
};
199244
}
200-
return { prefix: '', content: line, prefixColor: '' };
245+
return { prefix: "", content: line, prefixColor: "" };
201246
};
202247

203248
const getLineColor = (line: string): string => {
204-
if (line.startsWith('$')) {
205-
return 'text-theme-secondary';
249+
if (line.startsWith("$")) {
250+
return "text-theme-secondary";
206251
}
207-
if (line.startsWith('Out[')) {
208-
return 'text-theme-accent';
252+
if (line.startsWith("Out[")) {
253+
return "text-theme-accent";
209254
}
210-
if (line.startsWith('IPython') || line.includes('Type')) {
211-
return 'text-gray-600 dark:text-gray-300';
255+
if (line.startsWith("IPython") || line.includes("Type")) {
256+
return "text-gray-600 dark:text-gray-300";
212257
}
213-
if (line.trim() === '') {
214-
return 'text-gray-500 dark:text-gray-400';
258+
if (line.trim() === "") {
259+
return "text-gray-500 dark:text-gray-400";
215260
}
216-
return 'text-gray-700 dark:text-gray-300';
261+
return "text-gray-700 dark:text-gray-300";
217262
};
218263

219264
// Calculate max height based on the longest example
220-
const maxLines = Math.max(...examples.map(ex => ex.lines.length));
265+
const maxLines = Math.max(...examples.map((ex) => ex.lines.length));
221266
const lineHeight = 1.5; // rem (24px for text-sm)
222267
const padding = 1.5 * 2; // rem (top + bottom padding)
223268
const controlsHeight = 2.5; // rem (window controls height)
224269
const minHeight = `${maxLines * lineHeight + padding + controlsHeight}rem`;
225270

226271
return (
227272
<div>
228-
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg overflow-hidden font-mono text-sm" style={{ minHeight }}>
273+
<div
274+
className="bg-gray-50 dark:bg-gray-900 rounded-lg overflow-hidden font-mono text-sm"
275+
style={{ minHeight }}
276+
>
229277
{/* macOS Window Controls */}
230278
<div className="bg-gray-200 dark:bg-gray-800 border-b border-gray-300 dark:border-gray-700 px-4 py-2 flex items-center gap-2">
231279
<div className="w-3 h-3 rounded-full bg-red-500"></div>
232280
<div className="w-3 h-3 rounded-full bg-yellow-500"></div>
233281
<div className="w-3 h-3 rounded-full bg-green-500"></div>
234282
</div>
235-
283+
236284
{/* Terminal Content */}
237285
<div className="p-6 whitespace-pre">
238286
{displayedLines.map((line, index) => {
@@ -242,7 +290,7 @@ export default function AnimatedTerminal({ version }: AnimatedTerminalProps) {
242290
return (
243291
<div key={index} className={`${lineColor} whitespace-pre`}>
244292
{prefix && <span className={prefixColor}>{prefix}</span>}
245-
{content}
293+
{content || "\u00A0"}
246294
</div>
247295
);
248296
})}
@@ -251,7 +299,7 @@ export default function AnimatedTerminal({ version }: AnimatedTerminalProps) {
251299
)}
252300
</div>
253301
</div>
254-
302+
255303
{/* Indicator Dots */}
256304
<div className="flex justify-center items-center gap-2 mt-4">
257305
{examples.map((example, index) => (
@@ -260,8 +308,8 @@ export default function AnimatedTerminal({ version }: AnimatedTerminalProps) {
260308
onClick={() => switchToExample(index)}
261309
className={`transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-theme-primary focus:ring-offset-2 rounded-full ${
262310
index === currentExample
263-
? 'w-3 h-3 bg-theme-accent border-2 border-theme-primary scale-125'
264-
: 'w-2 h-2 bg-gray-400 dark:bg-gray-600 hover:bg-gray-500 dark:hover:bg-gray-500'
311+
? "w-3 h-3 bg-theme-accent border-2 border-theme-primary scale-125"
312+
: "w-2 h-2 bg-gray-400 dark:bg-gray-600 hover:bg-gray-500 dark:hover:bg-gray-500"
265313
}`}
266314
aria-label={`Go to ${example.name}`}
267315
title={example.name}

0 commit comments

Comments
 (0)