https://github.com/python/cpython/commit/4f1f648c3bd36a875501778786bb8d718ab33869
commit: 4f1f648c3bd36a875501778786bb8d718ab33869
branch: 3.14
author: Miss Islington (bot) <31488909+miss-isling...@users.noreply.github.com>
committer: ambv <luk...@langa.pl>
date: 2025-07-22T13:25:35+02:00
summary:

[3.14] gh-136251: Improvements to WASM demo REPL (GH-136252) (GH-136977)

(cherry picked from commit d1d526afe7ce62c787b150652a2ba136cb949d74)

Co-authored-by: adam j hartz <a...@smatz.net>
Co-authored-by: Hood Chatham <roberthoodchat...@gmail.com>

files:
A Misc/NEWS.d/next/Tools-Demos/2025-07-05-15-10-42.gh-issue-136251.GRM6o8.rst
A Tools/wasm/emscripten/web_example/index.html
D Tools/wasm/emscripten/web_example/python.html
M Makefile.pre.in
M Python/emscripten_syscalls.c
M Tools/wasm/README.md
M configure
M configure.ac

diff --git a/Makefile.pre.in b/Makefile.pre.in
index 66b34b779f27cb..fae5e384d3245f 100644
--- a/Makefile.pre.in
+++ b/Makefile.pre.in
@@ -1096,7 +1096,7 @@ $(DLLLIBRARY) libpython$(LDVERSION).dll.a: $(LIBRARY_OBJS)
 # wasm32-emscripten browser web example
 
 WEBEX_DIR=$(srcdir)/Tools/wasm/emscripten/web_example/
-web_example/python.html: $(WEBEX_DIR)/python.html
+web_example/index.html: $(WEBEX_DIR)/index.html
        @mkdir -p web_example
        @cp $< $@
 
@@ -1124,7 +1124,7 @@ web_example/python.mjs web_example/python.wasm: 
$(BUILDPYTHON)
        cp python.wasm web_example/python.wasm
 
 .PHONY: web_example
-web_example: web_example/python.mjs web_example/python.worker.mjs 
web_example/python.html web_example/server.py $(WEB_STDLIB)
+web_example: web_example/python.mjs web_example/python.worker.mjs 
web_example/index.html web_example/server.py $(WEB_STDLIB)
 
 ############################################################################
 # Header files
diff --git 
a/Misc/NEWS.d/next/Tools-Demos/2025-07-05-15-10-42.gh-issue-136251.GRM6o8.rst 
b/Misc/NEWS.d/next/Tools-Demos/2025-07-05-15-10-42.gh-issue-136251.GRM6o8.rst
new file mode 100644
index 00000000000000..6a35afe15e3875
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Tools-Demos/2025-07-05-15-10-42.gh-issue-136251.GRM6o8.rst
@@ -0,0 +1 @@
+Fixes and usability improvements for ``Tools/wasm/emscripten/web_example``
diff --git a/Python/emscripten_syscalls.c b/Python/emscripten_syscalls.c
index 886262acbc6810..d3eedad30e3639 100644
--- a/Python/emscripten_syscalls.c
+++ b/Python/emscripten_syscalls.c
@@ -100,7 +100,7 @@ EM_JS_MACROS(void, _emscripten_promising_main_js, (void), {
         return;
     }
     const origResolveGlobalSymbol = resolveGlobalSymbol;
-    if (!Module.onExit && process?.exit) {
+    if (!Module.onExit && globalThis?.process?.exit) {
         Module.onExit = (code) => process.exit(code);
     }
     // * wrap the main symbol with WebAssembly.promising,
diff --git a/Tools/wasm/README.md b/Tools/wasm/README.md
index 232321c515721e..9288598a0abc29 100644
--- a/Tools/wasm/README.md
+++ b/Tools/wasm/README.md
@@ -22,7 +22,7 @@ https://github.com/psf/webassembly for more information.
 ### Build
 
 To cross compile to the ``wasm32-emscripten`` platform you need
-[the Emscripten compiler toolchain](https://emscripten.org/), 
+[the Emscripten compiler toolchain](https://emscripten.org/),
 a Python interpreter, and an installation of Node version 18 or newer.
 Emscripten version 4.0.2 is recommended; newer versions may also work, but all
 official testing is performed with that version. All commands below are 
relative
@@ -86,11 +86,11 @@ CLI you will need to write your own alternative to 
`node_entry.mjs`.
 
 ### The Web Example
 
-When building for Emscripten, the web example will be built automatically. It 
is
-in the ``web_example`` directory. To run the web example, ``cd`` into the
+When building for Emscripten, the web example will be built automatically. It
+is in the ``web_example`` directory. To run the web example, ``cd`` into the
 ``web_example`` directory, then run ``python server.py``. This will start a web
-server; you can then visit ``http://localhost:8000/python.html`` in a browser 
to
-see a simple REPL example.
+server; you can then visit ``http://localhost:8000/`` in a browser to see a
+simple REPL example.
 
 The web example relies on a bug fix in Emscripten version 3.1.73 so if you 
build
 with earlier versions of Emscripten it may not work. The web example uses
diff --git a/Tools/wasm/emscripten/web_example/python.html 
b/Tools/wasm/emscripten/web_example/index.html
similarity index 52%
rename from Tools/wasm/emscripten/web_example/python.html
rename to Tools/wasm/emscripten/web_example/index.html
index 078f86eb764419..9c89c9c0ed3bf5 100644
--- a/Tools/wasm/emscripten/web_example/python.html
+++ b/Tools/wasm/emscripten/web_example/index.html
@@ -1,31 +1,39 @@
 <!doctype html>
 <html lang="en">
     <head>
-        <meta charset="UTF-8" />
-        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
-        <meta name="viewport" content="width=device-width, initial-scale=1.0" 
/>
-        <meta name="author" content="Katie Bell" />
-        <meta name="description" content="Simple REPL for Python WASM" />
+        <meta charset="UTF-8">
+        <meta http-equiv="X-UA-Compatible" content="IE=edge">
+        <meta name="viewport" content="width=device-width, initial-scale=1.0">
+        <meta name="author" content="Katie Bell, Adam Hartz">
+        <meta name="description" content="Simple REPL for Python WASM">
         <title>wasm-python terminal</title>
         <link
             rel="stylesheet"
             href="https://unpkg.com/xterm@4.18.0/css/xterm.css";
             crossorigin
             
integrity="sha384-4eEEn/eZgVHkElpKAzzPx/Kow/dTSgFk1BNe+uHdjHa+NkZJDh5Vqkq31+y7Eycd"
-        />
+        >
         <style>
             body {
                 font-family: arial;
                 max-width: 800px;
                 margin: 0 auto;
             }
-            #code {
+            #editor {
+                padding: 5px;
+                border: 1px solid black;
                 width: 100%;
-                height: 180px;
+                height: 300px;
             }
             #info {
                 padding-top: 20px;
             }
+            .error {
+              border: 1px solid red;
+              background-color: #ffd9d9;
+              padding: 5px;
+              margin-top: 20px;
+            }
             .button-container {
                 display: flex;
                 justify-content: end;
@@ -41,8 +49,14 @@
             src="https://unpkg.com/xterm@4.18.0/lib/xterm.js";
             crossorigin
             
integrity="sha384-yYdNmem1ioP5Onm7RpXutin5A8TimLheLNQ6tnMi01/ZpxXdAwIm2t4fJMx1Djs+"
-        />
+        ></script>
+        <script
+            src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.43.1/ace.js";
+            crossorigin
+            
integrity="sha512-kmA5vhcxOkZI0ReiKJMGNb8/KKbgbExIlnt6aXuPtl86AgHBEi6OHHOz2wsTazBDGZKxe7fmiE+pIuZJQks4+A=="
+        ></script>
         <script type="module">
+            const _magic_ctrlc_string = "__WASM_REPL_CTRLC_" + (Date.now()) + 
"__";
             class WorkerManager {
                 constructor(
                     workerURL,
@@ -132,11 +146,14 @@
 
             class WasmTerminal {
                 constructor() {
-                    this.inputBuffer = new BufferQueue();
-                    this.input = "";
-                    this.resolveInput = null;
-                    this.activeInput = false;
-                    this.inputStartCursor = null;
+                    try {
+                        this.history = 
JSON.parse(sessionStorage.getItem('__python_wasm_repl.history'));
+                        this.historyBuffer = this.history.slice();
+                    } catch(e) {
+                        this.history = [];
+                        this.historyBuffer = [];
+                    }
+                    this.reset();
 
                     this.xterm = new Terminal({
                         scrollback: 10000,
@@ -155,6 +172,18 @@
                     this.xterm.onData(this.handleTermData);
                 }
 
+                reset() {
+                    this.inputBuffer = new BufferQueue();
+                    this.input = "";
+                    this.resolveInput = null;
+                    this.activeInput = false;
+                    this.inputStartCursor = null;
+
+                    this.cursorPosition = 0;
+                    this.historyIndex = -1;
+                    this.beforeHistoryNav = "";
+                }
+
                 open(container) {
                     this.xterm.open(container);
                 }
@@ -186,9 +215,34 @@
                         if (!(ord === 0x1b || ord == 0x7f || ord < 32)) {
                             this.inputBuffer.addData(data);
                         }
-                        // TODO: Handle ANSI escape sequences
+                        // TODO: Handle more escape sequences?
                     } else if (ord === 0x1b) {
                         // Handle special characters
+                        switch (data.slice(1)) {
+                            case "[A": // up
+                                this.historyBack();
+                                break;
+                            case "[B": // down
+                                this.historyForward();
+                                break;
+                            case "[C": // right
+                                this.cursorRight();
+                                break;
+                            case "[D": // left
+                                this.cursorLeft();
+                                break;
+                            case "[H": // home key
+                                this.cursorHome(true);
+                                break;
+                            case "[F": // end key
+                                this.cursorEnd(true);
+                                break;
+                            case "[3~": // delete key
+                                this.deleteAtCursor();
+                                break;
+                            default:
+                                break;
+                        }
                     } else if (ord < 32 || ord === 0x7f) {
                         switch (data) {
                             case "\x0c": // CTRL+L
@@ -201,8 +255,18 @@
                                     this.input + this.writeLine("\n"),
                                 );
                                 this.input = "";
+                                this.cursorPosition = 0;
                                 this.activeInput = false;
                                 break;
+                            case "\x03": // CTRL+C
+                                this.input = "";
+                                this.cursorPosition = 0;
+                                this.historyIndex = -1;
+                                this.resolveInput(_magic_ctrlc_string + "\n");
+                                break;
+                            case "\x09": // TAB
+                                this.handleTab();
+                                break;
                             case "\x7F": // BACKSPACE
                             case "\x08": // CTRL+H
                                 this.handleCursorErase(true);
@@ -211,14 +275,20 @@
                                 // Send empty input
                                 if (this.input === "") {
                                     this.resolveInput("");
+                                    this.cursorPosition = 0;
                                     this.activeInput = false;
                                 }
                         }
                     } else {
                         this.handleCursorInsert(data);
+                        this.updateHistory();
                     }
                 };
 
+                clearLine() {
+                    this.xterm.write("\x1b[K");
+                }
+
                 writeLine(line) {
                     this.xterm.write(line.slice(0, -1));
                     this.xterm.write("\r\n");
@@ -226,8 +296,36 @@
                 }
 
                 handleCursorInsert(data) {
-                    this.input += data;
+                    const trailing = this.input.slice(this.cursorPosition);
+                    this.input =
+                        this.input.slice(0, this.cursorPosition) +
+                        data +
+                        trailing;
+                    this.cursorPosition += data.length;
                     this.xterm.write(data);
+                    if (trailing.length !== 0) {
+                        this.xterm.write(trailing);
+                        this.xterm.write("\x1b[" + trailing.length + "D");
+                    }
+                    this.updateHistory();
+                }
+
+                handleTab() {
+                    // handle tabs: from the current position, add spaces until
+                    // this.cursorPosition is a multiple of 4.
+                    const prefix = this.input.slice(0, this.cursorPosition);
+                    const suffix = this.input.slice(this.cursorPosition);
+                    const count = 4 - (this.cursorPosition % 4);
+                    const toAdd = " ".repeat(count);
+                    this.input = prefix + toAdd + suffix;
+                    this.cursorHome(false);
+                    this.clearLine();
+                    this.xterm.write(this.input);
+                    if (suffix) {
+                        this.xterm.write("\x1b[" + suffix.length + "D");
+                    }
+                    this.cursorPosition += count;
+                    this.updateHistory();
                 }
 
                 handleCursorErase() {
@@ -238,9 +336,113 @@
                     ) {
                         return;
                     }
-                    this.input = this.input.slice(0, -1);
-                    this.xterm.write("\x1B[D");
-                    this.xterm.write("\x1B[P");
+                    const trailing = this.input.slice(this.cursorPosition);
+                    this.input =
+                        this.input.slice(0, this.cursorPosition - 1) + 
trailing;
+                    this.cursorLeft();
+                    this.clearLine();
+                    if (trailing.length !== 0) {
+                        this.xterm.write(trailing);
+                        this.xterm.write("\x1b[" + trailing.length + "D");
+                    }
+                    this.updateHistory();
+                }
+
+                deleteAtCursor() {
+                    if (this.cursorPosition < this.input.length) {
+                        const trailing = this.input.slice(
+                            this.cursorPosition + 1,
+                        );
+                        this.input =
+                            this.input.slice(0, this.cursorPosition) + 
trailing;
+                        this.clearLine();
+                        if (trailing.length !== 0) {
+                            this.xterm.write(trailing);
+                            this.xterm.write("\x1b[" + trailing.length + "D");
+                        }
+                        this.updateHistory();
+                    }
+                }
+
+                cursorRight() {
+                    if (this.cursorPosition < this.input.length) {
+                        this.cursorPosition += 1;
+                        this.xterm.write("\x1b[C");
+                    }
+                }
+
+                cursorLeft() {
+                    if (this.cursorPosition > 0) {
+                        this.cursorPosition -= 1;
+                        this.xterm.write("\x1b[D");
+                    }
+                }
+
+                cursorHome(updatePosition) {
+                    if (this.cursorPosition > 0) {
+                        this.xterm.write("\x1b[" + this.cursorPosition + "D");
+                        if (updatePosition) {
+                            this.cursorPosition = 0;
+                        }
+                    }
+                }
+
+                cursorEnd() {
+                    if (this.cursorPosition < this.input.length) {
+                        this.xterm.write(
+                            "\x1b[" +
+                                (this.input.length - this.cursorPosition) +
+                                "C",
+                        );
+                        this.cursorPosition = this.input.length;
+                    }
+                }
+
+                updateHistory() {
+                    if (this.historyIndex !== -1) {
+                        this.historyBuffer[this.historyIndex] = this.input;
+                    } else {
+                        this.beforeHistoryNav = this.input;
+                    }
+                }
+
+                historyBack() {
+                    if (this.history.length === 0) {
+                        return;
+                    } else if (this.historyIndex === -1) {
+                        // we're not currently navigating the history; store
+                        // the current command and then look at the end of our
+                        // history buffer
+                        this.beforeHistoryNav = this.input;
+                        this.historyIndex = this.history.length - 1;
+                    } else if (this.historyIndex > 0) {
+                        this.historyIndex -= 1;
+                    }
+                    this.input = this.historyBuffer[this.historyIndex];
+                    this.cursorHome(false);
+                    this.clearLine();
+                    this.xterm.write(this.input);
+                    this.cursorPosition = this.input.length;
+                }
+
+                historyForward() {
+                    if (this.history.length === 0 || this.historyIndex === -1) 
{
+                        // we're not currently navigating the history; NOP.
+                        return;
+                    } else if (this.historyIndex < this.history.length - 1) {
+                        this.historyIndex += 1;
+                        this.input = this.historyBuffer[this.historyIndex];
+                    } else if (this.historyIndex == this.history.length - 1) {
+                        // we're coming back from the last history value; reset
+                        // the input to whatever it was when we started going
+                        // through the history
+                        this.input = this.beforeHistoryNav;
+                        this.historyIndex = -1;
+                    }
+                    this.cursorHome(false);
+                    this.clearLine();
+                    this.xterm.write(this.input);
+                    this.cursorPosition = this.input.length;
                 }
 
                 prompt = async () => {
@@ -263,12 +465,29 @@
                         // Hack to ensure cursor input start doesn't end up 
after user input
                         setTimeout(() => {
                             this.handleCursorInsert(
-                                this.inputBuffer.nextLine(),
+                                this.inputBuffer.nextLine()
                             );
                         }, 1);
                     }
                     return new Promise((resolve, reject) => {
                         this.resolveInput = (value) => {
+                            if (
+                                value.replace(/\s/g, "").length != 0 &&
+                                value != _magic_ctrlc_string + "\n"
+                            ) {
+                                if (this.historyIndex !== -1) {
+                                    this.historyBuffer[this.historyIndex] =
+                                        this.history[this.historyIndex];
+                                }
+                                this.history.push(value.slice(0, -1));
+                                this.historyBuffer.push(value.slice(0, -1));
+                                this.historyIndex = -1;
+                                this.cursorPosition = 0;
+                                try {
+                                    
sessionStorage.setItem('__python_wasm_repl.history', 
JSON.stringify(this.history));
+                                } catch(e) {
+                                }
+                            }
                             resolve(value);
                         };
                     });
@@ -327,8 +546,6 @@
             const stopButton = document.getElementById("stop");
             const clearButton = document.getElementById("clear");
 
-            const codeBox = document.getElementById("codebox");
-
             window.onload = () => {
                 const terminal = new WasmTerminal();
                 terminal.open(document.getElementById("terminal"));
@@ -362,8 +579,9 @@
 
                 runButton.addEventListener("click", (e) => {
                     terminal.clear();
+                    terminal.reset(); // reset the history
                     programRunning(true);
-                    const code = codeBox.value;
+                    const code = editor.getValue();
                     pythonWorkerManager.run({
                         args: ["main.py"],
                         files: { "main.py": code },
@@ -372,10 +590,28 @@
 
                 replButton.addEventListener("click", (e) => {
                     terminal.clear();
+                    terminal.reset(); // reset the history
+                    const REPL = `
+class WASMREPLKeyboardInterrupt(KeyboardInterrupt):
+    pass
+
+import sys
+import code
+import builtins
+
+def _interrupt_aware_input(prompt=''):
+    line = builtins.input(prompt)
+    if line.strip() == "${_magic_ctrlc_string}":
+        raise KeyboardInterrupt()
+    return line
+
+cprt = 'Type "help", "copyright", "credits" or "license" for more information.'
+banner = f'Python {sys.version} on {sys.platform}\\n{cprt}'
+
+code.interact(banner=banner, readfunc=_interrupt_aware_input, exitmsg='')
+`;
                     programRunning(true);
-                    // Need to use "-i -" to force interactive mode.
-                    // Looks like isatty always returns false in emscripten
-                    pythonWorkerManager.run({ args: ["-i", "-"], files: {} });
+                    pythonWorkerManager.run({ args: ["-c", REPL], files: {} });
                 });
 
                 stopButton.addEventListener("click", (e) => {
@@ -395,6 +631,7 @@
 
                 const finishedCallback = () => {
                     programRunning(false);
+                    pythonWorkerManager.reset();
                 };
 
                 const pythonWorkerManager = new WorkerManager(
@@ -404,30 +641,63 @@
                     finishedCallback,
                 );
             };
+            var editor;
+            document.addEventListener("DOMContentLoaded", () => {
+                editor = ace.edit("editor");
+                editor.session.setMode("ace/mode/python");
+            });
         </script>
     </head>
     <body>
-        <h1>Simple REPL for Python WASM</h1>
-        <textarea id="codebox" cols="108" rows="16">
-print('Welcome to WASM!')
-</textarea
-        >
-        <div class="button-container">
-            <button id="run" disabled>Run</button>
-            <button id="repl" disabled>Start REPL</button>
-            <button id="stop" disabled>Stop</button>
-            <button id="clear" disabled>Clear</button>
+        <div id="repldemo">
+            <h1>Simple REPL for Python WASM</h1>
+            <div id="editor">print('Welcome to WASM!')</div>
+            <div class="button-container">
+                <button id="run" disabled>Run code</button>
+                <button id="repl" disabled>Start REPL</button>
+                <button id="stop" disabled>Stop</button>
+                <button id="clear" disabled>Clear</button>
+            </div>
+            <div id="terminal"></div>
+            <div id="info">
+                The simple REPL provides a limited Python experience in the
+                browser.
+                <a
+                    
href="https://github.com/python/cpython/blob/main/Tools/wasm/README.md";
+                >
+                    Tools/wasm/README.md
+                </a>
+                contains a list of known limitations and issues. Networking,
+                subprocesses, and threading are not available.
+            </div>
         </div>
-        <div id="terminal"></div>
-        <div id="info">
-            The simple REPL provides a limited Python experience in the 
browser.
-            <a
-                
href="https://github.com/python/cpython/blob/main/Tools/wasm/README.md";
-            >
-                Tools/wasm/README.md
-            </a>
-            contains a list of known limitations and issues. Networking,
-            subprocesses, and threading are not available.
+        <div id="buffererror" class="error" style="display: none">
+            <p>
+                <code>SharedArrayBuffer</code>, which is required for this 
demo,
+                is not available in your browser environment.  One common cause
+                of this failure is loading <code>index.html</code> directly in
+                your browser instead of using <code>server.py</code> as
+                described in
+                <a
+                    
href="https://github.com/python/cpython/blob/main/Tools/wasm/README.md#the-web-example";
+                >
+                    Tools/wasm/README.md
+                </a>.
+            </p>
+            <p>
+                For more details about security requirements for
+                <code>SharedArrayBuffer</code>, see
+                <a
+                    
href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements";
+                  >this MDN page</a
+                >.
+            </p>
+            <script>
+              if (typeof SharedArrayBuffer === 'undefined') {
+                  document.getElementById('repldemo').style.display = 'none';
+                  document.getElementById('buffererror').style.display = 
'block';
+              }
+            </script>
         </div>
     </body>
 </html>
diff --git a/configure b/configure
index 2242865313e57c..946cd471f64d32 100755
--- a/configure
+++ b/configure
@@ -9603,7 +9603,7 @@ fi
         as_fn_append LDFLAGS_NODIST " -sWASM_BIGINT"
 
         as_fn_append LINKFORSHARED " -sFORCE_FILESYSTEM -lidbfs.js -lnodefs.js 
-lproxyfs.js -lworkerfs.js"
-    as_fn_append LINKFORSHARED " -sEXPORTED_RUNTIME_METHODS=FS,callMain,ENV"
+    as_fn_append LINKFORSHARED " 
-sEXPORTED_RUNTIME_METHODS=FS,callMain,ENV,HEAPU32"
     as_fn_append LINKFORSHARED " 
-sEXPORTED_FUNCTIONS=_main,_Py_Version,__PyRuntime,__PyEM_EMSCRIPTEN_COUNT_ARGS_OFFSET,_PyGILState_GetThisThreadState,__Py_DumpTraceback"
     as_fn_append LINKFORSHARED " -sSTACK_SIZE=5MB"
         as_fn_append LINKFORSHARED " -sTEXTDECODER=2"
diff --git a/configure.ac b/configure.ac
index a05b3b18efec36..6b15beb050c1af 100644
--- a/configure.ac
+++ b/configure.ac
@@ -2335,7 +2335,7 @@ AS_CASE([$ac_sys_system],
 
     dnl Include file system support
     AS_VAR_APPEND([LINKFORSHARED], [" -sFORCE_FILESYSTEM -lidbfs.js 
-lnodefs.js -lproxyfs.js -lworkerfs.js"])
-    AS_VAR_APPEND([LINKFORSHARED], [" 
-sEXPORTED_RUNTIME_METHODS=FS,callMain,ENV"])
+    AS_VAR_APPEND([LINKFORSHARED], [" 
-sEXPORTED_RUNTIME_METHODS=FS,callMain,ENV,HEAPU32"])
     AS_VAR_APPEND([LINKFORSHARED], [" 
-sEXPORTED_FUNCTIONS=_main,_Py_Version,__PyRuntime,__PyEM_EMSCRIPTEN_COUNT_ARGS_OFFSET,_PyGILState_GetThisThreadState,__Py_DumpTraceback"])
     AS_VAR_APPEND([LINKFORSHARED], [" -sSTACK_SIZE=5MB"])
     dnl Avoid bugs in JS fallback string decoding path

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: arch...@mail-archive.com

Reply via email to