diff --git a/joy/vui/core.py b/joy/vui/core.py
index ec8aeed..d31a84c 100644
--- a/joy/vui/core.py
+++ b/joy/vui/core.py
@@ -1,280 +1,281 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © 2019 Simon Forman
-#
-# This file is part of Thun
-#
-# Thun 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.
-#
-# Thun 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 Thun. If not see .
-#
-'''
-
-Core
-=====================
-
-The core module defines a bunch of system-wide "constants" (some colors
-and PyGame event groups), the message classes for Oberon-style message
-passing, a "world" class that holds the main context for the system, and
-a mainloop class that manages the, uh, main loop (the PyGame event queue.)
-
-'''
-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
- })
-'PyGame mouse events.'
-
-ARROW_KEYS = frozenset({
- pygame.K_UP,
- pygame.K_DOWN,
- pygame.K_LEFT,
- pygame.K_RIGHT
- })
-'PyGame arrow key events.'
-
-
-TASK_EVENTS = tuple(range(pygame.USEREVENT, pygame.NUMEVENTS))
-'Keep track of all possible task events.'
-
-AVAILABLE_TASK_EVENTS = set(TASK_EVENTS)
-'Task IDs that have not been assigned to a task.'
-
-ALLOWED_EVENTS = [pygame.QUIT, pygame.KEYUP, pygame.KEYDOWN]
-ALLOWED_EVENTS.extend(MOUSE_EVENTS)
-ALLOWED_EVENTS.extend(TASK_EVENTS)
-'Event "mask" for PyGame event queue, we are only interested in these event types.'
-
-
-ERROR = -1
-PENDING = 0
-SUCCESS = 1
-# 'Message status codes... dunno if this is a good idea or not...
-
-
-class Message(object):
- '''Message base class. Contains ``sender`` field.'''
- def __init__(self, sender):
- self.sender = sender
-
-
-class CommandMessage(Message):
- '''For commands, adds ``command`` field.'''
- def __init__(self, sender, command):
- Message.__init__(self, sender)
- self.command = command
-
-
-class ModifyMessage(Message):
- '''
- For when resources are modified, adds ``subject`` and ``details``
- fields.
- '''
- def __init__(self, sender, subject, **details):
- Message.__init__(self, sender)
- self.subject = subject
- self.details = details
-
-
-class OpenMessage(Message):
- '''
- For when resources are modified, adds ``name``, content_id``,
- ``status``, and ``traceback`` fields.
- '''
- def __init__(self, sender, name):
- Message.__init__(self, sender)
- self.name = name
- self.content_id = self.thing = None
- self.status = PENDING
- self.traceback = None
-
-
-class PersistMessage(Message):
- '''
- For when resources are modified, adds ``content_id`` and ``details``
- fields.
- '''
- def __init__(self, sender, content_id, **details):
- Message.__init__(self, sender)
- self.content_id = content_id
- self.details = details
-
-
-class ShutdownMessage(Message):
- '''Signals that the system is shutting down.'''
-
-
-# Joy Interpreter & Context
-
-
-class World(object):
- '''
- This object contains the system context, the stack, dictionary, a
- reference to the display broadcast method, and the log.
- '''
-
- def __init__(self, stack_id, stack_holder, dictionary, notify, log):
- self.stack_holder = stack_holder
- 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):
- '''
- Deal with updates to the stack and commands.
- '''
- 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'):
- '''
- Helper function to push an item onto the system stack with message.
- '''
- 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):
- '''
- Helper function to open a text viewer on a string.
- Typically used to show tracebacks.
- '''
- push(sender, content, notify)
- notify(CommandMessage(sender, 'good_viewer_location open_viewer'))
-
-
-# main loop
-
-
-class TheLoop(object):
- '''
- The main loop manages tasks and the PyGame event queue
- and framerate clock.
- '''
-
- 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):
- '''
- Install a task to run every so many 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):
- '''
- Remove an installed task.
- '''
- assert task_event_id in self.tasks, repr(task_event_id)
- pygame.time.set_timer(task_event_id, 0)
- del self.tasks[task_event_id]
- AVAILABLE_TASK_EVENTS.add(task_event_id)
-
- def __del__(self):
- # Best effort to cancel all running tasks.
- for task_event_id in self.tasks:
- pygame.time.set_timer(task_event_id, 0)
-
- def run_task(self, task_event_id):
- '''
- Give a task its time to shine.
- '''
- task = self.tasks[task_event_id]
- try:
- task()
- 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):
- '''
- The actual main loop machinery.
-
- Maintain a ``running`` flag, pump the PyGame event queue and
- handle the events (dispatching to the display), tick the clock.
-
- When the loop is exited (by clicking the window close button or
- pressing the ``escape`` key) it broadcasts a ``ShutdownMessage``.
- '''
- self.running = True
- while self.running:
- for event in pygame.event.get():
- 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))
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2019 Simon Forman
+#
+# This file is part of Thun
+#
+# Thun 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.
+#
+# Thun 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 Thun. If not see .
+#
+'''
+
+Core
+=====================
+
+The core module defines a bunch of system-wide "constants" (some colors
+and PyGame event groups), the message classes for Oberon-style message
+passing, a "world" class that holds the main context for the system, and
+a mainloop class that manages the, uh, main loop (the PyGame event queue.)
+
+'''
+from __future__ import print_function
+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
+ })
+'PyGame mouse events.'
+
+ARROW_KEYS = frozenset({
+ pygame.K_UP,
+ pygame.K_DOWN,
+ pygame.K_LEFT,
+ pygame.K_RIGHT
+ })
+'PyGame arrow key events.'
+
+
+TASK_EVENTS = tuple(range(pygame.USEREVENT, pygame.NUMEVENTS))
+'Keep track of all possible task events.'
+
+AVAILABLE_TASK_EVENTS = set(TASK_EVENTS)
+'Task IDs that have not been assigned to a task.'
+
+ALLOWED_EVENTS = [pygame.QUIT, pygame.KEYUP, pygame.KEYDOWN]
+ALLOWED_EVENTS.extend(MOUSE_EVENTS)
+ALLOWED_EVENTS.extend(TASK_EVENTS)
+'Event "mask" for PyGame event queue, we are only interested in these event types.'
+
+
+ERROR = -1
+PENDING = 0
+SUCCESS = 1
+# 'Message status codes... dunno if this is a good idea or not...
+
+
+class Message(object):
+ '''Message base class. Contains ``sender`` field.'''
+ def __init__(self, sender):
+ self.sender = sender
+
+
+class CommandMessage(Message):
+ '''For commands, adds ``command`` field.'''
+ def __init__(self, sender, command):
+ Message.__init__(self, sender)
+ self.command = command
+
+
+class ModifyMessage(Message):
+ '''
+ For when resources are modified, adds ``subject`` and ``details``
+ fields.
+ '''
+ def __init__(self, sender, subject, **details):
+ Message.__init__(self, sender)
+ self.subject = subject
+ self.details = details
+
+
+class OpenMessage(Message):
+ '''
+ For when resources are modified, adds ``name``, content_id``,
+ ``status``, and ``traceback`` fields.
+ '''
+ def __init__(self, sender, name):
+ Message.__init__(self, sender)
+ self.name = name
+ self.content_id = self.thing = None
+ self.status = PENDING
+ self.traceback = None
+
+
+class PersistMessage(Message):
+ '''
+ For when resources are modified, adds ``content_id`` and ``details``
+ fields.
+ '''
+ def __init__(self, sender, content_id, **details):
+ Message.__init__(self, sender)
+ self.content_id = content_id
+ self.details = details
+
+
+class ShutdownMessage(Message):
+ '''Signals that the system is shutting down.'''
+
+
+# Joy Interpreter & Context
+
+
+class World(object):
+ '''
+ This object contains the system context, the stack, dictionary, a
+ reference to the display broadcast method, and the log.
+ '''
+
+ def __init__(self, stack_id, stack_holder, dictionary, notify, log):
+ self.stack_holder = stack_holder
+ 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):
+ '''
+ Deal with updates to the stack and commands.
+ '''
+ 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(format_exc(), file=stderr)
+ return str(self.stack_holder[0])
+
+
+def push(sender, item, notify, stack_name='stack.pickle'):
+ '''
+ Helper function to push an item onto the system stack with message.
+ '''
+ om = OpenMessage(sender, stack_name)
+ notify(om)
+ if om.status == SUCCESS:
+ 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):
+ '''
+ Helper function to open a text viewer on a string.
+ Typically used to show tracebacks.
+ '''
+ push(sender, content, notify)
+ notify(CommandMessage(sender, 'good_viewer_location open_viewer'))
+
+
+# main loop
+
+
+class TheLoop(object):
+ '''
+ The main loop manages tasks and the PyGame event queue
+ and framerate clock.
+ '''
+
+ 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):
+ '''
+ Install a task to run every so many 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):
+ '''
+ Remove an installed task.
+ '''
+ assert task_event_id in self.tasks, repr(task_event_id)
+ pygame.time.set_timer(task_event_id, 0)
+ del self.tasks[task_event_id]
+ AVAILABLE_TASK_EVENTS.add(task_event_id)
+
+ def __del__(self):
+ # Best effort to cancel all running tasks.
+ for task_event_id in self.tasks:
+ pygame.time.set_timer(task_event_id, 0)
+
+ def run_task(self, task_event_id):
+ '''
+ Give a task its time to shine.
+ '''
+ task = self.tasks[task_event_id]
+ try:
+ task()
+ except:
+ traceback = format_exc()
+ self.remove_task(task_event_id)
+ print(traceback, file=stderr)
+ print('TASK removed due to ERROR', task, file=stderr)
+ open_viewer_on_string(self, traceback, self.display.broadcast)
+
+ def loop(self):
+ '''
+ The actual main loop machinery.
+
+ Maintain a ``running`` flag, pump the PyGame event queue and
+ handle the events (dispatching to the display), tick the clock.
+
+ When the loop is exited (by clicking the window close button or
+ pressing the ``escape`` key) it broadcasts a ``ShutdownMessage``.
+ '''
+ self.running = True
+ while self.running:
+ for event in pygame.event.get():
+ 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
index 128a993..aeda7b3 100644
--- a/joy/vui/debug_main.py
+++ b/joy/vui/debug_main.py
@@ -1,18 +1,19 @@
-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)
+from __future__ import absolute_import
+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
+
+from . 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/display.py b/joy/vui/display.py
index d479e7c..2e94417 100644
--- a/joy/vui/display.py
+++ b/joy/vui/display.py
@@ -1,504 +1,505 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © 2019 Simon Forman
-#
-# This file is part of Thun
-#
-# Thun 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.
-#
-# Thun 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 Thun. If not see .
-#
-'''
-
-Display
-=================
-
-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
-from joy.vui 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):
- '''
- Iterate through all viewers yielding (viewer, x, y) three-tuples.
- The x and y coordinates are screen pixels of the top-left corner
- of the viewer.
- '''
- for x, T in self.tracks:
- for y, V in T.viewers:
- yield V, x, y
-
- def done_resizing(self):
- '''
- Helper method called directly by ``MenuViewer.mouse_up()`` to (hackily)
- update the display when done resizing a viewer.
- '''
- for _, track in self.tracks: # This should be done by a Message?
- if track.resizing_viewer:
- track.resizing_viewer.draw()
- track.resizing_viewer = None
- break
-
- def broadcast(self, message):
- '''
- Broadcast a message to all viewers (except the sender) and all
- registered handlers.
- '''
- for _, track in self.tracks:
- track.broadcast(message)
- for handler in self.handlers:
- handler(message)
-
- def redraw(self):
- '''
- Redraw all tracks (which will redraw all viewers.)
- '''
- for _, track in self.tracks:
- track.redraw()
-
- def focus(self, viewer):
- '''
- Set system focus to a given viewer (or no viewer if a track.)
- '''
- if isinstance(viewer, Track):
- if self.focused_viewer: self.focused_viewer.unfocus()
- self.focused_viewer = None
- 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):
- '''
- Open and return a ``TextViewer`` on a given file (which must be present
- in the ``JOYHOME`` directory.)
- '''
- viewer = self.open_viewer(x, y, text_viewer.TextViewer)
- viewer.content_id, viewer.lines = pt.open(filename)
- viewer.draw()
- return viewer
-
-
-class Track(Viewer):
- '''
- Manage a vertical strip of the display, and the viewers on it.
- '''
-
- 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):
- '''
- Broadcast a message to all viewers on this track (except the sender.)
- '''
- for _, viewer in self.viewers:
- if viewer is not message.sender:
- viewer.handle(message)
-
- def redraw(self):
- '''Redraw the track and all of its viewers.'''
- self.draw()
- for _, viewer in self.viewers:
- viewer.draw()
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2019 Simon Forman
+#
+# This file is part of Thun
+#
+# Thun 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.
+#
+# Thun 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 Thun. If not see .
+#
+'''
+
+Display
+=================
+
+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 __future__ import print_function
+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
+from joy.vui 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):
+ '''
+ Iterate through all viewers yielding (viewer, x, y) three-tuples.
+ The x and y coordinates are screen pixels of the top-left corner
+ of the viewer.
+ '''
+ for x, T in self.tracks:
+ for y, V in T.viewers:
+ yield V, x, y
+
+ def done_resizing(self):
+ '''
+ Helper method called directly by ``MenuViewer.mouse_up()`` to (hackily)
+ update the display when done resizing a viewer.
+ '''
+ for _, track in self.tracks: # This should be done by a Message?
+ if track.resizing_viewer:
+ track.resizing_viewer.draw()
+ track.resizing_viewer = None
+ break
+
+ def broadcast(self, message):
+ '''
+ Broadcast a message to all viewers (except the sender) and all
+ registered handlers.
+ '''
+ for _, track in self.tracks:
+ track.broadcast(message)
+ for handler in self.handlers:
+ handler(message)
+
+ def redraw(self):
+ '''
+ Redraw all tracks (which will redraw all viewers.)
+ '''
+ for _, track in self.tracks:
+ track.redraw()
+
+ def focus(self, viewer):
+ '''
+ Set system focus to a given viewer (or no viewer if a track.)
+ '''
+ if isinstance(viewer, Track):
+ if self.focused_viewer: self.focused_viewer.unfocus()
+ self.focused_viewer = None
+ 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((
+ 'received event %s Use pygame.event.set_allowed().'
+ % pygame.event.event_name(event.type)
+ ), file=stderr)
+ # Catch all exceptions and open a viewer.
+ except:
+ err = format_exc()
+ print(err, file=stderr) # 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):
+ '''
+ Open and return a ``TextViewer`` on a given file (which must be present
+ in the ``JOYHOME`` directory.)
+ '''
+ viewer = self.open_viewer(x, y, text_viewer.TextViewer)
+ viewer.content_id, viewer.lines = pt.open(filename)
+ viewer.draw()
+ return viewer
+
+
+class Track(Viewer):
+ '''
+ Manage a vertical strip of the display, and the viewers on it.
+ '''
+
+ 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):
+ '''
+ Broadcast a message to all viewers on this track (except the sender.)
+ '''
+ for _, viewer in self.viewers:
+ if viewer is not message.sender:
+ viewer.handle(message)
+
+ 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
index ff84e51..7f2fc48 100644
--- a/joy/vui/font_data.py
+++ b/joy/vui/font_data.py
@@ -1,186 +1,187 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © 2019 Simon Forman
-#
-# This file is part of Thun
-#
-# Thun 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.
-#
-# Thun 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 Thun. If not see .
-#
-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()
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2019 Simon Forman
+#
+# This file is part of Thun
+#
+# Thun 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.
+#
+# Thun 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 Thun. If not see .
+#
+from __future__ import print_function
+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
index 52ea90f..ec494a8 100644
--- a/joy/vui/init_joy_home.py
+++ b/joy/vui/init_joy_home.py
@@ -1,275 +1,276 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © 2019 Simon Forman
-#
-# This file is part of Thun
-#
-# Thun 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.
-#
-# Thun 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 Thun. If not see .
-#
-'''
-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
-IFtdIFtfQlRyZWUtcmVjdXJdIGdlbnJlYw0KUEsDBBQAAAAAACFrpk7/HHjxGBYAABgWAAAKAAAA
-bGlicmFyeS5weScnJw0KVGhpcyBmaWxlIGlzIGV4ZWNmaWxlKCknZCB3aXRoIGEgbmFtZXNwYWNl
-IGNvbnRhaW5pbmc6DQoNCiAgRCAtIHRoZSBKb3kgZGljdGlvbmFyeQ0KICBkIC0gdGhlIERpc3Bs
-YXkgb2JqZWN0DQogIHB0IC0gdGhlIFBlcnNpc3RUYXNrIG9iamVjdA0KICBsb2cgLSB0aGUgbG9n
-LnR4dCB2aWV3ZXINCiAgbG9vcCAtIHRoZSBUaGVMb29wIG1haW4gbG9vcCBvYmplY3QNCiAgc3Rh
-Y2tfaG9sZGVyIC0gdGhlIFB5dGhvbiBsaXN0IG9iamVjdCB0aGF0IGhvbGRzIHRoZSBKb3kgc3Rh
-Y2sgdHVwbGUNCiAgd29ybGQgLSB0aGUgSm95IGVudmlyb25tZW50DQoNCicnJw0KZnJvbSBqb3ku
-bGlicmFyeSBpbXBvcnQgKA0KICAgIERlZmluaXRpb25XcmFwcGVyLA0KICAgIEZ1bmN0aW9uV3Jh
-cHBlciwNCiAgICBTaW1wbGVGdW5jdGlvbldyYXBwZXIsDQogICAgKQ0KZnJvbSBqb3kudXRpbHMu
-c3RhY2sgaW1wb3J0IGxpc3RfdG9fc3RhY2ssIGNvbmNhdA0KZnJvbSBqb3kudnVpIGltcG9ydCBj
-b3JlLCB0ZXh0X3ZpZXdlciwgc3RhY2tfdmlld2VyDQoNCg0KZGVmIGluc3RhbGwoY29tbWFuZCk6
-IERbY29tbWFuZC5uYW1lXSA9IGNvbW1hbmQNCg0KDQpAaW5zdGFsbA0KQFNpbXBsZUZ1bmN0aW9u
-V3JhcHBlcg0KZGVmIGxpc3RfcmVzb3VyY2VzKHN0YWNrKToNCiAgICAnJycNCiAgICBQdXQgYSBz
-dHJpbmcgb24gdGhlIHN0YWNrIHdpdGggdGhlIG5hbWVzIG9mIGFsbCB0aGUga25vd24gcmVzb3Vy
-Y2VzDQogICAgb25lLXBlci1saW5lLg0KICAgICcnJw0KICAgIHJldHVybiAnXG4nLmpvaW4ocHQu
-c2NhbigpKSwgc3RhY2sNCg0KDQpAaW5zdGFsbA0KQFNpbXBsZUZ1bmN0aW9uV3JhcHBlcg0KZGVm
-IG9wZW5fc3RhY2soc3RhY2spOg0KICAgICcnJw0KICAgIEdpdmVuIGEgY29vcmRpbmF0ZSBwYWly
-IFt4IHldIChpbiBwaXhlbHMpIG9wZW4gYSBTdGFja1ZpZXdlciB0aGVyZS4NCiAgICAnJycNCiAg
-ICAoeCwgKHksIF8pKSwgc3RhY2sgPSBzdGFjaw0KICAgIFYgPSBkLm9wZW5fdmlld2VyKHgsIHks
-IHN0YWNrX3ZpZXdlci5TdGFja1ZpZXdlcikNCiAgICBWLmRyYXcoKQ0KICAgIHJldHVybiBzdGFj
-aw0KDQoNCkBpbnN0YWxsDQpAU2ltcGxlRnVuY3Rpb25XcmFwcGVyDQpkZWYgb3Blbl9yZXNvdXJj
-ZShzdGFjayk6DQogICAgJycnDQogICAgR2l2ZW4gYSBjb29yZGluYXRlIHBhaXIgW3ggeV0gKGlu
-IHBpeGVscykgYW5kIHRoZSBuYW1lIG9mIGEgcmVzb3VyY2UNCiAgICAoZnJvbSBsaXN0X3Jlc291
-cmNlcyBjb21tYW5kKSBvcGVuIGEgdmlld2VyIG9uIHRoYXQgcmVzb3VyY2UgYXQgdGhhdA0KICAg
-IGxvY2F0aW9uLg0KICAgICcnJw0KICAgICgoeCwgKHksIF8pKSwgKG5hbWUsIHN0YWNrKSkgPSBz
-dGFjaw0KICAgIG9tID0gY29yZS5PcGVuTWVzc2FnZSh3b3JsZCwgbmFtZSkNCiAgICBkLmJyb2Fk
-Y2FzdChvbSkNCiAgICBpZiBvbS5zdGF0dXMgPT0gY29yZS5TVUNDRVNTOg0KICAgICAgICBWID0g
-ZC5vcGVuX3ZpZXdlcih4LCB5LCB0ZXh0X3ZpZXdlci5UZXh0Vmlld2VyKQ0KICAgICAgICBWLmNv
-bnRlbnRfaWQsIFYubGluZXMgPSBvbS5jb250ZW50X2lkLCBvbS50aGluZw0KICAgICAgICBWLmRy
-YXcoKQ0KICAgIHJldHVybiBzdGFjaw0KDQoNCkBpbnN0YWxsDQpAU2ltcGxlRnVuY3Rpb25XcmFw
-cGVyDQpkZWYgbmFtZV92aWV3ZXIoc3RhY2spOg0KICAgICcnJw0KICAgIEdpdmVuIGEgc3RyaW5n
-IG5hbWUgb24gdGhlIHN0YWNrLCBpZiB0aGUgY3VycmVudGx5IGZvY3VzZWQgdmlld2VyIGlzDQog
-ICAgYW5vbnltb3VzLCBuYW1lIHRoZSB2aWV3ZXIgYW5kIHBlcnNpc3QgaXQgaW4gdGhlIHJlc291
-cmNlIHN0b3JlIHVuZGVyDQogICAgdGhhdCBuYW1lLg0KICAgICcnJw0KICAgIG5hbWUsIHN0YWNr
-ID0gc3RhY2sNCiAgICBhc3NlcnQgaXNpbnN0YW5jZShuYW1lLCBzdHIpLCByZXByKG5hbWUpDQog
-ICAgaWYgZC5mb2N1c2VkX3ZpZXdlciBhbmQgbm90IGQuZm9jdXNlZF92aWV3ZXIuY29udGVudF9p
-ZDoNCiAgICAgICAgZC5mb2N1c2VkX3ZpZXdlci5jb250ZW50X2lkID0gbmFtZQ0KICAgICAgICBw
-bSA9IGNvcmUuUGVyc2lzdE1lc3NhZ2Uod29ybGQsIG5hbWUsIHRoaW5nPWQuZm9jdXNlZF92aWV3
-ZXIubGluZXMpDQogICAgICAgIGQuYnJvYWRjYXN0KHBtKQ0KICAgICAgICBkLmZvY3VzZWRfdmll
-d2VyLmRyYXdfbWVudSgpDQogICAgcmV0dXJuIHN0YWNrDQoNCg0KIyNAaW5zdGFsbA0KIyNAU2lt
-cGxlRnVuY3Rpb25XcmFwcGVyDQojI2RlZiBwZXJzaXN0X3ZpZXdlcihzdGFjayk6DQojIyAgICBp
-ZiBzZWxmLmZvY3VzZWRfdmlld2VyOg0KIyMgICAgICAgIA0KIyMgICAgICAgIHNlbGYuZm9jdXNl
-ZF92aWV3ZXIuY29udGVudF9pZCA9IG5hbWUNCiMjICAgICAgICBzZWxmLmZvY3VzZWRfdmlld2Vy
-LmRyYXdfbWVudSgpDQojIyAgICByZXR1cm4gc3RhY2sNCg0KDQpAaW5zdGFsbA0KQFNpbXBsZUZ1
-bmN0aW9uV3JhcHBlcg0KZGVmIGluc2NyaWJlKHN0YWNrKToNCiAgICAnJycNCiAgICBDcmVhdGUg
-YSBuZXcgSm95IGZ1bmN0aW9uIGRlZmluaXRpb24gaW4gdGhlIEpveSBkaWN0aW9uYXJ5LiAgQQ0K
-ICAgIGRlZmluaXRpb24gaXMgZ2l2ZW4gYXMgYSBzdHJpbmcgd2l0aCBhIG5hbWUgZm9sbG93ZWQg
-YnkgYSBkb3VibGUNCiAgICBlcXVhbCBzaWduIHRoZW4gb25lIG9yIG1vcmUgSm95IGZ1bmN0aW9u
-cywgdGhlIGJvZHkuIGZvciBleGFtcGxlOg0KDQogICAgICAgIHNxciA9PSBkdXAgbXVsDQoNCiAg
-ICBJZiB5b3Ugd2FudCB0aGUgZGVmaW5pdGlvbiB0byBwZXJzaXN0IG92ZXIgcmVzdGFydHMsIGVu
-dGVyIGl0IGludG8NCiAgICB0aGUgZGVmaW5pdGlvbnMudHh0IHJlc291cmNlLg0KICAgICcnJw0K
-ICAgIGRlZmluaXRpb24sIHN0YWNrID0gc3RhY2sNCiAgICBEZWZpbml0aW9uV3JhcHBlci5hZGRf
-ZGVmKGRlZmluaXRpb24sIEQpDQogICAgcmV0dXJuIHN0YWNrDQoNCg0KQGluc3RhbGwNCkBTaW1w
-bGVGdW5jdGlvbldyYXBwZXINCmRlZiBvcGVuX3ZpZXdlcihzdGFjayk6DQogICAgJycnDQogICAg
-R2l2ZW4gYSBjb29yZGluYXRlIHBhaXIgW3ggeV0gKGluIHBpeGVscykgYW5kIGEgc3RyaW5nLCBv
-cGVuIGEgbmV3DQogICAgdW5uYW1lZCB2aWV3ZXIgb24gdGhhdCBzdHJpbmcgYXQgdGhhdCBsb2Nh
-dGlvbi4NCiAgICAnJycNCiAgICAoKHgsICh5LCBfKSksIChjb250ZW50LCBzdGFjaykpID0gc3Rh
-Y2sNCiAgICBWID0gZC5vcGVuX3ZpZXdlcih4LCB5LCB0ZXh0X3ZpZXdlci5UZXh0Vmlld2VyKQ0K
-ICAgIFYubGluZXMgPSBjb250ZW50LnNwbGl0bGluZXMoKQ0KICAgIFYuZHJhdygpDQogICAgcmV0
-dXJuIHN0YWNrDQoNCg0KQGluc3RhbGwNCkBTaW1wbGVGdW5jdGlvbldyYXBwZXINCmRlZiBnb29k
-X3ZpZXdlcl9sb2NhdGlvbihzdGFjayk6DQogICAgJycnDQogICAgTGVhdmUgYSBjb29yZGluYXRl
-IHBhaXIgW3ggeV0gKGluIHBpeGVscykgb24gdGhlIHN0YWNrIHRoYXQgd291bGQNCiAgICBiZSBh
-IGdvb2QgbG9jYXRpb24gYXQgd2hpY2ggdG8gb3BlbiBhIG5ldyB2aWV3ZXIuICAoVGhlIGhldXJp
-c3RpYw0KICAgIGVtcGxveWVkIGlzIHRvIHRha2UgdXAgdGhlIGJvdHRvbSBoYWxmIG9mIHRoZSBj
-dXJyZW50bHkgb3BlbiB2aWV3ZXINCiAgICB3aXRoIHRoZSBncmVhdGVzdCBhcmVhLikNCiAgICAn
-JycNCiAgICB2aWV3ZXJzID0gbGlzdChkLml0ZXJfdmlld2VycygpKQ0KICAgIGlmIHZpZXdlcnM6
-DQogICAgICAgIHZpZXdlcnMuc29ydChrZXk9bGFtYmRhIChWLCB4LCB5KTogVi53ICogVi5oKQ0K
-ICAgICAgICBWLCB4LCB5ID0gdmlld2Vyc1stMV0NCiAgICAgICAgY29vcmRzID0gKHggKyAxLCAo
-eSArIFYuaCAvIDIsICgpKSkNCiAgICBlbHNlOg0KICAgICAgICBjb29yZHMgPSAoMCwgKDAsICgp
-KSkNCiAgICByZXR1cm4gY29vcmRzLCBzdGFjaw0KDQoNCkBpbnN0YWxsDQpARnVuY3Rpb25XcmFw
-cGVyDQpkZWYgY21wXyhzdGFjaywgZXhwcmVzc2lvbiwgZGljdGlvbmFyeSk6DQogICAgJycnDQog
-ICAgVGhlIGNtcCBjb21iaW5hdG9yIHRha2VzIHR3byB2YWx1ZXMgYW5kIHRocmVlIHF1b3RlZCBw
-cm9ncmFtcyBvbiB0aGUNCiAgICBzdGFjayBhbmQgcnVucyBvbmUgb2YgdGhlIHRocmVlIGRlcGVu
-ZGluZyBvbiB0aGUgcmVzdWx0cyBvZiBjb21wYXJpbmcNCiAgICB0aGUgdHdvIHZhbHVlczoNCg0K
-ICAgICAgICAgICBhIGIgW0ddIFtFXSBbTF0gY21wDQogICAgICAgIC0tLS0tLS0tLS0tLS0tLS0t
-LS0tLS0tLS0gYSA+IGINCiAgICAgICAgICAgICAgICBHDQoNCiAgICAgICAgICAgYSBiIFtHXSBb
-RV0gW0xdIGNtcA0KICAgICAgICAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tIGEgPSBiDQogICAg
-ICAgICAgICAgICAgICAgIEUNCg0KICAgICAgICAgICBhIGIgW0ddIFtFXSBbTF0gY21wDQogICAg
-ICAgIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0gYSA8IGINCiAgICAgICAgICAgICAgICAgICAg
-ICAgIEwNCg0KICAgICcnJw0KICAgIEwsIChFLCAoRywgKGIsIChhLCBzdGFjaykpKSkgPSBzdGFj
-aw0KICAgIGV4cHJlc3Npb24gPSBjb25jYXQoRyBpZiBhID4gYiBlbHNlIEwgaWYgYSA8IGIgZWxz
-ZSBFLCBleHByZXNzaW9uKQ0KICAgIHJldHVybiBzdGFjaywgZXhwcmVzc2lvbiwgZGljdGlvbmFy
-eQ0KDQoNCkBpbnN0YWxsDQpAU2ltcGxlRnVuY3Rpb25XcmFwcGVyDQpkZWYgbGlzdF92aWV3ZXJz
-KHN0YWNrKToNCiAgICAnJycNCiAgICBQdXQgYSBzdHJpbmcgb24gdGhlIHN0YWNrIHdpdGggc29t
-ZSBpbmZvcm1hdGlvbiBhYm91dCB0aGUgY3VycmVudGx5DQogICAgb3BlbiB2aWV3ZXJzLCBvbmUt
-cGVyLWxpbmUuICBUaGlzIGlzIGtpbmQgb2YgYSBkZW1vIGZ1bmN0aW9uLCByYXRoZXINCiAgICB0
-aGFuIHNvbWV0aGluZyByZWFsbHkgdXNlZnVsLg0KICAgICcnJw0KICAgIGxpbmVzID0gW10NCiAg
-ICBmb3IgeCwgVCBpbiBkLnRyYWNrczoNCiAgICAgICAgI2xpbmVzLmFwcGVuZCgneDogJWksIHc6
-ICVpLCAlcicgJSAoeCwgVC53LCBUKSkNCiAgICAgICAgZm9yIHksIFYgaW4gVC52aWV3ZXJzOg0K
-ICAgICAgICAgICAgbGluZXMuYXBwZW5kKCd4OiAlaSB5OiAlaSBoOiAlaSAlciAlcicgJSAoeCwg
-eSwgVi5oLCBWLmNvbnRlbnRfaWQsIFYpKQ0KICAgIHJldHVybiAnXG4nLmpvaW4obGluZXMpLCBz
-dGFjaw0KDQoNCkBpbnN0YWxsDQpAU2ltcGxlRnVuY3Rpb25XcmFwcGVyDQpkZWYgc3BsaXRsaW5l
-cyhzdGFjayk6DQogICAgJycnDQogICAgR2l2ZW4gYSBzdHJpbmcgb24gdGhlIHN0YWNrIHJlcGxh
-Y2UgaXQgd2l0aCBhIGxpc3Qgb2YgdGhlIGxpbmVzIGluDQogICAgdGhlIHN0cmluZy4NCiAgICAn
-JycNCiAgICB0ZXh0LCBzdGFjayA9IHN0YWNrDQogICAgYXNzZXJ0IGlzaW5zdGFuY2UodGV4dCwg
-c3RyKSwgcmVwcih0ZXh0KQ0KICAgIHJldHVybiBsaXN0X3RvX3N0YWNrKHRleHQuc3BsaXRsaW5l
-cygpKSwgc3RhY2sNCg0KDQpAaW5zdGFsbA0KQFNpbXBsZUZ1bmN0aW9uV3JhcHBlcg0KZGVmIGhp
-eWEoc3RhY2spOg0KICAgICcnJw0KICAgIERlbW8gZnVuY3Rpb24gdG8gaW5zZXJ0ICJIaSBXb3Js
-ZCEiIGludG8gdGhlIGN1cnJlbnQgdmlld2VyLCBpZiBhbnkuDQogICAgJycnDQogICAgaWYgZC5m
-b2N1c2VkX3ZpZXdlcjoNCiAgICAgICAgZC5mb2N1c2VkX3ZpZXdlci5pbnNlcnQoJ0hpIFdvcmxk
-IScpDQogICAgcmV0dXJuIHN0YWNrDQpQSwMEFAAAAAAA5GZ4TkXs5NYLAAAACwAAAAcAAABsb2cu
-dHh0Sm95cHkgbG9nDQpQSwMEFAAAAAAA5GZ4Tmf2u80CBQAAAgUAAAgAAABtZW51LnR4dCAgbmFt
-ZV92aWV3ZXINCiAgbGlzdF9yZXNvdXJjZXMNCiAgb3Blbl9yZXNvdXJjZV9hdF9nb29kX2xvY2F0
-aW9uDQogIGdvb2Rfdmlld2VyX2xvY2F0aW9uDQogIG9wZW5fdmlld2VyDQogIHNlZV9zdGFjaw0K
-ICBzZWVfcmVzb3VyY2VzDQogIHNlZV9kZWZpbml0aW9ucw0KICBzZWVfbG9nDQogIHJlc2V0X2xv
-Zw0KDQogIGluc2NyaWJlDQogIGV2YWx1YXRlDQoNCiAgcG9wIGNsZWFyICAgIGR1cCBzd2FwDQoN
-CiAgYWRkIHN1YiBtdWwgZGl2IHRydWVkaXYgbW9kdWx1cyBkaXZtb2QNCiAgcG0gKysgLS0gc3Vt
-IHByb2R1Y3QgcG93IHNxciBzcXJ0DQogIDwgPD0gPSA+PSA+IDw+DQogICYgPDwgPj4NCg0KICBp
-IGR1cGRpcA0KDQohPSAlICYgKiAqZnJhY3Rpb24gKmZyYWN0aW9uMCArICsrIC0gLS0gLyA8IDw8
-IDw9IDw+ID0gPiA+PSA+PiA/IF4NCmFicyBhZGQgYW5hbW9ycGhpc20gYW5kIGFwcDEgYXBwMiBh
-cHAzIGF0IGF2ZXJhZ2UNCmIgYmluYXJ5IGJyYW5jaA0KY2hvaWNlIGNsZWFyIGNsZWF2ZSBjb25j
-YXQgY29ucw0KZGluZnJpcnN0IGRpcCBkaXBkIGRpcGRkIGRpc2Vuc3RhY2tlbiBkaXYgZGl2bW9k
-IGRvd25fdG9femVybyBkcm9wDQpkdWRpcGQgZHVwIGR1cGQgZHVwZGlwDQplbnN0YWNrZW4gZXEN
-CmZpcnN0IGZsYXR0ZW4gZmxvb3IgZmxvb3JkaXYNCmdjZCBnZSBnZW5yZWMgZ2V0aXRlbSBncmFu
-ZF9yZXNldCBndA0KaGVscA0KaSBpZCBpZnRlIGluZnJhIGluc2NyaWJlDQprZXlfYmluZGluZ3MN
-CmxlIGxlYXN0X2ZyYWN0aW9uIGxvb3AgbHNoaWZ0IGx0DQptYXAgbWF4IG1pbiBtb2QgbW9kdWx1
-cyBtb3VzZV9iaW5kaW5ncyBtdWwNCm5lIG5lZyBub3QgbnVsbGFyeQ0Kb2Ygb3Igb3Zlcg0KcGFt
-IHBhcnNlIHBpY2sgcG0gcG9wIHBvcGQgcG9wZGQgcG9wb3AgcG93IHByZWQgcHJpbXJlYyBwcm9k
-dWN0DQpxdW90ZWQNCnJhbmdlIHJhbmdlX3RvX3plcm8gcmVtIHJlbWFpbmRlciByZW1vdmUgcmVz
-ZXRfbG9nIHJlc3QgcmV2ZXJzZQ0Kcm9sbDwgcm9sbD4gcm9sbGRvd24gcm9sbHVwIHJzaGlmdCBy
-dW4NCnNlY29uZCBzZWxlY3Qgc2hhcmluZyBzaG93X2xvZyBzaHVudCBzaXplIHNvcnQgc3FyIHNx
-cnQgc3RhY2sgc3RlcA0Kc3RlcF96ZXJvIHN1YiBzdWNjIHN1bSBzd2FhY2sgc3dhcCBzd29uY2F0
-IHN3b25zDQp0YWtlIHRlcm5hcnkgdGhpcmQgdGltZXMgdHJ1ZWRpdiB0cnV0aHkgdHVjaw0KdW5h
-cnkgdW5jb25zIHVuaXF1ZSB1bml0IHVucXVvdGVkIHVuc3RhY2sNCnZvaWQNCndhcnJhbnR5IHdo
-aWxlIHdvcmRzDQp4IHhvcg0KemlwDQpQSwMEFAAAAAAA5GZ4TgCrPcaOEAAAjhAAAAsAAABzY3Jh
-dGNoLnR4dFdoYXQgaXMgaXQ/DQoNCkEgc2ltcGxlIEdyYXBoaWNhbCBVc2VyIEludGVyZmFjZSBm
-b3IgdGhlIEpveSBwcm9ncmFtbWluZyBsYW5ndWFnZSwNCndyaXR0ZW4gdXNpbmcgUHlnYW1lIHRv
-IGJ5cGFzcyBYMTEgZXQuIGFsLiwgbW9kZWxlZCBvbiB0aGUgT2Jlcm9uIE9TLCBhbmQNCmludGVu
-ZGVkIHRvIGJlIGp1c3QgZnVuY3Rpb25hbCBlbm91Z2ggdG8gc3VwcG9ydCBib290c3RyYXBwaW5n
-IGZ1cnRoZXIgSm95DQpkZXZlbG9wbWVudC4NCg0KSXQncyBiYXNpYyBmdW5jdGlvbmFsaXR5IGlz
-IG1vcmUtb3ItbGVzcyBhcyBhIGNydWRlIHRleHQgZWRpdG9yIGFsb25nIHdpdGgNCmEgc2ltcGxl
-IEpveSBydW50aW1lIChpbnRlcnByZXRlciwgc3RhY2ssIGFuZCBkaWN0aW9uYXJ5LikgIEl0IGF1
-dG8tIHNhdmVzDQphbnkgbmFtZWQgZmlsZXMgKGluIGEgdmVyc2lvbmVkIGhvbWUgZGlyZWN0b3J5
-KSBhbmQgeW91IGNhbiB3cml0ZSBuZXcgSm95DQpwcmltaXRpdmVzIGluIFB5dGhvbiBhbmQgSm95
-IGRlZmluaXRpb25zIGFuZCBpbW1lZGlhdGVseSBpbnN0YWxsIGFuZCB1c2UNCnRoZW0sIGFzIHdl
-bGwgYXMgcmVjb3JkaW5nIHRoZW0gZm9yIHJldXNlIChhZnRlciByZXN0YXJ0cy4pDQoNCkN1cnJl
-bnRseSwgdGhlcmUgYXJlIG9ubHkgdHdvIGtpbmRzIG9mIChpbnRlcmVzdGluZykgdmlld2Vyczog
-VGV4dFZpZXdlcnMNCmFuZCBTdGFja1ZpZXdlci4gVGhlIFRleHRWaWV3ZXJzIGFyZSBjcnVkZSB0
-ZXh0IGVkaXRvcnMuICBUaGV5IHByb3ZpZGUNCmp1c3QgZW5vdWdoIGZ1bmN0aW9uYWxpdHkgdG8g
-bGV0IHRoZSB1c2VyIHdyaXRlIHRleHQgYW5kIGNvZGUgKFB5dGhvbiBhbmQNCkpveSkgYW5kIGV4
-ZWN1dGUgSm95IGZ1bmN0aW9ucy4gIE9uZSBpbXBvcnRhbnQgdGhpbmcgdGhleSBkbyBpcw0KYXV0
-b21hdGljYWxseSBzYXZlIHRoZWlyIGNvbnRlbnQgYWZ0ZXIgY2hhbmdlcy4gIE5vIG1vcmUgbG9z
-dCB3b3JrLg0KDQpUaGUgU3RhY2tWaWV3ZXIgaXMgYSBzcGVjaWFsaXplZCBUZXh0Vmlld2VyIHRo
-YXQgc2hvd3MgdGhlIGNvbnRlbnRzIG9mIHRoZQ0KSm95IHN0YWNrIG9uZSBsaW5lIHBlciBzdGFj
-ayBpdGVtLiAgSXQncyBhIHZlcnkgaGFuZHkgdmlzdWFsIGFpZCB0byBrZWVwDQp0cmFjayBvZiB3
-aGF0J3MgZ29pbmcgb24uICBUaGVyZSdzIGFsc28gYSBsb2cudHh0IGZpbGUgdGhhdCBnZXRzIHdy
-aXR0ZW4NCnRvIHdoZW4gY29tbWFuZHMgYXJlIGV4ZWN1dGVkLCBhbmQgc28gcmVjb3JkcyB0aGUg
-bG9nIG9mIHVzZXIgYWN0aW9ucyBhbmQNCnN5c3RlbSBldmVudHMuICBJdCB0ZW5kcyB0byBmaWxs
-IHVwIHF1aWNrbHkgc28gdGhlcmUncyBhIHJlc2V0X2xvZyBjb21tYW5kDQp0aGF0IGNsZWFycyBp
-dCBvdXQuDQoNClZpZXdlcnMgaGF2ZSAiZ3JvdyIgYW5kICJjbG9zZSIgaW4gdGhlaXIgbWVudSBi
-YXJzLiAgVGhlc2UgYXJlIGJ1dHRvbnMuDQpXaGVuIHlvdSByaWdodC1jbGljayBvbiBncm93IGEg
-dmlld2VyIGEgY29weSBpcyBjcmVhdGVkIHRoYXQgY292ZXJzIHRoYXQNCnZpZXdlcidzIGVudGly
-ZSB0cmFjay4gIElmIHlvdSBncm93IGEgdmlld2VyIHRoYXQgYWxyZWFkeSB0YWtlcyB1cCBpdHMN
-Cndob2xlIHRyYWNrIHRoZW4gYSBjb3B5IGlzIGNyZWF0ZWQgdGhhdCB0YWtlcyB1cCBhbiBhZGRp
-dGlvbmFsIHRyYWNrLCB1cA0KdG8gdGhlIHdob2xlIHNjcmVlbi4gIENsb3NpbmcgYSB2aWV3ZXIg
-anVzdCBkZWxldGVzIHRoYXQgdmlld2VyLCBhbmQgd2hlbg0KYSB0cmFjayBoYXMgbm8gbW9yZSB2
-aWV3ZXJzLCBpdCBpcyBkZWxldGVkIGFuZCB0aGF0IGV4cG9zZXMgYW55IHByZXZpb3VzDQp0cmFj
-a3MgYW5kIHZpZXdlcnMgdGhhdCB3ZXJlIGhpZGRlbi4NCg0KKE5vdGU6IGlmIHlvdSBldmVyIGNs
-b3NlIGFsbCB0aGUgdmlld2VycyBhbmQgYXJlIHNpdHRpbmcgYXQgYSBibGFuayBzY3JlZW4NCndp
-dGggIG5vd2hlcmUgdG8gdHlwZSBhbmQgZXhlY3V0ZSBjb21tYW5kcywgcHJlc3MgdGhlIFBhdXNl
-L0JyZWFrIGtleS4NClRoaXMgd2lsbCBvcGVuIGEgbmV3ICJ0cmFwIiB2aWV3ZXIgd2hpY2ggeW91
-IGNhbiB0aGVuIHVzZSB0byByZWNvdmVyLikNCg0KQ29waWVzIG9mIGEgdmlld2VyIGFsbCBzaGFy
-ZSB0aGUgc2FtZSBtb2RlbCBhbmQgdXBkYXRlIHRoZWlyIGRpc3BsYXkgYXMgaXQNCmNoYW5nZXMu
-IChJZiB5b3UgaGF2ZSB0d28gdmlld2VycyBvcGVuIG9uIHRoZSBzYW1lIG5hbWVkIHJlc291cmNl
-IGFuZCBlZGl0DQpvbmUgeW91J2xsIHNlZSB0aGUgb3RoZXIgdXBkYXRlIGFzIHlvdSB0eXBlLikN
-Cg0KVUkgR3VpZGUNCg0KbGVmdCBtb3VzZSBzZXRzIGN1cnNvciBpbiB0ZXh0LCBpbiBtZW51IGJh
-ciByZXNpemVzIHZpZXdlciBpbnRlcmFjdGl2ZWx5DQoodGhpcyBpcyBhIGxpdHRsZSBidWdneSBp
-biB0aGF0IHlvdSBjYW4gbW92ZSB0aGUgbW91c2UgcXVpY2tseSBhbmQgZ2V0DQpvdXRzaWRlIHRo
-ZSBtZW51LCBsZWF2aW5nIHRoZSB2aWV3ZXIgaW4gdGhlICJyZXNpemluZyIgc3RhdGUuIFVudGls
-IEkgZml4DQp0aGlzLCB0aGUgd29ya2Fyb3VuZCBpcyB0byBqdXN0IGdyYWIgdGhlIG1lbnUgYmFy
-IGFnYWluIGFuZCB3aWdnbGUgaXQgYQ0KZmV3IHBpeGVscyBhbmQgbGV0IGdvLiAgVGhpcyB3aWxs
-IHJlc2V0IHRoZSBtYWNoaW5lcnkuKQ0KDQpSaWdodCBtb3VzZSBleGVjdXRlcyBKb3kgY29tbWFu
-ZCAoZnVuY3Rpb25zKSwgYW5kIHlvdSBjYW4gZHJhZyB3aXRoIHRoZQ0KcmlnaHQgYnV0dG9uIHRv
-IGhpZ2hsaWdodCAod2VsbCwgdW5kZXJsaW5lKSBjb21tYW5kcy4gIFdvcmRzIHRoYXQgYXJlbid0
-DQpuYW1lcyBvZiBKb3kgY29tbWFuZHMgd29uJ3QgYmUgdW5kZXJsaW5lZC4gIFJlbGVhc2UgdGhl
-IGJ1dHRvbiB0byBleGVjdXRlDQp0aGUgY29tbWFuZC4NCg0KVGhlIG1pZGRsZSBtb3VzZSBidXR0
-b24gKHVzdWFsbHkgYSB3aGVlbCB0aGVzZSBkYXlzKSBzY3JvbGxzIHRoZSB0ZXh0IGJ1dA0KeW91
-IGNhbiBhbHNvIGNsaWNrIGFuZCBkcmFnIGFueSB2aWV3ZXIgd2l0aCBpdCB0byBtb3ZlIHRoYXQg
-dmlld2VyIHRvDQphbm90aGVyIHRyYWNrIG9yIHRvIGEgZGlmZmVyZW50IGxvY2F0aW9uIGluIHRo
-ZSBzYW1lIHRyYWNrLiAgVGhlcmUncyBubw0KZGlyZWN0IHZpc3VhbCBmZWVkYmFjayBmb3IgdGhp
-cyAoeWV0KSBidXQgdGhhdCBkb3Nlbid0IHNlZW0gdG8gaW1wYWlyIGl0cw0KdXNlZnVsbmVzcy4N
-Cg0KRjEsIEYyIC0gc2V0IHNlbGVjdGlvbiBiZWdpbiBhbmQgZW5kIG1hcmtlcnMgKGNydWRlIGJ1
-dCB1c2FibGUuKQ0KDQpGMyAtIGNvcHkgc2VsZWN0ZWQgdGV4dCB0byB0aGUgdG9wIG9mIHRoZSBz
-dGFjay4NCg0KU2hpZnQtRjMgLSBhcyBjb3B5IHRoZW4gcnVuICJwYXJzZSIgY29tbWFuZCBvbiB0
-aGUgc3RyaW5nLg0KDQpGNCAtIGN1dCBzZWxlY3RlZCB0ZXh0IHRvIHRoZSB0b3Agb2YgdGhlIHN0
-YWNrLg0KDQpTaGlmdC1GNCAtIGFzIGN1dCB0aGVuIHJ1biAicG9wIiAoZGVsZXRlIHNlbGVjdGlv
-bi4pDQoNCkpveQ0KDQpQcmV0dHkgbXVjaCBhbGwgb2YgdGhlIHJlc3Qgb2YgdGhlIGZ1bmN0aW9u
-YWxpdHkgb2YgdGhlIHN5c3RlbSBpcyBwcm92aWRlZA0KYnkgZXhlY3V0aW5nIEpveSBjb21tYW5k
-cyAoYWthIGZ1bmN0aW9ucywgYWthICJ3b3JkcyIgaW4gRm9ydGgpIGJ5IHJpZ2h0LQ0KY2xpY2tp
-bmcgb24gdGhlaXIgbmFtZXMgaW4gYW55IHRleHQuDQoNClRvIGdldCBoZWxwIG9uIGEgSm95IGZ1
-bmN0aW9uIHNlbGVjdCB0aGUgbmFtZSBvZiB0aGUgZnVuY3Rpb24gaW4gYQ0KVGV4dFZpZXdlciB1
-c2luZyBGMSBhbmQgRjIsIHRoZW4gcHJlc3Mgc2hpZnQtRjMgdG8gcGFyc2UgdGhlIHNlbGVjdGlv
-bi4NClRoZSBmdW5jdGlvbiAocmVhbGx5IGl0cyBTeW1ib2wpIHdpbGwgYXBwZWFyIG9uIHRoZSBz
-dGFjayBpbiBicmFja2V0cyAoYQ0KInF1b3RlZCBwcm9ncmFtIiBzdWNoIGFzICJbcG9wXSIuKSAg
-VGhlbiByaWdodC1jbGljayBvbiB0aGUgd29yZCBoZWxwIGluDQphbnkgVGV4dFZpZXdlciAoaWYg
-aXQncyBub3QgYWxyZWFkeSB0aGVyZSwganVzdCB0eXBlIGl0IGluIHNvbWV3aGVyZS4pDQpUaGlz
-IHdpbGwgcHJpbnQgdGhlIGRvY3N0cmluZyBvciBkZWZpbml0aW9uIG9mIHRoZSB3b3JkIChmdW5j
-dGlvbikgdG8NCnN0ZG91dC4gIEF0IHNvbWUgcG9pbnQgSSdsbCB3cml0ZSBhIHRoaW5nIHRvIHNl
-bmQgdGhhdCB0byB0aGUgbG9nLnR4dCBmaWxlDQppbnN0ZWFkLCBidXQgZm9yIG5vdyBsb29rIGZv
-ciBvdXRwdXQgaW4gdGhlIHRlcm1pbmFsLg0KUEsDBBQAAAAAAORmeE53f5peAwAAAAMAAAAMAAAA
-c3RhY2sucGlja2xlKHQuUEsBAhQAFAAAAAAA5GZ4Tv3gGVF+AwAAfgMAAA8AAAAAAAAAAAAAALaB
-AAAAAGRlZmluaXRpb25zLnR4dFBLAQIUABQAAAAAACFrpk7/HHjxGBYAABgWAAAKAAAAAAAAAAAA
-AAC2gasDAABsaWJyYXJ5LnB5UEsBAhQAFAAAAAAA5GZ4TkXs5NYLAAAACwAAAAcAAAAAAAAAAAAA
-ALaB6xkAAGxvZy50eHRQSwECFAAUAAAAAADkZnhOZ/a7zQIFAAACBQAACAAAAAAAAAAAAAAAtoEb
-GgAAbWVudS50eHRQSwECFAAUAAAAAADkZnhOAKs9xo4QAACOEAAACwAAAAAAAAAAAAAAtoFDHwAA
-c2NyYXRjaC50eHRQSwECFAAUAAAAAADkZnhOd3+aXgMAAAADAAAADAAAAAAAAAAAAAAAtoH6LwAA
-c3RhY2sucGlja2xlUEsFBgAAAAAGAAYAUwEAACcwAAAAAA==''')))
-
-
-if __name__ == '__main__':
- print create_data()
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2019 Simon Forman
+#
+# This file is part of Thun
+#
+# Thun 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.
+#
+# Thun 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 Thun. If not see .
+#
+'''
+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)
+
+'''
+from __future__ import print_function
+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
+IFtdIFtfQlRyZWUtcmVjdXJdIGdlbnJlYw0KUEsDBBQAAAAAACFrpk7/HHjxGBYAABgWAAAKAAAA
+bGlicmFyeS5weScnJw0KVGhpcyBmaWxlIGlzIGV4ZWNmaWxlKCknZCB3aXRoIGEgbmFtZXNwYWNl
+IGNvbnRhaW5pbmc6DQoNCiAgRCAtIHRoZSBKb3kgZGljdGlvbmFyeQ0KICBkIC0gdGhlIERpc3Bs
+YXkgb2JqZWN0DQogIHB0IC0gdGhlIFBlcnNpc3RUYXNrIG9iamVjdA0KICBsb2cgLSB0aGUgbG9n
+LnR4dCB2aWV3ZXINCiAgbG9vcCAtIHRoZSBUaGVMb29wIG1haW4gbG9vcCBvYmplY3QNCiAgc3Rh
+Y2tfaG9sZGVyIC0gdGhlIFB5dGhvbiBsaXN0IG9iamVjdCB0aGF0IGhvbGRzIHRoZSBKb3kgc3Rh
+Y2sgdHVwbGUNCiAgd29ybGQgLSB0aGUgSm95IGVudmlyb25tZW50DQoNCicnJw0KZnJvbSBqb3ku
+bGlicmFyeSBpbXBvcnQgKA0KICAgIERlZmluaXRpb25XcmFwcGVyLA0KICAgIEZ1bmN0aW9uV3Jh
+cHBlciwNCiAgICBTaW1wbGVGdW5jdGlvbldyYXBwZXIsDQogICAgKQ0KZnJvbSBqb3kudXRpbHMu
+c3RhY2sgaW1wb3J0IGxpc3RfdG9fc3RhY2ssIGNvbmNhdA0KZnJvbSBqb3kudnVpIGltcG9ydCBj
+b3JlLCB0ZXh0X3ZpZXdlciwgc3RhY2tfdmlld2VyDQoNCg0KZGVmIGluc3RhbGwoY29tbWFuZCk6
+IERbY29tbWFuZC5uYW1lXSA9IGNvbW1hbmQNCg0KDQpAaW5zdGFsbA0KQFNpbXBsZUZ1bmN0aW9u
+V3JhcHBlcg0KZGVmIGxpc3RfcmVzb3VyY2VzKHN0YWNrKToNCiAgICAnJycNCiAgICBQdXQgYSBz
+dHJpbmcgb24gdGhlIHN0YWNrIHdpdGggdGhlIG5hbWVzIG9mIGFsbCB0aGUga25vd24gcmVzb3Vy
+Y2VzDQogICAgb25lLXBlci1saW5lLg0KICAgICcnJw0KICAgIHJldHVybiAnXG4nLmpvaW4ocHQu
+c2NhbigpKSwgc3RhY2sNCg0KDQpAaW5zdGFsbA0KQFNpbXBsZUZ1bmN0aW9uV3JhcHBlcg0KZGVm
+IG9wZW5fc3RhY2soc3RhY2spOg0KICAgICcnJw0KICAgIEdpdmVuIGEgY29vcmRpbmF0ZSBwYWly
+IFt4IHldIChpbiBwaXhlbHMpIG9wZW4gYSBTdGFja1ZpZXdlciB0aGVyZS4NCiAgICAnJycNCiAg
+ICAoeCwgKHksIF8pKSwgc3RhY2sgPSBzdGFjaw0KICAgIFYgPSBkLm9wZW5fdmlld2VyKHgsIHks
+IHN0YWNrX3ZpZXdlci5TdGFja1ZpZXdlcikNCiAgICBWLmRyYXcoKQ0KICAgIHJldHVybiBzdGFj
+aw0KDQoNCkBpbnN0YWxsDQpAU2ltcGxlRnVuY3Rpb25XcmFwcGVyDQpkZWYgb3Blbl9yZXNvdXJj
+ZShzdGFjayk6DQogICAgJycnDQogICAgR2l2ZW4gYSBjb29yZGluYXRlIHBhaXIgW3ggeV0gKGlu
+IHBpeGVscykgYW5kIHRoZSBuYW1lIG9mIGEgcmVzb3VyY2UNCiAgICAoZnJvbSBsaXN0X3Jlc291
+cmNlcyBjb21tYW5kKSBvcGVuIGEgdmlld2VyIG9uIHRoYXQgcmVzb3VyY2UgYXQgdGhhdA0KICAg
+IGxvY2F0aW9uLg0KICAgICcnJw0KICAgICgoeCwgKHksIF8pKSwgKG5hbWUsIHN0YWNrKSkgPSBz
+dGFjaw0KICAgIG9tID0gY29yZS5PcGVuTWVzc2FnZSh3b3JsZCwgbmFtZSkNCiAgICBkLmJyb2Fk
+Y2FzdChvbSkNCiAgICBpZiBvbS5zdGF0dXMgPT0gY29yZS5TVUNDRVNTOg0KICAgICAgICBWID0g
+ZC5vcGVuX3ZpZXdlcih4LCB5LCB0ZXh0X3ZpZXdlci5UZXh0Vmlld2VyKQ0KICAgICAgICBWLmNv
+bnRlbnRfaWQsIFYubGluZXMgPSBvbS5jb250ZW50X2lkLCBvbS50aGluZw0KICAgICAgICBWLmRy
+YXcoKQ0KICAgIHJldHVybiBzdGFjaw0KDQoNCkBpbnN0YWxsDQpAU2ltcGxlRnVuY3Rpb25XcmFw
+cGVyDQpkZWYgbmFtZV92aWV3ZXIoc3RhY2spOg0KICAgICcnJw0KICAgIEdpdmVuIGEgc3RyaW5n
+IG5hbWUgb24gdGhlIHN0YWNrLCBpZiB0aGUgY3VycmVudGx5IGZvY3VzZWQgdmlld2VyIGlzDQog
+ICAgYW5vbnltb3VzLCBuYW1lIHRoZSB2aWV3ZXIgYW5kIHBlcnNpc3QgaXQgaW4gdGhlIHJlc291
+cmNlIHN0b3JlIHVuZGVyDQogICAgdGhhdCBuYW1lLg0KICAgICcnJw0KICAgIG5hbWUsIHN0YWNr
+ID0gc3RhY2sNCiAgICBhc3NlcnQgaXNpbnN0YW5jZShuYW1lLCBzdHIpLCByZXByKG5hbWUpDQog
+ICAgaWYgZC5mb2N1c2VkX3ZpZXdlciBhbmQgbm90IGQuZm9jdXNlZF92aWV3ZXIuY29udGVudF9p
+ZDoNCiAgICAgICAgZC5mb2N1c2VkX3ZpZXdlci5jb250ZW50X2lkID0gbmFtZQ0KICAgICAgICBw
+bSA9IGNvcmUuUGVyc2lzdE1lc3NhZ2Uod29ybGQsIG5hbWUsIHRoaW5nPWQuZm9jdXNlZF92aWV3
+ZXIubGluZXMpDQogICAgICAgIGQuYnJvYWRjYXN0KHBtKQ0KICAgICAgICBkLmZvY3VzZWRfdmll
+d2VyLmRyYXdfbWVudSgpDQogICAgcmV0dXJuIHN0YWNrDQoNCg0KIyNAaW5zdGFsbA0KIyNAU2lt
+cGxlRnVuY3Rpb25XcmFwcGVyDQojI2RlZiBwZXJzaXN0X3ZpZXdlcihzdGFjayk6DQojIyAgICBp
+ZiBzZWxmLmZvY3VzZWRfdmlld2VyOg0KIyMgICAgICAgIA0KIyMgICAgICAgIHNlbGYuZm9jdXNl
+ZF92aWV3ZXIuY29udGVudF9pZCA9IG5hbWUNCiMjICAgICAgICBzZWxmLmZvY3VzZWRfdmlld2Vy
+LmRyYXdfbWVudSgpDQojIyAgICByZXR1cm4gc3RhY2sNCg0KDQpAaW5zdGFsbA0KQFNpbXBsZUZ1
+bmN0aW9uV3JhcHBlcg0KZGVmIGluc2NyaWJlKHN0YWNrKToNCiAgICAnJycNCiAgICBDcmVhdGUg
+YSBuZXcgSm95IGZ1bmN0aW9uIGRlZmluaXRpb24gaW4gdGhlIEpveSBkaWN0aW9uYXJ5LiAgQQ0K
+ICAgIGRlZmluaXRpb24gaXMgZ2l2ZW4gYXMgYSBzdHJpbmcgd2l0aCBhIG5hbWUgZm9sbG93ZWQg
+YnkgYSBkb3VibGUNCiAgICBlcXVhbCBzaWduIHRoZW4gb25lIG9yIG1vcmUgSm95IGZ1bmN0aW9u
+cywgdGhlIGJvZHkuIGZvciBleGFtcGxlOg0KDQogICAgICAgIHNxciA9PSBkdXAgbXVsDQoNCiAg
+ICBJZiB5b3Ugd2FudCB0aGUgZGVmaW5pdGlvbiB0byBwZXJzaXN0IG92ZXIgcmVzdGFydHMsIGVu
+dGVyIGl0IGludG8NCiAgICB0aGUgZGVmaW5pdGlvbnMudHh0IHJlc291cmNlLg0KICAgICcnJw0K
+ICAgIGRlZmluaXRpb24sIHN0YWNrID0gc3RhY2sNCiAgICBEZWZpbml0aW9uV3JhcHBlci5hZGRf
+ZGVmKGRlZmluaXRpb24sIEQpDQogICAgcmV0dXJuIHN0YWNrDQoNCg0KQGluc3RhbGwNCkBTaW1w
+bGVGdW5jdGlvbldyYXBwZXINCmRlZiBvcGVuX3ZpZXdlcihzdGFjayk6DQogICAgJycnDQogICAg
+R2l2ZW4gYSBjb29yZGluYXRlIHBhaXIgW3ggeV0gKGluIHBpeGVscykgYW5kIGEgc3RyaW5nLCBv
+cGVuIGEgbmV3DQogICAgdW5uYW1lZCB2aWV3ZXIgb24gdGhhdCBzdHJpbmcgYXQgdGhhdCBsb2Nh
+dGlvbi4NCiAgICAnJycNCiAgICAoKHgsICh5LCBfKSksIChjb250ZW50LCBzdGFjaykpID0gc3Rh
+Y2sNCiAgICBWID0gZC5vcGVuX3ZpZXdlcih4LCB5LCB0ZXh0X3ZpZXdlci5UZXh0Vmlld2VyKQ0K
+ICAgIFYubGluZXMgPSBjb250ZW50LnNwbGl0bGluZXMoKQ0KICAgIFYuZHJhdygpDQogICAgcmV0
+dXJuIHN0YWNrDQoNCg0KQGluc3RhbGwNCkBTaW1wbGVGdW5jdGlvbldyYXBwZXINCmRlZiBnb29k
+X3ZpZXdlcl9sb2NhdGlvbihzdGFjayk6DQogICAgJycnDQogICAgTGVhdmUgYSBjb29yZGluYXRl
+IHBhaXIgW3ggeV0gKGluIHBpeGVscykgb24gdGhlIHN0YWNrIHRoYXQgd291bGQNCiAgICBiZSBh
+IGdvb2QgbG9jYXRpb24gYXQgd2hpY2ggdG8gb3BlbiBhIG5ldyB2aWV3ZXIuICAoVGhlIGhldXJp
+c3RpYw0KICAgIGVtcGxveWVkIGlzIHRvIHRha2UgdXAgdGhlIGJvdHRvbSBoYWxmIG9mIHRoZSBj
+dXJyZW50bHkgb3BlbiB2aWV3ZXINCiAgICB3aXRoIHRoZSBncmVhdGVzdCBhcmVhLikNCiAgICAn
+JycNCiAgICB2aWV3ZXJzID0gbGlzdChkLml0ZXJfdmlld2VycygpKQ0KICAgIGlmIHZpZXdlcnM6
+DQogICAgICAgIHZpZXdlcnMuc29ydChrZXk9bGFtYmRhIChWLCB4LCB5KTogVi53ICogVi5oKQ0K
+ICAgICAgICBWLCB4LCB5ID0gdmlld2Vyc1stMV0NCiAgICAgICAgY29vcmRzID0gKHggKyAxLCAo
+eSArIFYuaCAvIDIsICgpKSkNCiAgICBlbHNlOg0KICAgICAgICBjb29yZHMgPSAoMCwgKDAsICgp
+KSkNCiAgICByZXR1cm4gY29vcmRzLCBzdGFjaw0KDQoNCkBpbnN0YWxsDQpARnVuY3Rpb25XcmFw
+cGVyDQpkZWYgY21wXyhzdGFjaywgZXhwcmVzc2lvbiwgZGljdGlvbmFyeSk6DQogICAgJycnDQog
+ICAgVGhlIGNtcCBjb21iaW5hdG9yIHRha2VzIHR3byB2YWx1ZXMgYW5kIHRocmVlIHF1b3RlZCBw
+cm9ncmFtcyBvbiB0aGUNCiAgICBzdGFjayBhbmQgcnVucyBvbmUgb2YgdGhlIHRocmVlIGRlcGVu
+ZGluZyBvbiB0aGUgcmVzdWx0cyBvZiBjb21wYXJpbmcNCiAgICB0aGUgdHdvIHZhbHVlczoNCg0K
+ICAgICAgICAgICBhIGIgW0ddIFtFXSBbTF0gY21wDQogICAgICAgIC0tLS0tLS0tLS0tLS0tLS0t
+LS0tLS0tLS0gYSA+IGINCiAgICAgICAgICAgICAgICBHDQoNCiAgICAgICAgICAgYSBiIFtHXSBb
+RV0gW0xdIGNtcA0KICAgICAgICAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tIGEgPSBiDQogICAg
+ICAgICAgICAgICAgICAgIEUNCg0KICAgICAgICAgICBhIGIgW0ddIFtFXSBbTF0gY21wDQogICAg
+ICAgIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0gYSA8IGINCiAgICAgICAgICAgICAgICAgICAg
+ICAgIEwNCg0KICAgICcnJw0KICAgIEwsIChFLCAoRywgKGIsIChhLCBzdGFjaykpKSkgPSBzdGFj
+aw0KICAgIGV4cHJlc3Npb24gPSBjb25jYXQoRyBpZiBhID4gYiBlbHNlIEwgaWYgYSA8IGIgZWxz
+ZSBFLCBleHByZXNzaW9uKQ0KICAgIHJldHVybiBzdGFjaywgZXhwcmVzc2lvbiwgZGljdGlvbmFy
+eQ0KDQoNCkBpbnN0YWxsDQpAU2ltcGxlRnVuY3Rpb25XcmFwcGVyDQpkZWYgbGlzdF92aWV3ZXJz
+KHN0YWNrKToNCiAgICAnJycNCiAgICBQdXQgYSBzdHJpbmcgb24gdGhlIHN0YWNrIHdpdGggc29t
+ZSBpbmZvcm1hdGlvbiBhYm91dCB0aGUgY3VycmVudGx5DQogICAgb3BlbiB2aWV3ZXJzLCBvbmUt
+cGVyLWxpbmUuICBUaGlzIGlzIGtpbmQgb2YgYSBkZW1vIGZ1bmN0aW9uLCByYXRoZXINCiAgICB0
+aGFuIHNvbWV0aGluZyByZWFsbHkgdXNlZnVsLg0KICAgICcnJw0KICAgIGxpbmVzID0gW10NCiAg
+ICBmb3IgeCwgVCBpbiBkLnRyYWNrczoNCiAgICAgICAgI2xpbmVzLmFwcGVuZCgneDogJWksIHc6
+ICVpLCAlcicgJSAoeCwgVC53LCBUKSkNCiAgICAgICAgZm9yIHksIFYgaW4gVC52aWV3ZXJzOg0K
+ICAgICAgICAgICAgbGluZXMuYXBwZW5kKCd4OiAlaSB5OiAlaSBoOiAlaSAlciAlcicgJSAoeCwg
+eSwgVi5oLCBWLmNvbnRlbnRfaWQsIFYpKQ0KICAgIHJldHVybiAnXG4nLmpvaW4obGluZXMpLCBz
+dGFjaw0KDQoNCkBpbnN0YWxsDQpAU2ltcGxlRnVuY3Rpb25XcmFwcGVyDQpkZWYgc3BsaXRsaW5l
+cyhzdGFjayk6DQogICAgJycnDQogICAgR2l2ZW4gYSBzdHJpbmcgb24gdGhlIHN0YWNrIHJlcGxh
+Y2UgaXQgd2l0aCBhIGxpc3Qgb2YgdGhlIGxpbmVzIGluDQogICAgdGhlIHN0cmluZy4NCiAgICAn
+JycNCiAgICB0ZXh0LCBzdGFjayA9IHN0YWNrDQogICAgYXNzZXJ0IGlzaW5zdGFuY2UodGV4dCwg
+c3RyKSwgcmVwcih0ZXh0KQ0KICAgIHJldHVybiBsaXN0X3RvX3N0YWNrKHRleHQuc3BsaXRsaW5l
+cygpKSwgc3RhY2sNCg0KDQpAaW5zdGFsbA0KQFNpbXBsZUZ1bmN0aW9uV3JhcHBlcg0KZGVmIGhp
+eWEoc3RhY2spOg0KICAgICcnJw0KICAgIERlbW8gZnVuY3Rpb24gdG8gaW5zZXJ0ICJIaSBXb3Js
+ZCEiIGludG8gdGhlIGN1cnJlbnQgdmlld2VyLCBpZiBhbnkuDQogICAgJycnDQogICAgaWYgZC5m
+b2N1c2VkX3ZpZXdlcjoNCiAgICAgICAgZC5mb2N1c2VkX3ZpZXdlci5pbnNlcnQoJ0hpIFdvcmxk
+IScpDQogICAgcmV0dXJuIHN0YWNrDQpQSwMEFAAAAAAA5GZ4TkXs5NYLAAAACwAAAAcAAABsb2cu
+dHh0Sm95cHkgbG9nDQpQSwMEFAAAAAAA5GZ4Tmf2u80CBQAAAgUAAAgAAABtZW51LnR4dCAgbmFt
+ZV92aWV3ZXINCiAgbGlzdF9yZXNvdXJjZXMNCiAgb3Blbl9yZXNvdXJjZV9hdF9nb29kX2xvY2F0
+aW9uDQogIGdvb2Rfdmlld2VyX2xvY2F0aW9uDQogIG9wZW5fdmlld2VyDQogIHNlZV9zdGFjaw0K
+ICBzZWVfcmVzb3VyY2VzDQogIHNlZV9kZWZpbml0aW9ucw0KICBzZWVfbG9nDQogIHJlc2V0X2xv
+Zw0KDQogIGluc2NyaWJlDQogIGV2YWx1YXRlDQoNCiAgcG9wIGNsZWFyICAgIGR1cCBzd2FwDQoN
+CiAgYWRkIHN1YiBtdWwgZGl2IHRydWVkaXYgbW9kdWx1cyBkaXZtb2QNCiAgcG0gKysgLS0gc3Vt
+IHByb2R1Y3QgcG93IHNxciBzcXJ0DQogIDwgPD0gPSA+PSA+IDw+DQogICYgPDwgPj4NCg0KICBp
+IGR1cGRpcA0KDQohPSAlICYgKiAqZnJhY3Rpb24gKmZyYWN0aW9uMCArICsrIC0gLS0gLyA8IDw8
+IDw9IDw+ID0gPiA+PSA+PiA/IF4NCmFicyBhZGQgYW5hbW9ycGhpc20gYW5kIGFwcDEgYXBwMiBh
+cHAzIGF0IGF2ZXJhZ2UNCmIgYmluYXJ5IGJyYW5jaA0KY2hvaWNlIGNsZWFyIGNsZWF2ZSBjb25j
+YXQgY29ucw0KZGluZnJpcnN0IGRpcCBkaXBkIGRpcGRkIGRpc2Vuc3RhY2tlbiBkaXYgZGl2bW9k
+IGRvd25fdG9femVybyBkcm9wDQpkdWRpcGQgZHVwIGR1cGQgZHVwZGlwDQplbnN0YWNrZW4gZXEN
+CmZpcnN0IGZsYXR0ZW4gZmxvb3IgZmxvb3JkaXYNCmdjZCBnZSBnZW5yZWMgZ2V0aXRlbSBncmFu
+ZF9yZXNldCBndA0KaGVscA0KaSBpZCBpZnRlIGluZnJhIGluc2NyaWJlDQprZXlfYmluZGluZ3MN
+CmxlIGxlYXN0X2ZyYWN0aW9uIGxvb3AgbHNoaWZ0IGx0DQptYXAgbWF4IG1pbiBtb2QgbW9kdWx1
+cyBtb3VzZV9iaW5kaW5ncyBtdWwNCm5lIG5lZyBub3QgbnVsbGFyeQ0Kb2Ygb3Igb3Zlcg0KcGFt
+IHBhcnNlIHBpY2sgcG0gcG9wIHBvcGQgcG9wZGQgcG9wb3AgcG93IHByZWQgcHJpbXJlYyBwcm9k
+dWN0DQpxdW90ZWQNCnJhbmdlIHJhbmdlX3RvX3plcm8gcmVtIHJlbWFpbmRlciByZW1vdmUgcmVz
+ZXRfbG9nIHJlc3QgcmV2ZXJzZQ0Kcm9sbDwgcm9sbD4gcm9sbGRvd24gcm9sbHVwIHJzaGlmdCBy
+dW4NCnNlY29uZCBzZWxlY3Qgc2hhcmluZyBzaG93X2xvZyBzaHVudCBzaXplIHNvcnQgc3FyIHNx
+cnQgc3RhY2sgc3RlcA0Kc3RlcF96ZXJvIHN1YiBzdWNjIHN1bSBzd2FhY2sgc3dhcCBzd29uY2F0
+IHN3b25zDQp0YWtlIHRlcm5hcnkgdGhpcmQgdGltZXMgdHJ1ZWRpdiB0cnV0aHkgdHVjaw0KdW5h
+cnkgdW5jb25zIHVuaXF1ZSB1bml0IHVucXVvdGVkIHVuc3RhY2sNCnZvaWQNCndhcnJhbnR5IHdo
+aWxlIHdvcmRzDQp4IHhvcg0KemlwDQpQSwMEFAAAAAAA5GZ4TgCrPcaOEAAAjhAAAAsAAABzY3Jh
+dGNoLnR4dFdoYXQgaXMgaXQ/DQoNCkEgc2ltcGxlIEdyYXBoaWNhbCBVc2VyIEludGVyZmFjZSBm
+b3IgdGhlIEpveSBwcm9ncmFtbWluZyBsYW5ndWFnZSwNCndyaXR0ZW4gdXNpbmcgUHlnYW1lIHRv
+IGJ5cGFzcyBYMTEgZXQuIGFsLiwgbW9kZWxlZCBvbiB0aGUgT2Jlcm9uIE9TLCBhbmQNCmludGVu
+ZGVkIHRvIGJlIGp1c3QgZnVuY3Rpb25hbCBlbm91Z2ggdG8gc3VwcG9ydCBib290c3RyYXBwaW5n
+IGZ1cnRoZXIgSm95DQpkZXZlbG9wbWVudC4NCg0KSXQncyBiYXNpYyBmdW5jdGlvbmFsaXR5IGlz
+IG1vcmUtb3ItbGVzcyBhcyBhIGNydWRlIHRleHQgZWRpdG9yIGFsb25nIHdpdGgNCmEgc2ltcGxl
+IEpveSBydW50aW1lIChpbnRlcnByZXRlciwgc3RhY2ssIGFuZCBkaWN0aW9uYXJ5LikgIEl0IGF1
+dG8tIHNhdmVzDQphbnkgbmFtZWQgZmlsZXMgKGluIGEgdmVyc2lvbmVkIGhvbWUgZGlyZWN0b3J5
+KSBhbmQgeW91IGNhbiB3cml0ZSBuZXcgSm95DQpwcmltaXRpdmVzIGluIFB5dGhvbiBhbmQgSm95
+IGRlZmluaXRpb25zIGFuZCBpbW1lZGlhdGVseSBpbnN0YWxsIGFuZCB1c2UNCnRoZW0sIGFzIHdl
+bGwgYXMgcmVjb3JkaW5nIHRoZW0gZm9yIHJldXNlIChhZnRlciByZXN0YXJ0cy4pDQoNCkN1cnJl
+bnRseSwgdGhlcmUgYXJlIG9ubHkgdHdvIGtpbmRzIG9mIChpbnRlcmVzdGluZykgdmlld2Vyczog
+VGV4dFZpZXdlcnMNCmFuZCBTdGFja1ZpZXdlci4gVGhlIFRleHRWaWV3ZXJzIGFyZSBjcnVkZSB0
+ZXh0IGVkaXRvcnMuICBUaGV5IHByb3ZpZGUNCmp1c3QgZW5vdWdoIGZ1bmN0aW9uYWxpdHkgdG8g
+bGV0IHRoZSB1c2VyIHdyaXRlIHRleHQgYW5kIGNvZGUgKFB5dGhvbiBhbmQNCkpveSkgYW5kIGV4
+ZWN1dGUgSm95IGZ1bmN0aW9ucy4gIE9uZSBpbXBvcnRhbnQgdGhpbmcgdGhleSBkbyBpcw0KYXV0
+b21hdGljYWxseSBzYXZlIHRoZWlyIGNvbnRlbnQgYWZ0ZXIgY2hhbmdlcy4gIE5vIG1vcmUgbG9z
+dCB3b3JrLg0KDQpUaGUgU3RhY2tWaWV3ZXIgaXMgYSBzcGVjaWFsaXplZCBUZXh0Vmlld2VyIHRo
+YXQgc2hvd3MgdGhlIGNvbnRlbnRzIG9mIHRoZQ0KSm95IHN0YWNrIG9uZSBsaW5lIHBlciBzdGFj
+ayBpdGVtLiAgSXQncyBhIHZlcnkgaGFuZHkgdmlzdWFsIGFpZCB0byBrZWVwDQp0cmFjayBvZiB3
+aGF0J3MgZ29pbmcgb24uICBUaGVyZSdzIGFsc28gYSBsb2cudHh0IGZpbGUgdGhhdCBnZXRzIHdy
+aXR0ZW4NCnRvIHdoZW4gY29tbWFuZHMgYXJlIGV4ZWN1dGVkLCBhbmQgc28gcmVjb3JkcyB0aGUg
+bG9nIG9mIHVzZXIgYWN0aW9ucyBhbmQNCnN5c3RlbSBldmVudHMuICBJdCB0ZW5kcyB0byBmaWxs
+IHVwIHF1aWNrbHkgc28gdGhlcmUncyBhIHJlc2V0X2xvZyBjb21tYW5kDQp0aGF0IGNsZWFycyBp
+dCBvdXQuDQoNClZpZXdlcnMgaGF2ZSAiZ3JvdyIgYW5kICJjbG9zZSIgaW4gdGhlaXIgbWVudSBi
+YXJzLiAgVGhlc2UgYXJlIGJ1dHRvbnMuDQpXaGVuIHlvdSByaWdodC1jbGljayBvbiBncm93IGEg
+dmlld2VyIGEgY29weSBpcyBjcmVhdGVkIHRoYXQgY292ZXJzIHRoYXQNCnZpZXdlcidzIGVudGly
+ZSB0cmFjay4gIElmIHlvdSBncm93IGEgdmlld2VyIHRoYXQgYWxyZWFkeSB0YWtlcyB1cCBpdHMN
+Cndob2xlIHRyYWNrIHRoZW4gYSBjb3B5IGlzIGNyZWF0ZWQgdGhhdCB0YWtlcyB1cCBhbiBhZGRp
+dGlvbmFsIHRyYWNrLCB1cA0KdG8gdGhlIHdob2xlIHNjcmVlbi4gIENsb3NpbmcgYSB2aWV3ZXIg
+anVzdCBkZWxldGVzIHRoYXQgdmlld2VyLCBhbmQgd2hlbg0KYSB0cmFjayBoYXMgbm8gbW9yZSB2
+aWV3ZXJzLCBpdCBpcyBkZWxldGVkIGFuZCB0aGF0IGV4cG9zZXMgYW55IHByZXZpb3VzDQp0cmFj
+a3MgYW5kIHZpZXdlcnMgdGhhdCB3ZXJlIGhpZGRlbi4NCg0KKE5vdGU6IGlmIHlvdSBldmVyIGNs
+b3NlIGFsbCB0aGUgdmlld2VycyBhbmQgYXJlIHNpdHRpbmcgYXQgYSBibGFuayBzY3JlZW4NCndp
+dGggIG5vd2hlcmUgdG8gdHlwZSBhbmQgZXhlY3V0ZSBjb21tYW5kcywgcHJlc3MgdGhlIFBhdXNl
+L0JyZWFrIGtleS4NClRoaXMgd2lsbCBvcGVuIGEgbmV3ICJ0cmFwIiB2aWV3ZXIgd2hpY2ggeW91
+IGNhbiB0aGVuIHVzZSB0byByZWNvdmVyLikNCg0KQ29waWVzIG9mIGEgdmlld2VyIGFsbCBzaGFy
+ZSB0aGUgc2FtZSBtb2RlbCBhbmQgdXBkYXRlIHRoZWlyIGRpc3BsYXkgYXMgaXQNCmNoYW5nZXMu
+IChJZiB5b3UgaGF2ZSB0d28gdmlld2VycyBvcGVuIG9uIHRoZSBzYW1lIG5hbWVkIHJlc291cmNl
+IGFuZCBlZGl0DQpvbmUgeW91J2xsIHNlZSB0aGUgb3RoZXIgdXBkYXRlIGFzIHlvdSB0eXBlLikN
+Cg0KVUkgR3VpZGUNCg0KbGVmdCBtb3VzZSBzZXRzIGN1cnNvciBpbiB0ZXh0LCBpbiBtZW51IGJh
+ciByZXNpemVzIHZpZXdlciBpbnRlcmFjdGl2ZWx5DQoodGhpcyBpcyBhIGxpdHRsZSBidWdneSBp
+biB0aGF0IHlvdSBjYW4gbW92ZSB0aGUgbW91c2UgcXVpY2tseSBhbmQgZ2V0DQpvdXRzaWRlIHRo
+ZSBtZW51LCBsZWF2aW5nIHRoZSB2aWV3ZXIgaW4gdGhlICJyZXNpemluZyIgc3RhdGUuIFVudGls
+IEkgZml4DQp0aGlzLCB0aGUgd29ya2Fyb3VuZCBpcyB0byBqdXN0IGdyYWIgdGhlIG1lbnUgYmFy
+IGFnYWluIGFuZCB3aWdnbGUgaXQgYQ0KZmV3IHBpeGVscyBhbmQgbGV0IGdvLiAgVGhpcyB3aWxs
+IHJlc2V0IHRoZSBtYWNoaW5lcnkuKQ0KDQpSaWdodCBtb3VzZSBleGVjdXRlcyBKb3kgY29tbWFu
+ZCAoZnVuY3Rpb25zKSwgYW5kIHlvdSBjYW4gZHJhZyB3aXRoIHRoZQ0KcmlnaHQgYnV0dG9uIHRv
+IGhpZ2hsaWdodCAod2VsbCwgdW5kZXJsaW5lKSBjb21tYW5kcy4gIFdvcmRzIHRoYXQgYXJlbid0
+DQpuYW1lcyBvZiBKb3kgY29tbWFuZHMgd29uJ3QgYmUgdW5kZXJsaW5lZC4gIFJlbGVhc2UgdGhl
+IGJ1dHRvbiB0byBleGVjdXRlDQp0aGUgY29tbWFuZC4NCg0KVGhlIG1pZGRsZSBtb3VzZSBidXR0
+b24gKHVzdWFsbHkgYSB3aGVlbCB0aGVzZSBkYXlzKSBzY3JvbGxzIHRoZSB0ZXh0IGJ1dA0KeW91
+IGNhbiBhbHNvIGNsaWNrIGFuZCBkcmFnIGFueSB2aWV3ZXIgd2l0aCBpdCB0byBtb3ZlIHRoYXQg
+dmlld2VyIHRvDQphbm90aGVyIHRyYWNrIG9yIHRvIGEgZGlmZmVyZW50IGxvY2F0aW9uIGluIHRo
+ZSBzYW1lIHRyYWNrLiAgVGhlcmUncyBubw0KZGlyZWN0IHZpc3VhbCBmZWVkYmFjayBmb3IgdGhp
+cyAoeWV0KSBidXQgdGhhdCBkb3Nlbid0IHNlZW0gdG8gaW1wYWlyIGl0cw0KdXNlZnVsbmVzcy4N
+Cg0KRjEsIEYyIC0gc2V0IHNlbGVjdGlvbiBiZWdpbiBhbmQgZW5kIG1hcmtlcnMgKGNydWRlIGJ1
+dCB1c2FibGUuKQ0KDQpGMyAtIGNvcHkgc2VsZWN0ZWQgdGV4dCB0byB0aGUgdG9wIG9mIHRoZSBz
+dGFjay4NCg0KU2hpZnQtRjMgLSBhcyBjb3B5IHRoZW4gcnVuICJwYXJzZSIgY29tbWFuZCBvbiB0
+aGUgc3RyaW5nLg0KDQpGNCAtIGN1dCBzZWxlY3RlZCB0ZXh0IHRvIHRoZSB0b3Agb2YgdGhlIHN0
+YWNrLg0KDQpTaGlmdC1GNCAtIGFzIGN1dCB0aGVuIHJ1biAicG9wIiAoZGVsZXRlIHNlbGVjdGlv
+bi4pDQoNCkpveQ0KDQpQcmV0dHkgbXVjaCBhbGwgb2YgdGhlIHJlc3Qgb2YgdGhlIGZ1bmN0aW9u
+YWxpdHkgb2YgdGhlIHN5c3RlbSBpcyBwcm92aWRlZA0KYnkgZXhlY3V0aW5nIEpveSBjb21tYW5k
+cyAoYWthIGZ1bmN0aW9ucywgYWthICJ3b3JkcyIgaW4gRm9ydGgpIGJ5IHJpZ2h0LQ0KY2xpY2tp
+bmcgb24gdGhlaXIgbmFtZXMgaW4gYW55IHRleHQuDQoNClRvIGdldCBoZWxwIG9uIGEgSm95IGZ1
+bmN0aW9uIHNlbGVjdCB0aGUgbmFtZSBvZiB0aGUgZnVuY3Rpb24gaW4gYQ0KVGV4dFZpZXdlciB1
+c2luZyBGMSBhbmQgRjIsIHRoZW4gcHJlc3Mgc2hpZnQtRjMgdG8gcGFyc2UgdGhlIHNlbGVjdGlv
+bi4NClRoZSBmdW5jdGlvbiAocmVhbGx5IGl0cyBTeW1ib2wpIHdpbGwgYXBwZWFyIG9uIHRoZSBz
+dGFjayBpbiBicmFja2V0cyAoYQ0KInF1b3RlZCBwcm9ncmFtIiBzdWNoIGFzICJbcG9wXSIuKSAg
+VGhlbiByaWdodC1jbGljayBvbiB0aGUgd29yZCBoZWxwIGluDQphbnkgVGV4dFZpZXdlciAoaWYg
+aXQncyBub3QgYWxyZWFkeSB0aGVyZSwganVzdCB0eXBlIGl0IGluIHNvbWV3aGVyZS4pDQpUaGlz
+IHdpbGwgcHJpbnQgdGhlIGRvY3N0cmluZyBvciBkZWZpbml0aW9uIG9mIHRoZSB3b3JkIChmdW5j
+dGlvbikgdG8NCnN0ZG91dC4gIEF0IHNvbWUgcG9pbnQgSSdsbCB3cml0ZSBhIHRoaW5nIHRvIHNl
+bmQgdGhhdCB0byB0aGUgbG9nLnR4dCBmaWxlDQppbnN0ZWFkLCBidXQgZm9yIG5vdyBsb29rIGZv
+ciBvdXRwdXQgaW4gdGhlIHRlcm1pbmFsLg0KUEsDBBQAAAAAAORmeE53f5peAwAAAAMAAAAMAAAA
+c3RhY2sucGlja2xlKHQuUEsBAhQAFAAAAAAA5GZ4Tv3gGVF+AwAAfgMAAA8AAAAAAAAAAAAAALaB
+AAAAAGRlZmluaXRpb25zLnR4dFBLAQIUABQAAAAAACFrpk7/HHjxGBYAABgWAAAKAAAAAAAAAAAA
+AAC2gasDAABsaWJyYXJ5LnB5UEsBAhQAFAAAAAAA5GZ4TkXs5NYLAAAACwAAAAcAAAAAAAAAAAAA
+ALaB6xkAAGxvZy50eHRQSwECFAAUAAAAAADkZnhOZ/a7zQIFAAACBQAACAAAAAAAAAAAAAAAtoEb
+GgAAbWVudS50eHRQSwECFAAUAAAAAADkZnhOAKs9xo4QAACOEAAACwAAAAAAAAAAAAAAtoFDHwAA
+c2NyYXRjaC50eHRQSwECFAAUAAAAAADkZnhOd3+aXgMAAAADAAAADAAAAAAAAAAAAAAAtoH6LwAA
+c3RhY2sucGlja2xlUEsFBgAAAAAGAAYAUwEAACcwAAAAAA==''')))
+
+
+if __name__ == '__main__':
+ print(create_data())
diff --git a/joy/vui/main.py b/joy/vui/main.py
index 399d9d2..8cd5fe3 100644
--- a/joy/vui/main.py
+++ b/joy/vui/main.py
@@ -1,174 +1,175 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © 2019 Simon Forman
-#
-# This file is part of Thun
-#
-# Thun 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.
-#
-# Thun 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 Thun. If not see .
-#
-'''
-
-Main Module
-======================================
-
-Pulls everything together.
-
-'''
-import os, sys, traceback
-import pygame
-from joy.library import initialize, DefinitionWrapper, SimpleFunctionWrapper
-from joy.vui 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('~/.thun')
- if not os.path.isabs(JOY_HOME):
- raise ValueError('what directory?')
-
-
-def load_definitions(pt, dictionary):
- '''Load definitions from ``definitions.txt``.'''
- lines = pt.open('definitions.txt')[1]
- for line in lines:
- if '==' in line:
- DefinitionWrapper.add_def(line, dictionary)
-
-
-def load_primitives(home, name_space):
- '''Load primitives from ``library.py``.'''
- fn = os.path.join(home, 'library.py')
- if os.path.exists(fn):
- execfile(fn, name_space)
-
-
-def init():
- '''
- Initialize the system.
-
- * Init PyGame
- * Create main window
- * Start the PyGame clock
- * Set the event mask
- * Create the PersistTask
-
- '''
- print 'Initializing Pygame...'
- pygame.init()
- print 'Creating window...'
- 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):
- '''
- More initialization
-
- * Create the Joy dictionary
- * Create the Display
- * Open the log, menu, and scratch text viewers, and the stack pickle
- * Start the main loop
- * Create the World object
- * Register PersistTask and World message handlers with the Display
- * Load user function definitions.
-
- '''
- D = initialize()
- d = display.Display(
- screen,
- 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):
- '''
- Run a loop function, retry for ``n`` exceptions.
- Prints tracebacks on ``sys.stderr``.
- '''
- error_count = 0
- while error_count < n:
- try:
- loop()
- break
- except:
- traceback.print_exc(file=sys.stderr)
- error_count += 1
-
-
-class FileFaker(object):
- '''Pretends to be a file object but writes to log instead.'''
-
- def __init__(self, log):
- self.log = log
-
- def write(self, text):
- '''Write text to log.'''
- self.log.append(text)
-
- def flush(self):
- pass
-
-
-def main(screen, clock, pt):
- '''
- Main function.
-
- * Call ``init_context()``
- * Load primitives
- * Create an ``evaluate`` function that lets you just eval some Python code
- * Redirect ``stdout`` to the log using a ``FileFaker`` object, and...
- * Start the main loop.
- '''
- name_space = init_context(screen, clock, pt)
- load_primitives(pt.home, name_space.copy())
-
- @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']
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2019 Simon Forman
+#
+# This file is part of Thun
+#
+# Thun 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.
+#
+# Thun 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 Thun. If not see .
+#
+'''
+
+Main Module
+======================================
+
+Pulls everything together.
+
+'''
+from __future__ import print_function
+import os, sys, traceback
+import pygame
+from joy.library import initialize, DefinitionWrapper, SimpleFunctionWrapper
+from joy.vui 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('~/.thun')
+ if not os.path.isabs(JOY_HOME):
+ raise ValueError('what directory?')
+
+
+def load_definitions(pt, dictionary):
+ '''Load definitions from ``definitions.txt``.'''
+ lines = pt.open('definitions.txt')[1]
+ for line in lines:
+ if '==' in line:
+ DefinitionWrapper.add_def(line, dictionary)
+
+
+def load_primitives(home, name_space):
+ '''Load primitives from ``library.py``.'''
+ fn = os.path.join(home, 'library.py')
+ if os.path.exists(fn):
+ execfile(fn, name_space)
+
+
+def init():
+ '''
+ Initialize the system.
+
+ * Init PyGame
+ * Create main window
+ * Start the PyGame clock
+ * Set the event mask
+ * Create the PersistTask
+
+ '''
+ print('Initializing Pygame...')
+ pygame.init()
+ print('Creating window...')
+ 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):
+ '''
+ More initialization
+
+ * Create the Joy dictionary
+ * Create the Display
+ * Open the log, menu, and scratch text viewers, and the stack pickle
+ * Start the main loop
+ * Create the World object
+ * Register PersistTask and World message handlers with the Display
+ * Load user function definitions.
+
+ '''
+ D = initialize()
+ d = display.Display(
+ screen,
+ 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):
+ '''
+ Run a loop function, retry for ``n`` exceptions.
+ Prints tracebacks on ``sys.stderr``.
+ '''
+ error_count = 0
+ while error_count < n:
+ try:
+ loop()
+ break
+ except:
+ traceback.print_exc(file=sys.stderr)
+ error_count += 1
+
+
+class FileFaker(object):
+ '''Pretends to be a file object but writes to log instead.'''
+
+ def __init__(self, log):
+ self.log = log
+
+ def write(self, text):
+ '''Write text to log.'''
+ self.log.append(text)
+
+ def flush(self):
+ pass
+
+
+def main(screen, clock, pt):
+ '''
+ Main function.
+
+ * Call ``init_context()``
+ * Load primitives
+ * Create an ``evaluate`` function that lets you just eval some Python code
+ * Redirect ``stdout`` to the log using a ``FileFaker`` object, and...
+ * Start the main loop.
+ '''
+ name_space = init_context(screen, clock, pt)
+ load_primitives(pt.home, name_space.copy())
+
+ @SimpleFunctionWrapper
+ def evaluate(stack):
+ '''Evaluate the Python code text on the top of the stack.'''
+ code, stack = stack
+ exec(code, 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']
diff --git a/joy/vui/persist_task.py b/joy/vui/persist_task.py
index 7d99405..662fc43 100644
--- a/joy/vui/persist_task.py
+++ b/joy/vui/persist_task.py
@@ -1,272 +1,273 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © 2019 Simon Forman
-#
-# This file is part of Thun
-#
-# Thun 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.
-#
-# Thun 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 Thun. If not see .
-#
-'''
-
-Persist Task
-===========================
-
-This module deals with persisting the "resources" (text files and the
-stack) to the git repo in the ``JOY_HOME`` directory.
-
-'''
-import os, pickle, traceback
-from collections import Counter
-from dulwich.errors import NotGitRepository
-from dulwich.repo import Repo
-from joy.vui import core, init_joy_home
-
-
-def open_repo(repo_dir=None, initialize=False):
- '''
- Open, or create, and return a Dulwich git repo object for the given
- directory. If the dir path doesn't exist it will be created. If it
- does exist but isn't a repo the result depends on the ``initialize``
- argument. If it is ``False`` (the default) a ``NotGitRepository``
- exception is raised, otherwise ``git init`` is effected in the dir.
- '''
- 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):
- '''
- Initialize a git repository in the directory. Stage and commit all
- files (toplevel, not those in subdirectories if any) in the 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):
- '''
- Helper function to return a function that returns a path given a path,
- that's relative to the repository.
- '''
- 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):
- '''
- Handle the content of a text files as a list of lines, deal with
- saving it and staging the changes to a repo.
- '''
-
- 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):
- '''
- Save the lines to the file and stage the file in the 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):
- '''
- A ``Resource`` subclass that uses ``pickle`` on its file/thing.
- '''
-
- def _from_file(self, f):
- return [pickle.load(f)]
-
- def _to_file(self, f):
- pickle.dump(self.thing[0], f)
-
-
-class PersistTask(object):
- '''
- This class deals with saving changes to the git repo.
- '''
-
- 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 named file in home and return its content_id and 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):
- '''
- Handle messages, dispatch to ``handle_FOO()`` methods.
- '''
- 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):
- '''
- Foo.
- '''
- 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):
- '''
- Foo.
- '''
- 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):
- '''
- Foo.
- '''
- 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):
- '''
- Foo.
- '''
- 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):
- '''
- Persist a resource.
- '''
- del self.counter[content_id]
- self.store[content_id].persist(self.repo)
-
- def task_run(self):
- '''
- Stage any outstanding changes.
- '''
- 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'):
- '''
- Commit.
- '''
- return self.repo.do_commit(message, committer=core.COMMITTER)
-
- def scan(self):
- '''
- Return a sorted list of all the files in the home dir.
- '''
- return sorted([
- fn
- for fn in os.listdir(self.home)
- if os.path.isfile(os.path.join(self.home, fn))
- ])
-
-
-def check_filename(name):
- '''
- Sanity checks for filename.
- '''
- # 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('~/.thun')
- 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
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2019 Simon Forman
+#
+# This file is part of Thun
+#
+# Thun 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.
+#
+# Thun 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 Thun. If not see .
+#
+'''
+
+Persist Task
+===========================
+
+This module deals with persisting the "resources" (text files and the
+stack) to the git repo in the ``JOY_HOME`` directory.
+
+'''
+from __future__ import print_function
+import os, pickle, traceback
+from collections import Counter
+from dulwich.errors import NotGitRepository
+from dulwich.repo import Repo
+from joy.vui import core, init_joy_home
+
+
+def open_repo(repo_dir=None, initialize=False):
+ '''
+ Open, or create, and return a Dulwich git repo object for the given
+ directory. If the dir path doesn't exist it will be created. If it
+ does exist but isn't a repo the result depends on the ``initialize``
+ argument. If it is ``False`` (the default) a ``NotGitRepository``
+ exception is raised, otherwise ``git init`` is effected in the dir.
+ '''
+ if not os.path.exists(repo_dir):
+ os.makedirs(repo_dir, 0o700)
+ 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):
+ '''
+ Initialize a git repository in the directory. Stage and commit all
+ files (toplevel, not those in subdirectories if any) in the 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):
+ '''
+ Helper function to return a function that returns a path given a path,
+ that's relative to the repository.
+ '''
+ 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):
+ '''
+ Handle the content of a text files as a list of lines, deal with
+ saving it and staging the changes to a repo.
+ '''
+
+ 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(line, file=f)
+
+ def persist(self, repo):
+ '''
+ Save the lines to the file and stage the file in the repo.
+ '''
+ with open(self.filename, 'w') as f:
+ os.chmod(self.filename, 0o600)
+ 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):
+ '''
+ A ``Resource`` subclass that uses ``pickle`` on its file/thing.
+ '''
+
+ def _from_file(self, f):
+ return [pickle.load(f)]
+
+ def _to_file(self, f):
+ pickle.dump(self.thing[0], f)
+
+
+class PersistTask(object):
+ '''
+ This class deals with saving changes to the git repo.
+ '''
+
+ 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 named file in home and return its content_id and 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):
+ '''
+ Handle messages, dispatch to ``handle_FOO()`` methods.
+ '''
+ 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):
+ '''
+ Foo.
+ '''
+ 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):
+ '''
+ Foo.
+ '''
+ 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):
+ '''
+ Foo.
+ '''
+ 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):
+ '''
+ Foo.
+ '''
+ 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):
+ '''
+ Persist a resource.
+ '''
+ del self.counter[content_id]
+ self.store[content_id].persist(self.repo)
+
+ def task_run(self):
+ '''
+ Stage any outstanding changes.
+ '''
+ 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'):
+ '''
+ Commit.
+ '''
+ return self.repo.do_commit(message, committer=core.COMMITTER)
+
+ def scan(self):
+ '''
+ Return a sorted list of all the files in the home dir.
+ '''
+ return sorted([
+ fn
+ for fn in os.listdir(self.home)
+ if os.path.isfile(os.path.join(self.home, fn))
+ ])
+
+
+def check_filename(name):
+ '''
+ Sanity checks for filename.
+ '''
+ # 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('~/.thun')
+ 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/text_viewer.py b/joy/vui/text_viewer.py
index 4b2fa5f..6e1f9b2 100644
--- a/joy/vui/text_viewer.py
+++ b/joy/vui/text_viewer.py
@@ -1,699 +1,700 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © 2019 Simon Forman
-#
-# This file is part of Thun
-#
-# Thun 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.
-#
-# Thun 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 Thun. If not see .
-#
-'''
-
-Text Viewer
-=================
-
-'''
-import string
-import pygame
-from joy.utils.stack import expression_to_string
-from joy.vui.core import (
- ARROW_KEYS,
- BACKGROUND as BG,
- FOREGROUND as FG,
- CommandMessage,
- ModifyMessage,
- OpenMessage,
- SUCCESS,
- push,
- )
-from joy.vui 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)
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2019 Simon Forman
+#
+# This file is part of Thun
+#
+# Thun 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.
+#
+# Thun 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 Thun. If not see .
+#
+'''
+
+Text Viewer
+=================
+
+'''
+from __future__ import print_function
+import string
+import pygame
+from joy.utils.stack import expression_to_string
+from joy.vui.core import (
+ ARROW_KEYS,
+ BACKGROUND as BG,
+ FOREGROUND as FG,
+ CommandMessage,
+ ModifyMessage,
+ OpenMessage,
+ SUCCESS,
+ push,
+ )
+from joy.vui 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
index 6fa61a0..1d7d81b 100644
--- a/joy/vui/viewer.py
+++ b/joy/vui/viewer.py
@@ -1,245 +1,246 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © 2019 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 .
-#
-'''
-
-Viewer
-=================
-
-'''
-import pygame
-from joy.vui.core import BACKGROUND, FOREGROUND
-
-
-class Viewer(object):
- '''
- Base Viewer class
- '''
-
- 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):
-
- '''
- MenuViewer class
- '''
-
- 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)
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2019 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 .
+#
+'''
+
+Viewer
+=================
+
+'''
+from __future__ import print_function
+import pygame
+from joy.vui.core import BACKGROUND, FOREGROUND
+
+
+class Viewer(object):
+ '''
+ Base Viewer class
+ '''
+
+ 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):
+
+ '''
+ MenuViewer class
+ '''
+
+ 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), end=' ')
+ 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)