@@ -59,7 +59,12 @@ enum ManimShellEvent {
59
59
* execution has ended before we have detected the start of the ManimGL
60
60
* session.
61
61
*/
62
- MANIM_NOT_STARTED = 'manimglNotStarted'
62
+ MANIM_NOT_STARTED = 'manimglNotStarted' ,
63
+
64
+ /**
65
+ * Event emitted when the active shell is reset.
66
+ */
67
+ RESET = 'reset'
63
68
}
64
69
65
70
/**
@@ -81,6 +86,12 @@ export interface CommandExecutionEventHandler {
81
86
* of ANSI control codes.
82
87
*/
83
88
onData ?: ( data : string ) => void ;
89
+
90
+ /**
91
+ * Callback that is invoked when the manim shell is reset. This indicates
92
+ * that the event handler should clean up any resources.
93
+ */
94
+ onReset ?: ( ) => void ;
84
95
}
85
96
86
97
/**
@@ -263,6 +274,8 @@ export class ManimShell {
263
274
264
275
this . isExecutingCommand = true ;
265
276
Logger . debug ( "🔒 Command execution locked" ) ;
277
+ const resetListener = ( ) => { handler ?. onReset ?.( ) ; } ;
278
+ this . eventEmitter . on ( ManimShellEvent . RESET , resetListener ) ;
266
279
267
280
let shell : Terminal ;
268
281
if ( errorOnNoActiveShell ) {
@@ -285,6 +298,7 @@ export class ManimShell {
285
298
Logger . debug ( "🔓 Command execution unlocked" ) ;
286
299
this . isExecutingCommand = false ;
287
300
this . eventEmitter . off ( ManimShellEvent . DATA , dataListener ) ;
301
+ this . eventEmitter . off ( ManimShellEvent . RESET , resetListener ) ;
288
302
} ) ;
289
303
if ( waitUntilFinished ) {
290
304
Logger . debug ( `🕒 Waiting until command has finished: ${ command } ` ) ;
@@ -317,12 +331,14 @@ export class ManimShell {
317
331
const shouldAsk = await vscode . workspace . getConfiguration ( "manim-notebook" )
318
332
. get ( "confirmKillingActiveSceneToStartNewOne" ) ;
319
333
if ( shouldAsk ) {
334
+ Logger . debug ( "🔆 Active shell found, asking user to kill active scene" ) ;
320
335
if ( ! await this . doesUserWantToKillActiveScene ( ) ) {
336
+ Logger . debug ( "🔆 User didn't want to kill active scene" ) ;
321
337
return ;
322
338
}
323
339
}
324
340
Logger . debug ( "🔆 User confirmed to kill active scene" ) ;
325
- exitScene ( ) ;
341
+ await this . forceQuitActiveShell ( ) ;
326
342
}
327
343
this . activeShell = window . createTerminal ( ) ;
328
344
} else {
@@ -361,12 +377,39 @@ export class ManimShell {
361
377
this . iPythonCellCount = 0 ;
362
378
this . activeShell = null ;
363
379
this . shellWeTryToSpawnIn = null ;
380
+ this . eventEmitter . emit ( ManimShellEvent . RESET ) ;
364
381
this . eventEmitter . removeAllListeners ( ) ;
365
382
}
366
383
384
+ /**
385
+ * Forces the terminal to quit and thus the ManimGL session to end.
386
+ *
387
+ * Beforehand, this was implemented more gracefully by sending a keyboard
388
+ * interrupt (`Ctrl+C`), followed by the `exit()` command in the IPython
389
+ * session. However, on MacOS, the keyboard interrupt itself already exits
390
+ * from the entire IPython session and does not just interrupt the current
391
+ * running command (inside IPython) as would be expected.
392
+ * See https://github.com/3b1b/manim/discussions/2236
393
+ */
394
+ public async forceQuitActiveShell ( ) {
395
+ if ( this . activeShell ) {
396
+ Logger . debug ( "🔚 Force-quitting active shell" ) ;
397
+ let lastActiveShell = this . activeShell ;
398
+ await this . sendKeyboardInterrupt ( ) ;
399
+ lastActiveShell ?. dispose ( ) ;
400
+ // This is also taken care of when we detect that the shell has ended
401
+ // in the `onDidEndTerminalShellExecution` event handler. However,
402
+ // it doesn't harm to reset the active shell here as well just to
403
+ // be sure.
404
+ this . resetActiveShell ( ) ;
405
+ } else {
406
+ Logger . debug ( "🔚 No active shell found to force quit" ) ;
407
+ }
408
+ }
409
+
367
410
/**
368
411
* Ask the user if they want to kill the active scene. Might modify the
369
- * setting that control if the user should be asked in the future.
412
+ * setting that controls if the user should be asked in the future.
370
413
*
371
414
* @returns true if the user wants to kill the active scene, false otherwise.
372
415
*/
@@ -377,6 +420,9 @@ export class ManimShell {
377
420
const selection = await Window . showWarningMessage (
378
421
"We need to kill your Manim session to spawn a new one." ,
379
422
"Kill it" , KILL_IT_ALWAYS_OPTION , CANCEL_OPTION ) ;
423
+ if ( selection === undefined ) {
424
+ Logger . warn ( "❌ User selection undefined, but shouldn't be" ) ;
425
+ }
380
426
if ( selection === undefined || selection === CANCEL_OPTION ) {
381
427
return false ;
382
428
}
@@ -398,7 +444,10 @@ export class ManimShell {
398
444
* Manim session (IPython environment), is considered inactive.
399
445
*/
400
446
private hasActiveShell ( ) : boolean {
401
- return this . activeShell !== null && this . activeShell . exitStatus === undefined ;
447
+ const hasActiveShell =
448
+ this . activeShell !== null && this . activeShell . exitStatus === undefined ;
449
+ Logger . debug ( `👩💻 Has active shell?: ${ hasActiveShell } ` ) ;
450
+ return hasActiveShell ;
402
451
}
403
452
404
453
/**
@@ -492,7 +541,8 @@ export class ManimShell {
492
541
493
542
private async sendKeyboardInterrupt ( ) {
494
543
Logger . debug ( "💨 Sending keyboard interrupt to terminal" ) ;
495
- this . activeShell ?. sendText ( '\x03' ) ; // send `Ctrl+C`
544
+ await this . activeShell ?. sendText ( '\x03' ) ; // send `Ctrl+C`
545
+ await new Promise ( resolve => setTimeout ( resolve , 250 ) ) ;
496
546
}
497
547
498
548
/**
@@ -514,17 +564,26 @@ export class ManimShell {
514
564
for await ( const data of withoutAnsiCodes ( stream ) ) {
515
565
Logger . trace ( `🧾 Terminal data:\n${ data } ` ) ;
516
566
517
- this . eventEmitter . emit ( ManimShellEvent . DATA , data ) ;
518
-
519
567
if ( data . match ( MANIM_WELCOME_REGEX ) ) {
568
+ // Manim detected in new terminal
520
569
if ( this . activeShell && this . activeShell !== event . terminal ) {
521
570
Logger . debug ( "👋 Manim detected in new terminal, exiting old scene" ) ;
522
- exitScene ( ) ;
571
+ await this . forceQuitActiveShell ( ) ;
523
572
}
524
573
Logger . debug ( "👋 Manim welcome string detected" ) ;
525
574
this . activeShell = event . terminal ;
526
575
}
527
576
577
+ // Subsequent data handling should only occur for the
578
+ // currently active shell. This is important during
579
+ // overlapping commands, e.g. when one shell is exited
580
+ // and another one started.
581
+ if ( this . activeShell !== event . terminal ) {
582
+ continue ;
583
+ }
584
+
585
+ this . eventEmitter . emit ( ManimShellEvent . DATA , data ) ;
586
+
528
587
if ( data . match ( KEYBOARD_INTERRUPT_REGEX ) ) {
529
588
Logger . debug ( "🛑 Keyboard interrupt detected" ) ;
530
589
this . eventEmitter . emit ( ManimShellEvent . KEYBOARD_INTERRUPT ) ;
0 commit comments