From d4fdde50f90048614e9778ea6c84ab482b62fb98 Mon Sep 17 00:00:00 2001 From: Simon Forman Date: Mon, 6 May 2019 13:07:01 -0700 Subject: [PATCH] Initial bring over of VUI code. (Won't work yet.) --- joy/vui/Iosevka12.BMP | Bin 0 -> 46938 bytes joy/vui/README.txt | 163 ++++++ joy/vui/__init__.py | 0 joy/vui/core.py | 201 +++++++ joy/vui/debug_main.py | 18 + joy/vui/default_joy_home/definitions.txt | 17 + joy/vui/default_joy_home/library.py | 206 +++++++ joy/vui/default_joy_home/log.txt | 1 + joy/vui/default_joy_home/menu.txt | 51 ++ joy/vui/default_joy_home/scratch.txt | 85 +++ joy/vui/default_joy_home/stack.pickle | 1 + joy/vui/display.py | 471 ++++++++++++++++ joy/vui/font_data.py | 167 ++++++ joy/vui/init_joy_home.py | 256 +++++++++ joy/vui/main.py | 113 ++++ joy/vui/persist_task.py | 185 +++++++ joy/vui/stack_viewer.py | 50 ++ joy/vui/text_viewer.py | 674 +++++++++++++++++++++++ joy/vui/viewer.py | 232 ++++++++ 19 files changed, 2891 insertions(+) create mode 100644 joy/vui/Iosevka12.BMP create mode 100644 joy/vui/README.txt create mode 100644 joy/vui/__init__.py create mode 100644 joy/vui/core.py create mode 100644 joy/vui/debug_main.py create mode 100644 joy/vui/default_joy_home/definitions.txt create mode 100644 joy/vui/default_joy_home/library.py create mode 100644 joy/vui/default_joy_home/log.txt create mode 100644 joy/vui/default_joy_home/menu.txt create mode 100644 joy/vui/default_joy_home/scratch.txt create mode 100644 joy/vui/default_joy_home/stack.pickle create mode 100644 joy/vui/display.py create mode 100644 joy/vui/font_data.py create mode 100644 joy/vui/init_joy_home.py create mode 100644 joy/vui/main.py create mode 100644 joy/vui/persist_task.py create mode 100644 joy/vui/stack_viewer.py create mode 100644 joy/vui/text_viewer.py create mode 100644 joy/vui/viewer.py diff --git a/joy/vui/Iosevka12.BMP b/joy/vui/Iosevka12.BMP new file mode 100644 index 0000000000000000000000000000000000000000..755eca12247688a9a7a855c1cfe33b52d6e4d3e2 GIT binary patch literal 46938 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 Ii5T$qfA+l?Z2$lO literal 0 HcmV?d00001 diff --git a/joy/vui/README.txt b/joy/vui/README.txt new file mode 100644 index 0000000..6ecc42b --- /dev/null +++ b/joy/vui/README.txt @@ -0,0 +1,163 @@ +What is it? + +A simple Graphical User Interface for the Joy programming language, +written using Pygame to bypass X11 et. al., modeled on the Oberon OS, and +intended to be just functional enough to support bootstrapping further Joy +development. + +It's basic functionality is more-or-less as a crude text editor along with +a simple Joy runtime (interpreter, stack, and dictionary.) It auto- saves +any named files (in a versioned home directory) and you can write new Joy +primitives in Python and Joy definitions and immediately install and use +them, as well as recording them for reuse (after restarts.) + +How it works now. + +The only dependencies are Pygame and Dulwich (a Python Git library.) + +When the main.py script starts it checks for an environment var "JOY_HOME" +which should point to a directory where you want the system to store the +files ("resources") it will edit and save, this directory defaults to +'~/.joypy'. The first time you run it, it will create some default files +as content. Right click on see_resources to open a viewer with the list +of resources (files), copy a name to the stack and right click on +open_resource_at_good_location to open a viewer on that resource. + +Right now the screen size defaults to windowed 1024x768, but if you pass +the '-f' option to the main.py script the UI will take up the full screen +at the highest available resolution. The window is divided into two (or +three in fullscreen) vertical "tracks", and the number and width of the +tracks are fixed at start up. (Feel free to edit the values in main.py to +play around with different track configurations.) Each track gets divided +horizontally into zero or more "viewers" (like windows in a windowed GUI, +cf. Chapter 4 of "Project Oberon") for a kind of tiled layout. + +Currently, there are only two kinds of (interesting) viewers: TextViewers +and StackViewer. The TextViewers are crude text editors. They provide +just enough functionality to let the user write text and code (Python and +Joy) and execute Joy functions. One important thing they do is +automatically save their content after changes. No more lost work. + +The StackViewer is a specialized TextViewer that shows the contents of the +Joy stack one line per stack item. It's a very handy visual aid to keep +track of what's going on. There's also a log.txt file that gets written +to when commands are executed, and so records the log of user actions and +system events. It tends to fill up quickly so there's a reset_log command +that clears it out. + +Viewers have "grow" and "close" in ther menu bars. These are buttons. +When you right-click on grow a viewer a copy is created that covers that +viewer's entire track. If you grow a viewer that already takes up its +whole track then a copy is created that takes up an additional track, up +to the whole screen. Closing a viewer just deletes that viewer, and when +a track has no more viewers, it is deleted and that exposes any previous +tracks and viewers that were hidden. + +(Note: if you ever close all the viewers and are sitting at a blank screen +with nowhere to type and execute commands, press the Pause/Break key. +This will open a new "trap" viewer which you can then use to recover.) + +Copies of a viewer all share the same model and update their display as it +changes. (If you have two viewers open on the same named resource and edit +one you'll see the other update as you type.) + +UI Guide + +left mouse sets cursor in text, in menu bar resizes viewer interactively +(this is a little buggy in that you can move the mouse quickly and get +outside the menu, leaving the viewer in the "resizing" state. Until I fix +this, the workaround is to just grab the menu bar again and wiggle it a +few pixels and let go. This will reset the machinery.) + +Right mouse executes Joy command (functions), and you can drag with the +right button to highlight (well, underline) commands. Words that aren't +names of Joy commands won't be underlined. Release the button to execute +the command. + +The middle mouse button (usually a wheel these days) scrolls the text but +you can also click and drag any viewer with it to move that viewer to +another track or to a different location in the same track. There's no +direct visual feedback for this (yet) but that dosen't seem to impair its +usefulness. + +F1, F2 - set selection begin and end markers (crude but usable.) + +F3 - copy selected text to the top of the stack. + +Shift-F3 - as copy then run "parse" command on the string. + +F4 - cut selected text to the top of the stack. + +Shift-F4 - as cut then run "pop" (delete selection.) + +Joy + +Pretty much all of the rest of the functionality of the system is provided +by executing Joy commands (aka functions, aka "words" in Forth) by right- +clicking on their names in any text. + +To get help on a Joy function select the name of the function in a +TextViewer using F1 and F2, then press shift-F3 to parse the selection. +The function (really its Symbol) will appear on the stack in brackets (a +"quoted program" such as "[pop]".) Then right-click on the word help in +any TextViewer (if it's not already there, just type it in somewhere.) +This will print the docstring or definition of the word (function) to +stdout. At some point I'll write a thing to send that to the log.txt file +instead, but for now look for output in the terminal. + +I have pre-defined some system-specific commands, like see_stack to open a +StackViewer, and I should really go and add docstrings to those (so they +work with the help command.) + +... inscribe and evaluate for making new Joy and Python, respectively, +commands... + +---- + + +Still to do: +* Return key can orphan a line at the bottom of a viewer. +* Calculator buttons on the numpad? +* System query for most recent selection +* Home/End keys +* Vertical scrolling w/ scrollbar? +* Shift-scroll changes viewer height? +* Horizontal scrolling w/ keys +* Horizontal scrolling w/ scrollbar? +* Pgup/down keys? +* Tab key? +* When moving viewers sometimes a command gets executed from the underlying + viewer. This shouldn't happen. + +Done: +- Redirect stdout to "print" to the log. +- Initial contents for JOY_HOME. +- Pause/Break to open a trap viewer (in case you close them all.) +- "shutdown" signal to tell PT to commit outstanding changes. +- Local library auto-loaded at start-time + - library.py, primitives in Python + - definitions.txt +- Can name and persist a viewer on an unstored string(list). +- Inscribe function +- Reverse video, well, grey background, menu bars +- PT scans JOY_HOME for resource lists +- Capture and display tracebacks +- StackViewer +- Update log when stack changes +- Open a resource list +- Open a viewer on a (unstored) string +- Selecting text +- Copy and Cut +- Paste +- Menu text, commands and name or title +- "print" to e.g. log +- Command evaluation +- Joy integration +- Persistance of data +- Content change notification +- Vertical scrolling w/ keys +- Vertical scrolling w/ mouse wheel +- Enter/return key +- Arrow keys wrap at line ends +- Backspace/delete wrap at line ends + diff --git a/joy/vui/__init__.py b/joy/vui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/joy/vui/core.py b/joy/vui/core.py new file mode 100644 index 0000000..0501b30 --- /dev/null +++ b/joy/vui/core.py @@ -0,0 +1,201 @@ +from sys import stderr +from traceback import format_exc +import pygame +from joy.joy import run +from joy.utils.stack import stack_to_string + + +COMMITTER = 'Joy ' + + +BLACK = FOREGROUND = 0, 0, 0 +GREY = 127, 127, 127 +WHITE = BACKGROUND = 255, 255, 255 +BLUE = 100, 100, 255 +GREEN = 70, 200, 70 + + +MOUSE_EVENTS = frozenset({ + pygame.MOUSEMOTION, + pygame.MOUSEBUTTONDOWN, + pygame.MOUSEBUTTONUP + }) + + +ARROW_KEYS = frozenset({ + pygame.K_UP, + pygame.K_DOWN, + pygame.K_LEFT, + pygame.K_RIGHT + }) + + +TASK_EVENTS = tuple(range(pygame.USEREVENT, pygame.NUMEVENTS)) +AVAILABLE_TASK_EVENTS = set(TASK_EVENTS) + + +ALLOWED_EVENTS = [pygame.QUIT, pygame.KEYUP, pygame.KEYDOWN] +ALLOWED_EVENTS.extend(MOUSE_EVENTS) +ALLOWED_EVENTS.extend(TASK_EVENTS) + + +# Message status codes... dunno if this is a good idea or not... +ERROR = -1 +PENDING = 0 +SUCCESS = 1 + + +# messaging support + + +class Message(object): + + def __init__(self, sender): + self.sender = sender + + +class CommandMessage(Message): + + def __init__(self, sender, command): + Message.__init__(self, sender) + self.command = command + + +class ModifyMessage(Message): + + def __init__(self, sender, subject, **details): + Message.__init__(self, sender) + self.subject = subject + self.details = details + + +class OpenMessage(Message): + + def __init__(self, sender, name): + Message.__init__(self, sender) + self.name = name + self.content_id = self.thing = None + self.status = PENDING + self.traceback = None + + +class PersistMessage(Message): + def __init__(self, sender, content_id, **details): + Message.__init__(self, sender) + self.content_id = content_id + self.details = details + + +class ShutdownMessage(Message): pass + + +# Joy Interpreter & Context + + +class World(object): + + def __init__(self, stack_id, stack_holder, dictionary, notify, log): + self.stack_holder = stack_holder + self.dictionary = dictionary + self.notify = notify + self.stack_id = stack_id + self.log = log.lines + self.log_id = log.content_id + + def handle(self, message): + if (isinstance(message, ModifyMessage) + and message.subject is self.stack_holder + ): + self._log_lines('', '%s <-' % self.format_stack()) + if not isinstance(message, CommandMessage): + return + c, s, d = message.command, self.stack_holder[0], self.dictionary + self._log_lines('', '-> %s' % (c,)) + self.stack_holder[0], _, self.dictionary = run(c, s, d) + mm = ModifyMessage(self, self.stack_holder, content_id=self.stack_id) + self.notify(mm) + + def _log_lines(self, *lines): + self.log.extend(lines) + self.notify(ModifyMessage(self, self.log, content_id=self.log_id)) + + def format_stack(self): + try: + return stack_to_string(self.stack_holder[0]) + except: + print >> stderr, format_exc() + return str(self.stack_holder[0]) + + +def push(sender, item, notify, stack_name='stack.pickle'): + om = OpenMessage(sender, stack_name) + notify(om) + if om.status == SUCCESS: + om.thing[0] = item, om.thing[0] + notify(ModifyMessage(sender, om.thing, content_id=om.content_id)) + return om.status + + +def open_viewer_on_string(sender, content, notify): + push(sender, content, notify) + notify(CommandMessage(sender, 'good_viewer_location open_viewer')) + + +# main loop + + +class TheLoop(object): + + FRAME_RATE = 24 + + def __init__(self, display, clock): + self.display = display + self.clock = clock + self.tasks = {} + self.running = False + + def install_task(self, F, milliseconds): + try: + task_event_id = AVAILABLE_TASK_EVENTS.pop() + except KeyError: + raise RuntimeError('out of task ids') + self.tasks[task_event_id] = F + pygame.time.set_timer(task_event_id, milliseconds) + return task_event_id + + def remove_task(self, task_event_id): + assert task_event_id in self.tasks, repr(task_event_id) + pygame.time.set_timer(task_event_id, 0) + del self.tasks[task_event_id] + AVAILABLE_TASK_EVENTS.add(task_event_id) + + def __del__(self): + for task_event_id in self.tasks: + pygame.time.set_timer(task_event_id, 0) + + def run_task(self, task_event_id): + task = self.tasks[task_event_id] + try: + task() + except: + traceback = format_exc() + self.remove_task(task_event_id) + print >> stderr, traceback + print >> stderr, 'TASK removed due to ERROR', task + open_viewer_on_string(self, traceback, self.display.broadcast) + + def loop(self): + self.running = True + while self.running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + self.running = False + elif event.type == pygame.KEYUP and event.key == pygame.K_ESCAPE: + self.running = False + elif event.type in self.tasks: + self.run_task(event.type) + else: + self.display.dispatch_event(event) + pygame.display.update() + self.clock.tick(self.FRAME_RATE) + self.display.broadcast(ShutdownMessage(self)) diff --git a/joy/vui/debug_main.py b/joy/vui/debug_main.py new file mode 100644 index 0000000..128a993 --- /dev/null +++ b/joy/vui/debug_main.py @@ -0,0 +1,18 @@ +import sys, traceback + +# To enable "hot" reloading in the IDLE shell. +for name in 'core main display viewer text_viewer stack_viewer persist_task'.split(): + try: + del sys.modules[name] + except KeyError: + pass + +import main + +try: + A = A # (screen, clock, pt), three things that we DON'T want to recreate + # each time we restart main(). +except NameError: + A = main.init() + +d = main.main(*A) diff --git a/joy/vui/default_joy_home/definitions.txt b/joy/vui/default_joy_home/definitions.txt new file mode 100644 index 0000000..3c3bca2 --- /dev/null +++ b/joy/vui/default_joy_home/definitions.txt @@ -0,0 +1,17 @@ +see_stack == good_viewer_location open_stack +see_resources == list_resources good_viewer_location open_viewer +open_resource_at_good_location == good_viewer_location open_resource +see_log == "log.txt" open_resource_at_good_location +see_definitions == "definitions.txt" open_resource_at_good_location +round_to_cents == 100 * ++ floor 100 / +reset_log == "del log.lines[1:] ; log.at_line = 0" evaluate +see_menu == "menu.txt" good_viewer_location open_resource + +# Ordered Binary Tree datastructure functions. +BTree-new == swap [[] []] cons cons + _BTree-P == over [popop popop first] nullary + _BTree-T> == [cons cons dipdd] cons cons cons infra + _BTree-T< == [cons cons dipd] cons cons cons infra + _BTree-E == pop swap roll< rest rest cons cons + _BTree-recur == _BTree-P [_BTree-T>] [_BTree-E] [_BTree-T<] cmp +BTree-add == [popop not] [[pop] dipd BTree-new] [] [_BTree-recur] genrec diff --git a/joy/vui/default_joy_home/library.py b/joy/vui/default_joy_home/library.py new file mode 100644 index 0000000..921acce --- /dev/null +++ b/joy/vui/default_joy_home/library.py @@ -0,0 +1,206 @@ +''' +This file is execfile()'d with a namespace containing: + + D - the Joy dictionary + d - the Display object + pt - the PersistTask object + log - the log.txt viewer + loop - the TheLoop main loop object + stack_holder - the Python list object that holds the Joy stack tuple + world - the Joy environment + +''' +from joy.library import ( + DefinitionWrapper, + FunctionWrapper, + SimpleFunctionWrapper, + ) +from joy.utils.stack import list_to_stack, concat +from vui import core, text_viewer, stack_viewer + + +def install(command): D[command.name] = command + + +@install +@SimpleFunctionWrapper +def list_resources(stack): + ''' + Put a string on the stack with the names of all the known resources + one-per-line. + ''' + return '\n'.join(pt.scan()), stack + + +@install +@SimpleFunctionWrapper +def open_stack(stack): + ''' + Given a coordinate pair [x y] (in pixels) open a StackViewer there. + ''' + (x, (y, _)), stack = stack + V = d.open_viewer(x, y, stack_viewer.StackViewer) + V.draw() + return stack + + +@install +@SimpleFunctionWrapper +def open_resource(stack): + ''' + Given a coordinate pair [x y] (in pixels) and the name of a resource + (from list_resources command) open a viewer on that resource at that + location. + ''' + ((x, (y, _)), (name, stack)) = stack + om = core.OpenMessage(world, name) + d.broadcast(om) + if om.status == core.SUCCESS: + V = d.open_viewer(x, y, text_viewer.TextViewer) + V.content_id, V.lines = om.content_id, om.thing + V.draw() + return stack + + +@install +@SimpleFunctionWrapper +def name_viewer(stack): + ''' + Given a string name on the stack, if the currently focused viewer is + anonymous, name the viewer and persist it in the resource store under + that name. + ''' + name, stack = stack + assert isinstance(name, str), repr(name) + if d.focused_viewer and not d.focused_viewer.content_id: + d.focused_viewer.content_id = name + pm = core.PersistMessage(world, name, thing=d.focused_viewer.lines) + d.broadcast(pm) + d.focused_viewer.draw_menu() + return stack + + +##@install +##@SimpleFunctionWrapper +##def persist_viewer(stack): +## if self.focused_viewer: +## +## self.focused_viewer.content_id = name +## self.focused_viewer.draw_menu() +## return stack + + +@install +@SimpleFunctionWrapper +def inscribe(stack): + ''' + Create a new Joy function definition in the Joy dictionary. A + definition is given as a string with a name followed by a double + equal sign then one or more Joy functions, the body. for example: + + sqr == dup mul + + If you want the definition to persist over restarts, enter it into + the definitions.txt resource. + ''' + definition, stack = stack + DefinitionWrapper.add_def(definition, D) + return stack + + +@install +@SimpleFunctionWrapper +def open_viewer(stack): + ''' + Given a coordinate pair [x y] (in pixels) and a string, open a new + unnamed viewer on that string at that location. + ''' + ((x, (y, _)), (content, stack)) = stack + V = d.open_viewer(x, y, text_viewer.TextViewer) + V.lines = content.splitlines() + V.draw() + return stack + + +@install +@SimpleFunctionWrapper +def good_viewer_location(stack): + ''' + Leave a coordinate pair [x y] (in pixels) on the stack that would + be a good location at which to open a new viewer. (The heuristic + employed is to take up the bottom half of the currently open viewer + with the greatest area.) + ''' + viewers = list(d.iter_viewers()) + if viewers: + viewers.sort(key=lambda (V, x, y): V.w * V.h) + V, x, y = viewers[-1] + coords = (x + 1, (y + V.h / 2, ())) + else: + coords = (0, (0, ())) + return coords, stack + + +@install +@FunctionWrapper +def cmp_(stack, expression, dictionary): + ''' + The cmp combinator takes two values and three quoted programs on the + stack and runs one of the three depending on the results of comparing + the two values: + + a b [G] [E] [L] cmp + ------------------------- a > b + G + + a b [G] [E] [L] cmp + ------------------------- a = b + E + + a b [G] [E] [L] cmp + ------------------------- a < b + L + + ''' + L, (E, (G, (b, (a, stack)))) = stack + expression = concat(G if a > b else L if a < b else E, expression) + return stack, expression, dictionary + + +@install +@SimpleFunctionWrapper +def list_viewers(stack): + ''' + Put a string on the stack with some information about the currently + open viewers, one-per-line. This is kind of a demo function, rather + than something really useful. + ''' + lines = [] + for x, T in d.tracks: + #lines.append('x: %i, w: %i, %r' % (x, T.w, T)) + for y, V in T.viewers: + lines.append('x: %i y: %i h: %i %r %r' % (x, y, V.h, V.content_id, V)) + return '\n'.join(lines), stack + + +@install +@SimpleFunctionWrapper +def splitlines(stack): + ''' + Given a string on the stack replace it with a list of the lines in + the string. + ''' + text, stack = stack + assert isinstance(text, str), repr(text) + return list_to_stack(text.splitlines()), stack + + +@install +@SimpleFunctionWrapper +def hiya(stack): + ''' + Demo function to insert "Hi World!" into the current viewer, if any. + ''' + if d.focused_viewer: + d.focused_viewer.insert('Hi World!') + return stack diff --git a/joy/vui/default_joy_home/log.txt b/joy/vui/default_joy_home/log.txt new file mode 100644 index 0000000..92f9c3c --- /dev/null +++ b/joy/vui/default_joy_home/log.txt @@ -0,0 +1 @@ +Joypy log diff --git a/joy/vui/default_joy_home/menu.txt b/joy/vui/default_joy_home/menu.txt new file mode 100644 index 0000000..930390c --- /dev/null +++ b/joy/vui/default_joy_home/menu.txt @@ -0,0 +1,51 @@ + name_viewer + list_resources + open_resource_at_good_location + good_viewer_location + open_viewer + see_stack + see_resources + see_definitions + see_log + reset_log + + inscribe + evaluate + + pop clear dup swap + + add sub mul div truediv modulus divmod + pm ++ -- sum product pow sqr sqrt + < <= = >= > <> + & << >> + + i dupdip + +!= % & * *fraction *fraction0 + ++ - -- / < << <= <> = > >= >> ? ^ +abs add anamorphism and app1 app2 app3 at average +b binary branch +choice clear cleave concat cons +dinfrirst dip dipd dipdd disenstacken div divmod down_to_zero drop +dudipd dup dupd dupdip +enstacken eq +first flatten floor floordiv +gcd ge genrec getitem grand_reset gt +help +i id ifte infra inscribe +key_bindings +le least_fraction loop lshift lt +map max min mod modulus mouse_bindings mul +ne neg not nullary +of or over +pam parse pick pm pop popd popdd popop pow pred primrec product +quoted +range range_to_zero rem remainder remove reset_log rest reverse +roll< roll> rolldown rollup rshift run +second select sharing show_log shunt size sort sqr sqrt stack step +step_zero sub succ sum swaack swap swoncat swons +take ternary third times truediv truthy tuck +unary uncons unique unit unquoted unstack +void +warranty while words +x xor +zip diff --git a/joy/vui/default_joy_home/scratch.txt b/joy/vui/default_joy_home/scratch.txt new file mode 100644 index 0000000..e9f404d --- /dev/null +++ b/joy/vui/default_joy_home/scratch.txt @@ -0,0 +1,85 @@ +What is it? + +A simple Graphical User Interface for the Joy programming language, +written using Pygame to bypass X11 et. al., modeled on the Oberon OS, and +intended to be just functional enough to support bootstrapping further Joy +development. + +It's basic functionality is more-or-less as a crude text editor along with +a simple Joy runtime (interpreter, stack, and dictionary.) It auto- saves +any named files (in a versioned home directory) and you can write new Joy +primitives in Python and Joy definitions and immediately install and use +them, as well as recording them for reuse (after restarts.) + +Currently, there are only two kinds of (interesting) viewers: TextViewers +and StackViewer. The TextViewers are crude text editors. They provide +just enough functionality to let the user write text and code (Python and +Joy) and execute Joy functions. One important thing they do is +automatically save their content after changes. No more lost work. + +The StackViewer is a specialized TextViewer that shows the contents of the +Joy stack one line per stack item. It's a very handy visual aid to keep +track of what's going on. There's also a log.txt file that gets written +to when commands are executed, and so records the log of user actions and +system events. It tends to fill up quickly so there's a reset_log command +that clears it out. + +Viewers have "grow" and "close" in their menu bars. These are buttons. +When you right-click on grow a viewer a copy is created that covers that +viewer's entire track. If you grow a viewer that already takes up its +whole track then a copy is created that takes up an additional track, up +to the whole screen. Closing a viewer just deletes that viewer, and when +a track has no more viewers, it is deleted and that exposes any previous +tracks and viewers that were hidden. + +(Note: if you ever close all the viewers and are sitting at a blank screen +with nowhere to type and execute commands, press the Pause/Break key. +This will open a new "trap" viewer which you can then use to recover.) + +Copies of a viewer all share the same model and update their display as it +changes. (If you have two viewers open on the same named resource and edit +one you'll see the other update as you type.) + +UI Guide + +left mouse sets cursor in text, in menu bar resizes viewer interactively +(this is a little buggy in that you can move the mouse quickly and get +outside the menu, leaving the viewer in the "resizing" state. Until I fix +this, the workaround is to just grab the menu bar again and wiggle it a +few pixels and let go. This will reset the machinery.) + +Right mouse executes Joy command (functions), and you can drag with the +right button to highlight (well, underline) commands. Words that aren't +names of Joy commands won't be underlined. Release the button to execute +the command. + +The middle mouse button (usually a wheel these days) scrolls the text but +you can also click and drag any viewer with it to move that viewer to +another track or to a different location in the same track. There's no +direct visual feedback for this (yet) but that dosen't seem to impair its +usefulness. + +F1, F2 - set selection begin and end markers (crude but usable.) + +F3 - copy selected text to the top of the stack. + +Shift-F3 - as copy then run "parse" command on the string. + +F4 - cut selected text to the top of the stack. + +Shift-F4 - as cut then run "pop" (delete selection.) + +Joy + +Pretty much all of the rest of the functionality of the system is provided +by executing Joy commands (aka functions, aka "words" in Forth) by right- +clicking on their names in any text. + +To get help on a Joy function select the name of the function in a +TextViewer using F1 and F2, then press shift-F3 to parse the selection. +The function (really its Symbol) will appear on the stack in brackets (a +"quoted program" such as "[pop]".) Then right-click on the word help in +any TextViewer (if it's not already there, just type it in somewhere.) +This will print the docstring or definition of the word (function) to +stdout. At some point I'll write a thing to send that to the log.txt file +instead, but for now look for output in the terminal. diff --git a/joy/vui/default_joy_home/stack.pickle b/joy/vui/default_joy_home/stack.pickle new file mode 100644 index 0000000..4b4aaa9 --- /dev/null +++ b/joy/vui/default_joy_home/stack.pickle @@ -0,0 +1 @@ +(t. \ No newline at end of file diff --git a/joy/vui/display.py b/joy/vui/display.py new file mode 100644 index 0000000..20b044d --- /dev/null +++ b/joy/vui/display.py @@ -0,0 +1,471 @@ +# -*- coding: utf-8 -*- +# +# Copyright © 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 . +# +''' +This module implements a simple visual display system modeled on Oberon. + +Refer to Chapter 4 of the Project Oberon book for more information. + +There is a Display object that manages a pygame surface and N vertical +tracks each of which manages zero or more viewers. +''' +from copy import copy +from sys import stderr +from traceback import format_exc +import pygame +from core import ( + open_viewer_on_string, + GREY, + MOUSE_EVENTS, + ) +from viewer import Viewer +import text_viewer + + +class Display(object): + ''' + Manage tracks and viewers on a screen (Pygame surface.) + + 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 + small one on the left and two larger ones of the same size, each four + times wider than the left one. + + 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 + 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 + and reveals the hidden tracks (and their viewers, if any.) + + In order to facilitate command underlining while mouse dragging the + lookup parameter must be a function that accepts a string and returns + a Boolean indicating whether that string is a valid Joy function name. + Typically you pass in the __contains__ method of the Joy dict. This + 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 + World context object, going through the whole Display.broadcast() + machinery, etc. Not something you want to do on each MOUSEMOTION + event. + ''' + + def __init__(self, screen, lookup, *track_ratios): + self.screen = screen + self.w, self.h = screen.get_width(), screen.get_height() + self.lookup = lookup + self.focused_viewer = None + self.tracks = [] # (x, track) + self.handlers = [] # Non-viewers that should receive messages. + # Create the tracks. + if not track_ratios: track_ratios = 1, 4 + x, total = 0, sum(track_ratios) + for ratio in track_ratios[:-1]: + track_width = self.w * ratio / total + assert track_width >= 10 # minimum width 10 pixels + self._open_track(x, track_width) + x += track_width + self._open_track(x, self.w - x) + + def _open_track(self, x, w): + '''Helper function to create the pygame surface and Track.''' + track_surface = self.screen.subsurface((x, 0, w, self.h)) + self.tracks.append((x, Track(track_surface))) + + def open_viewer(self, x, y, class_): + ''' + Open a viewer of class_ at the x, y location on the display, + return the viewer. + ''' + track = self._track_at(x)[0] + V = track.open_viewer(y, class_) + V.focus(self) + return V + + def close_viewer(self, viewer): + '''Close the viewer.''' + for x, track in self.tracks: + if track.close_viewer(viewer): + if not track.viewers and track.hiding: + i = self.tracks.index((x, track)) + self.tracks[i:i + 1] = track.hiding + assert sorted(self.tracks) == self.tracks + for _, exposed_track in track.hiding: + exposed_track.redraw() + if viewer is self.focused_viewer: + self.focused_viewer = None + break + + def change_viewer(self, viewer, y, relative=False): + ''' + Adjust the top of the viewer to a new y within the boundaries of + its neighbors. + + If relative is False new_y should be in screen coords, else new_y + should be relative to the top of the viewer. + ''' + for _, track in self.tracks: + if track.change_viewer(viewer, y, relative): + break + + def grow_viewer(self, viewer): + ''' + Cause the viewer to take up its whole track or, if it does + already, take up another track, up to the whole screen. + + 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 + tracks and viewers are retained, and they get restored when the + covering track closes, which happens automatically when the last + viewer in the covering track is closed. + ''' + for x, track in self.tracks: + for _, V in track.viewers: + if V is viewer: + return self._grow_viewer(x, track, viewer) + + def _grow_viewer(self, x, track, viewer): + '''Helper function to "grow" a viewer.''' + new_viewer = None + + if viewer.h < self.h: + # replace the track with a new track that contains + # a copy of the viewer at full height. + new_track = Track(track.surface) # Reuse it, why not? + new_viewer = copy(viewer) + new_track._grow_by(new_viewer, 0, self.h - viewer.h) + new_track.viewers.append((0, new_viewer)) + new_track.hiding = [(x, track)] + self.tracks[self.tracks.index((x, track))] = x, new_track + + elif viewer.w < self.w: + # replace two tracks + i = self.tracks.index((x, track)) + try: # prefer the one on the right + xx, xtrack = self.tracks[i + 1] + except IndexError: + i -= 1 # okay, the one on the left + xx, xtrack = self.tracks[i] + hiding = [(xx, xtrack), (x, track)] + else: + hiding = [(x, track), (xx, xtrack)] + # 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 + # the whole display screen (the only way you can get just one + # track is by growing a viewer to cover the whole screen.) + # Ergo, viewer.w == self.w, so this branch doesn't run. + new_x = min(x, xx) + new_w = track.w + xtrack.w + r = new_x, 0, new_w, self.h + new_track = Track(self.screen.subsurface(r)) + new_viewer = copy(viewer) + r = 0, 0, new_w, self.h + new_viewer.resurface(new_track.surface.subsurface(r)) + new_track.viewers.append((0, new_viewer)) + new_track.hiding = hiding + self.tracks[i:i + 2] = [(new_x, new_track)] + new_viewer.draw() + + return new_viewer + + def _move_viewer(self, to, rel_y, viewer, _x, y): + ''' + Helper function to move (really copy) a viewer to a new location. + ''' + h = to.split(rel_y) + new_viewer = copy(viewer) + if not isinstance(to, Track): + to = next(T for _, T in self.tracks + for _, V in T.viewers + if V is to) + new_viewer.resurface(to.surface.subsurface((0, y, to.w, h))) + to.viewers.append((y, new_viewer)) + to.viewers.sort() # bisect.insort() would be overkill here. + new_viewer.draw() + self.close_viewer(viewer) + + def _track_at(self, x): + ''' + Return the track at x along with the track-relative x coordinate, + raise ValueError if x is off-screen. + ''' + for track_x, track in self.tracks: + if x < track_x + track.w: + return track, x - track_x + raise ValueError('x outside display: %r' % (x,)) + + def at(self, x, y): + ''' + Return the viewer (which can be a Track) at the x, y location, + along with the relative-to-viewer-surface x and y coordinates. + If there is no viewer at the location the Track will be returned + instead. + ''' + track, x = self._track_at(x) + viewer, y = track.viewer_at(y) + return viewer, x, y + + def iter_viewers(self): + for x, T in self.tracks: + for y, V in T.viewers: + yield V, x, y + + def done_resizing(self): + for _, track in self.tracks: # This should be done by a Message? + if track.resizing_viewer: + track.resizing_viewer.draw() + track.resizing_viewer = None + break + + def broadcast(self, message): + for _, track in self.tracks: + track.broadcast(message) + for handler in self.handlers: + handler(message) + + def redraw(self): + for _, track in self.tracks: + track.redraw() + + def focus(self, viewer): + if isinstance(viewer, Track): + if self.focused_viewer: self.focused_viewer.unfocus() + self.focused_viewer = None + elif viewer is not self.focused_viewer: + if self.focused_viewer: self.focused_viewer.unfocus() + self.focused_viewer = viewer + viewer.focus(self) + + def dispatch_event(self, event): + ''' + Display event handling. + ''' + try: + if event.type in {pygame.KEYUP, pygame.KEYDOWN}: + self._keyboard_event(event) + elif event.type in MOUSE_EVENTS: + self._mouse_event(event) + else: + print >> stderr, ( + 'received event %s Use pygame.event.set_allowed().' + % pygame.event.event_name(event.type) + ) + # Catch all exceptions and open a viewer. + except: + err = format_exc() + print >> stderr, err # To be safe just print it right away. + open_viewer_on_string(self, err, self.broadcast) + + def _keyboard_event(self, event): + if event.key == pygame.K_PAUSE and event.type == pygame.KEYUP: + # At least on my keyboard the break/pause key sends K_PAUSE. + # The main use of this is to open a TextViewer if you + # accidentally close all the viewers, so you can recover. + raise KeyboardInterrupt('break') + if not self.focused_viewer: + return + if event.type == pygame.KEYUP: + self.focused_viewer.key_up(self, event.key, event.mod) + elif event.type == pygame.KEYDOWN: + self.focused_viewer.key_down( + self, event.unicode, event.key, event.mod) + + def _mouse_event(self, event): + V, x, y = self.at(*event.pos) + + if event.type == pygame.MOUSEMOTION: + if not isinstance(V, Track): + V.mouse_motion(self, x, y, *(event.rel + event.buttons)) + + elif event.type == pygame.MOUSEBUTTONDOWN: + if event.button == 1: + self.focus(V) + V.mouse_down(self, x, y, event.button) + + else: + assert event.type == pygame.MOUSEBUTTONUP + + # Check for moving viewer. + if (event.button == 2 + and self.focused_viewer + and V is not self.focused_viewer + and V.MINIMUM_HEIGHT < y < V.h - self.focused_viewer.MINIMUM_HEIGHT + ): + self._move_viewer(V, y, self.focused_viewer, *event.pos) + + else: + V.mouse_up(self, x, y, event.button) + + def init_text(self, pt, x, y, filename): + viewer = self.open_viewer(x, y, text_viewer.TextViewer) + viewer.content_id, viewer.lines = pt.open(filename) + viewer.draw() + return viewer + + +class Track(Viewer): + + def __init__(self, surface): + Viewer.__init__(self, surface) + self.viewers = [] # (y, viewer) + self.hiding = None + self.resizing_viewer = None + self.draw() + + def split(self, y): + ''' + Split the Track at the y coordinate and return the height + available for a new viewer. Tracks manage a vertical strip of + the display screen so they don't resize their surface when split. + ''' + h = self.viewers[0][0] if self.viewers else self.h + assert h > y + return h - y + + def draw(self, rect=None): + '''Draw the track onto its surface, clearing all content. + + If rect is passed only draw to that area. This supports e.g. + closing a viewer that then exposes part of the track. + ''' + self.surface.fill(GREY, rect=rect) + + def viewer_at(self, y): + ''' + Return the viewer at y along with the viewer-relative y coordinate, + if there's no viewer at y return this track and y. + ''' + for viewer_y, viewer in self.viewers: + if viewer_y < y <= viewer_y + viewer.h: + return viewer, y - viewer_y + return self, y + + def open_viewer(self, y, class_): + '''Open and return a viewer of class_ at y.''' + # Todo: if y coincides with some other viewer's y replace it. + viewer, viewer_y = self.viewer_at(y) + h = viewer.split(viewer_y) + new_viewer = class_(self.surface.subsurface((0, y, self.w, h))) + new_viewer.draw() + self.viewers.append((y, new_viewer)) + self.viewers.sort() # Could use bisect module but how many + # viewers will you ever have? + return new_viewer + + def close_viewer(self, viewer): + '''Close the viewer, reuse the freed space.''' + for y, V in self.viewers: + if V is viewer: + self._close_viewer(y, V) + return True + return False + + def _close_viewer(self, y, viewer): + '''Helper function to do the actual closing.''' + i = self.viewers.index((y, viewer)) + del self.viewers[i] + if i: # The previous viewer gets the space. + previous_y, previous_viewer = self.viewers[i - 1] + self._grow_by(previous_viewer, previous_y, viewer.h) + else: # This track gets the space. + self.draw((0, y, self.w, viewer.surface.get_height())) + viewer.close() + + def _grow_by(self, viewer, y, h): + '''Grow a viewer (located at y) by height h. + + 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 parent track's surface (to make a new subsurface) so it has + to be a method of the track, which has both. + ''' + h = viewer.surface.get_height() + h + try: + surface = self.surface.subsurface((0, y, self.w, h)) + except ValueError: # subsurface rectangle outside surface area + pass + else: + viewer.resurface(surface) + if h <= viewer.last_touch[1]: viewer.last_touch = 0, 0 + viewer.draw() + + def change_viewer(self, viewer, new_y, relative=False): + ''' + Adjust the top of the viewer to a new y within the boundaries of + its neighbors. + + If relative is False new_y should be in screen coords, else new_y + should be relative to the top of the viewer. + ''' + for old_y, V in self.viewers: + if V is viewer: + if relative: new_y += old_y + if new_y != old_y: self._change_viewer(new_y, old_y, V) + return True + return False + + def _change_viewer(self, new_y, old_y, viewer): + new_y = max(0, min(self.h, new_y)) + i = self.viewers.index((old_y, viewer)) + if new_y < old_y: # Enlarge self, shrink upper neighbor. + if i: + previous_y, previous_viewer = self.viewers[i - 1] + if new_y - previous_y < self.MINIMUM_HEIGHT: + return + previous_viewer.resizing = 1 + h = previous_viewer.split(new_y - previous_y) + previous_viewer.resizing = 0 + self.resizing_viewer = previous_viewer + else: + h = old_y - new_y + self._grow_by(viewer, new_y, h) + + else: # Shink self, enlarge upper neighbor. + # Enforce invariant. + try: + h, _ = self.viewers[i + 1] + except IndexError: # No next viewer. + h = self.h + if h - new_y < self.MINIMUM_HEIGHT: + return + + # Change the viewer and adjust the upper viewer or track. + h = new_y - old_y + self._grow_by(viewer, new_y, -h) # grow by negative height! + if i: + previous_y, previous_viewer = self.viewers[i - 1] + previous_viewer.resizing = 1 + self._grow_by(previous_viewer, previous_y, h) + previous_viewer.resizing = 0 + self.resizing_viewer = previous_viewer + else: + self.draw((0, old_y, self.w, h)) + + self.viewers[i] = new_y, viewer + # self.viewers.sort() # Not necessary, invariant holds. + assert sorted(self.viewers) == self.viewers + + def broadcast(self, message): + for _, viewer in self.viewers: + if viewer is not message.sender: + viewer.handle(message) + + def redraw(self): + '''Redraw the track and all of its viewers.''' + self.draw() + for _, viewer in self.viewers: + viewer.draw() diff --git a/joy/vui/font_data.py b/joy/vui/font_data.py new file mode 100644 index 0000000..bfc7763 --- /dev/null +++ b/joy/vui/font_data.py @@ -0,0 +1,167 @@ +from StringIO import StringIO +import base64, zlib + + +def create(fn='Iosevka12.BMP'): + with open(fn, 'rb') as f: + data = f.read() + return base64.encodestring(zlib.compress(data)) + + +data = StringIO(zlib.decompress(base64.decodestring('''\ +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 +lnalXc/9SsNb2vUirzS8pV0v8gJv/w/2vRht'''))) + + +if __name__ == '__main__': + print create() diff --git a/joy/vui/init_joy_home.py b/joy/vui/init_joy_home.py new file mode 100644 index 0000000..b721bf1 --- /dev/null +++ b/joy/vui/init_joy_home.py @@ -0,0 +1,256 @@ +''' +Utility module to help with setting up the initial contents of the +JOY_HOME directory. + +These contents are kept in this Python module as a base64-encoded zip +file, so you can just do, e.g.: + + import init_joy_home + init_joy_home.initialize(JOY_HOME) + +''' +import base64, os, StringIO, zipfile + + +def initialize(joy_home): + Z.extractall(joy_home) + + +def create_data(from_dir='./default_joy_home'): + f = StringIO.StringIO() + z = zipfile.ZipFile(f, mode='w') + for fn in os.listdir(from_dir): + from_fn = os.path.join(from_dir, fn) + z.write(from_fn, fn) + z.close() + return base64.encodestring(f.getvalue()) + + +Z = zipfile.ZipFile(StringIO.StringIO(base64.decodestring('''\ +UEsDBBQAAAAAAORmeE794BlRfgMAAH4DAAAPAAAAZGVmaW5pdGlvbnMudHh0c2VlX3N0YWNrID09 +IGdvb2Rfdmlld2VyX2xvY2F0aW9uIG9wZW5fc3RhY2sNCnNlZV9yZXNvdXJjZXMgPT0gbGlzdF9y +ZXNvdXJjZXMgZ29vZF92aWV3ZXJfbG9jYXRpb24gb3Blbl92aWV3ZXINCm9wZW5fcmVzb3VyY2Vf +YXRfZ29vZF9sb2NhdGlvbiA9PSBnb29kX3ZpZXdlcl9sb2NhdGlvbiBvcGVuX3Jlc291cmNlDQpz +ZWVfbG9nID09ICJsb2cudHh0IiBvcGVuX3Jlc291cmNlX2F0X2dvb2RfbG9jYXRpb24NCnNlZV9k +ZWZpbml0aW9ucyA9PSAiZGVmaW5pdGlvbnMudHh0IiBvcGVuX3Jlc291cmNlX2F0X2dvb2RfbG9j +YXRpb24NCnJvdW5kX3RvX2NlbnRzID09IDEwMCAqICsrIGZsb29yIDEwMCAvDQpyZXNldF9sb2cg +PT0gImRlbCBsb2cubGluZXNbMTpdIDsgbG9nLmF0X2xpbmUgPSAwIiBldmFsdWF0ZQ0Kc2VlX21l +bnUgPT0gIm1lbnUudHh0IiBnb29kX3ZpZXdlcl9sb2NhdGlvbiBvcGVuX3Jlc291cmNlDQoNCiMg +T3JkZXJlZCBCaW5hcnkgVHJlZSBkYXRhc3RydWN0dXJlIGZ1bmN0aW9ucy4NCkJUcmVlLW5ldyA9 +PSBzd2FwIFtbXSBbXV0gY29ucyBjb25zDQogX0JUcmVlLVAgPT0gb3ZlciBbcG9wb3AgcG9wb3Ag +Zmlyc3RdIG51bGxhcnkNCiBfQlRyZWUtVD4gPT0gW2NvbnMgY29ucyBkaXBkZF0gY29ucyBjb25z +IGNvbnMgaW5mcmENCiBfQlRyZWUtVDwgPT0gW2NvbnMgY29ucyBkaXBkXSBjb25zIGNvbnMgY29u +cyBpbmZyYQ0KIF9CVHJlZS1FID09IHBvcCBzd2FwIHJvbGw8IHJlc3QgcmVzdCBjb25zIGNvbnMN +CiBfQlRyZWUtcmVjdXIgPT0gX0JUcmVlLVAgW19CVHJlZS1UPl0gW19CVHJlZS1FXSBbX0JUcmVl +LVQ8XSBjbXANCkJUcmVlLWFkZCA9PSBbcG9wb3Agbm90XSBbW3BvcF0gZGlwZCBCVHJlZS1uZXdd +IFtdIFtfQlRyZWUtcmVjdXJdIGdlbnJlYw0KUEsDBBQAAAAAAAVjpk4zTskZFBYAABQWAAAKAAAA +bGlicmFyeS5weScnJw0KVGhpcyBmaWxlIGlzIGV4ZWNmaWxlKCknZCB3aXRoIGEgbmFtZXNwYWNl +IGNvbnRhaW5pbmc6DQoNCiAgRCAtIHRoZSBKb3kgZGljdGlvbmFyeQ0KICBkIC0gdGhlIERpc3Bs +YXkgb2JqZWN0DQogIHB0IC0gdGhlIFBlcnNpc3RUYXNrIG9iamVjdA0KICBsb2cgLSB0aGUgbG9n +LnR4dCB2aWV3ZXINCiAgbG9vcCAtIHRoZSBUaGVMb29wIG1haW4gbG9vcCBvYmplY3QNCiAgc3Rh +Y2tfaG9sZGVyIC0gdGhlIFB5dGhvbiBsaXN0IG9iamVjdCB0aGF0IGhvbGRzIHRoZSBKb3kgc3Rh +Y2sgdHVwbGUNCiAgd29ybGQgLSB0aGUgSm95IGVudmlyb25tZW50DQoNCicnJw0KZnJvbSBqb3ku +bGlicmFyeSBpbXBvcnQgKA0KICAgIERlZmluaXRpb25XcmFwcGVyLA0KICAgIEZ1bmN0aW9uV3Jh +cHBlciwNCiAgICBTaW1wbGVGdW5jdGlvbldyYXBwZXIsDQogICAgKQ0KZnJvbSBqb3kudXRpbHMu +c3RhY2sgaW1wb3J0IGxpc3RfdG9fc3RhY2ssIGNvbmNhdA0KZnJvbSB2dWkgaW1wb3J0IGNvcmUs +IHRleHRfdmlld2VyLCBzdGFja192aWV3ZXINCg0KDQpkZWYgaW5zdGFsbChjb21tYW5kKTogRFtj +b21tYW5kLm5hbWVdID0gY29tbWFuZA0KDQoNCkBpbnN0YWxsDQpAU2ltcGxlRnVuY3Rpb25XcmFw +cGVyDQpkZWYgbGlzdF9yZXNvdXJjZXMoc3RhY2spOg0KICAgICcnJw0KICAgIFB1dCBhIHN0cmlu +ZyBvbiB0aGUgc3RhY2sgd2l0aCB0aGUgbmFtZXMgb2YgYWxsIHRoZSBrbm93biByZXNvdXJjZXMN +CiAgICBvbmUtcGVyLWxpbmUuDQogICAgJycnDQogICAgcmV0dXJuICdcbicuam9pbihwdC5zY2Fu +KCkpLCBzdGFjaw0KDQoNCkBpbnN0YWxsDQpAU2ltcGxlRnVuY3Rpb25XcmFwcGVyDQpkZWYgb3Bl +bl9zdGFjayhzdGFjayk6DQogICAgJycnDQogICAgR2l2ZW4gYSBjb29yZGluYXRlIHBhaXIgW3gg +eV0gKGluIHBpeGVscykgb3BlbiBhIFN0YWNrVmlld2VyIHRoZXJlLg0KICAgICcnJw0KICAgICh4 +LCAoeSwgXykpLCBzdGFjayA9IHN0YWNrDQogICAgViA9IGQub3Blbl92aWV3ZXIoeCwgeSwgc3Rh +Y2tfdmlld2VyLlN0YWNrVmlld2VyKQ0KICAgIFYuZHJhdygpDQogICAgcmV0dXJuIHN0YWNrDQoN +Cg0KQGluc3RhbGwNCkBTaW1wbGVGdW5jdGlvbldyYXBwZXINCmRlZiBvcGVuX3Jlc291cmNlKHN0 +YWNrKToNCiAgICAnJycNCiAgICBHaXZlbiBhIGNvb3JkaW5hdGUgcGFpciBbeCB5XSAoaW4gcGl4 +ZWxzKSBhbmQgdGhlIG5hbWUgb2YgYSByZXNvdXJjZQ0KICAgIChmcm9tIGxpc3RfcmVzb3VyY2Vz +IGNvbW1hbmQpIG9wZW4gYSB2aWV3ZXIgb24gdGhhdCByZXNvdXJjZSBhdCB0aGF0DQogICAgbG9j +YXRpb24uDQogICAgJycnDQogICAgKCh4LCAoeSwgXykpLCAobmFtZSwgc3RhY2spKSA9IHN0YWNr +DQogICAgb20gPSBjb3JlLk9wZW5NZXNzYWdlKHdvcmxkLCBuYW1lKQ0KICAgIGQuYnJvYWRjYXN0 +KG9tKQ0KICAgIGlmIG9tLnN0YXR1cyA9PSBjb3JlLlNVQ0NFU1M6DQogICAgICAgIFYgPSBkLm9w +ZW5fdmlld2VyKHgsIHksIHRleHRfdmlld2VyLlRleHRWaWV3ZXIpDQogICAgICAgIFYuY29udGVu +dF9pZCwgVi5saW5lcyA9IG9tLmNvbnRlbnRfaWQsIG9tLnRoaW5nDQogICAgICAgIFYuZHJhdygp +DQogICAgcmV0dXJuIHN0YWNrDQoNCg0KQGluc3RhbGwNCkBTaW1wbGVGdW5jdGlvbldyYXBwZXIN +CmRlZiBuYW1lX3ZpZXdlcihzdGFjayk6DQogICAgJycnDQogICAgR2l2ZW4gYSBzdHJpbmcgbmFt +ZSBvbiB0aGUgc3RhY2ssIGlmIHRoZSBjdXJyZW50bHkgZm9jdXNlZCB2aWV3ZXIgaXMNCiAgICBh +bm9ueW1vdXMsIG5hbWUgdGhlIHZpZXdlciBhbmQgcGVyc2lzdCBpdCBpbiB0aGUgcmVzb3VyY2Ug +c3RvcmUgdW5kZXINCiAgICB0aGF0IG5hbWUuDQogICAgJycnDQogICAgbmFtZSwgc3RhY2sgPSBz +dGFjaw0KICAgIGFzc2VydCBpc2luc3RhbmNlKG5hbWUsIHN0ciksIHJlcHIobmFtZSkNCiAgICBp +ZiBkLmZvY3VzZWRfdmlld2VyIGFuZCBub3QgZC5mb2N1c2VkX3ZpZXdlci5jb250ZW50X2lkOg0K +ICAgICAgICBkLmZvY3VzZWRfdmlld2VyLmNvbnRlbnRfaWQgPSBuYW1lDQogICAgICAgIHBtID0g +Y29yZS5QZXJzaXN0TWVzc2FnZSh3b3JsZCwgbmFtZSwgdGhpbmc9ZC5mb2N1c2VkX3ZpZXdlci5s +aW5lcykNCiAgICAgICAgZC5icm9hZGNhc3QocG0pDQogICAgICAgIGQuZm9jdXNlZF92aWV3ZXIu +ZHJhd19tZW51KCkNCiAgICByZXR1cm4gc3RhY2sNCg0KDQojI0BpbnN0YWxsDQojI0BTaW1wbGVG +dW5jdGlvbldyYXBwZXINCiMjZGVmIHBlcnNpc3Rfdmlld2VyKHN0YWNrKToNCiMjICAgIGlmIHNl +bGYuZm9jdXNlZF92aWV3ZXI6DQojIyAgICAgICAgDQojIyAgICAgICAgc2VsZi5mb2N1c2VkX3Zp +ZXdlci5jb250ZW50X2lkID0gbmFtZQ0KIyMgICAgICAgIHNlbGYuZm9jdXNlZF92aWV3ZXIuZHJh +d19tZW51KCkNCiMjICAgIHJldHVybiBzdGFjaw0KDQoNCkBpbnN0YWxsDQpAU2ltcGxlRnVuY3Rp +b25XcmFwcGVyDQpkZWYgaW5zY3JpYmUoc3RhY2spOg0KICAgICcnJw0KICAgIENyZWF0ZSBhIG5l +dyBKb3kgZnVuY3Rpb24gZGVmaW5pdGlvbiBpbiB0aGUgSm95IGRpY3Rpb25hcnkuICBBDQogICAg +ZGVmaW5pdGlvbiBpcyBnaXZlbiBhcyBhIHN0cmluZyB3aXRoIGEgbmFtZSBmb2xsb3dlZCBieSBh +IGRvdWJsZQ0KICAgIGVxdWFsIHNpZ24gdGhlbiBvbmUgb3IgbW9yZSBKb3kgZnVuY3Rpb25zLCB0 +aGUgYm9keS4gZm9yIGV4YW1wbGU6DQoNCiAgICAgICAgc3FyID09IGR1cCBtdWwNCg0KICAgIElm +IHlvdSB3YW50IHRoZSBkZWZpbml0aW9uIHRvIHBlcnNpc3Qgb3ZlciByZXN0YXJ0cywgZW50ZXIg +aXQgaW50bw0KICAgIHRoZSBkZWZpbml0aW9ucy50eHQgcmVzb3VyY2UuDQogICAgJycnDQogICAg +ZGVmaW5pdGlvbiwgc3RhY2sgPSBzdGFjaw0KICAgIERlZmluaXRpb25XcmFwcGVyLmFkZF9kZWYo +ZGVmaW5pdGlvbiwgRCkNCiAgICByZXR1cm4gc3RhY2sNCg0KDQpAaW5zdGFsbA0KQFNpbXBsZUZ1 +bmN0aW9uV3JhcHBlcg0KZGVmIG9wZW5fdmlld2VyKHN0YWNrKToNCiAgICAnJycNCiAgICBHaXZl +biBhIGNvb3JkaW5hdGUgcGFpciBbeCB5XSAoaW4gcGl4ZWxzKSBhbmQgYSBzdHJpbmcsIG9wZW4g +YSBuZXcNCiAgICB1bm5hbWVkIHZpZXdlciBvbiB0aGF0IHN0cmluZyBhdCB0aGF0IGxvY2F0aW9u +Lg0KICAgICcnJw0KICAgICgoeCwgKHksIF8pKSwgKGNvbnRlbnQsIHN0YWNrKSkgPSBzdGFjaw0K +ICAgIFYgPSBkLm9wZW5fdmlld2VyKHgsIHksIHRleHRfdmlld2VyLlRleHRWaWV3ZXIpDQogICAg +Vi5saW5lcyA9IGNvbnRlbnQuc3BsaXRsaW5lcygpDQogICAgVi5kcmF3KCkNCiAgICByZXR1cm4g +c3RhY2sNCg0KDQpAaW5zdGFsbA0KQFNpbXBsZUZ1bmN0aW9uV3JhcHBlcg0KZGVmIGdvb2Rfdmll +d2VyX2xvY2F0aW9uKHN0YWNrKToNCiAgICAnJycNCiAgICBMZWF2ZSBhIGNvb3JkaW5hdGUgcGFp +ciBbeCB5XSAoaW4gcGl4ZWxzKSBvbiB0aGUgc3RhY2sgdGhhdCB3b3VsZA0KICAgIGJlIGEgZ29v +ZCBsb2NhdGlvbiBhdCB3aGljaCB0byBvcGVuIGEgbmV3IHZpZXdlci4gIChUaGUgaGV1cmlzdGlj +DQogICAgZW1wbG95ZWQgaXMgdG8gdGFrZSB1cCB0aGUgYm90dG9tIGhhbGYgb2YgdGhlIGN1cnJl +bnRseSBvcGVuIHZpZXdlcg0KICAgIHdpdGggdGhlIGdyZWF0ZXN0IGFyZWEuKQ0KICAgICcnJw0K +ICAgIHZpZXdlcnMgPSBsaXN0KGQuaXRlcl92aWV3ZXJzKCkpDQogICAgaWYgdmlld2VyczoNCiAg +ICAgICAgdmlld2Vycy5zb3J0KGtleT1sYW1iZGEgKFYsIHgsIHkpOiBWLncgKiBWLmgpDQogICAg +ICAgIFYsIHgsIHkgPSB2aWV3ZXJzWy0xXQ0KICAgICAgICBjb29yZHMgPSAoeCArIDEsICh5ICsg +Vi5oIC8gMiwgKCkpKQ0KICAgIGVsc2U6DQogICAgICAgIGNvb3JkcyA9ICgwLCAoMCwgKCkpKQ0K +ICAgIHJldHVybiBjb29yZHMsIHN0YWNrDQoNCg0KQGluc3RhbGwNCkBGdW5jdGlvbldyYXBwZXIN +CmRlZiBjbXBfKHN0YWNrLCBleHByZXNzaW9uLCBkaWN0aW9uYXJ5KToNCiAgICAnJycNCiAgICBU +aGUgY21wIGNvbWJpbmF0b3IgdGFrZXMgdHdvIHZhbHVlcyBhbmQgdGhyZWUgcXVvdGVkIHByb2dy +YW1zIG9uIHRoZQ0KICAgIHN0YWNrIGFuZCBydW5zIG9uZSBvZiB0aGUgdGhyZWUgZGVwZW5kaW5n +IG9uIHRoZSByZXN1bHRzIG9mIGNvbXBhcmluZw0KICAgIHRoZSB0d28gdmFsdWVzOg0KDQogICAg +ICAgICAgIGEgYiBbR10gW0VdIFtMXSBjbXANCiAgICAgICAgLS0tLS0tLS0tLS0tLS0tLS0tLS0t +LS0tLSBhID4gYg0KICAgICAgICAgICAgICAgIEcNCg0KICAgICAgICAgICBhIGIgW0ddIFtFXSBb +TF0gY21wDQogICAgICAgIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0gYSA9IGINCiAgICAgICAg +ICAgICAgICAgICAgRQ0KDQogICAgICAgICAgIGEgYiBbR10gW0VdIFtMXSBjbXANCiAgICAgICAg +LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSBhIDwgYg0KICAgICAgICAgICAgICAgICAgICAgICAg +TA0KDQogICAgJycnDQogICAgTCwgKEUsIChHLCAoYiwgKGEsIHN0YWNrKSkpKSA9IHN0YWNrDQog +ICAgZXhwcmVzc2lvbiA9IGNvbmNhdChHIGlmIGEgPiBiIGVsc2UgTCBpZiBhIDwgYiBlbHNlIEUs +IGV4cHJlc3Npb24pDQogICAgcmV0dXJuIHN0YWNrLCBleHByZXNzaW9uLCBkaWN0aW9uYXJ5DQoN +Cg0KQGluc3RhbGwNCkBTaW1wbGVGdW5jdGlvbldyYXBwZXINCmRlZiBsaXN0X3ZpZXdlcnMoc3Rh +Y2spOg0KICAgICcnJw0KICAgIFB1dCBhIHN0cmluZyBvbiB0aGUgc3RhY2sgd2l0aCBzb21lIGlu +Zm9ybWF0aW9uIGFib3V0IHRoZSBjdXJyZW50bHkNCiAgICBvcGVuIHZpZXdlcnMsIG9uZS1wZXIt +bGluZS4gIFRoaXMgaXMga2luZCBvZiBhIGRlbW8gZnVuY3Rpb24sIHJhdGhlcg0KICAgIHRoYW4g +c29tZXRoaW5nIHJlYWxseSB1c2VmdWwuDQogICAgJycnDQogICAgbGluZXMgPSBbXQ0KICAgIGZv +ciB4LCBUIGluIGQudHJhY2tzOg0KICAgICAgICAjbGluZXMuYXBwZW5kKCd4OiAlaSwgdzogJWks +ICVyJyAlICh4LCBULncsIFQpKQ0KICAgICAgICBmb3IgeSwgViBpbiBULnZpZXdlcnM6DQogICAg +ICAgICAgICBsaW5lcy5hcHBlbmQoJ3g6ICVpIHk6ICVpIGg6ICVpICVyICVyJyAlICh4LCB5LCBW +LmgsIFYuY29udGVudF9pZCwgVikpDQogICAgcmV0dXJuICdcbicuam9pbihsaW5lcyksIHN0YWNr +DQoNCg0KQGluc3RhbGwNCkBTaW1wbGVGdW5jdGlvbldyYXBwZXINCmRlZiBzcGxpdGxpbmVzKHN0 +YWNrKToNCiAgICAnJycNCiAgICBHaXZlbiBhIHN0cmluZyBvbiB0aGUgc3RhY2sgcmVwbGFjZSBp +dCB3aXRoIGEgbGlzdCBvZiB0aGUgbGluZXMgaW4NCiAgICB0aGUgc3RyaW5nLg0KICAgICcnJw0K +ICAgIHRleHQsIHN0YWNrID0gc3RhY2sNCiAgICBhc3NlcnQgaXNpbnN0YW5jZSh0ZXh0LCBzdHIp +LCByZXByKHRleHQpDQogICAgcmV0dXJuIGxpc3RfdG9fc3RhY2sodGV4dC5zcGxpdGxpbmVzKCkp +LCBzdGFjaw0KDQoNCkBpbnN0YWxsDQpAU2ltcGxlRnVuY3Rpb25XcmFwcGVyDQpkZWYgaGl5YShz +dGFjayk6DQogICAgJycnDQogICAgRGVtbyBmdW5jdGlvbiB0byBpbnNlcnQgIkhpIFdvcmxkISIg +aW50byB0aGUgY3VycmVudCB2aWV3ZXIsIGlmIGFueS4NCiAgICAnJycNCiAgICBpZiBkLmZvY3Vz +ZWRfdmlld2VyOg0KICAgICAgICBkLmZvY3VzZWRfdmlld2VyLmluc2VydCgnSGkgV29ybGQhJykN +CiAgICByZXR1cm4gc3RhY2sNClBLAwQUAAAAAADkZnhORezk1gsAAAALAAAABwAAAGxvZy50eHRK +b3lweSBsb2cNClBLAwQUAAAAAADkZnhOZ/a7zQIFAAACBQAACAAAAG1lbnUudHh0ICBuYW1lX3Zp +ZXdlcg0KICBsaXN0X3Jlc291cmNlcw0KICBvcGVuX3Jlc291cmNlX2F0X2dvb2RfbG9jYXRpb24N +CiAgZ29vZF92aWV3ZXJfbG9jYXRpb24NCiAgb3Blbl92aWV3ZXINCiAgc2VlX3N0YWNrDQogIHNl +ZV9yZXNvdXJjZXMNCiAgc2VlX2RlZmluaXRpb25zDQogIHNlZV9sb2cNCiAgcmVzZXRfbG9nDQoN +CiAgaW5zY3JpYmUNCiAgZXZhbHVhdGUNCg0KICBwb3AgY2xlYXIgICAgZHVwIHN3YXANCg0KICBh +ZGQgc3ViIG11bCBkaXYgdHJ1ZWRpdiBtb2R1bHVzIGRpdm1vZA0KICBwbSArKyAtLSBzdW0gcHJv +ZHVjdCBwb3cgc3FyIHNxcnQNCiAgPCA8PSA9ID49ID4gPD4NCiAgJiA8PCA+Pg0KDQogIGkgZHVw +ZGlwDQoNCiE9ICUgJiAqICpmcmFjdGlvbiAqZnJhY3Rpb24wICsgKysgLSAtLSAvIDwgPDwgPD0g +PD4gPSA+ID49ID4+ID8gXg0KYWJzIGFkZCBhbmFtb3JwaGlzbSBhbmQgYXBwMSBhcHAyIGFwcDMg +YXQgYXZlcmFnZQ0KYiBiaW5hcnkgYnJhbmNoDQpjaG9pY2UgY2xlYXIgY2xlYXZlIGNvbmNhdCBj +b25zDQpkaW5mcmlyc3QgZGlwIGRpcGQgZGlwZGQgZGlzZW5zdGFja2VuIGRpdiBkaXZtb2QgZG93 +bl90b196ZXJvIGRyb3ANCmR1ZGlwZCBkdXAgZHVwZCBkdXBkaXANCmVuc3RhY2tlbiBlcQ0KZmly +c3QgZmxhdHRlbiBmbG9vciBmbG9vcmRpdg0KZ2NkIGdlIGdlbnJlYyBnZXRpdGVtIGdyYW5kX3Jl +c2V0IGd0DQpoZWxwDQppIGlkIGlmdGUgaW5mcmEgaW5zY3JpYmUNCmtleV9iaW5kaW5ncw0KbGUg +bGVhc3RfZnJhY3Rpb24gbG9vcCBsc2hpZnQgbHQNCm1hcCBtYXggbWluIG1vZCBtb2R1bHVzIG1v +dXNlX2JpbmRpbmdzIG11bA0KbmUgbmVnIG5vdCBudWxsYXJ5DQpvZiBvciBvdmVyDQpwYW0gcGFy +c2UgcGljayBwbSBwb3AgcG9wZCBwb3BkZCBwb3BvcCBwb3cgcHJlZCBwcmltcmVjIHByb2R1Y3QN +CnF1b3RlZA0KcmFuZ2UgcmFuZ2VfdG9femVybyByZW0gcmVtYWluZGVyIHJlbW92ZSByZXNldF9s +b2cgcmVzdCByZXZlcnNlDQpyb2xsPCByb2xsPiByb2xsZG93biByb2xsdXAgcnNoaWZ0IHJ1bg0K +c2Vjb25kIHNlbGVjdCBzaGFyaW5nIHNob3dfbG9nIHNodW50IHNpemUgc29ydCBzcXIgc3FydCBz +dGFjayBzdGVwDQpzdGVwX3plcm8gc3ViIHN1Y2Mgc3VtIHN3YWFjayBzd2FwIHN3b25jYXQgc3dv +bnMNCnRha2UgdGVybmFyeSB0aGlyZCB0aW1lcyB0cnVlZGl2IHRydXRoeSB0dWNrDQp1bmFyeSB1 +bmNvbnMgdW5pcXVlIHVuaXQgdW5xdW90ZWQgdW5zdGFjaw0Kdm9pZA0Kd2FycmFudHkgd2hpbGUg +d29yZHMNCnggeG9yDQp6aXANClBLAwQUAAAAAADkZnhOAKs9xo4QAACOEAAACwAAAHNjcmF0Y2gu +dHh0V2hhdCBpcyBpdD8NCg0KQSBzaW1wbGUgR3JhcGhpY2FsIFVzZXIgSW50ZXJmYWNlIGZvciB0 +aGUgSm95IHByb2dyYW1taW5nIGxhbmd1YWdlLA0Kd3JpdHRlbiB1c2luZyBQeWdhbWUgdG8gYnlw +YXNzIFgxMSBldC4gYWwuLCBtb2RlbGVkIG9uIHRoZSBPYmVyb24gT1MsIGFuZA0KaW50ZW5kZWQg +dG8gYmUganVzdCBmdW5jdGlvbmFsIGVub3VnaCB0byBzdXBwb3J0IGJvb3RzdHJhcHBpbmcgZnVy +dGhlciBKb3kNCmRldmVsb3BtZW50Lg0KDQpJdCdzIGJhc2ljIGZ1bmN0aW9uYWxpdHkgaXMgbW9y +ZS1vci1sZXNzIGFzIGEgY3J1ZGUgdGV4dCBlZGl0b3IgYWxvbmcgd2l0aA0KYSBzaW1wbGUgSm95 +IHJ1bnRpbWUgKGludGVycHJldGVyLCBzdGFjaywgYW5kIGRpY3Rpb25hcnkuKSAgSXQgYXV0by0g +c2F2ZXMNCmFueSBuYW1lZCBmaWxlcyAoaW4gYSB2ZXJzaW9uZWQgaG9tZSBkaXJlY3RvcnkpIGFu +ZCB5b3UgY2FuIHdyaXRlIG5ldyBKb3kNCnByaW1pdGl2ZXMgaW4gUHl0aG9uIGFuZCBKb3kgZGVm +aW5pdGlvbnMgYW5kIGltbWVkaWF0ZWx5IGluc3RhbGwgYW5kIHVzZQ0KdGhlbSwgYXMgd2VsbCBh +cyByZWNvcmRpbmcgdGhlbSBmb3IgcmV1c2UgKGFmdGVyIHJlc3RhcnRzLikNCg0KQ3VycmVudGx5 +LCB0aGVyZSBhcmUgb25seSB0d28ga2luZHMgb2YgKGludGVyZXN0aW5nKSB2aWV3ZXJzOiBUZXh0 +Vmlld2Vycw0KYW5kIFN0YWNrVmlld2VyLiBUaGUgVGV4dFZpZXdlcnMgYXJlIGNydWRlIHRleHQg +ZWRpdG9ycy4gIFRoZXkgcHJvdmlkZQ0KanVzdCBlbm91Z2ggZnVuY3Rpb25hbGl0eSB0byBsZXQg +dGhlIHVzZXIgd3JpdGUgdGV4dCBhbmQgY29kZSAoUHl0aG9uIGFuZA0KSm95KSBhbmQgZXhlY3V0 +ZSBKb3kgZnVuY3Rpb25zLiAgT25lIGltcG9ydGFudCB0aGluZyB0aGV5IGRvIGlzDQphdXRvbWF0 +aWNhbGx5IHNhdmUgdGhlaXIgY29udGVudCBhZnRlciBjaGFuZ2VzLiAgTm8gbW9yZSBsb3N0IHdv +cmsuDQoNClRoZSBTdGFja1ZpZXdlciBpcyBhIHNwZWNpYWxpemVkIFRleHRWaWV3ZXIgdGhhdCBz +aG93cyB0aGUgY29udGVudHMgb2YgdGhlDQpKb3kgc3RhY2sgb25lIGxpbmUgcGVyIHN0YWNrIGl0 +ZW0uICBJdCdzIGEgdmVyeSBoYW5keSB2aXN1YWwgYWlkIHRvIGtlZXANCnRyYWNrIG9mIHdoYXQn +cyBnb2luZyBvbi4gIFRoZXJlJ3MgYWxzbyBhIGxvZy50eHQgZmlsZSB0aGF0IGdldHMgd3JpdHRl +bg0KdG8gd2hlbiBjb21tYW5kcyBhcmUgZXhlY3V0ZWQsIGFuZCBzbyByZWNvcmRzIHRoZSBsb2cg +b2YgdXNlciBhY3Rpb25zIGFuZA0Kc3lzdGVtIGV2ZW50cy4gIEl0IHRlbmRzIHRvIGZpbGwgdXAg +cXVpY2tseSBzbyB0aGVyZSdzIGEgcmVzZXRfbG9nIGNvbW1hbmQNCnRoYXQgY2xlYXJzIGl0IG91 +dC4NCg0KVmlld2VycyBoYXZlICJncm93IiBhbmQgImNsb3NlIiBpbiB0aGVpciBtZW51IGJhcnMu +ICBUaGVzZSBhcmUgYnV0dG9ucy4NCldoZW4geW91IHJpZ2h0LWNsaWNrIG9uIGdyb3cgYSB2aWV3 +ZXIgYSBjb3B5IGlzIGNyZWF0ZWQgdGhhdCBjb3ZlcnMgdGhhdA0Kdmlld2VyJ3MgZW50aXJlIHRy +YWNrLiAgSWYgeW91IGdyb3cgYSB2aWV3ZXIgdGhhdCBhbHJlYWR5IHRha2VzIHVwIGl0cw0Kd2hv +bGUgdHJhY2sgdGhlbiBhIGNvcHkgaXMgY3JlYXRlZCB0aGF0IHRha2VzIHVwIGFuIGFkZGl0aW9u +YWwgdHJhY2ssIHVwDQp0byB0aGUgd2hvbGUgc2NyZWVuLiAgQ2xvc2luZyBhIHZpZXdlciBqdXN0 +IGRlbGV0ZXMgdGhhdCB2aWV3ZXIsIGFuZCB3aGVuDQphIHRyYWNrIGhhcyBubyBtb3JlIHZpZXdl +cnMsIGl0IGlzIGRlbGV0ZWQgYW5kIHRoYXQgZXhwb3NlcyBhbnkgcHJldmlvdXMNCnRyYWNrcyBh +bmQgdmlld2VycyB0aGF0IHdlcmUgaGlkZGVuLg0KDQooTm90ZTogaWYgeW91IGV2ZXIgY2xvc2Ug +YWxsIHRoZSB2aWV3ZXJzIGFuZCBhcmUgc2l0dGluZyBhdCBhIGJsYW5rIHNjcmVlbg0Kd2l0aCAg +bm93aGVyZSB0byB0eXBlIGFuZCBleGVjdXRlIGNvbW1hbmRzLCBwcmVzcyB0aGUgUGF1c2UvQnJl +YWsga2V5Lg0KVGhpcyB3aWxsIG9wZW4gYSBuZXcgInRyYXAiIHZpZXdlciB3aGljaCB5b3UgY2Fu +IHRoZW4gdXNlIHRvIHJlY292ZXIuKQ0KDQpDb3BpZXMgb2YgYSB2aWV3ZXIgYWxsIHNoYXJlIHRo +ZSBzYW1lIG1vZGVsIGFuZCB1cGRhdGUgdGhlaXIgZGlzcGxheSBhcyBpdA0KY2hhbmdlcy4gKElm +IHlvdSBoYXZlIHR3byB2aWV3ZXJzIG9wZW4gb24gdGhlIHNhbWUgbmFtZWQgcmVzb3VyY2UgYW5k +IGVkaXQNCm9uZSB5b3UnbGwgc2VlIHRoZSBvdGhlciB1cGRhdGUgYXMgeW91IHR5cGUuKQ0KDQpV +SSBHdWlkZQ0KDQpsZWZ0IG1vdXNlIHNldHMgY3Vyc29yIGluIHRleHQsIGluIG1lbnUgYmFyIHJl +c2l6ZXMgdmlld2VyIGludGVyYWN0aXZlbHkNCih0aGlzIGlzIGEgbGl0dGxlIGJ1Z2d5IGluIHRo +YXQgeW91IGNhbiBtb3ZlIHRoZSBtb3VzZSBxdWlja2x5IGFuZCBnZXQNCm91dHNpZGUgdGhlIG1l +bnUsIGxlYXZpbmcgdGhlIHZpZXdlciBpbiB0aGUgInJlc2l6aW5nIiBzdGF0ZS4gVW50aWwgSSBm +aXgNCnRoaXMsIHRoZSB3b3JrYXJvdW5kIGlzIHRvIGp1c3QgZ3JhYiB0aGUgbWVudSBiYXIgYWdh +aW4gYW5kIHdpZ2dsZSBpdCBhDQpmZXcgcGl4ZWxzIGFuZCBsZXQgZ28uICBUaGlzIHdpbGwgcmVz +ZXQgdGhlIG1hY2hpbmVyeS4pDQoNClJpZ2h0IG1vdXNlIGV4ZWN1dGVzIEpveSBjb21tYW5kIChm +dW5jdGlvbnMpLCBhbmQgeW91IGNhbiBkcmFnIHdpdGggdGhlDQpyaWdodCBidXR0b24gdG8gaGln +aGxpZ2h0ICh3ZWxsLCB1bmRlcmxpbmUpIGNvbW1hbmRzLiAgV29yZHMgdGhhdCBhcmVuJ3QNCm5h +bWVzIG9mIEpveSBjb21tYW5kcyB3b24ndCBiZSB1bmRlcmxpbmVkLiAgUmVsZWFzZSB0aGUgYnV0 +dG9uIHRvIGV4ZWN1dGUNCnRoZSBjb21tYW5kLg0KDQpUaGUgbWlkZGxlIG1vdXNlIGJ1dHRvbiAo +dXN1YWxseSBhIHdoZWVsIHRoZXNlIGRheXMpIHNjcm9sbHMgdGhlIHRleHQgYnV0DQp5b3UgY2Fu +IGFsc28gY2xpY2sgYW5kIGRyYWcgYW55IHZpZXdlciB3aXRoIGl0IHRvIG1vdmUgdGhhdCB2aWV3 +ZXIgdG8NCmFub3RoZXIgdHJhY2sgb3IgdG8gYSBkaWZmZXJlbnQgbG9jYXRpb24gaW4gdGhlIHNh +bWUgdHJhY2suICBUaGVyZSdzIG5vDQpkaXJlY3QgdmlzdWFsIGZlZWRiYWNrIGZvciB0aGlzICh5 +ZXQpIGJ1dCB0aGF0IGRvc2VuJ3Qgc2VlbSB0byBpbXBhaXIgaXRzDQp1c2VmdWxuZXNzLg0KDQpG +MSwgRjIgLSBzZXQgc2VsZWN0aW9uIGJlZ2luIGFuZCBlbmQgbWFya2VycyAoY3J1ZGUgYnV0IHVz +YWJsZS4pDQoNCkYzIC0gY29weSBzZWxlY3RlZCB0ZXh0IHRvIHRoZSB0b3Agb2YgdGhlIHN0YWNr +Lg0KDQpTaGlmdC1GMyAtIGFzIGNvcHkgdGhlbiBydW4gInBhcnNlIiBjb21tYW5kIG9uIHRoZSBz +dHJpbmcuDQoNCkY0IC0gY3V0IHNlbGVjdGVkIHRleHQgdG8gdGhlIHRvcCBvZiB0aGUgc3RhY2su +DQoNClNoaWZ0LUY0IC0gYXMgY3V0IHRoZW4gcnVuICJwb3AiIChkZWxldGUgc2VsZWN0aW9uLikN +Cg0KSm95DQoNClByZXR0eSBtdWNoIGFsbCBvZiB0aGUgcmVzdCBvZiB0aGUgZnVuY3Rpb25hbGl0 +eSBvZiB0aGUgc3lzdGVtIGlzIHByb3ZpZGVkDQpieSBleGVjdXRpbmcgSm95IGNvbW1hbmRzIChh +a2EgZnVuY3Rpb25zLCBha2EgIndvcmRzIiBpbiBGb3J0aCkgYnkgcmlnaHQtDQpjbGlja2luZyBv +biB0aGVpciBuYW1lcyBpbiBhbnkgdGV4dC4NCg0KVG8gZ2V0IGhlbHAgb24gYSBKb3kgZnVuY3Rp +b24gc2VsZWN0IHRoZSBuYW1lIG9mIHRoZSBmdW5jdGlvbiBpbiBhDQpUZXh0Vmlld2VyIHVzaW5n +IEYxIGFuZCBGMiwgdGhlbiBwcmVzcyBzaGlmdC1GMyB0byBwYXJzZSB0aGUgc2VsZWN0aW9uLg0K +VGhlIGZ1bmN0aW9uIChyZWFsbHkgaXRzIFN5bWJvbCkgd2lsbCBhcHBlYXIgb24gdGhlIHN0YWNr +IGluIGJyYWNrZXRzIChhDQoicXVvdGVkIHByb2dyYW0iIHN1Y2ggYXMgIltwb3BdIi4pICBUaGVu +IHJpZ2h0LWNsaWNrIG9uIHRoZSB3b3JkIGhlbHAgaW4NCmFueSBUZXh0Vmlld2VyIChpZiBpdCdz +IG5vdCBhbHJlYWR5IHRoZXJlLCBqdXN0IHR5cGUgaXQgaW4gc29tZXdoZXJlLikNClRoaXMgd2ls +bCBwcmludCB0aGUgZG9jc3RyaW5nIG9yIGRlZmluaXRpb24gb2YgdGhlIHdvcmQgKGZ1bmN0aW9u +KSB0bw0Kc3Rkb3V0LiAgQXQgc29tZSBwb2ludCBJJ2xsIHdyaXRlIGEgdGhpbmcgdG8gc2VuZCB0 +aGF0IHRvIHRoZSBsb2cudHh0IGZpbGUNCmluc3RlYWQsIGJ1dCBmb3Igbm93IGxvb2sgZm9yIG91 +dHB1dCBpbiB0aGUgdGVybWluYWwuDQpQSwMEFAAAAAAA5GZ4Tnd/ml4DAAAAAwAAAAwAAABzdGFj +ay5waWNrbGUodC5QSwECFAAUAAAAAADkZnhO/eAZUX4DAAB+AwAADwAAAAAAAAAAAAAAtoEAAAAA +ZGVmaW5pdGlvbnMudHh0UEsBAhQAFAAAAAAABWOmTjNOyRkUFgAAFBYAAAoAAAAAAAAAAAAAALaB +qwMAAGxpYnJhcnkucHlQSwECFAAUAAAAAADkZnhORezk1gsAAAALAAAABwAAAAAAAAAAAAAAtoHn +GQAAbG9nLnR4dFBLAQIUABQAAAAAAORmeE5n9rvNAgUAAAIFAAAIAAAAAAAAAAAAAAC2gRcaAABt +ZW51LnR4dFBLAQIUABQAAAAAAORmeE4Aqz3GjhAAAI4QAAALAAAAAAAAAAAAAAC2gT8fAABzY3Jh +dGNoLnR4dFBLAQIUABQAAAAAAORmeE53f5peAwAAAAMAAAAMAAAAAAAAAAAAAAC2gfYvAABzdGFj +ay5waWNrbGVQSwUGAAAAAAYABgBTAQAAIzAAAAAA'''))) + + +if __name__ == '__main__': + print create_data() diff --git a/joy/vui/main.py b/joy/vui/main.py new file mode 100644 index 0000000..7557fc6 --- /dev/null +++ b/joy/vui/main.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python +import os, sys, traceback +import pygame +from joy.library import initialize, DefinitionWrapper, SimpleFunctionWrapper +import core, display, persist_task + + +FULLSCREEN = '-f' in sys.argv + + +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): + raise ValueError('what directory?') + + +def load_definitions(pt, dictionary): + lines = pt.open('definitions.txt')[1] + for line in lines: + if '==' in line: + DefinitionWrapper.add_def(line, dictionary) + + +def load_primitives(home, name_space): + fn = os.path.join(home, 'library.py') + if os.path.exists(fn): + execfile(fn, name_space) + + +def init(): + print 'Initializing Pygame...' + pygame.init() + print 'Creating window...' + if FULLSCREEN: + screen = pygame.display.set_mode() + else: + screen = pygame.display.set_mode((1024, 768)) + clock = pygame.time.Clock() + pygame.event.set_allowed(None) + pygame.event.set_allowed(core.ALLOWED_EVENTS) + pt = persist_task.PersistTask(JOY_HOME) + return screen, clock, pt + + +def init_context(screen, clock, pt): + D = initialize() + d = display.Display( + screen, + D.__contains__, + *((144 - 89, 144, 89) if FULLSCREEN else (89, 144)) + ) + log = d.init_text(pt, 0, 0, 'log.txt') + tho = d.init_text(pt, 0, d.h / 3, 'menu.txt') + t = d.init_text(pt, d.w / 2, 0, 'scratch.txt') + loop = core.TheLoop(d, clock) + stack_id, stack_holder = pt.open('stack.pickle') + world = core.World(stack_id, stack_holder, D, d.broadcast, log) + loop.install_task(pt.task_run, 10000) # save files every ten seconds + d.handlers.append(pt.handle) + d.handlers.append(world.handle) + load_definitions(pt, D) + return locals() + + +def error_guard(loop, n=10): + error_count = 0 + while error_count < n: + try: + loop() + break + except: + traceback.print_exc(file=sys.stderr) + error_count += 1 + + +class FileFaker(object): + + def __init__(self, log): + self.log = log + + def write(self, text): + self.log.append(text) + + def flush(self): + pass + + +def main(screen, clock, pt): + name_space = init_context(screen, clock, pt) + load_primitives(pt.home, name_space.copy()) + + @SimpleFunctionWrapper + def evaluate(stack): + '''Evaluate the Python code text on the top of the stack.''' + code, stack = stack + exec code in name_space.copy() + return stack + + name_space['D']['evaluate'] = evaluate + + + sys.stdout, old_stdout = FileFaker(name_space['log']), sys.stdout + try: + error_guard(name_space['loop'].loop) + finally: + sys.stdout = old_stdout + + return name_space['d'] + + +if __name__ == '__main__': + main(*init()) diff --git a/joy/vui/persist_task.py b/joy/vui/persist_task.py new file mode 100644 index 0000000..ab3520f --- /dev/null +++ b/joy/vui/persist_task.py @@ -0,0 +1,185 @@ +import os, pickle, traceback +from collections import Counter +from dulwich.errors import NotGitRepository +from dulwich.repo import Repo +import core, init_joy_home + + +def open_repo(repo_dir=None, initialize=False): + if not os.path.exists(repo_dir): + os.makedirs(repo_dir, 0700) + return init_repo(repo_dir) + try: + return Repo(repo_dir) + except NotGitRepository: + if initialize: + return init_repo(repo_dir) + raise + + +def init_repo(repo_dir): + repo = Repo.init(repo_dir) + init_joy_home.initialize(repo_dir) + repo.stage([ + fn + for fn in os.listdir(repo_dir) + if os.path.isfile(os.path.join(repo_dir, fn)) + ]) + repo.do_commit('Initial commit.', committer=core.COMMITTER) + return repo + + +def make_repo_relative_path_maker(repo): + c = repo.controldir() + def repo_relative_path(path): + return os.path.relpath(path, os.path.commonprefix((c, path))) + return repo_relative_path + + +class Resource(object): + + def __init__(self, filename, repo_relative_filename, thing=None): + self.filename = filename + self.repo_relative_filename = repo_relative_filename + self.thing = thing or self._from_file(open(filename)) + + def _from_file(self, f): + return f.read().splitlines() + + def _to_file(self, f): + for line in self.thing: + print >> f, line + + def persist(self, repo): + with open(self.filename, 'w') as f: + os.chmod(self.filename, 0600) + self._to_file(f) + f.flush() + os.fsync(f.fileno()) + # For goodness's sake, write it to the disk already! + repo.stage([self.repo_relative_filename]) + + +class PickledResource(Resource): + + def _from_file(self, f): + return [pickle.load(f)] + + def _to_file(self, f): + pickle.dump(self.thing[0], f) + + +class PersistTask(object): + + LIMIT = 10 + MAX_SAVE = 10 + + def __init__(self, home): + self.home = home + self.repo = open_repo(home) + self._r = make_repo_relative_path_maker(self.repo) + self.counter = Counter() + self.store = {} + + def open(self, name): + # look up the file in home and get its data + fn = os.path.join(self.home, name) + content_id = name # hash(fn) + try: + resource = self.store[content_id] + except KeyError: + R = PickledResource if name.endswith('.pickle') else Resource + resource = self.store[content_id] = R(fn, self._r(fn)) + return content_id, resource.thing + + def handle(self, message): + if isinstance(message, core.OpenMessage): + self.handle_open(message) + elif isinstance(message, core.ModifyMessage): + self.handle_modify(message) + elif isinstance(message, core.PersistMessage): + self.handle_persist(message) + elif isinstance(message, core.ShutdownMessage): + for content_id in self.counter: + self.store[content_id].persist(self.repo) + self.commit('shutdown') + + def handle_open(self, message): + try: + message.content_id, message.thing = self.open(message.name) + except: + message.traceback = traceback.format_exc() + message.status = core.ERROR + else: + message.status = core.SUCCESS + + def handle_modify(self, message): + try: + content_id = message.details['content_id'] + except KeyError: + return + if not content_id: + return + self.counter[content_id] += 1 + if self.counter[content_id] > self.LIMIT: + self.persist(content_id) + self.commit('due to activity') + + def handle_persist(self, message): + try: + resource = self.store[message.content_id] + except KeyError: + resource = self.handle_persist_new(message) + resource.persist(self.repo) + self.commit('by request from %r' % (message.sender,)) + + def handle_persist_new(self, message): + name = message.content_id + check_filename(name) + fn = os.path.join(self.home, name) + thing = message.details['thing'] + R = PickledResource if name.endswith('.pickle') else Resource # !!! refactor! + resource = self.store[name] = R(fn, self._r(fn), thing) + return resource + + def persist(self, content_id): + del self.counter[content_id] + self.store[content_id].persist(self.repo) + + def task_run(self): + if not self.counter: + return + for content_id, _ in self.counter.most_common(self.MAX_SAVE): + self.persist(content_id) + self.commit() + + def commit(self, message='auto-commit'): + return self.repo.do_commit(message, committer=core.COMMITTER) + + def scan(self): + return sorted([ + fn + for fn in os.listdir(self.home) + if os.path.isfile(os.path.join(self.home, fn)) + ]) + + +def check_filename(name): + # TODO: improve this... + if len(name) > 64: + raise ValueError('bad name %r' % (name,)) + left, dot, right = name.partition('.') + if not left.isalnum() or dot and not right.isalnum(): + raise ValueError('bad name %r' % (name,)) + + + +if __name__ == '__main__': + JOY_HOME = os.path.expanduser('~/.joypy') + pt = PersistTask(JOY_HOME) + content_id, thing = pt.open('stack.pickle') + pt.persist(content_id) + print pt.counter + mm = core.ModifyMessage(None, None, content_id=content_id) + pt.handle(mm) + print pt.counter diff --git a/joy/vui/stack_viewer.py b/joy/vui/stack_viewer.py new file mode 100644 index 0000000..74050ef --- /dev/null +++ b/joy/vui/stack_viewer.py @@ -0,0 +1,50 @@ +from joy.utils.stack import expression_to_string, iter_stack +import core, text_viewer + + +MAX_WIDTH = 64 + + +def fsi(item): + '''Format Stack Item''' + if isinstance(item, tuple): + res = '[%s]' % expression_to_string(item) + elif isinstance(item, str): + res = '"%s"' % item + else: + assert not isinstance(item, unicode), repr(item) + res = str(item) + if len(res) > MAX_WIDTH: + return res[:MAX_WIDTH - 3] + '...' + return res + + +class StackViewer(text_viewer.TextViewer): + + def __init__(self, surface): + super(StackViewer, self).__init__(surface) + self.stack_holder = None + self.content_id = 'stack viewer' + + def _attach(self, display): + if self.stack_holder: + return + om = core.OpenMessage(self, 'stack.pickle') + display.broadcast(om) + if om.status != core.SUCCESS: + raise RuntimeError('stack unavailable') + self.stack_holder = om.thing + + def _update(self): + self.lines[:] = map(fsi, iter_stack(self.stack_holder[0])) or [''] + + def focus(self, display): + self._attach(display) + super(StackViewer, self).focus(display) + + def handle(self, message): + if (isinstance(message, core.ModifyMessage) + and message.subject is self.stack_holder + ): + self._update() + self.draw_body() diff --git a/joy/vui/text_viewer.py b/joy/vui/text_viewer.py new file mode 100644 index 0000000..451c363 --- /dev/null +++ b/joy/vui/text_viewer.py @@ -0,0 +1,674 @@ +import string +import pygame +from joy.utils.stack import expression_to_string +from core import ( + ARROW_KEYS, + BACKGROUND as BG, + FOREGROUND as FG, + CommandMessage, + ModifyMessage, + OpenMessage, + SUCCESS, + push, + ) +import viewer, font_data +reload(viewer) + + +MenuViewer = viewer.MenuViewer + + +SELECTION_COLOR = 235, 255, 0, 32 +SELECTION_KEYS = { + pygame.K_F1, + pygame.K_F2, + pygame.K_F3, + pygame.K_F4, + } +STACK_CHATTER_KEYS = { + pygame.K_F5, + pygame.K_F6, + pygame.K_F7, + pygame.K_F8, + } + + +def _is_command(display, word): + return display.lookup(word) or word.isdigit() or all( + not s or s.isdigit() for s in word.split('.', 1) + ) and len(word) > 1 + + +def format_stack_item(content): + if isinstance(content, tuple): + return '[%s]' % expression_to_string(content) + return str(content) + + +class Font(object): + + IMAGE = pygame.image.load(font_data.data, 'Iosevka12.BMP') + LOOKUP = (string.ascii_letters + + string.digits + + '''@#$&_~|`'"%^=-+*/\\<>[]{}(),.;:!?''') + + def __init__(self, char_w=8, char_h=19, line_h=19): + self.char_w = char_w + self.char_h = char_h + self.line_h = line_h + + def size(self, text): + return self.char_w * len(text), self.line_h + + def render(self, text): + surface = pygame.Surface(self.size(text)) + surface.fill(BG) + x = 0 + for ch in text: + if not ch.isspace(): + try: + i = self.LOOKUP.index(ch) + except ValueError: + # render a lil box... + r = (x + 1, self.line_h / 2 - 3, + self.char_w - 2, self.line_h / 2) + pygame.draw.rect(surface, FG, r, 1) + else: + iy, ix = divmod(i, 26) + ix *= self.char_w + iy *= self.char_h + area = ix, iy, self.char_w, self.char_h + surface.blit(self.IMAGE, (x, 0), area) + x += self.char_w + return surface + + def __contains__(self, char): + assert len(char) == 1, repr(char) + return char in self.LOOKUP + + +FONT = Font() + + +class TextViewer(MenuViewer): + + MINIMUM_HEIGHT = FONT.line_h + 3 + CLOSE_TEXT = FONT.render('close') + GROW_TEXT = FONT.render('grow') + + class Cursor(object): + + def __init__(self, viewer): + self.v = viewer + self.x = self.y = 0 + self.w, self.h = 2, FONT.line_h + self.mem = pygame.Surface((self.w, self.h)) + self.can_fade = False + + def set_to(self, x, y): + self.fade() + self.x, self.y = x, y + self.draw() + + def draw(self): + r = self.x * FONT.char_w, self.screen_y(), self.w, self.h + self.mem.blit(self.v.body_surface, (0, 0), r) + self.v.body_surface.fill(FG, r) + self.can_fade = True + + def fade(self): + if self.can_fade: + dest = self.x * FONT.char_w, self.screen_y() + self.v.body_surface.blit(self.mem, dest) + self.can_fade = False + + def screen_y(self, row=None): + if row is None: row = self.y + return (row - self.v.at_line) * FONT.line_h + + def up(self, _mod): + if self.y: + self.fade() + self.y -= 1 + self.x = min(self.x, len(self.v.lines[self.y])) + self.draw() + + def down(self, _mod): + if self.y < len(self.v.lines) - 1: + self.fade() + self.y += 1 + self.x = min(self.x, len(self.v.lines[self.y])) + self.draw() + self._check_scroll() + + def left(self, _mod): + if self.x: + self.fade() + self.x -= 1 + self.draw() + elif self.y: + self.fade() + self.y -= 1 + self.x = len(self.v.lines[self.y]) + self.draw() + self._check_scroll() + + def right(self, _mod): + if self.x < len(self.v.lines[self.y]): + self.fade() + self.x += 1 + self.draw() + elif self.y < len(self.v.lines) - 1: + self.fade() + self.y += 1 + self.x = 0 + self.draw() + self._check_scroll() + + def _check_scroll(self): + if self.y < self.v.at_line: + self.v.scroll_down() + elif self.y > self.v.at_line + self.v.h_in_lines: + self.v.scroll_up() + + def __init__(self, surface): + self.cursor = self.Cursor(self) + MenuViewer.__init__(self, surface) + self.lines = [''] + self.content_id = None + self.at_line = 0 + self.bg = BG + self.command = self.command_rect = None + self._sel_start = self._sel_end = None + + def resurface(self, surface): + self.cursor.fade() + MenuViewer.resurface(self, surface) + + w, h = self.CLOSE_TEXT.get_size() + self.close_rect = pygame.rect.Rect(self.w - 2 - w, 1, w, h) + w, h = self.GROW_TEXT.get_size() + self.grow_rect = pygame.rect.Rect(1, 1, w, h) + + self.body_surface = surface.subsurface(self.body_rect) + self.line_w = self.body_rect.w / FONT.char_w + 1 + self.h_in_lines = self.body_rect.h / FONT.line_h - 1 + self.command_rect = self.command = None + self._sel_start = self._sel_end = None + + def handle(self, message): + if super(TextViewer, self).handle(message): + return + if (isinstance(message, ModifyMessage) + and message.subject is self.lines + ): + # TODO: check self.at_line + self.draw_body() + + # Drawing + + def draw_menu(self): + #MenuViewer.draw_menu(self) + self.surface.blit(self.GROW_TEXT, (1, 1)) + self.surface.blit(self.CLOSE_TEXT, + (self.w - 2 - self.close_rect.w, 1)) + if self.content_id: + self.surface.blit(FONT.render('| ' + self.content_id), + (self.grow_rect.w + FONT.char_w + 3, 1)) + self.surface.fill( # light grey background + (196, 196, 196), + (0, 0, self.w - 1, self.MINIMUM_HEIGHT), + pygame.BLEND_MULT + ) + + def draw_body(self): + MenuViewer.draw_body(self) + ys = xrange(0, self.body_rect.height, FONT.line_h) + ls = self.lines[self.at_line:self.at_line + self.h_in_lines + 2] + for y, line in zip(ys, ls): + self.draw_line(y, line) + + def draw_line(self, y, line): + surface = FONT.render(line[:self.line_w]) + self.body_surface.blit(surface, (0, y)) + + def _redraw_line(self, row): + try: line = self.lines[row] + except IndexError: line = ' ' * self.line_w + else: + n = self.line_w - len(line) + if n > 0: line = line + ' ' * n + self.draw_line(self.cursor.screen_y(row), line) + + # General Functionality + + def focus(self, display): + self.cursor.v = self + self.cursor.draw() + + def unfocus(self): + self.cursor.fade() + + def scroll_up(self): + if self.at_line < len(self.lines) - 1: + self._fade_command() + self._deselect() + self._sel_start = self._sel_end = None + self.at_line += 1 + self.body_surface.scroll(0, -FONT.line_h) + row = self.h_in_lines + self.at_line + self._redraw_line(row) + self._redraw_line(row + 1) + self.cursor.draw() + + def scroll_down(self): + if self.at_line: + self._fade_command() + self._deselect() + self._sel_start = self._sel_end = None + self.at_line -= 1 + self.body_surface.scroll(0, FONT.line_h) + self._redraw_line(self.at_line) + self.cursor.draw() + + def command_down(self, display, x, y): + if self.command_rect and self.command_rect.collidepoint(x, y): + return + self._fade_command() + line, column, _row = self.at(x, y) + word_start = line.rfind(' ', 0, column) + 1 + word_end = line.find(' ', column) + if word_end == -1: word_end = len(line) + word = line[word_start:word_end] + if not _is_command(display, word): + return + r = self.command_rect = pygame.Rect( + word_start * FONT.char_w, # x + y / FONT.line_h * FONT.line_h, # y + len(word) * FONT.char_w, # w + FONT.line_h # h + ) + pygame.draw.line(self.body_surface, FG, r.bottomleft, r.bottomright) + self.command = word + + def command_up(self, display): + if self.command: + command = self.command + self._fade_command() + display.broadcast(CommandMessage(self, command)) + + def _fade_command(self): + self.command = None + r, self.command_rect = self.command_rect, None + if r: + pygame.draw.line(self.body_surface, BG, r.bottomleft, r.bottomright) + + def at(self, x, y): + ''' + Given screen coordinates return the line, row, and column of the + character there. + ''' + row = self.at_line + y / FONT.line_h + try: + line = self.lines[row] + except IndexError: + row = len(self.lines) - 1 + line = self.lines[row] + column = len(line) + else: + column = min(x / FONT.char_w, len(line)) + return line, column, row + + # Event Processing + + def body_click(self, display, x, y, button): + if button == 1: + _line, column, row = self.at(x, y) + self.cursor.set_to(column, row) + elif button == 2: + if pygame.KMOD_SHIFT & pygame.key.get_mods(): + self.scroll_up() + else: + self.scroll_down() + elif button == 3: + self.command_down(display, x, y) + elif button == 4: self.scroll_down() + elif button == 5: self.scroll_up() + + def menu_click(self, display, x, y, button): + if MenuViewer.menu_click(self, display, x, y, button): + return True + + def mouse_up(self, display, x, y, button): + if MenuViewer.mouse_up(self, display, x, y, button): + return True + elif button == 3 and self.body_rect.collidepoint(x, y): + self.command_up(display) + + 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, + button0, button1, button2): + return True + if (button0 + and display.focused_viewer is self + and self.body_rect.collidepoint(x, y) + ): + bx, by = self.body_rect.topleft + _line, column, row = self.at(x - bx, y - by) + self.cursor.set_to(column, row) + elif button2 and self.body_rect.collidepoint(x, y): + bx, by = self.body_rect.topleft + self.command_down(display, x - bx, y - by) + + def close(self): + self._sel_start = self._sel_end = None + + def key_down(self, display, uch, key, mod): + + if key in SELECTION_KEYS: + self._selection_key(display, key, mod) + return + if key in STACK_CHATTER_KEYS: + self._stack_chatter_key(display, key, mod) + return + if key in ARROW_KEYS: + self._arrow_key(key, mod) + return + + line, i = self.lines[self.cursor.y], self.cursor.x + modified = () + if key == pygame.K_RETURN: + self._return_key(mod, line, i) + modified = True + elif key == pygame.K_BACKSPACE: + modified = self._backspace_key(mod, line, i) + elif key == pygame.K_DELETE: + modified = self._delete_key(mod, line, i) + elif key == pygame.K_INSERT: + modified = self._insert_key(display, mod, line, i) + elif uch and uch in FONT or uch == ' ': + self._printable_key(uch, mod, line, i) + modified = True + else: + print '%r %i %s' % (uch, key, bin(mod)) + + if modified: + # The selection is fragile. + self._deselect() + self._sel_start = self._sel_end = None + message = ModifyMessage( + self, self.lines, content_id=self.content_id) + display.broadcast(message) + + def _stack_chatter_key(self, display, key, mod): + if key == pygame.K_F5: + if mod & pygame.KMOD_SHIFT: + command = 'roll<' + else: + command = 'swap' + elif key == pygame.K_F6: + if mod & pygame.KMOD_SHIFT: + command = 'roll>' + else: + command = 'dup' + elif key == pygame.K_F7: + if mod & pygame.KMOD_SHIFT: + command = 'tuck' + else: + command = 'over' +## elif key == pygame.K_F8: +## if mod & pygame.KMOD_SHIFT: +## command = '' +## else: +## command = '' + else: + return + display.broadcast(CommandMessage(self, command)) + + # Selection Handling + + def _selection_key(self, display, key, mod): + self.cursor.fade() + self._deselect() + if key == pygame.K_F1: # set sel start + self._sel_start = self.cursor.y, self.cursor.x + self._update_selection() + elif key == pygame.K_F2: # set sel end + self._sel_end = self.cursor.y, self.cursor.x + self._update_selection() + elif key == pygame.K_F3: # copy + if mod & pygame.KMOD_SHIFT: + self._parse_selection(display) + else: + self._copy_selection(display) + self._update_selection() + elif key == pygame.K_F4: # cut or delete + if mod & pygame.KMOD_SHIFT: + self._delete_selection(display) + else: + self._cut_selection(display) + self.cursor.draw() + + def _deselect(self): + if self._has_selection(): + srow, erow = self._sel_start[0], self._sel_end[0] + # Just erase the whole selection. + for r in range(min(srow, erow), max(srow, erow) + 1): + self._redraw_line(r) + + def _copy_selection(self, display): + if push(self, self._get_selection(), display.broadcast) == SUCCESS: + return True +## om = OpenMessage(self, 'stack.pickle') +## display.broadcast(om) +## if om.status == SUCCESS: +## selection = self._get_selection() +## om.thing[0] = selection, om.thing[0] +## display.broadcast(ModifyMessage( +## self, om.thing, content_id=om.content_id)) + + def _parse_selection(self, display): + if self._has_selection(): + if self._copy_selection(display): + display.broadcast(CommandMessage(self, 'parse')) + + def _cut_selection(self, display): + if self._has_selection(): + if self._copy_selection(display): + self._delete_selection(display) + + def _delete_selection(self, display): + if not self._has_selection(): + return + self.cursor.fade() + srow, scolumn, erow, ecolumn = self._selection_coords() + if srow == erow: + line = self.lines[srow] + self.lines[srow] = line[:scolumn] + line[ecolumn:] + else: + left = self.lines[srow][:scolumn] + right = self.lines[erow][ecolumn:] + self.lines[srow:erow + 1] = [left + right] + self.draw_body() + self.cursor.set_to(srow, scolumn) + display.broadcast(ModifyMessage( + self, self.lines, content_id=self.content_id)) + + def _has_selection(self): + return (self._sel_start + and self._sel_end + and self._sel_start != self._sel_end) + + def _get_selection(self): + '''Return the current selection if any as a single string.''' + if not self._has_selection(): + return '' + srow, scolumn, erow, ecolumn = self._selection_coords() + if srow == erow: + return str(self.lines[srow][scolumn:ecolumn]) + lines = [] + assert srow < erow + while srow <= erow: + line = self.lines[srow] + e = ecolumn if srow == erow else len(line) + lines.append(line[scolumn:e]) + scolumn = 0 + srow += 1 + return str('\n'.join(lines)) + + def _selection_coords(self): + (srow, scolumn), (erow, ecolumn) = ( + min(self._sel_start, self._sel_end), + max(self._sel_start, self._sel_end) + ) + return srow, scolumn, erow, ecolumn + + def _update_selection(self): + if self._sel_start is None and self._sel_end: + self._sel_start = self._sel_end + elif self._sel_end is None and self._sel_start: + self._sel_end = self._sel_start + assert self._sel_start and self._sel_end + if self._sel_start != self._sel_end: + for rect in self._iter_selection_rectangles(): + self.body_surface.fill( + SELECTION_COLOR, + rect, + pygame.BLEND_RGBA_MULT + ) + + def _iter_selection_rectangles(self, ): + srow, scolumn, erow, ecolumn = self._selection_coords() + if srow == erow: + yield ( + scolumn * FONT.char_w, + self.cursor.screen_y(srow), + (ecolumn - scolumn) * FONT.char_w, + FONT.line_h + ) + return + lines = self.lines[srow:erow + 1] + assert len(lines) >= 2 + first_line = lines[0] + yield ( + scolumn * FONT.char_w, + self.cursor.screen_y(srow), + (len(first_line) - scolumn) * FONT.char_w, + FONT.line_h + ) + yield ( + 0, + self.cursor.screen_y(erow), + ecolumn * FONT.char_w, + FONT.line_h + ) + if len(lines) > 2: + for line in lines[1:-1]: + srow += 1 + yield ( + 0, + self.cursor.screen_y(srow), + len(line) * FONT.char_w, + FONT.line_h + ) + + # Key Handlers + + def _printable_key(self, uch, _mod, line, i): + line = line[:i] + uch + line[i:] + self.lines[self.cursor.y] = line + self.cursor.fade() + self.cursor.x += 1 + self.draw_line(self.cursor.screen_y(), line) + self.cursor.draw() + + def _backspace_key(self, _mod, line, i): + res = False + if i: + line = line[:i - 1] + line[i:] + self.lines[self.cursor.y] = line + self.cursor.fade() + self.cursor.x -= 1 + self.draw_line(self.cursor.screen_y(), line + ' ') + self.cursor.draw() + res = True + elif self.cursor.y: + y = self.cursor.y + left, right = self.lines[y - 1:y + 1] + self.lines[y - 1:y + 1] = [left + right] + self.cursor.x = len(left) + self.cursor.y -= 1 + self.draw_body() + self.cursor.draw() + res = True + return res + + def _delete_key(self, _mod, line, i): + res = False + if i < len(line): + line = line[:i] + line[i + 1:] + self.lines[self.cursor.y] = line + self.cursor.fade() + self.draw_line(self.cursor.screen_y(), line + ' ') + self.cursor.draw() + res = True + elif self.cursor.y < len(self.lines) - 1: + y = self.cursor.y + left, right = self.lines[y:y + 2] + self.lines[y:y + 2] = [left + right] + self.draw_body() + self.cursor.draw() + res = True + return res + + def _arrow_key(self, key, mod): + if key == pygame.K_UP: self.cursor.up(mod) + elif key == pygame.K_DOWN: self.cursor.down(mod) + elif key == pygame.K_LEFT: self.cursor.left(mod) + elif key == pygame.K_RIGHT: self.cursor.right(mod) + + def _return_key(self, _mod, line, i): + self.cursor.fade() + # Ignore the mods for now. + n = self.cursor.y + self.lines[n:n + 1] = [line[:i], line[i:]] + self.cursor.y += 1 + self.cursor.x = 0 + if self.cursor.y > self.at_line + self.h_in_lines: + self.scroll_up() + else: + self.draw_body() + self.cursor.draw() + + def _insert_key(self, display, mod, _line, _i): + om = OpenMessage(self, 'stack.pickle') + display.broadcast(om) + if om.status != SUCCESS: + return + stack = om.thing[0] + if stack: + content = format_stack_item(stack[0]) + if self.insert(content): + if mod & pygame.KMOD_SHIFT: + display.broadcast(CommandMessage(self, 'pop')) + return True + + def insert(self, content): + assert isinstance(content, basestring), repr(content) + if content: + self.cursor.fade() + row, column = self.cursor.y, self.cursor.x + line = self.lines[row] + lines = (line[:column] + content + line[column:]).splitlines() + self.lines[row:row + 1] = lines + self.draw_body() + self.cursor.y = row + len(lines) - 1 + self.cursor.x = len(lines[-1]) - len(line) + column + self.cursor.draw() + return True + + def append(self, content): + self.cursor.fade() + self.cursor.y = len(self.lines) - 1 + self.cursor.x = len(self.lines[self.cursor.y]) + self.insert(content) diff --git a/joy/vui/viewer.py b/joy/vui/viewer.py new file mode 100644 index 0000000..9b56467 --- /dev/null +++ b/joy/vui/viewer.py @@ -0,0 +1,232 @@ +# -*- coding: utf-8 -*- +# +# Copyright © 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 . +# +import pygame +from core import BACKGROUND, FOREGROUND + + +class Viewer(object): + + MINIMUM_HEIGHT = 11 + + def __init__(self, surface): + self.resurface(surface) + self.last_touch = 0, 0 + + def resurface(self, surface): + self.w, self.h = surface.get_width(), surface.get_height() + self.surface = surface + + def split(self, y): + ''' + Split the viewer at the y coordinate (which is relative to the + viewer's surface and must be inside it somewhere) and return the + remaining height. The upper part of the viewer remains (and gets + redrawn on a new surface) and the lower space is now available + for e.g. a new viewer. + ''' + assert y >= self.MINIMUM_HEIGHT + new_viewer_h = self.h - y + self.resurface(self.surface.subsurface((0, 0, self.w, y))) + if y <= self.last_touch[1]: self.last_touch = 0, 0 + self.draw() + return new_viewer_h + + def handle(self, message): + assert self is not message.sender + pass + + def draw(self): + '''Draw the viewer onto its surface.''' + self.surface.fill(BACKGROUND) + x, y, h = self.w - 1, self.MINIMUM_HEIGHT, self.h - 1 + # Right-hand side. + pygame.draw.line(self.surface, FOREGROUND, (x, 0), (x, h)) + # Between header and body. + pygame.draw.line(self.surface, FOREGROUND, (0, y), (x, y)) + # Bottom. + pygame.draw.line(self.surface, FOREGROUND, (0, h), (x, h)) + + def close(self): + '''Close the viewer and release any resources, etc...''' + + def focus(self, display): + pass + + def unfocus(self): + pass + + # Event handling. + + def mouse_down(self, display, x, y, button): + self.last_touch = x, y + + def mouse_up(self, display, x, y, button): + pass + + def mouse_motion(self, display, x, y, dx, dy, button0, button1, button2): + pass + + def key_up(self, display, key, mod): + if key == pygame.K_q and mod & pygame.KMOD_CTRL: # Ctrl-q + display.close_viewer(self) + return True + if key == pygame.K_g and mod & pygame.KMOD_CTRL: # Ctrl-g + display.grow_viewer(self) + return True + + def key_down(self, display, uch, key, mod): + pass + + +class MenuViewer(Viewer): + + MINIMUM_HEIGHT = 26 + + def __init__(self, surface): + Viewer.__init__(self, surface) + self.resizing = 0 + self.bg = 100, 150, 100 + + def resurface(self, surface): + Viewer.resurface(self, surface) + n = self.MINIMUM_HEIGHT - 2 + self.close_rect = pygame.rect.Rect(self.w - 2 - n, 1, n, n) + self.grow_rect = pygame.rect.Rect(1, 1, n, n) + self.body_rect = pygame.rect.Rect( + 0, self.MINIMUM_HEIGHT + 1, + self.w - 1, self.h - self.MINIMUM_HEIGHT - 2) + + def draw(self): + '''Draw the viewer onto its surface.''' + Viewer.draw(self) + if not self.resizing: + self.draw_menu() + self.draw_body() + + def draw_menu(self): + # menu buttons + pygame.draw.rect(self.surface, FOREGROUND, self.close_rect, 1) + pygame.draw.rect(self.surface, FOREGROUND, self.grow_rect, 1) + + def draw_body(self): + self.surface.fill(self.bg, self.body_rect) + + def mouse_down(self, display, x, y, button): + Viewer.mouse_down(self, display, x, y, button) + if y <= self.MINIMUM_HEIGHT: + self.menu_click(display, x, y, button) + else: + bx, by = self.body_rect.topleft + self.body_click(display, x - bx, y - by, button) + + def body_click(self, display, x, y, button): + if button == 1: + self.draw_an_a(x, y) + + def menu_click(self, display, x, y, button): + if button == 1: + self.resizing = 1 + elif button == 3: + if self.close_rect.collidepoint(x, y): + display.close_viewer(self) + return True + elif self.grow_rect.collidepoint(x, y): + display.grow_viewer(self) + return True + + def mouse_up(self, display, x, y, button): + + if button == 1 and self.resizing: + if self.resizing == 2: + self.resizing = 0 + self.draw() + display.done_resizing() + self.resizing = 0 + return True + + def mouse_motion(self, display, x, y, rel_x, rel_y, button0, button1, button2): + if self.resizing and button0: + self.resizing = 2 + display.change_viewer(self, rel_y, relative=True) + return True + else: + self.resizing = 0 + #self.draw_an_a(x, y) + + def key_up(self, display, key, mod): + if Viewer.key_up(self, display, key, mod): + return True + + def draw_an_a(self, x, y): + # Draw a crude letter A. + lw, lh = 10, 14 + try: surface = self.surface.subsurface((x - lw, y - lh, lw, lh)) + except ValueError: return + draw_a(surface, blend=1) + + +class SomeViewer(MenuViewer): + + def __init__(self, surface): + MenuViewer.__init__(self, surface) + + def resurface(self, surface): + MenuViewer.resurface(self, surface) + + def draw_menu(self): + MenuViewer.draw_menu(self) + + def draw_body(self): + pass + + def body_click(self, display, x, y, button): + pass + + def menu_click(self, display, x, y, button): + if MenuViewer.menu_click(self, display, x, y, button): + return True + + def mouse_up(self, display, x, y, button): + if MenuViewer.mouse_up(self, display, x, y, button): + return True + + 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, + button0, button1, button2): + return True + + def key_down(self, display, uch, key, mod): + try: + print chr(key), + except ValueError: + pass + + +# Note that Oberon book says that if you split at the exact top of a viewer +# it should close, and I think this implies the new viewer gets the old +# viewer's whole height. I haven't implemented that yet, so the edge-case +# in the code is broken by "intent" for now.. + + +def draw_a(surface, color=FOREGROUND, blend=False): + w, h = surface.get_width() - 2, surface.get_height() - 2 + pygame.draw.aalines(surface, color, False, ( + (1, h), (w / 2, 1), (w, h), (1, h / 2) + ), blend)