More docs...

This commit is contained in:
Simon Forman 2019-05-07 13:49:27 -07:00
parent 13515b071b
commit 4f5caf4ab3
4 changed files with 250 additions and 16 deletions

View File

@ -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`

View File

@ -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():

View File

@ -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)

View File

@ -17,6 +17,14 @@
# You should have received a copy of the GNU General Public License
# along with Thun. If not see <http://www.gnu.org/licenses/>.
#
'''
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())