Skip to content
This repository was archived by the owner on Aug 31, 2025. It is now read-only.

Commit e16ff95

Browse files
authored
Merge pull request #908 from keith-packard/snek
Add snek mode
2 parents 00edfc4 + 5449a88 commit e16ff95

14 files changed

Lines changed: 2044 additions & 3 deletions

File tree

‎AUTHORS.rst‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ These include (but are not limited to):
1818
* Zander Brown
1919
* Alistair Broomhead (alistair.broomhead@gmail.com)
2020
* Frank Morton (fmorton@mac.com)
21+
* Keith Packard (keithp@keithp.com)
2122

2223
We welcome contributions from anyone! Please see :doc:`contributing` for more
2324
information.

‎mu/app.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
PyboardMode,
5757
LegoMode,
5858
PicoMode,
59+
SnekMode,
5960
)
6061
from .interface.themes import NIGHT_STYLE, DAY_STYLE, CONTRAST_STYLE
6162
from . import settings
@@ -242,6 +243,7 @@ def setup_modes(editor, view):
242243
"""
243244
return {
244245
"python": PythonMode(editor, view),
246+
"snek": SnekMode(editor, view),
245247
"circuitpython": CircuitPythonMode(editor, view),
246248
"microbit": MicrobitMode(editor, view),
247249
"esp": ESPMode(editor, view),

‎mu/interface/main.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
MicroPythonREPLPane,
6262
FileSystemPane,
6363
PlotterPane,
64+
SnekREPLPane,
6465
)
6566
from mu.interface.editor import EditorPane
6667
from mu.interface.widgets import DeviceSelector
@@ -641,6 +642,16 @@ def add_micropython_repl(self, name, connection):
641642
connection.data_received.connect(repl_pane.process_tty_data)
642643
self.add_repl(repl_pane, name)
643644

645+
def add_snek_repl(
646+
self, name, connection, force_interrupt=True, wait_input=False
647+
):
648+
"""
649+
Adds a Snek based REPL pane to the application.
650+
"""
651+
repl_pane = SnekREPLPane(connection)
652+
connection.data_received.connect(repl_pane.process_bytes)
653+
self.add_repl(repl_pane, name)
654+
644655
def add_micropython_plotter(self, name, connection, data_flood_handler):
645656
"""
646657
Adds a plotter that reads data from a serial connection.

‎mu/interface/panes.py‎

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,168 @@ def set_zoom(self, size):
475475
self.set_font_size(PANE_ZOOM_SIZES[size])
476476

477477

478+
class SnekREPLPane(MicroPythonREPLPane):
479+
"""
480+
REPL = Read, Evaluate, Print, Loop.
481+
482+
This widget represents a REPL client connected to a device running
483+
Snek.
484+
485+
The device MUST be flashed with Snek for this to work.
486+
"""
487+
488+
def __init__(self, connection, theme="day", parent=None):
489+
super().__init__(connection, theme, parent)
490+
self.getting_text = False
491+
self.text = b""
492+
self.text_recv = None
493+
494+
def keyPressEvent(self, data):
495+
"""
496+
Called when the user types something in the REPL.
497+
498+
Correctly encodes it and sends it to the connected device.
499+
"""
500+
tc = self.textCursor()
501+
key = data.key()
502+
mod = data.modifiers()
503+
if not mod:
504+
mod = 0
505+
ctrl_only = mod == Qt.ControlModifier
506+
meta_only = mod == Qt.MetaModifier
507+
ctrl_shift_only = mod == Qt.ControlModifier | Qt.ShiftModifier
508+
shift_down = mod & Qt.ShiftModifier
509+
on_osx = platform.system() == "Darwin"
510+
511+
if key == Qt.Key_Return:
512+
# Move cursor to the end of document before sending carriage return
513+
tc.movePosition(QTextCursor.End, mode=QTextCursor.MoveAnchor)
514+
self.device_cursor_position = tc.position()
515+
self.send(VT100_RETURN)
516+
elif key == Qt.Key_Backspace:
517+
if not self.delete_selection():
518+
self.send(VT100_BACKSPACE)
519+
elif key == Qt.Key_Delete:
520+
if not self.delete_selection():
521+
self.send(VT100_DELETE)
522+
elif key == Qt.Key_Right:
523+
if shift_down:
524+
# Text selection - pass down
525+
super().keyPressEvent(data)
526+
elif tc.hasSelection():
527+
self.move_cursor_to(tc.selectionEnd())
528+
elif key == Qt.Key_Left:
529+
if shift_down:
530+
# Text selection - pass down
531+
super().keyPressEvent(data)
532+
elif tc.hasSelection():
533+
self.move_cursor_to(tc.selectionStart())
534+
elif (on_osx and meta_only) or (not on_osx and ctrl_only):
535+
# Handle the Control key. On OSX/macOS/Darwin (python calls this
536+
# platform Darwin), this is handled by Qt.MetaModifier. Other
537+
# platforms (Linux, Windows) call this Qt.ControlModifier. Go
538+
# figure. See http://doc.qt.io/qt-5/qt.html#KeyboardModifier-enum
539+
if Qt.Key_A <= key <= Qt.Key_Z:
540+
# The microbit treats an input of \x01 as Ctrl+A, etc.
541+
self.send(bytes([1 + key - Qt.Key_A]))
542+
elif ctrl_shift_only or (on_osx and ctrl_only):
543+
# Command-key on Mac, Ctrl-Shift on Win/Lin
544+
if key == Qt.Key_C:
545+
self.copy()
546+
elif key == Qt.Key_V:
547+
self.delete_selection()
548+
self.paste()
549+
else:
550+
self.delete_selection()
551+
self.send(bytes(data.text(), "utf8"))
552+
553+
def insertFromMimeData(self, source):
554+
"""
555+
Insert mime data by sending it to the REPL
556+
"""
557+
if source and source.text():
558+
text = source.text().replace("\r\n", "\n").replace("\r", "\n")
559+
self.connection.write(bytes(text, "utf8"))
560+
561+
def set_devicecursor_to_qtcursor(self):
562+
# Move cursor to the end of document
563+
tc = self.textCursor()
564+
tc.movePosition(QTextCursor.End, mode=QTextCursor.MoveAnchor)
565+
self.device_cursor_position = tc.position()
566+
567+
def send_commands(self, commands):
568+
"""
569+
Send commands to the REPL via raw mode.
570+
"""
571+
raw_on = [ # Sequence of commands to get into raw mode.
572+
b"\x0e\x03",
573+
]
574+
commands = [c.encode("utf-8") for c in commands]
575+
raw_off = [
576+
b"\x0f",
577+
]
578+
command_sequence = raw_on + commands + raw_off
579+
logger.info(command_sequence)
580+
self.execute(command_sequence)
581+
582+
def execute(self, commands):
583+
"""
584+
Execute a series of commands over a period of time (scheduling
585+
remaining commands to be run in the next iteration of the event loop).
586+
"""
587+
if commands:
588+
command = commands[0]
589+
logger.info("Sending command {}".format(command))
590+
self.connection.write(command)
591+
remainder = commands[1:]
592+
remaining_task = lambda commands=remainder: self.execute(commands)
593+
QTimer.singleShot(1000, remaining_task)
594+
595+
def process_bytes(self, data):
596+
"""
597+
Given some incoming bytes of data, work out how to handle / display
598+
them in the REPL widget.
599+
"""
600+
tc = self.textCursor()
601+
# The text cursor must be on the last line of the document. If it isn't
602+
# then move it there.
603+
while tc.movePosition(QTextCursor.Down):
604+
pass
605+
i = 0
606+
while i < len(data):
607+
if data[i] == 2: # ctrl-b
608+
self.getting_text = True
609+
self.text = b""
610+
elif data[i] == 3: # ctrl-c
611+
if self.text_recv:
612+
s = self.text.decode("utf-8", "replace")
613+
self.text_recv.recv_text(s)
614+
self.text = b""
615+
self.getting_text = False
616+
elif data[i] == 17 or data[i] == 19: # XON/XOFF
617+
pass
618+
else:
619+
if self.getting_text:
620+
if data[i] != 13:
621+
self.text += bytes([data[i]])
622+
else:
623+
if data[i] == 8: # \b
624+
tc.movePosition(QTextCursor.Left)
625+
self.setTextCursor(tc)
626+
elif data[i] == 13: # \r
627+
pass
628+
elif data[i] == 10: # \n
629+
tc.movePosition(QTextCursor.End)
630+
self.setTextCursor(tc)
631+
self.insertPlainText(chr(data[i]))
632+
else:
633+
tc.deleteChar()
634+
self.setTextCursor(tc)
635+
self.insertPlainText(chr(data[i]))
636+
i += 1
637+
self.ensureCursorVisible()
638+
639+
478640
class MuFileList(QListWidget):
479641
"""
480642
Contains shared methods for the two types of file listing used in Mu.

‎mu/modes/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from .microbit import MicrobitMode
44
from .debugger import DebugMode
55
from .pygamezero import PyGameZeroMode
6+
from .snek import SnekMode
67
from .esp import ESPMode
78
from .web import WebMode
89
from .pyboard import PyboardMode
@@ -15,6 +16,7 @@
1516
"MicrobitMode",
1617
"DebugMode",
1718
"PyGameZeroMode",
19+
"SnekMode",
1820
"ESPMode",
1921
"WebMode",
2022
"PyboardMode",

‎mu/modes/api/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from .pi import PI_APIS
55
from .shared import SHARED_APIS
66
from .pygamezero import PYGAMEZERO_APIS
7+
from .snek import SNEK_APIS
78
from .esp import ESP_APIS
89
from .flask import FLASK_APIS
910
from .pyboard import PYBOARD_APIS
@@ -16,6 +17,7 @@
1617
"PI_APIS",
1718
"SHARED_APIS",
1819
"PYGAMEZERO_APIS",
20+
"SNEK_APIS",
1921
"ESP_APIS",
2022
"FLASK_APIS",
2123
"PYBOARD_APIS",

0 commit comments

Comments
 (0)