Switch to tabs for indentation.

Instead of a mix of 2- and 4-space tabs just use actual tabs.  ;-P
This commit is contained in:
Simon Forman 2020-04-24 12:48:15 -07:00
parent 2fb610e733
commit 078f29830d
26 changed files with 4080 additions and 4080 deletions

View File

@ -16,17 +16,17 @@ import base64, os, io, zipfile
def initialize(joy_home): def initialize(joy_home):
Z.extractall(joy_home) Z.extractall(joy_home)
def create_data(from_dir='./default_joy_home'): def create_data(from_dir='./default_joy_home'):
f = io.StringIO() f = io.StringIO()
z = zipfile.ZipFile(f, mode='w') z = zipfile.ZipFile(f, mode='w')
for fn in os.listdir(from_dir): for fn in os.listdir(from_dir):
from_fn = os.path.join(from_dir, fn) from_fn = os.path.join(from_dir, fn)
z.write(from_fn, fn) z.write(from_fn, fn)
z.close() z.close()
return base64.encodestring(f.getvalue()) return base64.encodestring(f.getvalue())
Z = zipfile.ZipFile(io.StringIO(base64.decodestring('''\ Z = zipfile.ZipFile(io.StringIO(base64.decodestring('''\
@ -103,4 +103,4 @@ AAAAAAAAAAAAtIH2BgAAZGVmaW5pdGlvbnMudHh0UEsFBgAAAAAFAAUAHgEAAF0OAAAAAA==''')))
if __name__ == '__main__': if __name__ == '__main__':
print(create_data()) print(create_data())

View File

@ -23,10 +23,10 @@ repo = init_home(JOY_HOME)
_log = logging.getLogger(__name__) _log = logging.getLogger(__name__)
logging.basicConfig( logging.basicConfig(
format='%(asctime)-15s %(levelname)s %(name)s %(message)s', format='%(asctime)-15s %(levelname)s %(name)s %(message)s',
filename=os.path.join(JOY_HOME, 'thun.log'), filename=os.path.join(JOY_HOME, 'thun.log'),
level=logging.INFO, level=logging.INFO,
) )
_log.info('Starting with JOY_HOME=%s', JOY_HOME) _log.info('Starting with JOY_HOME=%s', JOY_HOME)
@ -39,73 +39,73 @@ from joy.utils.stack import stack_to_string
cp = RawConfigParser() cp = RawConfigParser()
cp.optionxform = str # Don't mess with uppercase. cp.optionxform = str # Don't mess with uppercase.
with open(os.path.join(args.joy_home, 'thun.config')) as f: with open(os.path.join(args.joy_home, 'thun.config')) as f:
cp.readfp(f) cp.readfp(f)
GLOBAL_COMMANDS = dict(cp.items('key bindings')) GLOBAL_COMMANDS = dict(cp.items('key bindings'))
def repo_relative_path(path): def repo_relative_path(path):
return os.path.relpath( return os.path.relpath(
path, path,
os.path.commonprefix((repo.controldir(), path)) os.path.commonprefix((repo.controldir(), path))
) )
def commands(): def commands():
# pylint: disable=unused-variable # pylint: disable=unused-variable
def key_bindings(*args): def key_bindings(*args):
commands = [ # These are bound in the TextViewerWidget. commands = [ # These are bound in the TextViewerWidget.
'Control-Enter - Run the selection as Joy code, or if there\'s no selection the line containing the cursor.', 'Control-Enter - Run the selection as Joy code, or if there\'s no selection the line containing the cursor.',
'F3 - Copy selection to stack.', 'F3 - Copy selection to stack.',
'Shift-F3 - Cut selection to stack.', 'Shift-F3 - Cut selection to stack.',
'F4 - Paste item on top of stack to insertion cursor.', 'F4 - Paste item on top of stack to insertion cursor.',
'Shift-F4 - Pop and paste top of stack to insertion cursor.', 'Shift-F4 - Pop and paste top of stack to insertion cursor.',
] ]
for key, command in GLOBAL_COMMANDS.items(): for key, command in GLOBAL_COMMANDS.items():
commands.append('%s - %s' % (key.lstrip('<').rstrip('>'), command)) commands.append('%s - %s' % (key.lstrip('<').rstrip('>'), command))
print('\n'.join([''] + sorted(commands))) print('\n'.join([''] + sorted(commands)))
return args return args
def mouse_bindings(*args): def mouse_bindings(*args):
print(dedent(''' print(dedent('''
Mouse button chords (to cancel a chord, click the third mouse button.) Mouse button chords (to cancel a chord, click the third mouse button.)
Left - Point, sweep selection Left - Point, sweep selection
Left-Middle - Copy the selection, place text on stack Left-Middle - Copy the selection, place text on stack
Left-Right - Run the selection as Joy code Left-Right - Run the selection as Joy code
Middle - Paste selection (bypass stack); click and drag to scroll. Middle - Paste selection (bypass stack); click and drag to scroll.
Middle-Left - Paste from top of stack, preserve Middle-Left - Paste from top of stack, preserve
Middle-Right - Paste from top of stack, pop Middle-Right - Paste from top of stack, pop
Right - Execute command word under mouse cursor Right - Execute command word under mouse cursor
Right-Left - Print docs of command word under mouse cursor Right-Left - Print docs of command word under mouse cursor
Right-Middle - Lookup word (kinda useless now) Right-Middle - Lookup word (kinda useless now)
''')) '''))
return args return args
def reset_log(*args): def reset_log(*args):
log.delete('0.0', tk.END) log.delete('0.0', tk.END)
print(__doc__) print(__doc__)
return args return args
def show_log(*args): def show_log(*args):
log_window.wm_deiconify() log_window.wm_deiconify()
log_window.update() log_window.update()
return args return args
def grand_reset(s, e, d): def grand_reset(s, e, d):
stack = world.load_stack() or () stack = world.load_stack() or ()
log.reset() log.reset()
t.reset() t.reset()
return stack, e, d return stack, e, d
return locals() return locals()
STACK_FN = os.path.join(JOY_HOME, 'stack.pickle') STACK_FN = os.path.join(JOY_HOME, 'stack.pickle')
@ -125,19 +125,19 @@ FONT = get_font('Iosevka', size=14) # Requires Tk root already set up.
log.init('Log', LOG_FN, repo_relative_path(LOG_FN), repo, FONT) log.init('Log', LOG_FN, repo_relative_path(LOG_FN), repo, FONT)
t.init('Joy - ' + JOY_HOME, JOY_FN, repo_relative_path(JOY_FN), repo, FONT) t.init('Joy - ' + JOY_HOME, JOY_FN, repo_relative_path(JOY_FN), repo, FONT)
for event, command in GLOBAL_COMMANDS.items(): for event, command in GLOBAL_COMMANDS.items():
callback = lambda _, _command=command: world.interpret(_command) callback = lambda _, _command=command: world.interpret(_command)
t.bind(event, callback) t.bind(event, callback)
log.bind(event, callback) log.bind(event, callback)
def main(): def main():
sys.stdout, old_stdout = FileFaker(log), sys.stdout sys.stdout, old_stdout = FileFaker(log), sys.stdout
try: try:
t.mainloop() t.mainloop()
finally: finally:
sys.stdout = old_stdout sys.stdout = old_stdout
return 0 return 0
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View File

@ -25,187 +25,187 @@ nothing = lambda event: None
class MouseBindingsMixin(object): class MouseBindingsMixin(object):
"""TextViewerWidget mixin class to provide mouse bindings.""" """TextViewerWidget mixin class to provide mouse bindings."""
def __init__(self): def __init__(self):
#Remember our mouse button state #Remember our mouse button state
self.B1_DOWN = False self.B1_DOWN = False
self.B2_DOWN = False self.B2_DOWN = False
self.B3_DOWN = False self.B3_DOWN = False
#Remember our pending action. #Remember our pending action.
self.dothis = nothing self.dothis = nothing
#We'll need to remember whether or not we've been moving B2. #We'll need to remember whether or not we've been moving B2.
self.beenMovingB2 = False self.beenMovingB2 = False
#Unbind the events we're interested in. #Unbind the events we're interested in.
for sequence in ( for sequence in (
"<Button-1>", "<B1-Motion>", "<ButtonRelease-1>", "<Button-1>", "<B1-Motion>", "<ButtonRelease-1>",
"<Button-2>", "<B2-Motion>", "<ButtonRelease-2>", "<Button-2>", "<B2-Motion>", "<ButtonRelease-2>",
"<Button-3>", "<B3-Motion>", "<ButtonRelease-3>", "<Button-3>", "<B3-Motion>", "<ButtonRelease-3>",
"<B1-Leave>", "<B2-Leave>", "<B3-Leave>", "<Any-Leave>", "<Leave>" "<B1-Leave>", "<B2-Leave>", "<B3-Leave>", "<Any-Leave>", "<Leave>"
): ):
self.unbind(sequence) self.unbind(sequence)
self.unbind_all(sequence) self.unbind_all(sequence)
self.event_delete('<<PasteSelection>>') #I forgot what this was for! :-P D'oh! self.event_delete('<<PasteSelection>>') #I forgot what this was for! :-P D'oh!
#Bind our event handlers to their events. #Bind our event handlers to their events.
self.bind("<Button-1>", self.B1d) self.bind("<Button-1>", self.B1d)
self.bind("<B1-Motion>", self.B1m) self.bind("<B1-Motion>", self.B1m)
self.bind("<ButtonRelease-1>", self.B1r) self.bind("<ButtonRelease-1>", self.B1r)
self.bind("<Button-2>", self.B2d) self.bind("<Button-2>", self.B2d)
self.bind("<B2-Motion>", self.B2m) self.bind("<B2-Motion>", self.B2m)
self.bind("<ButtonRelease-2>", self.B2r) self.bind("<ButtonRelease-2>", self.B2r)
self.bind("<Button-3>", self.B3d) self.bind("<Button-3>", self.B3d)
self.bind("<B3-Motion>", self.B3m) self.bind("<B3-Motion>", self.B3m)
self.bind("<ButtonRelease-3>", self.B3r) self.bind("<ButtonRelease-3>", self.B3r)
self.bind("<Any-Leave>", self.leave) self.bind("<Any-Leave>", self.leave)
self.bind("<Motion>", self.scan_command) self.bind("<Motion>", self.scan_command)
def B1d(self, event): def B1d(self, event):
'''button one pressed''' '''button one pressed'''
self.B1_DOWN = True self.B1_DOWN = True
if self.B2_DOWN: if self.B2_DOWN:
self.unhighlight_command() self.unhighlight_command()
if self.B3_DOWN : if self.B3_DOWN :
self.dothis = self.cancel self.dothis = self.cancel
else: else:
#copy TOS to the mouse (instead of system selection.) #copy TOS to the mouse (instead of system selection.)
self.dothis = self.copyto #middle-left-interclick self.dothis = self.copyto #middle-left-interclick
elif self.B3_DOWN : elif self.B3_DOWN :
self.unhighlight_command() self.unhighlight_command()
self.dothis = self.opendoc #right-left-interclick self.dothis = self.opendoc #right-left-interclick
else: else:
##button 1 down, set insertion and begin selection. ##button 1 down, set insertion and begin selection.
##Actually, do nothing. Tk Text widget defaults take care of it. ##Actually, do nothing. Tk Text widget defaults take care of it.
self.dothis = nothing self.dothis = nothing
return return
#Prevent further event handling by returning "break". #Prevent further event handling by returning "break".
return "break" return "break"
def B2d(self, event): def B2d(self, event):
'''button two pressed''' '''button two pressed'''
self.B2_DOWN = 1 self.B2_DOWN = 1
if self.B1_DOWN : if self.B1_DOWN :
if self.B3_DOWN : if self.B3_DOWN :
self.dothis = self.cancel self.dothis = self.cancel
else: else:
#left-middle-interclick - copy selection to stack #left-middle-interclick - copy selection to stack
self.dothis = self.copy_selection_to_stack self.dothis = self.copy_selection_to_stack
elif self.B3_DOWN : elif self.B3_DOWN :
self.unhighlight_command() self.unhighlight_command()
self.dothis = self.lookup #right-middle-interclick - lookup self.dothis = self.lookup #right-middle-interclick - lookup
else: else:
#middle-click - paste X selection to mouse pointer #middle-click - paste X selection to mouse pointer
self.set_insertion_point(event) self.set_insertion_point(event)
self.dothis = self.paste_X_selection_to_mouse_pointer self.dothis = self.paste_X_selection_to_mouse_pointer
return return
return "break" return "break"
def B3d(self, event): def B3d(self, event):
'''button three pressed''' '''button three pressed'''
self.B3_DOWN = 1 self.B3_DOWN = 1
if self.B1_DOWN : if self.B1_DOWN :
if self.B2_DOWN : if self.B2_DOWN :
self.dothis = self.cancel self.dothis = self.cancel
else: else:
#left-right-interclick - run selection #left-right-interclick - run selection
self.dothis = self.run_selection self.dothis = self.run_selection
elif self.B2_DOWN : elif self.B2_DOWN :
#middle-right-interclick - Pop/Cut from TOS to insertion cursor #middle-right-interclick - Pop/Cut from TOS to insertion cursor
self.unhighlight_command() self.unhighlight_command()
self.dothis = self.pastecut self.dothis = self.pastecut
else: else:
#right-click #right-click
self.CommandFirstDown(event) self.CommandFirstDown(event)
return "break" return "break"
def B1m(self, event): def B1m(self, event):
'''button one moved''' '''button one moved'''
if self.B2_DOWN or self.B3_DOWN: if self.B2_DOWN or self.B3_DOWN:
return "break" return "break"
def B2m(self, event): def B2m(self, event):
'''button two moved''' '''button two moved'''
if self.dothis == self.paste_X_selection_to_mouse_pointer and \ if self.dothis == self.paste_X_selection_to_mouse_pointer and \
not (self.B1_DOWN or self.B3_DOWN): not (self.B1_DOWN or self.B3_DOWN):
self.beenMovingB2 = True self.beenMovingB2 = True
return return
return "break" return "break"
def B3m(self, event): def B3m(self, event):
'''button three moved''' '''button three moved'''
if self.dothis == self.do_command and \ if self.dothis == self.do_command and \
not (self.B1_DOWN or self.B2_DOWN): not (self.B1_DOWN or self.B2_DOWN):
self.update_command_word(event) self.update_command_word(event)
return "break" return "break"
def scan_command(self, event): def scan_command(self, event):
self.update_command_word(event) self.update_command_word(event)
def B1r(self, event): def B1r(self, event):
'''button one released''' '''button one released'''
self.B1_DOWN = False self.B1_DOWN = False
if not (self.B2_DOWN or self.B3_DOWN): if not (self.B2_DOWN or self.B3_DOWN):
self.dothis(event) self.dothis(event)
return "break" return "break"
def B2r(self, event): def B2r(self, event):
'''button two released''' '''button two released'''
self.B2_DOWN = False self.B2_DOWN = False
if not (self.B1_DOWN or self.B3_DOWN or self.beenMovingB2): if not (self.B1_DOWN or self.B3_DOWN or self.beenMovingB2):
self.dothis(event) self.dothis(event)
self.beenMovingB2 = False self.beenMovingB2 = False
return "break" return "break"
def B3r(self, event): def B3r(self, event):
'''button three released''' '''button three released'''
self.B3_DOWN = False self.B3_DOWN = False
if not (self.B1_DOWN or self.B2_DOWN) : if not (self.B1_DOWN or self.B2_DOWN) :
self.dothis(event) self.dothis(event)
return "break" return "break"
def InsertFirstDown(self, event): def InsertFirstDown(self, event):
self.focus() self.focus()
self.dothis = nothing self.dothis = nothing
self.set_insertion_point(event) self.set_insertion_point(event)
def CommandFirstDown(self, event): def CommandFirstDown(self, event):
self.dothis = self.do_command self.dothis = self.do_command
self.update_command_word(event) self.update_command_word(event)

View File

@ -25,12 +25,12 @@ A Graphical User Interface for a dialect of Joy in Python.
The GUI The GUI
History History
Structure Structure
Commands Commands
Mouse Chords Mouse Chords
Keyboard Keyboard
Output from Joy Output from Joy
''' '''
@ -40,11 +40,11 @@ standard_library.install_aliases()
from builtins import str, map, object from builtins import str, map, object
from past.builtins import basestring from past.builtins import basestring
try: try:
import tkinter as tk import tkinter as tk
from tkinter.font import families, Font from tkinter.font import families, Font
except ImportError: except ImportError:
import Tkinter as tk import Tkinter as tk
from tkFont import families, Font from tkFont import families, Font
from re import compile as regular_expression from re import compile as regular_expression
from traceback import format_exc from traceback import format_exc
@ -58,17 +58,17 @@ from .world import World
def make_gui(dictionary): def make_gui(dictionary):
t = TextViewerWidget(World(dictionary=dictionary)) t = TextViewerWidget(World(dictionary=dictionary))
t['font'] = get_font() t['font'] = get_font()
t._root().title('Joy') t._root().title('Joy')
t.pack(expand=True, fill=tk.BOTH) t.pack(expand=True, fill=tk.BOTH)
return t return t
def get_font(family='EB Garamond', size=14): def get_font(family='EB Garamond', size=14):
if family not in families(): if family not in families():
family = 'Times' family = 'Times'
return Font(family=family, size=size) return Font(family=family, size=size)
#: Define mapping between Tkinter events and functions or methods. The #: Define mapping between Tkinter events and functions or methods. The
@ -77,85 +77,85 @@ def get_font(family='EB Garamond', size=14):
#: must return the actual callable to which to bind the event sequence. #: must return the actual callable to which to bind the event sequence.
TEXT_BINDINGS = { TEXT_BINDINGS = {
#I want to ensure that these keyboard shortcuts work. #I want to ensure that these keyboard shortcuts work.
'<Control-Return>': lambda tv: tv._control_enter, '<Control-Return>': lambda tv: tv._control_enter,
'<Control-v>': lambda tv: tv._paste, '<Control-v>': lambda tv: tv._paste,
'<Control-V>': lambda tv: tv._paste, '<Control-V>': lambda tv: tv._paste,
'<F3>': lambda tv: tv.copy_selection_to_stack, '<F3>': lambda tv: tv.copy_selection_to_stack,
'<F4>': lambda tv: tv.copyto, '<F4>': lambda tv: tv.copyto,
'<Shift-F3>': lambda tv: tv.cut, '<Shift-F3>': lambda tv: tv.cut,
'<Shift-F4>': lambda tv: tv.pastecut, '<Shift-F4>': lambda tv: tv.pastecut,
'<Shift-Insert>': lambda tv: tv._paste, '<Shift-Insert>': lambda tv: tv._paste,
} }
class SavingMixin(object): class SavingMixin(object):
def __init__(self, saver=None, filename=None, save_delay=2000): def __init__(self, saver=None, filename=None, save_delay=2000):
self.saver = self._saver if saver is None else saver self.saver = self._saver if saver is None else saver
self.filename = filename self.filename = filename
self._save_delay = save_delay self._save_delay = save_delay
self.tk.call(self._w, 'edit', 'modified', 0) self.tk.call(self._w, 'edit', 'modified', 0)
self.bind('<<Modified>>', self._beenModified) self.bind('<<Modified>>', self._beenModified)
self._resetting_modified_flag = False self._resetting_modified_flag = False
self._save = None self._save = None
def save(self): def save(self):
''' '''
Call _saveFunc() after a certain amount of idle time. Call _saveFunc() after a certain amount of idle time.
Called by _beenModified(). Called by _beenModified().
''' '''
self._cancelSave() self._cancelSave()
if self.saver: if self.saver:
self._saveAfter(self._save_delay) self._saveAfter(self._save_delay)
def _saveAfter(self, delay): def _saveAfter(self, delay):
''' '''
Trigger a cancel-able call to _saveFunc() after delay milliseconds. Trigger a cancel-able call to _saveFunc() after delay milliseconds.
''' '''
self._save = self.after(delay, self._saveFunc) self._save = self.after(delay, self._saveFunc)
def _saveFunc(self): def _saveFunc(self):
self._save = None self._save = None
self.saver(self._get_contents()) self.saver(self._get_contents())
def _saver(self, text): def _saver(self, text):
if not self.filename: if not self.filename:
return return
with open(self.filename, 'wb') as f: with open(self.filename, 'wb') as f:
os.chmod(self.filename, 0o600) os.chmod(self.filename, 0o600)
f.write(text.encode('UTF_8')) f.write(text.encode('UTF_8'))
f.flush() f.flush()
os.fsync(f.fileno()) os.fsync(f.fileno())
if hasattr(self, 'repo'): if hasattr(self, 'repo'):
self.repo.stage([self.repo_relative_filename]) self.repo.stage([self.repo_relative_filename])
self.world.save() self.world.save()
def _cancelSave(self): def _cancelSave(self):
if self._save is not None: if self._save is not None:
self.after_cancel(self._save) self.after_cancel(self._save)
self._save = None self._save = None
def _get_contents(self): def _get_contents(self):
self['state'] = 'disabled' self['state'] = 'disabled'
try: try:
return self.get('0.0', 'end')[:-1] return self.get('0.0', 'end')[:-1]
finally: finally:
self['state'] = 'normal' self['state'] = 'normal'
def _beenModified(self, event): def _beenModified(self, event):
if self._resetting_modified_flag: if self._resetting_modified_flag:
return return
self._clearModifiedFlag() self._clearModifiedFlag()
self.save() self.save()
def _clearModifiedFlag(self): def _clearModifiedFlag(self):
self._resetting_modified_flag = True self._resetting_modified_flag = True
try: try:
self.tk.call(self._w, 'edit', 'modified', 0) self.tk.call(self._w, 'edit', 'modified', 0)
finally: finally:
self._resetting_modified_flag = False self._resetting_modified_flag = False
## tags = self._saveTags() ## tags = self._saveTags()
## chunks = self.DUMP() ## chunks = self.DUMP()
@ -163,309 +163,309 @@ class SavingMixin(object):
class TextViewerWidget(tk.Text, MouseBindingsMixin, SavingMixin): class TextViewerWidget(tk.Text, MouseBindingsMixin, SavingMixin):
""" """
This class is a Tkinter Text with special mousebindings to make This class is a Tkinter Text with special mousebindings to make
it act as a Xerblin Text Viewer. it act as a Xerblin Text Viewer.
""" """
#This is a regular expression for finding commands in the text. #This is a regular expression for finding commands in the text.
command_re = regular_expression(r'[-a-zA-Z0-9_\\~/.:!@#$%&*?=+<>]+') command_re = regular_expression(r'[-a-zA-Z0-9_\\~/.:!@#$%&*?=+<>]+')
#These are the config tags for command text when it's highlighted. #These are the config tags for command text when it's highlighted.
command_tags = dict( command_tags = dict(
#underline = 1, #underline = 1,
#bgstipple = "gray50", #bgstipple = "gray50",
borderwidth = 2, borderwidth = 2,
relief=tk.RIDGE, relief=tk.RIDGE,
foreground = "green" foreground = "green"
) )
def __init__(self, world, master=None, **kw): def __init__(self, world, master=None, **kw):
self.world = world self.world = world
if self.world.text_widget is None: if self.world.text_widget is None:
self.world.text_widget = self self.world.text_widget = self
#Turn on undo, but don't override a passed-in setting. #Turn on undo, but don't override a passed-in setting.
kw.setdefault('undo', True) kw.setdefault('undo', True)
# kw.setdefault('bg', 'white') # kw.setdefault('bg', 'white')
kw.setdefault('wrap', 'word') kw.setdefault('wrap', 'word')
kw.setdefault('font', 'arial 12') kw.setdefault('font', 'arial 12')
text_bindings = kw.pop('text_bindings', TEXT_BINDINGS) text_bindings = kw.pop('text_bindings', TEXT_BINDINGS)
#Create ourselves as a Tkinter Text #Create ourselves as a Tkinter Text
tk.Text.__init__(self, master, **kw) tk.Text.__init__(self, master, **kw)
#Initialize our mouse mixin. #Initialize our mouse mixin.
MouseBindingsMixin.__init__(self) MouseBindingsMixin.__init__(self)
#Initialize our saver mixin. #Initialize our saver mixin.
SavingMixin.__init__(self) SavingMixin.__init__(self)
#Add tag config for command highlighting. #Add tag config for command highlighting.
self.tag_config('command', **self.command_tags) self.tag_config('command', **self.command_tags)
self.tag_config('bzzt', foreground = "orange") self.tag_config('bzzt', foreground = "orange")
self.tag_config('huh', foreground = "grey") self.tag_config('huh', foreground = "grey")
self.tag_config('number', foreground = "blue") self.tag_config('number', foreground = "blue")
#Create us a command instance variable #Create us a command instance variable
self.command = '' self.command = ''
#Activate event bindings. Modify text_bindings in your config #Activate event bindings. Modify text_bindings in your config
#file to affect the key bindings and whatnot here. #file to affect the key bindings and whatnot here.
for event_sequence, callback_finder in text_bindings.items(): for event_sequence, callback_finder in text_bindings.items():
callback = callback_finder(self) callback = callback_finder(self)
self.bind(event_sequence, callback) self.bind(event_sequence, callback)
## T.protocol("WM_DELETE_WINDOW", self.on_close) ## T.protocol("WM_DELETE_WINDOW", self.on_close)
def find_command_in_line(self, line, index): def find_command_in_line(self, line, index):
''' '''
Return the command at index in line and its begin and end indices. Return the command at index in line and its begin and end indices.
find_command_in_line(line, index) => command, begin, end find_command_in_line(line, index) => command, begin, end
''' '''
for match in self.command_re.finditer(line): for match in self.command_re.finditer(line):
b, e = match.span() b, e = match.span()
if b <= index <= e: if b <= index <= e:
return match.group(), b, e return match.group(), b, e
def paste_X_selection_to_mouse_pointer(self, event): def paste_X_selection_to_mouse_pointer(self, event):
'''Paste the X selection to the mouse pointer.''' '''Paste the X selection to the mouse pointer.'''
try: try:
text = self.selection_get() text = self.selection_get()
except tk.TclError: except tk.TclError:
return 'break' return 'break'
self.insert_it(text) self.insert_it(text)
def update_command_word(self, event): def update_command_word(self, event):
'''Highlight the command under the mouse.''' '''Highlight the command under the mouse.'''
self.unhighlight_command() self.unhighlight_command()
self.command = '' self.command = ''
index = '@%d,%d' % (event.x, event.y) index = '@%d,%d' % (event.x, event.y)
linestart = self.index(index + 'linestart') linestart = self.index(index + 'linestart')
lineend = self.index(index + 'lineend') lineend = self.index(index + 'lineend')
line = self.get(linestart, lineend) line = self.get(linestart, lineend)
row, offset = self._get_index(index) row, offset = self._get_index(index)
if offset >= len(line) or line[offset].isspace(): if offset >= len(line) or line[offset].isspace():
# The mouse is off the end of the line or on a space so there's no # The mouse is off the end of the line or on a space so there's no
# command, we're done. # command, we're done.
return return
cmd = self.find_command_in_line(line, offset) cmd = self.find_command_in_line(line, offset)
if cmd is None: if cmd is None:
return return
cmd, b, e = cmd cmd, b, e = cmd
if is_numerical(cmd): if is_numerical(cmd):
extra_tags = 'number', extra_tags = 'number',
elif self.world.has(cmd): elif self.world.has(cmd):
check = self.world.check(cmd) check = self.world.check(cmd)
if check: extra_tags = () if check: extra_tags = ()
elif check is None: extra_tags = 'huh', elif check is None: extra_tags = 'huh',
else: extra_tags = 'bzzt', else: extra_tags = 'bzzt',
else: else:
return return
self.command = cmd self.command = cmd
self.highlight_command( self.highlight_command(
'%d.%d' % (row, b), '%d.%d' % (row, b),
'%d.%d' % (row, e), '%d.%d' % (row, e),
*extra_tags) *extra_tags)
def highlight_command(self, from_, to, *extra_tags): def highlight_command(self, from_, to, *extra_tags):
'''Apply command style from from_ to to.''' '''Apply command style from from_ to to.'''
cmdstart = self.index(from_) cmdstart = self.index(from_)
cmdend = self.index(to) cmdend = self.index(to)
self.tag_add('command', cmdstart, cmdend) self.tag_add('command', cmdstart, cmdend)
for tag in extra_tags: for tag in extra_tags:
self.tag_add(tag, cmdstart, cmdend) self.tag_add(tag, cmdstart, cmdend)
def do_command(self, event): def do_command(self, event):
'''Do the currently highlighted command.''' '''Do the currently highlighted command.'''
self.unhighlight_command() self.unhighlight_command()
if self.command: if self.command:
self.run_command(self.command) self.run_command(self.command)
def _control_enter(self, event): def _control_enter(self, event):
select_indices = self.tag_ranges(tk.SEL) select_indices = self.tag_ranges(tk.SEL)
if select_indices: if select_indices:
command = self.get(select_indices[0], select_indices[1]) command = self.get(select_indices[0], select_indices[1])
else: else:
linestart = self.index(tk.INSERT + ' linestart') linestart = self.index(tk.INSERT + ' linestart')
lineend = self.index(tk.INSERT + ' lineend') lineend = self.index(tk.INSERT + ' lineend')
command = self.get(linestart, lineend) command = self.get(linestart, lineend)
if command and not command.isspace(): if command and not command.isspace():
self.run_command(command) self.run_command(command)
return 'break' return 'break'
def run_command(self, command): def run_command(self, command):
'''Given a string run it on the stack, report errors.''' '''Given a string run it on the stack, report errors.'''
try: try:
self.world.interpret(command) self.world.interpret(command)
except SystemExit: except SystemExit:
raise raise
except: except:
self.popupTB(format_exc().rstrip()) self.popupTB(format_exc().rstrip())
def unhighlight_command(self): def unhighlight_command(self):
'''Remove any command highlighting.''' '''Remove any command highlighting.'''
self.tag_remove('number', 1.0, tk.END) self.tag_remove('number', 1.0, tk.END)
self.tag_remove('huh', 1.0, tk.END) self.tag_remove('huh', 1.0, tk.END)
self.tag_remove('bzzt', 1.0, tk.END) self.tag_remove('bzzt', 1.0, tk.END)
self.tag_remove('command', 1.0, tk.END) self.tag_remove('command', 1.0, tk.END)
def set_insertion_point(self, event): def set_insertion_point(self, event):
'''Set the insertion cursor to the current mouse location.''' '''Set the insertion cursor to the current mouse location.'''
self.focus() self.focus()
self.mark_set(tk.INSERT, '@%d,%d' % (event.x, event.y)) self.mark_set(tk.INSERT, '@%d,%d' % (event.x, event.y))
def copy_selection_to_stack(self, event): def copy_selection_to_stack(self, event):
'''Copy selection to stack.''' '''Copy selection to stack.'''
select_indices = self.tag_ranges(tk.SEL) select_indices = self.tag_ranges(tk.SEL)
if select_indices: if select_indices:
s = self.get(select_indices[0], select_indices[1]) s = self.get(select_indices[0], select_indices[1])
self.world.push(s) self.world.push(s)
def cut(self, event): def cut(self, event):
'''Cut selection to stack.''' '''Cut selection to stack.'''
self.copy_selection_to_stack(event) self.copy_selection_to_stack(event)
# Let the pre-existing machinery take care of cutting the selection. # Let the pre-existing machinery take care of cutting the selection.
self.event_generate("<<Cut>>") self.event_generate("<<Cut>>")
def copyto(self, event): def copyto(self, event):
'''Actually "paste" from TOS''' '''Actually "paste" from TOS'''
s = self.world.peek() s = self.world.peek()
if s is not None: if s is not None:
self.insert_it(s) self.insert_it(s)
def insert_it(self, s): def insert_it(self, s):
if not isinstance(s, basestring): if not isinstance(s, basestring):
s = stack_to_string(s) s = stack_to_string(s)
# When pasting from the mouse we have to remove the current selection # When pasting from the mouse we have to remove the current selection
# to prevent destroying it by the paste operation. # to prevent destroying it by the paste operation.
select_indices = self.tag_ranges(tk.SEL) select_indices = self.tag_ranges(tk.SEL)
if select_indices: if select_indices:
# Set two marks to remember the selection. # Set two marks to remember the selection.
self.mark_set('_sel_start', select_indices[0]) self.mark_set('_sel_start', select_indices[0])
self.mark_set('_sel_end', select_indices[1]) self.mark_set('_sel_end', select_indices[1])
self.tag_remove(tk.SEL, 1.0, tk.END) self.tag_remove(tk.SEL, 1.0, tk.END)
self.insert(tk.INSERT, s) self.insert(tk.INSERT, s)
if select_indices: if select_indices:
self.tag_add(tk.SEL, '_sel_start', '_sel_end') self.tag_add(tk.SEL, '_sel_start', '_sel_end')
self.mark_unset('_sel_start') self.mark_unset('_sel_start')
self.mark_unset('_sel_end') self.mark_unset('_sel_end')
def run_selection(self, event): def run_selection(self, event):
'''Run the current selection if any on the stack.''' '''Run the current selection if any on the stack.'''
select_indices = self.tag_ranges(tk.SEL) select_indices = self.tag_ranges(tk.SEL)
if select_indices: if select_indices:
selection = self.get(select_indices[0], select_indices[1]) selection = self.get(select_indices[0], select_indices[1])
self.tag_remove(tk.SEL, 1.0, tk.END) self.tag_remove(tk.SEL, 1.0, tk.END)
self.run_command(selection) self.run_command(selection)
def pastecut(self, event): def pastecut(self, event):
'''Cut the TOS item to the mouse.''' '''Cut the TOS item to the mouse.'''
self.copyto(event) self.copyto(event)
self.world.pop() self.world.pop()
def opendoc(self, event): def opendoc(self, event):
'''OpenDoc the current command.''' '''OpenDoc the current command.'''
if self.command: if self.command:
self.world.do_opendoc(self.command) self.world.do_opendoc(self.command)
def lookup(self, event): def lookup(self, event):
'''Look up the current command.''' '''Look up the current command.'''
if self.command: if self.command:
self.world.do_lookup(self.command) self.world.do_lookup(self.command)
def cancel(self, event): def cancel(self, event):
'''Cancel whatever we're doing.''' '''Cancel whatever we're doing.'''
self.leave(None) self.leave(None)
self.tag_remove(tk.SEL, 1.0, tk.END) self.tag_remove(tk.SEL, 1.0, tk.END)
self._sel_anchor = '0.0' self._sel_anchor = '0.0'
self.mark_unset(tk.INSERT) self.mark_unset(tk.INSERT)
def leave(self, event): def leave(self, event):
'''Called when mouse leaves the Text window.''' '''Called when mouse leaves the Text window.'''
self.unhighlight_command() self.unhighlight_command()
self.command = '' self.command = ''
def _get_index(self, index): def _get_index(self, index):
'''Get the index in (int, int) form of index.''' '''Get the index in (int, int) form of index.'''
return tuple(map(int, self.index(index).split('.'))) return tuple(map(int, self.index(index).split('.')))
def _paste(self, event): def _paste(self, event):
'''Paste the system selection to the current selection, replacing it.''' '''Paste the system selection to the current selection, replacing it.'''
# If we're "key" pasting, we have to move the insertion point # If we're "key" pasting, we have to move the insertion point
# to the selection so the pasted text gets inserted at the # to the selection so the pasted text gets inserted at the
# location of the deleted selection. # location of the deleted selection.
select_indices = self.tag_ranges(tk.SEL) select_indices = self.tag_ranges(tk.SEL)
if select_indices: if select_indices:
# Mark the location of the current insertion cursor # Mark the location of the current insertion cursor
self.mark_set('tmark', tk.INSERT) self.mark_set('tmark', tk.INSERT)
# Put the insertion cursor at the selection # Put the insertion cursor at the selection
self.mark_set(tk.INSERT, select_indices[1]) self.mark_set(tk.INSERT, select_indices[1])
# Paste to the current selection, or if none, to the insertion cursor. # Paste to the current selection, or if none, to the insertion cursor.
self.event_generate("<<Paste>>") self.event_generate("<<Paste>>")
# If we mess with the insertion cursor above, fix it now. # If we mess with the insertion cursor above, fix it now.
if select_indices: if select_indices:
# Put the insertion cursor back where it was. # Put the insertion cursor back where it was.
self.mark_set(tk.INSERT, 'tmark') self.mark_set(tk.INSERT, 'tmark')
# And get rid of our unneeded mark. # And get rid of our unneeded mark.
self.mark_unset('tmark') self.mark_unset('tmark')
return 'break' return 'break'
def init(self, title, filename, repo_relative_filename, repo, font): def init(self, title, filename, repo_relative_filename, repo, font):
self.winfo_toplevel().title(title) self.winfo_toplevel().title(title)
if os.path.exists(filename): if os.path.exists(filename):
with open(filename) as f: with open(filename) as f:
data = f.read() data = f.read()
self.insert(tk.END, data) self.insert(tk.END, data)
# Prevent this from triggering a git commit. # Prevent this from triggering a git commit.
self.update() self.update()
self._cancelSave() self._cancelSave()
self.pack(expand=True, fill=tk.BOTH) self.pack(expand=True, fill=tk.BOTH)
self.filename = filename self.filename = filename
self.repo_relative_filename = repo_relative_filename self.repo_relative_filename = repo_relative_filename
self.repo = repo self.repo = repo
self['font'] = font # See below. self['font'] = font # See below.
def reset(self): def reset(self):
if os.path.exists(self.filename): if os.path.exists(self.filename):
with open(self.filename) as f: with open(self.filename) as f:
data = f.read() data = f.read()
if data: if data:
self.delete('0.0', tk.END) self.delete('0.0', tk.END)
self.insert(tk.END, data) self.insert(tk.END, data)
def popupTB(self, tb): def popupTB(self, tb):
top = tk.Toplevel() top = tk.Toplevel()
T = TextViewerWidget( T = TextViewerWidget(
self.world, self.world,
top, top,
width=max(len(s) for s in tb.splitlines()) + 3, width=max(len(s) for s in tb.splitlines()) + 3,
) )
T['background'] = 'darkgrey' T['background'] = 'darkgrey'
T['foreground'] = 'darkblue' T['foreground'] = 'darkblue'
T.tag_config('err', foreground='yellow') T.tag_config('err', foreground='yellow')
T.insert(tk.END, tb) T.insert(tk.END, tb)
last_line = str(int(T.index(tk.END).split('.')[0]) - 1) + '.0' last_line = str(int(T.index(tk.END).split('.')[0]) - 1) + '.0'
T.tag_add('err', last_line, tk.END) T.tag_add('err', last_line, tk.END)
T['state'] = tk.DISABLED T['state'] = tk.DISABLED
top.title(T.get(last_line, tk.END).strip()) top.title(T.get(last_line, tk.END).strip())
T.pack(expand=1, fill=tk.BOTH) T.pack(expand=1, fill=tk.BOTH)
T.see(tk.END) T.see(tk.END)

View File

@ -13,85 +13,85 @@ DEFAULT_JOY_HOME = expanduser(join('~', '.joypy'))
def is_numerical(s): def is_numerical(s):
try: try:
float(s) float(s)
except ValueError: except ValueError:
return False return False
return True return True
def home_dir(path): def home_dir(path):
'''Return the absolute path of an existing directory.''' '''Return the absolute path of an existing directory.'''
fullpath = expanduser(path) if path.startswith('~') else abspath(path) fullpath = expanduser(path) if path.startswith('~') else abspath(path)
if not exists(fullpath): if not exists(fullpath):
if path == DEFAULT_JOY_HOME: if path == DEFAULT_JOY_HOME:
print('Creating JOY_HOME', repr(fullpath)) print('Creating JOY_HOME', repr(fullpath))
mkdir(fullpath, 0o700) mkdir(fullpath, 0o700)
else: else:
print(repr(fullpath), "doesn't exist.", file=sys.stderr) print(repr(fullpath), "doesn't exist.", file=sys.stderr)
raise ValueError(path) raise ValueError(path)
return fullpath return fullpath
def init_home(fullpath): def init_home(fullpath):
''' '''
Open or create the Repo. Open or create the Repo.
If there are contents in the dir but it's not a git repo, quit. If there are contents in the dir but it's not a git repo, quit.
''' '''
try: try:
repo = Repo(fullpath) repo = Repo(fullpath)
except NotGitRepository: except NotGitRepository:
print(repr(fullpath), "no repository", file=sys.stderr) print(repr(fullpath), "no repository", file=sys.stderr)
if listdir(fullpath): if listdir(fullpath):
print(repr(fullpath), "has contents\nQUIT.", file=sys.stderr) print(repr(fullpath), "has contents\nQUIT.", file=sys.stderr)
sys.exit(2) sys.exit(2)
print('Initializing repository in', fullpath) print('Initializing repository in', fullpath)
repo = init_repo(fullpath) repo = init_repo(fullpath)
print('Using repository in', fullpath) print('Using repository in', fullpath)
return repo return repo
def init_repo(repo_dir): def init_repo(repo_dir):
''' '''
Create a repo, load the initial content, and make the first commit. Create a repo, load the initial content, and make the first commit.
Return the Repo object. Return the Repo object.
''' '''
repo = Repo.init(repo_dir) repo = Repo.init(repo_dir)
import joy.gui.init_joy_home import joy.gui.init_joy_home
joy.gui.init_joy_home.initialize(repo_dir) joy.gui.init_joy_home.initialize(repo_dir)
repo.stage([fn for fn in listdir(repo_dir) if isfile(join(repo_dir, fn))]) repo.stage([fn for fn in listdir(repo_dir) if isfile(join(repo_dir, fn))])
repo.do_commit('Initial commit.', committer=COMMITTER) repo.do_commit('Initial commit.', committer=COMMITTER)
return repo return repo
argparser = argparse.ArgumentParser( argparser = argparse.ArgumentParser(
description='Experimental Brutalist UI for Joy.', description='Experimental Brutalist UI for Joy.',
) )
argparser.add_argument( argparser.add_argument(
'-j', '--joy-home', '-j', '--joy-home',
help='Use a directory other than %s as JOY_HOME' % DEFAULT_JOY_HOME, help='Use a directory other than %s as JOY_HOME' % DEFAULT_JOY_HOME,
default=DEFAULT_JOY_HOME, default=DEFAULT_JOY_HOME,
dest='joy_home', dest='joy_home',
type=home_dir, type=home_dir,
) )
class FileFaker(object): class FileFaker(object):
def __init__(self, T): def __init__(self, T):
self.T = T self.T = T
def write(self, text): def write(self, text):
self.T.insert('end', text) self.T.insert('end', text)
self.T.see('end') self.T.see('end')
def flush(self): def flush(self):
pass pass

View File

@ -35,118 +35,118 @@ from .utils import is_numerical
class World(object): class World(object):
def __init__(self, stack=(), dictionary=None, text_widget=None): def __init__(self, stack=(), dictionary=None, text_widget=None):
self.stack = stack self.stack = stack
self.dictionary = dictionary or {} self.dictionary = dictionary or {}
self.text_widget = text_widget self.text_widget = text_widget
self.check_cache = {} self.check_cache = {}
def check(self, name): def check(self, name):
try: try:
res = self.check_cache[name] res = self.check_cache[name]
except KeyError: except KeyError:
res = self.check_cache[name] = type_check(name, self.stack) res = self.check_cache[name] = type_check(name, self.stack)
return res return res
def do_lookup(self, name): def do_lookup(self, name):
if name in self.dictionary: if name in self.dictionary:
self.stack = (Symbol(name), ()), self.stack self.stack = (Symbol(name), ()), self.stack
self.print_stack() self.print_stack()
self.check_cache.clear() self.check_cache.clear()
else: else:
assert is_numerical(name) assert is_numerical(name)
self.interpret(name) self.interpret(name)
def do_opendoc(self, name): def do_opendoc(self, name):
if is_numerical(name): if is_numerical(name):
print('The number', name) print('The number', name)
else: else:
try: try:
word = self.dictionary[name] word = self.dictionary[name]
except KeyError: except KeyError:
print(repr(name), '???') print(repr(name), '???')
else: else:
print(getdoc(word)) print(getdoc(word))
self.print_stack() self.print_stack()
def pop(self): def pop(self):
if self.stack: if self.stack:
self.stack = self.stack[1] self.stack = self.stack[1]
self.print_stack() self.print_stack()
self.check_cache.clear() self.check_cache.clear()
def push(self, it): def push(self, it):
it = it.encode('utf8') it = it.encode('utf8')
self.stack = it, self.stack self.stack = it, self.stack
self.print_stack() self.print_stack()
self.check_cache.clear() self.check_cache.clear()
def peek(self): def peek(self):
if self.stack: if self.stack:
return self.stack[0] return self.stack[0]
def interpret(self, command): def interpret(self, command):
if self.has(command) and self.check(command) == False: # not in {True, None}: if self.has(command) and self.check(command) == False: # not in {True, None}:
return return
old_stack = self.stack old_stack = self.stack
try: try:
self.stack, _, self.dictionary = run( self.stack, _, self.dictionary = run(
command, command,
self.stack, self.stack,
self.dictionary, self.dictionary,
) )
finally: finally:
self.print_stack() self.print_stack()
if old_stack != self.stack: if old_stack != self.stack:
self.check_cache.clear() self.check_cache.clear()
def has(self, name): def has(self, name):
return name in self.dictionary return name in self.dictionary
def save(self): def save(self):
pass pass
def print_stack(self): def print_stack(self):
stack_out_index = self.text_widget.search('<' 'STACK', 1.0) stack_out_index = self.text_widget.search('<' 'STACK', 1.0)
if stack_out_index: if stack_out_index:
self.text_widget.see(stack_out_index) self.text_widget.see(stack_out_index)
s = stack_to_string(self.stack) + '\n' s = stack_to_string(self.stack) + '\n'
self.text_widget.insert(stack_out_index, s) self.text_widget.insert(stack_out_index, s)
class StackDisplayWorld(World): class StackDisplayWorld(World):
def __init__(self, repo, filename, rel_filename, dictionary=None, text_widget=None): def __init__(self, repo, filename, rel_filename, dictionary=None, text_widget=None):
self.filename = filename self.filename = filename
stack = self.load_stack() or () stack = self.load_stack() or ()
World.__init__(self, stack, dictionary, text_widget) World.__init__(self, stack, dictionary, text_widget)
self.repo = repo self.repo = repo
self.relative_STACK_FN = rel_filename self.relative_STACK_FN = rel_filename
def interpret(self, command): def interpret(self, command):
command = command.strip() command = command.strip()
if self.has(command) and self.check(command) == False: # not in {True, None}: if self.has(command) and self.check(command) == False: # not in {True, None}:
return return
print('\njoy?', command) print('\njoy?', command)
super(StackDisplayWorld, self).interpret(command) super(StackDisplayWorld, self).interpret(command)
def print_stack(self): def print_stack(self):
print('\n%s <-' % stack_to_string(self.stack)) print('\n%s <-' % stack_to_string(self.stack))
def save(self): def save(self):
with open(self.filename, 'wb') as f: with open(self.filename, 'wb') as f:
os.chmod(self.filename, 0o600) os.chmod(self.filename, 0o600)
pickle.dump(self.stack, f, protocol=2) pickle.dump(self.stack, f, protocol=2)
f.flush() f.flush()
os.fsync(f.fileno()) os.fsync(f.fileno())
self.repo.stage([self.relative_STACK_FN]) self.repo.stage([self.relative_STACK_FN])
commit_id = self.repo.do_commit( commit_id = self.repo.do_commit(
b'auto-save', b'auto-save',
committer=b'thun-auto-save <nobody@example.com>', committer=b'thun-auto-save <nobody@example.com>',
) )
_log.info('commit %s', commit_id) _log.info('commit %s', commit_id)
def load_stack(self): def load_stack(self):
if os.path.exists(self.filename): if os.path.exists(self.filename):
with open(self.filename, 'rb') as f: with open(self.filename, 'rb') as f:
return pickle.load(f) return pickle.load(f)

View File

@ -32,86 +32,86 @@ from .utils.pretty_print import TracePrinter
def joy(stack, expression, dictionary, viewer=None): def joy(stack, expression, dictionary, viewer=None):
'''Evaluate a Joy expression on a stack. '''Evaluate a Joy expression on a stack.
This function iterates through a sequence of terms which are either This function iterates through a sequence of terms which are either
literals (strings, numbers, sequences of terms) or function symbols. literals (strings, numbers, sequences of terms) or function symbols.
Literals are put onto the stack and functions are looked up in the Literals are put onto the stack and functions are looked up in the
disctionary and executed. disctionary and executed.
The viewer is a function that is called with the stack and expression The viewer is a function that is called with the stack and expression
on every iteration, its return value is ignored. on every iteration, its return value is ignored.
:param stack stack: The stack. :param stack stack: The stack.
:param stack expression: The expression to evaluate. :param stack expression: The expression to evaluate.
:param dict dictionary: A ``dict`` mapping names to Joy functions. :param dict dictionary: A ``dict`` mapping names to Joy functions.
:param function viewer: Optional viewer function. :param function viewer: Optional viewer function.
:rtype: (stack, (), dictionary) :rtype: (stack, (), dictionary)
''' '''
while expression: while expression:
if viewer: viewer(stack, expression) if viewer: viewer(stack, expression)
term, expression = expression term, expression = expression
if isinstance(term, Symbol): if isinstance(term, Symbol):
term = dictionary[term] term = dictionary[term]
stack, expression, dictionary = term(stack, expression, dictionary) stack, expression, dictionary = term(stack, expression, dictionary)
else: else:
stack = term, stack stack = term, stack
if viewer: viewer(stack, expression) if viewer: viewer(stack, expression)
return stack, expression, dictionary return stack, expression, dictionary
def run(text, stack, dictionary, viewer=None): def run(text, stack, dictionary, viewer=None):
''' '''
Return the stack resulting from running the Joy code text on the stack. Return the stack resulting from running the Joy code text on the stack.
:param str text: Joy code. :param str text: Joy code.
:param stack stack: The stack. :param stack stack: The stack.
:param dict dictionary: A ``dict`` mapping names to Joy functions. :param dict dictionary: A ``dict`` mapping names to Joy functions.
:param function viewer: Optional viewer function. :param function viewer: Optional viewer function.
:rtype: (stack, (), dictionary) :rtype: (stack, (), dictionary)
''' '''
expression = text_to_expression(text) expression = text_to_expression(text)
return joy(stack, expression, dictionary, viewer) return joy(stack, expression, dictionary, viewer)
def repl(stack=(), dictionary=None): def repl(stack=(), dictionary=None):
''' '''
Read-Evaluate-Print Loop Read-Evaluate-Print Loop
Accept input and run it on the stack, loop. Accept input and run it on the stack, loop.
:param stack stack: The stack. :param stack stack: The stack.
:param dict dictionary: A ``dict`` mapping names to Joy functions. :param dict dictionary: A ``dict`` mapping names to Joy functions.
:rtype: stack :rtype: stack
''' '''
if dictionary is None: if dictionary is None:
dictionary = {} dictionary = {}
try: try:
while True: while True:
print() print()
print(stack_to_string(stack), '<-top') print(stack_to_string(stack), '<-top')
print() print()
try: try:
text = input('joy? ') text = input('joy? ')
except (EOFError, KeyboardInterrupt): except (EOFError, KeyboardInterrupt):
break break
viewer = TracePrinter() viewer = TracePrinter()
try: try:
stack, _, dictionary = run(text, stack, dictionary, viewer.viewer) stack, _, dictionary = run(text, stack, dictionary, viewer.viewer)
except: except:
exc = format_exc() # Capture the exception. exc = format_exc() # Capture the exception.
viewer.print_() # Print the Joy trace. viewer.print_() # Print the Joy trace.
print('-' * 73) print('-' * 73)
print(exc) # Print the original exception. print(exc) # Print the original exception.
else: else:
viewer.print_() viewer.print_()
except: except:
print_exc() print_exc()
print() print()
return stack return stack

File diff suppressed because it is too large Load Diff

View File

@ -51,74 +51,74 @@ BLANKS = r'\s+'
class Symbol(str): class Symbol(str):
'''A string class that represents Joy function names.''' '''A string class that represents Joy function names.'''
__repr__ = str.__str__ __repr__ = str.__str__
def text_to_expression(text): def text_to_expression(text):
'''Convert a string to a Joy expression. '''Convert a string to a Joy expression.
When supplied with a string this function returns a Python datastructure When supplied with a string this function returns a Python datastructure
that represents the Joy datastructure described by the text expression. that represents the Joy datastructure described by the text expression.
Any unbalanced square brackets will raise a ParseError. Any unbalanced square brackets will raise a ParseError.
:param str text: Text to convert. :param str text: Text to convert.
:rtype: stack :rtype: stack
:raises ParseError: if the parse fails. :raises ParseError: if the parse fails.
''' '''
return _parse(_tokenize(text)) return _parse(_tokenize(text))
class ParseError(ValueError): class ParseError(ValueError):
'''Raised when there is a error while parsing text.''' '''Raised when there is a error while parsing text.'''
def _tokenize(text): def _tokenize(text):
'''Convert a text into a stream of tokens. '''Convert a text into a stream of tokens.
Converts function names to Symbols. Converts function names to Symbols.
Raise ParseError (with some of the failing text) if the scan fails. Raise ParseError (with some of the failing text) if the scan fails.
''' '''
tokens, rest = _scanner.scan(text) tokens, rest = _scanner.scan(text)
if rest: if rest:
raise ParseError( raise ParseError(
'Scan failed at position %i, %r' 'Scan failed at position %i, %r'
% (len(text) - len(rest), rest[:10]) % (len(text) - len(rest), rest[:10])
) )
return tokens return tokens
def _parse(tokens): def _parse(tokens):
''' '''
Return a stack/list expression of the tokens. Return a stack/list expression of the tokens.
''' '''
frame = [] frame = []
stack = [] stack = []
for tok in tokens: for tok in tokens:
if tok == '[': if tok == '[':
stack.append(frame) stack.append(frame)
frame = [] frame = []
stack[-1].append(frame) stack[-1].append(frame)
elif tok == ']': elif tok == ']':
try: try:
frame = stack.pop() frame = stack.pop()
except IndexError: except IndexError:
raise ParseError('Extra closing bracket.') raise ParseError('Extra closing bracket.')
frame[-1] = list_to_stack(frame[-1]) frame[-1] = list_to_stack(frame[-1])
else: else:
frame.append(tok) frame.append(tok)
if stack: if stack:
raise ParseError('Unclosed bracket.') raise ParseError('Unclosed bracket.')
return list_to_stack(frame) return list_to_stack(frame)
_scanner = Scanner([ _scanner = Scanner([
(FLOAT, lambda _, token: float(token)), (FLOAT, lambda _, token: float(token)),
(INT, lambda _, token: int(token)), (INT, lambda _, token: int(token)),
(SYMBOL, lambda _, token: Symbol(token)), (SYMBOL, lambda _, token: Symbol(token)),
(BRACKETS, lambda _, token: token), (BRACKETS, lambda _, token: token),
(STRING_DOUBLE_QUOTED, lambda _, token: token[1:-1].replace('\\"', '"')), (STRING_DOUBLE_QUOTED, lambda _, token: token[1:-1].replace('\\"', '"')),
(STRING_SINGLE_QUOTED, lambda _, token: token[1:-1].replace("\\'", "'")), (STRING_SINGLE_QUOTED, lambda _, token: token[1:-1].replace("\\'", "'")),
(BLANKS, None), (BLANKS, None),
]) ])

View File

@ -54,43 +54,43 @@ function name! Hopefully they will discover this documentation.
def rename_code_object(new_name): def rename_code_object(new_name):
''' '''
If you want to wrap a function in another function and have the wrapped If you want to wrap a function in another function and have the wrapped
function's name show up in the traceback when an exception occurs in function's name show up in the traceback when an exception occurs in
the wrapper function, you must do this brutal hackery to change the the wrapper function, you must do this brutal hackery to change the
func.__code__.co_name attribute. Just functools.wraps() is not enough. func.__code__.co_name attribute. Just functools.wraps() is not enough.
See: See:
https://stackoverflow.com/questions/29919804/function-decorated-using-functools-wraps-raises-typeerror-with-the-name-of-the-w https://stackoverflow.com/questions/29919804/function-decorated-using-functools-wraps-raises-typeerror-with-the-name-of-the-w
https://stackoverflow.com/questions/29488327/changing-the-name-of-a-generator/29488561#29488561 https://stackoverflow.com/questions/29488327/changing-the-name-of-a-generator/29488561#29488561
I'm just glad it's possible. I'm just glad it's possible.
''' '''
def inner(func): def inner(func):
name = new_name + ':' + func.__name__ name = new_name + ':' + func.__name__
code_object = func.__code__ code_object = func.__code__
return type(func)( return type(func)(
type(code_object)( type(code_object)(
code_object.co_argcount, code_object.co_argcount,
code_object.co_nlocals, code_object.co_nlocals,
code_object.co_stacksize, code_object.co_stacksize,
code_object.co_flags, code_object.co_flags,
code_object.co_code, code_object.co_code,
code_object.co_consts, code_object.co_consts,
code_object.co_names, code_object.co_names,
code_object.co_varnames, code_object.co_varnames,
code_object.co_filename, code_object.co_filename,
name, name,
code_object.co_firstlineno, code_object.co_firstlineno,
code_object.co_lnotab, code_object.co_lnotab,
code_object.co_freevars, code_object.co_freevars,
code_object.co_cellvars code_object.co_cellvars
), ),
func.__globals__, func.__globals__,
name, name,
func.__defaults__, func.__defaults__,
func.__closure__ func.__closure__
) )
return inner return inner

View File

@ -21,49 +21,49 @@ from functools import reduce
def import_yin(): def import_yin():
from joy.utils.generated_library import * from joy.utils.generated_library import *
return locals() return locals()
class InfiniteStack(tuple): class InfiniteStack(tuple):
def _names(): def _names():
n = 0 n = 0
while True: while True:
m = yield Symbol('a' + str(n)) m = yield Symbol('a' + str(n))
n = n + 1 if m is None else m n = n + 1 if m is None else m
_NAMES = _names() _NAMES = _names()
next(_NAMES) next(_NAMES)
names = lambda: next(_NAMES) names = lambda: next(_NAMES)
reset = lambda _self, _n=_NAMES: _n.send(-1) reset = lambda _self, _n=_NAMES: _n.send(-1)
def __init__(self, code): def __init__(self, code):
self.reset() self.reset()
self.code = code self.code = code
def __iter__(self): def __iter__(self):
if not self: if not self:
new_var = self.names() new_var = self.names()
self.code.append(('pop', new_var)) self.code.append(('pop', new_var))
return iter((new_var, self)) return iter((new_var, self))
def I(expression): def I(expression):
code = [] code = []
stack = InfiniteStack(code) stack = InfiniteStack(code)
while expression: while expression:
term, expression = expression term, expression = expression
if isinstance(term, Symbol): if isinstance(term, Symbol):
func = D[term] func = D[term]
stack, expression, _ = func(stack, expression, code) stack, expression, _ = func(stack, expression, code)
else: else:
stack = term, stack stack = term, stack
code.append(tuple(['ret'] + list(iter_stack(stack)))) code.append(tuple(['ret'] + list(iter_stack(stack))))
return code return code
strtup = lambda a, b: '(%s, %s)' % (b, a) strtup = lambda a, b: '(%s, %s)' % (b, a)
@ -71,123 +71,123 @@ strstk = lambda rest: reduce(strtup, rest, 'stack')
def code_gen(code): def code_gen(code):
#for p in code: print p #for p in code: print p
coalesce_pops(code) coalesce_pops(code)
lines = [] lines = []
emit = lines.append emit = lines.append
for t in code: for t in code:
tag, rest = t[0], t[1:] tag, rest = t[0], t[1:]
if tag == 'pop': emit(strstk(rest) + ' = stack') if tag == 'pop': emit(strstk(rest) + ' = stack')
elif tag == 'call': emit('%s = %s%s' % rest) elif tag == 'call': emit('%s = %s%s' % rest)
elif tag == 'ret': emit('return ' + strstk(rest[::-1])) elif tag == 'ret': emit('return ' + strstk(rest[::-1]))
else: else:
raise ValueError(tag) raise ValueError(tag)
return '\n'.join(' ' + line for line in lines) return '\n'.join(' ' + line for line in lines)
def coalesce_pops(code): def coalesce_pops(code):
code.sort(key=lambda p: p[0] != 'pop') # All pops to the front. code.sort(key=lambda p: p[0] != 'pop') # All pops to the front.
try: index = next((i for i, t in enumerate(code) if t[0] != 'pop')) try: index = next((i for i, t in enumerate(code) if t[0] != 'pop'))
except StopIteration: return except StopIteration: return
code[:index] = [tuple(['pop'] + [t for _, t in code[:index][::-1]])] code[:index] = [tuple(['pop'] + [t for _, t in code[:index][::-1]])]
def compile_yinyang(name, text): def compile_yinyang(name, text):
return ''' return '''
def %s(stack): def %s(stack):
%s %s
''' % (name, code_gen(I(text_to_expression(text)))) ''' % (name, code_gen(I(text_to_expression(text))))
def q(): def q():
memo = {} memo = {}
def bar(type_var): def bar(type_var):
try: try:
res = memo[type_var] res = memo[type_var]
except KeyError: except KeyError:
res = memo[type_var] = InfiniteStack.names() res = memo[type_var] = InfiniteStack.names()
return res return res
return bar return bar
def type_vars_to_labels(thing, map_): def type_vars_to_labels(thing, map_):
if not thing: if not thing:
return thing return thing
if not isinstance(thing, tuple): if not isinstance(thing, tuple):
return map_(thing) return map_(thing)
return tuple(type_vars_to_labels(inner, map_) for inner in thing) return tuple(type_vars_to_labels(inner, map_) for inner in thing)
def remap_inputs(in_, stack, code): def remap_inputs(in_, stack, code):
map_ = q() map_ = q()
while in_: while in_:
term, in_ = in_ term, in_ = in_
arg0, stack = stack arg0, stack = stack
term = type_vars_to_labels(term, map_) term = type_vars_to_labels(term, map_)
code.append(('call', term, '', arg0)) code.append(('call', term, '', arg0))
return stack, map_ return stack, map_
class BinaryBuiltin(object): class BinaryBuiltin(object):
def __init__(self, name): def __init__(self, name):
self.name = name self.name = name
def __call__(self, stack, expression, code): def __call__(self, stack, expression, code):
in1, (in0, stack) = stack in1, (in0, stack) = stack
out = InfiniteStack.names() out = InfiniteStack.names()
code.append(('call', out, self.name, (in0, in1))) code.append(('call', out, self.name, (in0, in1)))
return (out, stack), expression, code return (out, stack), expression, code
YIN = import_yin() YIN = import_yin()
D = { D = {
name: SimpleFunctionWrapper(YIN[name]) name: SimpleFunctionWrapper(YIN[name])
for name in ''' for name in '''
ccons ccons
cons cons
dup dup
dupd dupd
dupdd dupdd
over over
pop pop
popd popd
popdd popdd
popop popop
popopd popopd
popopdd popopdd
rolldown rolldown
rollup rollup
swap swap
swons swons
tuck tuck
unit unit
'''.split() '''.split()
} }
for name in ''' for name in '''
first first
first_two first_two
fourth fourth
rest rest
rrest rrest
second second
third third
uncons uncons
unswons unswons
'''.split(): '''.split():
def foo(stack, expression, code, name=name): def foo(stack, expression, code, name=name):
in_, out = YIN_STACK_EFFECTS[name] in_, out = YIN_STACK_EFFECTS[name]
stack, map_ = remap_inputs(in_, stack, code) stack, map_ = remap_inputs(in_, stack, code)
out = type_vars_to_labels(out, map_) out = type_vars_to_labels(out, map_)
return concat(out, stack), expression, code return concat(out, stack), expression, code
foo.__name__ = name foo.__name__ = name
D[name] = foo D[name] = foo
for name in ''' for name in '''
@ -210,18 +210,18 @@ for name in '''
sub sub
truediv truediv
'''.split(): '''.split():
D[name.rstrip('-')] = BinaryBuiltin(name) D[name.rstrip('-')] = BinaryBuiltin(name)
''' '''
stack stack
stuncons stuncons
stununcons stununcons
swaack swaack
''' '''
for name in sorted(D): for name in sorted(D):
print(name, end=' ') print(name, end=' ')
## print compile_yinyang(name, name) ## print compile_yinyang(name, name)
print('-' * 100) print('-' * 100)

View File

@ -3,19 +3,19 @@ from joy.parser import Symbol
def _names(): def _names():
n = 0 n = 0
while True: while True:
yield Symbol('a' + str(n)) yield Symbol('a' + str(n))
n += 1 n += 1
class InfiniteStack(tuple): class InfiniteStack(tuple):
names = lambda n=_names(): next(n) names = lambda n=_names(): next(n)
def __iter__(self): def __iter__(self):
if not self: if not self:
return iter((self.names(), self)) return iter((self.names(), self))
i = InfiniteStack() i = InfiniteStack()
@ -23,9 +23,9 @@ i = InfiniteStack()
a, b = i a, b = i
lambda u: (lambda fu, u: fu * fu * u)( lambda u: (lambda fu, u: fu * fu * u)(
(lambda u: (lambda fu, u: fu * fu)( (lambda u: (lambda fu, u: fu * fu)(
(lambda u: (lambda fu, u: fu * fu * u)( (lambda u: (lambda fu, u: fu * fu * u)(
(lambda u: 1)(u), u))(u), u))(u), (lambda u: 1)(u), u))(u), u))(u),
u) u)
lambda u: (lambda fu, u: fu * fu * u)((lambda u: (lambda fu, u: fu * fu)((lambda u: (lambda fu, u: fu * fu * u)((lambda u: 1)(u), u))(u), u))(u), u) lambda u: (lambda fu, u: fu * fu * u)((lambda u: (lambda fu, u: fu * fu)((lambda u: (lambda fu, u: fu * fu * u)((lambda u: 1)(u), u))(u), u))(u), u)

View File

@ -46,54 +46,54 @@ from .stack import expression_to_string, stack_to_string
class TracePrinter(object): class TracePrinter(object):
''' '''
This is what does the formatting. You instantiate it and pass the ``viewer()`` This is what does the formatting. You instantiate it and pass the ``viewer()``
method to the :py:func:`joy.joy.joy` function, then print it to see the method to the :py:func:`joy.joy.joy` function, then print it to see the
trace. trace.
''' '''
def __init__(self): def __init__(self):
self.history = [] self.history = []
def viewer(self, stack, expression): def viewer(self, stack, expression):
''' '''
Record the current stack and expression in the TracePrinter's history. Record the current stack and expression in the TracePrinter's history.
Pass this method as the ``viewer`` argument to the :py:func:`joy.joy.joy` function. Pass this method as the ``viewer`` argument to the :py:func:`joy.joy.joy` function.
:param stack quote: A stack. :param stack quote: A stack.
:param stack expression: A stack. :param stack expression: A stack.
''' '''
self.history.append((stack, expression)) self.history.append((stack, expression))
def __str__(self): def __str__(self):
return '\n'.join(self.go()) return '\n'.join(self.go())
def go(self): def go(self):
''' '''
Return a list of strings, one for each entry in the history, prefixed Return a list of strings, one for each entry in the history, prefixed
with enough spaces to align all the interpreter dots. with enough spaces to align all the interpreter dots.
This method is called internally by the ``__str__()`` method. This method is called internally by the ``__str__()`` method.
:rtype: list(str) :rtype: list(str)
''' '''
max_stack_length = 0 max_stack_length = 0
lines = [] lines = []
for stack, expression in self.history: for stack, expression in self.history:
stack = stack_to_string(stack) stack = stack_to_string(stack)
expression = expression_to_string(expression) expression = expression_to_string(expression)
n = len(stack) n = len(stack)
if n > max_stack_length: if n > max_stack_length:
max_stack_length = n max_stack_length = n
lines.append((n, '%s . %s' % (stack, expression))) lines.append((n, '%s . %s' % (stack, expression)))
return [ # Prefix spaces to line up '.'s. return [ # Prefix spaces to line up '.'s.
(' ' * (max_stack_length - length) + line) (' ' * (max_stack_length - length) + line)
for length, line in lines for length, line in lines
] ]
def print_(self): def print_(self):
try: try:
print(self) print(self)
except: except:
print_exc() print_exc()
print('Exception while printing viewer.') print('Exception while printing viewer.')

View File

@ -43,8 +43,8 @@ means we can directly "unpack" the expected arguments to a Joy function.
For example:: For example::
def dup((head, tail)): def dup((head, tail)):
return head, (head, tail) return head, (head, tail)
We replace the argument "stack" by the expected structure of the stack, We replace the argument "stack" by the expected structure of the stack,
in this case "(head, tail)", and Python takes care of unpacking the in this case "(head, tail)", and Python takes care of unpacking the
@ -56,9 +56,9 @@ Unfortunately, the Sphinx documentation generator, which is used to generate thi
web page, doesn't handle tuples in the function parameters. And in Python 3, this web page, doesn't handle tuples in the function parameters. And in Python 3, this
syntax was removed entirely. Instead you would have to write:: syntax was removed entirely. Instead you would have to write::
def dup(stack): def dup(stack):
head, tail = stack head, tail = stack
return head, (head, tail) return head, (head, tail)
We have two very simple functions, one to build up a stack from a Python We have two very simple functions, one to build up a stack from a Python
@ -74,95 +74,95 @@ printed left-to-right. These functions are written to support :doc:`../pretty`.
from builtins import map from builtins import map
def list_to_stack(el, stack=()): def list_to_stack(el, stack=()):
'''Convert a Python list (or other sequence) to a Joy stack:: '''Convert a Python list (or other sequence) to a Joy stack::
[1, 2, 3] -> (1, (2, (3, ()))) [1, 2, 3] -> (1, (2, (3, ())))
:param list el: A Python list or other sequence (iterators and generators :param list el: A Python list or other sequence (iterators and generators
won't work because ``reverse()`` is called on ``el``.) won't work because ``reverse()`` is called on ``el``.)
:param stack stack: A stack, optional, defaults to the empty stack. :param stack stack: A stack, optional, defaults to the empty stack.
:rtype: stack :rtype: stack
''' '''
for item in reversed(el): for item in reversed(el):
stack = item, stack stack = item, stack
return stack return stack
def iter_stack(stack): def iter_stack(stack):
'''Iterate through the items on the stack. '''Iterate through the items on the stack.
:param stack stack: A stack. :param stack stack: A stack.
:rtype: iterator :rtype: iterator
''' '''
while stack: while stack:
item, stack = stack item, stack = stack
yield item yield item
def stack_to_string(stack): def stack_to_string(stack):
''' '''
Return a "pretty print" string for a stack. Return a "pretty print" string for a stack.
The items are written right-to-left:: The items are written right-to-left::
(top, (second, ...)) -> '... second top' (top, (second, ...)) -> '... second top'
:param stack stack: A stack. :param stack stack: A stack.
:rtype: str :rtype: str
''' '''
f = lambda stack: reversed(list(iter_stack(stack))) f = lambda stack: reversed(list(iter_stack(stack)))
return _to_string(stack, f) return _to_string(stack, f)
def expression_to_string(expression): def expression_to_string(expression):
''' '''
Return a "pretty print" string for a expression. Return a "pretty print" string for a expression.
The items are written left-to-right:: The items are written left-to-right::
(top, (second, ...)) -> 'top second ...' (top, (second, ...)) -> 'top second ...'
:param stack expression: A stack. :param stack expression: A stack.
:rtype: str :rtype: str
''' '''
return _to_string(expression, iter_stack) return _to_string(expression, iter_stack)
def _to_string(stack, f): def _to_string(stack, f):
if not isinstance(stack, tuple): return repr(stack) if not isinstance(stack, tuple): return repr(stack)
if not stack: return '' # shortcut if not stack: return '' # shortcut
return ' '.join(map(_s, f(stack))) return ' '.join(map(_s, f(stack)))
_s = lambda s: ( _s = lambda s: (
'[%s]' % expression_to_string(s) if isinstance(s, tuple) '[%s]' % expression_to_string(s) if isinstance(s, tuple)
else repr(s) else repr(s)
) )
def concat(quote, expression): def concat(quote, expression):
'''Concatinate quote onto expression. '''Concatinate quote onto expression.
In joy [1 2] [3 4] would become [1 2 3 4]. In joy [1 2] [3 4] would become [1 2 3 4].
:param stack quote: A stack. :param stack quote: A stack.
:param stack expression: A stack. :param stack expression: A stack.
:raises RuntimeError: if quote is larger than sys.getrecursionlimit(). :raises RuntimeError: if quote is larger than sys.getrecursionlimit().
:rtype: stack :rtype: stack
''' '''
# This is the fastest implementation, but will trigger # This is the fastest implementation, but will trigger
# RuntimeError: maximum recursion depth exceeded # RuntimeError: maximum recursion depth exceeded
# on quotes longer than sys.getrecursionlimit(). # on quotes longer than sys.getrecursionlimit().
return (quote[0], concat(quote[1], expression)) if quote else expression return (quote[0], concat(quote[1], expression)) if quote else expression
# Original implementation. # Original implementation.
## return list_to_stack(list(iter_stack(quote)), expression) ## return list_to_stack(list(iter_stack(quote)), expression)
# In-lining is slightly faster (and won't break the # In-lining is slightly faster (and won't break the
# recursion limit on long quotes.) # recursion limit on long quotes.)
## temp = [] ## temp = []
## while quote: ## while quote:
@ -175,23 +175,23 @@ def concat(quote, expression):
def pick(stack, n): def pick(stack, n):
''' '''
Return the nth item on the stack. Return the nth item on the stack.
:param stack stack: A stack. :param stack stack: A stack.
:param int n: An index into the stack. :param int n: An index into the stack.
:raises ValueError: if ``n`` is less than zero. :raises ValueError: if ``n`` is less than zero.
:raises IndexError: if ``n`` is equal to or greater than the length of ``stack``. :raises IndexError: if ``n`` is equal to or greater than the length of ``stack``.
:rtype: whatever :rtype: whatever
''' '''
if n < 0: if n < 0:
raise ValueError raise ValueError
while True: while True:
try: try:
item, stack = stack item, stack = stack
except ValueError: except ValueError:
raise IndexError raise IndexError
n -= 1 n -= 1
if n < 0: if n < 0:
break break
return item return item

File diff suppressed because it is too large Load Diff

View File

@ -48,18 +48,18 @@ GREEN = 70, 200, 70
MOUSE_EVENTS = frozenset({ MOUSE_EVENTS = frozenset({
pygame.MOUSEMOTION, pygame.MOUSEMOTION,
pygame.MOUSEBUTTONDOWN, pygame.MOUSEBUTTONDOWN,
pygame.MOUSEBUTTONUP pygame.MOUSEBUTTONUP
}) })
'PyGame mouse events.' 'PyGame mouse events.'
ARROW_KEYS = frozenset({ ARROW_KEYS = frozenset({
pygame.K_UP, pygame.K_UP,
pygame.K_DOWN, pygame.K_DOWN,
pygame.K_LEFT, pygame.K_LEFT,
pygame.K_RIGHT pygame.K_RIGHT
}) })
'PyGame arrow key events.' 'PyGame arrow key events.'
@ -82,201 +82,201 @@ SUCCESS = 1
class Message(object): class Message(object):
'''Message base class. Contains ``sender`` field.''' '''Message base class. Contains ``sender`` field.'''
def __init__(self, sender): def __init__(self, sender):
self.sender = sender self.sender = sender
class CommandMessage(Message): class CommandMessage(Message):
'''For commands, adds ``command`` field.''' '''For commands, adds ``command`` field.'''
def __init__(self, sender, command): def __init__(self, sender, command):
Message.__init__(self, sender) Message.__init__(self, sender)
self.command = command self.command = command
class ModifyMessage(Message): class ModifyMessage(Message):
''' '''
For when resources are modified, adds ``subject`` and ``details`` For when resources are modified, adds ``subject`` and ``details``
fields. fields.
''' '''
def __init__(self, sender, subject, **details): def __init__(self, sender, subject, **details):
Message.__init__(self, sender) Message.__init__(self, sender)
self.subject = subject self.subject = subject
self.details = details self.details = details
class OpenMessage(Message): class OpenMessage(Message):
''' '''
For when resources are modified, adds ``name``, content_id``, For when resources are modified, adds ``name``, content_id``,
``status``, and ``traceback`` fields. ``status``, and ``traceback`` fields.
''' '''
def __init__(self, sender, name): def __init__(self, sender, name):
Message.__init__(self, sender) Message.__init__(self, sender)
self.name = name self.name = name
self.content_id = self.thing = None self.content_id = self.thing = None
self.status = PENDING self.status = PENDING
self.traceback = None self.traceback = None
class PersistMessage(Message): class PersistMessage(Message):
''' '''
For when resources are modified, adds ``content_id`` and ``details`` For when resources are modified, adds ``content_id`` and ``details``
fields. fields.
''' '''
def __init__(self, sender, content_id, **details): def __init__(self, sender, content_id, **details):
Message.__init__(self, sender) Message.__init__(self, sender)
self.content_id = content_id self.content_id = content_id
self.details = details self.details = details
class ShutdownMessage(Message): class ShutdownMessage(Message):
'''Signals that the system is shutting down.''' '''Signals that the system is shutting down.'''
# Joy Interpreter & Context # Joy Interpreter & Context
class World(object): class World(object):
''' '''
This object contains the system context, the stack, dictionary, a This object contains the system context, the stack, dictionary, a
reference to the display broadcast method, and the log. reference to the display broadcast method, and the log.
''' '''
def __init__(self, stack_id, stack_holder, dictionary, notify, log): def __init__(self, stack_id, stack_holder, dictionary, notify, log):
self.stack_holder = stack_holder self.stack_holder = stack_holder
self.dictionary = dictionary self.dictionary = dictionary
self.notify = notify self.notify = notify
self.stack_id = stack_id self.stack_id = stack_id
self.log = log.lines self.log = log.lines
self.log_id = log.content_id self.log_id = log.content_id
def handle(self, message): def handle(self, message):
''' '''
Deal with updates to the stack and commands. Deal with updates to the stack and commands.
''' '''
if (isinstance(message, ModifyMessage) if (isinstance(message, ModifyMessage)
and message.subject is self.stack_holder and message.subject is self.stack_holder
): ):
self._log_lines('', '%s <-' % self.format_stack()) self._log_lines('', '%s <-' % self.format_stack())
if not isinstance(message, CommandMessage): if not isinstance(message, CommandMessage):
return return
c, s, d = message.command, self.stack_holder[0], self.dictionary c, s, d = message.command, self.stack_holder[0], self.dictionary
self._log_lines('', '-> %s' % (c,)) self._log_lines('', '-> %s' % (c,))
self.stack_holder[0], _, self.dictionary = run(c, s, d) self.stack_holder[0], _, self.dictionary = run(c, s, d)
mm = ModifyMessage(self, self.stack_holder, content_id=self.stack_id) mm = ModifyMessage(self, self.stack_holder, content_id=self.stack_id)
self.notify(mm) self.notify(mm)
def _log_lines(self, *lines): def _log_lines(self, *lines):
self.log.extend(lines) self.log.extend(lines)
self.notify(ModifyMessage(self, self.log, content_id=self.log_id)) self.notify(ModifyMessage(self, self.log, content_id=self.log_id))
def format_stack(self): def format_stack(self):
try: try:
return stack_to_string(self.stack_holder[0]) return stack_to_string(self.stack_holder[0])
except: except:
print(format_exc(), file=stderr) print(format_exc(), file=stderr)
return str(self.stack_holder[0]) return str(self.stack_holder[0])
def push(sender, item, notify, stack_name='stack.pickle'): def push(sender, item, notify, stack_name='stack.pickle'):
''' '''
Helper function to push an item onto the system stack with message. Helper function to push an item onto the system stack with message.
''' '''
om = OpenMessage(sender, stack_name) om = OpenMessage(sender, stack_name)
notify(om) notify(om)
if om.status == SUCCESS: if om.status == SUCCESS:
om.thing[0] = item, om.thing[0] om.thing[0] = item, om.thing[0]
notify(ModifyMessage(sender, om.thing, content_id=om.content_id)) notify(ModifyMessage(sender, om.thing, content_id=om.content_id))
return om.status return om.status
def open_viewer_on_string(sender, content, notify): def open_viewer_on_string(sender, content, notify):
''' '''
Helper function to open a text viewer on a string. Helper function to open a text viewer on a string.
Typically used to show tracebacks. Typically used to show tracebacks.
''' '''
push(sender, content, notify) push(sender, content, notify)
notify(CommandMessage(sender, 'good_viewer_location open_viewer')) notify(CommandMessage(sender, 'good_viewer_location open_viewer'))
# main loop # main loop
class TheLoop(object): class TheLoop(object):
''' '''
The main loop manages tasks and the PyGame event queue The main loop manages tasks and the PyGame event queue
and framerate clock. and framerate clock.
''' '''
FRAME_RATE = 24 FRAME_RATE = 24
def __init__(self, display, clock): def __init__(self, display, clock):
self.display = display self.display = display
self.clock = clock self.clock = clock
self.tasks = {} self.tasks = {}
self.running = False self.running = False
def install_task(self, F, milliseconds): def install_task(self, F, milliseconds):
''' '''
Install a task to run every so many milliseconds. Install a task to run every so many milliseconds.
''' '''
try: try:
task_event_id = AVAILABLE_TASK_EVENTS.pop() task_event_id = AVAILABLE_TASK_EVENTS.pop()
except KeyError: except KeyError:
raise RuntimeError('out of task ids') raise RuntimeError('out of task ids')
self.tasks[task_event_id] = F self.tasks[task_event_id] = F
pygame.time.set_timer(task_event_id, milliseconds) pygame.time.set_timer(task_event_id, milliseconds)
return task_event_id return task_event_id
def remove_task(self, task_event_id): def remove_task(self, task_event_id):
''' '''
Remove an installed task. Remove an installed task.
''' '''
assert task_event_id in self.tasks, repr(task_event_id) assert task_event_id in self.tasks, repr(task_event_id)
pygame.time.set_timer(task_event_id, 0) pygame.time.set_timer(task_event_id, 0)
del self.tasks[task_event_id] del self.tasks[task_event_id]
AVAILABLE_TASK_EVENTS.add(task_event_id) AVAILABLE_TASK_EVENTS.add(task_event_id)
def __del__(self): def __del__(self):
# Best effort to cancel all running tasks. # Best effort to cancel all running tasks.
for task_event_id in self.tasks: for task_event_id in self.tasks:
pygame.time.set_timer(task_event_id, 0) pygame.time.set_timer(task_event_id, 0)
def run_task(self, task_event_id): def run_task(self, task_event_id):
''' '''
Give a task its time to shine. Give a task its time to shine.
''' '''
task = self.tasks[task_event_id] task = self.tasks[task_event_id]
try: try:
task() task()
except: except:
traceback = format_exc() traceback = format_exc()
self.remove_task(task_event_id) self.remove_task(task_event_id)
print(traceback, file=stderr) print(traceback, file=stderr)
print('TASK removed due to ERROR', task, file=stderr) print('TASK removed due to ERROR', task, file=stderr)
open_viewer_on_string(self, traceback, self.display.broadcast) open_viewer_on_string(self, traceback, self.display.broadcast)
def loop(self): def loop(self):
''' '''
The actual main loop machinery. The actual main loop machinery.
Maintain a ``running`` flag, pump the PyGame event queue and Maintain a ``running`` flag, pump the PyGame event queue and
handle the events (dispatching to the display), tick the clock. handle the events (dispatching to the display), tick the clock.
When the loop is exited (by clicking the window close button or When the loop is exited (by clicking the window close button or
pressing the ``escape`` key) it broadcasts a ``ShutdownMessage``. pressing the ``escape`` key) it broadcasts a ``ShutdownMessage``.
''' '''
self.running = True self.running = True
while self.running: while self.running:
for event in pygame.event.get(): for event in pygame.event.get():
if event.type == pygame.QUIT: if event.type == pygame.QUIT:
self.running = False self.running = False
elif event.type == pygame.KEYUP and event.key == pygame.K_ESCAPE: elif event.type == pygame.KEYUP and event.key == pygame.K_ESCAPE:
self.running = False self.running = False
elif event.type in self.tasks: elif event.type in self.tasks:
self.run_task(event.type) self.run_task(event.type)
else: else:
self.display.dispatch_event(event) self.display.dispatch_event(event)
pygame.display.update() pygame.display.update()
self.clock.tick(self.FRAME_RATE) self.clock.tick(self.FRAME_RATE)
self.display.broadcast(ShutdownMessage(self)) self.display.broadcast(ShutdownMessage(self))

View File

@ -3,17 +3,17 @@ import sys, traceback
# To enable "hot" reloading in the IDLE shell. # To enable "hot" reloading in the IDLE shell.
for name in 'core main display viewer text_viewer stack_viewer persist_task'.split(): for name in 'core main display viewer text_viewer stack_viewer persist_task'.split():
try: try:
del sys.modules[name] del sys.modules[name]
except KeyError: except KeyError:
pass pass
from . import main from . import main
try: try:
A = A # (screen, clock, pt), three things that we DON'T want to recreate A = A # (screen, clock, pt), three things that we DON'T want to recreate
# each time we restart main(). # each time we restart main().
except NameError: except NameError:
A = main.init() A = main.init()
d = main.main(*A) d = main.main(*A)

View File

@ -38,473 +38,473 @@ from sys import stderr
from traceback import format_exc from traceback import format_exc
import pygame import pygame
from .core import ( from .core import (
open_viewer_on_string, open_viewer_on_string,
GREY, GREY,
MOUSE_EVENTS, MOUSE_EVENTS,
) )
from .viewer import Viewer from .viewer import Viewer
from joy.vui import text_viewer from joy.vui import text_viewer
class Display(object): class Display(object):
''' '''
Manage tracks and viewers on a screen (Pygame surface.) Manage tracks and viewers on a screen (Pygame surface.)
The size and number of tracks are defined by passing in at least two The size and number of tracks are defined by passing in at least two
ratios, e.g. Display(screen, 1, 4, 4) would create three tracks, one ratios, e.g. Display(screen, 1, 4, 4) would create three tracks, one
small one on the left and two larger ones of the same size, each four small one on the left and two larger ones of the same size, each four
times wider than the left one. times wider than the left one.
All tracks take up the whole height of the display screen. Tracks All tracks take up the whole height of the display screen. Tracks
manage zero or more Viewers. When you "grow" a viewer a new track is manage zero or more Viewers. When you "grow" a viewer a new track is
created that overlays or hides one or two existing tracks, and when created that overlays or hides one or two existing tracks, and when
the last viewer in an overlay track is closed the track closes too the last viewer in an overlay track is closed the track closes too
and reveals the hidden tracks (and their viewers, if any.) and reveals the hidden tracks (and their viewers, if any.)
In order to facilitate command underlining while mouse dragging the In order to facilitate command underlining while mouse dragging the
lookup parameter must be a function that accepts a string and returns lookup parameter must be a function that accepts a string and returns
a Boolean indicating whether that string is a valid Joy function name. a Boolean indicating whether that string is a valid Joy function name.
Typically you pass in the __contains__ method of the Joy dict. This Typically you pass in the __contains__ method of the Joy dict. This
is a case of breaking "loose coupling" to gain efficiency, as otherwise is a case of breaking "loose coupling" to gain efficiency, as otherwise
we would have to e.g. send some sort of lookup message to the we would have to e.g. send some sort of lookup message to the
World context object, going through the whole Display.broadcast() World context object, going through the whole Display.broadcast()
machinery, etc. Not something you want to do on each MOUSEMOTION machinery, etc. Not something you want to do on each MOUSEMOTION
event. event.
''' '''
def __init__(self, screen, lookup, *track_ratios): def __init__(self, screen, lookup, *track_ratios):
self.screen = screen self.screen = screen
self.w, self.h = screen.get_width(), screen.get_height() self.w, self.h = screen.get_width(), screen.get_height()
self.lookup = lookup self.lookup = lookup
self.focused_viewer = None self.focused_viewer = None
self.tracks = [] # (x, track) self.tracks = [] # (x, track)
self.handlers = [] # Non-viewers that should receive messages. self.handlers = [] # Non-viewers that should receive messages.
# Create the tracks. # Create the tracks.
if not track_ratios: track_ratios = 1, 4 if not track_ratios: track_ratios = 1, 4
x, total = 0, sum(track_ratios) x, total = 0, sum(track_ratios)
for ratio in track_ratios[:-1]: for ratio in track_ratios[:-1]:
track_width = old_div(self.w * ratio, total) track_width = old_div(self.w * ratio, total)
assert track_width >= 10 # minimum width 10 pixels assert track_width >= 10 # minimum width 10 pixels
self._open_track(x, track_width) self._open_track(x, track_width)
x += track_width x += track_width
self._open_track(x, self.w - x) self._open_track(x, self.w - x)
def _open_track(self, x, w): def _open_track(self, x, w):
'''Helper function to create the pygame surface and Track.''' '''Helper function to create the pygame surface and Track.'''
track_surface = self.screen.subsurface((x, 0, w, self.h)) track_surface = self.screen.subsurface((x, 0, w, self.h))
self.tracks.append((x, Track(track_surface))) self.tracks.append((x, Track(track_surface)))
def open_viewer(self, x, y, class_): def open_viewer(self, x, y, class_):
''' '''
Open a viewer of class_ at the x, y location on the display, Open a viewer of class_ at the x, y location on the display,
return the viewer. return the viewer.
''' '''
track = self._track_at(x)[0] track = self._track_at(x)[0]
V = track.open_viewer(y, class_) V = track.open_viewer(y, class_)
V.focus(self) V.focus(self)
return V return V
def close_viewer(self, viewer): def close_viewer(self, viewer):
'''Close the viewer.''' '''Close the viewer.'''
for x, track in self.tracks: for x, track in self.tracks:
if track.close_viewer(viewer): if track.close_viewer(viewer):
if not track.viewers and track.hiding: if not track.viewers and track.hiding:
i = self.tracks.index((x, track)) i = self.tracks.index((x, track))
self.tracks[i:i + 1] = track.hiding self.tracks[i:i + 1] = track.hiding
assert sorted(self.tracks) == self.tracks assert sorted(self.tracks) == self.tracks
for _, exposed_track in track.hiding: for _, exposed_track in track.hiding:
exposed_track.redraw() exposed_track.redraw()
if viewer is self.focused_viewer: if viewer is self.focused_viewer:
self.focused_viewer = None self.focused_viewer = None
break break
def change_viewer(self, viewer, y, relative=False): def change_viewer(self, viewer, y, relative=False):
''' '''
Adjust the top of the viewer to a new y within the boundaries of Adjust the top of the viewer to a new y within the boundaries of
its neighbors. its neighbors.
If relative is False new_y should be in screen coords, else new_y If relative is False new_y should be in screen coords, else new_y
should be relative to the top of the viewer. should be relative to the top of the viewer.
''' '''
for _, track in self.tracks: for _, track in self.tracks:
if track.change_viewer(viewer, y, relative): if track.change_viewer(viewer, y, relative):
break break
def grow_viewer(self, viewer): def grow_viewer(self, viewer):
''' '''
Cause the viewer to take up its whole track or, if it does Cause the viewer to take up its whole track or, if it does
already, take up another track, up to the whole screen. already, take up another track, up to the whole screen.
This is the inverse of closing a viewer. "Growing" a viewer This is the inverse of closing a viewer. "Growing" a viewer
actually creates a new copy and a new track to hold it. The old actually creates a new copy and a new track to hold it. The old
tracks and viewers are retained, and they get restored when the tracks and viewers are retained, and they get restored when the
covering track closes, which happens automatically when the last covering track closes, which happens automatically when the last
viewer in the covering track is closed. viewer in the covering track is closed.
''' '''
for x, track in self.tracks: for x, track in self.tracks:
for _, V in track.viewers: for _, V in track.viewers:
if V is viewer: if V is viewer:
return self._grow_viewer(x, track, viewer) return self._grow_viewer(x, track, viewer)
def _grow_viewer(self, x, track, viewer): def _grow_viewer(self, x, track, viewer):
'''Helper function to "grow" a viewer.''' '''Helper function to "grow" a viewer.'''
new_viewer = None new_viewer = None
if viewer.h < self.h: if viewer.h < self.h:
# replace the track with a new track that contains # replace the track with a new track that contains
# a copy of the viewer at full height. # a copy of the viewer at full height.
new_track = Track(track.surface) # Reuse it, why not? new_track = Track(track.surface) # Reuse it, why not?
new_viewer = copy(viewer) new_viewer = copy(viewer)
new_track._grow_by(new_viewer, 0, self.h - viewer.h) new_track._grow_by(new_viewer, 0, self.h - viewer.h)
new_track.viewers.append((0, new_viewer)) new_track.viewers.append((0, new_viewer))
new_track.hiding = [(x, track)] new_track.hiding = [(x, track)]
self.tracks[self.tracks.index((x, track))] = x, new_track self.tracks[self.tracks.index((x, track))] = x, new_track
elif viewer.w < self.w: elif viewer.w < self.w:
# replace two tracks # replace two tracks
i = self.tracks.index((x, track)) i = self.tracks.index((x, track))
try: # prefer the one on the right try: # prefer the one on the right
xx, xtrack = self.tracks[i + 1] xx, xtrack = self.tracks[i + 1]
except IndexError: except IndexError:
i -= 1 # okay, the one on the left i -= 1 # okay, the one on the left
xx, xtrack = self.tracks[i] xx, xtrack = self.tracks[i]
hiding = [(xx, xtrack), (x, track)] hiding = [(xx, xtrack), (x, track)]
else: else:
hiding = [(x, track), (xx, xtrack)] hiding = [(x, track), (xx, xtrack)]
# We know there has to be at least one other track because it # We know there has to be at least one other track because it
# there weren't then that implies that the one track takes up # there weren't then that implies that the one track takes up
# the whole display screen (the only way you can get just one # the whole display screen (the only way you can get just one
# track is by growing a viewer to cover the whole screen.) # track is by growing a viewer to cover the whole screen.)
# Ergo, viewer.w == self.w, so this branch doesn't run. # Ergo, viewer.w == self.w, so this branch doesn't run.
new_x = min(x, xx) new_x = min(x, xx)
new_w = track.w + xtrack.w new_w = track.w + xtrack.w
r = new_x, 0, new_w, self.h r = new_x, 0, new_w, self.h
new_track = Track(self.screen.subsurface(r)) new_track = Track(self.screen.subsurface(r))
new_viewer = copy(viewer) new_viewer = copy(viewer)
r = 0, 0, new_w, self.h r = 0, 0, new_w, self.h
new_viewer.resurface(new_track.surface.subsurface(r)) new_viewer.resurface(new_track.surface.subsurface(r))
new_track.viewers.append((0, new_viewer)) new_track.viewers.append((0, new_viewer))
new_track.hiding = hiding new_track.hiding = hiding
self.tracks[i:i + 2] = [(new_x, new_track)] self.tracks[i:i + 2] = [(new_x, new_track)]
new_viewer.draw() new_viewer.draw()
return new_viewer return new_viewer
def _move_viewer(self, to, rel_y, viewer, _x, y): def _move_viewer(self, to, rel_y, viewer, _x, y):
''' '''
Helper function to move (really copy) a viewer to a new location. Helper function to move (really copy) a viewer to a new location.
''' '''
h = to.split(rel_y) h = to.split(rel_y)
new_viewer = copy(viewer) new_viewer = copy(viewer)
if not isinstance(to, Track): if not isinstance(to, Track):
to = next(T for _, T in self.tracks to = next(T for _, T in self.tracks
for _, V in T.viewers for _, V in T.viewers
if V is to) if V is to)
new_viewer.resurface(to.surface.subsurface((0, y, to.w, h))) new_viewer.resurface(to.surface.subsurface((0, y, to.w, h)))
to.viewers.append((y, new_viewer)) to.viewers.append((y, new_viewer))
to.viewers.sort() # bisect.insort() would be overkill here. to.viewers.sort() # bisect.insort() would be overkill here.
new_viewer.draw() new_viewer.draw()
self.close_viewer(viewer) self.close_viewer(viewer)
def _track_at(self, x): def _track_at(self, x):
''' '''
Return the track at x along with the track-relative x coordinate, Return the track at x along with the track-relative x coordinate,
raise ValueError if x is off-screen. raise ValueError if x is off-screen.
''' '''
for track_x, track in self.tracks: for track_x, track in self.tracks:
if x < track_x + track.w: if x < track_x + track.w:
return track, x - track_x return track, x - track_x
raise ValueError('x outside display: %r' % (x,)) raise ValueError('x outside display: %r' % (x,))
def at(self, x, y): def at(self, x, y):
''' '''
Return the viewer (which can be a Track) at the x, y location, Return the viewer (which can be a Track) at the x, y location,
along with the relative-to-viewer-surface x and y coordinates. along with the relative-to-viewer-surface x and y coordinates.
If there is no viewer at the location the Track will be returned If there is no viewer at the location the Track will be returned
instead. instead.
''' '''
track, x = self._track_at(x) track, x = self._track_at(x)
viewer, y = track.viewer_at(y) viewer, y = track.viewer_at(y)
return viewer, x, y return viewer, x, y
def iter_viewers(self): def iter_viewers(self):
''' '''
Iterate through all viewers yielding (viewer, x, y) three-tuples. Iterate through all viewers yielding (viewer, x, y) three-tuples.
The x and y coordinates are screen pixels of the top-left corner The x and y coordinates are screen pixels of the top-left corner
of the viewer. of the viewer.
''' '''
for x, T in self.tracks: for x, T in self.tracks:
for y, V in T.viewers: for y, V in T.viewers:
yield V, x, y yield V, x, y
def done_resizing(self): def done_resizing(self):
''' '''
Helper method called directly by ``MenuViewer.mouse_up()`` to (hackily) Helper method called directly by ``MenuViewer.mouse_up()`` to (hackily)
update the display when done resizing a viewer. update the display when done resizing a viewer.
''' '''
for _, track in self.tracks: # This should be done by a Message? for _, track in self.tracks: # This should be done by a Message?
if track.resizing_viewer: if track.resizing_viewer:
track.resizing_viewer.draw() track.resizing_viewer.draw()
track.resizing_viewer = None track.resizing_viewer = None
break break
def broadcast(self, message): def broadcast(self, message):
''' '''
Broadcast a message to all viewers (except the sender) and all Broadcast a message to all viewers (except the sender) and all
registered handlers. registered handlers.
''' '''
for _, track in self.tracks: for _, track in self.tracks:
track.broadcast(message) track.broadcast(message)
for handler in self.handlers: for handler in self.handlers:
handler(message) handler(message)
def redraw(self): def redraw(self):
''' '''
Redraw all tracks (which will redraw all viewers.) Redraw all tracks (which will redraw all viewers.)
''' '''
for _, track in self.tracks: for _, track in self.tracks:
track.redraw() track.redraw()
def focus(self, viewer): def focus(self, viewer):
''' '''
Set system focus to a given viewer (or no viewer if a track.) Set system focus to a given viewer (or no viewer if a track.)
''' '''
if isinstance(viewer, Track): if isinstance(viewer, Track):
if self.focused_viewer: self.focused_viewer.unfocus() if self.focused_viewer: self.focused_viewer.unfocus()
self.focused_viewer = None self.focused_viewer = None
elif viewer is not self.focused_viewer: elif viewer is not self.focused_viewer:
if self.focused_viewer: self.focused_viewer.unfocus() if self.focused_viewer: self.focused_viewer.unfocus()
self.focused_viewer = viewer self.focused_viewer = viewer
viewer.focus(self) viewer.focus(self)
def dispatch_event(self, event): def dispatch_event(self, event):
''' '''
Display event handling. Display event handling.
''' '''
try: try:
if event.type in {pygame.KEYUP, pygame.KEYDOWN}: if event.type in {pygame.KEYUP, pygame.KEYDOWN}:
self._keyboard_event(event) self._keyboard_event(event)
elif event.type in MOUSE_EVENTS: elif event.type in MOUSE_EVENTS:
self._mouse_event(event) self._mouse_event(event)
else: else:
print(( print((
'received event %s Use pygame.event.set_allowed().' 'received event %s Use pygame.event.set_allowed().'
% pygame.event.event_name(event.type) % pygame.event.event_name(event.type)
), file=stderr) ), file=stderr)
# Catch all exceptions and open a viewer. # Catch all exceptions and open a viewer.
except: except:
err = format_exc() err = format_exc()
print(err, file=stderr) # To be safe just print it right away. print(err, file=stderr) # To be safe just print it right away.
open_viewer_on_string(self, err, self.broadcast) open_viewer_on_string(self, err, self.broadcast)
def _keyboard_event(self, event): def _keyboard_event(self, event):
if event.key == pygame.K_PAUSE and event.type == pygame.KEYUP: if event.key == pygame.K_PAUSE and event.type == pygame.KEYUP:
# At least on my keyboard the break/pause key sends K_PAUSE. # At least on my keyboard the break/pause key sends K_PAUSE.
# The main use of this is to open a TextViewer if you # The main use of this is to open a TextViewer if you
# accidentally close all the viewers, so you can recover. # accidentally close all the viewers, so you can recover.
raise KeyboardInterrupt('break') raise KeyboardInterrupt('break')
if not self.focused_viewer: if not self.focused_viewer:
return return
if event.type == pygame.KEYUP: if event.type == pygame.KEYUP:
self.focused_viewer.key_up(self, event.key, event.mod) self.focused_viewer.key_up(self, event.key, event.mod)
elif event.type == pygame.KEYDOWN: elif event.type == pygame.KEYDOWN:
self.focused_viewer.key_down( self.focused_viewer.key_down(
self, event.unicode, event.key, event.mod) self, event.unicode, event.key, event.mod)
# This is not UnicodeType. TODO does this need to be fixed? # This is not UnicodeType. TODO does this need to be fixed?
# self, event.str, event.key, event.mod) # self, event.str, event.key, event.mod)
def _mouse_event(self, event): def _mouse_event(self, event):
V, x, y = self.at(*event.pos) V, x, y = self.at(*event.pos)
if event.type == pygame.MOUSEMOTION: if event.type == pygame.MOUSEMOTION:
if not isinstance(V, Track): if not isinstance(V, Track):
V.mouse_motion(self, x, y, *(event.rel + event.buttons)) V.mouse_motion(self, x, y, *(event.rel + event.buttons))
elif event.type == pygame.MOUSEBUTTONDOWN: elif event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 1: if event.button == 1:
self.focus(V) self.focus(V)
V.mouse_down(self, x, y, event.button) V.mouse_down(self, x, y, event.button)
else: else:
assert event.type == pygame.MOUSEBUTTONUP assert event.type == pygame.MOUSEBUTTONUP
# Check for moving viewer. # Check for moving viewer.
if (event.button == 2 if (event.button == 2
and self.focused_viewer and self.focused_viewer
and V is not self.focused_viewer and V is not self.focused_viewer
and V.MINIMUM_HEIGHT < y < V.h - self.focused_viewer.MINIMUM_HEIGHT and V.MINIMUM_HEIGHT < y < V.h - self.focused_viewer.MINIMUM_HEIGHT
): ):
self._move_viewer(V, y, self.focused_viewer, *event.pos) self._move_viewer(V, y, self.focused_viewer, *event.pos)
else: else:
V.mouse_up(self, x, y, event.button) V.mouse_up(self, x, y, event.button)
def init_text(self, pt, x, y, filename): def init_text(self, pt, x, y, filename):
''' '''
Open and return a ``TextViewer`` on a given file (which must be present Open and return a ``TextViewer`` on a given file (which must be present
in the ``JOYHOME`` directory.) in the ``JOYHOME`` directory.)
''' '''
viewer = self.open_viewer(x, y, text_viewer.TextViewer) viewer = self.open_viewer(x, y, text_viewer.TextViewer)
viewer.content_id, viewer.lines = pt.open(filename) viewer.content_id, viewer.lines = pt.open(filename)
viewer.draw() viewer.draw()
return viewer return viewer
class Track(Viewer): class Track(Viewer):
''' '''
Manage a vertical strip of the display, and the viewers on it. Manage a vertical strip of the display, and the viewers on it.
''' '''
def __init__(self, surface): def __init__(self, surface):
Viewer.__init__(self, surface) Viewer.__init__(self, surface)
self.viewers = [] # (y, viewer) self.viewers = [] # (y, viewer)
self.hiding = None self.hiding = None
self.resizing_viewer = None self.resizing_viewer = None
self.draw() self.draw()
def split(self, y): def split(self, y):
''' '''
Split the Track at the y coordinate and return the height Split the Track at the y coordinate and return the height
available for a new viewer. Tracks manage a vertical strip of available for a new viewer. Tracks manage a vertical strip of
the display screen so they don't resize their surface when split. the display screen so they don't resize their surface when split.
''' '''
h = self.viewers[0][0] if self.viewers else self.h h = self.viewers[0][0] if self.viewers else self.h
assert h > y assert h > y
return h - y return h - y
def draw(self, rect=None): def draw(self, rect=None):
'''Draw the track onto its surface, clearing all content. '''Draw the track onto its surface, clearing all content.
If rect is passed only draw to that area. This supports e.g. If rect is passed only draw to that area. This supports e.g.
closing a viewer that then exposes part of the track. closing a viewer that then exposes part of the track.
''' '''
self.surface.fill(GREY, rect=rect) self.surface.fill(GREY, rect=rect)
def viewer_at(self, y): def viewer_at(self, y):
''' '''
Return the viewer at y along with the viewer-relative y coordinate, Return the viewer at y along with the viewer-relative y coordinate,
if there's no viewer at y return this track and y. if there's no viewer at y return this track and y.
''' '''
for viewer_y, viewer in self.viewers: for viewer_y, viewer in self.viewers:
if viewer_y < y <= viewer_y + viewer.h: if viewer_y < y <= viewer_y + viewer.h:
return viewer, y - viewer_y return viewer, y - viewer_y
return self, y return self, y
def open_viewer(self, y, class_): def open_viewer(self, y, class_):
'''Open and return a viewer of class at y.''' '''Open and return a viewer of class at y.'''
# Todo: if y coincides with some other viewer's y replace it. # Todo: if y coincides with some other viewer's y replace it.
viewer, viewer_y = self.viewer_at(y) viewer, viewer_y = self.viewer_at(y)
h = viewer.split(viewer_y) h = viewer.split(viewer_y)
new_viewer = class_(self.surface.subsurface((0, y, self.w, h))) new_viewer = class_(self.surface.subsurface((0, y, self.w, h)))
new_viewer.draw() new_viewer.draw()
self.viewers.append((y, new_viewer)) self.viewers.append((y, new_viewer))
self.viewers.sort() # Could use bisect module but how many self.viewers.sort() # Could use bisect module but how many
# viewers will you ever have? # viewers will you ever have?
return new_viewer return new_viewer
def close_viewer(self, viewer): def close_viewer(self, viewer):
'''Close the viewer, reuse the freed space.''' '''Close the viewer, reuse the freed space.'''
for y, V in self.viewers: for y, V in self.viewers:
if V is viewer: if V is viewer:
self._close_viewer(y, V) self._close_viewer(y, V)
return True return True
return False return False
def _close_viewer(self, y, viewer): def _close_viewer(self, y, viewer):
'''Helper function to do the actual closing.''' '''Helper function to do the actual closing.'''
i = self.viewers.index((y, viewer)) i = self.viewers.index((y, viewer))
del self.viewers[i] del self.viewers[i]
if i: # The previous viewer gets the space. if i: # The previous viewer gets the space.
previous_y, previous_viewer = self.viewers[i - 1] previous_y, previous_viewer = self.viewers[i - 1]
self._grow_by(previous_viewer, previous_y, viewer.h) self._grow_by(previous_viewer, previous_y, viewer.h)
else: # This track gets the space. else: # This track gets the space.
self.draw((0, y, self.w, viewer.surface.get_height())) self.draw((0, y, self.w, viewer.surface.get_height()))
viewer.close() viewer.close()
def _grow_by(self, viewer, y, h): def _grow_by(self, viewer, y, h):
'''Grow a viewer (located at y) by height h. '''Grow a viewer (located at y) by height h.
This might seem like it should be a method of the viewer, but This might seem like it should be a method of the viewer, but
the viewer knows nothing of its own y location on the screen nor the viewer knows nothing of its own y location on the screen nor
the parent track's surface (to make a new subsurface) so it has the parent track's surface (to make a new subsurface) so it has
to be a method of the track, which has both. to be a method of the track, which has both.
''' '''
h = viewer.surface.get_height() + h h = viewer.surface.get_height() + h
try: try:
surface = self.surface.subsurface((0, y, self.w, h)) surface = self.surface.subsurface((0, y, self.w, h))
except ValueError: # subsurface rectangle outside surface area except ValueError: # subsurface rectangle outside surface area
pass pass
else: else:
viewer.resurface(surface) viewer.resurface(surface)
if h <= viewer.last_touch[1]: viewer.last_touch = 0, 0 if h <= viewer.last_touch[1]: viewer.last_touch = 0, 0
viewer.draw() viewer.draw()
def change_viewer(self, viewer, new_y, relative=False): def change_viewer(self, viewer, new_y, relative=False):
''' '''
Adjust the top of the viewer to a new y within the boundaries of Adjust the top of the viewer to a new y within the boundaries of
its neighbors. its neighbors.
If relative is False new_y should be in screen coords, else new_y If relative is False new_y should be in screen coords, else new_y
should be relative to the top of the viewer. should be relative to the top of the viewer.
''' '''
for old_y, V in self.viewers: for old_y, V in self.viewers:
if V is viewer: if V is viewer:
if relative: new_y += old_y if relative: new_y += old_y
if new_y != old_y: self._change_viewer(new_y, old_y, V) if new_y != old_y: self._change_viewer(new_y, old_y, V)
return True return True
return False return False
def _change_viewer(self, new_y, old_y, viewer): def _change_viewer(self, new_y, old_y, viewer):
new_y = max(0, min(self.h, new_y)) new_y = max(0, min(self.h, new_y))
i = self.viewers.index((old_y, viewer)) i = self.viewers.index((old_y, viewer))
if new_y < old_y: # Enlarge self, shrink upper neighbor. if new_y < old_y: # Enlarge self, shrink upper neighbor.
if i: if i:
previous_y, previous_viewer = self.viewers[i - 1] previous_y, previous_viewer = self.viewers[i - 1]
if new_y - previous_y < self.MINIMUM_HEIGHT: if new_y - previous_y < self.MINIMUM_HEIGHT:
return return
previous_viewer.resizing = 1 previous_viewer.resizing = 1
h = previous_viewer.split(new_y - previous_y) h = previous_viewer.split(new_y - previous_y)
previous_viewer.resizing = 0 previous_viewer.resizing = 0
self.resizing_viewer = previous_viewer self.resizing_viewer = previous_viewer
else: else:
h = old_y - new_y h = old_y - new_y
self._grow_by(viewer, new_y, h) self._grow_by(viewer, new_y, h)
else: # Shink self, enlarge upper neighbor. else: # Shink self, enlarge upper neighbor.
# Enforce invariant. # Enforce invariant.
try: try:
h, _ = self.viewers[i + 1] h, _ = self.viewers[i + 1]
except IndexError: # No next viewer. except IndexError: # No next viewer.
h = self.h h = self.h
if h - new_y < self.MINIMUM_HEIGHT: if h - new_y < self.MINIMUM_HEIGHT:
return return
# Change the viewer and adjust the upper viewer or track. # Change the viewer and adjust the upper viewer or track.
h = new_y - old_y h = new_y - old_y
self._grow_by(viewer, new_y, -h) # grow by negative height! self._grow_by(viewer, new_y, -h) # grow by negative height!
if i: if i:
previous_y, previous_viewer = self.viewers[i - 1] previous_y, previous_viewer = self.viewers[i - 1]
previous_viewer.resizing = 1 previous_viewer.resizing = 1
self._grow_by(previous_viewer, previous_y, h) self._grow_by(previous_viewer, previous_y, h)
previous_viewer.resizing = 0 previous_viewer.resizing = 0
self.resizing_viewer = previous_viewer self.resizing_viewer = previous_viewer
else: else:
self.draw((0, old_y, self.w, h)) self.draw((0, old_y, self.w, h))
self.viewers[i] = new_y, viewer self.viewers[i] = new_y, viewer
# self.viewers.sort() # Not necessary, invariant holds. # self.viewers.sort() # Not necessary, invariant holds.
assert sorted(self.viewers) == self.viewers assert sorted(self.viewers) == self.viewers
def broadcast(self, message): def broadcast(self, message):
''' '''
Broadcast a message to all viewers on this track (except the sender.) Broadcast a message to all viewers on this track (except the sender.)
''' '''
for _, viewer in self.viewers: for _, viewer in self.viewers:
if viewer is not message.sender: if viewer is not message.sender:
viewer.handle(message) viewer.handle(message)
def redraw(self): def redraw(self):
'''Redraw the track and all of its viewers.''' '''Redraw the track and all of its viewers.'''
self.draw() self.draw()
for _, viewer in self.viewers: for _, viewer in self.viewers:
viewer.draw() viewer.draw()

View File

@ -25,9 +25,9 @@ import base64, zlib
def create(fn='Iosevka12.BMP'): def create(fn='Iosevka12.BMP'):
with open(fn, 'rb') as f: with open(fn, 'rb') as f:
data = f.read() data = f.read()
return base64.encodestring(zlib.compress(data)) return base64.encodestring(zlib.compress(data))
data = StringIO(zlib.decompress(base64.decodestring('''\ data = StringIO(zlib.decompress(base64.decodestring('''\
@ -186,4 +186,4 @@ lnalXc/9SsNb2vUirzS8pV0v8gJv/w/2vRht''')))
if __name__ == '__main__': if __name__ == '__main__':
print(create()) print(create())

View File

@ -24,8 +24,8 @@ JOY_HOME directory.
These contents are kept in this Python module as a base64-encoded zip These contents are kept in this Python module as a base64-encoded zip
file, so you can just do, e.g.: file, so you can just do, e.g.:
import init_joy_home import init_joy_home
init_joy_home.initialize(JOY_HOME) init_joy_home.initialize(JOY_HOME)
''' '''
from __future__ import print_function from __future__ import print_function
@ -35,17 +35,17 @@ import base64, os, io, zipfile
def initialize(joy_home): def initialize(joy_home):
Z.extractall(joy_home) Z.extractall(joy_home)
def create_data(from_dir='./default_joy_home'): def create_data(from_dir='./default_joy_home'):
f = io.StringIO() f = io.StringIO()
z = zipfile.ZipFile(f, mode='w') z = zipfile.ZipFile(f, mode='w')
for fn in os.listdir(from_dir): for fn in os.listdir(from_dir):
from_fn = os.path.join(from_dir, fn) from_fn = os.path.join(from_dir, fn)
z.write(from_fn, fn) z.write(from_fn, fn)
z.close() z.close()
return base64.encodestring(f.getvalue()) return base64.encodestring(f.getvalue())
Z = zipfile.ZipFile(io.StringIO(base64.decodestring('''\ Z = zipfile.ZipFile(io.StringIO(base64.decodestring('''\
@ -275,4 +275,4 @@ c3RhY2sucGlja2xlUEsFBgAAAAAGAAYAUwEAACcwAAAAAA==''')))
if __name__ == '__main__': if __name__ == '__main__':
print(create_data()) print(create_data())

View File

@ -41,139 +41,139 @@ FULLSCREEN = '-f' in sys.argv
JOY_HOME = os.environ.get('JOY_HOME') JOY_HOME = os.environ.get('JOY_HOME')
if JOY_HOME is None: if JOY_HOME is None:
JOY_HOME = os.path.expanduser('~/.thun') JOY_HOME = os.path.expanduser('~/.thun')
if not os.path.isabs(JOY_HOME): if not os.path.isabs(JOY_HOME):
raise ValueError('what directory?') raise ValueError('what directory?')
def load_definitions(pt, dictionary): def load_definitions(pt, dictionary):
'''Load definitions from ``definitions.txt``.''' '''Load definitions from ``definitions.txt``.'''
lines = pt.open('definitions.txt')[1] lines = pt.open('definitions.txt')[1]
for line in lines: for line in lines:
if '==' in line: if '==' in line:
DefinitionWrapper.add_def(line, dictionary) DefinitionWrapper.add_def(line, dictionary)
def load_primitives(home, name_space): def load_primitives(home, name_space):
'''Load primitives from ``library.py``.''' '''Load primitives from ``library.py``.'''
fn = os.path.join(home, 'library.py') fn = os.path.join(home, 'library.py')
if os.path.exists(fn): if os.path.exists(fn):
execfile(fn, name_space) execfile(fn, name_space)
def init(): def init():
''' '''
Initialize the system. Initialize the system.
* Init PyGame * Init PyGame
* Create main window * Create main window
* Start the PyGame clock * Start the PyGame clock
* Set the event mask * Set the event mask
* Create the PersistTask * Create the PersistTask
''' '''
print('Initializing Pygame...') print('Initializing Pygame...')
pygame.init() pygame.init()
print('Creating window...') print('Creating window...')
if FULLSCREEN: if FULLSCREEN:
screen = pygame.display.set_mode() screen = pygame.display.set_mode()
else: else:
screen = pygame.display.set_mode((1024, 768)) screen = pygame.display.set_mode((1024, 768))
clock = pygame.time.Clock() clock = pygame.time.Clock()
pygame.event.set_allowed(None) pygame.event.set_allowed(None)
pygame.event.set_allowed(core.ALLOWED_EVENTS) pygame.event.set_allowed(core.ALLOWED_EVENTS)
pt = persist_task.PersistTask(JOY_HOME) pt = persist_task.PersistTask(JOY_HOME)
return screen, clock, pt return screen, clock, pt
def init_context(screen, clock, pt): def init_context(screen, clock, pt):
''' '''
More initialization More initialization
* Create the Joy dictionary * Create the Joy dictionary
* Create the Display * Create the Display
* Open the log, menu, and scratch text viewers, and the stack pickle * Open the log, menu, and scratch text viewers, and the stack pickle
* Start the main loop * Start the main loop
* Create the World object * Create the World object
* Register PersistTask and World message handlers with the Display * Register PersistTask and World message handlers with the Display
* Load user function definitions. * Load user function definitions.
''' '''
D = initialize() D = initialize()
d = display.Display( d = display.Display(
screen, screen,
D.__contains__, D.__contains__,
*((144 - 89, 144, 89) if FULLSCREEN else (89, 144)) *((144 - 89, 144, 89) if FULLSCREEN else (89, 144))
) )
log = d.init_text(pt, 0, 0, 'log.txt') log = d.init_text(pt, 0, 0, 'log.txt')
tho = d.init_text(pt, 0, old_div(d.h, 3), 'menu.txt') tho = d.init_text(pt, 0, old_div(d.h, 3), 'menu.txt')
t = d.init_text(pt, old_div(d.w, 2), 0, 'scratch.txt') t = d.init_text(pt, old_div(d.w, 2), 0, 'scratch.txt')
loop = core.TheLoop(d, clock) loop = core.TheLoop(d, clock)
stack_id, stack_holder = pt.open('stack.pickle') stack_id, stack_holder = pt.open('stack.pickle')
world = core.World(stack_id, stack_holder, D, d.broadcast, log) world = core.World(stack_id, stack_holder, D, d.broadcast, log)
loop.install_task(pt.task_run, 10000) # save files every ten seconds loop.install_task(pt.task_run, 10000) # save files every ten seconds
d.handlers.append(pt.handle) d.handlers.append(pt.handle)
d.handlers.append(world.handle) d.handlers.append(world.handle)
load_definitions(pt, D) load_definitions(pt, D)
return locals() return locals()
def error_guard(loop, n=10): def error_guard(loop, n=10):
''' '''
Run a loop function, retry for ``n`` exceptions. Run a loop function, retry for ``n`` exceptions.
Prints tracebacks on ``sys.stderr``. Prints tracebacks on ``sys.stderr``.
''' '''
error_count = 0 error_count = 0
while error_count < n: while error_count < n:
try: try:
loop() loop()
break break
except: except:
traceback.print_exc(file=sys.stderr) traceback.print_exc(file=sys.stderr)
error_count += 1 error_count += 1
class FileFaker(object): class FileFaker(object):
'''Pretends to be a file object but writes to log instead.''' '''Pretends to be a file object but writes to log instead.'''
def __init__(self, log): def __init__(self, log):
self.log = log self.log = log
def write(self, text): def write(self, text):
'''Write text to log.''' '''Write text to log.'''
self.log.append(text) self.log.append(text)
def flush(self): def flush(self):
pass pass
def main(screen, clock, pt): def main(screen, clock, pt):
''' '''
Main function. Main function.
* Call ``init_context()`` * Call ``init_context()``
* Load primitives * Load primitives
* Create an ``evaluate`` function that lets you just eval some Python code * Create an ``evaluate`` function that lets you just eval some Python code
* Redirect ``stdout`` to the log using a ``FileFaker`` object, and... * Redirect ``stdout`` to the log using a ``FileFaker`` object, and...
* Start the main loop. * Start the main loop.
''' '''
name_space = init_context(screen, clock, pt) name_space = init_context(screen, clock, pt)
load_primitives(pt.home, name_space.copy()) load_primitives(pt.home, name_space.copy())
@SimpleFunctionWrapper @SimpleFunctionWrapper
def evaluate(stack): def evaluate(stack):
'''Evaluate the Python code text on the top of the stack.''' '''Evaluate the Python code text on the top of the stack.'''
code, stack = stack code, stack = stack
exec(code, name_space.copy()) exec(code, name_space.copy())
return stack return stack
name_space['D']['evaluate'] = evaluate name_space['D']['evaluate'] = evaluate
sys.stdout, old_stdout = FileFaker(name_space['log']), sys.stdout sys.stdout, old_stdout = FileFaker(name_space['log']), sys.stdout
try: try:
error_guard(name_space['loop'].loop) error_guard(name_space['loop'].loop)
finally: finally:
sys.stdout = old_stdout sys.stdout = old_stdout
return name_space['d'] return name_space['d']

View File

@ -36,239 +36,239 @@ from joy.vui import core, init_joy_home
def open_repo(repo_dir=None, initialize=False): def open_repo(repo_dir=None, initialize=False):
''' '''
Open, or create, and return a Dulwich git repo object for the given Open, or create, and return a Dulwich git repo object for the given
directory. If the dir path doesn't exist it will be created. If it directory. If the dir path doesn't exist it will be created. If it
does exist but isn't a repo the result depends on the ``initialize`` does exist but isn't a repo the result depends on the ``initialize``
argument. If it is ``False`` (the default) a ``NotGitRepository`` argument. If it is ``False`` (the default) a ``NotGitRepository``
exception is raised, otherwise ``git init`` is effected in the dir. exception is raised, otherwise ``git init`` is effected in the dir.
''' '''
if not os.path.exists(repo_dir): if not os.path.exists(repo_dir):
os.makedirs(repo_dir, 0o700) os.makedirs(repo_dir, 0o700)
return init_repo(repo_dir) return init_repo(repo_dir)
try: try:
return Repo(repo_dir) return Repo(repo_dir)
except NotGitRepository: except NotGitRepository:
if initialize: if initialize:
return init_repo(repo_dir) return init_repo(repo_dir)
raise raise
def init_repo(repo_dir): def init_repo(repo_dir):
''' '''
Initialize a git repository in the directory. Stage and commit all Initialize a git repository in the directory. Stage and commit all
files (toplevel, not those in subdirectories if any) in the dir. files (toplevel, not those in subdirectories if any) in the dir.
''' '''
repo = Repo.init(repo_dir) repo = Repo.init(repo_dir)
init_joy_home.initialize(repo_dir) init_joy_home.initialize(repo_dir)
repo.stage([ repo.stage([
fn fn
for fn in os.listdir(repo_dir) for fn in os.listdir(repo_dir)
if os.path.isfile(os.path.join(repo_dir, fn)) if os.path.isfile(os.path.join(repo_dir, fn))
]) ])
repo.do_commit('Initial commit.', committer=core.COMMITTER) repo.do_commit('Initial commit.', committer=core.COMMITTER)
return repo return repo
def make_repo_relative_path_maker(repo): def make_repo_relative_path_maker(repo):
''' '''
Helper function to return a function that returns a path given a path, Helper function to return a function that returns a path given a path,
that's relative to the repository. that's relative to the repository.
''' '''
c = repo.controldir() c = repo.controldir()
def repo_relative_path(path): def repo_relative_path(path):
return os.path.relpath(path, os.path.commonprefix((c, path))) return os.path.relpath(path, os.path.commonprefix((c, path)))
return repo_relative_path return repo_relative_path
class Resource(object): class Resource(object):
''' '''
Handle the content of a text files as a list of lines, deal with Handle the content of a text files as a list of lines, deal with
saving it and staging the changes to a repo. saving it and staging the changes to a repo.
''' '''
def __init__(self, filename, repo_relative_filename, thing=None): def __init__(self, filename, repo_relative_filename, thing=None):
self.filename = filename self.filename = filename
self.repo_relative_filename = repo_relative_filename self.repo_relative_filename = repo_relative_filename
self.thing = thing or self._from_file(open(filename)) self.thing = thing or self._from_file(open(filename))
def _from_file(self, f): def _from_file(self, f):
return f.read().splitlines() return f.read().splitlines()
def _to_file(self, f): def _to_file(self, f):
for line in self.thing: for line in self.thing:
print(line, file=f) print(line, file=f)
def persist(self, repo): def persist(self, repo):
''' '''
Save the lines to the file and stage the file in the repo. Save the lines to the file and stage the file in the repo.
''' '''
with open(self.filename, 'w') as f: with open(self.filename, 'w') as f:
os.chmod(self.filename, 0o600) os.chmod(self.filename, 0o600)
self._to_file(f) self._to_file(f)
f.flush() f.flush()
os.fsync(f.fileno()) os.fsync(f.fileno())
# For goodness's sake, write it to the disk already! # For goodness's sake, write it to the disk already!
repo.stage([self.repo_relative_filename]) repo.stage([self.repo_relative_filename])
class PickledResource(Resource): class PickledResource(Resource):
''' '''
A ``Resource`` subclass that uses ``pickle`` on its file/thing. A ``Resource`` subclass that uses ``pickle`` on its file/thing.
''' '''
def _from_file(self, f): def _from_file(self, f):
return [pickle.load(f)] return [pickle.load(f)]
def _to_file(self, f): def _to_file(self, f):
pickle.dump(self.thing[0], f, protocol=2) pickle.dump(self.thing[0], f, protocol=2)
class PersistTask(object): class PersistTask(object):
''' '''
This class deals with saving changes to the git repo. This class deals with saving changes to the git repo.
''' '''
LIMIT = 10 LIMIT = 10
MAX_SAVE = 10 MAX_SAVE = 10
def __init__(self, home): def __init__(self, home):
self.home = home self.home = home
self.repo = open_repo(home) self.repo = open_repo(home)
self._r = make_repo_relative_path_maker(self.repo) self._r = make_repo_relative_path_maker(self.repo)
self.counter = Counter() self.counter = Counter()
self.store = {} self.store = {}
def open(self, name): def open(self, name):
''' '''
Look up the named file in home and return its content_id and data. Look up the named file in home and return its content_id and data.
''' '''
fn = os.path.join(self.home, name) fn = os.path.join(self.home, name)
content_id = name # hash(fn) content_id = name # hash(fn)
try: try:
resource = self.store[content_id] resource = self.store[content_id]
except KeyError: except KeyError:
R = PickledResource if name.endswith('.pickle') else Resource R = PickledResource if name.endswith('.pickle') else Resource
resource = self.store[content_id] = R(fn, self._r(fn)) resource = self.store[content_id] = R(fn, self._r(fn))
return content_id, resource.thing return content_id, resource.thing
def handle(self, message): def handle(self, message):
''' '''
Handle messages, dispatch to ``handle_FOO()`` methods. Handle messages, dispatch to ``handle_FOO()`` methods.
''' '''
if isinstance(message, core.OpenMessage): if isinstance(message, core.OpenMessage):
self.handle_open(message) self.handle_open(message)
elif isinstance(message, core.ModifyMessage): elif isinstance(message, core.ModifyMessage):
self.handle_modify(message) self.handle_modify(message)
elif isinstance(message, core.PersistMessage): elif isinstance(message, core.PersistMessage):
self.handle_persist(message) self.handle_persist(message)
elif isinstance(message, core.ShutdownMessage): elif isinstance(message, core.ShutdownMessage):
for content_id in self.counter: for content_id in self.counter:
self.store[content_id].persist(self.repo) self.store[content_id].persist(self.repo)
self.commit('shutdown') self.commit('shutdown')
def handle_open(self, message): def handle_open(self, message):
''' '''
Foo. Foo.
''' '''
try: try:
message.content_id, message.thing = self.open(message.name) message.content_id, message.thing = self.open(message.name)
except: except:
message.traceback = traceback.format_exc() message.traceback = traceback.format_exc()
message.status = core.ERROR message.status = core.ERROR
else: else:
message.status = core.SUCCESS message.status = core.SUCCESS
def handle_modify(self, message): def handle_modify(self, message):
''' '''
Foo. Foo.
''' '''
try: try:
content_id = message.details['content_id'] content_id = message.details['content_id']
except KeyError: except KeyError:
return return
if not content_id: if not content_id:
return return
self.counter[content_id] += 1 self.counter[content_id] += 1
if self.counter[content_id] > self.LIMIT: if self.counter[content_id] > self.LIMIT:
self.persist(content_id) self.persist(content_id)
self.commit('due to activity') self.commit('due to activity')
def handle_persist(self, message): def handle_persist(self, message):
''' '''
Foo. Foo.
''' '''
try: try:
resource = self.store[message.content_id] resource = self.store[message.content_id]
except KeyError: except KeyError:
resource = self.handle_persist_new(message) resource = self.handle_persist_new(message)
resource.persist(self.repo) resource.persist(self.repo)
self.commit('by request from %r' % (message.sender,)) self.commit('by request from %r' % (message.sender,))
def handle_persist_new(self, message): def handle_persist_new(self, message):
''' '''
Foo. Foo.
''' '''
name = message.content_id name = message.content_id
check_filename(name) check_filename(name)
fn = os.path.join(self.home, name) fn = os.path.join(self.home, name)
thing = message.details['thing'] thing = message.details['thing']
R = PickledResource if name.endswith('.pickle') else Resource # !!! refactor! R = PickledResource if name.endswith('.pickle') else Resource # !!! refactor!
resource = self.store[name] = R(fn, self._r(fn), thing) resource = self.store[name] = R(fn, self._r(fn), thing)
return resource return resource
def persist(self, content_id): def persist(self, content_id):
''' '''
Persist a resource. Persist a resource.
''' '''
del self.counter[content_id] del self.counter[content_id]
self.store[content_id].persist(self.repo) self.store[content_id].persist(self.repo)
def task_run(self): def task_run(self):
''' '''
Stage any outstanding changes. Stage any outstanding changes.
''' '''
if not self.counter: if not self.counter:
return return
for content_id, _ in self.counter.most_common(self.MAX_SAVE): for content_id, _ in self.counter.most_common(self.MAX_SAVE):
self.persist(content_id) self.persist(content_id)
self.commit() self.commit()
def commit(self, message='auto-commit'): def commit(self, message='auto-commit'):
''' '''
Commit. Commit.
''' '''
return self.repo.do_commit(message, committer=core.COMMITTER) return self.repo.do_commit(message, committer=core.COMMITTER)
def scan(self): def scan(self):
''' '''
Return a sorted list of all the files in the home dir. Return a sorted list of all the files in the home dir.
''' '''
return sorted([ return sorted([
fn fn
for fn in os.listdir(self.home) for fn in os.listdir(self.home)
if os.path.isfile(os.path.join(self.home, fn)) if os.path.isfile(os.path.join(self.home, fn))
]) ])
def check_filename(name): def check_filename(name):
''' '''
Sanity checks for filename. Sanity checks for filename.
''' '''
# TODO: improve this... # TODO: improve this...
if len(name) > 64: if len(name) > 64:
raise ValueError('bad name %r' % (name,)) raise ValueError('bad name %r' % (name,))
left, dot, right = name.partition('.') left, dot, right = name.partition('.')
if not left.isalnum() or dot and not right.isalnum(): if not left.isalnum() or dot and not right.isalnum():
raise ValueError('bad name %r' % (name,)) raise ValueError('bad name %r' % (name,))
if __name__ == '__main__': if __name__ == '__main__':
JOY_HOME = os.path.expanduser('~/.thun') JOY_HOME = os.path.expanduser('~/.thun')
pt = PersistTask(JOY_HOME) pt = PersistTask(JOY_HOME)
content_id, thing = pt.open('stack.pickle') content_id, thing = pt.open('stack.pickle')
pt.persist(content_id) pt.persist(content_id)
print(pt.counter) print(pt.counter)
mm = core.ModifyMessage(None, None, content_id=content_id) mm = core.ModifyMessage(None, None, content_id=content_id)
pt.handle(mm) pt.handle(mm)
print(pt.counter) print(pt.counter)

View File

@ -32,44 +32,44 @@ MAX_WIDTH = 64
def fsi(item): def fsi(item):
'''Format Stack Item''' '''Format Stack Item'''
if isinstance(item, tuple): if isinstance(item, tuple):
res = '[%s]' % expression_to_string(item) res = '[%s]' % expression_to_string(item)
elif isinstance(item, str): elif isinstance(item, str):
res = '"%s"' % item res = '"%s"' % item
else: else:
res = str(item) res = str(item)
if len(res) > MAX_WIDTH: if len(res) > MAX_WIDTH:
return res[:MAX_WIDTH - 3] + '...' return res[:MAX_WIDTH - 3] + '...'
return res return res
class StackViewer(text_viewer.TextViewer): class StackViewer(text_viewer.TextViewer):
def __init__(self, surface): def __init__(self, surface):
super(StackViewer, self).__init__(surface) super(StackViewer, self).__init__(surface)
self.stack_holder = None self.stack_holder = None
self.content_id = 'stack viewer' self.content_id = 'stack viewer'
def _attach(self, display): def _attach(self, display):
if self.stack_holder: if self.stack_holder:
return return
om = core.OpenMessage(self, 'stack.pickle') om = core.OpenMessage(self, 'stack.pickle')
display.broadcast(om) display.broadcast(om)
if om.status != core.SUCCESS: if om.status != core.SUCCESS:
raise RuntimeError('stack unavailable') raise RuntimeError('stack unavailable')
self.stack_holder = om.thing self.stack_holder = om.thing
def _update(self): def _update(self):
self.lines[:] = list(map(fsi, iter_stack(self.stack_holder[0]))) or [''] self.lines[:] = list(map(fsi, iter_stack(self.stack_holder[0]))) or ['']
def focus(self, display): def focus(self, display):
self._attach(display) self._attach(display)
super(StackViewer, self).focus(display) super(StackViewer, self).focus(display)
def handle(self, message): def handle(self, message):
if (isinstance(message, core.ModifyMessage) if (isinstance(message, core.ModifyMessage)
and message.subject is self.stack_holder and message.subject is self.stack_holder
): ):
self._update() self._update()
self.draw_body() self.draw_body()

File diff suppressed because it is too large Load Diff

View File

@ -32,208 +32,208 @@ from joy.vui.core import BACKGROUND, FOREGROUND
class Viewer(object): class Viewer(object):
''' '''
Base Viewer class Base Viewer class
''' '''
MINIMUM_HEIGHT = 11 MINIMUM_HEIGHT = 11
def __init__(self, surface): def __init__(self, surface):
self.resurface(surface) self.resurface(surface)
self.last_touch = 0, 0 self.last_touch = 0, 0
def resurface(self, surface): def resurface(self, surface):
self.w, self.h = surface.get_width(), surface.get_height() self.w, self.h = surface.get_width(), surface.get_height()
self.surface = surface self.surface = surface
def split(self, y): def split(self, y):
''' '''
Split the viewer at the y coordinate (which is relative to the Split the viewer at the y coordinate (which is relative to the
viewer's surface and must be inside it somewhere) and return the viewer's surface and must be inside it somewhere) and return the
remaining height. The upper part of the viewer remains (and gets remaining height. The upper part of the viewer remains (and gets
redrawn on a new surface) and the lower space is now available redrawn on a new surface) and the lower space is now available
for e.g. a new viewer. for e.g. a new viewer.
''' '''
assert y >= self.MINIMUM_HEIGHT assert y >= self.MINIMUM_HEIGHT
new_viewer_h = self.h - y new_viewer_h = self.h - y
self.resurface(self.surface.subsurface((0, 0, self.w, y))) self.resurface(self.surface.subsurface((0, 0, self.w, y)))
if y <= self.last_touch[1]: self.last_touch = 0, 0 if y <= self.last_touch[1]: self.last_touch = 0, 0
self.draw() self.draw()
return new_viewer_h return new_viewer_h
def handle(self, message): def handle(self, message):
assert self is not message.sender assert self is not message.sender
pass pass
def draw(self): def draw(self):
'''Draw the viewer onto its surface.''' '''Draw the viewer onto its surface.'''
self.surface.fill(BACKGROUND) self.surface.fill(BACKGROUND)
x, y, h = self.w - 1, self.MINIMUM_HEIGHT, self.h - 1 x, y, h = self.w - 1, self.MINIMUM_HEIGHT, self.h - 1
# Right-hand side. # Right-hand side.
pygame.draw.line(self.surface, FOREGROUND, (x, 0), (x, h)) pygame.draw.line(self.surface, FOREGROUND, (x, 0), (x, h))
# Between header and body. # Between header and body.
pygame.draw.line(self.surface, FOREGROUND, (0, y), (x, y)) pygame.draw.line(self.surface, FOREGROUND, (0, y), (x, y))
# Bottom. # Bottom.
pygame.draw.line(self.surface, FOREGROUND, (0, h), (x, h)) pygame.draw.line(self.surface, FOREGROUND, (0, h), (x, h))
def close(self): def close(self):
'''Close the viewer and release any resources, etc...''' '''Close the viewer and release any resources, etc...'''
def focus(self, display): def focus(self, display):
pass pass
def unfocus(self): def unfocus(self):
pass pass
# Event handling. # Event handling.
def mouse_down(self, display, x, y, button): def mouse_down(self, display, x, y, button):
self.last_touch = x, y self.last_touch = x, y
def mouse_up(self, display, x, y, button): def mouse_up(self, display, x, y, button):
pass pass
def mouse_motion(self, display, x, y, dx, dy, button0, button1, button2): def mouse_motion(self, display, x, y, dx, dy, button0, button1, button2):
pass pass
def key_up(self, display, key, mod): def key_up(self, display, key, mod):
if key == pygame.K_q and mod & pygame.KMOD_CTRL: # Ctrl-q if key == pygame.K_q and mod & pygame.KMOD_CTRL: # Ctrl-q
display.close_viewer(self) display.close_viewer(self)
return True return True
if key == pygame.K_g and mod & pygame.KMOD_CTRL: # Ctrl-g if key == pygame.K_g and mod & pygame.KMOD_CTRL: # Ctrl-g
display.grow_viewer(self) display.grow_viewer(self)
return True return True
def key_down(self, display, uch, key, mod): def key_down(self, display, uch, key, mod):
pass pass
class MenuViewer(Viewer): class MenuViewer(Viewer):
''' '''
MenuViewer class MenuViewer class
''' '''
MINIMUM_HEIGHT = 26 MINIMUM_HEIGHT = 26
def __init__(self, surface): def __init__(self, surface):
Viewer.__init__(self, surface) Viewer.__init__(self, surface)
self.resizing = 0 self.resizing = 0
self.bg = 100, 150, 100 self.bg = 100, 150, 100
def resurface(self, surface): def resurface(self, surface):
Viewer.resurface(self, surface) Viewer.resurface(self, surface)
n = self.MINIMUM_HEIGHT - 2 n = self.MINIMUM_HEIGHT - 2
self.close_rect = pygame.rect.Rect(self.w - 2 - n, 1, n, n) self.close_rect = pygame.rect.Rect(self.w - 2 - n, 1, n, n)
self.grow_rect = pygame.rect.Rect(1, 1, n, n) self.grow_rect = pygame.rect.Rect(1, 1, n, n)
self.body_rect = pygame.rect.Rect( self.body_rect = pygame.rect.Rect(
0, self.MINIMUM_HEIGHT + 1, 0, self.MINIMUM_HEIGHT + 1,
self.w - 1, self.h - self.MINIMUM_HEIGHT - 2) self.w - 1, self.h - self.MINIMUM_HEIGHT - 2)
def draw(self): def draw(self):
'''Draw the viewer onto its surface.''' '''Draw the viewer onto its surface.'''
Viewer.draw(self) Viewer.draw(self)
if not self.resizing: if not self.resizing:
self.draw_menu() self.draw_menu()
self.draw_body() self.draw_body()
def draw_menu(self): def draw_menu(self):
# menu buttons # menu buttons
pygame.draw.rect(self.surface, FOREGROUND, self.close_rect, 1) pygame.draw.rect(self.surface, FOREGROUND, self.close_rect, 1)
pygame.draw.rect(self.surface, FOREGROUND, self.grow_rect, 1) pygame.draw.rect(self.surface, FOREGROUND, self.grow_rect, 1)
def draw_body(self): def draw_body(self):
self.surface.fill(self.bg, self.body_rect) self.surface.fill(self.bg, self.body_rect)
def mouse_down(self, display, x, y, button): def mouse_down(self, display, x, y, button):
Viewer.mouse_down(self, display, x, y, button) Viewer.mouse_down(self, display, x, y, button)
if y <= self.MINIMUM_HEIGHT: if y <= self.MINIMUM_HEIGHT:
self.menu_click(display, x, y, button) self.menu_click(display, x, y, button)
else: else:
bx, by = self.body_rect.topleft bx, by = self.body_rect.topleft
self.body_click(display, x - bx, y - by, button) self.body_click(display, x - bx, y - by, button)
def body_click(self, display, x, y, button): def body_click(self, display, x, y, button):
if button == 1: if button == 1:
self.draw_an_a(x, y) self.draw_an_a(x, y)
def menu_click(self, display, x, y, button): def menu_click(self, display, x, y, button):
if button == 1: if button == 1:
self.resizing = 1 self.resizing = 1
elif button == 3: elif button == 3:
if self.close_rect.collidepoint(x, y): if self.close_rect.collidepoint(x, y):
display.close_viewer(self) display.close_viewer(self)
return True return True
elif self.grow_rect.collidepoint(x, y): elif self.grow_rect.collidepoint(x, y):
display.grow_viewer(self) display.grow_viewer(self)
return True return True
def mouse_up(self, display, x, y, button): def mouse_up(self, display, x, y, button):
if button == 1 and self.resizing: if button == 1 and self.resizing:
if self.resizing == 2: if self.resizing == 2:
self.resizing = 0 self.resizing = 0
self.draw() self.draw()
display.done_resizing() display.done_resizing()
self.resizing = 0 self.resizing = 0
return True return True
def mouse_motion(self, display, x, y, rel_x, rel_y, button0, button1, button2): def mouse_motion(self, display, x, y, rel_x, rel_y, button0, button1, button2):
if self.resizing and button0: if self.resizing and button0:
self.resizing = 2 self.resizing = 2
display.change_viewer(self, rel_y, relative=True) display.change_viewer(self, rel_y, relative=True)
return True return True
else: else:
self.resizing = 0 self.resizing = 0
#self.draw_an_a(x, y) #self.draw_an_a(x, y)
def key_up(self, display, key, mod): def key_up(self, display, key, mod):
if Viewer.key_up(self, display, key, mod): if Viewer.key_up(self, display, key, mod):
return True return True
def draw_an_a(self, x, y): def draw_an_a(self, x, y):
# Draw a crude letter A. # Draw a crude letter A.
lw, lh = 10, 14 lw, lh = 10, 14
try: surface = self.surface.subsurface((x - lw, y - lh, lw, lh)) try: surface = self.surface.subsurface((x - lw, y - lh, lw, lh))
except ValueError: return except ValueError: return
draw_a(surface, blend=1) draw_a(surface, blend=1)
class SomeViewer(MenuViewer): class SomeViewer(MenuViewer):
def __init__(self, surface): def __init__(self, surface):
MenuViewer.__init__(self, surface) MenuViewer.__init__(self, surface)
def resurface(self, surface): def resurface(self, surface):
MenuViewer.resurface(self, surface) MenuViewer.resurface(self, surface)
def draw_menu(self): def draw_menu(self):
MenuViewer.draw_menu(self) MenuViewer.draw_menu(self)
def draw_body(self): def draw_body(self):
pass pass
def body_click(self, display, x, y, button): def body_click(self, display, x, y, button):
pass pass
def menu_click(self, display, x, y, button): def menu_click(self, display, x, y, button):
if MenuViewer.menu_click(self, display, x, y, button): if MenuViewer.menu_click(self, display, x, y, button):
return True return True
def mouse_up(self, display, x, y, button): def mouse_up(self, display, x, y, button):
if MenuViewer.mouse_up(self, display, x, y, button): if MenuViewer.mouse_up(self, display, x, y, button):
return True return True
def mouse_motion(self, display, x, y, rel_x, rel_y, button0, button1, button2): def mouse_motion(self, display, x, y, rel_x, rel_y, button0, button1, button2):
if MenuViewer.mouse_motion(self, display, x, y, rel_x, rel_y, if MenuViewer.mouse_motion(self, display, x, y, rel_x, rel_y,
button0, button1, button2): button0, button1, button2):
return True return True
def key_down(self, display, uch, key, mod): def key_down(self, display, uch, key, mod):
try: try:
print(chr(key), end=' ') print(chr(key), end=' ')
except ValueError: except ValueError:
pass pass
# Note that Oberon book says that if you split at the exact top of a viewer # Note that Oberon book says that if you split at the exact top of a viewer
@ -243,7 +243,7 @@ class SomeViewer(MenuViewer):
def draw_a(surface, color=FOREGROUND, blend=False): def draw_a(surface, color=FOREGROUND, blend=False):
w, h = surface.get_width() - 2, surface.get_height() - 2 w, h = surface.get_width() - 2, surface.get_height() - 2
pygame.draw.aalines(surface, color, False, ( pygame.draw.aalines(surface, color, False, (
(1, h), (old_div(w, 2), 1), (w, h), (1, old_div(h, 2)) (1, h), (old_div(w, 2), 1), (w, h), (1, old_div(h, 2))
), blend) ), blend)

View File

@ -23,25 +23,25 @@ from textwrap import dedent
setup( setup(
name='Thun', name='Thun',
version='0.2.0', version='0.2.0',
description='Python Implementation of Joy', description='Python Implementation of Joy',
long_description=dedent('''\ long_description=dedent('''\
Joy is a programming language created by Manfred von Thun that is easy to Joy is a programming language created by Manfred von Thun that is easy to
use and understand and has many other nice properties. This Python use and understand and has many other nice properties. This Python
package implements an interpreter for a dialect of Joy that attempts to package implements an interpreter for a dialect of Joy that attempts to
stay very close to the spirit of Joy but does not precisely match the stay very close to the spirit of Joy but does not precisely match the
behaviour of the original version written in C.'''), behaviour of the original version written in C.'''),
author='Simon Forman', author='Simon Forman',
author_email='forman.simon@gmail.com', author_email='forman.simon@gmail.com',
url='https://joypy.osdn.io', url='https://joypy.osdn.io',
license='GPLv3+', license='GPLv3+',
packages=['joy', 'joy.utils', 'joy.gui', 'joy.vui'], packages=['joy', 'joy.utils', 'joy.gui', 'joy.vui'],
classifiers=[ classifiers=[
'Development Status :: 4 - Beta', 'Development Status :: 4 - Beta',
'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 2.7',
'Programming Language :: Other', 'Programming Language :: Other',
'Topic :: Software Development :: Interpreters', 'Topic :: Software Development :: Interpreters',
], ],
) )