1- import { useEffect , useState , useMemo , useRef } from ' react' ;
1+ import { useEffect , useState , useMemo , useRef } from " react" ;
22
33type TerminalExample = {
44 name : string ;
@@ -14,16 +14,8 @@ interface AnimatedTerminalProps {
1414 * and split it into lines
1515 */
1616function 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
98120const 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 ( / ^ ( I n \[ \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 ( / ^ ( O u t \[ \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