[shpy-commit] r2877 - in shpy/trunk/dist/shpy: . net ui_pygame

arigo@codespeak.net arigo@codespeak.net
Thu, 22 Jan 2004 19:08:38 +0100 (MET)


Author: arigo
Date: Thu Jan 22 19:08:33 2004
New Revision: 2877

Added:
   shpy/trunk/dist/shpy/ui_pygame/
   shpy/trunk/dist/shpy/ui_pygame/__init__.py
   shpy/trunk/dist/shpy/ui_pygame/autopath.py   (props changed)
      - copied unchanged from r2872, shpy/trunk/dist/shpy/autopath.py
   shpy/trunk/dist/shpy/ui_pygame/decorate.py
   shpy/trunk/dist/shpy/ui_pygame/ui_pygame.py   (contents, props changed)
      - copied, changed from r2873, shpy/trunk/dist/shpy/ui_pygame.py
Removed:
   shpy/trunk/dist/shpy/ui_pygame.py
Modified:
   shpy/trunk/dist/shpy/net/shared.py
   shpy/trunk/dist/shpy/net/structure.py
Log:
Complete reorganization.  Now ui_pygame.py looks nicer and doesn't need
to know about Structures any more: this is all done with "Decorators"
which are client-side classes wrapping the low-level Structures that are
passed around between machines.

Most of the hiding stuff is done by the classes in decorate.py.

(this is a join hpk+arigo commit)


Modified: shpy/trunk/dist/shpy/net/shared.py
==============================================================================
--- shpy/trunk/dist/shpy/net/shared.py	(original)
+++ shpy/trunk/dist/shpy/net/shared.py	Thu Jan 22 19:08:33 2004
@@ -14,30 +14,31 @@
 
 
 class Structure:
-    def __init__(self, **attributes):
+    def __init__(self, typeid, **attributes):
+        self.typeid = typeid
         self.__dict__.update(attributes)
 
-
-def structurelist(*args):
+def cellstructure(*args):
     l = []
     for arg in args:
-        l.append(Structure(content=arg))
-    return l
+        l.append(Structure("line:0", content=arg))
+    return Structure("cell:0", lines=Structure("list:0", listitems=l))
 
-root = Structure()
+root = Structure("root:0")
 
-root.cells = Structure(list = [
-        Structure(lines = structurelist(
+root.cells = Structure("list:0", listitems = [
+        cellstructure(
         'def f():',
         '   for i in range(10):',
         '       print i * 42',
         '       break',
-        )),
-        Structure(lines = structurelist(
+        ),
+        cellstructure(
         'def f():',
         '   print 42',
-        )),
+        ),
     ]
 )
 
-root.users = Structure()
+root.users = Structure("list:0", listitems = [])
+

Modified: shpy/trunk/dist/shpy/net/structure.py
==============================================================================
--- shpy/trunk/dist/shpy/net/structure.py	(original)
+++ shpy/trunk/dist/shpy/net/structure.py	Thu Jan 22 19:08:33 2004
@@ -7,8 +7,10 @@
 
 
 
+ATOMIC = (int, float, str, unicode, type(None))
+
 def represent(obj, ref):
-    if isinstance(obj, (int, float, str, unicode)):
+    if isinstance(obj, ATOMIC):
         return repr(obj)
     elif isinstance(obj, dict):
         items = ['%s: %s' % (represent(key,ref), represent(value,ref))

Deleted: /shpy/trunk/dist/shpy/ui_pygame.py
==============================================================================
--- /shpy/trunk/dist/shpy/ui_pygame.py	Thu Jan 22 19:08:33 2004
+++ (empty file)
@@ -1,337 +0,0 @@
-import autopath
-
-import sys
-import pygame
-from pygame.locals import *
-import shpy.net.register
-from shpy.net.structure import Structure, representstructure, getstructureid
-from shpy import info 
-
-RESOLUTION = (768, 512)
-#RESOLUTION = (300, 300)
-#FONT = 'lucon.ttf'
-FONT = None
-HEIGHT = 24
-
-WAKEUPEVENT = USEREVENT
-
-
-KEYMAP = {}
-for name, value in pygame.locals.__dict__.items():
-    if name.startswith('K_'):
-        KEYMAP[value] = name
-
-##LINECACHE = {}
-##def rendertext(font, text, fgcolor, bgcolor=(255,255,255,255)):
-##    key = (font, text, fgcolor, bgcolor)
-##    try:
-##        image = LINECACHE[key]
-##    except KeyError:
-##        image = LINECACHE[key] = font.render(text, 1, fgcolor, bgcolor)
-##    return image
-
-
-class Terminal:
-    
-    def __init__(self, sharedserver, execserver):
-        ns = {'terminal': self}
-        self.servergateway = shpy.net.register.ServerGateway(sharedserver, ns)
-        self.execgateway = shpy.net.register.ExecGateway(execserver)
-        
-        pygame.init()
-        pygame.key.set_repeat(500,30)
-        self.screen = pygame.display.set_mode(RESOLUTION, RESIZABLE)
-        self.font = pygame.font.Font(FONT, HEIGHT)
-        self.fontheight = self.font.size('X')[1]
-        self.root = self.servergateway.registerclient()
-        self.changed = {}
-        username = info.getusername()
-        try:
-            self.cursor = getattr(self.root.users, username)
-        except AttributeError:
-            self.cursor = Structure(x=0, color=info.getcolor())
-            setattr(self.root.users, username, self.cursor)
-        #print self.root.users.__dict__
-        if not hasattr(self.cursor, 'cell') or self.cursor.cell not in self.root.cells.list:
-            self.cursor.cell = self.root.cells.list[-1]
-        if not hasattr(self.cursor, 'line') or self.cursor.line not in self.cursor.cell.lines:
-            self.cursor.line = self.cursor.cell.lines[-1]
-        self.lastline_cell = self.cursor.cell
-        self.lastline = self.lastline_cell.lines[-1]
-        self.changed[self.cursor] = 1
-        self.changed[self.root.users] = 1
-
-    def repaint(self):
-        while 1:
-            self.screen.fill((255,255,255))
-            self.line2ypos = {}
-
-            index = self.lastline_cell.lines.index(self.lastline)
-            lines = self.lastline_cell.lines[:index+1]
-            ypos = self.drawlinesbackwards(lines)
-            if ypos > 0:
-                index = self.root.cells.list.index(self.lastline_cell)
-                remainingcells = self.root.cells.list[:index]
-                while remainingcells and ypos > 0:
-                    cell = remainingcells.pop()
-                    ypos = self.drawspacing(ypos)
-                    ypos = self.drawlinesbackwards(cell.lines, ypos)
-            cursor_in_view = self.drawcursors()
-            pygame.display.flip()
-            if cursor_in_view:
-                break
-            self.scroll_a_bit_towards_cursor()
-
-    def drawcursors(self):
-        cursors = self.root.users.__dict__.values()
-        assert self.cursor in cursors
-        cursor_in_view = False
-        for cursor in cursors:
-            x = cursor.x
-            line = cursor.line
-            try:
-                ypos = self.line2ypos[line]
-            except KeyError:
-                continue
-            char = line.content[x:x+1]
-            if not char:
-                char = ' '
-            charimage = self.font.render(char, 1, (255,255,255), cursor.color)
-            startoflineimagesize = self.font.size(line.content[:x])
-            self.screen.blit(charimage, (startoflineimagesize[0], ypos))
-            if cursor is self.cursor and ypos >= 0:
-                cursor_in_view = True
-        return cursor_in_view
-
-    def drawspacing(self, ypos):
-        self.screen.fill((160, 160, 160), Rect(0, ypos-2, 100, 1))
-        return ypos - 3
-    
-    def drawlinesbackwards(self, lines, ypos = None):
-        if ypos is None:
-            ypos = self.screen.get_size()[1]
-        l = lines[:]
-        while l and ypos > 0:
-            line = l.pop()
-            lineimage = self.font.render(line.content or ' ', 1, (0,0,0))
-            ypos -= self.fontheight
-            self.screen.blit(lineimage, (0, ypos))
-            self.line2ypos[line] = ypos
-        return ypos
-                
-    def scroll_a_bit_towards_cursor(self):
-        if self.cursor.cell is self.lastline_cell:
-            cursorindex = self.cursor.cell.lines.index(self.cursor.line)
-            lastlineindex = self.lastline_cell.lines.index(self.lastline)
-        else:
-            cursorindex = self.root.cells.list.index(self.cursor.cell)
-            lastlineindex = self.root.cells.list.index(self.lastline_cell)
-        if cursorindex < lastlineindex:
-            self.lastline, self.lastline_cell = self.previousline(self.lastline, self.lastline_cell)
-        else:
-            self.lastline, self.lastline_cell = self.nextline(self.lastline, self.lastline_cell)
-
-    def previousline(self, line, cell):
-        index = cell.lines.index(line)
-        if index > 0:
-            return cell.lines[index-1], cell
-        else:
-            # XXX this changes if we have output cells ...
-            index = self.root.cells.list.index(cell)
-            if index > 0:
-                cell = self.root.cells.list[index-1]
-                return cell.lines[-1], cell
-        return line, cell
-
-    def nextline(self, line, cell):
-        index = cell.lines.index(line)
-        if index + 1 < len(cell.lines):
-            return cell.lines[index+1], cell
-        else:
-            # XXX this changes if we have output cells ...
-            index = self.root.cells.list.index(cell)
-            if index + 1 < len(self.root.cells.list):
-                cell = self.root.cells.list[index+1]
-                return cell.lines[0], cell
-        return line, cell
-
-##    def drawcell(self, screen, cell):
-##        self.scroll_cursor_into_view(screen)
-##        start = -self.vscroll//self.fontheight
-##        stop = (-self.vscroll + screen.get_size()[1])//self.fontheight + 1
-##        ypos = self.vscroll + start*self.fontheight
-##        for line in cell.lines[start:stop]:
-##            lineimage = self.font.render(line or ' ', 1, (0,0,0))
-##            screen.blit(lineimage, (0, ypos))
-##            ypos += self.fontheight
-##        self.drawcursors(screen, cell)
-
-##    def char_pos(self, cell, x, y):
-##        line = cell.lines[y]
-##        return (self.font.size(line[:x])[0],
-##                y*self.fontheight + self.vscroll)
-
-##    def char_rect(self, cell, x, y):
-##        c = cell.lines[y][x:x+1]
-##        if not c:
-##            c = ' '
-##        return Rect(self.char_pos(x, y), self.font.size(c))
-
-##    def scroll_cursor_into_view(self, screen):
-##        ypos = self.vscroll + self.cursor.y * self.fontheight
-##        if ypos < 0:
-##            self.vscroll -= ypos
-##        elif ypos + self.fontheight > screen.get_size()[1]:
-##            self.vscroll -= ypos + self.fontheight - screen.get_size()[1]
-
-##    def drawcursors(self, screen, cell):
-##        for cursor in self.root.users.__dict__.values():
-##            x = cursor.x
-##            y = cursor.y
-##            if y >= len(cell.lines):
-##                y = len(cell.lines)-1
-##            char = cell.lines[y][x:x+1]
-##            if not char:
-##                char = ' '
-##            charimage = self.font.render(char, 1, (255,255,255), cursor.color)
-##            screen.blit(charimage, self.char_pos(cell, x, y))
-
-    def PRINTABLE_KEY(self, event):
-        assert event.unicode
-        x = self.cursor.x
-        line = self.cursor.line
-        line.content = line.content[:x] + event.unicode + line.content[x:]
-        self.cursor.x += len(event.unicode)
-        self.changed[line] = 1
-
-    def CTRL_K_a(self, event):
-        self.cursor.x = 0
-
-    def CTRL_K_e(self, event):
-        self.cursor.x = len(self.cursor.line.content)
-        
-    def K_UP(self, event):
-        self.cursor.line, self.cursor.cell = self.previousline(self.cursor.line, self.cursor.cell)
-                
-    def K_DOWN(self, event):
-        self.cursor.line, self.cursor.cell = self.nextline(self.cursor.line, self.cursor.cell)
-
-    def K_LEFT(self, event):
-        if self.cursor.x > 0:
-            self.cursor.x -= 1
-
-    def K_RIGHT(self, event):
-        if self.cursor.x < len(self.cursor.line.content):
-            self.cursor.x += 1
-
-    def K_BACKSPACE(self, event):
-        x = self.cursor.x
-        line = self.cursor.line
-        if x > 0:
-            line.content = line.content[:x-1] + line.content[x:]
-            self.changed[line] = 1
-            self.cursor.x -= 1
-        else:
-            index = self.cursor.cell.lines.index(line)
-            if index > 0:
-                prevline = self.cursor.cell.lines[index-1]
-                prevline.content += line.content
-                del self.cursor.cell.lines[index]
-                self.changed[prevline] = 1
-                self.changed[self.cursor.cell] = 1
-                self.cursor.line = prevline
-                self.cursor.x = len(prevline.content)
-
-    def K_RETURN(self, event):
-        x = self.cursor.x
-        line = self.cursor.line
-        newline = Structure(content=line.content[x:])
-        line.content = line.content[:x]
-        index = self.cursor.cell.lines.index(line)
-        self.cursor.cell.lines.insert(index+1, newline)
-        self.cursor.line = newline
-        self.cursor.x = 0
-        self.changed[self.cursor.cell] = 1
-        self.changed[line] = 1
-        self.changed[newline] = 1
-
-    def ALT_K_RETURN(self, event):
-        inputcell = self.cursor.cell
-        lines = [line.content for line in inputcell.lines]
-        outputcell = getattr(inputcell, 'outputcell', None)
-        if outputcell is None:
-            outputcell = Structure()
-            inputcell.outputcell = outputcell
-            index = self.root.cells.list.index(inputcell)
-            self.root.cells.list.insert(index+1, outputcell)
-            self.changed[inputcell] = 1
-            self.changed[self.root.cells] = 1
-        outputcell.lines = [Structure(content="")]
-        self.changed[outputcell] = 1
-        self.changed[outputcell.lines[0]] = 1
-        def fill_output_cell(value):
-            lines = value.split('\n')
-            outputcell.lines = [Structure(content=line) for line in lines]
-            self.changed[outputcell] = 1
-            for s in outputcell.lines:
-                self.changed[s] = 1
-        self.execgateway.userexec_remote(lines, fill_output_cell)
-
-    def run(self):
-        self.invalid = True
-        while 1:
-            if self.changed:
-                self.servergateway.notifychanges(*self.changed.keys())
-                self.changed.clear()
-                self.invalid = True
-            if self.invalid:
-                self.invalid = False
-                self.repaint()
-            event = pygame.event.wait()
-            print event
-            if event.type == KEYDOWN:
-                keynames = []
-                if event.key in KEYMAP:
-                    keyname = KEYMAP[event.key]
-                    if event.mod & KMOD_ALT:
-                        keyname = 'ALT_' + keyname
-                    if event.mod & KMOD_CTRL:
-                        keyname = 'CTRL_' + keyname
-                    keynames.append(keyname)
-                if event.unicode:
-                    keynames.append("PRINTABLE_KEY")
-                for keyname in keynames:
-                    method = getattr(self, keyname, None)
-                    if method:
-                        method(event)
-                        self.changed[self.cursor] = 1
-                        break
-            if event.type == QUIT:
-                break
-            if event.type == VIDEORESIZE:
-                self.screen = pygame.display.set_mode(event.size, RESIZABLE)
-                self.invalid = True
-
-    def close(self):
-        print "trying to quit the gateway ..."
-        self.servergateway.exit()
-        print "calling pygame.quit()"
-        pygame.quit()
-
-    def postrepaintevent(self):
-        self.invalid = True
-        pygame.event.post(pygame.event.Event(WAKEUPEVENT))
-
-
-if __name__ == '__main__':
-    sharedserver = sys.argv[1]
-    if len(sys.argv) > 2:
-        execserver = sys.argv[2]
-    else:
-        execserver = 'localhost:8888'
-    t = Terminal(sharedserver, execserver)
-    try:
-        t.run()
-    finally:
-        t.close()
-

Added: shpy/trunk/dist/shpy/ui_pygame/__init__.py
==============================================================================

Added: shpy/trunk/dist/shpy/ui_pygame/decorate.py
==============================================================================
--- (empty file)
+++ shpy/trunk/dist/shpy/ui_pygame/decorate.py	Thu Jan 22 19:08:33 2004
@@ -0,0 +1,156 @@
+from __future__ import generators
+import inspect, new
+from shpy.net.structure import Structure, ATOMIC
+
+
+class Decorator:
+    #typeid = 'structure:0'   # should always be overridden by subclasses
+    #slots = ()
+
+    def __init__(self, **kwds):
+        if not hasattr(self.__class__, 'typeid2class'):
+            raise TypeError, 'cannot instantiate class (use collectdecorators)'
+        _value = Structure(typeid = self.__class__.typeid)
+        self.__dict__['_value'] = _value
+        for key, value in kwds.items():
+            if key not in self.__class__.slots:
+                raise TypeError, 'no %r slot allowed' % (key,)
+            setattr(self._value, key, undecorate(value))
+        self.init_more()
+        self.changed()
+
+    def init_more(self):
+        pass
+
+    def __eq__(self, other):
+        return self.__class__ is other.__class__ and self._value is other._value
+
+    def __ne__(self, other):
+        return not (self == other)
+
+    def __hash__(self):
+        return id(self._value)
+
+    def __getattr__(self, attr):
+        x = getattr(self._value, attr)
+        return self.decorate(x)
+    
+    def __setattr__(self, attr, value):
+        if attr not in self.__class__.slots:
+            self.__dict__[attr] = value
+        else:
+            setattr(self._value, attr, undecorate(value))
+            self.changed()
+
+    def changed(self):
+        self._changedict[self._value] = True
+
+    def decorate(cls, value):
+        if isinstance(value, Structure):
+            typeid2class = cls.typeid2class
+            try:
+                classobj = typeid2class[value.typeid]
+            except KeyError:
+                raise DecorateError, 'no decorator for %r' % (value.typeid,)
+            decorator = new.instance(classobj, {'_value': value})
+            decorator.init_more()
+            return decorator
+        elif isinstance(value, ATOMIC):
+            return value
+        elif isinstance(value, tuple):
+            return tuple([cls.decorate(v) for v in value])
+        else:
+            raise TypeError, '%r cannot be decorated' % (value,)
+    decorate = classmethod(decorate)
+
+
+class List(Decorator):
+    typeid = 'list:0'
+    slots = 'listitems',
+
+    def __init__(self, items=[]):
+        Decorator.__init__(self)
+        self._value.listitems = []
+        for item in items:
+            self.append(item)
+
+    def append(self, item):
+        self._value.listitems.append(undecorate(item))
+        self.changed()
+
+    def insertafter(self, previtem, newitem):
+        previtem = undecorate(previtem)
+        newitem  = undecorate(newitem)
+        index = self._value.listitems.index(previtem)
+        self._value.listitems.insert(index+1, newitem)
+        self.changed()
+
+    def insertbefore(self, nextitem, newitem):
+        nextitem = undecorate(nextitem)
+        newitem  = undecorate(newitem)
+        index = self._value.listitems.index(nextitem)
+        self._value.listitems.insert(index, newitem)
+        self.changed()
+
+    def remove(self, item):
+        index = self.index(item)
+        del self._value.listitems[index]
+        self.changed()
+
+    def prev(self, item):
+        item = undecorate(item)
+        index = self._value.listitems.index(item)
+        if index > 0:
+            return self.decorate(self._value.listitems[index-1])
+        else:
+            raise IndexError
+
+    def next(self, item):
+        item = undecorate(item)
+        index = self._value.listitems.index(item)
+        return self.decorate(self._value.listitems[index+1])
+
+    def __getitem__(self, index):
+        return self.decorate(self._value.listitems[index])
+
+    def __len__(self):
+        return len(self._value.listitems)
+
+    def index(self, item):
+        item = undecorate(item)
+        return self._value.listitems.index(item)
+
+    def __iter__(self):
+        for listitem in self._value.listitems:
+            yield self.decorate(listitem)
+
+    def iterfrom(self, item):
+        index = self.index(item)
+        for listitem in self._value.listitems[index:]:
+            yield self.decorate(listitem)
+
+
+def collectdecorators(changedict, classes):
+    result = {}
+    typeid2class = {}
+    for existingcls in classes:
+        newcls = new.classobj(existingcls.__name__, (existingcls,),
+                              {'typeid2class': typeid2class,
+                               '_changedict':  changedict})
+        typeid2class[existingcls.typeid] = newcls
+        result[existingcls.__name__] = newcls
+    return result
+
+
+class DecorateError(Exception):
+    pass
+
+def undecorate(value):
+    if isinstance(value, Decorator):
+        return value._value
+    elif isinstance(value, ATOMIC):
+        return value
+    elif isinstance(value, tuple):
+        return tuple([undecorate(v) for v in value])
+    else:
+        raise TypeError, '%r is not simple enough to be stored' % (value,)

Copied: shpy/trunk/dist/shpy/ui_pygame/ui_pygame.py (from r2873, shpy/trunk/dist/shpy/ui_pygame.py)
==============================================================================
--- shpy/trunk/dist/shpy/ui_pygame.py	(original)
+++ shpy/trunk/dist/shpy/ui_pygame/ui_pygame.py	Thu Jan 22 19:08:33 2004
@@ -4,8 +4,9 @@
 import pygame
 from pygame.locals import *
 import shpy.net.register
-from shpy.net.structure import Structure, representstructure, getstructureid
-from shpy import info 
+from shpy.net.structure import representstructure, getstructureid
+from shpy import info
+from shpy.ui_pygame import decorate
 
 RESOLUTION = (768, 512)
 #RESOLUTION = (300, 300)
@@ -31,6 +32,78 @@
 ##    return image
 
 
+class Cursor(decorate.Decorator):
+    typeid = 'cursor:0'
+    slots = 'x','line','cell','color','name',
+
+    def init_more(self):
+        self.fixme()
+        self.lineindex = self.cell.lines.index(self.line)
+
+    def moveup(self):
+        try:
+            self.line = self.cell.lines.prev(self.line)
+        except IndexError:
+            self.cell = self.terminal.root.cells.prev(self.cell)
+            self.line = self.cell.lines[-1]
+
+    def movedown(self):
+        try:
+            self.line = self.cell.lines.next(self.line)
+        except IndexError:
+            self.cell = self.terminal.root.cells.next(self.cell)
+            self.line = self.cell.lines[0]
+
+    def moveleft(self):
+        x, line = self.xline()
+        if x > 0:
+            self.x = x - 1
+        else:
+            self.line = self.cell.lines.prev(line)
+            self.x = len(self.line.content)
+
+    def moveright(self):
+        x, line = self.xline()
+        if x < len(line.content):
+            self.x = x + 1
+        else:
+            self.line = self.cell.lines.next(line)
+            self.x = 0
+
+    def isbelow(self, other):
+        if self.cell == other.cell:
+            selfindex  = self .cell.lines.index(self .line)
+            otherindex = other.cell.lines.index(other.line)
+        else:
+            selfindex  = self .terminal.root.cells.index(self .cell)
+            otherindex = other.terminal.root.cells.index(other.cell)
+        return selfindex > otherindex
+
+    def xline(self):
+        if self.x > len(self.line.content):
+            self.x = len(self.line.content)
+        return self.x, self.line
+
+    def fixme(self):
+        if self.cell not in self.terminal.root.cells:
+            self.cell = self.terminal.root.cells[0]  # or somewhere else
+        if self.line not in self.cell.lines:
+            self.line = self.cell.lines[0]
+
+
+class Root(decorate.Decorator):
+    typeid = 'root:0'
+    slots = 'cells', 'users',
+
+class Cell(decorate.Decorator):
+    typeid = 'cell:0'
+    slots = 'lines',
+
+class Line(decorate.Decorator):
+    typeid = 'line:0'
+    slots = 'content',
+
+
 class Terminal:
     
     def __init__(self, sharedserver, execserver):
@@ -43,39 +116,45 @@
         self.screen = pygame.display.set_mode(RESOLUTION, RESIZABLE)
         self.font = pygame.font.Font(FONT, HEIGHT)
         self.fontheight = self.font.size('X')[1]
-        self.root = self.servergateway.registerclient()
-        self.changed = {}
+        self.builddecorators()
+        self.root = self.Root.decorate(self.servergateway.registerclient())
         username = info.getusername()
-        try:
-            self.cursor = getattr(self.root.users, username)
-        except AttributeError:
-            self.cursor = Structure(x=0, color=info.getcolor())
-            setattr(self.root.users, username, self.cursor)
-        #print self.root.users.__dict__
-        if not hasattr(self.cursor, 'cell') or self.cursor.cell not in self.root.cells.list:
-            self.cursor.cell = self.root.cells.list[-1]
-        if not hasattr(self.cursor, 'line') or self.cursor.line not in self.cursor.cell.lines:
-            self.cursor.line = self.cursor.cell.lines[-1]
-        self.lastline_cell = self.cursor.cell
-        self.lastline = self.lastline_cell.lines[-1]
-        self.changed[self.cursor] = 1
-        self.changed[self.root.users] = 1
+        for cursor in self.root.users:
+            if cursor.name == username:
+                self.cursor = cursor
+                break
+        else:
+            self.cursor = self.Cursor(name=username, x=0, color=info.getcolor(),
+                                      cell=None, line=None)
+            self.root.users.append(self.cursor)
+        self.firstline = self.Cursor(x=0, cell=None, line=None)#, _private=True)
+
+    def builddecorators(self):
+        self.changed = {}
+        decorators = decorate.collectdecorators(self.changed, [
+            Root, Cursor, Cell, Line, decorate.List])
+        for name, cls in decorators.items():
+            cls.terminal = self
+            setattr(self, name, cls)
 
     def repaint(self):
+        screenheight = self.screen.get_size()[1]
         while 1:
             self.screen.fill((255,255,255))
             self.line2ypos = {}
-
-            index = self.lastline_cell.lines.index(self.lastline)
-            lines = self.lastline_cell.lines[:index+1]
-            ypos = self.drawlinesbackwards(lines)
-            if ypos > 0:
-                index = self.root.cells.list.index(self.lastline_cell)
-                remainingcells = self.root.cells.list[:index]
-                while remainingcells and ypos > 0:
-                    cell = remainingcells.pop()
+            ypos = 0
+            line_inside_cell = self.firstline.line
+            for cell in self.root.cells.iterfrom(self.firstline.cell):
+                if line_inside_cell is not None:
+                    lines = cell.lines.iterfrom(line_inside_cell)
+                    line_inside_cell = None
+                else:
+                    lines = cell.lines
                     ypos = self.drawspacing(ypos)
-                    ypos = self.drawlinesbackwards(cell.lines, ypos)
+                ypos = self.drawlinesforward(lines, ypos)
+                if ypos >= screenheight:
+                    break
+                
             cursor_in_view = self.drawcursors()
             pygame.display.flip()
             if cursor_in_view:
@@ -83,10 +162,9 @@
             self.scroll_a_bit_towards_cursor()
 
     def drawcursors(self):
-        cursors = self.root.users.__dict__.values()
-        assert self.cursor in cursors
+        assert self.cursor in self.root.users
         cursor_in_view = False
-        for cursor in cursors:
+        for cursor in self.root.users:
             x = cursor.x
             line = cursor.line
             try:
@@ -99,110 +177,36 @@
             charimage = self.font.render(char, 1, (255,255,255), cursor.color)
             startoflineimagesize = self.font.size(line.content[:x])
             self.screen.blit(charimage, (startoflineimagesize[0], ypos))
-            if cursor is self.cursor and ypos >= 0:
+            if cursor == self.cursor and ypos >= 0:
                 cursor_in_view = True
         return cursor_in_view
 
     def drawspacing(self, ypos):
-        self.screen.fill((160, 160, 160), Rect(0, ypos-2, 100, 1))
-        return ypos - 3
+        self.screen.fill((160, 160, 160), Rect(0, ypos+1, 100, 1))
+        return ypos + 3
     
-    def drawlinesbackwards(self, lines, ypos = None):
-        if ypos is None:
-            ypos = self.screen.get_size()[1]
-        l = lines[:]
-        while l and ypos > 0:
-            line = l.pop()
+    def drawlinesforward(self, lines, ypos):
+        screenheight = self.screen.get_size()[1]
+        for line in lines:
+            if ypos > screenheight:
+                break
             lineimage = self.font.render(line.content or ' ', 1, (0,0,0))
-            ypos -= self.fontheight
             self.screen.blit(lineimage, (0, ypos))
             self.line2ypos[line] = ypos
+            ypos += self.fontheight
         return ypos
                 
     def scroll_a_bit_towards_cursor(self):
-        if self.cursor.cell is self.lastline_cell:
-            cursorindex = self.cursor.cell.lines.index(self.cursor.line)
-            lastlineindex = self.lastline_cell.lines.index(self.lastline)
-        else:
-            cursorindex = self.root.cells.list.index(self.cursor.cell)
-            lastlineindex = self.root.cells.list.index(self.lastline_cell)
-        if cursorindex < lastlineindex:
-            self.lastline, self.lastline_cell = self.previousline(self.lastline, self.lastline_cell)
-        else:
-            self.lastline, self.lastline_cell = self.nextline(self.lastline, self.lastline_cell)
-
-    def previousline(self, line, cell):
-        index = cell.lines.index(line)
-        if index > 0:
-            return cell.lines[index-1], cell
-        else:
-            # XXX this changes if we have output cells ...
-            index = self.root.cells.list.index(cell)
-            if index > 0:
-                cell = self.root.cells.list[index-1]
-                return cell.lines[-1], cell
-        return line, cell
-
-    def nextline(self, line, cell):
-        index = cell.lines.index(line)
-        if index + 1 < len(cell.lines):
-            return cell.lines[index+1], cell
-        else:
-            # XXX this changes if we have output cells ...
-            index = self.root.cells.list.index(cell)
-            if index + 1 < len(self.root.cells.list):
-                cell = self.root.cells.list[index+1]
-                return cell.lines[0], cell
-        return line, cell
-
-##    def drawcell(self, screen, cell):
-##        self.scroll_cursor_into_view(screen)
-##        start = -self.vscroll//self.fontheight
-##        stop = (-self.vscroll + screen.get_size()[1])//self.fontheight + 1
-##        ypos = self.vscroll + start*self.fontheight
-##        for line in cell.lines[start:stop]:
-##            lineimage = self.font.render(line or ' ', 1, (0,0,0))
-##            screen.blit(lineimage, (0, ypos))
-##            ypos += self.fontheight
-##        self.drawcursors(screen, cell)
-
-##    def char_pos(self, cell, x, y):
-##        line = cell.lines[y]
-##        return (self.font.size(line[:x])[0],
-##                y*self.fontheight + self.vscroll)
-
-##    def char_rect(self, cell, x, y):
-##        c = cell.lines[y][x:x+1]
-##        if not c:
-##            c = ' '
-##        return Rect(self.char_pos(x, y), self.font.size(c))
-
-##    def scroll_cursor_into_view(self, screen):
-##        ypos = self.vscroll + self.cursor.y * self.fontheight
-##        if ypos < 0:
-##            self.vscroll -= ypos
-##        elif ypos + self.fontheight > screen.get_size()[1]:
-##            self.vscroll -= ypos + self.fontheight - screen.get_size()[1]
-
-##    def drawcursors(self, screen, cell):
-##        for cursor in self.root.users.__dict__.values():
-##            x = cursor.x
-##            y = cursor.y
-##            if y >= len(cell.lines):
-##                y = len(cell.lines)-1
-##            char = cell.lines[y][x:x+1]
-##            if not char:
-##                char = ' '
-##            charimage = self.font.render(char, 1, (255,255,255), cursor.color)
-##            screen.blit(charimage, self.char_pos(cell, x, y))
+        if self.cursor.isbelow(self.firstline):
+           self.firstline.movedown()
+        else: 
+           self.firstline.moveup()
 
     def PRINTABLE_KEY(self, event):
         assert event.unicode
-        x = self.cursor.x
-        line = self.cursor.line
+        x, line = self.cursor.xline()
         line.content = line.content[:x] + event.unicode + line.content[x:]
         self.cursor.x += len(event.unicode)
-        self.changed[line] = 1
 
     def CTRL_K_a(self, event):
         self.cursor.x = 0
@@ -211,78 +215,60 @@
         self.cursor.x = len(self.cursor.line.content)
         
     def K_UP(self, event):
-        self.cursor.line, self.cursor.cell = self.previousline(self.cursor.line, self.cursor.cell)
+        self.cursor.moveup()
                 
     def K_DOWN(self, event):
-        self.cursor.line, self.cursor.cell = self.nextline(self.cursor.line, self.cursor.cell)
+        self.cursor.movedown()
 
     def K_LEFT(self, event):
-        if self.cursor.x > 0:
-            self.cursor.x -= 1
+        self.cursor.moveleft()
 
     def K_RIGHT(self, event):
-        if self.cursor.x < len(self.cursor.line.content):
-            self.cursor.x += 1
+        self.cursor.moveright()
 
-    def K_BACKSPACE(self, event):
-        x = self.cursor.x
-        line = self.cursor.line
-        if x > 0:
-            line.content = line.content[:x-1] + line.content[x:]
-            self.changed[line] = 1
-            self.cursor.x -= 1
+    def K_DELETE(self, event):
+        x, line = self.cursor.xline()
+        if x < len(line.content):
+            line.content = line.content[:x] + line.content[x+1:]
         else:
-            index = self.cursor.cell.lines.index(line)
-            if index > 0:
-                prevline = self.cursor.cell.lines[index-1]
-                prevline.content += line.content
-                del self.cursor.cell.lines[index]
-                self.changed[prevline] = 1
-                self.changed[self.cursor.cell] = 1
-                self.cursor.line = prevline
-                self.cursor.x = len(prevline.content)
+            nextline = self.cursor.cell.lines.next(line)
+            line.content += nextline.content
+            self.cursor.cell.lines.remove(nextline)
 
+    def K_BACKSPACE(self, event):
+        self.cursor.moveleft()
+        self.K_DELETE(event)   # hack
+        
     def K_RETURN(self, event):
         x = self.cursor.x
         line = self.cursor.line
-        newline = Structure(content=line.content[x:])
+        newline = self.Line(content=line.content[x:])
         line.content = line.content[:x]
-        index = self.cursor.cell.lines.index(line)
-        self.cursor.cell.lines.insert(index+1, newline)
+        self.cursor.cell.lines.insertafter(line, newline)
         self.cursor.line = newline
         self.cursor.x = 0
-        self.changed[self.cursor.cell] = 1
-        self.changed[line] = 1
-        self.changed[newline] = 1
 
     def ALT_K_RETURN(self, event):
         inputcell = self.cursor.cell
         lines = [line.content for line in inputcell.lines]
         outputcell = getattr(inputcell, 'outputcell', None)
         if outputcell is None:
-            outputcell = Structure()
+            outputcell = self.Cell()
             inputcell.outputcell = outputcell
-            index = self.root.cells.list.index(inputcell)
-            self.root.cells.list.insert(index+1, outputcell)
-            self.changed[inputcell] = 1
-            self.changed[self.root.cells] = 1
-        outputcell.lines = [Structure(content="")]
-        self.changed[outputcell] = 1
-        self.changed[outputcell.lines[0]] = 1
+            self.root.cells.insertafter(inputcell, outputcell)
+        outputcell.lines = self.List([self.Line(content="")])
         def fill_output_cell(value):
             lines = value.split('\n')
-            outputcell.lines = [Structure(content=line) for line in lines]
-            self.changed[outputcell] = 1
-            for s in outputcell.lines:
-                self.changed[s] = 1
+            outputcell.lines = self.List([self.Line(content=line) for line in lines])
         self.execgateway.userexec_remote(lines, fill_output_cell)
 
     def run(self):
         self.invalid = True
         while 1:
             if self.changed:
-                self.servergateway.notifychanges(*self.changed.keys())
+                keys = self.changed.keys()
                 self.changed.clear()
+                self.servergateway.notifychanges(*keys)
                 self.invalid = True
             if self.invalid:
                 self.invalid = False
@@ -303,8 +289,10 @@
                 for keyname in keynames:
                     method = getattr(self, keyname, None)
                     if method:
-                        method(event)
-                        self.changed[self.cursor] = 1
+                        try:
+                            method(event)
+                        except IndexError:
+                            print "XXX flash red"
                         break
             if event.type == QUIT:
                 break