#!/usr/bin/env python import curses, curses.ascii, getpass, os, re, string, sets, sys, termios, time import traceback import buffer2, bufferlist, color, completer, keyinput, method, minibuffer import util, window2 from point2 import Point # 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 import mode2, mode_mini, mode_python, mode_perl 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 = True #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': mode2.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(buffer2.ScratchBuffer()) buffers.append(buffer2.ConsoleBuffer()) self.bufferlist = bufferlist.BufferList(height, width) self.active_slot = 0 #self.resize_slots() # build windows for our buffers for b in buffers: window2.Window(b, self, height, width, mode_name=init_mode) self.bufferlist.add_buffer(b) self.bufferlist.set_slot(0, buffers[0]) # see if the user has requested that we go to a particular line if jump_to_line: w = self.bufferlist.slots[0].window 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): # XYZ b = self.bufferlist.slots[self.active_slot].window.buffer n = self.bufferlist.add_slot() self.bufferlist.set_slot(n, b) def remove_slot(self, n): assert len(self.bufferlist.slots) > 1, "oh no you didn't!" assert n >= 0 and n < len(self.bufferlist.slots), \ "invalid slot: %r (%r)" % (n, len(self.bufferlist.slots)) self.bufferlist.remove_slot(n) if self.active_slot > slotname: self.active_slot = max(0, self.active_slot - 1) 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, i): assert i >= 0 and i < len(self.bufferlist.slots), \ "invalid slot: %r" % slotname slot = self.bufferlist.slots[i] 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) window2.Window(self.mini_buffer, self, height=1, width=self.x-1-len(self.mini_prompt)-1) 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): return self.bufferlist.slots[self.active_slot].window def active_window(self): if self.mini_active: return self.mini_buffer.windows[0] else: assert 0 <= self.active_slot and self.active_slot < len(self.bufferlist.slots), \ "0 <= %d < %d" % (self.active_slot, len(self.bufferlist.slots)) i = self.active_slot return self.bufferlist.slots[i].window # 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 = buffer2.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 = buffer2.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): # XYZ if not b.has_window(slotname): slot = self.bufferlist.slots[slotname] window2.Window(b, self, height=slot.height, width=slot.width) # 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() def resize_slots(self): n = len(self.bufferlist.slots) assert n > 0 x = self.x - 1 y_sum = self.y - 1 - n # XYZ this method seems broken self.bufferlist.resize(x, y_sum) # 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().undo() except Exception, e: self.set_error("%s" % (e)) def redo(self): try: self.window().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() # 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, i): slot = self.bufferlist.slots[i] if slot.window is None: return w = slot.window modename = w.mode.name() redattr = color.build_attr(color.pairs('red', 'default')) lines = w.buffer.lines count = 0 (x, y) = w.first.xy() cursor = w.logical_cursor() (cx, cy) = cursor.xy() (px, py) = (None, None) while count < slot.height: if y >= len(lines): self.win.addstr(slot.offset + count, 0, '~', redattr) else: # let's find the cursor if cy == y and cx >= x and cx < x + slot.width: px = cx - x py = count line = lines[y] if modename in w.buffer.highlights: group = w.buffer.highlights[modename].tokens[y] j = 0 for token in group: assert token.y == y if token.x < x: continue elif token.x >= x + slot.width: break c = w.mode.colors.get(token.name, w.mode.default_color) # c = w.mode.default_color # name_parts = token.name.split('.') # for i in range(0, len(name_parts)): # name = '.'.join(name_parts[i:]) # if name in w.mode.colors: # c = w.mode.colors[name] # break if DARK_BACKGROUND: c |= curses.A_BOLD if token.x + len(token.string) >= x + slot.width: n = len(token.string) - x - slot.width + token.x s = token.string[:n] self.win.addstr(slot.offset + count, token.x - x, s, c) else: self.win.addstr(slot.offset + count, token.x - x, token.string, c) else: self.win.addstr(slot.offset + count, 0, line[x:x + slot.width]) if x + slot.width >= len(line): x = 0 y += 1 else: x += slot.width count += 1 if self.margins_visible: for (limit, shade) in self.margins: if self.x > limit: for j in range(0, slot.height): # 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(j + slot.offset, limit) & 255 attr = color.build('default', shade, 'bold') self.win.addch(j + slot.offset, limit, char, attr) if self.mini_active is False and self.active_slot == i: if False and 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 + slot.offset, va.x, a, curses.A_REVERSE) else: assert px is not None and py is not None if cy >= len(lines): self.set_error('in main1: cursor error; %d >= %d' % (cy, len(lines))) return elif cx == len(lines[cy]): c = ' ' elif px > len(lines[cy]): self.set_error('why? %r %r' % (cx, len(lines[cy]))) return else: c = lines[cy][cx] self.win.addch(slot.offset + py , px, c, curses.A_REVERSE) def draw_status_bar(self, slotname): slot = self.bufferlist.slots[slotname] if slot.window is None: return w = slot.window b = w.buffer cursor = w.logical_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(-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) #format = "----:%s-Fl %-18s (%s)--L%d--C%d--%s--%s--%s" #status = format % (modflag, name, w.mode.name(), cursor.y+1, cursor.x+1, w.first, w.last, 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 b = self.mini_buffer s1 = self.mini_prompt + b.lines[0] s2 = util.padtrunc(s1, l) self.win.addnstr(self.y-1, 0, s2, l) if self.mini_active: w = b.windows[0] cursor = w.logical_cursor() (cx, cy) = cursor.xy() if cy >= len(b.lines): return elif cx == len(b.lines[cy]): c = ' ' else: c = b.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 = buffer2.AesBuffer(path, p, nl, name) return b def open_plain_file(path, nl, name=None): b = buffer2.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: mode2.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:] if opts.goto is not None: opts.goto += 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)