• R/O
  • SSH

Joypy: コミット

Main interpreter and library.


コミットメタ情報

リビジョンe3d0ddd774cbbbd87dfb5814ef8adb0d9db30eab (tree)
日時2019-05-08 05:49:27
作者Simon Forman <sforman@hush...>
コミッターSimon Forman

ログメッセージ

More docs...

変更サマリ

差分

diff -r 8569d6404691 -r e3d0ddd774cb docs/VUI-docs/source/index.rst
--- a/docs/VUI-docs/source/index.rst Tue May 07 10:23:43 2019 -0700
+++ b/docs/VUI-docs/source/index.rst Tue May 07 13:49:27 2019 -0700
@@ -15,6 +15,10 @@
1515 -----------------------------
1616 .. image:: _static/Joy-VUI-screenshot.PNG
1717
18+
19+Quick Start
20+-----------------------------
21+
1822 If you have PyGame and Dulwich installed you should be able to start the
1923 VUI with the following command:
2024
@@ -22,9 +26,112 @@
2226
2327 python -m joy.vui
2428
25-This will create a `~/.thun` directory in your home dir to store your
29+This will create a ``~/.thun`` directory in your home dir to store your
2630 data.
2731
32+How it works now.
33+-----------------------------
34+
35+The VUI is more-or-less a crude text editor along with
36+a simple Joy runtime (interpreter, stack, and dictionary.) It auto-saves
37+any named files (in a versioned home directory) and you can write new Joy
38+primitives in Python and Joy definitions and immediately install and use
39+them, as well as recording them for reuse (after restarts.)
40+
41+The only dependencies are Pygame and Dulwich (a Python Git library.)
42+
43+When the main.py script starts it checks for an environment var "JOY_HOME"
44+which should point to a directory where you want the system to store the
45+files ("resources") it will edit and save, this directory defaults to
46+``~/.thun``. The first time you run it, it will create some default files
47+as content. Right click on see_resources to open a viewer with the list
48+of resources (files), copy a name to the stack and right click on
49+open_resource_at_good_location to open a viewer on that resource.
50+
51+Right now the screen size defaults to windowed 1024x768, but if you pass
52+the ``-f`` option to the main.py script the UI will take up the full screen
53+at the highest available resolution. The window is divided into two (or
54+three in fullscreen) vertical "tracks", and the number and width of the
55+tracks are fixed at start up. (Feel free to edit the values in main.py to
56+play around with different track configurations.) Each track gets divided
57+horizontally into zero or more "viewers" (like windows in a windowed GUI,
58+cf. Chapter 4 of "Project Oberon") for a kind of tiled layout.
59+
60+Currently, there are only two kinds of (interesting) viewers: TextViewers
61+and StackViewer. The TextViewers are crude text editors. They provide
62+just enough functionality to let the user write text and code (Python and
63+Joy) and execute Joy functions. One important thing they do is
64+automatically save their content after changes. No more lost work.
65+
66+The StackViewer is a specialized TextViewer that shows the contents of the
67+Joy stack one line per stack item. It's a very handy visual aid to keep
68+track of what's going on. There's also a log.txt file that gets written
69+to when commands are executed, and so records the log of user actions and
70+system events. It tends to fill up quickly so there's a reset_log command
71+that clears it out.
72+
73+Viewers have "grow" and "close" in their menu bars. These are buttons.
74+When you right-click on grow a viewer a copy is created that covers that
75+viewer's entire track. If you grow a viewer that already takes up its
76+whole track then a copy is created that takes up an additional track, up
77+to the whole screen. Closing a viewer just deletes that viewer, and when
78+a track has no more viewers, it is deleted and that exposes any previous
79+tracks and viewers that were hidden.
80+
81+(Note: if you ever close all the viewers and are sitting at a blank screen
82+with nowhere to type and execute commands, press the Pause/Break key.
83+This will open a new "trap" viewer which you can then use to recover.)
84+
85+Copies of a viewer all share the same model and update their display as it
86+changes. (If you have two viewers open on the same named resource and edit
87+one you'll see the other update as you type.)
88+
89+UI Guide
90+-----------------------------
91+
92+left mouse sets cursor in text, in menu bar resizes viewer interactively
93+(this is a little buggy in that you can move the mouse quickly and get
94+outside the menu, leaving the viewer in the "resizing" state. Until I fix
95+this, the workaround is to just grab the menu bar again and wiggle it a
96+few pixels and let go. This will reset the machinery.)
97+
98+Right mouse executes Joy command (functions), and you can drag with the
99+right button to highlight (well, underline) commands. Words that aren't
100+names of Joy commands won't be underlined. Release the button to execute
101+the command.
102+
103+The middle mouse button (usually a wheel these days) scrolls the text but
104+you can also click and drag any viewer with it to move that viewer to
105+another track or to a different location in the same track. There's no
106+direct visual feedback for this (yet) but that dosen't seem to impair its
107+usefulness.
108+
109+F1, F2 - set selection begin and end markers (crude but usable.)
110+
111+F3 - copy selected text to the top of the stack.
112+
113+Shift-F3 - as copy then run "parse" command on the string.
114+
115+F4 - cut selected text to the top of the stack.
116+
117+Shift-F4 - as cut then run "pop" (delete selection.)
118+
119+Joy
120+----------------------------
121+
122+Pretty much all of the rest of the functionality of the system is provided
123+by executing Joy commands (aka functions, aka "words" in Forth) by right-
124+clicking on their names in any text.
125+
126+To get help on a Joy function select the name of the function in a
127+TextViewer using F1 and F2, then press shift-F3 to parse the selection.
128+The function (really its Symbol) will appear on the stack in brackets (a
129+"quoted program" such as "[pop]".) Then right-click on the word help in
130+any TextViewer (if it's not already there, just type it in somewhere.)
131+This will print the docstring or definition of the word (function) to
132+stdout. At some point I'll write a thing to send that to the log.txt file
133+instead, but for now look for output in the terminal.
134+
28135
29136 Modules
30137 -----------------------------
@@ -37,6 +144,7 @@
37144 :caption: Contents:
38145
39146 core
147+ main
40148 display
41149 viewer
42150 text_viewer
@@ -45,7 +153,7 @@
45153
46154
47155 Indices and tables
48-==================
156+------------------
49157
50158 * :ref:`genindex`
51159 * :ref:`modindex`
diff -r 8569d6404691 -r e3d0ddd774cb joy/vui/core.py
--- a/joy/vui/core.py Tue May 07 10:23:43 2019 -0700
+++ b/joy/vui/core.py Tue May 07 13:49:27 2019 -0700
@@ -22,8 +22,11 @@
2222 Core
2323 =====================
2424
25+The core module defines a bunch of system-wide "constants" (some colors
26+and PyGame event groups), the message classes for Oberon-style message
27+passing, a "world" class that holds the main context for the system, and
28+a mainloop class that manages the, uh, main loop (the PyGame event queue.)
2529
26-A Module docstring yo.
2730 '''
2831 from sys import stderr
2932 from traceback import format_exc
@@ -47,51 +50,53 @@
4750 pygame.MOUSEBUTTONDOWN,
4851 pygame.MOUSEBUTTONUP
4952 })
50-# AM I DOCSY?
53+'PyGame mouse events.'
5154
52-# What about *moi?*
5355 ARROW_KEYS = frozenset({
5456 pygame.K_UP,
5557 pygame.K_DOWN,
5658 pygame.K_LEFT,
5759 pygame.K_RIGHT
5860 })
61+'PyGame arrow key events.'
5962
6063
6164 TASK_EVENTS = tuple(range(pygame.USEREVENT, pygame.NUMEVENTS))
65+'Keep track of all possible task events.'
66+
6267 AVAILABLE_TASK_EVENTS = set(TASK_EVENTS)
63-
68+'Task IDs that have not been assigned to a task.'
6469
6570 ALLOWED_EVENTS = [pygame.QUIT, pygame.KEYUP, pygame.KEYDOWN]
6671 ALLOWED_EVENTS.extend(MOUSE_EVENTS)
6772 ALLOWED_EVENTS.extend(TASK_EVENTS)
73+'Event "mask" for PyGame event queue, we are only interested in these event types.'
6874
6975
70-# Message status codes... dunno if this is a good idea or not...
7176 ERROR = -1
7277 PENDING = 0
7378 SUCCESS = 1
74-
75-
76-# messaging support
79+# 'Message status codes... dunno if this is a good idea or not...
7780
7881
7982 class Message(object):
80- '''Message class.'''
81-
83+ '''Message base class. Contains ``sender`` field.'''
8284 def __init__(self, sender):
8385 self.sender = sender
8486
8587
8688 class CommandMessage(Message):
87-
89+ '''For commands, adds ``command`` field.'''
8890 def __init__(self, sender, command):
8991 Message.__init__(self, sender)
9092 self.command = command
9193
9294
9395 class ModifyMessage(Message):
94-
96+ '''
97+ For when resources are modified, adds ``subject`` and ``details``
98+ fields.
99+ '''
95100 def __init__(self, sender, subject, **details):
96101 Message.__init__(self, sender)
97102 self.subject = subject
@@ -99,7 +104,10 @@
99104
100105
101106 class OpenMessage(Message):
102-
107+ '''
108+ For when resources are modified, adds ``name``, content_id``,
109+ ``status``, and ``traceback`` fields.
110+ '''
103111 def __init__(self, sender, name):
104112 Message.__init__(self, sender)
105113 self.name = name
@@ -109,19 +117,28 @@
109117
110118
111119 class PersistMessage(Message):
120+ '''
121+ For when resources are modified, adds ``content_id`` and ``details``
122+ fields.
123+ '''
112124 def __init__(self, sender, content_id, **details):
113125 Message.__init__(self, sender)
114126 self.content_id = content_id
115127 self.details = details
116128
117129
118-class ShutdownMessage(Message): pass
130+class ShutdownMessage(Message):
131+ '''Signals that the system is shutting down.'''
119132
120133
121134 # Joy Interpreter & Context
122135
123136
124137 class World(object):
138+ '''
139+ This object contains the system context, the stack, dictionary, a
140+ reference to the display broadcast method, and the log.
141+ '''
125142
126143 def __init__(self, stack_id, stack_holder, dictionary, notify, log):
127144 self.stack_holder = stack_holder
@@ -132,6 +149,9 @@
132149 self.log_id = log.content_id
133150
134151 def handle(self, message):
152+ '''
153+ Deal with updates to the stack and commands.
154+ '''
135155 if (isinstance(message, ModifyMessage)
136156 and message.subject is self.stack_holder
137157 ):
@@ -157,6 +177,9 @@
157177
158178
159179 def push(sender, item, notify, stack_name='stack.pickle'):
180+ '''
181+ Helper function to push an item onto the system stack with message.
182+ '''
160183 om = OpenMessage(sender, stack_name)
161184 notify(om)
162185 if om.status == SUCCESS:
@@ -166,6 +189,10 @@
166189
167190
168191 def open_viewer_on_string(sender, content, notify):
192+ '''
193+ Helper function to open a text viewer on a string.
194+ Typically used to show tracebacks.
195+ '''
169196 push(sender, content, notify)
170197 notify(CommandMessage(sender, 'good_viewer_location open_viewer'))
171198
@@ -174,6 +201,10 @@
174201
175202
176203 class TheLoop(object):
204+ '''
205+ The main loop manages tasks and the PyGame event queue
206+ and framerate clock.
207+ '''
177208
178209 FRAME_RATE = 24
179210
@@ -184,6 +215,9 @@
184215 self.running = False
185216
186217 def install_task(self, F, milliseconds):
218+ '''
219+ Install a task to run every so many milliseconds.
220+ '''
187221 try:
188222 task_event_id = AVAILABLE_TASK_EVENTS.pop()
189223 except KeyError:
@@ -193,16 +227,23 @@
193227 return task_event_id
194228
195229 def remove_task(self, task_event_id):
230+ '''
231+ Remove an installed task.
232+ '''
196233 assert task_event_id in self.tasks, repr(task_event_id)
197234 pygame.time.set_timer(task_event_id, 0)
198235 del self.tasks[task_event_id]
199236 AVAILABLE_TASK_EVENTS.add(task_event_id)
200237
201238 def __del__(self):
239+ # Best effort to cancel all running tasks.
202240 for task_event_id in self.tasks:
203241 pygame.time.set_timer(task_event_id, 0)
204242
205243 def run_task(self, task_event_id):
244+ '''
245+ Give a task its time to shine.
246+ '''
206247 task = self.tasks[task_event_id]
207248 try:
208249 task()
@@ -214,6 +255,15 @@
214255 open_viewer_on_string(self, traceback, self.display.broadcast)
215256
216257 def loop(self):
258+ '''
259+ The actual main loop machinery.
260+
261+ Maintain a ``running`` flag, pump the PyGame event queue and
262+ handle the events (dispatching to the display), tick the clock.
263+
264+ When the loop is exited (by clicking the window close button or
265+ pressing the ``escape`` key) it broadcasts a ``ShutdownMessage``.
266+ '''
217267 self.running = True
218268 while self.running:
219269 for event in pygame.event.get():
diff -r 8569d6404691 -r e3d0ddd774cb joy/vui/display.py
--- a/joy/vui/display.py Tue May 07 10:23:43 2019 -0700
+++ b/joy/vui/display.py Tue May 07 13:49:27 2019 -0700
@@ -224,11 +224,20 @@
224224 return viewer, x, y
225225
226226 def iter_viewers(self):
227+ '''
228+ Iterate through all viewers yielding (viewer, x, y) three-tuples.
229+ The x and y coordinates are screen pixels of the top-left corner
230+ of the viewer.
231+ '''
227232 for x, T in self.tracks:
228233 for y, V in T.viewers:
229234 yield V, x, y
230235
231236 def done_resizing(self):
237+ '''
238+ Helper method called directly by ``MenuViewer.mouse_up()`` to (hackily)
239+ update the display when done resizing a viewer.
240+ '''
232241 for _, track in self.tracks: # This should be done by a Message?
233242 if track.resizing_viewer:
234243 track.resizing_viewer.draw()
@@ -236,16 +245,26 @@
236245 break
237246
238247 def broadcast(self, message):
248+ '''
249+ Broadcast a message to all viewers (except the sender) and all
250+ registered handlers.
251+ '''
239252 for _, track in self.tracks:
240253 track.broadcast(message)
241254 for handler in self.handlers:
242255 handler(message)
243256
244257 def redraw(self):
258+ '''
259+ Redraw all tracks (which will redraw all viewers.)
260+ '''
245261 for _, track in self.tracks:
246262 track.redraw()
247263
248264 def focus(self, viewer):
265+ '''
266+ Set system focus to a given viewer (or no viewer if a track.)
267+ '''
249268 if isinstance(viewer, Track):
250269 if self.focused_viewer: self.focused_viewer.unfocus()
251270 self.focused_viewer = None
@@ -315,6 +334,10 @@
315334 V.mouse_up(self, x, y, event.button)
316335
317336 def init_text(self, pt, x, y, filename):
337+ '''
338+ Open and return a ``TextViewer`` on a given file (which must be present
339+ in the ``JOYHOME`` directory.)
340+ '''
318341 viewer = self.open_viewer(x, y, text_viewer.TextViewer)
319342 viewer.content_id, viewer.lines = pt.open(filename)
320343 viewer.draw()
@@ -322,6 +345,9 @@
322345
323346
324347 class Track(Viewer):
348+ '''
349+ Manage a vertical strip of the display, and the viewers on it.
350+ '''
325351
326352 def __init__(self, surface):
327353 Viewer.__init__(self, surface)
@@ -464,6 +490,9 @@
464490 assert sorted(self.viewers) == self.viewers
465491
466492 def broadcast(self, message):
493+ '''
494+ Broadcast a message to all viewers on this track (except the sender.)
495+ '''
467496 for _, viewer in self.viewers:
468497 if viewer is not message.sender:
469498 viewer.handle(message)
diff -r 8569d6404691 -r e3d0ddd774cb joy/vui/main.py
--- a/joy/vui/main.py Tue May 07 10:23:43 2019 -0700
+++ b/joy/vui/main.py Tue May 07 13:49:27 2019 -0700
@@ -17,6 +17,14 @@
1717 # You should have received a copy of the GNU General Public License
1818 # along with Thun. If not see <http://www.gnu.org/licenses/>.
1919 #
20+'''
21+
22+Main Module
23+======================================
24+
25+Pulls everything together.
26+
27+'''
2028 import os, sys, traceback
2129 import pygame
2230 from joy.library import initialize, DefinitionWrapper, SimpleFunctionWrapper
@@ -34,6 +42,7 @@
3442
3543
3644 def load_definitions(pt, dictionary):
45+ '''Load definitions from ``definitions.txt``.'''
3746 lines = pt.open('definitions.txt')[1]
3847 for line in lines:
3948 if '==' in line:
@@ -41,12 +50,23 @@
4150
4251
4352 def load_primitives(home, name_space):
53+ '''Load primitives from ``library.py``.'''
4454 fn = os.path.join(home, 'library.py')
4555 if os.path.exists(fn):
4656 execfile(fn, name_space)
4757
4858
4959 def init():
60+ '''
61+ Initialize the system.
62+
63+ * Init PyGame
64+ * Create main window
65+ * Start the PyGame clock
66+ * Set the event mask
67+ * Create the PersistTask
68+
69+ '''
5070 print 'Initializing Pygame...'
5171 pygame.init()
5272 print 'Creating window...'
@@ -62,6 +82,18 @@
6282
6383
6484 def init_context(screen, clock, pt):
85+ '''
86+ More initialization
87+
88+ * Create the Joy dictionary
89+ * Create the Display
90+ * Open the log, menu, and scratch text viewers, and the stack pickle
91+ * Start the main loop
92+ * Create the World object
93+ * Register PersistTask and World message handlers with the Display
94+ * Load user function definitions.
95+
96+ '''
6597 D = initialize()
6698 d = display.Display(
6799 screen,
@@ -82,6 +114,10 @@
82114
83115
84116 def error_guard(loop, n=10):
117+ '''
118+ Run a loop function, retry for ``n`` exceptions.
119+ Prints tracebacks on ``sys.stderr``.
120+ '''
85121 error_count = 0
86122 while error_count < n:
87123 try:
@@ -93,11 +129,13 @@
93129
94130
95131 class FileFaker(object):
132+ '''Pretends to be a file object but writes to log instead.'''
96133
97134 def __init__(self, log):
98135 self.log = log
99136
100137 def write(self, text):
138+ '''Write text to log.'''
101139 self.log.append(text)
102140
103141 def flush(self):
@@ -105,6 +143,15 @@
105143
106144
107145 def main(screen, clock, pt):
146+ '''
147+ Main function.
148+
149+ * Call ``init_context()``
150+ * Load primitives
151+ * Create an ``evaluate`` function that lets you just eval some Python code
152+ * Redirect ``stdout`` to the log using a ``FileFaker`` object, and...
153+ * Start the main loop.
154+ '''
108155 name_space = init_context(screen, clock, pt)
109156 load_primitives(pt.home, name_space.copy())
110157
旧リポジトリブラウザで表示