| 
 | 1 | +# MIT license; Copyright (c) 2022 Jim Mussared  | 
 | 2 | + | 
 | 3 | +import micropython  | 
 | 4 | +import re  | 
 | 5 | +import sys  | 
 | 6 | +import time  | 
 | 7 | +import uasyncio as asyncio  | 
 | 8 | + | 
 | 9 | +# Import statement (needs to be global, and does not return).  | 
 | 10 | +_RE_IMPORT = re.compile("^import ([^ ]+)( as ([^ ]+))?")  | 
 | 11 | +_RE_FROM_IMPORT = re.compile("^from [^ ]+ import ([^ ]+)( as ([^ ]+))?")  | 
 | 12 | +# Global variable assignment.  | 
 | 13 | +_RE_GLOBAL = re.compile("^([a-zA-Z0-9_]+) ?=[^=]")  | 
 | 14 | +# General assignment expression or import statement (does not return a value).  | 
 | 15 | +_RE_ASSIGN = re.compile("[^=]=[^=]")  | 
 | 16 | + | 
 | 17 | +# Command hist (One reserved slot for the current command).  | 
 | 18 | +_HISTORY_LIMIT = const(5 + 1)  | 
 | 19 | + | 
 | 20 | + | 
 | 21 | +async def execute(code, g, s):  | 
 | 22 | +    if not code.strip():  | 
 | 23 | +        return  | 
 | 24 | + | 
 | 25 | +    try:  | 
 | 26 | +        if "await " in code:  | 
 | 27 | +            # Execute the code snippet in an async context.  | 
 | 28 | +            if m := _RE_IMPORT.match(code) or _RE_FROM_IMPORT.match(code):  | 
 | 29 | +                code = f"global {m.group(3) or m.group(1)}\n    {code}"  | 
 | 30 | +            elif m := _RE_GLOBAL.match(code):  | 
 | 31 | +                code = f"global {m.group(1)}\n    {code}"  | 
 | 32 | +            elif not _RE_ASSIGN.search(code):  | 
 | 33 | +                code = f"return {code}"  | 
 | 34 | + | 
 | 35 | +            code = f"""  | 
 | 36 | +import uasyncio as asyncio  | 
 | 37 | +async def __code():  | 
 | 38 | +    {code}  | 
 | 39 | +
  | 
 | 40 | +__exec_task = asyncio.create_task(__code())  | 
 | 41 | +"""  | 
 | 42 | + | 
 | 43 | +            async def kbd_intr_task(exec_task, s):  | 
 | 44 | +                while True:  | 
 | 45 | +                    if ord(await s.read(1)) == 0x03:  | 
 | 46 | +                        exec_task.cancel()  | 
 | 47 | +                        return  | 
 | 48 | + | 
 | 49 | +            l = {"__exec_task": None}  | 
 | 50 | +            exec(code, g, l)  | 
 | 51 | +            exec_task = l["__exec_task"]  | 
 | 52 | + | 
 | 53 | +            # Concurrently wait for either Ctrl-C from the stream or task  | 
 | 54 | +            # completion.  | 
 | 55 | +            intr_task = asyncio.create_task(kbd_intr_task(exec_task, s))  | 
 | 56 | + | 
 | 57 | +            try:  | 
 | 58 | +                try:  | 
 | 59 | +                    return await exec_task  | 
 | 60 | +                except asyncio.CancelledError:  | 
 | 61 | +                    pass  | 
 | 62 | +            finally:  | 
 | 63 | +                intr_task.cancel()  | 
 | 64 | +                try:  | 
 | 65 | +                    await intr_task  | 
 | 66 | +                except asyncio.CancelledError:  | 
 | 67 | +                    pass  | 
 | 68 | +        else:  | 
 | 69 | +            # Excute code snippet directly.  | 
 | 70 | +            try:  | 
 | 71 | +                try:  | 
 | 72 | +                    micropython.kbd_intr(3)  | 
 | 73 | +                    try:  | 
 | 74 | +                        return eval(code, g)  | 
 | 75 | +                    except SyntaxError:  | 
 | 76 | +                        # Maybe an assignment, try with exec.  | 
 | 77 | +                        return exec(code, g)  | 
 | 78 | +                except KeyboardInterrupt:  | 
 | 79 | +                    pass  | 
 | 80 | +            finally:  | 
 | 81 | +                micropython.kbd_intr(-1)  | 
 | 82 | + | 
 | 83 | +    except Exception as err:  | 
 | 84 | +        print(f"{type(err).__name__}: {err}")  | 
 | 85 | + | 
 | 86 | + | 
 | 87 | +# REPL task. Invoke this with an optional mutable globals dict.  | 
 | 88 | +async def task(g=None, prompt="--> "):  | 
 | 89 | +    print("Starting asyncio REPL...")  | 
 | 90 | +    if g is None:  | 
 | 91 | +        g = __import__("__main__").__dict__  | 
 | 92 | +    try:  | 
 | 93 | +        micropython.kbd_intr(-1)  | 
 | 94 | +        s = asyncio.StreamReader(sys.stdin)  | 
 | 95 | +        # clear = True  | 
 | 96 | +        hist = [None] * _HISTORY_LIMIT  | 
 | 97 | +        hist_i = 0  # Index of most recent entry.  | 
 | 98 | +        hist_n = 0  # Number of history entries.  | 
 | 99 | +        c = 0  # ord of most recent character.  | 
 | 100 | +        t = 0  # timestamp of most recent character.  | 
 | 101 | +        while True:  | 
 | 102 | +            hist_b = 0  # How far back in the history are we currently.  | 
 | 103 | +            sys.stdout.write(prompt)  | 
 | 104 | +            cmd = ""  | 
 | 105 | +            while True:  | 
 | 106 | +                b = await s.read(1)  | 
 | 107 | +                c = ord(b)  | 
 | 108 | +                pc = c  # save previous character  | 
 | 109 | +                pt = t  # save previous time  | 
 | 110 | +                t = time.ticks_ms()  | 
 | 111 | +                if c < 0x20 or c > 0x7E:  | 
 | 112 | +                    if c == 0x0A:  | 
 | 113 | +                        # CR  | 
 | 114 | +                        sys.stdout.write("\n")  | 
 | 115 | +                        if cmd:  | 
 | 116 | +                            # Push current command.  | 
 | 117 | +                            hist[hist_i] = cmd  | 
 | 118 | +                            # Increase history length if possible, and rotate ring forward.  | 
 | 119 | +                            hist_n = min(_HISTORY_LIMIT - 1, hist_n + 1)  | 
 | 120 | +                            hist_i = (hist_i + 1) % _HISTORY_LIMIT  | 
 | 121 | + | 
 | 122 | +                            result = await execute(cmd, g, s)  | 
 | 123 | +                            if result is not None:  | 
 | 124 | +                                sys.stdout.write(repr(result))  | 
 | 125 | +                                sys.stdout.write("\n")  | 
 | 126 | +                        break  | 
 | 127 | +                    elif c == 0x08 or c == 0x7F:  | 
 | 128 | +                        # Backspace.  | 
 | 129 | +                        if cmd:  | 
 | 130 | +                            cmd = cmd[:-1]  | 
 | 131 | +                            sys.stdout.write("\x08 \x08")  | 
 | 132 | +                    elif c == 0x02:  | 
 | 133 | +                        # Ctrl-B  | 
 | 134 | +                        continue  | 
 | 135 | +                    elif c == 0x03:  | 
 | 136 | +                        # Ctrl-C  | 
 | 137 | +                        if pc == 0x03 and time.ticks_diff(t, pt) < 20:  | 
 | 138 | +                            # Two very quick Ctrl-C (faster than a human  | 
 | 139 | +                            # typing) likely means mpremote trying to  | 
 | 140 | +                            # escape.  | 
 | 141 | +                            asyncio.new_event_loop()  | 
 | 142 | +                            return  | 
 | 143 | +                        sys.stdout.write("\n")  | 
 | 144 | +                        break  | 
 | 145 | +                    elif c == 0x04:  | 
 | 146 | +                        # Ctrl-D  | 
 | 147 | +                        sys.stdout.write("\n")  | 
 | 148 | +                        # Shutdown asyncio.  | 
 | 149 | +                        asyncio.new_event_loop()  | 
 | 150 | +                        return  | 
 | 151 | +                    elif c == 0x1B:  | 
 | 152 | +                        # Start of escape sequence.  | 
 | 153 | +                        key = await s.read(2)  | 
 | 154 | +                        if key in ("[A", "[B"):  | 
 | 155 | +                            # Stash the current command.  | 
 | 156 | +                            hist[(hist_i - hist_b) % _HISTORY_LIMIT] = cmd  | 
 | 157 | +                            # Clear current command.  | 
 | 158 | +                            b = "\x08" * len(cmd)  | 
 | 159 | +                            sys.stdout.write(b)  | 
 | 160 | +                            sys.stdout.write(" " * len(cmd))  | 
 | 161 | +                            sys.stdout.write(b)  | 
 | 162 | +                            # Go backwards or forwards in the history.  | 
 | 163 | +                            if key == "[A":  | 
 | 164 | +                                hist_b = min(hist_n, hist_b + 1)  | 
 | 165 | +                            else:  | 
 | 166 | +                                hist_b = max(0, hist_b - 1)  | 
 | 167 | +                            # Update current command.  | 
 | 168 | +                            cmd = hist[(hist_i - hist_b) % _HISTORY_LIMIT]  | 
 | 169 | +                            sys.stdout.write(cmd)  | 
 | 170 | +                    else:  | 
 | 171 | +                        # sys.stdout.write("\\x")  | 
 | 172 | +                        # sys.stdout.write(hex(c))  | 
 | 173 | +                        pass  | 
 | 174 | +                else:  | 
 | 175 | +                    sys.stdout.write(b)  | 
 | 176 | +                    cmd += b  | 
 | 177 | +    finally:  | 
 | 178 | +        micropython.kbd_intr(3)  | 
0 commit comments