Created
November 7, 2025 23:08
-
-
Save FoamyGuy/41f078548b496e31b41f42fe94d02eb7 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries | |
| # SPDX-License-Identifier: MIT | |
| """ | |
| Fruit Jam OS Launcher | |
| """ | |
| import array | |
| import atexit | |
| import json | |
| import math | |
| import time | |
| import displayio | |
| import os | |
| import supervisor | |
| import sys | |
| import terminalio | |
| from adafruit_usb_host_mouse import find_and_init_boot_mouse | |
| 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 | |
| from adafruit_anchored_group import AnchoredGroup | |
| from adafruit_fruitjam.peripherals import request_display_config, VALID_DISPLAY_SIZES | |
| from adafruit_argv_file import read_argv, write_argv | |
| from launcher_config import LauncherConfig | |
| """ | |
| desktop launcher code.py arguments | |
| 0: next code files | |
| 1-N: args to pass to next code 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: | |
| write_argv(next_code_file, remaining_args) | |
| 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() | |
| if (width_config := os.getenv("CIRCUITPY_DISPLAY_WIDTH")) is not None: | |
| if width_config not in [x[0] for x in VALID_DISPLAY_SIZES]: | |
| raise ValueError(f"Invalid display size. Must be one of: {VALID_DISPLAY_SIZES}") | |
| for display_size in VALID_DISPLAY_SIZES: | |
| if display_size[0] == width_config: | |
| break | |
| else: | |
| display_size = (720, 400) | |
| request_display_config(*display_size) | |
| display = supervisor.runtime.display | |
| SCREENSAVER_TIMEOUT = 3 # seconds | |
| last_interaction_time = time.monotonic() | |
| screensaver = None | |
| previous_mouse_location = [0, 0] | |
| scale = 1 | |
| if display.width > 360: | |
| scale = 2 | |
| launcher_config = LauncherConfig() | |
| font_file = "/fonts/terminal.lvfontbin" | |
| font = bitmap_font.load_font(font_file) | |
| 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] = launcher_config.palette_bg | |
| bg_tg = displayio.TileGrid(bitmap=background_bmp, pixel_shader=bg_palette) | |
| scaled_group.append(bg_tg) | |
| WIDTH = int(298 / 360 * display.width // scale) | |
| HEIGHT = int(182 / 200 * display.height // scale) | |
| mouse = None | |
| last_left_button_state = 0 | |
| left_button_pressed = False | |
| if launcher_config.use_mouse: | |
| mouse = find_and_init_boot_mouse() | |
| if mouse: | |
| mouse.scale = scale | |
| mouse_tg = mouse.tilegrid | |
| mouse_tg.x = display.width // (2 * scale) | |
| mouse_tg.y = display.height // (2 * scale) | |
| config = { | |
| "menu_title": "Launcher Menu", | |
| "width": 3, | |
| "height": 2, | |
| } | |
| cell_width = WIDTH // config["width"] | |
| cell_height = HEIGHT // config["height"] | |
| page_size = config["width"] * config["height"] | |
| default_icon_bmp, default_icon_palette = adafruit_imageload.load("launcher_assets/default_icon.bmp") | |
| default_icon_palette.make_transparent(0) | |
| menu_grid = GridLayout(x=(display.width // scale - WIDTH) // 2, | |
| y=(display.height // scale - HEIGHT) // 2, | |
| width=WIDTH, height=HEIGHT, grid_size=(config["width"], config["height"]), | |
| divider_lines=False) | |
| scaled_group.append(menu_grid) | |
| menu_title_txt = Label(font, text="Fruit Jam OS", color=launcher_config.palette_fg) | |
| menu_title_txt.anchor_point = (0.5, 0.5) | |
| menu_title_txt.anchored_position = (display.width // (2 * scale), 2) | |
| scaled_group.append(menu_title_txt) | |
| app_titles = [] | |
| apps = [] | |
| app_paths = ( | |
| pathlib.Path("/apps"), | |
| pathlib.Path("/sd/apps") | |
| ) | |
| pages = [{}] | |
| cur_file_index = 0 | |
| for app_path in app_paths: | |
| if not app_path.exists(): | |
| continue | |
| for path in app_path.iterdir(): | |
| print(path) | |
| code_file = path / "code.py" | |
| if not code_file.exists(): | |
| continue | |
| metadata_file = path / "metadata.json" | |
| if not metadata_file.exists(): | |
| metadata_file = None | |
| metadata = None | |
| if metadata_file is not None: | |
| with open(metadata_file.absolute(), "r") as f: | |
| metadata = json.load(f) | |
| if metadata is not None and "icon" in metadata: | |
| icon_file = path / metadata["icon"] | |
| else: | |
| icon_file = path / "icon.bmp" | |
| if not icon_file.exists(): | |
| icon_file = None | |
| if metadata is not None and "title" in metadata: | |
| title = metadata["title"] | |
| else: | |
| title = path.name | |
| apps.append({ | |
| "title": title, | |
| "icon": str(icon_file.absolute()) if icon_file is not None else None, | |
| "file": str(code_file.absolute()), | |
| "dir": path | |
| }) | |
| apps = sorted(apps, key=lambda app: app["title"].lower()) | |
| print("launcher config", launcher_config) | |
| if len(launcher_config.favorites): | |
| for favorite_app in reversed(launcher_config.favorites): | |
| print("checking favorite", favorite_app) | |
| for app in apps: | |
| app_name = str(app["dir"].absolute()).split("/")[-1] | |
| print(f"checking app: {app_name}") | |
| if app_name == favorite_app: | |
| apps.remove(app) | |
| apps.insert(0, app) | |
| def reuse_cell(grid_coords): | |
| try: | |
| cell_group = menu_grid.get_content(grid_coords) | |
| return cell_group | |
| except KeyError: | |
| return None | |
| def _create_cell_group(app): | |
| cell_group = AnchoredGroup() | |
| if app["icon"] is None: | |
| icon_tg = displayio.TileGrid(bitmap=default_icon_bmp, pixel_shader=default_icon_palette) | |
| cell_group.append(icon_tg) | |
| else: | |
| icon_bmp, icon_palette = adafruit_imageload.load(app["icon"]) | |
| icon_tg = displayio.TileGrid(bitmap=icon_bmp, pixel_shader=icon_palette) | |
| cell_group.append(icon_tg) | |
| icon_tg.x = cell_width // 2 - icon_tg.tile_width // 2 | |
| title_txt = TextBox(font, text=app["title"], width=cell_width, height=18, | |
| align=TextBox.ALIGN_CENTER, color=launcher_config.palette_fg) | |
| icon_tg.y = (cell_height - icon_tg.tile_height - title_txt.height) // 2 | |
| cell_group.append(title_txt) | |
| title_txt.anchor_point = (0, 0) | |
| title_txt.anchored_position = (0, icon_tg.y + icon_tg.tile_height) | |
| return cell_group | |
| def _reuse_cell_group(app, cell_group): | |
| _unhide_cell_group(cell_group) | |
| if app["icon"] is None: | |
| icon_tg = cell_group[0] | |
| icon_tg.bitmap = default_icon_bmp | |
| icon_tg.pixel_shader = default_icon_palette | |
| else: | |
| icon_bmp, icon_palette = adafruit_imageload.load(app["icon"]) | |
| icon_tg = cell_group[0] | |
| icon_tg.bitmap = icon_bmp | |
| icon_tg.pixel_shader = icon_palette | |
| icon_tg.x = cell_width // 2 - icon_tg.tile_width // 2 | |
| # title_txt = TextBox(font, text=app["title"], width=cell_width, height=18, | |
| # align=TextBox.ALIGN_CENTER, color=launcher_config.palette_fg) | |
| # cell_group.append(title_txt) | |
| title_txt = cell_group[1] | |
| title_txt.text = app["title"] | |
| # title_txt.anchor_point = (0, 0) | |
| # title_txt.anchored_position = (0, icon_tg.y + icon_tg.tile_height) | |
| def _hide_cell_group(cell_group): | |
| # hide the tilegrid | |
| cell_group[0].hidden = True | |
| # set the title to blank space | |
| cell_group[1].text = " " | |
| def _unhide_cell_group(cell_group): | |
| # show tilegrid | |
| cell_group[0].hidden = False | |
| def display_page(page_index): | |
| max_pages = math.ceil(len(apps) / page_size) | |
| page_txt.text = f"{page_index + 1}/{max_pages}" | |
| for grid_index in range(page_size): | |
| grid_pos = (grid_index % config["width"], grid_index // config["width"]) | |
| try: | |
| cur_app = apps[grid_index + (page_index * page_size)] | |
| except IndexError: | |
| try: | |
| cell_group = menu_grid.get_content(grid_pos) | |
| _hide_cell_group(cell_group) | |
| except KeyError: | |
| pass | |
| # skip to the next for loop iteration | |
| continue | |
| try: | |
| cell_group = menu_grid.get_content(grid_pos) | |
| _reuse_cell_group(cur_app, cell_group) | |
| except KeyError: | |
| cell_group = _create_cell_group(cur_app) | |
| menu_grid.add_content(cell_group, grid_position=grid_pos, cell_size=(1, 1)) | |
| # app_titles.append(title_txt) | |
| print(f"{grid_index} | {grid_index % config["width"], grid_index // config["width"]}") | |
| page_txt = Label(terminalio.FONT, text="", scale=scale, color=launcher_config.palette_fg) | |
| page_txt.anchor_point = (1.0, 1.0) | |
| page_txt.anchored_position = (display.width - 2, display.height - 2) | |
| main_group.append(page_txt) | |
| cur_page = 0 | |
| display_page(cur_page) | |
| left_bmp, left_palette = adafruit_imageload.load("launcher_assets/arrow_left.bmp") | |
| left_palette.make_transparent(0) | |
| right_bmp, right_palette = adafruit_imageload.load("launcher_assets/arrow_right.bmp") | |
| right_palette.make_transparent(0) | |
| left_palette[2] = right_palette[2] = launcher_config.palette_arrow | |
| left_tg = AnchoredTileGrid(bitmap=left_bmp, pixel_shader=left_palette) | |
| left_tg.anchor_point = (0, 0.5) | |
| left_tg.anchored_position = (0, (display.height // 2 // scale) - 2) | |
| right_tg = AnchoredTileGrid(bitmap=right_bmp, pixel_shader=right_palette) | |
| right_tg.anchor_point = (1.0, 0.5) | |
| right_tg.anchored_position = ((display.width // scale), (display.height // 2 // scale) - 2) | |
| original_arrow_btn_color = left_palette[2] | |
| scaled_group.append(left_tg) | |
| scaled_group.append(right_tg) | |
| if len(apps) <= page_size: | |
| right_tg.hidden = True | |
| left_tg.hidden = True | |
| if mouse: | |
| scaled_group.append(mouse_tg) | |
| help_txt = Label(terminalio.FONT, text="[Arrow]: Move [E]: Edit [Enter]: Run [1-9]: Page", | |
| color=launcher_config.palette_fg) | |
| help_txt.anchor_point = (0.0, 1.0) | |
| help_txt.anchored_position = (2, display.height - 2) | |
| print(help_txt.bounding_box) | |
| main_group.append(help_txt) | |
| def atexit_callback(): | |
| """ | |
| re-attach USB devices to kernel if needed. | |
| :return: | |
| """ | |
| print("inside atexit callback") | |
| if mouse and mouse.was_attached and not mouse.device.is_kernel_driver_active(0): | |
| mouse.device.attach_kernel_driver(0) | |
| atexit.register(atexit_callback) | |
| selected = None | |
| def change_selected(new_selected): | |
| global selected | |
| # tuple means an item in the grid is selected | |
| if isinstance(selected, tuple): | |
| menu_grid.get_content(selected)[1].background_color = None | |
| # TileGrid means arrow is selected | |
| elif isinstance(selected, AnchoredTileGrid): | |
| selected.pixel_shader[2] = original_arrow_btn_color | |
| # tuple means an item in the grid is selected | |
| if isinstance(new_selected, tuple): | |
| menu_grid.get_content(new_selected)[1].background_color = launcher_config.palette_accent | |
| # TileGrid means arrow is selected | |
| elif isinstance(new_selected, AnchoredTileGrid): | |
| new_selected.pixel_shader[2] = launcher_config.palette_accent | |
| selected = new_selected | |
| change_selected((0, 0)) | |
| def page_right(): | |
| global cur_page | |
| if cur_page < math.ceil(len(apps) / page_size) - 1: | |
| cur_page += 1 | |
| display_page(cur_page) | |
| def page_left(): | |
| global cur_page | |
| if cur_page > 0: | |
| cur_page -= 1 | |
| display_page(cur_page) | |
| def handle_key_press(key): | |
| global index, editor_index, cur_page | |
| # print(key) | |
| # up key | |
| if key == "\x1b[A": | |
| if isinstance(selected, tuple): | |
| change_selected((selected[0], (selected[1] - 1) % config["height"])) | |
| elif selected is left_tg: | |
| change_selected((0, 0)) | |
| elif selected is right_tg: | |
| change_selected((2, 0)) | |
| # down key | |
| elif key == "\x1b[B": | |
| if isinstance(selected, tuple): | |
| change_selected((selected[0], (selected[1] + 1) % config["height"])) | |
| elif selected is left_tg: | |
| change_selected((0, 1)) | |
| elif selected is right_tg: | |
| change_selected((2, 1)) | |
| # selected = min(len(config["apps"]) - 1, selected + 1) | |
| # left key | |
| elif key == "\x1b[D": | |
| if isinstance(selected, tuple): | |
| if selected[0] >= 1: | |
| change_selected((selected[0] - 1, selected[1])) | |
| elif not left_tg.hidden: | |
| change_selected(left_tg) | |
| else: | |
| change_selected(((selected[0] - 1) % config["width"], selected[1])) | |
| elif selected is left_tg: | |
| change_selected(right_tg) | |
| elif selected is right_tg: | |
| change_selected((2, 0)) | |
| # right key | |
| elif key == "\x1b[C": | |
| if isinstance(selected, tuple): | |
| if selected[0] <= 1: | |
| change_selected((selected[0] + 1, selected[1])) | |
| elif not right_tg.hidden: | |
| change_selected(right_tg) | |
| else: | |
| change_selected(((selected[0] + 1) % config["width"], selected[1])) | |
| elif selected is left_tg: | |
| change_selected((0, 0)) | |
| elif selected is right_tg: | |
| change_selected(left_tg) | |
| elif key == "\n": | |
| if isinstance(selected, tuple): | |
| index = (selected[1] * config["width"] + selected[0]) + (cur_page * page_size) | |
| if index >= len(apps): | |
| index = None | |
| print("go!") | |
| elif selected is left_tg: | |
| page_left() | |
| elif selected is right_tg: | |
| page_right() | |
| elif key == "e": | |
| if isinstance(selected, tuple): | |
| editor_index = (selected[1] * config["width"] + selected[0]) + (cur_page * page_size) | |
| if editor_index >= len(apps): | |
| editor_index = None | |
| print("go!") | |
| elif key in "123456789": | |
| if key != "9": | |
| requested_page = int(key) | |
| max_page = math.ceil(len(apps) / page_size) | |
| if requested_page <= max_page: | |
| cur_page = requested_page - 1 | |
| display_page(requested_page - 1) | |
| else: # key == 9 | |
| max_page = math.ceil(len(apps) / page_size) | |
| cur_page = max_page - 1 | |
| display_page(max_page - 1) | |
| else: | |
| print(f"unhandled key: {repr(key)}") | |
| print(f"apps: {apps}") | |
| while True: | |
| index = None | |
| editor_index = None | |
| now = time.monotonic() | |
| available = supervisor.runtime.serial_bytes_available | |
| if available: | |
| c = sys.stdin.read(available) | |
| print(repr(c)) | |
| # app_titles[selected].background_color = None | |
| handle_key_press(c) | |
| print("selected", selected) | |
| last_interaction_time = now | |
| # app_titles[selected].background_color = launcher_config.palette_accent | |
| if mouse: | |
| buttons = mouse.update() | |
| if [mouse.x, mouse.y] != previous_mouse_location: | |
| last_interaction_time = now | |
| previous_mouse_location[0] = mouse.x | |
| previous_mouse_location[1] = mouse.y | |
| # Extract button states | |
| if buttons is None: | |
| current_left_button_state = 0 | |
| else: | |
| current_left_button_state = 1 if 'left' in buttons else 0 | |
| # Detect button presses | |
| if current_left_button_state == 1 and last_left_button_state == 0: | |
| left_button_pressed = True | |
| elif current_left_button_state == 0 and last_left_button_state == 1: | |
| left_button_pressed = False | |
| # Update button states | |
| last_left_button_state = current_left_button_state | |
| if left_button_pressed: | |
| print("left click") | |
| last_interaction_time = now | |
| clicked_cell = menu_grid.which_cell_contains((mouse_tg.x, mouse_tg.y)) | |
| if clicked_cell is not None: | |
| index = (clicked_cell[1] * config["width"] + clicked_cell[0]) + (cur_page * page_size) | |
| if right_tg.contains((mouse_tg.x, mouse_tg.y, 0)): | |
| page_right() | |
| if left_tg.contains((mouse_tg.x, mouse_tg.y, 0)): | |
| page_left() | |
| if last_interaction_time + SCREENSAVER_TIMEOUT < now: | |
| if display.auto_refresh: | |
| display.auto_refresh = False | |
| # show the screensaver | |
| if screensaver is None: | |
| m = __import__(launcher_config.data["screensaver"]["module"]) | |
| cls = getattr(m, launcher_config.data["screensaver"]["class"]) | |
| screensaver = cls() | |
| request_display_config(screensaver.display_size[0], screensaver.display_size[1]) | |
| display = supervisor.runtime.display | |
| if display.root_group != main_group: | |
| display.root_group = main_group | |
| if screensaver not in main_group: | |
| main_group.append(screensaver) | |
| needs_refresh = screensaver.tick() | |
| if needs_refresh: | |
| display.refresh() | |
| else: | |
| if not display.auto_refresh: | |
| display.auto_refresh = True | |
| if screensaver in main_group: | |
| main_group.remove(screensaver) | |
| request_display_config(*display_size) | |
| display = supervisor.runtime.display | |
| if display.root_group != main_group: | |
| display.root_group = main_group | |
| if index is not None: | |
| print("index", index) | |
| print(f"selected: {apps[index]}") | |
| launch_file = apps[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.reload() | |
| if editor_index is not None: | |
| print("editor_index", editor_index) | |
| print(f"editor selected: {apps[editor_index]}") | |
| edit_file = 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(editor_launch_file, sticky_on_reload=False, reload_on_error=True, | |
| working_directory="/".join(editor_launch_file.split("/")[:-1])) | |
| supervisor.reload() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries | |
| # SPDX-License-Identifier: MIT | |
| """ | |
| Matrix rain visual effect | |
| Largely ported from Arduino version in Metro_HSTX_Matrix to | |
| CircuitPython by claude with some additional tweaking to the | |
| colors and refresh functionality. | |
| """ | |
| import sys | |
| import random | |
| import time | |
| import displayio | |
| import supervisor | |
| from displayio import Group, TileGrid | |
| from tilepalettemapper import TilePaletteMapper | |
| from adafruit_fruitjam.peripherals import request_display_config | |
| import adafruit_imageload | |
| # Define structures for character streams | |
| class CharStream: | |
| def __init__(self): | |
| self.x = 0 # X position | |
| self.y = 0 # Y position (head of the stream) | |
| self.length = 0 # Length of the stream | |
| self.speed = 0 # How many frames to wait before moving | |
| self.countdown = 0 # Counter for movement | |
| self.active = False # Whether this stream is currently active | |
| self.chars = [" "] * 30 # Characters in the stream | |
| class MatrixScreenSaver(Group): | |
| display_size = (320, 240) | |
| # screen size in tiles, tiles are 16x16 | |
| SCREEN_WIDTH = display_size[0] // 16 | |
| SCREEN_HEIGHT = display_size[1] // 16 | |
| # Color gradient list from white to dark green | |
| COLORS = [ | |
| 0xFFFFFF, | |
| 0x88FF88, | |
| 0x00FF00, | |
| 0x00DD00, | |
| 0x00BB00, | |
| 0x009900, | |
| 0x007700, | |
| 0x006600, | |
| 0x005500, | |
| 0x005500, | |
| 0x003300, | |
| 0x003300, | |
| 0x002200, | |
| 0x002200, | |
| 0x001100, | |
| 0x001100, | |
| ] | |
| def __init__(self): | |
| super().__init__() | |
| self.init_graphics() | |
| def init_graphics(self): | |
| # Palette to use with the mapper. Has 1 extra color | |
| # so it can have black at index 0 | |
| shader_palette = displayio.Palette(len(self.COLORS) + 1) | |
| # set black at index 0 | |
| shader_palette[0] = 0x000000 | |
| # set the colors from the gradient above in the | |
| # remaining indexes | |
| for i in range(0, len(self.COLORS)): | |
| shader_palette[i + 1] = self.COLORS[i] | |
| # mapper to change colors of tiles within the grid | |
| if sys.implementation.version[0] == 9: | |
| self.grid_color_shader = TilePaletteMapper( | |
| shader_palette, 2, self.SCREEN_WIDTH, self.SCREEN_HEIGHT | |
| ) | |
| elif sys.implementation.version[0] >= 10: | |
| self.grid_color_shader = TilePaletteMapper(shader_palette, 2) | |
| # load the spritesheet | |
| self.katakana_bmp, self.katakana_pixelshader = adafruit_imageload.load("matrix_characters.bmp") | |
| # how many characters are in the sprite sheet | |
| self.char_count = self.katakana_bmp.width // 16 | |
| # grid to display characters within | |
| self.display_text_grid = TileGrid( | |
| bitmap=self.katakana_bmp, | |
| width=self.SCREEN_WIDTH, | |
| height=self.SCREEN_HEIGHT, | |
| tile_height=16, | |
| tile_width=16, | |
| pixel_shader=self.grid_color_shader, | |
| ) | |
| # flip x to get backwards characters | |
| self.display_text_grid.flip_x = True | |
| # add the text grid to main_group, so it will be visible on the display | |
| self.append(self.display_text_grid) | |
| # Array of character streams | |
| self.streams = [CharStream() for _ in range(250)] | |
| # Stream creation rate (higher = more frequent new streams) | |
| self.STREAM_CREATION_CHANCE = 65 # % chance per frame to create new stream | |
| # Initial streams to create at startup | |
| self.INITIAL_STREAMS = 30 | |
| self.setup() | |
| def setup(self): | |
| """Initialize the system""" | |
| # Seed the random number generator | |
| random.seed(int(time.monotonic() * 1000)) | |
| # Initialize all streams | |
| self.init_streams() | |
| def loop(self): | |
| """Main program loop""" | |
| # Update and draw all streams | |
| self.update_streams() | |
| # Randomly create new streams at a higher rate | |
| if random.randint(0, 99) < self.STREAM_CREATION_CHANCE: | |
| self.create_new_stream() | |
| return True | |
| def tick(self): | |
| return self.loop() | |
| def init_streams(self): | |
| """Initialize all streams as inactive""" | |
| for _ in range(len(self.streams)): | |
| self.streams[_].active = False | |
| # Create initial streams for immediate visual impact | |
| for _ in range(self.INITIAL_STREAMS): | |
| self.create_new_stream() | |
| def create_new_stream(self): | |
| """Create a new active stream""" | |
| # Find an inactive stream | |
| for _ in range(len(self.streams)): | |
| if not self.streams[_].active: | |
| # Initialize the stream | |
| self.streams[_].x = random.randint(0, self.SCREEN_WIDTH - 1) | |
| self.streams[_].y = random.randint(-5, -1) # Start above the screen | |
| self.streams[_].length = random.randint(5, 20) | |
| self.streams[_].speed = random.randint(0, 3) | |
| self.streams[_].countdown = self.streams[_].speed | |
| self.streams[_].active = True | |
| # Fill with random characters | |
| for j in range(self.streams[_].length): | |
| # streams[i].chars[j] = get_random_char() | |
| self.streams[_].chars[j] = random.randrange(0, self.char_count) | |
| return | |
| def update_streams(self): | |
| """Update and draw all streams""" | |
| # Clear the display (we'll implement this by looping through display grid) | |
| for x in range(self.SCREEN_WIDTH): | |
| for y in range(self.SCREEN_HEIGHT): | |
| self.display_text_grid[x, y] = 0 # Clear character | |
| # Count active streams (for debugging if needed) | |
| active_count = 0 | |
| for _ in range(len(self.streams)): | |
| if self.streams[_].active: | |
| active_count += 1 | |
| self.streams[_].countdown -= 1 | |
| # Time to move the stream down | |
| if self.streams[_].countdown <= 0: | |
| self.streams[_].y += 1 | |
| self.streams[_].countdown = self.streams[_].speed | |
| # Change a random character in the stream | |
| random_index = random.randint(0, self.streams[_].length - 1) | |
| # streams[i].chars[random_index] = get_random_char() | |
| self.streams[_].chars[random_index] = random.randrange(0, self.char_count) | |
| # Draw the stream | |
| self.draw_stream(self.streams[_]) | |
| # Check if the stream has moved completely off the screen | |
| if self.streams[_].y - self.streams[_].length > self.SCREEN_HEIGHT: | |
| self.streams[_].active = False | |
| def draw_stream(self, stream): | |
| """Draw a single character stream""" | |
| for _ in range(stream.length): | |
| y = stream.y - _ | |
| # Only draw if the character is on screen | |
| if 0 <= y < self.SCREEN_HEIGHT and 0 <= stream.x < self.SCREEN_WIDTH: | |
| # Set the character | |
| self.display_text_grid[stream.x, y] = stream.chars[_] | |
| if _ + 1 < len(self.COLORS): | |
| self.grid_color_shader[stream.x, y] = [0, _ + 1] | |
| else: | |
| self.grid_color_shader[stream.x, y] = [0, len(self.COLORS) - 1] | |
| # Occasionally change a character in the stream | |
| if random.randint(0, 99) < 25: # 25% chance | |
| idx = random.randint(0, stream.length - 1) | |
| stream.chars[idx] = random.randrange(0, 112) | |
| # # Main program | |
| # setup() | |
| # while True: | |
| # loop() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment