diff --git a/application.py b/application.py index 9a48a8f..d6e06a3 100755 --- a/application.py +++ b/application.py @@ -209,6 +209,8 @@ class Application(object): curses.cbreak() curses.noecho() curses.nonl() + # for non-blocking junk + curses.halfdelay(1) curses.def_prog_mode() def _load_config_defaults(self): diff --git a/buffer.py b/buffer.py index 1e19165..2c9787a 100644 --- a/buffer.py +++ b/buffer.py @@ -1,9 +1,12 @@ import codecs, datetime, grp, md5, os, pwd, re, sets, shutil, stat, string -import aes, dirutil, regex, highlight, lex +import fcntl, select, pty, threading +import aes, dirutil, regex, highlight, lex, term from point import Point from subprocess import Popen, PIPE, STDOUT +from keyinput import MAP # undo/redo stack constants +ACT_NONE = -1 ACT_NORM = 0 ACT_UNDO = 1 ACT_REDO = 2 @@ -103,7 +106,9 @@ class Buffer(object): while len(stack) > self.stack_limit: stack.pop(0) def add_to_stack(self, move, act): - if act == ACT_NORM: + if act == ACT_NONE: + pass + elif act == ACT_NORM: self.redo_stack = [] self.undo_stack.append(move) self._stack_trim(self.undo_stack) @@ -528,6 +533,125 @@ class IpythonBuffer(InterpreterBuffer): class BinaryDataException(Exception): pass +class PipeBuffer(Buffer): + btype = 'pipe' + terms = { + 'dumb': term.Dumb, + 'xterm': term.XTerm, + } + modename = 'colortext' + def __init__(self, cmd, args, name=None, term='dumb'): + Buffer.__init__(self) + self.cmd = cmd + if name: + self._name = name + else: + self._name = '*Pipe*' + + self.term = self.terms[term]() + + self._pid, self._pty = pty.fork() + if self._pid == 0: + # child process + os.execve(cmd, [cmd] + args, {'TERM': self.term.name}) + + self._lock = threading.Lock() + self._towrite = '' + self._done = False + self._set_nonblock(self._pty) + self._thread = threading.Thread(target=self.pipe_read) + self._thread.setDaemon(True) + self._thread.start() + + def _set_nonblock(self, fd): + flags = fcntl.fcntl(fd, fcntl.F_GETFL) + fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NDELAY) + + def _filter_output(self, output): + output2 = [] + i = 0 + escaped = [] + for c in output: + if escaped: + escaped.append(c) + if c == 'm': + escaped = [] + elif c == '\x1b': + escaped.append(c) + elif c == '\n': + output2.append(c) + i = 0 + elif c == '\t': + j = i % 8 + output2.append(' ' * (8 - j)) + i += 8 - j + elif c == '\a': + pass + elif c == '\b': + if i > 0: + output2.pop(-1) + i -= 1 + else: + output2.append(c) + i += 1 + return ''.join(output2) + + def pipe_read(self): + fd = self._pty + while not self._done: + if self._towrite: + ifd, ofd, efd = select.select([fd], [fd], [fd], 0.1) + else: + ifd, ofd, efd = select.select([fd], [], [fd], 0.1) + if ifd: + data = os.read(ifd[0], 1024) + end = self.get_buffer_end() + #data = self._filter_output(data) + data = self.term.filter(data) + self.insert_string(end, data, force=True, act=ACT_NONE) + if ofd: + self._lock.acquire() + n = os.write(ofd[0], self._towrite) + self._towrite = self._towrite[n:] + self._lock.release() + if efd: + raise Exception, "exception is ready: %s" % repr(efd) + os.close(fd) + + def pipe_write(self, s): + self._lock.acquire() + self._towrite += s + self._lock.release() + + def insert_string(self, p, s, act=ACT_NORM, force=False): + lines = s.split("\n") + self.insert_lines(p, lines, act, force) + + def insert_lines(self, p, lines, act=ACT_NORM, force=False): + llen = len(lines) + assert llen > 0 + if not force and self.readonly(): + raise ReadOnlyError("buffer is read-only") + p2 = p.vadd(len(lines[-1]), llen - 1) + if llen > 1: + self.lines.insert(p.y + 1, []) + self.lines[p.y + 1] = lines[-1] + self.lines[p.y][p.x:] + self.lines[p.y] = self.lines[p.y][:p.x] + lines[0] + for i in range(1, llen - 1): + self.lines.insert(p.y + i, lines[i]) + else: + self.lines[p.y] = self.lines[p.y][:p.x] + lines[-1] + self.lines[p.y][p.x:] + self._region_add(p, p2, lines, act) + self.modified = True + + def name(self): return self._name + def changed(self): return False + def readonly(self): return True + def undo(self, move, act): raise Exception, "invalid" + def redo(self, move, act): raise Exception, "invalid" + def reload(self): raise Exception, "invalid" + def close(self): raise Exception, "invalid" + class FileBuffer(Buffer): btype = 'file' def __init__(self, path, name=None): diff --git a/mode/shellmini.py b/mode/shellmini.py index 0d246e2..0d3a29f 100644 --- a/mode/shellmini.py +++ b/mode/shellmini.py @@ -5,18 +5,7 @@ from point import Point from method import Method from subprocess import Popen, PIPE, STDOUT -LIMIT = 79 - class ShellExec(Method): - sequences = { - '\x1b[01;30m': "[green:default]", - '\x1b[01;31m': "[green:default]", - '\x1b[01;32m': "[green:default]", - '\x1b[01;34m': "[blue:default]", - '\x1b[01;36m': "[cyan:default]", - '\x1b[0m': "[default:default]", - } - sequences = {} def _execute(self, w, **vargs): a = w.application if a.completion_window_is_open(): @@ -33,78 +22,7 @@ class ShellExec(Method): b = a.bufferlist.get_buffer_by_name('*Shell*') if a.window().buffer is not b: a.switch_buffer(b) - p = a.get_mini_buffer_prompt() - b.insert_string(b.get_buffer_end(), p + s + '\n', force=True) - - a.set_mini_buffer_prompt('>>> ') - ok = True - - args = ['sh', '-c', s] - env = {'COLUMNS': '80', 'LINES': '24', 'TERM': os.environ['TERM']} - p = Popen(args, bufsize=1024, stderr=STDOUT, stdout=PIPE, - close_fds=True, env=env) - output = p.stdout.read() - output2 = [] - i = 0 - escaped = [] - for c in output: - if escaped: - escaped.append(c) - if c == 'm': - #seq = ''.join(escaped) - #if seq in self.sequences: - # output2.append(self.sequences[seq]) - escaped = [] - elif c == '\x1b': - escaped.append(c) - elif c == '\n': - output2.append(c) - i = 0 - elif c == '\t': - j = i % 8 - output2.append(' ' * (8 - j)) - i += 8 - j - elif c == '\a': - pass - elif c == '\b': - if i > 0: - output2.pop(-1) - i -= 1 - else: - output2.append(c) - i += 1 - output = ''.join(output2) - - limit = 1000 - for w2 in b.windows: - limit = min(w.width, limit) - if limit == 1000: - limit = LIMIT - - if output: - newlines = [] - for line in output.split('\n'): - line = line.replace('\x1b[01;32m', "[green:default]") - line = line.replace('\x1b[01;34m', "[blue:default]") - line = line.replace('\x1b[01;36m', "[cyan:default]") - line = line.replace('\x1b[0m', "[default:default]") - - i = 0 - while i + limit < len(line): - j = limit - while j > 0 and line[i + j] != ' ': - j -= 1 - if j == 0: - newlines.append(line[i:i + limit]) - i += limit - else: - newlines.append(line[i:i + j]) - i += j + 1 - newlines.append(line[i:]) - newlines[-1] = '' - b.insert_lines(b.get_buffer_end(), newlines, force=True) - for w2 in b.windows: - w2.goto_end(force=True) + b.pipe_write(s + "\n") class ShellCancel(Method): def execute(self, w, **vargs): @@ -117,7 +35,6 @@ class ShellClear(Method): if not a.has_buffer_name('*Shell*'): raise Exception, "No shell found!" b = a.bufferlist.get_buffer_by_name('*Shell*') - b.clear() class ShellHistoryPrev(Method): def execute(self, w, **vargs): @@ -207,7 +124,7 @@ class OpenShell(Method): def execute(self, w, **vargs): a = w.application if not a.has_buffer_name('*Shell*'): - b = buffer.ShellBuffer() + b = buffer.PipeBuffer('/bin/bash', [], name="*Shell*", term='xterm') a.add_buffer(b) window.Window(b, a) b = a.bufferlist.get_buffer_by_name('*Shell*') @@ -216,24 +133,15 @@ class OpenShell(Method): f = lambda x: None w.application.open_mini_buffer('>>> ', f, self, None, 'shellmini') -class ShellMiniGrammar(Grammar): - rules = [ - PatternRule(r'word', r"'.*'"), - PatternRule(r'word', r'"(:\\.|[^"\\])*"'), - PatternRule(r'word', r'(?:[^ \n"\'\\]|\\.)+'), - ] - class ShellMini(mode.Fundamental): modename = 'ShellMini' - grammar = ShellMiniGrammar - actions = [ShellExec, ShellClear, ShellCancel, ShellHistoryPrev, - ShellHistoryNext, ShellTab, + actions = [ShellExec, ShellClear, ShellCancel, + ShellHistoryPrev, ShellHistoryNext, + ShellTab, ShellPageUp, ShellPageDown, ShellGotoBeginning, ShellGotoEnd, OpenShell] def __init__(self, w): mode.Fundamental.__init__(self, w) - self.globals = dict(w.application.globals()) - self.locals = dict(w.application.locals()) self.saved_input = "" self.history = [''] self.hindex = 0 diff --git a/term.py b/term.py new file mode 100644 index 0000000..d95e489 --- /dev/null +++ b/term.py @@ -0,0 +1,112 @@ +class Dumb: + name = 'dumb' + def insert(self, s): + assert self.i <= len(self.outc) + if self.i == len(self.outc): + self.outc.append(s) + else: + self.outc[self.i] = s + self.i += 1 + + def do_backspace(self): + self.i = max(0, self.i - 1) + def do_tab(self): + self.insert(' ') + def do_newline(self): + self.outs += ''.join(self.outc) + '\n' + self.i = 0 + self.outc = [] + def do_careturn(self): + self.i = 0 + def do_esc(self, c): + pass + def do_delete(self): + if self.i < len(self.outc): + del self.outc[self.i] + + def handle_ctl(self, c): + n = ord(c) + if n == 8: + self.do_backspace() + elif n == 9: + self.do_tab() + elif n == 10: + self.do_newline() + elif n == 13: + self.do_careturn() + elif n == 27: + self.do_esc(c) + elif n == 127: + self.do_delete() + def handle_print(self, c): + self.insert(c) + def handle_8bit(self, c): + pass + + def handle(self, c): + n = ord(c) + assert n >= 0 and n < 256 + if n <= 27 or n == 127: + self.handle_ctl(c) + elif n < 127: + self.handle_print(c) + else: + self.handle_8bit(c) + + def filter(self, s): + self.i = 0 + self.outc = [] + self.outs = "" + for c in s: + self.handle(c) + return self.outs + ''.join(self.outc) + +class XTerm(Dumb): + name = 'xterm' + ansi_colors = { + '\033[30m': '[B:d]', + '\033[30;0m': '[B:d]', + '\033[30;1m': '[B:d:*]', + '\033[31m': '[r:d]', + '\033[31;0m': '[r:d]', + '\033[31;1m': '[r:d:*]', + '\033[32m': '[g:d]', + '\033[32;0m': '[g:d]', + '\033[32;1m': '[g:d:*]', + '\033[33m': '[y:d]', + '\033[33;0m': '[y:d]', + '\033[33;1m': '[y:d:*]', + '\033[34m': '[b:d]', + '\033[34;0m': '[b:d]', + '\033[34;1m': '[b:d:*]', + '\033[35m': '[m:d]', + '\033[35;0m': '[m:d]', + '\033[35;1m': '[m:d:*]', + '\033[36m': '[c:d]', + '\033[36;0m': '[c:d]', + '\033[36;1m': '[c:d:*]', + '\033[37m': '[w:d]', + '\033[37;0m': '[w:d]', + '\033[37;1m': '[w:d:*]', + '\033[39m': '[d:d]', + '\033[39;0m': '[d:d]', + '\033[39;1m': '[d:d:*]', + } + def filter(self, s): + self.meta = [] + return Dumb.filter(self, s) + + def do_esc(self, c): + self.meta.append(c) + + def handle(self, c): + if self.meta: + self.meta.append(c) + if c == 'm': + s = ''.join(self.meta) + if s in self.ansi_colors: + #self.insert(self.ansi_colors[s]) + pass + self.meta = [] + else: + Dumb.handle(self, c) diff --git a/window.py b/window.py index 98e2c81..91b842d 100644 --- a/window.py +++ b/window.py @@ -239,10 +239,42 @@ class Window(object): counter += 1 self.first = Point(x - (x % self.width), y) self.redraw() + def lower_view(self): + (x, y) = self.logical_cursor().xy() + counter = 0 + while counter < self.height - 1: + if x > self.width: + x -= self.width + elif y > 0: + y -= 1 + x = len(self.buffer.lines[y]) + else: + (x, y) = (0, 0) + break + counter += 1 + self.first = Point(x - (x % self.width), y) + self.redraw() + def upper_view(self): + (x, y) = self.logical_cursor().xy() + counter = 0 + while counter < 2: + if x > self.width: + x -= self.width + elif y > 0: + y -= 1 + x = len(self.buffer.lines[y]) + else: + (x, y) = (0, 0) + break + counter += 1 + self.first = Point(x - (x % self.width), y) + self.redraw() def assure_visible_cursor(self): - if not self.cursor_is_visible(): - #raise Exception, "%s < %s" % (self.last, self.logical_cursor()) - self.center_view() + p = self.logical_cursor() + if self.first > p: + self.upper_view() + elif p > self.last: + self.lower_view() # moving in buffer def forward(self):