#!/usr/bin/env python import curses, curses.ascii, getpass, os, re, string, sys, termios, time import traceback import buffer, bufferlist, color, completer, keyinput, method, minibuffer import mode, point, sets, util, window # modes import mode, mode_c, mode_mini, mode_python, mode_nasm, mode_perl, mode_search import mode_replace, mode_xml, mode_console, mode_sh, mode_text, mode_which import mode_mutt, mode_sql, mode_javascript, mode_diff, mode_blame, mode_tt def run(buffers, jump_to_line=None, init_mode=None): # save terminal state so we can restore it when the program exits attr = termios.tcgetattr(sys.stdin) keyinput.disable_control_chars() retval = 1 try: retval = curses.wrapper(run_app, buffers, jump_to_line, init_mode) except: traceback.print_exc() # restore terminal state termios.tcsetattr(sys.stdin, termios.TCSANOW, attr) return retval def run_app(stdscr, buffers, jump_to_line=None, init_mode=None): a = Application(stdscr, buffers, jump_to_line, init_mode) a.run() KILL_RING_LIMIT = 128 WORD_LETTERS = list(string.letters + string.digits) ERROR_TIMEOUT = -1 #ERROR_TIMEOUT = 2 #DARK_BACKGROUND = False DARK_BACKGROUND = True class Application: def __init__(self, stdscr, buffers=[], jump_to_line=None, init_mode=None): # initalize curses primitives self.stdscr = stdscr self.y, self.x = self.stdscr.getmaxyx() # initialize some basic stuff self.margins_visible = False #self.margins = [(80, 'blue'), (90, 'red')] self.margins = [(80, 'blue'), ] # each highlighted_range contains three things: [window, start_p, end_p] self.highlighted_ranges = [] self.mini_active = False self.mini_buffer = None self.mini_prompt = "" self.error_string = "" self.error_timestamp = None self.input = keyinput.Handler() # initialize our colors if curses.has_colors(): curses.start_color() try: curses.use_default_colors() color.default_color = True except: # guess we weren't on 2.4 color.default_color = False color.init() # this is how we can change color settings if curses.can_change_color(): #curses.init_color(curses.COLOR_BLUE, 750, 400, 0) pass else: self.set_error("Dynamic color not available") # initialize our modes self.modes = { 'blame': mode_blame.Blame, 'c': mode_c.C, 'console': mode_console.Console, 'diff': mode_diff.Diff, 'fundamental': mode.Fundamental, 'mini': mode_mini.Mini, 'nasm': mode_nasm.Nasm, 'perl': mode_perl.Perl, 'python': mode_python.Python, 'replace': mode_replace.Replace, 'search': mode_search.Search, 'sh': mode_sh.Sh, 'text': mode_text.Text, 'which': mode_which.Which, 'xml': mode_xml.XML, 'mutt': mode_mutt.Mutt, 'sql': mode_sql.Sql, 'javascript': mode_javascript.Javascript, 'template': mode_tt.Template, } # these are used in this order to determine which mode to open certain # kinds of files self.mode_paths = { '/etc/profile': 'sh', } self.mode_basenames = { '.bashrc': 'sh', '.bash_profile': 'sh', '.profile': 'sh', } self.mode_extensions = { '.py': 'python', '.pl': 'perl', '.pm': 'perl', '.t': 'perl', '.c': 'c', '.txt': 'text', '.s': 'nasm', '.sh': 'sh', '.bash': 'sh', '.xml': 'xml', '.xml.in': 'xml', '.html': 'xml', '.htm': 'xml', '.sql': 'sql', '.js': 'javascript', '.tt': 'template' } self.mode_detection = { 'python': 'python', 'perl': 'perl', 'sh': 'sh', 'bash': 'sh', } # initialize our methods self.methods = {} for name in dir(method): cls = eval("method.%s" % name) if hasattr(cls, '_is_method') and cls._is_method: self.methods[cls._name()] = cls() # create all the insert methods for the character ranges we like for c in string.letters + string.digits + string.punctuation: ## closing tags are handled differently #if c == ')' or c == ']' or c == '}': # continue obj = method.InsertString(c) self.methods[obj.name] = obj # window/slot height/width height = self.y - 2 width = self.x - 1 # initialize our buffers # note that the first buffer in buffers will be initially visible buffers.append(buffer.ScratchBuffer()) buffers.append(buffer.ConsoleBuffer()) self.bufferlist = bufferlist.BufferList(height, width) self.active_slot = 0 self.resize_slots() # build windows for our buffers for b in buffers: window.Window(b, self, height, width, slot=self.active_slot, mode_name=init_mode) self.bufferlist.add_buffer(b) self.resize_windows() # see if the user has requested that we go to a particular line if jump_to_line: name = buffers[0].name() b = self.bufferlist.get_buffer_by_name(name) w = b.get_window(self.active_slot) method.GotoLine().execute(w, lineno=jump_to_line) # initialize our kill ring and last action self.kill_ring = [] self.kill_commands = ['kill', 'kill-region'] self.last_action = None self.last_search = None self.last_replace_before = None self.last_replace_after = None # initialize tab handlers method.DATATYPES['path'] = completer.FileCompleter() method.DATATYPES['buffer'] = completer.BufferCompleter(self) method.DATATYPES['command'] = completer.CommandCompleter() method.DATATYPES['shell'] = completer.ShellCompleter() method.DATATYPES['method'] = completer.MethodCompleter() method.DATATYPES['mode'] = completer.ModeCompleter() method.DATATYPES['perlfunction'] = completer.PerlFunctionCompleter() # set up curses self.win = curses.newwin(self.y, self.x, 0, 0) self.win.leaveok(1) curses.meta(1) curses.halfdelay(1) self.hide_cursor() def globals(self): return globals() def locals(self): return locals() def add_slot(self): b = self.bufferlist.slots[self.active_slot].buffer n = self.bufferlist.add_slot(0, 0, 0, b) self.resize_slots() self.add_window_to_buffer(b, n) self.resize_windows() def remove_slot(self, slotname): assert len(self.bufferlist.slots) > 1, "oh no you didn't!" assert slotname >= 0 and slotname < len(self.bufferlist.slots), \ "invalid slot: %r (%r)" % (slotname, len(self.bufferlist.slots)) b = self.bufferlist.slots[slotname].buffer self.bufferlist.remove_slot(slotname) if self.active_slot > slotname: self.active_slot = max(0, self.active_slot - 1) self.resize_slots() self.resize_windows() def single_slot(self): while len(self.bufferlist.slots) > 1: if self.active_slot == 0: self.remove_slot(1) else: self.remove_slot(0) def get_window_height_width(self, slotname): assert slotname >= 0 and slotname < len(self.bufferlist.slots), \ "invalid slot: %r" % slotname slot = self.bufferlist.slots[slotname] return (slot.height, slot.width) # mini buffer handling def get_mini_buffer(self): return self.mini_buffer def mini_buffer_is_open(self): return self.mini_buffer is not None def open_mini_buffer(self, prompt, callback, method=None, tabber=None, modename=None): if self.mini_buffer_is_open(): self.close_mini_buffer() self.mini_prompt = prompt self.mini_buffer = minibuffer.MiniBuffer(callback, method, tabber, modename) window.Window(self.mini_buffer, self, height=1, width=self.x-1-len(self.mini_prompt)-1, slot='mini') self.mini_active = True def exec_mini_buffer(self): self.mini_buffer.callback(self.mini_buffer.make_string()) self.close_mini_buffer() def close_mini_buffer(self): if self.mini_buffer_is_open(): self.mini_buffer.close() self.mini_buffer = None self.mini_prompt = "" self.mini_active = False def get_mini_buffer_prompt(self): return self.mini_prompt def set_mini_buffer_prompt(self, p): self.mini_prompt = p # window handling def toggle_window(self): assert 0 <= self.active_slot and self.active_slot < len(self.bufferlist.slots) self.active_slot = (self.active_slot + 1) % len(self.bufferlist.slots) def window(self): slotname = self.active_slot return self.bufferlist.slots[slotname].buffer.get_window(slotname) def active_window(self): if self.mini_active: return self.mini_buffer.get_window('mini') else: assert 0 <= self.active_slot and self.active_slot < len(self.bufferlist.slots), \ "0 <= %d < %d" % (self.active_slot, len(self.bufferlist.slots)) slotname = self.active_slot return self.bufferlist.slots[slotname].buffer.get_window(slotname) # buffer handling def file_buffer(self, path, data, switch_to=True): assert not self.has_buffer_name(path), 'oh no! %r is already open' % path assert not os.path.exists(path), 'oh no! %r already exists in fs' % path f = open(path, 'w') f.write(data) f.close() b = buffer.FileBuffer(path) b.open() self.add_window_to_buffer(b, self.active_slot) self.add_buffer(b) if switch_to: self.switch_buffer(b) def data_buffer(self, name, data, switch_to=True, modename=None): if self.has_buffer_name(name): b = self.bufferlist.buffer_names[name] self.remove_buffer(b) b = buffer.DataBuffer(name, data) if modename is not None: b.modename = modename self.add_window_to_buffer(b, self.active_slot) self.add_buffer(b) if switch_to: self.switch_buffer(b) def get_buffer_by_path(self, path): return self.bufferlist.get_buffer_by_path(path) def has_buffer_name(self, name): return self.bufferlist.has_buffer_name(name) def get_buffer_by_name(self, name): return self.bufferlist.get_buffer_by_name(name) def has_buffer(self, b): return self.bufferlist.has_buffer(b) def add_buffer(self, b): self.bufferlist.add_buffer(b) def remove_buffer(self, b): assert b.name() is not "*Scratch*", "can't kill the scratch" assert self.bufferlist.has_buffer(b), "can't kill what's not there" assert len(self.bufferlist.buffers) > 1, "can't kill with no other buffers" self.bufferlist.remove_buffer(b) b.close() if self.bufferlist.empty_slot(self.active_slot): b2 = self.bufferlist.hidden_buffers[0] self.bufferlist.set_slot(self.active_slot, b2) def switch_buffer(self, b): assert self.has_buffer_name(b.name()), "buffer %s does not exist" % (b.name()) assert 0 <= self.active_slot and self.active_slot < len(self.bufferlist.slots) self.add_window_to_buffer(b, self.active_slot) self.bufferlist.set_slot(self.active_slot, b) def add_window_to_buffer(self, b, slotname): if not b.has_window(slotname): slot = self.bufferlist.slots[slotname] window.Window(b, self, height=slot.height, width=slot.width, slot=slotname) # error string handling def set_error(self, s): self.error_string = s self.error_timestamp = time.time() def clear_error(self): self.error_string = "" self.error_timestamp = None def resize_event(self): self.y, self.x = self.stdscr.getmaxyx() self.resize_slots() self.resize_windows() def resize_slots(self): n = len(self.bufferlist.slots) x = self.x - 1 y_sum = self.y - 1 - n y_pool = y_sum y_offset = 0 for i in range(0, n - 1): slot = self.bufferlist.slots[i] y = y_sum / n slot.resize(y, x, y_offset) y_pool -= y y_offset += y + 1 slot = self.bufferlist.slots[n-1].resize(y_pool, x, y_offset) def resize_windows(self): for b in self.bufferlist.buffers: keys = b.windows.keys() for name in keys: try: (height, width) = self.get_window_height_width(name) b.windows[name].set_size(width, height) except: w = b.windows[name] del b.windows[name] # kill w now # hide the curses cursor def hide_cursor(self): self.win.move(self.y-2, 0) try: curses.curs_set(0) except: pass # exit def exit(self): self.done = True # kill stack manipulation def push_kill(self, s): if s is not None: if self.last_action in self.kill_commands and \ len(self.kill_ring): self.kill_ring[-1] = self.kill_ring[-1] + s else: self.kill_ring.append(s) if len(self.kill_ring) > KILL_RING_LIMIT: self.kill_ring.pop(0) def pop_kill(self): return self.kill_ring.pop(-1) def has_kill(self, i=-1): return len(self.kill_ring) >= abs(i) def get_kill(self, i=-1): return self.kill_ring[i] # undo/redo def undo(self): try: self.window().buffer.undo() except Exception, e: self.set_error("%s" % (e)) def redo(self): try: self.window().buffer.redo() except Exception, e: self.set_error("%s" % (e)) # action creating methods def make_insert_action(self, c): return lambda: self.window().insert_string(c) def make_window_action(self, methodname): f = getattr(self.window(), methodname) f() # we are evil def eval(self, s): return eval(s) # the might run-loop! def run(self): self.done = False #keycodes = [] while not self.done: i = self.win.getch() #if i > 0: # if len(keycodes) >= 6: # keycodes.pop(0) # keycodes.append(str(i)) #self.set_error('keycodes: %s' % repr(keycodes)) if i == curses.KEY_RESIZE: while i == curses.KEY_RESIZE: i = self.win.getch() self.resize_event() err = '' try: self.input.parse(i) except Exception, e: err = str(e) while len(self.input.tokens): t = self.input.tokens.pop(0) self.active_window().mode.handle_token(t) self.draw() if err: self.set_error(err) if self.error_timestamp is not None and \ ERROR_TIMEOUT > 0 and \ time.time() - self.error_timestamp > ERROR_TIMEOUT: self.clear_error() return # highlighting # each highlighted_range contains three things: [window, start_p, end_p] def add_highlighted_range(self, w, start_p, end_p): self.highlighted_ranges.append([w, start_p, end_p]) def clear_highlighted_ranges(self): self.highlighted_ranges = [] # full screen drawer def draw(self): self.hide_cursor() self.draw_slots() self.draw_input_bar() self.hide_cursor() self.win.noutrefresh() self.hide_cursor() curses.doupdate() # debugging def dump(self): w = self.window() ll = len(w.buffer.lines) pl = len(w.get_physical_lines()) vl = len(w.visible_lines()) first = w.first last = w.last cursor = w.logical_cursor() vcursor = w.visible_cursor() s = "" s += "width: %d\n" % (w.width) s += "height: %d\n" % (w.height) s += "len logical lines: %d\n" % (ll) s += "logical first: %s\n" % (first) s += "logical last: %s\n" % (last) s += "logical cursor: %s\n" % (cursor) s += "len physical lines: %d\n" % (pl) s += "physical first: %s\n" % (w.physical_point(first)) s += "physical last: %s\n" % (w.physical_point(last)) s += "physical cursor: %s\n" % (w.physical_point(cursor)) s += "len visible lines: %d\n" % (vl) s += "visible first: %s\n" % ("n/a") s += "visible last: %s\n" % ("n/a") s += "visible cursor: %s\n" % (vcursor) return s # sub-drawing methods def draw_slots(self): self.win.erase() for i in range(0, len(self.bufferlist.slots)): slot = self.bufferlist.slots[i] self.draw_slot(i) self.draw_status_bar(i) def draw_slot(self, slotname): slot = self.bufferlist.slots[slotname] if not slot.buffer.has_window(slotname): return w = slot.buffer.get_window(slotname) lines = w.visible_lines() regions = w.mode.visible_regions() ## FIXME: why isn't this always the same???? assert (len(lines) == len(regions) or len(lines) == len(regions) - 1), "%d,%d" % (len(lines), len(regions)-1) assert len(lines) > 0, "no lines... why?" m = min(len(lines), slot.height) assert m > 0 x = slot.width y = slot.height y_offset = slot.offset assert x > 0 assert y > 0 red_attr = color.build_attr(color.pairs('red', 'default')) for i in range(0, m): j = 0 line = lines[i] for r in regions[i]: try: # start, end, attr, value, ttype = r assert 0 <= r.start, "0 <= %d" % (r.start) assert r.start <= r.end, "%d <= %d" % (r.start, r.end) assert r.end <= len(line), "%d <= %d" % (r.end, len(line)) except Exception, e: s = "\n".join([repr(x) for x in regions]) raise Exception, "%s\n%s\n\n%s\n\n%s\n\n%s\n\n%d" % \ (e, s, regions[i], r, repr(line), len(line)) assert line[r.start:r.end] == r.value, \ "%r != %r" % (line[r.start:r.end], r.value) try: if DARK_BACKGROUND: attr = r.attr | curses.A_BOLD else: attr = r.attr self.win.addnstr(i + y_offset, r.start, r.value, r.end - r.start, attr) except Exception, e: raise Exception, "%s\n%s %s %s %s" % \ (e, repr(i), repr(r.start), repr(r.value), repr(r.end - r.start)) j = r.end if j < len(line): # this is cheating... FIXME self.win.addnstr(i + y_offset, j, line[j:], len(line) - j) j += len(line) - j if j < x: self.win.addnstr(i + y_offset, j, ' ' * (x-j), (x-j)) if w.continued_visible_line(i): self.win.addch(i + y_offset, x, '\\', red_attr) else: self.win.addch(i + y_offset, x, ' ') for i in range(m, y): self.win.addnstr(i + y_offset, 0, '~' + ' ' * (x), x + 1, red_attr) for (high_w, lp1, lp2) in self.highlighted_ranges: if lp1.y != lp2.y: # this region is incoherent, so skip it, or die, whatever #raise Exception, "haddock %d != %d" % (lp1.y, lp2.y) pass elif w is not high_w: # this region isn't in the current window so skip it pass else: (pp1, pp2) = (w.physical_point(lp1), w.physical_point(lp2)) vo = w.visible_offset() (vp1, vp2) = (pp1.offset(0, -vo), pp2.offset(0, -vo)) if vp2.y < 0 or vp1.y > w.height: # this region is not visible, so skip it pass else: # first let's fix our points so we're sure they're visible if vp1.y < 0: vp1 = point.Point(0,0) if vp2.y > w.height: vp2 = point.Point(len(lines[-1]), w.height-1) if vp1.y == vp2.y: # our region physically fits on one line; this is easy b = lines[vp1.y][vp1.x:vp2.x] self.win.addstr(vp1.y + y_offset, vp1.x, b, curses.A_REVERSE) else: # our region spans multiple physical lines, so deal b1 = lines[vp1.y][vp1.x:] self.win.addstr(vp1.y + y_offset, vp1.x, b1, curses.A_REVERSE) for i in range(vp1.y + 1, vp2.y): b = lines[i] self.wind.addstr(i + y_offset, 0, b, curses.A_REVERSE) b2 = lines[vp2.y][:vp2.x] self.win.addstr(vp2.y + y_offset, 0, b2, curses.A_REVERSE) if self.margins_visible: for (limit, shade) in self.margins: if self.x > limit: for i in range(0, y): # the actual character is the lower 8 bits, and the # attribute is the upper 8 bits; we will ignore the # attribute and just get the character char = self.win.inch(i + y_offset, limit) & 255 attr = color.build('default', shade, 'bold') self.win.addch(i + y_offset, limit, char, attr) if self.mini_active is False and self.active_slot == slotname: if w.active_point is not None and w.point_is_visible(w.active_point): pa = w.physical_point(w.active_point) va = pa.offset(0, -w.visible_offset()) if len(lines[va.y]): a = lines[va.y][va.x] else: a = ' ' self.win.addch(va.y + y_offset, va.x, a, curses.A_REVERSE) else: cursor = w.visible_cursor() cx, cy = (cursor.x, cursor.y) if cy >= len(lines): self.set_error('in main1: cursor error; %d >= %d' % (cy, len(lines))) return elif cx == len(lines[cy]): c = ' ' elif cx > len(lines[cy]): self.set_error('why? %r %r' % (cursor, len(lines[cy]))) return else: c = lines[cy][cx] self.win.addch(cy + y_offset, cx, c, curses.A_REVERSE) def draw_status_bar(self, slotname): slot = self.bufferlist.slots[slotname] if not slot.buffer.has_window(slotname): return w = slot.buffer.get_window(slotname) b = w.buffer cursor = w.logical_cursor() pcursor = w.physical_cursor() first = w.first last = w.last if b.readonly(): if b.changed(): modflag = '%*' else: modflag = '%%' else: if b.changed(): modflag = '**' else: modflag = '--' if w.mark: mark = w.mark else: mark = point.Point(-1, -1) name = b.name() if w.first_is_visible(): perc = "Top" elif w.last_is_visible(): perc = "Bot" else: perc = "%2d%%" % (first.y*100 / len(b.lines)) # XYZ: we should actually use more of the 'state' variables format = "----:%s-Fl %-18s (%s)--L%d--C%d--%s" status = format % (modflag, name, w.mode.name(), cursor.y+1, cursor.x+1, perc) status = status[:slot.width + 1] status += "-" * (slot.width - len(status) + 1) self.win.addnstr(slot.height + slot.offset, 0, status, slot.width + 1, curses.A_REVERSE) # input bar drawing def draw_input_bar(self): if self.error_string: self.draw_error() elif self.mini_buffer_is_open(): self.draw_mini_buffer() else: self.draw_nothing() try: # fucking python, fucking curses, fucking fuck self.win.addch(self.y-1, self.x-1, ' ') except: pass def draw_error(self): l = self.x - 1 s1 = self.error_string s2 = util.cleanse(util.padtrunc(s1, l)) self.win.addnstr(self.y-1, 0, s2, l) def draw_mini_buffer(self): l = self.x - 1 w = self.mini_buffer.get_window('mini') lines = w.visible_lines() s1 = self.mini_prompt + lines[0] s2 = util.padtrunc(s1, l) self.win.addnstr(self.y-1, 0, s2, l) if self.mini_active: cursor = w.visible_cursor() cx, cy = (cursor.x, cursor.y) if cy >= len(lines): #self.set_error('in main2: cursor error; %d >= %d' % # (cy, len(lines))) self.set_error('in main2: %r, %r [f:%r,l:%r] {h:%r,w:%r} %r' % (len(lines), cursor, w.first, w.last, w.height, w.width, len(lines[0]))) return elif cx == len(lines[cy]): c = ' ' else: c = lines[cy][cx] self.win.addch(self.y-1, cx + len(self.mini_prompt), c, curses.A_REVERSE) def draw_nothing(self): l = self.x - 1 self.win.addnstr(self.y-1, 0, util.pad('', l), l) def open_aes_file(path, nl, name=None): p = getpass.getpass("Please enter the AES password: ") b = buffer.AesBuffer(path, p, nl, name) return b def open_plain_file(path, nl, name=None): b = buffer.FileBuffer(path, nl, name) return b if __name__ == "__main__": ciphers = { 'none': open_plain_file, 'aes': open_aes_file } linetypes = { 'win': '\r\n', 'mac': '\r', 'unix': '\n' } import optparse parser = optparse.OptionParser() parser.set_defaults(debug=False) parser.set_defaults(goto=None) parser.set_defaults(mode=None) parser.set_defaults(cipher='none') parser.set_defaults(linetype='unix') parser.add_option('-d', '--debug', dest='debug', action='store_true', help='run in debug mode') parser.add_option('-e', '--encrypt', dest='cipher', metavar='CIPHER', help='decrypt and encrypt with CIPHER (default: none)') parser.add_option('-g', '--goto', dest='goto', metavar='NUM', type='int', help='jump to line NUM of the first argument') parser.add_option('-l', '--line-end', dest='linetype', metavar='TYPE', help='use TYPE (win,mac,unix) line endings (default: unix)') parser.add_option('-m', '--mode', dest='mode', metavar='MODE', help='open arguments in MODE') (opts, args) = parser.parse_args() # if debugging, disable error handling to produce backtraces if opts.debug: mode.DEBUG = True # we will support using +19 as the first argument to indicate opening the # first file on line 19 (same as -g 19 or --goto 19) if len(sys.argv) > 1 and args[0].startswith('+'): opts.goto = int(args[0][1:]) args = args[1:] # figure out which kind of line types we're using if opts.linetype not in linetypes: sys.stderr.write('invalid linetype: %r' % opts.linetype) sys.exit(1) nl = linetypes[opts.linetype] # figure out what kind of file open function to use if opts.cipher not in ciphers: sys.stderr.write('invalid cipher: %r' % opts.cipher) sys.exit(2) f = ciphers[opts.cipher] # open each path using our callback to get a buffer, open that buffer, etc. buffers = [] names = sets.Set() paths = sets.Set() for path in args: path = os.path.abspath(os.path.realpath(util.expand_tilde(path))) if path in paths: continue name = os.path.basename(path) if name in names: i = 1 auxname = '%s/%d' % (name, i) while auxname in names: i += 1 auxname = '%s/%d' % (name, i) name = auxname b = f(path, nl, name) b.open() buffers.append(b) paths.add(path) names.add(name) # ok, now run our app run(buffers, opts.goto, opts.mode)