# -*- 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 <http://www.gnu.org/licenses/>.
#
'''

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)

    def key_down(self, display, uch, key, mod):
        pass

class MenuViewer(Viewer):

    '''
    MenuViewer class
    '''

    MINIMUM_HEIGHT = 26

        Viewer.__init__(self, surface)
        self.resizing = 0
        self.bg = 100, 150, 100

        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)

        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)

        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):
            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)

        if button == 1 and self.resizing:
            if self.resizing == 2:
                self.resizing = 0
                self.draw()
                display.done_resizing()
            self.resizing = 0

    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)
        else:
            #self.draw_an_a(x, y)

        if Viewer.key_up(self, display, key, mod):

    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):

        MenuViewer.__init__(self, surface)

        MenuViewer.resurface(self, surface)

        MenuViewer.draw_menu(self)

        pass

        pass

        if MenuViewer.menu_click(self, display, x, y, button):

        if MenuViewer.mouse_up(self, display, x, y, button):

        if MenuViewer.mouse_motion(self, display, x, y, rel_x, rel_y,
                               button0, button1, button2):

        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)

#
#
#    This file is part of Thun
#
#    Thun is free software: you can redistribute it and/or modify
#
#    Thun is distributed in the hope that it will be useful,
#
#    along with Thun.  If not see <http://www.gnu.org/licenses/>.
#
'''

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
from joy.joy import run
from joy.utils.stack import stack_to_string

COMMITTER = 'Joy <auto-commit@example.com>'

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):
        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):
        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):
        self.content_id = content_id

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

        '''
        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:
                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))

#
#
#
#
#
#
'''

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.
    '''

        return [pickle.load(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]
            R = PickledResource if name.endswith('.pickle') else Resource
            resource = self.store[content_id] = R(fn, self._r(fn))
        return content_id, resource.thing

        '''
        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']
            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]
            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)
        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.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():

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)

#
#
#
#
#
#
'''

Main Module
======================================

Pulls everything together.

'''
import os, sys, traceback
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:
    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']

#
#
#
#
#
#
'''

Text Viewer

'''
import string
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]))

        def down(self, _mod):
            if self.y < len(self.v.lines) - 1:
                self.y += 1
                self._check_scroll()

        def left(self, _mod):
            if self.x:
                self.x -= 1
            elif self.y:
                self.y -= 1
                self.x = len(self.v.lines[self.y])

        def right(self, _mod):
            if self.x < len(self.v.lines[self.y]):
                self.x += 1
            elif self.y < len(self.v.lines) - 1:
                self.y += 1
                self.x = 0

        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()

        self.cursor = self.Cursor(self)
        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

        self.cursor.fade()

        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

        if super(TextViewer, self).handle(message):
            return
            and message.subject is self.lines
            ):
            # TODO: check self.at_line

    # Drawing

        #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
            )

        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

        self.cursor.v = self
        self.cursor.draw()

    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.at_line -= 1
            self.body_surface.scroll(0, FONT.line_h)
            self._redraw_line(self.at_line)

    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
            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
            column = len(line)
        else:
            column = min(x / FONT.char_w, len(line))
        return line, column, row

    # Event Processing

            _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()
            self.command_down(display, x, y)
        elif button == 4: self.scroll_down()
        elif button == 5: self.scroll_up()

        elif button == 3 and self.body_rect.collidepoint(x, y):
            self.command_up(display)

                                   button0, button1, button2):
        if (button0
            and display.focused_viewer is self
            and self.body_rect.collidepoint(x, y)
            ):
            _line, column, row = self.at(x - bx, y - by)
        elif button2 and self.body_rect.collidepoint(x, y):
            self.command_down(display, x - bx, y - by)

        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)
        else:
            print '%r %i %s' % (uch, key, bin(mod))

        if modified:
            # The selection is fragile.
            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:
                command = 'roll>'
            else:
                command = 'dup'
        elif key == pygame.K_F7:
                command = 'tuck'
            else:
                command = 'over'
##        elif key == pygame.K_F8:
##            if mod & pygame.KMOD_SHIFT:
##                command = ''
##            else:
        else:
            return
        display.broadcast(CommandMessage(self, command))

    # Selection Handling

    def _selection_key(self, display, key, mod):
        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
        elif key == pygame.K_F3: # copy
                self._parse_selection(display)
            else:
                self._copy_selection(display)
        elif key == pygame.K_F4: # cut or delete
                self._delete_selection(display)
            else:
                self._cut_selection(display)

    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:
##        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._copy_selection(display):
                display.broadcast(CommandMessage(self, 'parse'))

    def _cut_selection(self, display):

    def _delete_selection(self, display):
        if not self._has_selection():
            return
        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.'''
            return ''
            return str(self.lines[srow][scolumn:ecolumn])
        lines = []
        assert srow < erow
        while srow <= erow:
            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, ):
            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,
            )
        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.x += 1
        self.draw_line(self.cursor.screen_y(), line)

    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 + ' ')
            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
            res = True
        return res

    def _delete_key(self, _mod, line, i):
        res = False
        if i < len(line):
            line = line[:i] + line[i + 1:]
            res = True
        elif self.cursor.y < len(self.lines) - 1:
            left, right = self.lines[y:y + 2]
            self.lines[y:y + 2] = [left + right]
            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):
        # 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:

    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'))

    def insert(self, content):
        assert isinstance(content, basestring), repr(content)
        if content:
            row, column = self.cursor.y, self.cursor.x
            lines = (line[:column] + content + line[column:]).splitlines()
            self.lines[row:row + 1] = lines
            self.cursor.y = row + len(lines) - 1
            self.cursor.x = len(lines[-1]) - len(line) + column

    def append(self, content):
        self.cursor.y = len(self.lines) - 1
        self.cursor.x = len(self.lines[self.cursor.y])
        self.insert(content)

#
#
#
#
#
#
'''

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 .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 _, 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))
            r = 0, 0, new_w, self.h
            new_viewer.resurface(new_track.surface.subsurface(r))
            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,))

        '''
        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.
        '''
            track.broadcast(message)
        for handler in self.handlers:
            handler(message)

    def redraw(self):
        '''
        Redraw all tracks (which will redraw all viewers.)
        '''
            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:
            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.
    '''

        self.viewers = []  # (y, viewer)
        self.hiding = None
        self.resizing_viewer = None

        '''
        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)))
        self.viewers.append((y, new_viewer))
        self.viewers.sort()  # Could use bisect module but how many
                             # viewers will you ever have?

        '''Close the viewer, reuse the freed space.'''
        for y, V in self.viewers:
            if V is viewer:
                self._close_viewer(y, V)
        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):
        '''

        '''
        for old_y, V in self.viewers:
                if relative: new_y += old_y
                if new_y != old_y: self._change_viewer(new_y, old_y, V)

    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:
                self._grow_by(previous_viewer, previous_y, h)
            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

        '''
        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)

        '''Redraw the track and all of its viewers.'''

#
#
#
#
#
#
'''

Stack Viewer

'''
from joy.utils.stack import expression_to_string, iter_stack
from joy.vui import core, text_viewer

MAX_WIDTH = 64

def fsi(item):
    '''Format Stack Item'''
    if isinstance(item, tuple):
        res = '[%s]' % expression_to_string(item)
    elif isinstance(item, str):
        res = '"%s"' % item
    else:
        assert not isinstance(item, unicode), repr(item)
        res = str(item)
    if len(res) > MAX_WIDTH:
        return res[:MAX_WIDTH - 3] + '...'
    return res

class StackViewer(text_viewer.TextViewer):

        super(StackViewer, self).__init__(surface)
        self.stack_holder = None
        self.content_id = 'stack viewer'

    def _attach(self, display):
        if self.stack_holder:
            return
        om = core.OpenMessage(self, 'stack.pickle')
        if om.status != core.SUCCESS:
            raise RuntimeError('stack unavailable')
        self.stack_holder = om.thing

    def _update(self):
        self.lines[:] = map(fsi, iter_stack(self.stack_holder[0])) or ['']

        self._attach(display)
        super(StackViewer, self).focus(display)

        if (isinstance(message, core.ModifyMessage)
            ):
            self._update()

make_repo_relative_path_maker() (in module joy.vui.persist_task)