Merge pull request #6 from adafruit/move_editor
Moving editor into apps and more
This commit is contained in:
commit
b1930c7231
10 changed files with 764 additions and 44 deletions
5
build.py
5
build.py
|
|
@ -12,7 +12,7 @@ LEARN_PROJECT_URLS = [
|
|||
"https://cdn-learn.adafruit.com/downloads/zip/3194658/Metro/Metro_RP2350_FlappyNyanCat.zip?timestamp={}",
|
||||
"https://cdn-learn.adafruit.com/downloads/zip/3196927/Metro/Metro_RP2350_Match3/match3_game.zip?timestamp={}",
|
||||
"https://cdn-learn.adafruit.com/downloads/zip/3194422/Metro/Metro_RP2350_Breakout.zip?timestamp={}",
|
||||
"https://cdn-learn.adafruit.com/downloads/zip/3196755/Metro/Metro_RP2350_Chips_Challenge.zip?timestamp=1746112286",
|
||||
"https://cdn-learn.adafruit.com/downloads/zip/3196755/Metro/Metro_RP2350_Chips_Challenge.zip?timestamp={}",
|
||||
]
|
||||
|
||||
def create_font_specific_zip(font_path: Path, src_dir: Path, learn_projects_dir: Path, output_dir: Path):
|
||||
|
|
@ -99,6 +99,9 @@ def create_font_specific_zip(font_path: Path, src_dir: Path, learn_projects_dir:
|
|||
# Write the file
|
||||
with open(target, 'wb') as f:
|
||||
f.write(source.read())
|
||||
|
||||
# copy builtin apps
|
||||
shutil.copytree("builtin_apps", apps_dir, dirs_exist_ok=True)
|
||||
|
||||
# Create the final zip file
|
||||
with zipfile.ZipFile(output_zip, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||
|
|
|
|||
3
builtin_apps/editor/adafruit_editor/__init__.py
Normal file
3
builtin_apps/editor/adafruit_editor/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# SPDX-FileCopyrightText: 2024 Tim Cocks
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
148
builtin_apps/editor/adafruit_editor/dang.py
Normal file
148
builtin_apps/editor/adafruit_editor/dang.py
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
# SPDX-FileCopyrightText: 2023 Jeff Epler for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import select
|
||||
import sys
|
||||
# pylint: disable=no-self-use
|
||||
|
||||
try:
|
||||
import termios
|
||||
|
||||
_orig_attr = None # pylint: disable=invalid-name
|
||||
|
||||
def _nonblocking():
|
||||
global _orig_attr # pylint: disable=global-statement
|
||||
_orig_attr = termios.tcgetattr(sys.stdin)
|
||||
attr = termios.tcgetattr(sys.stdin)
|
||||
attr[3] &= ~(termios.ECHO | termios.ICANON)
|
||||
attr[6][termios.VMIN] = 1
|
||||
attr[6][termios.VTIME] = 0
|
||||
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, attr)
|
||||
|
||||
def _blocking():
|
||||
if _orig_attr is not None:
|
||||
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, _orig_attr)
|
||||
|
||||
except ImportError:
|
||||
|
||||
def _nonblocking():
|
||||
pass
|
||||
|
||||
def _blocking():
|
||||
pass
|
||||
|
||||
|
||||
# LINES = 24
|
||||
# COLS = 80
|
||||
|
||||
LINES = 33
|
||||
COLS = 120
|
||||
|
||||
special_keys = {
|
||||
"\x1b": ..., # all prefixes of special keys must be entered as Ellipsis
|
||||
"\x1b[": ...,
|
||||
"\x1b[3": ...,
|
||||
"\x1b[5": ...,
|
||||
"\x1b[6": ...,
|
||||
"\x1b[A": "KEY_UP",
|
||||
"\x1b[B": "KEY_DOWN",
|
||||
"\x1b[C": "KEY_RIGHT",
|
||||
"\x1b[D": "KEY_LEFT",
|
||||
"\x1b[H": "KEY_HOME",
|
||||
"\x1b[F": "KEY_END",
|
||||
"\x1b[5~": "KEY_PGUP",
|
||||
"\x1b[6~": "KEY_PGDN",
|
||||
"\x1b[3~": "KEY_DELETE",
|
||||
}
|
||||
|
||||
|
||||
class Screen:
|
||||
def __init__(self, terminal=None):
|
||||
self._poll = select.poll()
|
||||
self._poll.register(sys.stdin, select.POLLIN)
|
||||
self._pending = ""
|
||||
self._terminal = terminal
|
||||
|
||||
def _sys_stdin_readable(self):
|
||||
return hasattr(sys.stdin, "readable") and sys.stdin.readable()
|
||||
|
||||
def _sys_stdout_flush(self):
|
||||
if hasattr(sys.stdout, "flush"):
|
||||
sys.stdout.flush()
|
||||
|
||||
def _terminal_read_blocking(self):
|
||||
return sys.stdin.read(1)
|
||||
|
||||
def _terminal_read_timeout(self, timeout):
|
||||
if self._sys_stdin_readable() or self._poll.poll(timeout):
|
||||
r = sys.stdin.read(1)
|
||||
return r
|
||||
return None
|
||||
|
||||
def move(self, y, x):
|
||||
if self._terminal is not None:
|
||||
self._terminal.write(f"\033[{y+1};{x+1}H")
|
||||
else:
|
||||
print(end=f"\033[{y+1};{x+1}H")
|
||||
|
||||
def erase(self):
|
||||
if self._terminal is not None:
|
||||
self._terminal.write("\033H\033[2J")
|
||||
else:
|
||||
print(end="\033H\033[2J")
|
||||
|
||||
def addstr(self, y, x, text):
|
||||
self.move(y, x)
|
||||
if self._terminal is not None:
|
||||
self._terminal.write(text)
|
||||
else:
|
||||
print(end=text)
|
||||
|
||||
def getkey(self):
|
||||
self._sys_stdout_flush()
|
||||
pending = self._pending
|
||||
if pending and (code := special_keys.get(pending)) is None:
|
||||
self._pending = pending[1:]
|
||||
return pending[0]
|
||||
|
||||
while True:
|
||||
if pending:
|
||||
c = self._terminal_read_timeout(50)
|
||||
if c is None:
|
||||
self._pending = pending[1:]
|
||||
return pending[0]
|
||||
else:
|
||||
c = self._terminal_read_blocking()
|
||||
c = pending + c
|
||||
|
||||
code = special_keys.get(c)
|
||||
|
||||
if code is None:
|
||||
self._pending = c[1:]
|
||||
return c[0]
|
||||
if code is not Ellipsis:
|
||||
return code
|
||||
|
||||
pending = c
|
||||
|
||||
|
||||
def wrapper(func, *args, **kwds):
|
||||
stdscr = Screen()
|
||||
try:
|
||||
_nonblocking()
|
||||
return func(stdscr, *args, **kwds)
|
||||
finally:
|
||||
_blocking()
|
||||
stdscr.move(LINES - 1, 0)
|
||||
print("\n")
|
||||
|
||||
def custom_terminal_wrapper(terminal, func, *args, **kwds):
|
||||
stdscr = Screen(terminal)
|
||||
try:
|
||||
_nonblocking()
|
||||
return func(stdscr, *args, **kwds)
|
||||
finally:
|
||||
_blocking()
|
||||
stdscr.move(LINES - 1, 0)
|
||||
print("\n")
|
||||
390
builtin_apps/editor/adafruit_editor/editor.py
Normal file
390
builtin_apps/editor/adafruit_editor/editor.py
Normal file
|
|
@ -0,0 +1,390 @@
|
|||
# SPDX-FileCopyrightText: 2020 Wasim Lorgat
|
||||
# SPDX-FileCopyrightText: 2023 Jeff Epler for Adafruit Industries
|
||||
# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import gc
|
||||
import os
|
||||
|
||||
import microcontroller
|
||||
import supervisor
|
||||
import usb_cdc
|
||||
from . import dang as curses
|
||||
from . import util
|
||||
from adafruit_pathlib import Path
|
||||
import time
|
||||
import json
|
||||
from argv_file_helper import argv_filename
|
||||
|
||||
# pylint: disable=redefined-builtin
|
||||
|
||||
# def print(message):
|
||||
# usb_cdc.data.write(f"{message}\r\n".encode("utf-8"))
|
||||
|
||||
INPUT_DISPLAY_REFRESH_COOLDOWN = 0.3 # s
|
||||
SHOW_MEMFREE = False
|
||||
|
||||
|
||||
class MaybeDisableReload:
|
||||
def __enter__(self):
|
||||
try:
|
||||
from supervisor import runtime # pylint: disable=import-outside-toplevel
|
||||
except ImportError:
|
||||
return
|
||||
|
||||
self._old_autoreload = ( # pylint: disable=attribute-defined-outside-init
|
||||
runtime.autoreload
|
||||
)
|
||||
runtime.autoreload = False
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
try:
|
||||
from supervisor import runtime # pylint: disable=import-outside-toplevel
|
||||
except ImportError:
|
||||
return
|
||||
|
||||
runtime.autoreload = self._old_autoreload
|
||||
|
||||
|
||||
def os_exists(filename):
|
||||
try:
|
||||
os.stat(filename)
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def gc_mem_free_hint():
|
||||
if not SHOW_MEMFREE:
|
||||
return ""
|
||||
if hasattr(gc, "mem_free"):
|
||||
gc.collect()
|
||||
return f" | free: {gc.mem_free()}"
|
||||
return ""
|
||||
|
||||
|
||||
class Buffer:
|
||||
def __init__(self, lines):
|
||||
self.lines = lines
|
||||
|
||||
def __len__(self):
|
||||
return len(self.lines)
|
||||
|
||||
def __getitem__(self, index):
|
||||
return self.lines[index]
|
||||
|
||||
@property
|
||||
def bottom(self):
|
||||
return len(self) - 1
|
||||
|
||||
def insert(self, cursor, string):
|
||||
row, col = cursor.row, cursor.col
|
||||
# print(f"len: {len(self.lines)}")
|
||||
# print(f"row: {row}")
|
||||
try:
|
||||
current = self.lines.pop(row)
|
||||
except IndexError:
|
||||
current = ""
|
||||
new = current[:col] + string + current[col:]
|
||||
self.lines.insert(row, new)
|
||||
|
||||
def split(self, cursor):
|
||||
row, col = cursor.row, cursor.col
|
||||
current = self.lines.pop(row)
|
||||
self.lines.insert(row, current[:col])
|
||||
self.lines.insert(row + 1, current[col:])
|
||||
|
||||
def delete(self, cursor):
|
||||
row, col = cursor.row, cursor.col
|
||||
if (row, col) < (self.bottom, len(self[row])):
|
||||
current = self.lines.pop(row)
|
||||
if col < len(current):
|
||||
new = current[:col] + current[col + 1:]
|
||||
self.lines.insert(row, new)
|
||||
else:
|
||||
nextline = self.lines.pop(row)
|
||||
new = current + nextline
|
||||
self.lines.insert(row, new)
|
||||
|
||||
|
||||
def clamp(x, lower, upper):
|
||||
if x < lower:
|
||||
return lower
|
||||
if x > upper:
|
||||
return upper
|
||||
return x
|
||||
|
||||
|
||||
class Cursor:
|
||||
def __init__(self, row=0, col=0, col_hint=None):
|
||||
self.row = row
|
||||
self._col = col
|
||||
self._col_hint = col if col_hint is None else col_hint
|
||||
|
||||
@property
|
||||
def col(self):
|
||||
return self._col
|
||||
|
||||
@col.setter
|
||||
def col(self, col):
|
||||
self._col = col
|
||||
self._col_hint = col
|
||||
|
||||
def _clamp_col(self, buffer):
|
||||
self._col = min(self._col_hint, len(buffer[self.row]))
|
||||
|
||||
def up(self, buffer): # pylint: disable=invalid-name
|
||||
if self.row > 0:
|
||||
self.row -= 1
|
||||
self._clamp_col(buffer)
|
||||
# print(f"cursor pos: {self.row}, {self.col}")
|
||||
|
||||
def down(self, buffer):
|
||||
if self.row < len(buffer) - 1:
|
||||
self.row += 1
|
||||
self._clamp_col(buffer)
|
||||
# print(f"cursor pos: {self.row}, {self.col}")
|
||||
|
||||
def left(self, buffer):
|
||||
if self.col > 0:
|
||||
self.col -= 1
|
||||
# print(f"cursor pos: {self.row}, {self.col}")
|
||||
elif self.row > 0:
|
||||
self.row -= 1
|
||||
self.col = len(buffer[self.row])
|
||||
# print(f"cursor pos: {self.row}, {self.col}")
|
||||
|
||||
def right(self, buffer):
|
||||
# print(f"len: {len(buffer)}")
|
||||
if len(buffer) > 0 and self.col < len(buffer[self.row]):
|
||||
self.col += 1
|
||||
# print(f"cursor pos: {self.row}, {self.col}")
|
||||
elif self.row < len(buffer) - 1:
|
||||
self.row += 1
|
||||
self.col = 0
|
||||
# print(f"cursor pos: {self.row}, {self.col}")
|
||||
|
||||
def end(self, buffer):
|
||||
self.col = len(buffer[self.row])
|
||||
# print(f"cursor pos: {self.row}, {self.col}")
|
||||
|
||||
|
||||
class Window:
|
||||
def __init__(self, n_rows, n_cols, row=0, col=0):
|
||||
self.n_rows = n_rows
|
||||
self.n_cols = n_cols
|
||||
self.row = row
|
||||
self.col = col
|
||||
|
||||
@property
|
||||
def bottom(self):
|
||||
return self.row + self.n_rows - 1
|
||||
|
||||
def up(self, cursor): # pylint: disable=invalid-name
|
||||
if cursor.row == self.row - 1 and self.row > 0:
|
||||
self.row -= 1
|
||||
|
||||
def down(self, buffer, cursor):
|
||||
if cursor.row == self.bottom + 1 and self.bottom < len(buffer) - 1:
|
||||
self.row += 1
|
||||
|
||||
def horizontal_scroll(self, cursor, left_margin=5, right_margin=2):
|
||||
n_pages = cursor.col // (self.n_cols - right_margin)
|
||||
self.col = max(n_pages * self.n_cols - right_margin - left_margin, 0)
|
||||
|
||||
def translate(self, cursor):
|
||||
return cursor.row - self.row, cursor.col - self.col
|
||||
|
||||
|
||||
def left(window, buffer, cursor):
|
||||
cursor.left(buffer)
|
||||
window.up(cursor)
|
||||
window.horizontal_scroll(cursor)
|
||||
|
||||
|
||||
def right(window, buffer, cursor):
|
||||
cursor.right(buffer)
|
||||
window.down(buffer, cursor)
|
||||
window.horizontal_scroll(cursor)
|
||||
|
||||
|
||||
def home(window, buffer, cursor): # pylint: disable=unused-argument
|
||||
cursor.col = 0
|
||||
window.horizontal_scroll(cursor)
|
||||
|
||||
|
||||
def end(window, buffer, cursor):
|
||||
cursor.end(buffer)
|
||||
window.horizontal_scroll(cursor)
|
||||
|
||||
|
||||
def editor(stdscr, filename, visible_cursor): # pylint: disable=too-many-branches,too-many-statements
|
||||
if os_exists(filename):
|
||||
with open(filename, "r", encoding="utf-8") as f:
|
||||
buffer = Buffer(f.read().splitlines())
|
||||
else:
|
||||
buffer = Buffer([""])
|
||||
|
||||
user_message = None
|
||||
|
||||
window = Window(curses.LINES - 1, curses.COLS - 1)
|
||||
cursor = Cursor()
|
||||
visible_cursor.text = buffer[0][0]
|
||||
last_refresh_time = -1
|
||||
|
||||
stdscr.erase()
|
||||
|
||||
img = [None] * curses.LINES
|
||||
|
||||
def setline(row, line):
|
||||
if img[row] == line:
|
||||
return
|
||||
img[row] = line
|
||||
line += " " * (window.n_cols - len(line))
|
||||
stdscr.addstr(row, 0, line)
|
||||
|
||||
while True:
|
||||
lastrow = 0
|
||||
for row, line in enumerate(buffer[window.row: window.row + window.n_rows]):
|
||||
lastrow = row
|
||||
if row == cursor.row - window.row and window.col > 0:
|
||||
line = "«" + line[window.col + 1:]
|
||||
if len(line) > window.n_cols:
|
||||
line = line[: window.n_cols - 1] + "»"
|
||||
setline(row, line)
|
||||
for row in range(lastrow + 1, window.n_rows):
|
||||
setline(row, "~~ EOF ~~")
|
||||
row = curses.LINES - 1
|
||||
|
||||
if user_message is None:
|
||||
if util.readonly():
|
||||
line = f"{filename:12} (mnt RO ^W) | ^R run | ^C: quit{gc_mem_free_hint()}"
|
||||
else:
|
||||
line = f"{filename:12} (mnt RW ^W) | ^R run | ^S save | ^X: save & exit | ^C: exit no save{gc_mem_free_hint()}"
|
||||
else:
|
||||
line = user_message
|
||||
user_message = None
|
||||
setline(row, line)
|
||||
|
||||
stdscr.move(*window.translate(cursor))
|
||||
old_cursor_pos = (cursor.col, cursor.row)
|
||||
# display.refresh(minimum_frames_per_second=20)
|
||||
k = stdscr.getkey()
|
||||
# print(repr(k))
|
||||
if len(k) == 1 and " " <= k <= "~":
|
||||
buffer.insert(cursor, k)
|
||||
for _ in k:
|
||||
right(window, buffer, cursor)
|
||||
elif k == "\x18": # ctrl-x
|
||||
if not util.readonly():
|
||||
with open(filename, "w", encoding="utf-8") as f:
|
||||
for row in buffer:
|
||||
f.write(f"{row}\n")
|
||||
return
|
||||
else:
|
||||
print("Unable to Save due to readonly mode! File Contents:")
|
||||
print("---- begin file contents ----")
|
||||
for row in buffer:
|
||||
print(row)
|
||||
print("---- end file contents ----")
|
||||
elif k == "\x13": # Ctrl-S
|
||||
if not util.readonly():
|
||||
with open(filename, "w", encoding="utf-8") as f:
|
||||
for row in buffer:
|
||||
f.write(f"{row}\n")
|
||||
user_message = "Saved"
|
||||
else:
|
||||
print("Unable to Save due to readonly mode!")
|
||||
elif k == "\x11": # Ctrl-Q
|
||||
print("ctrl-Q")
|
||||
for row in buffer:
|
||||
print(row)
|
||||
elif k == "\x17": # Ctrl-W
|
||||
boot_args_file = argv_filename("/boot.py")
|
||||
with open(boot_args_file, "w") as f:
|
||||
f.write(json.dumps([not util.readonly(), "/apps/editor/code.py", Path(filename).absolute()]))
|
||||
microcontroller.reset()
|
||||
elif k == "\x12": # Ctrl-R
|
||||
print(f"Run: {filename}")
|
||||
|
||||
launcher_code_args_file = argv_filename("/code.py")
|
||||
with open(launcher_code_args_file, "w") as f:
|
||||
f.write(json.dumps(["/apps/editor/code.py", Path(filename).absolute()]))
|
||||
|
||||
supervisor.set_next_code_file(filename, sticky_on_reload=False, reload_on_error=True,
|
||||
working_directory=Path(filename).parent.absolute())
|
||||
supervisor.reload()
|
||||
elif k == "KEY_HOME":
|
||||
home(window, buffer, cursor)
|
||||
elif k == "KEY_END":
|
||||
end(window, buffer, cursor)
|
||||
elif k == "KEY_LEFT":
|
||||
left(window, buffer, cursor)
|
||||
elif k == "KEY_DOWN":
|
||||
|
||||
cursor.down(buffer)
|
||||
window.down(buffer, cursor)
|
||||
window.horizontal_scroll(cursor)
|
||||
print(f"scroll pos: {window.row}")
|
||||
elif k == "KEY_PGDN":
|
||||
for _ in range(window.n_rows):
|
||||
cursor.down(buffer)
|
||||
window.down(buffer, cursor)
|
||||
window.horizontal_scroll(cursor)
|
||||
elif k == "KEY_UP":
|
||||
cursor.up(buffer)
|
||||
window.up(cursor)
|
||||
window.horizontal_scroll(cursor)
|
||||
elif k == "KEY_PGUP":
|
||||
for _ in range(window.n_rows):
|
||||
cursor.up(buffer)
|
||||
window.up(cursor)
|
||||
window.horizontal_scroll(cursor)
|
||||
elif k == "KEY_RIGHT":
|
||||
right(window, buffer, cursor)
|
||||
elif k == "\n":
|
||||
buffer.split(cursor)
|
||||
right(window, buffer, cursor)
|
||||
elif k in ("KEY_DELETE", "\x04"):
|
||||
print("delete")
|
||||
if cursor.row < len(buffer.lines) - 1 or \
|
||||
cursor.col < len(buffer.lines[cursor.row]):
|
||||
buffer.delete(cursor)
|
||||
try:
|
||||
visible_cursor.text = buffer.lines[cursor.row][cursor.col]
|
||||
except IndexError:
|
||||
visible_cursor.text = " "
|
||||
|
||||
elif k in ("KEY_BACKSPACE", "\x7f", "\x08"):
|
||||
print(f"backspace {bytes(k, 'utf-8')}")
|
||||
if (cursor.row, cursor.col) > (0, 0):
|
||||
left(window, buffer, cursor)
|
||||
buffer.delete(cursor)
|
||||
|
||||
else:
|
||||
print(f"unhandled k: {k}")
|
||||
print(f"unhandled K: {ord(k)}")
|
||||
print(f"unhandled k: {bytes(k, 'utf-8')}")
|
||||
# print("updating visible cursor")
|
||||
# print(f"anchored pos: {((cursor.col * 6) - 1, (cursor.row * 12) + 20)}")
|
||||
if old_cursor_pos != (cursor.col, cursor.row):
|
||||
|
||||
# terminal_tilegrid.pixel_shader[old_cursor_pos[0], old_cursor_pos[1]] = [0,1]
|
||||
# terminal_tilegrid.pixel_shader[cursor.col, cursor.row] = [1,0]
|
||||
|
||||
# visible_cursor.anchored_position = ((cursor.col * 6) - 1, (cursor.row * 12) + 20)
|
||||
visible_cursor.anchored_position = ((cursor.col * 6), ((cursor.row - window.row) * 12))
|
||||
|
||||
try:
|
||||
visible_cursor.text = buffer.lines[cursor.row][cursor.col]
|
||||
except IndexError:
|
||||
visible_cursor.text = " "
|
||||
|
||||
|
||||
def edit(filename, terminal=None, visible_cursor=None):
|
||||
with MaybeDisableReload():
|
||||
if terminal is None:
|
||||
return curses.wrapper(editor, filename)
|
||||
else:
|
||||
return curses.custom_terminal_wrapper(terminal, editor, filename, visible_cursor)
|
||||
158
builtin_apps/editor/adafruit_editor/picker.py
Normal file
158
builtin_apps/editor/adafruit_editor/picker.py
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
# SPDX-FileCopyrightText: 2023 Jeff Epler for Adafruit Industries
|
||||
# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import os
|
||||
import usb_cdc
|
||||
from . import dang as curses
|
||||
from . import util
|
||||
|
||||
# pylint: disable=redefined-builtin
|
||||
|
||||
# def print(message):
|
||||
# usb_cdc.data.write(f"{message}\r\n".encode("utf-8"))
|
||||
|
||||
|
||||
always = ["code.py", "boot.py", "settings.toml", "boot_out.txt"]
|
||||
good_extensions = [".py", ".toml", ".txt", ".json"]
|
||||
|
||||
|
||||
def os_exists(filename):
|
||||
try:
|
||||
os.stat(filename)
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def isdir(filename):
|
||||
return os.stat(filename)[0] & 0o40_000
|
||||
|
||||
|
||||
def has_good_extension(filename):
|
||||
for g in good_extensions:
|
||||
if filename.endswith(g):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def picker(stdscr, options, notes=(), start_idx=0):
|
||||
stdscr.erase()
|
||||
visible_files = None
|
||||
if len(options) > curses.LINES - 1:
|
||||
visible_files = options[:curses.LINES - 1]
|
||||
else:
|
||||
visible_files = options
|
||||
|
||||
scroll_offset = 0
|
||||
need_to_scroll = False
|
||||
|
||||
# del options[curses.LINES - 1:]
|
||||
print(f"len opts: {len(options)}")
|
||||
print(f"len vis: {len(visible_files)}")
|
||||
|
||||
def _draw_file_list():
|
||||
|
||||
for row, option in enumerate(visible_files):
|
||||
if row < len(notes) and (note := notes[row]):
|
||||
option = f"{option} {note}"
|
||||
try:
|
||||
space_count = max(len(visible_files[row + 1]), len(visible_files[row - 1])) - len(option)
|
||||
if space_count < 0:
|
||||
space_count = 0
|
||||
except IndexError:
|
||||
space_count = curses.COLS - len(option)
|
||||
stdscr.addstr(row, 3, option + " " * space_count)
|
||||
stdscr.addstr(curses.LINES - 1, 0, "Enter: select | ^C: quit | ^N: New")
|
||||
|
||||
_draw_file_list()
|
||||
|
||||
old_idx = None
|
||||
idx = start_idx
|
||||
while True:
|
||||
|
||||
if need_to_scroll:
|
||||
need_to_scroll = False
|
||||
_draw_file_list()
|
||||
|
||||
if idx != old_idx:
|
||||
if old_idx is not None:
|
||||
stdscr.addstr(old_idx, 0, " ")
|
||||
stdscr.addstr(idx, 0, "=>")
|
||||
old_idx = idx
|
||||
|
||||
k = stdscr.getkey()
|
||||
|
||||
if k == "KEY_DOWN":
|
||||
print(f"{scroll_offset + len(visible_files)} < {len(options)}")
|
||||
if scroll_offset + len(visible_files) < len(options):
|
||||
if idx == len(visible_files) - 1:
|
||||
need_to_scroll = True
|
||||
scroll_offset += 1
|
||||
visible_files = options[scroll_offset:scroll_offset + curses.LINES - 1]
|
||||
idx = min(idx + 1, len(visible_files) - 1)
|
||||
|
||||
elif k == "KEY_UP":
|
||||
if scroll_offset > 0:
|
||||
if idx == 0:
|
||||
need_to_scroll = True
|
||||
scroll_offset -= 1
|
||||
visible_files = options[scroll_offset:scroll_offset + curses.LINES - 1]
|
||||
idx = max(idx - 1, 0)
|
||||
elif k == "\n":
|
||||
if visible_files[idx] == "../":
|
||||
os.chdir("../")
|
||||
options, notes = _files_list()
|
||||
return picker(stdscr, options, notes)
|
||||
elif isdir(visible_files[idx]):
|
||||
os.chdir(visible_files[idx])
|
||||
options, notes = _files_list()
|
||||
return picker(stdscr, options, notes)
|
||||
else:
|
||||
return visible_files[idx]
|
||||
|
||||
|
||||
# ctrl-N
|
||||
elif k == "\x0E":
|
||||
if not util.readonly():
|
||||
new_file_name = new_file(stdscr)
|
||||
if new_file_name is not None:
|
||||
return new_file_name
|
||||
|
||||
|
||||
# pylint: disable=inconsistent-return-statements
|
||||
def new_file(stdscr):
|
||||
stdscr.erase()
|
||||
new_file_name = input("New File Name: ")
|
||||
if os_exists(new_file_name):
|
||||
print("Error: File Already Exists")
|
||||
return
|
||||
with open(new_file_name, "w") as f:
|
||||
f.write("")
|
||||
|
||||
return new_file_name
|
||||
|
||||
|
||||
def _files_list():
|
||||
options = sorted(
|
||||
(
|
||||
g
|
||||
for g in os.listdir(".")
|
||||
if not g.startswith(".")
|
||||
),
|
||||
key=lambda filename: (not has_good_extension(filename), filename),
|
||||
) # + always[:]
|
||||
|
||||
if os.getcwd() != "/":
|
||||
options.insert(0, "../")
|
||||
|
||||
# notes = [None if os_exists(filename) else "(NEW)" for filename in options]
|
||||
notes = [None] * len(options)
|
||||
return options, notes
|
||||
|
||||
|
||||
def pick_file(terminal):
|
||||
os.chdir("/")
|
||||
options, notes = _files_list()
|
||||
return curses.custom_terminal_wrapper(terminal, picker, options, notes)
|
||||
10
builtin_apps/editor/adafruit_editor/util.py
Normal file
10
builtin_apps/editor/adafruit_editor/util.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# SPDX-FileCopyrightText: 2023 Jeff Epler for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
def readonly():
|
||||
try:
|
||||
import storage # pylint: disable=import-outside-toplevel
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
return storage.getmount("/").readonly
|
||||
|
|
@ -9,7 +9,7 @@ from adafruit_bitmap_font import bitmap_font
|
|||
from adafruit_editor import editor, picker
|
||||
from tilepalettemapper import TilePaletteMapper
|
||||
import json
|
||||
from argv_file_helper import argv_filename
|
||||
from adafruit_argv_file import read_argv, write_argv
|
||||
from adafruit_fruitjam.peripherals import request_display_config
|
||||
|
||||
request_display_config(720, 400)
|
||||
|
|
@ -44,19 +44,11 @@ visible_cursor.anchor_point = (0, 0)
|
|||
visible_cursor.anchored_position = (0, 0)
|
||||
main_group.append(visible_cursor)
|
||||
|
||||
|
||||
file = None
|
||||
try:
|
||||
editor_argv_file = argv_filename(__file__)
|
||||
with open(editor_argv_file, "r") as f:
|
||||
argv_data = json.load(f)
|
||||
file = argv_data[0]
|
||||
os.remove(editor_argv_file)
|
||||
except OSError:
|
||||
file = "boot_out.txt"
|
||||
args = read_argv(__file__)
|
||||
if args is not None and len(args) > 0:
|
||||
file = args[0]
|
||||
else:
|
||||
file = picker.pick_file(terminal)
|
||||
|
||||
print(f"opening {file}")
|
||||
editor.edit(file, terminal, visible_cursor)
|
||||
print("after edit")
|
||||
# while True:
|
||||
# pass
|
||||
BIN
builtin_apps/editor/icon.bmp
Normal file
BIN
builtin_apps/editor/icon.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
4
builtin_apps/editor/metadata.json
Normal file
4
builtin_apps/editor/metadata.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"title": "Editor",
|
||||
"icon": "icon.bmp"
|
||||
}
|
||||
70
src/code.py
70
src/code.py
|
|
@ -15,18 +15,21 @@ import displayio
|
|||
|
||||
import supervisor
|
||||
import sys
|
||||
|
||||
import terminalio
|
||||
import usb
|
||||
import adafruit_pathlib as pathlib
|
||||
from adafruit_bitmap_font import bitmap_font
|
||||
from adafruit_display_text.text_box import TextBox
|
||||
from adafruit_display_text.bitmap_label import Label
|
||||
|
||||
from adafruit_displayio_layout.layouts.grid_layout import GridLayout
|
||||
from adafruit_anchored_tilegrid import AnchoredTileGrid
|
||||
import adafruit_imageload
|
||||
import adafruit_usb_host_descriptors
|
||||
from adafruit_anchored_group import AnchoredGroup
|
||||
from adafruit_fruitjam.peripherals import request_display_config
|
||||
from argv_file_helper import argv_filename
|
||||
from adafruit_argv_file import read_argv, write_argv
|
||||
|
||||
"""
|
||||
desktop launcher code.py arguments
|
||||
|
|
@ -35,34 +38,24 @@ desktop launcher code.py arguments
|
|||
1-N: args to pass to next code file
|
||||
|
||||
"""
|
||||
try:
|
||||
arg_file = argv_filename(__file__)
|
||||
print(f"arg files: {arg_file}")
|
||||
with open(arg_file, "r") as f:
|
||||
args = json.load(f)
|
||||
os.remove(arg_file)
|
||||
|
||||
args = read_argv(__file__)
|
||||
if args is not None and len(args) > 0:
|
||||
next_code_file = None
|
||||
remaining_args = None
|
||||
|
||||
if len(args) > 0:
|
||||
next_code_file = args[0]
|
||||
if len(args) > 1:
|
||||
remaining_args = args[1:]
|
||||
|
||||
if remaining_args is not None:
|
||||
next_code_argv_filename = argv_filename(next_code_file)
|
||||
with open(next_code_argv_filename, "w") as f:
|
||||
f.write(json.dumps(remaining_args))
|
||||
write_argv(next_code_file, remaining_args)
|
||||
|
||||
next_code_file = next_code_file
|
||||
supervisor.set_next_code_file(next_code_file)
|
||||
supervisor.set_next_code_file(next_code_file, sticky_on_reload=False, reload_on_error=True,
|
||||
working_directory="/".join(next_code_file.split("/")[:-1]))
|
||||
print(f"launching: {next_code_file}")
|
||||
supervisor.reload()
|
||||
# os.rename("/saves/.boot_py_argv", "/saves/.not_boot_py_argv")
|
||||
|
||||
except OSError:
|
||||
# no args, just launch desktop
|
||||
pass
|
||||
|
||||
request_display_config(720, 400)
|
||||
display = supervisor.runtime.display
|
||||
|
|
@ -73,14 +66,18 @@ if display.width > 360:
|
|||
|
||||
font_file = "/fonts/terminal.lvfontbin"
|
||||
font = bitmap_font.load_font(font_file)
|
||||
main_group = displayio.Group(scale=scale)
|
||||
scaled_group = displayio.Group(scale=scale)
|
||||
|
||||
main_group = displayio.Group()
|
||||
main_group.append(scaled_group)
|
||||
|
||||
display.root_group = main_group
|
||||
|
||||
background_bmp = displayio.Bitmap(display.width, display.height, 1)
|
||||
bg_palette = displayio.Palette(1)
|
||||
bg_palette[0] = 0x222222
|
||||
bg_tg = displayio.TileGrid(bitmap=background_bmp, pixel_shader=bg_palette)
|
||||
main_group.append(bg_tg)
|
||||
scaled_group.append(bg_tg)
|
||||
|
||||
# load the mouse cursor bitmap
|
||||
mouse_bmp = displayio.OnDiskBitmap("launcher_assets/mouse_cursor.bmp")
|
||||
|
|
@ -209,12 +206,12 @@ default_icon_bmp, default_icon_palette = adafruit_imageload.load("launcher_asset
|
|||
default_icon_palette.make_transparent(0)
|
||||
menu_grid = GridLayout(x=40, y=16, width=WIDTH, height=HEIGHT, grid_size=(config["width"], config["height"]),
|
||||
divider_lines=False)
|
||||
main_group.append(menu_grid)
|
||||
scaled_group.append(menu_grid)
|
||||
|
||||
menu_title_txt = Label(font, text="Fruit Jam OS")
|
||||
menu_title_txt.anchor_point = (0.5, 0.5)
|
||||
menu_title_txt.anchored_position = (display.width // (2 * scale), 2)
|
||||
main_group.append(menu_title_txt)
|
||||
scaled_group.append(menu_title_txt)
|
||||
|
||||
app_titles = []
|
||||
apps = []
|
||||
|
|
@ -258,7 +255,10 @@ for path in app_path.iterdir():
|
|||
"icon": str(icon_file.absolute()) if icon_file is not None else None,
|
||||
"file": str(code_file.absolute())
|
||||
})
|
||||
|
||||
i += 1
|
||||
|
||||
|
||||
def reuse_cell(grid_coords):
|
||||
try:
|
||||
cell_group = menu_grid.get_content(grid_coords)
|
||||
|
|
@ -364,15 +364,26 @@ right_tg.anchor_point = (1.0, 0.5)
|
|||
right_tg.anchored_position = ((display.width // scale) - 4, (display.height // 2 // scale) - 2)
|
||||
original_arrow_btn_color = left_palette[2]
|
||||
|
||||
main_group.append(left_tg)
|
||||
main_group.append(right_tg)
|
||||
scaled_group.append(left_tg)
|
||||
scaled_group.append(right_tg)
|
||||
|
||||
if len(apps) <= 6:
|
||||
right_tg.hidden = True
|
||||
left_tg.hidden = True
|
||||
|
||||
if mouse:
|
||||
main_group.append(mouse_tg)
|
||||
scaled_group.append(mouse_tg)
|
||||
|
||||
|
||||
help_txt = Label(terminalio.FONT, text="[Arrow]: Move\n[E]: Edit\n[Enter]: Run")
|
||||
# help_txt = TextBox(terminalio.FONT, width=88, height=30, align=TextBox.ALIGN_RIGHT, background_color=0x008800, text="[E]: Edit\n[Enter]: Run")
|
||||
help_txt.anchor_point = (0, 0)
|
||||
|
||||
help_txt.anchored_position = (2, 2)
|
||||
# help_txt.anchored_position = (display.width - 89, 1)
|
||||
|
||||
print(help_txt.bounding_box)
|
||||
main_group.append(help_txt)
|
||||
|
||||
|
||||
def atexit_callback():
|
||||
|
|
@ -540,10 +551,11 @@ while True:
|
|||
print(f"editor selected: {apps[editor_index]}")
|
||||
edit_file = apps[editor_index]["file"]
|
||||
|
||||
launch_file = "/code_editor.py"
|
||||
with open(argv_filename(launch_file), "w") as f:
|
||||
f.write(json.dumps([apps[editor_index]["file"]]))
|
||||
editor_launch_file = "apps/editor/code.py"
|
||||
write_argv(editor_launch_file, [apps[editor_index]["file"]])
|
||||
# with open(argv_filename(launch_file), "w") as f:
|
||||
# f.write(json.dumps([apps[editor_index]["file"]]))
|
||||
|
||||
supervisor.set_next_code_file(launch_file, sticky_on_reload=False, reload_on_error=True,
|
||||
working_directory="/".join(launch_file.split("/")[:-1]))
|
||||
supervisor.set_next_code_file(editor_launch_file, sticky_on_reload=False, reload_on_error=True,
|
||||
working_directory="/".join(editor_launch_file.split("/")[:-1]))
|
||||
supervisor.reload()
|
||||
|
|
|
|||
Loading…
Reference in a new issue