This simplifies the way that `InteractiveConsole` does stream redirections and adds stream redirection for `stdin`. I also moved `banner` from a class method of `InteractiveConsole` to a module level constant `BANNER`.
Rearrange InteractiveConsole stream redirections. This simplifies the way that `InteractiveConsole` does stream redirections and adds stream redirection for `stdin`. I also moved `banner` from a class method of `InteractiveConsole` to a module level constant `BANNER`.
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <script src="https://cdn.jsdelivr.net/npm/jquery"></script>
    <script src="https://cdn.jsdelivr.net/npm/jquery.terminal@2.23.0/js/jquery.terminal.min.js"></script>
    <link href="https://cdn.jsdelivr.net/npm/jquery.terminal@2.23.0/css/jquery.terminal.min.css" rel="stylesheet"/>
    <script src="{{ PYODIDE_BASE_URL }}pyodide.js"></script>
    <style>
      .terminal { --size: 1.5; }
    </style>
  </head>
  <body>
    <script>
      function sleep(s){
        return new Promise(resolve => setTimeout(resolve, s));
      }
      async function main() {
        globalThis.pyodide = await loadPyodide({ indexURL : '{{ PYODIDE_BASE_URL }}' });
        let namespace = pyodide.globals.get("dict")();
        pyodide.runPython(`
            import sys
            import js
            from pyodide import console
            import __main__

            class PyConsole(console._InteractiveConsole):
                def __init__(self):
                    super().__init__(
                        __main__.__dict__,
                        persistent_stream_redirection=False,
                    )

                def banner(self):
                    return f"Welcome to the Pyodide terminal emulator 🐍\\n{super().banner()}"


            js.pyconsole = PyConsole()
        `, namespace);
        namespace.destroy();

        let ps1 = '>>> ', ps2 = '... ';

        async function lock(){
          let resolve;
          let ready = term.ready;
          term.ready = new Promise(res => resolve = res);
          await ready;
          return resolve;
        }

        async function interpreter(command) {
          let unlock = await lock();
          try {
            term.pause();
            // multiline should be splitted (useful when pasting)
            for( const c of command.split('\n') ) {
              let run_complete = pyconsole.run_complete;
              try {
                  const incomplete = pyconsole.push(c);
                  term.set_prompt(incomplete ? ps2 : ps1);
                  let r = await run_complete;
                  if(pyodide.isPyProxy(r)){
                    r.destroy();
                  }
                } catch(e){
                  if(e.name !== "PythonError"){
                    term.error(e);
                    throw e;
                  }
                }
                run_complete.destroy();
            }
          } finally {
            term.resume();
            await sleep(10);
            unlock();
          }
        }

        let term = $('body').terminal(
          interpreter,
          {
            greetings: pyconsole.banner(),
            prompt: ps1,
            completionEscape: false,
            completion: function(command, callback) {
              callback(pyconsole.complete(command).toJs()[0]);
            }
          }
        );
        window.term = term;
        pyconsole.stdout_callback = s => term.echo(s, {newline : false});
        pyconsole.stderr_callback = s => {
            term.error(s.trimEnd());
        };
        term.ready = Promise.resolve();
        pyodide._module.on_fatal = async (e) => {
          term.error("Pyodide has suffered a fatal error. Please report this to the Pyodide maintainers.");
          term.error("The cause of the fatal error was:");
          term.error(e);
          term.error("Look in the browser console for more details.");
          await term.ready;
          term.pause();
          await sleep(15);
          term.pause();
        };
      }
      window.console_ready = main();
    </script>
  </body>
</html>

<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <script src="https://cdn.jsdelivr.net/npm/jquery"></script>
    <script src="https://cdn.jsdelivr.net/npm/jquery.terminal@2.23.0/js/jquery.terminal.min.js"></script>
    <link href="https://cdn.jsdelivr.net/npm/jquery.terminal@2.23.0/css/jquery.terminal.min.css" rel="stylesheet"/>
    <script src="{{ PYODIDE_BASE_URL }}pyodide.js"></script>
    <style>
      .terminal { --size: 1.5; }
    </style>
  </head>
  <body>
    <script>
      function sleep(s){
        return new Promise(resolve => setTimeout(resolve, s));
      }
      async function main() {
        globalThis.pyodide = await loadPyodide({ indexURL : '{{ PYODIDE_BASE_URL }}' });
        let namespace = pyodide.globals.get("dict")();
        pyodide.runPython(`
            import sys
            import js
            from pyodide.console import _InteractiveConsole, BANNER
            import __main__

            class PyConsole(_InteractiveConsole):
                def __init__(self):
                    super().__init__(
                        __main__.__dict__,
                        persistent_stream_redirection=False,
                    )

            BANNER = "Welcome to the Pyodide terminal emulator 🐍\\n" + BANNER
            js.pyconsole = PyConsole()
        `, namespace);
        let banner = namespace.get("BANNER");
        namespace.destroy();

        let ps1 = '>>> ', ps2 = '... ';

        async function lock(){
          let resolve;
          let ready = term.ready;
          term.ready = new Promise(res => resolve = res);
          await ready;
          return resolve;
        }

        async function interpreter(command) {
          let unlock = await lock();
          try {
            term.pause();
            // multiline should be splitted (useful when pasting)
            for( const c of command.split('\n') ) {
              let run_complete = pyconsole.run_complete;
              try {
                  const incomplete = pyconsole.push(c);
                  term.set_prompt(incomplete ? ps2 : ps1);
                  let r = await run_complete;
                  if(pyodide.isPyProxy(r)){
                    r.destroy();
                  }
                } catch(e){
                  if(e.name !== "PythonError"){
                    term.error(e);
                    throw e;
                  }
                }
                run_complete.destroy();
            }
          } finally {
            term.resume();
            await sleep(10);
            unlock();
          }
        }

        let term = $('body').terminal(
          interpreter,
          {
            greetings: banner,
            prompt: ps1,
            completionEscape: false,
            completion: function(command, callback) {
              callback(pyconsole.complete(command).toJs()[0]);
            }
          }
        );
        window.term = term;
        pyconsole.stdout_callback = s => term.echo(s, {newline : false});
        let url_re = /(\bhttps?:\/\/(?:(?:(?!&[^;]+;)|(?=&amp;))[^\s"'<>\][)])+)/gi;
        pyconsole.stderr_callback = function(s){
          if(s.endsWith("\n")){
            term.error(s.slice(0, -1));
            return;
          }
          if(s === ""){
            return;
          }
          s = $.terminal.escape_brackets(s).
                    replace(/\\$/, '&#92;').
                    replace(url_re, ']$1[[;;;terminal-error]');
          s = `[[;;;terminal-error]${s}]`.replace(/(\n\s*)]/, "]$1");
          term.echo(s, { newline : false });
        }
        term.ready = Promise.resolve();
        pyodide._module.on_fatal = async (e) => {
          term.error("Pyodide has suffered a fatal error. Please report this to the Pyodide maintainers.");
          term.error("The cause of the fatal error was:");
          term.error(e);
          term.error("Look in the browser console for more details.");
          await term.ready;
          term.pause();
          await sleep(15);
          term.pause();
        };
      }
      window.console_ready = main();
    </script>
  </body>
</html>
