# -*- coding: utf-8 -*- # # Copyright © 2019 Simon Forman # # This file is part of Thun # # Thun is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Thun is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Thun. If not see . # ''' Core ===================== The core module defines a bunch of system-wide "constants" (some colors and PyGame event groups), the message classes for Oberon-style message passing, a "world" class that holds the main context for the system, and a mainloop class that manages the, uh, main loop (the PyGame event queue.) ''' from __future__ import print_function from sys import stderr from traceback import format_exc import pygame from joy.joy import run from joy.utils.stack import stack_to_string COMMITTER = 'Joy ' BLACK = FOREGROUND = 0, 0, 0 GREY = 127, 127, 127 WHITE = BACKGROUND = 255, 255, 255 BLUE = 100, 100, 255 GREEN = 70, 200, 70 MOUSE_EVENTS = frozenset({ pygame.MOUSEMOTION, pygame.MOUSEBUTTONDOWN, pygame.MOUSEBUTTONUP }) 'PyGame mouse events.' ARROW_KEYS = frozenset({ pygame.K_UP, pygame.K_DOWN, pygame.K_LEFT, pygame.K_RIGHT }) 'PyGame arrow key events.' TASK_EVENTS = tuple(range(pygame.USEREVENT, pygame.NUMEVENTS)) 'Keep track of all possible task events.' AVAILABLE_TASK_EVENTS = set(TASK_EVENTS) 'Task IDs that have not been assigned to a task.' ALLOWED_EVENTS = [pygame.QUIT, pygame.KEYUP, pygame.KEYDOWN] ALLOWED_EVENTS.extend(MOUSE_EVENTS) ALLOWED_EVENTS.extend(TASK_EVENTS) 'Event "mask" for PyGame event queue, we are only interested in these event types.' ERROR = -1 PENDING = 0 SUCCESS = 1 # 'Message status codes... dunno if this is a good idea or not... class Message(object): '''Message base class. Contains ``sender`` field.''' def __init__(self, sender): self.sender = sender class CommandMessage(Message): '''For commands, adds ``command`` field.''' def __init__(self, sender, command): Message.__init__(self, sender) self.command = command class ModifyMessage(Message): ''' For when resources are modified, adds ``subject`` and ``details`` fields. ''' def __init__(self, sender, subject, **details): Message.__init__(self, sender) self.subject = subject self.details = details class OpenMessage(Message): ''' For when resources are modified, adds ``name``, content_id``, ``status``, and ``traceback`` fields. ''' def __init__(self, sender, name): Message.__init__(self, sender) self.name = name self.content_id = self.thing = None self.status = PENDING self.traceback = None class PersistMessage(Message): ''' For when resources are modified, adds ``content_id`` and ``details`` fields. ''' def __init__(self, sender, content_id, **details): Message.__init__(self, sender) self.content_id = content_id self.details = details class ShutdownMessage(Message): '''Signals that the system is shutting down.''' # Joy Interpreter & Context class World(object): ''' This object contains the system context, the stack, dictionary, a reference to the display broadcast method, and the log. ''' def __init__(self, stack_id, stack_holder, dictionary, notify, log): self.stack_holder = stack_holder self.dictionary = dictionary self.notify = notify self.stack_id = stack_id self.log = log.lines self.log_id = log.content_id def handle(self, message): ''' Deal with updates to the stack and commands. ''' if (isinstance(message, ModifyMessage) and message.subject is self.stack_holder ): self._log_lines('', '%s <-' % self.format_stack()) if not isinstance(message, CommandMessage): return c, s, d = message.command, self.stack_holder[0], self.dictionary self._log_lines('', '-> %s' % (c,)) self.stack_holder[0], _, self.dictionary = run(c, s, d) mm = ModifyMessage(self, self.stack_holder, content_id=self.stack_id) self.notify(mm) def _log_lines(self, *lines): self.log.extend(lines) self.notify(ModifyMessage(self, self.log, content_id=self.log_id)) def format_stack(self): try: return stack_to_string(self.stack_holder[0]) except: print(format_exc(), file=stderr) return str(self.stack_holder[0]) def push(sender, item, notify, stack_name='stack.pickle'): ''' Helper function to push an item onto the system stack with message. ''' om = OpenMessage(sender, stack_name) notify(om) if om.status == SUCCESS: om.thing[0] = item, om.thing[0] notify(ModifyMessage(sender, om.thing, content_id=om.content_id)) return om.status def open_viewer_on_string(sender, content, notify): ''' Helper function to open a text viewer on a string. Typically used to show tracebacks. ''' push(sender, content, notify) notify(CommandMessage(sender, 'good_viewer_location open_viewer')) # main loop class TheLoop(object): ''' The main loop manages tasks and the PyGame event queue and framerate clock. ''' FRAME_RATE = 24 def __init__(self, display, clock): self.display = display self.clock = clock self.tasks = {} self.running = False def install_task(self, F, milliseconds): ''' Install a task to run every so many milliseconds. ''' try: task_event_id = AVAILABLE_TASK_EVENTS.pop() except KeyError: raise RuntimeError('out of task ids') self.tasks[task_event_id] = F pygame.time.set_timer(task_event_id, milliseconds) return task_event_id def remove_task(self, task_event_id): ''' Remove an installed task. ''' assert task_event_id in self.tasks, repr(task_event_id) pygame.time.set_timer(task_event_id, 0) del self.tasks[task_event_id] AVAILABLE_TASK_EVENTS.add(task_event_id) def __del__(self): # Best effort to cancel all running tasks. for task_event_id in self.tasks: pygame.time.set_timer(task_event_id, 0) def run_task(self, task_event_id): ''' Give a task its time to shine. ''' task = self.tasks[task_event_id] try: task() except: traceback = format_exc() self.remove_task(task_event_id) print(traceback, file=stderr) print('TASK removed due to ERROR', task, file=stderr) open_viewer_on_string(self, traceback, self.display.broadcast) def loop(self): ''' The actual main loop machinery. Maintain a ``running`` flag, pump the PyGame event queue and handle the events (dispatching to the display), tick the clock. When the loop is exited (by clicking the window close button or pressing the ``escape`` key) it broadcasts a ``ShutdownMessage``. ''' self.running = True while self.running: for event in pygame.event.get(): if event.type == pygame.QUIT: self.running = False elif event.type == pygame.KEYUP and event.key == pygame.K_ESCAPE: self.running = False elif event.type in self.tasks: self.run_task(event.type) else: self.display.dispatch_event(event) pygame.display.update() self.clock.tick(self.FRAME_RATE) self.display.broadcast(ShutdownMessage(self))