From 862e0b07a8718ac279ada8536f5640cf4d15a8e9 Mon Sep 17 00:00:00 2001 From: Simon Forman Date: Sat, 14 Jul 2018 12:45:52 -0700 Subject: [PATCH] It's time to bring in the GUI. Minimalist (not to say Brutalist) UI based on text windows and mouse chords. Experimental. --- joy/gui/__init__.py | 0 joy/gui/__main__.py | 24 +++ joy/gui/main.py | 244 +++++++++++++++++++++++++++ joy/gui/mousebindings.py | 206 +++++++++++++++++++++++ joy/gui/textwidget.py | 422 +++++++++++++++++++++++++++++++++++++++++++++++ joy/gui/world.py | 91 ++++++++++ setup.py | 2 +- 7 files changed, 988 insertions(+), 1 deletion(-) create mode 100644 joy/gui/__init__.py create mode 100644 joy/gui/__main__.py create mode 100755 joy/gui/main.py create mode 100644 joy/gui/mousebindings.py create mode 100644 joy/gui/textwidget.py create mode 100644 joy/gui/world.py diff --git a/joy/gui/__init__.py b/joy/gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/joy/gui/__main__.py b/joy/gui/__main__.py new file mode 100644 index 0000000..4ae7742 --- /dev/null +++ b/joy/gui/__main__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# +# Copyright © 2018 Simon Forman +# +# This file is part of Joypy. +# +# Joypy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Joypy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Joypy. If not see . +# +import sys +from joy.gui.main import main + + +sys.exit(main()) diff --git a/joy/gui/main.py b/joy/gui/main.py new file mode 100755 index 0000000..d0fdd30 --- /dev/null +++ b/joy/gui/main.py @@ -0,0 +1,244 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +('''\ +Joypy - Copyright © 2018 Simon Forman +''' +'This program comes with ABSOLUTELY NO WARRANTY; for details right-click "warranty".' +' This is free software, and you are welcome to redistribute it under certain conditions;' +' right-click "sharing" for details.' +' Right-click on these commands to see docs on UI commands: key_bindings mouse_bindings') +import os, pickle, sys +from textwrap import dedent + +from dulwich.errors import NotGitRepository +from dulwich.repo import Repo + +from joy.gui.textwidget import TextViewerWidget, tk, get_font, TEXT_BINDINGS +from joy.gui.world import World +from joy.library import initialize +from joy.utils.stack import stack_to_string + + +JOY_HOME = os.environ.get('JOY_HOME') +if JOY_HOME is None: + JOY_HOME = os.path.expanduser('~/.joypy') + if not os.path.isabs(JOY_HOME): + JOY_HOME = os.path.abspath('./JOY_HOME') + #print 'JOY_HOME=' + JOY_HOME + +if not os.path.exists(JOY_HOME): + #print 'creating...' + os.makedirs(JOY_HOME, 0700) + #print 'initializing git repository...' + repo = Repo.init(JOY_HOME) + +else: # path does exist + try: + repo = Repo(JOY_HOME) + except NotGitRepository: + #print 'initializing git repository...' + repo = Repo.init(JOY_HOME) + #else: + #print 'opened git repository.' + + +def repo_relative_path(path): + return os.path.relpath( + path, + os.path.commonprefix((repo.controldir(), path)) + ) + + +STACK_FN = os.path.join(JOY_HOME, 'stack.pickle') +JOY_FN = os.path.join(JOY_HOME, 'scratch.txt') +LOG_FN = os.path.join(JOY_HOME, 'log.txt') + + +class StackDisplayWorld(World): + + relative_STACK_FN = repo_relative_path(STACK_FN) + + def interpret(self, command): + print '\njoy?', command + super(StackDisplayWorld, self).interpret(command) + + def print_stack(self): + print '\n%s <-' % stack_to_string(self.stack) + + def save(self): + with open(STACK_FN, 'wb') as f: + os.chmod(STACK_FN, 0600) + pickle.dump(self.stack, f) + f.flush() + os.fsync(f.fileno()) + repo.stage([self.relative_STACK_FN]) + commit_id = repo.do_commit( + 'message', + committer='Simon Forman ', + ) + #print >> sys.stderr, commit_id + + +def init_text(t, title, filename): + t.winfo_toplevel().title(title) + if os.path.exists(filename): + with open(filename) as f: + data = f.read() + t.insert(tk.END, data) + # Prevent this from triggering a git commit. + t.update() + t._cancelSave() + t.pack(expand=True, fill=tk.BOTH) + t.filename = filename + t.repo_relative_filename = repo_relative_path(filename) + t.repo = repo + t['font'] = FONT # See below. + + +def key_bindings(*args): + print dedent(''' + Ctrl-Enter - Run the selection as Joy code. + F1 - Reset and show (if hidden) the log. + Esc - Like F1 but also clears the stack. + F5 - Copy the selection to text on the stack. + Shift-F5 - As F5 but cuts the selection. + F6 - Paste as text from top of stack. + Shift-F6 - As F6 but pops the item. + F12 - print a list of all command words, or right-click "words". + ''') + return args + + +def mouse_bindings(*args): + print dedent(''' + Mouse button chords (to cancel a chord, click the third mouse button.) + + Left - Point, sweep selection + Left-Middle - Copy the selection, place text on stack + Left-Right - Run the selection as Joy code + + Middle - Paste selection (bypass stack); click and drag to scroll. + Middle-Left - Paste from top of stack, preserve + Middle-Right - Paste from top of stack, pop + + Right - Execute command word under mouse cursor + Right-Left - Print docs of command word under mouse cursor + Right-Middle - Lookup word (kinda useless now) + ''') + return args + + +def reset_log(*args): + log.delete('0.0', tk.END) + print __doc__ + return args + + +def show_log(*args): + log_window.wm_deiconify() + log_window.update() + return args + + +def grand_reset(s, e, d): + stack = load_stack() or () + reset_text(log, LOG_FN) + reset_text(t, JOY_FN) + return stack, e, d + + +def reset_text(t, filename): + if os.path.exists(filename): + with open(filename) as f: + data = f.read() + if data: + t.delete('0.0', tk.END) + t.insert(tk.END, data) + + +def load_stack(): + if os.path.exists(STACK_FN): + with open(STACK_FN) as f: + return pickle.load(f) + + +tb = TEXT_BINDINGS.copy() +tb.update({ + '': lambda tv: tv.cut, + '': lambda tv: tv.copy_selection_to_stack, + '': lambda tv: tv.pastecut, + '': lambda tv: tv.copyto, + }) + + +defaults = dict(text_bindings=tb, width=80, height=25) + + +D = initialize() +for func in ( + reset_log, + show_log, + grand_reset, + key_bindings, + mouse_bindings, + ): + D[func.__name__] = func + + +stack = load_stack() + + +if stack is None: + w = StackDisplayWorld(dictionary=D) +else: + w = StackDisplayWorld(stack=stack, dictionary=D) + + +t = TextViewerWidget(w, **defaults) + + +log_window = tk.Toplevel() +log_window.protocol("WM_DELETE_WINDOW", log_window.withdraw) +log = TextViewerWidget(w, log_window, **defaults) + + +FONT = get_font('Iosevka', size=14) # Requires Tk root already set up. + + +init_text(log, 'Log', LOG_FN) +init_text(t, 'Joy - ' + JOY_HOME, JOY_FN) + + +GLOBAL_COMMANDS = { + '': 'words', + '': 'reset_log show_log', + '': 'clear reset_log show_log', + } +for event, command in GLOBAL_COMMANDS.items(): + t.bind_all(event, lambda _, _command=command: w.interpret(_command)) + + +class FileFaker(object): + + def __init__(self, T): + self.T = T + + def write(self, text): + self.T.insert('end', text) + self.T.see('end') + + def flush(self): + pass + + +def main(): + sys.stdout, old_stdout = FileFaker(log), sys.stdout + try: + t.mainloop() + finally: + sys.stdout = old_stdout + return 0 + + +if __name__ == '__main__': + main() diff --git a/joy/gui/mousebindings.py b/joy/gui/mousebindings.py new file mode 100644 index 0000000..baff8dd --- /dev/null +++ b/joy/gui/mousebindings.py @@ -0,0 +1,206 @@ +# -*- coding: utf-8 -*- +# +# Copyright © 2014, 2015 Simon Forman +# +# This file is part of joy.py +# +# joy.py is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# joy.py is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with joy.py. If not see . +# + + +#Do-nothing event handler. +nothing = lambda event: None + + +class MouseBindingsMixin: + """TextViewerWidget mixin class to provide mouse bindings.""" + + def __init__(self): + + #Remember our mouse button state + self.B1_DOWN = False + self.B2_DOWN = False + self.B3_DOWN = False + + #Remember our pending action. + self.dothis = nothing + + #We'll need to remember whether or not we've been moving B2. + self.beenMovingB2 = False + + #Unbind the events we're interested in. + for sequence in ( + "", "", "", + "", "", "", + "", "", "", + "", "", "", "", "" + ): + self.unbind(sequence) + self.unbind_all(sequence) + + self.event_delete('<>') #I forgot what this was for! :-P D'oh! + + #Bind our event handlers to their events. + self.bind("", self.B1d) + self.bind("", self.B1m) + self.bind("", self.B1r) + + self.bind("", self.B2d) + self.bind("", self.B2m) + self.bind("", self.B2r) + + self.bind("", self.B3d) + self.bind("", self.B3m) + self.bind("", self.B3r) + + self.bind("", self.leave) + + def B1d(self, event): + '''button one pressed''' + self.B1_DOWN = True + + if self.B2_DOWN: + + self.unhighlight_command() + + if self.B3_DOWN : + self.dothis = self.cancel + + else: + #copy TOS to the mouse (instead of system selection.) + self.dothis = self.copyto #middle-left-interclick + + elif self.B3_DOWN : + self.unhighlight_command() + self.dothis = self.opendoc #right-left-interclick + + else: + ##button 1 down, set insertion and begin selection. + ##Actually, do nothing. Tk Text widget defaults take care of it. + self.dothis = nothing + return + + #Prevent further event handling by returning "break". + return "break" + + def B2d(self, event): + '''button two pressed''' + self.B2_DOWN = 1 + + if self.B1_DOWN : + + if self.B3_DOWN : + self.dothis = self.cancel + + else: + #left-middle-interclick - cut selection to stack + self.dothis = self.cut + + elif self.B3_DOWN : + self.unhighlight_command() + self.dothis = self.lookup #right-middle-interclick - lookup + + else: + #middle-click - paste X selection to mouse pointer + self.set_insertion_point(event) + self.dothis = self.paste_X_selection_to_mouse_pointer + return + + return "break" + + def B3d(self, event): + '''button three pressed''' + self.B3_DOWN = 1 + + if self.B1_DOWN : + + if self.B2_DOWN : + self.dothis = self.cancel + + else: + #left-right-interclick - run selection + self.dothis = self.run_selection + + elif self.B2_DOWN : + #middle-right-interclick - Pop/Cut from TOS to insertion cursor + self.unhighlight_command() + self.dothis = self.pastecut + + else: + #right-click + self.CommandFirstDown(event) + + return "break" + + def B1m(self, event): + '''button one moved''' + if self.B2_DOWN or self.B3_DOWN: + return "break" + + def B2m(self, event): + '''button two moved''' + if self.dothis == self.paste_X_selection_to_mouse_pointer and \ + not (self.B1_DOWN or self.B3_DOWN): + + self.beenMovingB2 = True + return + + return "break" + + def B3m(self, event): + '''button three moved''' + if self.dothis == self.do_command and \ + not (self.B1_DOWN or self.B2_DOWN): + + self.update_command_word(event) + + return "break" + + def B1r(self, event): + '''button one released''' + self.B1_DOWN = False + + if not (self.B2_DOWN or self.B3_DOWN): + self.dothis(event) + + return "break" + + def B2r(self, event): + '''button two released''' + self.B2_DOWN = False + + if not (self.B1_DOWN or self.B3_DOWN or self.beenMovingB2): + self.dothis(event) + + self.beenMovingB2 = False + + return "break" + + def B3r(self, event): + '''button three released''' + self.B3_DOWN = False + + if not (self.B1_DOWN or self.B2_DOWN) : + self.dothis(event) + + return "break" + + def InsertFirstDown(self, event): + self.focus() + self.dothis = nothing + self.set_insertion_point(event) + + def CommandFirstDown(self, event): + self.dothis = self.do_command + self.update_command_word(event) diff --git a/joy/gui/textwidget.py b/joy/gui/textwidget.py new file mode 100644 index 0000000..0633dc0 --- /dev/null +++ b/joy/gui/textwidget.py @@ -0,0 +1,422 @@ +# -*- coding: utf-8 -*- +# +# Copyright © 2014, 2015, 2018 Simon Forman +# +# This file is part of joy.py +# +# joy.py is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# joy.py is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with joy.py. If not see . +# +''' + + +A Graphical User Interface for a dialect of Joy in Python. + + +The GUI + + History + Structure + Commands + Mouse Chords + Keyboard + Output from Joy + + +''' +from __future__ import print_function +try: + import tkinter as tk + from tkinter.font import families, Font +except ImportError: + import Tkinter as tk + from tkFont import families, Font + +from re import compile as regular_expression +from traceback import format_exc +import os, sys + +from joy.utils.stack import stack_to_string + +from .mousebindings import MouseBindingsMixin +from .world import World, is_numerical + + +def make_gui(dictionary): + t = TextViewerWidget(World(dictionary=dictionary)) + t['font'] = get_font() + t._root().title('Joy') + t.pack(expand=True, fill=tk.BOTH) + return t + + +def get_font(family='EB Garamond', size=14): + if family not in families(): + family = 'Times' + return Font(family=family, size=size) + + +#: Define mapping between Tkinter events and functions or methods. The +#: keys are string Tk "event sequences" and the values are callables that +#: get passed the TextViewer instance (so you can bind to methods) and +#: must return the actual callable to which to bind the event sequence. +TEXT_BINDINGS = { + + #I want to ensure that these keyboard shortcuts work. + '': lambda tv: tv._paste, + '': lambda tv: tv._paste, + '': lambda tv: tv._paste, + '': lambda tv: tv._control_enter, + } + + +class SavingMixin: + + def __init__(self, saver=None, filename=None, save_delay=2000): + self.saver = self._saver if saver is None else saver + self.filename = filename + self._save_delay = save_delay + self.tk.call(self._w, 'edit', 'modified', 0) + self.bind('<>', self._beenModified) + self._resetting_modified_flag = False + self._save = None + + def save(self): + ''' + Call _saveFunc() after a certain amount of idle time. + + Called by _beenModified(). + ''' + self._cancelSave() + if self.saver: + self._saveAfter(self._save_delay) + + def _saveAfter(self, delay): + ''' + Trigger a cancel-able call to _saveFunc() after delay milliseconds. + ''' + self._save = self.after(delay, self._saveFunc) + + def _saveFunc(self): + self._save = None + self.saver(self._get_contents()) + + def _saver(self, text): + if not self.filename: + return + with open(self.filename, 'w') as f: + os.chmod(self.filename, 0600) + f.write(text.encode('UTF_8')) + f.flush() + os.fsync(f.fileno()) + if hasattr(self, 'repo'): + self.repo.stage([self.repo_relative_filename]) + self.world.save() + + def _cancelSave(self): + if self._save is not None: + self.after_cancel(self._save) + self._save = None + + def _get_contents(self): + self['state'] = 'disabled' + try: + return self.get('0.0', 'end')[:-1] + finally: + self['state'] = 'normal' + + def _beenModified(self, event): + if self._resetting_modified_flag: + return + self._clearModifiedFlag() + self.save() + + def _clearModifiedFlag(self): + self._resetting_modified_flag = True + try: + self.tk.call(self._w, 'edit', 'modified', 0) + finally: + self._resetting_modified_flag = False + +## tags = self._saveTags() +## chunks = self.DUMP() +## print chunks + + +class TextViewerWidget(tk.Text, MouseBindingsMixin, SavingMixin): + """ + This class is a Tkinter Text with special mousebindings to make + it act as a Xerblin Text Viewer. + """ + + #This is a regular expression for finding commands in the text. + command_re = regular_expression(r'[-a-zA-Z0-9_\\~/.:!@#$%&*?=+<>]+') + + #These are the config tags for command text when it's highlighted. + command_tags = dict( + underline = 1, + bgstipple = "gray50", + borderwidth = "1", + foreground = "orange" + ) + + def __init__(self, world, master=None, **kw): + + self.world = world + if self.world.text_widget is None: + self.world.text_widget = self + + #Turn on undo, but don't override a passed-in setting. + kw.setdefault('undo', True) + +# kw.setdefault('bg', 'white') + kw.setdefault('wrap', 'word') + kw.setdefault('font', 'arial 12') + + text_bindings = kw.pop('text_bindings', TEXT_BINDINGS) + + #Create ourselves as a Tkinter Text + tk.Text.__init__(self, master, **kw) + + #Initialize our mouse mixin. + MouseBindingsMixin.__init__(self) + + #Initialize our saver mixin. + SavingMixin.__init__(self) + + #Add tag config for command highlighting. + self.tag_config('command', **self.command_tags) + + #Create us a command instance variable + self.command = '' + + #Activate event bindings. Modify text_bindings in your config + #file to affect the key bindings and whatnot here. + for event_sequence, callback_finder in text_bindings.items(): + callback = callback_finder(self) + self.bind(event_sequence, callback) + +## T.protocol("WM_DELETE_WINDOW", self.on_close) + + def find_command_in_line(self, line, index): + ''' + Return the command at index in line and its begin and end indices. + find_command_in_line(line, index) => command, begin, end + ''' + for match in self.command_re.finditer(line): + b, e = match.span() + if b <= index <= e: + return match.group(), b, e + + def paste_X_selection_to_mouse_pointer(self, event): + '''Paste the X selection to the mouse pointer.''' + try: + text = self.selection_get() + except tk.TclError: + return 'break' + self.insert_it(text) + + def update_command_word(self, event): + '''Highlight the command under the mouse.''' + self.unhighlight_command() + self.command = '' + index = '@%d,%d' % (event.x, event.y) + linestart = self.index(index + 'linestart') + lineend = self.index(index + 'lineend') + line = self.get(linestart, lineend) + row, offset = self._get_index(index) + + 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 + # command, we're done. + return + + cmd = self.find_command_in_line(line, offset) + if cmd is None: + return + + cmd, b, e = cmd + if self.world.has(cmd) or is_numerical(cmd): + self.command = cmd + self.highlight_command( + '%d.%d' % (row, b), + '%d.%d' % (row, e), + ) + + def highlight_command(self, from_, to): + '''Apply command style from from_ to to.''' + cmdstart = self.index(from_) + cmdend = self.index(to) + self.tag_add('command', cmdstart, cmdend) + + def do_command(self, event): + '''Do the currently highlighted command.''' + self.unhighlight_command() + if self.command: + self.run_command(self.command) + + def _control_enter(self, event): + select_indices = self.tag_ranges(tk.SEL) + if select_indices: + command = self.get(select_indices[0], select_indices[1]) + else: + linestart = self.index(tk.INSERT + ' linestart') + lineend = self.index(tk.INSERT + ' lineend') + command = self.get(linestart, lineend) + if command and not command.isspace(): + self.run_command(command) + return 'break' + + def run_command(self, command): + '''Given a string run it on the stack, report errors.''' + try: + self.world.interpret(command) + except SystemExit: + raise + except: + self.popupTB(format_exc().rstrip()) + + def unhighlight_command(self): + '''Remove any command highlighting.''' + self.tag_remove('command', 1.0, tk.END) + + def set_insertion_point(self, event): + '''Set the insertion cursor to the current mouse location.''' + self.focus() + self.mark_set(tk.INSERT, '@%d,%d' % (event.x, event.y)) + + def copy_selection_to_stack(self, event): + '''Copy selection to stack.''' + select_indices = self.tag_ranges(tk.SEL) + if select_indices: + s = self.get(select_indices[0], select_indices[1]) + self.world.push(s) + + def cut(self, event): + '''Cut selection to stack.''' + self.copy_selection_to_stack(event) + # Let the pre-existing machinery take care of cutting the selection. + self.event_generate("<>") + + def copyto(self, event): + '''Actually "paste" from TOS''' + s = self.world.peek() + if s is not None: + self.insert_it(s) + + def insert_it(self, s): + if not isinstance(s, basestring): + s = stack_to_string(s) + + # When pasting from the mouse we have to remove the current selection + # to prevent destroying it by the paste operation. + select_indices = self.tag_ranges(tk.SEL) + if select_indices: + # Set two marks to remember the selection. + self.mark_set('_sel_start', select_indices[0]) + self.mark_set('_sel_end', select_indices[1]) + self.tag_remove(tk.SEL, 1.0, tk.END) + + self.insert(tk.INSERT, s) + + if select_indices: + self.tag_add(tk.SEL, '_sel_start', '_sel_end') + self.mark_unset('_sel_start') + self.mark_unset('_sel_end') + + def run_selection(self, event): + '''Run the current selection if any on the stack.''' + select_indices = self.tag_ranges(tk.SEL) + if select_indices: + selection = self.get(select_indices[0], select_indices[1]) + self.tag_remove(tk.SEL, 1.0, tk.END) + self.run_command(selection) + + def pastecut(self, event): + '''Cut the TOS item to the mouse.''' + self.copyto(event) + self.world.pop() + + def opendoc(self, event): + '''OpenDoc the current command.''' + if self.command: + self.world.do_opendoc(self.command) + + def lookup(self, event): + '''Look up the current command.''' + if self.command: + self.world.do_lookup(self.command) + + def cancel(self, event): + '''Cancel whatever we're doing.''' + self.leave(None) + self.tag_remove(tk.SEL, 1.0, tk.END) + self._sel_anchor = '0.0' + self.mark_unset(tk.INSERT) + + def leave(self, event): + '''Called when mouse leaves the Text window.''' + self.unhighlight_command() + self.command = '' + + def _get_index(self, index): + '''Get the index in (int, int) form of index.''' + return tuple(map(int, self.index(index).split('.'))) + + def _paste(self, event): + '''Paste the system selection to the current selection, replacing it.''' + + # If we're "key" pasting, we have to move the insertion point + # to the selection so the pasted text gets inserted at the + # location of the deleted selection. + + select_indices = self.tag_ranges(tk.SEL) + if select_indices: + # Mark the location of the current insertion cursor + self.mark_set('tmark', tk.INSERT) + # Put the insertion cursor at the selection + self.mark_set(tk.INSERT, select_indices[1]) + + # Paste to the current selection, or if none, to the insertion cursor. + self.event_generate("<>") + + # If we mess with the insertion cursor above, fix it now. + if select_indices: + # Put the insertion cursor back where it was. + self.mark_set(tk.INSERT, 'tmark') + # And get rid of our unneeded mark. + self.mark_unset('tmark') + + return 'break' + + def popupTB(self, tb): + top = tk.Toplevel() + T = TextViewerWidget( + self.world, + top, + width=max(len(s) for s in tb.splitlines()) + 3, + ) + + T['background'] = 'darkgrey' + T['foreground'] = 'darkblue' + T.tag_config('err', foreground='yellow') + + T.insert(tk.END, tb) + last_line = str(int(T.index(tk.END).split('.')[0]) - 1) + '.0' + T.tag_add('err', last_line, tk.END) + T['state'] = tk.DISABLED + + top.title(T.get(last_line, tk.END).strip()) + + T.pack(expand=1, fill=tk.BOTH) + T.see(tk.END) diff --git a/joy/gui/world.py b/joy/gui/world.py new file mode 100644 index 0000000..a29875d --- /dev/null +++ b/joy/gui/world.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +# +# Copyright © 2014, 2015 Simon Forman +# +# This file is part of joy.py +# +# joy.py is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# joy.py is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with joy.py. If not see . +# +from inspect import getdoc + +from joy.joy import run +from joy.utils.stack import stack_to_string + + +def is_numerical(s): + try: + float(s) + except ValueError: + return False + return True + + +class World(object): + + def __init__(self, stack=(), dictionary=None, text_widget=None): + self.stack = stack + self.dictionary = dictionary or {} + self.text_widget = text_widget + + def do_lookup(self, name): + word = self.dictionary[name] + self.stack = word, self.stack + self.print_stack() + + def do_opendoc(self, name): + if is_numerical(name): + print 'The number', name + else: + try: + word = self.dictionary[name] + except KeyError: + print repr(name), '???' + else: + print getdoc(word) + self.text_widget.see('end') + + def pop(self): + if self.stack: + self.stack = self.stack[1] + self.print_stack() + + def push(self, it): + it = it.encode('utf8') + self.stack = it, self.stack + self.print_stack() + + def peek(self): + if self.stack: + return self.stack[0] + + def interpret(self, command): + self.stack, _, self.dictionary = run( + command, + self.stack, + self.dictionary, + ) + self.print_stack() + + def has(self, name): + return self.dictionary.has_key(name) + + def save(self): + pass + + def print_stack(self): + stack_out_index = self.text_widget.search('<' 'STACK', 1.0) + if stack_out_index: + self.text_widget.see(stack_out_index) + s = stack_to_string(self.stack) + '\n' + self.text_widget.insert(stack_out_index, s) diff --git a/setup.py b/setup.py index 9b9d4d7..4ca08b1 100755 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ setup( author_email='forman.simon@gmail.com', url='https://joypy.osdn.io', license='GPLv3+', - packages=['joy', 'joy.utils'], + packages=['joy', 'joy.utils', 'joy.gui'], classifiers=[ 'Development Status :: 4 - Beta', 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', -- 2.11.0