Merge pull request #7 from adafruit/more_stuff

Working on more things
This commit is contained in:
foamyguy 2025-05-08 11:59:40 -05:00 committed by GitHub
commit e49c754ebb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 291 additions and 142 deletions

View file

@ -4,6 +4,7 @@ import zipfile
import shutil
from pathlib import Path
import requests
from circup.commands import main as circup_cli
LEARN_PROJECT_URLS = [
"https://cdn-learn.adafruit.com/downloads/zip/3194974/Metro/Metro_RP2350_Snake.zip?timestamp={}",
@ -102,7 +103,11 @@ def create_font_specific_zip(font_path: Path, src_dir: Path, learn_projects_dir:
# copy builtin apps
shutil.copytree("builtin_apps", apps_dir, dirs_exist_ok=True)
shutil.copyfile("mock_boot_out.txt", temp_dir / "boot_out.txt")
for builtin_app_dir in os.listdir("builtin_apps"):
circup_cli(["--path", temp_dir, "install", "--auto", "--auto-file", f"apps/{builtin_app_dir}/code.py"],
standalone_mode=False)
os.remove(temp_dir / "boot_out.txt")
# Create the final zip file
with zipfile.ZipFile(output_zip, 'w', zipfile.ZIP_DEFLATED) as zf:
for file_path in temp_dir.rglob("*"):

View file

@ -113,7 +113,9 @@ class Screen:
self._pending = pending[1:]
return pending[0]
else:
c = self._terminal_read_blocking()
c = self._terminal_read_timeout(50)
if c is None:
return None
c = pending + c
code = special_keys.get(c)

View file

@ -116,6 +116,16 @@ def clamp(x, lower, upper):
return x
def _count_leading_characters(text, char):
count = 0
for c in text:
if c == char:
count += 1
else:
break
return count
class Cursor:
def __init__(self, row=0, col=0, col_hint=None):
self.row = row
@ -219,19 +229,40 @@ def end(window, buffer, cursor):
window.horizontal_scroll(cursor)
def editor(stdscr, filename, visible_cursor): # pylint: disable=too-many-branches,too-many-statements
def editor(stdscr, filename, mouse=None, terminal_tilegrid=None): # pylint: disable=too-many-branches,too-many-statements
def _only_spaces_before(cursor):
i = cursor.col - 1
while i >= 0:
print(f"i: {i} chr: '{buffer.lines[cursor.row][i]}'")
if buffer.lines[cursor.row][i] != " ":
return False
i -= 1
return True
if os_exists(filename):
with open(filename, "r", encoding="utf-8") as f:
buffer = Buffer(f.read().splitlines())
else:
buffer = Buffer([""])
print(f"cwd: {os.getcwd()} | {os.getcwd() == "/apps/editor"}")
if os.getcwd() != "/apps/editor" and os.getcwd() != "/":
absolute_filepath = os.getcwd() + "/" + filename
else:
absolute_filepath = filename
user_message = None
user_message_shown_time = -1
clicked_tile_coords = [None, None]
window = Window(curses.LINES - 1, curses.COLS - 1)
cursor = Cursor()
visible_cursor.text = buffer[0][0]
last_refresh_time = -1
terminal_tilegrid.pixel_shader[cursor.col,cursor.row] = [1, 0]
old_cursor_pos = (cursor.col, cursor.row)
# try:
# visible_cursor.text = buffer[0][0]
# except IndexError:
# visible_cursor.text = " "
stdscr.erase()
@ -244,6 +275,7 @@ def editor(stdscr, filename, visible_cursor): # pylint: disable=too-many-branch
line += " " * (window.n_cols - len(line))
stdscr.addstr(row, 0, line)
print(f"cwd: {os.getcwd()} | abs path: {absolute_filepath} | filename: {filename}")
while True:
lastrow = 0
for row, line in enumerate(buffer[window.row: window.row + window.n_rows]):
@ -258,133 +290,183 @@ def editor(stdscr, filename, visible_cursor): # pylint: disable=too-many-branch
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()}"
if (not absolute_filepath.startswith("/saves/") and
not absolute_filepath.startswith("/sd/") and
util.readonly()):
line = f"{absolute_filepath:12} (mnt RO ^W) | ^R run | ^O Open | ^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()}"
line = f"{absolute_filepath:12} (mnt RW ^W) | ^R run | ^O Open | ^S save | ^X: save & exit | ^C: exit no save{gc_mem_free_hint()}"
else:
line = user_message
user_message = None
if user_message_shown_time + 3.0 < time.monotonic():
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:
if k is not None:
# 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:
f.write(f"{row}\n")
return
else:
print("Unable to Save due to readonly mode! File Contents:")
print("---- begin file contents ----")
print(row)
print("---- end file contents ----")
elif k == "\x13": # Ctrl-S
print(absolute_filepath)
print(f"starts with saves: {absolute_filepath.startswith("/saves/")}")
print(f"stars saves: {absolute_filepath.startswith("/saves/")}")
print(f"stars sd: {absolute_filepath.startswith("/sd/")}")
print(f"readonly: {util.readonly()}")
if (absolute_filepath.startswith("/saves/") or
absolute_filepath.startswith("/sd/") or
not util.readonly()):
with open(absolute_filepath, "w", encoding="utf-8") as f:
for row in buffer:
f.write(f"{row}\n")
user_message = "Saved"
user_message_shown_time = time.monotonic()
else:
user_message = "Unable to Save due to readonly mode!"
user_message_shown_time = time.monotonic()
elif k == "\x11": # Ctrl-Q
print("ctrl-Q")
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}")
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()]))
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":
supervisor.set_next_code_file(filename, sticky_on_reload=False, reload_on_error=True,
working_directory=Path(filename).parent.absolute())
supervisor.reload()
elif k == "\x0f": # Ctrl-O
supervisor.set_next_code_file("/apps/editor/code.py", sticky_on_reload=False, reload_on_error=True,
working_directory="/apps/editor")
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):
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_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 == "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":
leading_spaces = _count_leading_characters(buffer.lines[cursor.row], " ")
buffer.split(cursor)
right(window, buffer, cursor)
for i in range(leading_spaces):
buffer.insert(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):
if cursor.col > 0 and buffer.lines[cursor.row][cursor.col-1] == " " and _only_spaces_before(cursor):
for i in range(4):
left(window, buffer, cursor)
buffer.delete(cursor)
else:
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')}")
if mouse is not None:
pressed_btns = mouse.update()
if pressed_btns is not None and "left" in pressed_btns:
clicked_tile_coords[0] = mouse.x // 6
clicked_tile_coords[1] = mouse.y // 12
if clicked_tile_coords[0] > len(buffer.lines[clicked_tile_coords[1]]):
clicked_tile_coords[0] = len(buffer.lines[clicked_tile_coords[1]])
cursor.row = clicked_tile_coords[1]
cursor.col = clicked_tile_coords[0]
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]
if old_cursor_pos != (cursor.col, cursor.row):
# print(f"old cursor: {old_cursor_pos}, new: {(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]
# print(f"old: {terminal_tilegrid.pixel_shader[old_cursor_pos[0], old_cursor_pos[1]]} new: {terminal_tilegrid.pixel_shader[cursor.col, cursor.row]}")
# 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 = " "
# 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):
old_cursor_pos = (cursor.col, cursor.row)
def edit(filename, terminal=None, mouse=None, terminal_tilegrid=None):
with MaybeDisableReload():
if terminal is None:
return curses.wrapper(editor, filename)
else:
return curses.custom_terminal_wrapper(terminal, editor, filename, visible_cursor)
return curses.custom_terminal_wrapper(terminal, editor, filename, mouse, terminal_tilegrid)

View file

@ -4,6 +4,8 @@
# SPDX-License-Identifier: MIT
import os
import time
import usb_cdc
from . import dang as curses
from . import util
@ -115,23 +117,57 @@ def picker(stdscr, options, notes=(), start_idx=0):
# ctrl-N
elif k == "\x0E":
if not util.readonly():
# if not util.readonly():
new_file_name = new_file(stdscr)
if new_file_name is not None:
return new_file_name
else:
time.sleep(2)
stdscr.erase()
old_idx = None
_draw_file_list()
def terminal_input(stdscr, message):
stdscr.erase()
stdscr.addstr(0, 0, message)
input_str_list = []
k = stdscr.getkey()
while k != "\n":
if k is not None:
if len(k) == 1 and " " <= k <= "~":
input_str_list.append(k)
stdscr.addstr(0, len(message) + len(input_str_list) - 1, k)
elif k == "\x08":
input_str_list.pop(len(input_str_list) - 1)
stdscr.addstr(0, len(message) + len(input_str_list) - 1, k)
k = stdscr.getkey()
# submit after enter pressed
return "".join(input_str_list)
# pylint: disable=inconsistent-return-statements
def new_file(stdscr):
stdscr.erase()
new_file_name = input("New File Name: ")
print(f"cwd inside new_file(): {os.getcwd()}")
new_file_name = terminal_input(stdscr, "New File Name: ")
if os_exists(new_file_name):
print("Error: File Already Exists")
stdscr.addstr(1,0, "Error: File Already Exists")
return
with open(new_file_name, "w") as f:
f.write("")
print(f"new filename: {new_file_name}")
if not new_file_name.startswith("/saves/") and not new_file_name.startswith("/sd/"):
if not util.readonly():
with open(new_file_name, "w") as f:
f.write("")
return new_file_name
return new_file_name
else:
stdscr.addstr(1, 0, "Error: Cannot create file in readonly storage")
else:
with open(new_file_name, "w") as f:
f.write("")
return new_file_name
def _files_list():

View file

@ -1,16 +1,17 @@
import atexit
import os
import supervisor
from displayio import Group, Palette, TileGrid
import terminalio
from lvfontio import OnDiskFont
from adafruit_display_text.bitmap_label import Label
from adafruit_bitmap_font import bitmap_font
from adafruit_editor import editor, picker
from tilepalettemapper import TilePaletteMapper
import json
from adafruit_argv_file import read_argv, write_argv
from adafruit_fruitjam.peripherals import request_display_config
from adafruit_usb_host_mouse import find_and_init_boot_mouse
print(f"cwd in editor/code.py: {os.getcwd()}")
request_display_config(720, 400)
display = supervisor.runtime.display
@ -30,19 +31,30 @@ char_size = font.get_bounding_box()
screen_size = (display.width // char_size[0], display.height // char_size[1])
print(screen_size)
terminal_area = TileGrid(bitmap=font.bitmap, pixel_shader=font_palette, width=screen_size[0], height=screen_size[1],
highlight_palette = Palette(3)
highlight_palette[0] = 0x000000
highlight_palette[1] = 0xFFFFFF
highlight_palette[2] = 0xC9C9C9
terminal_area = TileGrid(bitmap=font.bitmap, width=screen_size[0], height=screen_size[1],
tile_width=char_size[0], tile_height=char_size[1])
tpm = TilePaletteMapper(highlight_palette, 2, terminal_area)
for x in range(screen_size[0]):
tpm[x,screen_size[1]-1] = [2,0]
main_group.append(terminal_area)
terminal = terminalio.Terminal(terminal_area, font)
visible_cursor = Label(terminalio.FONT, text="",
color=0x000000, background_color=0xeeeeee, padding_left=1)
visible_cursor.hidden = False
visible_cursor.anchor_point = (0, 0)
visible_cursor.anchored_position = (0, 0)
main_group.append(visible_cursor)
# visible_cursor = Label(terminalio.FONT, text="",
# color=0x000000, background_color=0xeeeeee, padding_left=1)
# visible_cursor.hidden = False
# visible_cursor.anchor_point = (0, 0)
# visible_cursor.anchored_position = (0, 0)
# main_group.append(visible_cursor)
file = None
args = read_argv(__file__)
@ -51,4 +63,20 @@ if args is not None and len(args) > 0:
else:
file = picker.pick_file(terminal)
editor.edit(file, terminal, visible_cursor)
mouse = find_and_init_boot_mouse()
if mouse is not None:
mouse.x = display.width - 6
main_group.append(mouse.tilegrid)
def atexit_callback():
"""
re-attach USB devices to kernel if needed.
:return:
"""
print("inside atexit callback")
if mouse is not None:
mouse.release()
atexit.register(atexit_callback)
editor.edit(file, terminal, mouse, terminal_area)

4
mock_boot_out.txt Normal file
View file

@ -0,0 +1,4 @@
Adafruit CircuitPython 10.0.0-alpha.4-2-g01da5c7c88-dirty on 2025-05-07; Adafruit Fruit Jam with rp2350b
Board ID:adafruit_fruit_jam
UID:
boot.py output:

2
requirements.txt Normal file
View file

@ -0,0 +1,2 @@
circup
requests

View file

@ -1,7 +1,9 @@
# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries
# SPDX-License-Identifier: MIT
import os
import supervisor
from argv_file_helper import argv_filename
from adafruit_argv_file import read_argv, write_argv
import json
import storage
@ -15,15 +17,10 @@ boot.py arguments
2-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)
print("args file found and loaded")
os.remove(arg_file)
print("args file removed")
args = read_argv(__file__)
if args is not None and len(args) > 0:
readonly = args[0]
next_code_file = None
@ -35,20 +32,14 @@ try:
remaining_args = args[2:]
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))
print("next code args written")
write_argv(next_code_file, remaining_args)
print(f"setting storage readonly to: {readonly}")
# print(f"setting storage readonly to: {readonly}")
storage.remount("/", readonly=readonly)
next_code_file = next_code_file
supervisor.set_next_code_file(next_code_file)
print(f"launching: {next_code_file}")
# os.rename("/saves/.boot_py_argv", "/saves/.not_boot_py_argv")
supervisor.set_next_code_file(next_code_file, sticky_on_reload=False, reload_on_error=True,
working_directory="/".join(next_code_file.split("/")[:-1]))
except OSError:
print("launching boot animation")
supervisor.set_next_code_file("boot_animation.py")
else:
supervisor.set_next_code_file("boot_animation.py")

View file

@ -1,9 +1,8 @@
# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries
# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries
# SPDX-License-Identifier: MIT
"""
This example uses adafruit_display_text.label to display text using a custom font
loaded by adafruit_bitmap_font
Fruit Jam OS Launcher
"""
import array
import atexit