"""
ajaxWM - the free web based window manager for remote terminals
Copyright (C) 2007 Dennis Felsing, Andreas Waidler

This program 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.

This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.

$Id: Terminal.py 76 2007-12-15 11:26:24Z r0q $
"""

#{{{ Imports
import re
import array
import cgi

import ajaxwm
#}}}
class Terminal:#{{{
    """
Represents the contents of a terminal, with wich it can be populated 
with. Printing the contents can either be done as pure unicode and included
in a pre-tag for being directly used in HTML.

Up to now there are still some problems with encoding, if you don't use
Unicode (Unicode should work without problems, _should_, doesn't at some
extended characters). A class for handling the process of encoding and
decoding is already planned.
    """
    def __init__(self, width=80, height=24):#{{{
        self.width = width
        self.height = height
        self.init()
        self.reset()
    #}}}
    def init(self):#{{{
        """
Initialization of the escape-, the csi-sequences, the regex and the
to-html-translation table.
        """
        self.esc_seq = {u'\x00': None,
                        u'\x05': self.esc_da,  
                        u'\x07': None,
                        u'\x08': self.esc_0x08,
                        u'\x09': self.esc_0x09,
                        u'\x0a': self.esc_0x0a,
                        u'\x0b': self.esc_0x0a,
                        u'\x0c': self.esc_0x0a,
                        u'\x0d': self.esc_0x0d,
                        u'\x0e': None,
                        u'\x0f': None,
                        u'\x1b#8': None,
                        u'\x1b=': None,
                        u'\x1b>': None,
                        u'\x1b(0': None,
                        u'\x1b(A': None,
                        u'\x1b(B': None,
                        u'\x1b[c': self.esc_da,
                        u'\x1b[0c': self.esc_da,
                        u'\x1b]R': None,
                        u'\x1b7': self.esc_save,
                        u'\x1b8': self.esc_restore,
                        u'\x1bD': None,
                        u'\x1bE': None,
                        u'\x1bH': None,
                        u'\x1bM': self.esc_ri,
                        u'\x1bN': None,
                        u'\x1bO': None,
                        u'\x1bZ': self.esc_da,
                        u'\x1ba': None,
                        u'\x1bc': self.reset,
                        u'\x1bn': None,
                        u'\x1bo': None}
        for k, v in self.esc_seq.items():
            if v == None:
                self.esc_seq[k] = self.esc_ignore
        # regex
        d = {r'\[\??([0-9;]*)([@ABCDEFGHJKLMPXacdefghlmnqrstu`])': 
             self.csi_dispatch, r'\]([^\x07]+)\x07': self.esc_ignore}
        self.esc_re = []
        for k,v in d.items():
            self.esc_re.append((re.compile(''.join((u'\x1b', k))), v))
        # define csi sequences
        self.csi_seq = {'@': (self.csi_at, [1]),
                        '`': (self.csi_G, [1]),
                        'J': (self.csi_J, [0]),
                        'K': (self.csi_K, [0])}
        for i in map(lambda i: i[4], filter(lambda i: i.startswith('csi_') and
                                                      len(i) == 5, dir(self))):
            if not self.csi_seq.has_key(i):
                self.csi_seq[i] = (getattr(self, ''.join(('csi_', i))), [1])
        # First 33 characters are not printable. (^Z excluded)
        # Tuple instead of List to increase performance.
        validChars = tuple(range(0x20, 0xffff))
        self.trUnicode = (0x20,) * 0x20 + validChars
        self.trHTML = [0xa0] * 0x20
        self.trHTML[0x0a] = 0x0a
        self.trHTML = tuple(self.trHTML) + validChars
    #}}}
    def resize(self, width, height):#{{{
        """Resizes the Terminal. (Just says where to break line and how many 
        lines there are.)
        """
        self.width = width
        self.height = height
    #}}}
    def reset(self, s=''):#{{{
        """Resets all values."""
        # self.scr and self.sgr:
        # 0 x 0   0   0   0   0   0
        #     V   V   \           /
        #     bg  fg      char
        #     0-9 0-9   0-0xfffd
        self.scr = array.array('i', [0x070000] * (self.width * self.height))
        self.st = 0
        self.sb = self.height - 1
        self.cx_bak = self.cx = 0
        self.cy_bak = self.cy = 0
        self.cl = 0
        self.sgr = 0x070000
        self.buf = []
        self.outbuf = []
        self.last_html = u''
    #}}}
    def peek(self, y1, x1, y2, x2):#{{{
        return self.scr[self.width * y1 + x1:self.width * y2 + x2]
    #}}}
    def poke(self, y, x, s):#{{{
        pos = self.width * y + x
        self.scr[pos:pos + len(s)] = s
    #}}}
    def zero(self, y1, x1, y2, x2):#{{{
        w = self.width * (y2 - y1) + x2 - x1 + 1
        z = array.array('i', [0x070000] * w)
        self.scr[self.width * y1 + x1:self.width * y2 + x2 + 1] = z
    #}}}
    def scroll_up(self, y1, y2):#{{{
        self.poke(y1, 0, self.peek(y1 + 1, 0, y2, self.width))
        self.zero(y2, 0, y2, self.width - 1)
    #}}}
    def scroll_down(self, y1, y2):#{{{
        self.poke(y1 + 1, 0, self.peek(y1, 0, y2 - 1, self.width))
        self.zero(y1, 0, y1, self.width - 1)
    #}}}
    def scroll_right(self, y, x):#{{{
        self.poke(y, x + 1, self.peek(y, x, y, self.width))
        self.zero(y, x, y, x)
    #}}}
    def cursor_down(self):#{{{
        if self.cy >= self.st and self.cy <= self.sb:
            self.cl = 0
            q, r = divmod(self.cy + 1, self.sb + 1)
            if q:
                self.scroll_up(self.st, self.sb)
                self.cy = self.sb
            else:
                self.cy = r
    #}}}
    def cursor_right(self):#{{{
        q, r = divmod(self.cx + 1, self.width)
        if q:
            self.cl = 1
        else:
            self.cx = r
    #}}}
    def echo(self, c):#{{{
        if self.cl:
            self.cursor_down()
            self.cx = 0
        #print hex(self.sgr | ord(c))
        self.scr[(self.cy * self.width) + self.cx] = self.sgr | ord(c)
        self.cursor_right()
    #}}}
    def esc_0x08(self, s):#{{{
        self.cx = max(0, self.cx - 1)
    #}}}
    def esc_0x09(self, s):#{{{
        x = self.cx + 8
        q, r = divmod(x, 8)
        self.cx = (q * 8) % self.width
    #}}}
    def esc_0x0a(self, s):#{{{
        self.cursor_down()
    #}}}
    def esc_0x0d(self, s):#{{{
        self.cl = 0
        self.cx = 0
    #}}}
    def esc_save(self,s):#{{{
        self.cx_bak = self.cx
        self.cy_bak = self.cy
    #}}}
    def esc_restore(self, s):#{{{
        self.cx = self.cx_bak
        self.cy = self.cy_bak
        self.cl = 0
    #}}}
    def esc_da(self, s):#{{{
        self.outbuf = [i for i in u'\x1b[?6c']
    #}}}
    def esc_ri(self, s):#{{{
        self.cy = max(self.st, self.cy - 1)
        if self.cy == self.st:
            self.scroll_down(self.st, self.sb)
    #}}}
    def esc_ignore(self, *s):#{{{
        pass
        #print 'term:ignore: %s' % repr(s)
    #}}}
    def csi_dispatch(self, seq, mo):#{{{
        # CSI sequences
        s = mo.group(1)
        c = mo.group(2)
        f = self.csi_seq.get(c, None)
        if f:
            try:
                l = map(lambda i: min(int(i), 1024), 
                        filter(lambda i: len(i) < 4, s.split(';')))
            except ValueError:
                l = []
            if len(l) == 0:
                l = f[1]
            f[0](l)
        #else:
        #   print 'csi ignore', c, l
    #}}}
    def csi_at(self, l):#{{{
        for i in range(l[0]):
            self.scroll_right(self.cy, self.cx)
    #}}}
    def csi_A(self, l):#{{{
        self.cy = max(self.st, self.cy - l[0])
    #}}}
    def csi_B(self, l):#{{{
        self.cy = min(self.sb, self.cy + l[0])
    #}}}
    def csi_C(self, l):#{{{
        self.cx = min(self.width - 1, self.cx + l[0])
        self.cl = 0
    #}}}
    def csi_D(self, l):#{{{
        self.cx = max(0, self.cx - l[0])
        self.cl = 0
    #}}}
    def csi_E(self, l):#{{{
        self.csi_B(l)
        self.cx = 0
        self.cl = 0
    #}}}
    def csi_F(self, l):#{{{
        self.csi_A(l)
        self.cx = 0
        self.cl = 0
    #}}}
    def csi_G(self, l):#{{{
        self.cx = min(self.width, l[0]) - 1
    #}}}
    def csi_H(self, l):#{{{
        if len(l) < 2: 
            l = [1, 1]
        self.cx = min(self.width, l[1]) - 1
        self.cy = min(self.height, l[0]) - 1
        self.cl = 0
    #}}}
    def csi_J(self, l):#{{{
        if l[0] == 0:
            self.zero(self.cy, self.cx, self.height - 1, self.width - 1)
        elif l[0] == 1:
            self.zero(0, 0, self.cy, self.cx)
        elif l[0] == 2:
            self.zero(0, 0, self.height - 1, self.width - 1)
    #}}}
    def csi_K(self, l):#{{{
        if l[0] == 0:
            self.zero(self.cy, self.cx, self.cy, self.width - 1)
        elif l[0] == 1:
            self.zero(self.cy, 0, self.cy, self.cx)
        elif l[0] == 2:
            self.zero(self.cy, 0, self.cy, self.width - 1)
    #}}}
    def csi_L(self, l):#{{{
        for i in range(l[0]):
            if self.cy < self.sb:
                self.scroll_down(self.cy, self.sb)
    #}}}
    def csi_M(self, l):#{{{
        if self.cy >= self.st and self.cy <= self.sb:
            for i in range(l[0]):
                self.scroll_up(self.cy, self.sb)
    #}}}
    def csi_P(self, l):#{{{
        w = self.width
        cx = self.cx
        cy = self.cy
        end = self.peek(cy, cx, cy, w)
        self.csi_K([0])
        self.poke(cy, cx, end[l[0]:])
    #}}}
    def csi_X(self, l):#{{{
        self.zero(self.cy, self.cx, self.cy, self.cx + l[0])
    #}}}
    def csi_a(self, l):#{{{
        self.csi_C(l)
    #}}}
    def csi_c(self, l):#{{{
        #u'\x1b[?0c' 0-8 cursor size
        pass
    #}}}
    def csi_d(self, l):#{{{
        self.cy = min(self.height, l[0]) - 1
    #}}}
    def csi_e(self, l):#{{{
        self.csi_B(l)
    #}}}
    def csi_f(self, l):#{{{
        self.csi_H(l)
    #}}}
    def csi_h(self, l):#{{{
        if l[0] == 4:
            pass
            #print 'insert on'
    #}}}
    def csi_l(self, l):#{{{
        if l[0] == 4:
            pass
            #print 'insert off'
    #}}}
    def csi_m(self, l):#{{{
        # Select Graphic Rendition
        for i in l:
            if i in (0, 27, 39, 49):
                self.sgr = 0x070000
            elif i == 1:
                self.sgr = (self.sgr | 0x080000)
            elif i == 7:
                self.sgr = 0x700000
            elif i >= 30 and i <= 37:
                c = i - 30
                self.sgr = (self.sgr & 0xf8ffff) | (c << 16)
            elif i >= 40 and i <= 47:
                c = i - 40
                self.sgr = (self.sgr & 0x8fffff) | (c << 20)
        #   else:
        #       print 'CSI sgr ignore', l, i
        #print 'sgr: %r %x' % (l, self.sgr)
    #}}}
    def csi_r(self, l):#{{{
        if len(l) < 2: l = [0, self.height]
        self.st = min(self.height - 1, l[0] - 1)
        self.sb = min(self.height - 1, l[1] - 1)
        self.sb = max(self.st, self.sb)
    #}}}
    def csi_s(self, l):#{{{
        self.esc_save(0)
    #}}}
    def csi_u(self, l):#{{{
        self.esc_restore(0)
    #}}}
    def escape(self):#{{{
        e = u''.join(self.buf)
        if len(e) > 32:
            #print 'error %r' % e
            self.buf = []
        elif e in self.esc_seq:
            self.esc_seq[e](e)
            self.buf = []
        else:
            for r, f in self.esc_re:
                mo = r.match(e)
                if mo:
                    f(e, mo)
                    self.buf = []
                    break
        #if self.buf == []: print 'ESC %r\n' % e
    #}}}
    def write(self, s):#{{{
        for i in s:
            #print hex(ord(i)),
            if len(self.buf) or (i in self.esc_seq):
                #print '1'
                self.buf.append(i)
                self.escape()
            elif i == '\x1b':
                #print '2'
                self.buf.append(i)
            else:
                #print '3'
                self.echo(i)#}}}
    def read(self):#{{{
        b = u''.join(self.outbuf)
        self.outbuf = []
        return b
    #}}}
    def dumpunicode(self):#{{{
        """
Returns all characters of the terminal, non-printable ones excluded
in plain text.
        """
        return (''.join(map(lambda c: unichr(c & 0x00ffff), self.scr)))\
                .translate(self.trUnicode)
    #}}}
    def dumphtml(self, color=1):#{{{
        """Returns the terminal in (or not in) color as html."""
        h = self.height
        w = self.width
        r = []
        span = []
        span_bg = -1
        span_fg = -1
        for i in range(h * w):
            q, c = divmod(self.scr[i], 0x10000)
            if color:
                bg, fg = divmod(q, 0x10)
            else:
                bg = 0 # black
                fg = 7 # white
            if i == self.cy * w + self.cx:
                # cursor
                bg = 1 # red
                fg = 7 # white
            if (bg != span_bg or fg != span_fg or i == h * w - 1):
                # new color
                if len(span):
                    r.append(u'<span class="f%d b%d">%s</span>' % \
                             (span_fg, span_bg, cgi.escape(u''.join(span)\
                             .translate(self.trHTML))))
                span = []
                span_bg = bg
                span_fg = fg
            span.append(unichr(c)) 
            if i % w == w - 1:
                span.append(u'\n')
        r = u'<pre class="term">%s</pre>' % u''.join(r)
        if self.last_html == r:
            # Same result as on last run, no need to write again.
            return u'<idem></idem>'
        else:
            # Contents changed, return it all.
            self.last_html = r
            return r
    #}}}
    def __repr__(self):#{{{
        d = self.dumpunicode()
        r = []
        for i in range(self.height):
            r.append(u'|%s|\n' % d[self.width * i:self.width * (i + 1)])
        return ''.join(r).encode(ajaxwm.ENCODING)
    #}}}
#}}}

#------------------------------------------------------------------------------
# vim: fdm=marker ts=4 sw=4 expandtab 
