More docs...
This commit is contained in:
parent
13515b071b
commit
4f5caf4ab3
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue