From 4f5caf4ab31fc589b0a1ab9e46248b8cc1e2f2f2 Mon Sep 17 00:00:00 2001 From: Simon Forman Date: Tue, 7 May 2019 13:49:27 -0700 Subject: [PATCH] More docs... --- docs/VUI-docs/source/index.rst | 112 ++++++++++++++++++++++++++++++++- joy/vui/core.py | 78 ++++++++++++++++++----- joy/vui/display.py | 29 +++++++++ joy/vui/main.py | 47 ++++++++++++++ 4 files changed, 250 insertions(+), 16 deletions(-) diff --git a/docs/VUI-docs/source/index.rst b/docs/VUI-docs/source/index.rst index 74d4b4b..7adeebc 100644 --- a/docs/VUI-docs/source/index.rst +++ b/docs/VUI-docs/source/index.rst @@ -15,6 +15,10 @@ Screenshot ----------------------------- .. image:: _static/Joy-VUI-screenshot.PNG + +Quick Start +----------------------------- + If you have PyGame and Dulwich installed you should be able to start the VUI with the following command: @@ -22,9 +26,112 @@ VUI with the following command: python -m joy.vui -This will create a `~/.thun` directory in your home dir to store your +This will create a ``~/.thun`` directory in your home dir to store your data. +How it works now. +----------------------------- + +The VUI is more-or-less a crude text editor along with +a simple Joy runtime (interpreter, stack, and dictionary.) It auto-saves +any named files (in a versioned home directory) and you can write new Joy +primitives in Python and Joy definitions and immediately install and use +them, as well as recording them for reuse (after restarts.) + +The only dependencies are Pygame and Dulwich (a Python Git library.) + +When the main.py script starts it checks for an environment var "JOY_HOME" +which should point to a directory where you want the system to store the +files ("resources") it will edit and save, this directory defaults to +``~/.thun``. The first time you run it, it will create some default files +as content. Right click on see_resources to open a viewer with the list +of resources (files), copy a name to the stack and right click on +open_resource_at_good_location to open a viewer on that resource. + +Right now the screen size defaults to windowed 1024x768, but if you pass +the ``-f`` option to the main.py script the UI will take up the full screen +at the highest available resolution. The window is divided into two (or +three in fullscreen) vertical "tracks", and the number and width of the +tracks are fixed at start up. (Feel free to edit the values in main.py to +play around with different track configurations.) Each track gets divided +horizontally into zero or more "viewers" (like windows in a windowed GUI, +cf. Chapter 4 of "Project Oberon") for a kind of tiled layout. + +Currently, there are only two kinds of (interesting) viewers: TextViewers +and StackViewer. The TextViewers are crude text editors. They provide +just enough functionality to let the user write text and code (Python and +Joy) and execute Joy functions. One important thing they do is +automatically save their content after changes. No more lost work. + +The StackViewer is a specialized TextViewer that shows the contents of the +Joy stack one line per stack item. It's a very handy visual aid to keep +track of what's going on. There's also a log.txt file that gets written +to when commands are executed, and so records the log of user actions and +system events. It tends to fill up quickly so there's a reset_log command +that clears it out. + +Viewers have "grow" and "close" in their menu bars. These are buttons. +When you right-click on grow a viewer a copy is created that covers that +viewer's entire track. If you grow a viewer that already takes up its +whole track then a copy is created that takes up an additional track, up +to the whole screen. Closing a viewer just deletes that viewer, and when +a track has no more viewers, it is deleted and that exposes any previous +tracks and viewers that were hidden. + +(Note: if you ever close all the viewers and are sitting at a blank screen +with nowhere to type and execute commands, press the Pause/Break key. +This will open a new "trap" viewer which you can then use to recover.) + +Copies of a viewer all share the same model and update their display as it +changes. (If you have two viewers open on the same named resource and edit +one you'll see the other update as you type.) + +UI Guide +----------------------------- + +left mouse sets cursor in text, in menu bar resizes viewer interactively +(this is a little buggy in that you can move the mouse quickly and get +outside the menu, leaving the viewer in the "resizing" state. Until I fix +this, the workaround is to just grab the menu bar again and wiggle it a +few pixels and let go. This will reset the machinery.) + +Right mouse executes Joy command (functions), and you can drag with the +right button to highlight (well, underline) commands. Words that aren't +names of Joy commands won't be underlined. Release the button to execute +the command. + +The middle mouse button (usually a wheel these days) scrolls the text but +you can also click and drag any viewer with it to move that viewer to +another track or to a different location in the same track. There's no +direct visual feedback for this (yet) but that dosen't seem to impair its +usefulness. + +F1, F2 - set selection begin and end markers (crude but usable.) + +F3 - copy selected text to the top of the stack. + +Shift-F3 - as copy then run "parse" command on the string. + +F4 - cut selected text to the top of the stack. + +Shift-F4 - as cut then run "pop" (delete selection.) + +Joy +---------------------------- + +Pretty much all of the rest of the functionality of the system is provided +by executing Joy commands (aka functions, aka "words" in Forth) by right- +clicking on their names in any text. + +To get help on a Joy function select the name of the function in a +TextViewer using F1 and F2, then press shift-F3 to parse the selection. +The function (really its Symbol) will appear on the stack in brackets (a +"quoted program" such as "[pop]".) Then right-click on the word help in +any TextViewer (if it's not already there, just type it in somewhere.) +This will print the docstring or definition of the word (function) to +stdout. At some point I'll write a thing to send that to the log.txt file +instead, but for now look for output in the terminal. + Modules ----------------------------- @@ -37,6 +144,7 @@ Modules :caption: Contents: core + main display viewer text_viewer @@ -45,7 +153,7 @@ Modules Indices and tables -================== +------------------ * :ref:`genindex` * :ref:`modindex` diff --git a/joy/vui/core.py b/joy/vui/core.py index aabb4b7..ec8aeed 100644 --- a/joy/vui/core.py +++ b/joy/vui/core.py @@ -22,8 +22,11 @@ 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.) -A Module docstring yo. ''' from sys import stderr from traceback import format_exc @@ -47,51 +50,53 @@ MOUSE_EVENTS = frozenset({ pygame.MOUSEBUTTONDOWN, pygame.MOUSEBUTTONUP }) -# AM I DOCSY? +'PyGame mouse events.' -# What about *moi?* 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)) -AVAILABLE_TASK_EVENTS = set(TASK_EVENTS) +'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.' -# Message status codes... dunno if this is a good idea or not... ERROR = -1 PENDING = 0 SUCCESS = 1 - - -# messaging support +# 'Message status codes... dunno if this is a good idea or not... class Message(object): - '''Message class.''' - + '''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 @@ -99,7 +104,10 @@ class ModifyMessage(Message): 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 @@ -109,19 +117,28 @@ class OpenMessage(Message): 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): pass +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 @@ -132,6 +149,9 @@ class World(object): 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 ): @@ -157,6 +177,9 @@ class World(object): 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: @@ -166,6 +189,10 @@ def push(sender, item, notify, stack_name='stack.pickle'): 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')) @@ -174,6 +201,10 @@ def open_viewer_on_string(sender, content, notify): class TheLoop(object): + ''' + The main loop manages tasks and the PyGame event queue + and framerate clock. + ''' FRAME_RATE = 24 @@ -184,6 +215,9 @@ class TheLoop(object): 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: @@ -193,16 +227,23 @@ class TheLoop(object): 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() @@ -214,6 +255,15 @@ class TheLoop(object): 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(): diff --git a/joy/vui/display.py b/joy/vui/display.py index 903ad14..d479e7c 100644 --- a/joy/vui/display.py +++ b/joy/vui/display.py @@ -224,11 +224,20 @@ class Display(object): return viewer, x, y def iter_viewers(self): + ''' + Iterate through all viewers yielding (viewer, x, y) three-tuples. + The x and y coordinates are screen pixels of the top-left corner + of the viewer. + ''' for x, T in self.tracks: for y, V in T.viewers: yield V, x, y def done_resizing(self): + ''' + Helper method called directly by ``MenuViewer.mouse_up()`` to (hackily) + update the display when done resizing a viewer. + ''' for _, track in self.tracks: # This should be done by a Message? if track.resizing_viewer: track.resizing_viewer.draw() @@ -236,16 +245,26 @@ class Display(object): break def broadcast(self, message): + ''' + Broadcast a message to all viewers (except the sender) and all + registered handlers. + ''' for _, track in self.tracks: track.broadcast(message) for handler in self.handlers: handler(message) def redraw(self): + ''' + Redraw all tracks (which will redraw all viewers.) + ''' for _, track in self.tracks: track.redraw() def focus(self, viewer): + ''' + Set system focus to a given viewer (or no viewer if a track.) + ''' if isinstance(viewer, Track): if self.focused_viewer: self.focused_viewer.unfocus() self.focused_viewer = None @@ -315,6 +334,10 @@ class Display(object): V.mouse_up(self, x, y, event.button) def init_text(self, pt, x, y, filename): + ''' + Open and return a ``TextViewer`` on a given file (which must be present + in the ``JOYHOME`` directory.) + ''' viewer = self.open_viewer(x, y, text_viewer.TextViewer) viewer.content_id, viewer.lines = pt.open(filename) viewer.draw() @@ -322,6 +345,9 @@ class Display(object): class Track(Viewer): + ''' + Manage a vertical strip of the display, and the viewers on it. + ''' def __init__(self, surface): Viewer.__init__(self, surface) @@ -464,6 +490,9 @@ class Track(Viewer): assert sorted(self.viewers) == self.viewers def broadcast(self, message): + ''' + Broadcast a message to all viewers on this track (except the sender.) + ''' for _, viewer in self.viewers: if viewer is not message.sender: viewer.handle(message) diff --git a/joy/vui/main.py b/joy/vui/main.py index d0f7237..399d9d2 100644 --- a/joy/vui/main.py +++ b/joy/vui/main.py @@ -17,6 +17,14 @@ # You should have received a copy of the GNU General Public License # along with Thun. If not see . # +''' + +Main Module +====================================== + +Pulls everything together. + +''' import os, sys, traceback import pygame from joy.library import initialize, DefinitionWrapper, SimpleFunctionWrapper @@ -34,6 +42,7 @@ if JOY_HOME is None: def load_definitions(pt, dictionary): + '''Load definitions from ``definitions.txt``.''' lines = pt.open('definitions.txt')[1] for line in lines: if '==' in line: @@ -41,12 +50,23 @@ def load_definitions(pt, dictionary): def load_primitives(home, name_space): + '''Load primitives from ``library.py``.''' fn = os.path.join(home, 'library.py') if os.path.exists(fn): execfile(fn, name_space) def init(): + ''' + Initialize the system. + + * Init PyGame + * Create main window + * Start the PyGame clock + * Set the event mask + * Create the PersistTask + + ''' print 'Initializing Pygame...' pygame.init() print 'Creating window...' @@ -62,6 +82,18 @@ def init(): def init_context(screen, clock, pt): + ''' + More initialization + + * Create the Joy dictionary + * Create the Display + * Open the log, menu, and scratch text viewers, and the stack pickle + * Start the main loop + * Create the World object + * Register PersistTask and World message handlers with the Display + * Load user function definitions. + + ''' D = initialize() d = display.Display( screen, @@ -82,6 +114,10 @@ def init_context(screen, clock, pt): def error_guard(loop, n=10): + ''' + Run a loop function, retry for ``n`` exceptions. + Prints tracebacks on ``sys.stderr``. + ''' error_count = 0 while error_count < n: try: @@ -93,11 +129,13 @@ def error_guard(loop, n=10): class FileFaker(object): + '''Pretends to be a file object but writes to log instead.''' def __init__(self, log): self.log = log def write(self, text): + '''Write text to log.''' self.log.append(text) def flush(self): @@ -105,6 +143,15 @@ class FileFaker(object): def main(screen, clock, pt): + ''' + Main function. + + * Call ``init_context()`` + * Load primitives + * Create an ``evaluate`` function that lets you just eval some Python code + * Redirect ``stdout`` to the log using a ``FileFaker`` object, and... + * Start the main loop. + ''' name_space = init_context(screen, clock, pt) load_primitives(pt.home, name_space.copy())