# -*- coding: utf-8 -*-
# This code is based on AjaxTerm/Web-Shell which included a fairly complete
# vt100 implementation as well as a stable process multiplexer.
# I made some small fixes, improved some small parts and added a Session class
# which can be used by the widget.
# License: GPL2
import sys
import os
import fcntl
import array
import threading
import time
import termios
import pty
import signal
import struct
import select
import subprocess

__version__ = "0.3"


class Terminal(object):

    def __init__(self, w, h):
        self.w = w
        self.h = h
        self.vt100_charset_graph = [
            0x25ca, 0x2026, 0x2022, 0x3f,
            0xb6, 0x3f, 0xb0, 0xb1,
            0x3f, 0x3f, 0x2b, 0x2b,
            0x2b, 0x2b, 0x2b, 0xaf,
            0x2014, 0x2014, 0x2014, 0x5f,
            0x2b, 0x2b, 0x2b, 0x2b,
            0x7c, 0x2264, 0x2265, 0xb6,
            0x2260, 0xa3, 0xb7, 0x7f
        ]
        self.vt100_esc = {
            '#8': self.esc_DECALN,
            '(A': self.esc_G0_0,
            '(B': self.esc_G0_1,
            '(0': self.esc_G0_2,
            '(1': self.esc_G0_3,
            '(2': self.esc_G0_4,
            ')A': self.esc_G1_0,
            ')B': self.esc_G1_1,
            ')0': self.esc_G1_2,
            ')1': self.esc_G1_3,
            ')2': self.esc_G1_4,
            '7': self.esc_DECSC,
            '8': self.esc_DECRC,
            '=': self.esc_DECKPAM,
            '>': self.esc_DECKPNM,
            'D': self.esc_IND,
            'E': self.esc_NEL,
            'H': self.esc_HTS,
            'M': self.esc_RI,
            'N': self.esc_SS2,
            'O': self.esc_SS3,
            'P': self.esc_DCS,
            'X': self.esc_SOS,
            'Z': self.esc_DECID,
            '[': self.esc_CSI,
            '\\': self.esc_ST,
            ']': self.esc_OSC,
            '^': self.esc_PM,
            '_': self.esc_APC,
            'c': self.reset_hard,
        }
        self.vt100_csi = {
            '@': self.csi_ICH,
            'A': self.csi_CUU,
            'B': self.csi_CUD,
            'C': self.csi_CUF,
            'D': self.csi_CUB,
            'E': self.csi_CNL,
            'F': self.csi_CPL,
            'G': self.csi_CHA,
            'H': self.csi_CUP,
            'I': self.csi_CHT,
            'J': self.csi_ED,
            'K': self.csi_EL,
            'L': self.csi_IL,
            'M': self.csi_DL,
            'P': self.csi_DCH,
            'S': self.csi_SU,
            'T': self.csi_SD,
            'W': self.csi_CTC,
            'X': self.csi_ECH,
            'Z': self.csi_CBT,
            '`': self.csi_HPA,
            'a': self.csi_HPR,
            'b': self.csi_REP,
            'c': self.csi_DA,
            'd': self.csi_VPA,
            'e': self.csi_VPR,
            'f': self.csi_HVP,
            'g': self.csi_TBC,
            'h': self.csi_SM,
            'l': self.csi_RM,
            'm': self.csi_SGR,
            'n': self.csi_DSR,
            'r': self.csi_DECSTBM,
            's': self.csi_SCP,
            'u': self.csi_RCP,
            'x': self.csi_DECREQTPARM,
            '!p': self.csi_DECSTR,
        }
        self.vt100_keyfilter_ansikeys = {
            '~': '~',
            'A': '\x1b[A',
            'B': '\x1b[B',
            'C': '\x1b[C',
            'D': '\x1b[D',
            'F': '\x1b[F',
            'H': '\x1b[H',
            '1': '\x1b[5~',
            '2': '\x1b[6~',
            '3': '\x1b[2~',
            '4': '\x1b[3~',
            'a': '\x1bOP',
            'b': '\x1bOQ',
            'c': '\x1bOR',
            'd': '\x1bOS',
            'e': '\x1b[15~',
            'f': '\x1b[17~',
            'g': '\x1b[18~',
            'h': '\x1b[19~',
            'i': '\x1b[20~',
            'j': '\x1b[21~',
            'k': '\x1b[23~',
            'l': '\x1b[24~',
        }
        self.vt100_keyfilter_appkeys = {
            '~': '~',
            'A': '\x1bOA',
            'B': '\x1bOB',
            'C': '\x1bOC',
            'D': '\x1bOD',
            'F': '\x1bOF',
            'H': '\x1bOH',
            '1': '\x1b[5~',
            '2': '\x1b[6~',
            '3': '\x1b[2~',
            '4': '\x1b[3~',
            'a': '\x1bOP',
            'b': '\x1bOQ',
            'c': '\x1bOR',
            'd': '\x1bOS',
            'e': '\x1b[15~',
            'f': '\x1b[17~',
            'g': '\x1b[18~',
            'h': '\x1b[19~',
            'i': '\x1b[20~',
            'j': '\x1b[21~',
            'k': '\x1b[23~',
            'l': '\x1b[24~',
        }
        self.reset_hard()

    # Reset functions
    def reset_hard(self):
        # Attribute mask: 0x0XFB0000
        #	X:	Bit 0 - Underlined
        #		Bit 1 - Negative
        #		Bit 2 - Concealed
        #	F:	Foreground
        #	B:	Background
        self.attr = 0x00fe0000
        # UTF-8 decoder
        self.utf8_units_count = 0
        self.utf8_units_received = 0
        self.utf8_char = 0
        # Key filter
        self.vt100_keyfilter_escape = False
        # Last char
        self.vt100_lastchar = 0
        # Control sequences
        self.vt100_parse_len = 0
        self.vt100_parse_state = ""
        self.vt100_parse_func = ""
        self.vt100_parse_param = ""
        # Buffers
        self.vt100_out = ""
        # Invoke other resets
        self.reset_screen()
        self.reset_soft()

    def reset_soft(self):
        # Attribute mask: 0x0XFB0000
        #	X:	Bit 0 - Underlined
        #		Bit 1 - Negative
        #		Bit 2 - Concealed
        #	F:	Foreground
        #	B:	Background
        self.attr = 0x00fe0000
        # Scroll parameters
        self.scroll_area_y0 = 0
        self.scroll_area_y1 = self.h
        # Character sets
        self.vt100_charset_is_single_shift = False
        self.vt100_charset_is_graphical = False
        self.vt100_charset_g_sel = 0
        self.vt100_charset_g = [0, 0]
        # Modes
        self.vt100_mode_insert = False
        self.vt100_mode_lfnewline = False
        self.vt100_mode_cursorkey = False
        self.vt100_mode_column_switch = False
        self.vt100_mode_inverse = False
        self.vt100_mode_origin = False
        self.vt100_mode_autowrap = True
        self.vt100_mode_cursor = True
        self.vt100_mode_alt_screen = False
        self.vt100_mode_backspace = False
        # Init DECSC state
        self.esc_DECSC()
        self.vt100_saved2 = self.vt100_saved
        self.esc_DECSC()

    def reset_screen(self):
        # Screen
        self.screen = array.array('i', [self.attr | 0x20] * self.w * self.h)
        self.screen2 = array.array('i', [self.attr | 0x20] * self.w * self.h)
        # Scroll parameters
        self.scroll_area_y0 = 0
        self.scroll_area_y1 = self.h
        # Cursor position
        self.cx = 0
        self.cy = 0
        # Tab stops
        self.tab_stops = list(range(0, self.w, 8))

    # UTF-8 functions
    def utf8_decode(self, d):
        # o = ''
        # for c in d:
        #    char = ord(c)
        #    if self.utf8_units_count != self.utf8_units_received:
        #        self.utf8_units_received += 1
        #        if (char & 0xc0) == 0x80:
        #            self.utf8_char = (self.utf8_char << 6) | (char & 0x3f)
        #            if self.utf8_units_count == self.utf8_units_received:
        #                if self.utf8_char<0x10000:
        #                    o += chr(self.utf8_char)
        #                self.utf8_units_count = self.utf8_units_received = 0
        #        else:
        #            o += '?'
        #            while self.utf8_units_received:
        #                o += '?'
        #                self.utf8_units_received -= 1
        #            self.utf8_units_count = 0
        #    else:
        #        if (char & 0x80) == 0x00:
        #            o += c
        #        elif (char & 0xe0) == 0xc0:
        #            self.utf8_units_count = 1
        #            self.utf8_char = char & 0x1f
        #        elif (char & 0xf0) == 0xe0:
        #            self.utf8_units_count = 2
        #            self.utf8_char = char & 0x0f
        #        elif (char & 0xf8) == 0xf0:
        #            self.utf8_units_count = 3
        #            self.utf8_char = char & 0x07
        #        else:
        #            o += '?'
        # return o
        return d.decode("utf-8")

    def utf8_charwidth(self, char):
        if char >= 0x2e80:
            return 2
        else:
            return 1

    # Low-level terminal functions
    def peek(self, y0, x0, y1, x1):
        return self.screen[self.w * y0 + x0:self.w * (y1 - 1) + x1]

    def poke(self, y, x, s):
        pos = self.w * y + x
        self.screen[pos:pos + len(s)] = s

    def fill(self, y0, x0, y1, x1, char):
        n = self.w * (y1 - y0 - 1) + (x1 - x0)
        self.poke(y0, x0, array.array('i', [char] * n))

    def clear(self, y0, x0, y1, x1):
        self.fill(y0, x0, y1, x1, self.attr | 0x20)

    # Scrolling functions
    def scroll_area_up(self, y0, y1, n=1):
        n = min(y1 - y0, n)
        self.poke(y0, 0, self.peek(y0 + n, 0, y1, self.w))
        self.clear(y1 - n, 0, y1, self.w)

    def scroll_area_down(self, y0, y1, n=1):
        n = min(y1 - y0, n)
        self.poke(y0 + n, 0, self.peek(y0, 0, y1 - n, self.w))
        self.clear(y0, 0, y0 + n, self.w)

    def scroll_area_set(self, y0, y1):
        y0 = max(0, min(self.h - 1, y0))
        y1 = max(1, min(self.h, y1))
        if y1 > y0:
            self.scroll_area_y0 = y0
            self.scroll_area_y1 = y1

    def scroll_line_right(self, y, x, n=1):
        if x < self.w:
            n = min(self.w - self.cx, n)
            self.poke(y, x + n, self.peek(y, x, y + 1, self.w - n))
            self.clear(y, x, y + 1, x + n)

    def scroll_line_left(self, y, x, n=1):
        if x < self.w:
            n = min(self.w - self.cx, n)
            self.poke(y, x, self.peek(y, x + n, y + 1, self.w))
            self.clear(y, self.w - n, y + 1, self.w)

    # Cursor functions
    def cursor_line_width(self, next_char):
        wx = self.utf8_charwidth(next_char)
        lx = 0
        for x in range(min(self.cx, self.w)):
            char = self.peek(self.cy, x, self.cy + 1, x + 1)[0] & 0xffff
            wx += self.utf8_charwidth(char)
            lx += 1
        return wx, lx

    def cursor_up(self, n=1):
        self.cy = max(self.scroll_area_y0, self.cy - n)

    def cursor_down(self, n=1):
        self.cy = min(self.scroll_area_y1 - 1, self.cy + n)

    def cursor_left(self, n=1):
        self.cx = max(0, self.cx - n)

    def cursor_right(self, n=1):
        self.cx = min(self.w - 1, self.cx + n)

    def cursor_set_x(self, x):
        self.cx = max(0, x)

    def cursor_set_y(self, y):
        self.cy = max(0, min(self.h - 1, y))

    def cursor_set(self, y, x):
        self.cursor_set_x(x)
        self.cursor_set_y(y)

    # Dumb terminal
    def ctrl_BS(self):
        delta_y, cx = divmod(self.cx - 1, self.w)
        cy = max(self.scroll_area_y0, self.cy + delta_y)
        self.cursor_set(cy, cx)

    def ctrl_HT(self, n=1):
        if n > 0 and self.cx >= self.w:
            return
        if n <= 0 and self.cx == 0:
            return
        ts = 0
        for i in range(len(self.tab_stops)):
            if self.cx >= self.tab_stops[i]:
                ts = i
        ts += n
        if len(self.tab_stops) > ts >= 0:
            self.cursor_set_x(self.tab_stops[ts])
        else:
            self.cursor_set_x(self.w - 1)

    def ctrl_LF(self):
        if self.vt100_mode_lfnewline:
            self.ctrl_CR()
        if self.cy == self.scroll_area_y1 - 1:
            self.scroll_area_up(self.scroll_area_y0, self.scroll_area_y1)
        else:
            self.cursor_down()

    def ctrl_CR(self):
        self.cursor_set_x(0)

    def dumb_write(self, char):
        if char < 32:
            if char == 8:
                self.ctrl_BS()
            elif char == 9:
                self.ctrl_HT()
            elif 10 <= char <= 12:
                self.ctrl_LF()
            elif char == 13:
                self.ctrl_CR()
            return True
        return False

    def dumb_echo(self, char):
        # Check right bound
        wx, cx = self.cursor_line_width(char)
        # Newline
        if wx > self.w:
            if self.vt100_mode_autowrap:
                self.ctrl_CR()
                self.ctrl_LF()
            else:
                self.cx = cx - 1
        if self.vt100_mode_insert:
            self.scroll_line_right(self.cy, self.cx)
        if self.vt100_charset_is_single_shift:
            self.vt100_charset_is_single_shift = False
        elif self.vt100_charset_is_graphical and (char & 0xffe0) == 0x0060:
            char = self.vt100_charset_graph[char - 0x60]
        self.poke(self.cy, self.cx, array.array('i', [self.attr | char]))
        self.cursor_set_x(self.cx + 1)

    # VT100 CTRL, ESC, CSI handlers
    def vt100_charset_update(self):
        self.vt100_charset_is_graphical = (
                self.vt100_charset_g[self.vt100_charset_g_sel] == 2)

    def vt100_charset_set(self, g):
        # Invoke active character set
        self.vt100_charset_g_sel = g
        self.vt100_charset_update()

    def vt100_charset_select(self, g, charset):
        # Select charset
        self.vt100_charset_g[g] = charset
        self.vt100_charset_update()

    def vt100_setmode(self, p, state):
        # Set VT100 mode
        p = self.vt100_parse_params(p, [], False)
        for m in p:
            if m == '4':
                # Insertion replacement mode
                self.vt100_mode_insert = state
            elif m == '20':
                # Linefeed/new line mode
                self.vt100_mode_lfnewline = state
            elif m == '?1':
                # Cursor key mode
                self.vt100_mode_cursorkey = state
            elif m == '?3':
                # Column mode
                if self.vt100_mode_column_switch:
                    if state:
                        self.w = 132
                    else:
                        self.w = 80
                    self.reset_screen()
            elif m == '?5':
                # Screen mode
                self.vt100_mode_inverse = state
            elif m == '?6':
                # Region origin mode
                self.vt100_mode_origin = state
                if state:
                    self.cursor_set(self.scroll_area_y0, 0)
                else:
                    self.cursor_set(0, 0)
            elif m == '?7':
                # Autowrap mode
                self.vt100_mode_autowrap = state
            elif m == '?25':
                # Text cursor enable mode
                self.vt100_mode_cursor = state
            elif m == '?40':
                # Column switch control
                self.vt100_mode_column_switch = state
            elif m == '?47':
                # Alternate screen mode
                if ((state and not self.vt100_mode_alt_screen) or
                        (not state and self.vt100_mode_alt_screen)):
                    self.screen, self.screen2 = self.screen2, self.screen
                    self.vt100_saved, self.vt100_saved2 = self.vt100_saved2, self.vt100_saved
                self.vt100_mode_alt_screen = state
            elif m == '?67':
                # Backspace/delete
                self.vt100_mode_backspace = state

    def ctrl_SO(self):
        # Shift out
        self.vt100_charset_set(1)

    def ctrl_SI(self):
        # Shift in
        self.vt100_charset_set(0)

    def esc_CSI(self):
        # CSI start sequence
        self.vt100_parse_reset('csi')

    def esc_DECALN(self):
        # Screen alignment display
        self.fill(0, 0, self.h, self.w, 0x00fe0045)

    def esc_G0_0(self):
        self.vt100_charset_select(0, 0)

    def esc_G0_1(self):
        self.vt100_charset_select(0, 1)

    def esc_G0_2(self):
        self.vt100_charset_select(0, 2)

    def esc_G0_3(self):
        self.vt100_charset_select(0, 3)

    def esc_G0_4(self):
        self.vt100_charset_select(0, 4)

    def esc_G1_0(self):
        self.vt100_charset_select(1, 0)

    def esc_G1_1(self):
        self.vt100_charset_select(1, 1)

    def esc_G1_2(self):
        self.vt100_charset_select(1, 2)

    def esc_G1_3(self):
        self.vt100_charset_select(1, 3)

    def esc_G1_4(self):
        self.vt100_charset_select(1, 4)

    def esc_DECSC(self):
        # Store cursor
        self.vt100_saved = {}
        self.vt100_saved['cx'] = self.cx
        self.vt100_saved['cy'] = self.cy
        self.vt100_saved['attr'] = self.attr
        self.vt100_saved['charset_g_sel'] = self.vt100_charset_g_sel
        self.vt100_saved['charset_g'] = self.vt100_charset_g[:]
        self.vt100_saved['mode_autowrap'] = self.vt100_mode_autowrap
        self.vt100_saved['mode_origin'] = self.vt100_mode_origin

    def esc_DECRC(self):
        # Retore cursor
        self.cx = self.vt100_saved['cx']
        self.cy = self.vt100_saved['cy']
        self.attr = self.vt100_saved['attr']
        self.vt100_charset_g_sel = self.vt100_saved['charset_g_sel']
        self.vt100_charset_g = self.vt100_saved['charset_g'][:]
        self.vt100_charset_update()
        self.vt100_mode_autowrap = self.vt100_saved['mode_autowrap']
        self.vt100_mode_origin = self.vt100_saved['mode_origin']

    def esc_DECKPAM(self):
        # Application keypad mode
        pass

    def esc_DECKPNM(self):
        # Numeric keypad mode
        pass

    def esc_IND(self):
        # Index
        self.ctrl_LF()

    def esc_NEL(self):
        # Next line
        self.ctrl_CR()
        self.ctrl_LF()

    def esc_HTS(self):
        # Character tabulation set
        self.csi_CTC('0')

    def esc_RI(self):
        # Reverse line feed
        if self.cy == self.scroll_area_y0:
            self.scroll_area_down(self.scroll_area_y0, self.scroll_area_y1)
        else:
            self.cursor_up()

    def esc_SS2(self):
        # Single-shift two
        self.vt100_charset_is_single_shift = True

    def esc_SS3(self):
        # Single-shift three
        self.vt100_charset_is_single_shift = True

    def esc_DCS(self):
        # Device control string
        self.vt100_parse_reset('str')

    def esc_SOS(self):
        # Start of string
        self.vt100_parse_reset('str')

    def esc_DECID(self):
        # Identify terminal
        self.csi_DA('0')

    def esc_ST(self):
        # String terminator
        pass

    def esc_OSC(self):
        # Operating system command
        self.vt100_parse_reset('str')

    def esc_PM(self):
        # Privacy message
        self.vt100_parse_reset('str')

    def esc_APC(self):
        # Application program command
        self.vt100_parse_reset('str')

    def csi_ICH(self, p):
        # Insert character
        p = self.vt100_parse_params(p, [1])
        self.scroll_line_right(self.cy, self.cx, p[0])

    def csi_CUU(self, p):
        # Cursor up
        p = self.vt100_parse_params(p, [1])
        self.cursor_up(max(1, p[0]))

    def csi_CUD(self, p):
        # Cursor down
        p = self.vt100_parse_params(p, [1])
        self.cursor_down(max(1, p[0]))

    def csi_CUF(self, p):
        # Cursor right
        p = self.vt100_parse_params(p, [1])
        self.cursor_right(max(1, p[0]))

    def csi_CUB(self, p):
        # Cursor left
        p = self.vt100_parse_params(p, [1])
        self.cursor_left(max(1, p[0]))

    def csi_CNL(self, p):
        # Cursor next line
        self.csi_CUD(p)
        self.ctrl_CR()

    def csi_CPL(self, p):
        # Cursor preceding line
        self.csi_CUU(p)
        self.ctrl_CR()

    def csi_CHA(self, p):
        # Cursor character absolute
        p = self.vt100_parse_params(p, [1])
        self.cursor_set_x(p[0] - 1)

    def csi_CUP(self, p):
        # Set cursor position
        p = self.vt100_parse_params(p, [1, 1])
        if self.vt100_mode_origin:
            self.cursor_set(self.scroll_area_y0 + p[0] - 1, p[1] - 1)
        else:
            self.cursor_set(p[0] - 1, p[1] - 1)

    def csi_CHT(self, p):
        # Cursor forward tabulation
        p = self.vt100_parse_params(p, [1])
        self.ctrl_HT(max(1, p[0]))

    def csi_ED(self, p):
        # Erase in display
        p = self.vt100_parse_params(p, ['0'], False)
        if p[0] == '0':
            self.clear(self.cy, self.cx, self.h, self.w)
        elif p[0] == '1':
            self.clear(0, 0, self.cy + 1, self.cx + 1)
        elif p[0] == '2':
            self.clear(0, 0, self.h, self.w)

    def csi_EL(self, p):
        # Erase in line
        p = self.vt100_parse_params(p, ['0'], False)
        if p[0] == '0':
            self.clear(self.cy, self.cx, self.cy + 1, self.w)
        elif p[0] == '1':
            self.clear(self.cy, 0, self.cy + 1, self.cx + 1)
        elif p[0] == '2':
            self.clear(self.cy, 0, self.cy + 1, self.w)

    def csi_IL(self, p):
        # Insert line
        p = self.vt100_parse_params(p, [1])
        if self.cy >= self.scroll_area_y0 and self.cy < self.scroll_area_y1:
            self.scroll_area_down(self.cy, self.scroll_area_y1, max(1, p[0]))

    def csi_DL(self, p):
        # Delete line
        p = self.vt100_parse_params(p, [1])
        if self.scroll_area_y0 <= self.cy < self.scroll_area_y1:
            self.scroll_area_up(self.cy, self.scroll_area_y1, max(1, p[0]))

    def csi_DCH(self, p):
        # Delete characters
        p = self.vt100_parse_params(p, [1])
        self.scroll_line_left(self.cy, self.cx, max(1, p[0]))

    def csi_SU(self, p):
        # Scroll up
        p = self.vt100_parse_params(p, [1])
        self.scroll_area_up(self.scroll_area_y0, self.scroll_area_y1, max(1, p[0]))

    def csi_SD(self, p):
        # Scroll down
        p = self.vt100_parse_params(p, [1])
        self.scroll_area_down(self.scroll_area_y0, self.scroll_area_y1, max(1, p[0]))

    def csi_CTC(self, p):
        # Cursor tabulation control
        p = self.vt100_parse_params(p, ['0'], False)
        for m in p:
            if m == '0':
                try:
                    ts = self.tab_stops.index(self.cx)
                except ValueError:
                    tab_stops = self.tab_stops
                    tab_stops.append(self.cx)
                    tab_stops.sort()
                    self.tab_stops = tab_stops
            elif m == '2':
                try:
                    self.tab_stops.remove(self.cx)
                except ValueError:
                    pass
            elif m == '5':
                self.tab_stops = [0]

    def csi_ECH(self, p):
        # Erase character
        p = self.vt100_parse_params(p, [1])
        n = min(self.w - self.cx, max(1, p[0]))
        self.clear(self.cy, self.cx, self.cy + 1, self.cx + n)

    def csi_CBT(self, p):
        # Cursor backward tabulation
        p = self.vt100_parse_params(p, [1])
        self.ctrl_HT(1 - max(1, p[0]))

    def csi_HPA(self, p):
        # Character position absolute
        p = self.vt100_parse_params(p, [1])
        self.cursor_set_x(p[0] - 1)

    def csi_HPR(self, p):
        # Character position forward
        self.csi_CUF(p)

    def csi_REP(self, p):
        # Repeat
        p = self.vt100_parse_params(p, [1])
        if self.vt100_lastchar < 32:
            return
        n = min(2000, max(1, p[0]))
        while n:
            self.dumb_echo(self.vt100_lastchar)
            n -= 1
        self.vt100_lastchar = 0

    def csi_DA(self, p):
        # Device attributes
        p = self.vt100_parse_params(p, ['0'], False)
        if p[0] == '0':
            self.vt100_out = "\x1b[?1;2c"
        elif p[0] == '>0' or p[0] == '>':
            self.vt100_out = "\x1b[>0;184;0c"

    def csi_VPA(self, p):
        # Line position absolute
        p = self.vt100_parse_params(p, [1])
        self.cursor_set_y(p[0] - 1)

    def csi_VPR(self, p):
        # Line position forward
        self.csi_CUD(p)

    def csi_HVP(self, p):
        # Character and line position
        self.csi_CUP(p)

    def csi_TBC(self, p):
        # Tabulation clear
        p = self.vt100_parse_params(p, ['0'], False)
        if p[0] == '0':
            self.csi_CTC('2')
        elif p[0] == '3':
            self.csi_CTC('5')

    def csi_SM(self, p):
        # Set mode
        self.vt100_setmode(p, True)

    def csi_RM(self, p):
        # Reset mode
        self.vt100_setmode(p, False)

    def csi_SGR(self, p):
        # Select graphic rendition
        p = self.vt100_parse_params(p, [0])
        for m in p:
            if m == 0:
                # Reset
                self.attr = 0x00fe0000
            elif m == 4:
                # Underlined
                self.attr |= 0x01000000
            elif m == 7:
                # Negative
                self.attr |= 0x02000000
            elif m == 8:
                # Concealed
                self.attr |= 0x04000000
            elif m == 24:
                # Not underlined
                self.attr &= 0x7eff0000
            elif m == 27:
                # Positive
                self.attr &= 0x7dff0000
            elif m == 28:
                # Revealed
                self.attr &= 0x7bff0000
            elif 30 <= m <= 37:
                # Foreground
                self.attr = (self.attr & 0x7f0f0000) | ((m - 30) << 20)
            elif m == 39:
                # Default fg color
                self.attr = (self.attr & 0x7f0f0000) | 0x00f00000
            elif 40 <= m <= 47:
                # Background
                self.attr = (self.attr & 0x7ff00000) | ((m - 40) << 16)
            elif m == 49:
                # Default bg color
                self.attr = (self.attr & 0x7ff00000) | 0x000e0000

    def csi_DSR(self, p):
        # Device status report
        p = self.vt100_parse_params(p, ['0'], False)
        if p[0] == '5':
            self.vt100_out = "\x1b[0n"
        elif p[0] == '6':
            x = self.cx + 1
            y = self.cy + 1
            self.vt100_out = '\x1b[%d;%dR' % (y, x)
        elif p[0] == '7':
            self.vt100_out = 'WebShell'
        elif p[0] == '8':
            self.vt100_out = __version__
        elif p[0] == '?6':
            x = self.cx + 1
            y = self.cy + 1
            self.vt100_out = '\x1b[?%d;%dR' % (y, x)
        elif p[0] == '?15':
            self.vt100_out = '\x1b[?13n'
        elif p[0] == '?25':
            self.vt100_out = '\x1b[?20n'
        elif p[0] == '?26':
            self.vt100_out = '\x1b[?27;1n'
        elif p[0] == '?53':
            self.vt100_out = '\x1b[?53n'

    def csi_DECSTBM(self, p):
        # Set top and bottom margins
        p = self.vt100_parse_params(p, [1, self.h])
        self.scroll_area_set(p[0] - 1, p[1])
        if self.vt100_mode_origin:
            self.cursor_set(self.scroll_area_y0, 0)
        else:
            self.cursor_set(0, 0)

    def csi_SCP(self, p):
        # Save cursor position
        self.vt100_saved_cx = self.cx
        self.vt100_saved_cy = self.cy

    def csi_RCP(self, p):
        # Restore cursor position
        self.cx = self.vt100_saved_cx
        self.cy = self.vt100_saved_cy

    def csi_DECREQTPARM(self, p):
        # Request terminal parameters
        p = self.vt100_parse_params(p, [], False)
        if p[0] == '0':
            self.vt100_out = "\x1b[2;1;1;112;112;1;0x"
        elif p[0] == '1':
            self.vt100_out = "\x1b[3;1;1;112;112;1;0x"

    def csi_DECSTR(self, p):
        # Soft terminal reset
        self.reset_soft()

    # VT100 Parser
    def vt100_parse_params(self, p, d, to_int=True):
        # Process parameters (params p with defaults d)
        # Add prefix to all parameters
        prefix = ''
        if len(p) > 0:
            if '<' <= p[0] <= '?':
                prefix = p[0]
                p = p[1:]
            p = p.split(';')
        else:
            p = ''
        # Process parameters
        n = max(len(p), len(d))
        o = []
        for i in range(n):
            value_def = False
            if i < len(p):
                value = prefix + p[i]
                value_def = True
                if to_int:
                    try:
                        value = int(value)
                    except ValueError:
                        value_def = False
            if (not value_def) and i < len(d):
                value = d[i]
            o.append(value)
        return o

    def vt100_parse_reset(self, vt100_parse_state=""):
        self.vt100_parse_state = vt100_parse_state
        self.vt100_parse_len = 0
        self.vt100_parse_func = ""
        self.vt100_parse_param = ""

    def vt100_parse_process(self):
        if self.vt100_parse_state == 'esc':
            # ESC mode
            f = self.vt100_parse_func
            try:
                self.vt100_esc[f]()
            except KeyError:
                pass
            if self.vt100_parse_state == 'esc':
                self.vt100_parse_reset()
        else:
            # CSI mode
            f = self.vt100_parse_func
            p = self.vt100_parse_param
            try:
                self.vt100_csi[f](p)
            except KeyError:
                pass
            if self.vt100_parse_state == 'csi':
                self.vt100_parse_reset()

    def vt100_write(self, char):
        if char < 32:
            if char == 27:
                self.vt100_parse_reset('esc')
                return True
            elif char == 14:
                self.ctrl_SO()
            elif char == 15:
                self.ctrl_SI()
        elif (char & 0xffe0) == 0x0080:
            self.vt100_parse_reset('esc')
            self.vt100_parse_func = chr(char - 0x40)
            self.vt100_parse_process()
            return True

        if self.vt100_parse_state:
            if self.vt100_parse_state == 'str':
                if char >= 32:
                    return True
                self.vt100_parse_reset()
            else:
                if char < 32:
                    if char == 24 or char == 26:
                        self.vt100_parse_reset()
                        return True
                else:
                    self.vt100_parse_len += 1
                    if self.vt100_parse_len > 32:
                        self.vt100_parse_reset()
                    else:
                        char_msb = char & 0xf0
                        if char_msb == 0x20:
                            # Intermediate bytes (added to function)
                            self.vt100_parse_func += chr(char)
                        elif char_msb == 0x30 and self.vt100_parse_state == 'csi':
                            # Parameter byte
                            self.vt100_parse_param += chr(char)
                        else:
                            # Function byte
                            self.vt100_parse_func += chr(char)
                            self.vt100_parse_process()
                        return True
        self.vt100_lastchar = char
        return False

    # External interface
    def set_size(self, w, h):
        if w < 2 or w > 256 or h < 2 or h > 256:
            return False
        self.w = w
        self.h = h
        self.reset_screen()
        return True

    def read(self):
        d = self.vt100_out
        self.vt100_out = ""
        return d

    def write(self, d):
        d = self.utf8_decode(d)
        # d = d.decode("utf-8")
        for c in d:
            char = ord(c)
            if self.vt100_write(char):
                continue
            if self.dumb_write(char):
                continue
            if char <= 0xffff:
                self.dumb_echo(char)
        return True

    def pipe(self, d):
        o = ''
        d = self.utf8_decode(d)
        for c in d:
            char = ord(c)
            if self.vt100_keyfilter_escape:
                self.vt100_keyfilter_escape = False
                try:
                    if self.vt100_mode_cursorkey:
                        o += self.vt100_keyfilter_appkeys[c]
                    else:
                        o += self.vt100_keyfilter_ansikeys[c]
                except KeyError:
                    pass
            elif c == '~':
                self.vt100_keyfilter_escape = True
            elif char == 127:
                if self.vt100_mode_backspace:
                    o += chr(8)
                else:
                    o += chr(127)
            else:
                o += c
                if self.vt100_mode_lfnewline and char == 13:
                    o += chr(10)
        return o

    def dump(self):
        screen = []
        attr_ = -1
        cx, cy = min(self.cx, self.w - 1), self.cy
        for y in range(0, self.h):
            wx = 0
            line = [""]
            for x in range(0, self.w):
                d = self.screen[y * self.w + x]
                char = d & 0xffff
                attr = d >> 16
                # Cursor
                if cy == y and cx == x and self.vt100_mode_cursor:
                    attr = attr & 0xfff0 | 0x000c
                # Attributes
                if attr != attr_:
                    if attr_ != -1:
                        line.append("")
                    bg = attr & 0x000f
                    fg = (attr & 0x00f0) >> 4
                    # Inverse
                    inv = attr & 0x0200
                    inv2 = self.vt100_mode_inverse
                    if (inv and not inv2) or (inv2 and not inv):
                        fg, bg = bg, fg
                    # Concealed
                    if attr & 0x0400:
                        fg = 0xc
                    # Underline
                    if attr & 0x0100:
                        ul = True
                    else:
                        ul = False
                    line.append((fg, bg, ul))
                    line.append("")
                    attr_ = attr
                wx += self.utf8_charwidth(char)
                if wx <= self.w:
                    line[-1] += chr(char)
            screen.append(line)

        return (cx, cy), screen


def synchronized(func):
    def wrapper(self, *args, **kwargs):
        try:
            self.lock.acquire()
        except AttributeError:
            self.lock = threading.RLock()
            self.lock.acquire()
        try:
            result = func(self, *args, **kwargs)
        finally:
            self.lock.release()
        return result

    return wrapper


class Multiplexer(object):

    def __init__(self, cmd="/bin/bash", env_term="xterm-color", timeout=60 * 60 * 24):
        # Set Linux signal handler
        if sys.platform in ("linux2", "linux3"):
            self.sigchldhandler = signal.signal(signal.SIGCHLD, signal.SIG_IGN)
        # Session
        self.session = {}
        self.cmd = cmd
        self.env_term = env_term
        self.timeout = timeout

        # Supervisor thread
        self.signal_stop = 0
        self.thread = threading.Thread(target=self.proc_thread)
        self.thread.start()

    def stop(self):
        # Stop supervisor thread
        self.signal_stop = 1
        self.thread.join()

    def proc_resize(self, sid, w, h):
        fd = self.session[sid]['fd']
        # Set terminal size
        try:
            fcntl.ioctl(fd,
                        struct.unpack('i',
                                      struct.pack('I', termios.TIOCSWINSZ)
                                      )[0],
                        struct.pack("HHHH", h, w, 0, 0))
        except (IOError, OSError):
            pass
        self.session[sid]['term'].set_size(w, h)
        self.session[sid]['w'] = w
        self.session[sid]['h'] = h

    @synchronized
    def proc_keepalive(self, sid, w, h, cmd=None):
        if not sid in self.session:
            # Start a new session
            self.session[sid] = {
                'state': 'unborn',
                'term': Terminal(w, h),
                'time': time.time(),
                'w': w,
                'h': h}
            return self.proc_spawn(sid, cmd)
        elif self.session[sid]['state'] == 'alive':
            self.session[sid]['time'] = time.time()
            # Update terminal size
            if self.session[sid]['w'] != w or self.session[sid]['h'] != h:
                self.proc_resize(sid, w, h)
            return True
        else:
            return False

    def proc_spawn(self, sid, cmd=None):
        # Session
        self.session[sid]['state'] = 'alive'
        w, h = self.session[sid]['w'], self.session[sid]['h']
        # Fork new process
        try:
            pid, fd = pty.fork()
        except (IOError, OSError):
            self.session[sid]['state'] = 'dead'
            return False
        if pid == 0:
            cmd = cmd or self.cmd
            # Safe way to make it work under BSD and Linux
            try:
                ls = os.environ['LANG'].split('.')
            except KeyError:
                ls = []
            if len(ls) < 2:
                ls = ['en_US', 'UTF-8']
            try:
                os.putenv('COLUMNS', str(w))
                os.putenv('LINES', str(h))
                os.putenv('TERM', self.env_term)
                os.putenv('PATH', os.environ['PATH'])
                os.putenv('LANG', ls[0] + '.UTF-8')
                # os.system(cmd)
                p = subprocess.Popen(cmd, shell=False)
                # print "called with subprocess", p.pid
                child_pid, sts = os.waitpid(p.pid, 0)
                # print "child_pid", child_pid, sts
            except (IOError, OSError):
                pass
            # self.proc_finish(sid)
            os._exit(0)
        else:
            # Store session vars
            self.session[sid]['pid'] = pid
            self.session[sid]['fd'] = fd
            # Set file control
            fcntl.fcntl(fd, fcntl.F_SETFL, os.O_NONBLOCK)
            # Set terminal size
            self.proc_resize(sid, w, h)
            return True

    def proc_waitfordeath(self, sid):
        try:
            os.close(self.session[sid]['fd'])
        except (KeyError, IOError, OSError):
            pass
        if sid in self.session:
            if 'fd' in self.session[sid]:
                del self.session[sid]['fd']
        try:
            os.waitpid(self.session[sid]['pid'], 0)
        except (KeyError, IOError, OSError):
            pass
        if sid in self.session:
            if 'pid' in self.session[sid]:
                del self.session[sid]['pid']
        self.session[sid]['state'] = 'dead'
        return True

    def proc_bury(self, sid):
        if self.session[sid]['state'] == 'alive':
            try:
                os.kill(self.session[sid]['pid'], signal.SIGTERM)
            except (IOError, OSError):
                pass
        self.proc_waitfordeath(sid)
        if sid in self.session:
            del self.session[sid]
        return True

    @synchronized
    def proc_buryall(self):
        for sid in list(self.session.keys()):
            self.proc_bury(sid)

    @synchronized
    def proc_read(self, sid):
        """
        Read from process
        """
        if sid not in self.session:
            return False
        elif self.session[sid]['state'] != 'alive':
            return False
        try:
            fd = self.session[sid]['fd']
            d = os.read(fd, 65536)
            if not d:
                # Process finished, BSD
                self.proc_waitfordeath(sid)
                return False
        except (IOError, OSError):
            # Process finished, Linux
            self.proc_waitfordeath(sid)
            return False
        term = self.session[sid]['term']
        term.write(d)
        # Read terminal response
        d = term.read()
        if d:
            try:
                os.write(fd, d)
            except (IOError, OSError):
                return False
        return True

    @synchronized
    def proc_write(self, sid, d):
        """
        Write to process
        """
        if sid not in self.session:
            return False
        elif self.session[sid]['state'] != 'alive':
            return False
        try:
            term = self.session[sid]['term']
            d = term.pipe(d)
            fd = self.session[sid]['fd']
            os.write(fd, d.encode("utf-8"))
        except (IOError, OSError):
            return False
        return True

    @synchronized
    def proc_dump(self, sid):
        """
        Dump terminal output
        """
        if sid not in self.session:
            return False
        return self.session[sid]['term'].dump()

    @synchronized
    def proc_getalive(self):
        """
        Get alive sessions, bury timed out ones
        """
        fds = []
        fd2sid = {}
        now = time.time()
        for sid in list(self.session.keys()):
            then = self.session[sid]['time']
            if (now - then) > self.timeout:
                self.proc_bury(sid)
            else:
                if self.session[sid]['state'] == 'alive':
                    fds.append(self.session[sid]['fd'])
                    fd2sid[self.session[sid]['fd']] = sid
        return fds, fd2sid

    def proc_thread(self):
        """
        Supervisor thread
        """
        while not self.signal_stop:
            # Read fds
            (fds, fd2sid) = self.proc_getalive()
            try:
                i, o, e = select.select(fds, [], [], 1.0)
            except (IOError, OSError):
                i = []
            for fd in i:
                sid = fd2sid[fd]
                self.proc_read(sid)
                self.session[sid]["changed"] = time.time()
            if len(i):
                time.sleep(0.002)
        self.proc_buryall()


def ssh_command(login, executable="ssh"):
    cmd = executable
    cmd += ' -oPreferredAuthentications=keyboard-interactive,password'
    cmd += ' -oNoHostAuthenticationForLocalhost=yes'
    cmd += ' -oLogLevel=FATAL'
    cmd += ' -F/dev/null -l' + login + ' localhost'
    return cmd


class Session(object):
    _mux = None

    @classmethod
    def close_all(cls):
        Session._mux.stop()

    def __init__(self, cmd=None, width=80, height=24):
        if not Session._mux:
            Session._mux = Multiplexer()
        self._session_id = "%s-%s" % (time.time(), id(self))
        self._width = width
        self._height = height
        self._started = False

    def resize(self, width, height):
        self._width = width
        self._height = height
        if self._started:
            self.keepalive()

    def start(self, cmd=None):
        self._started = Session._mux.proc_keepalive(
            self._session_id, self._width, self._height, cmd or self.cmd)
        return self._started

    def close(self):
        return Session._mux.proc_bury(self._session_id)

    stop = close

    def is_alive(self):
        return Session._mux.session.get(self._session_id, {}).get('state') == 'alive'

    def keepalive(self):
        return Session._mux.proc_keepalive(self._session_id, self._width, self._height)

    def dump(self):
        if self.keepalive():
            return Session._mux.proc_dump(self._session_id)

    def write(self, data):
        if self.keepalive():
            Session._mux.proc_write(self._session_id, data)

    def last_change(self):
        return Session._mux.session.get(self._session_id, {}).get("changed", None)

    def pid(self):
        return Session._mux.session.get(self._session_id, {}).get("pid", None)


if __name__ == "__main__":
    w, h = (80, 24)
    cmd = "/bin/ls --color=yes"
    multiplex = Multiplexer(cmd)
    sid = "session-id-%s"
    if multiplex.proc_keepalive(sid, w, h):
        # multiplex.proc_write(sid, k)
        time.sleep(1)
        # print multiplex.proc_dump(sid)
        print("Output:", multiplex.proc_dump(sid))
    multiplex.stop()
