#!/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, mode, mode2 import util, window2 from point2 import Point 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(object): 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 # 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() # make sure the cursor is visible #curses.curs_set(1) # 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 # the first dict stores our modes by (lowercase) name, and the other # dicts all store various ways to "auto-detecting" the correct mode, # in the precedence with which they are used. self.modes = {} self.mode_paths = {} self.mode_basenames = {} self.mode_extensions = {} self.mode_detection = {} # ok, now let's load all the "standard" modes mode2.install(self) for name in ('blame', 'c', 'console', 'consolemini', 'css', 'diff', 'dir', 'elisp', 'hex', 'html', 'java', 'javascript', 'lisp', 'make', 'mini', 'mutt', 'nasm', 'ocaml', 'perl', 'python', 'replace', 'rst', 'scheme', 'search', 'sh', 'sql', 'tt', 'text', 'text2', 'which', 'xml'): exec("import mode.%s; mode.%s.install(self)" % (name, name)) # 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: obj = method.InsertString(c) self.methods[obj.name] = obj obj = method.OverwriteChar(c) self.methods[obj.name] = obj # window/slot height/width height = self.y - 2 width = self.x - 1 # run user custom code here self.rcerror = None self.loadrc() # initialize our buffers # note that the first buffer in buffers will be initially visible buffers.append(buffer2.ScratchBuffer()) buffers.append(buffer2.ConsoleBuffer()) if self.rcerror: buffers.insert(0, buffer2.DataBuffer('*RcError*', self.rcerror)) self.bufferlist = bufferlist.BufferList(height, width) self.active_slot = 0 # build windows for our buffers for b in buffers: if b.name() == '*Console*': window2.Window(b, self, height, width, mode_name='console') else: 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 self.registers = {} # 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(0) curses.meta(1) curses.cbreak() #curses.halfdelay(5) curses.noecho() curses.nonl() # this sets up a mode, as well as optionally adding information on when to # auto-load the mode def setmode(self, name, cls, paths=[], basenames=[], extensions=[], detection=[]): self.modes[name] = cls for p in paths: self.mode_paths[p] = name for b in basenames: self.mode_basenames[b] = name for e in extensions: self.mode_extensions[e] = name for d in detection: self.mode_detection[d] = name 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 > n: self.active_slot = max(0, self.active_slot - 1) #XYZ 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) # files and stuff def open_path(self, path, binary=False, cipher=None, password=None): path = os.path.abspath(os.path.realpath(util.expand_tilde(path))) b = self.get_buffer_by_path(path) if b is None: name = os.path.basename(path) if self.has_buffer_name(name): i = 1 auxname = '%s/%d' % (name, i) while self.has_buffer_name(auxname): i += 1 auxname = '%s/%d' % (name, i) name = auxname mode_name = None if cipher is None: if not os.path.exists(path) or os.path.isfile(path): if binary: b = buffer2.Binary32Buffer(path, name=name) else: b = buffer2.FileBuffer(path, name=name) elif os.path.isdir(path): b = buffer2.DirBuffer(path, name=name) mode_name = 'dir' else: raise Exception, "not a file or dir: %r" % path elif cipher == 'aes': if not password: raise Exception, "password is required" if not os.path.exists(path) or os.path.isfile(path): b = buffer2.AesBuffer(path, password, name=name) else: raise Exception, "not a file or dir: %r" % path b.open() window2.Window(b, self, height=0, width=0, mode_name=mode_name) self.add_buffer(b) return b # 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) try: window2.Window(self.mini_buffer, self, height=1, width=self.x-1-len(self.mini_prompt)-1) self.mini_active = True except minibuffer.MiniBufferError: self.mini_buffer = None self.mini_prompt = '' def exec_mini_buffer(self): self.mini_buffer.callback(self.mini_buffer.make_string()) self.close_mini_buffer() def close_mini_buffer(self): self.mini_active = False if self.mini_buffer_is_open(): self.mini_buffer.close() self.mini_buffer = None self.mini_prompt = "" assert not self.mini_active 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) #XYZ 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() window2.Window(b, self, height=0, width=0) 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 window2.Window(b, self, height=0, width=0) 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 self.bufferlist.resize(y_sum, x) # 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 KILL_RING_LIMIT and 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) # load user configuration NOW def loadrc(self): path = os.path.join(os.getenv('HOME'), '.pmc', 'conf') if os.path.exists(path): try: execfile(path) except Exception, e: s = traceback.format_exc() self.rcerror = 'There was an error during startup:\n\n%s' % s # the mighty run-loop! def run(self): self.done = False self.draw() while not self.done: i = self.win.getch() 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(err) # clear the error line; it might look confusing to the user try: self.win.addstr(self.y-1, 0, ' ' * self.x) except: pass self.win.refresh() return # highlighting # each highlighted_range contains three things: [window, start_p, end_p] def add_highlighted_range(self, w, p1, p2, fg='default', bg='default'): self.highlighted_ranges.append([w, p1, p2, fg, bg]) def clear_highlighted_ranges(self): self.highlighted_ranges = [] # full screen drawer def draw(self, err=""): try: self.draw_slots() self.draw_input_bar() self.draw_cursor() self.win.noutrefresh() curses.doupdate() except: # ok, so there was a problem... # let's see if the screen changed sizes and if so, resize our slots self.resize_event() 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() (y, x) = self.stdscr.getmaxyx() if y != self.y or x != self.x: self.resize_event() def draw_cursor(self): if self.mini_active: b = self.mini_buffer w = b.windows[0] p = w.logical_cursor() if p.y >= len(b.lines): return (vy, vx) = (self.y - 1, min(p.x + len(self.mini_prompt), self.x - 2)) else: slot = self.bufferlist.slots[self.active_slot] w = slot.window if w.active_point is not None and w.point_is_visible(w.active_point): p = w.active_point else: p = w.logical_cursor() count = 0 (x, y) = w.first.xy() (vy, vx) = (None, None) while count < slot.height: if p.y == y and p.x >= x and p.x <= x + slot.width: (vy, vx) = (slot.offset + count, p.x - x) break if x + slot.width >= len(w.buffer.lines[y]): x = 0 y += 1 else: x += slot.width count += 1 if vy is None or vx is None: return try: self.win.move(vy, vx) except: raise Exception, "(%d,%d==%r) was illegal (%d,%d)" % \ (vx, vy, p, self.x, self.y) # 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 highlight_char(self, sy, sx, fg='default', bg='default'): junk = self.win.inch(sy, sx) char = junk & 255 #attr = color.build(fg, bg, curses.A_REVERSE) attr = color.build(fg, bg) try: self.win.addch(sy, sx, char, attr) except: raise Exception, "(%d, %d, %r, %r) v. (%d, %d)" % \ (sy, sx, fg, bg, self.y, self.x) def highlight_chars(self, sy, sx1, sx2, fg='default', bg='default'): assert sx2 < self.x, "%d < %d" % (sx2, self.x) for x in range(sx1, sx2): self.highlight_char(sy, x, fg, bg) def draw_slot(self, i): assert self.active_slot < len(self.bufferlist.slots), "only two" assert i < len(self.bufferlist.slots), "only three" slot = self.bufferlist.slots[i] if slot.window is None: return w = slot.window modename = w.mode.name() if modename in w.buffer.highlights: self._draw_slot_lit(i) else: self._draw_slot_raw(i) # highlighted regions for (high_w, p1, p2, fg, bg) in self.highlighted_ranges: if w is high_w and p2 >= w.first and p1 <= w.last: count = 0 (x, y) = w.first.xy() px = p1.x while count < slot.height: if p1.y == y and px >= x and px - x < slot.width: if slot.width > p2.x - x: self.highlight_chars(slot.offset + count, px-x, p2.x-x, fg, bg) break else: self.highlight_chars(slot.offset + count, px-x, slot.width, fg, bg) px += slot.width - px + x if x + slot.width >= len(w.buffer.lines[y]): x = 0 y += 1 else: x += slot.width count += 1 if w.margins_visible: for (limit, shade) in w.margins: #if limit <= self.x: if limit < self.x: for j in range(0, slot.height): char = self.win.inch(j + slot.offset, limit) & 255 attr = color.build('default', shade, 'bold') self.win.addch(j + slot.offset, limit, char, attr) def _draw_slot_raw(self, i): slot = self.bufferlist.slots[i] w = slot.window modename = w.mode.name() redattr = color.build_attr(color.pairs('red', 'default')) (x, y) = w.first.xy() lines = w.buffer.lines count = 0 while count < slot.height: if y >= len(lines): self.win.addstr(slot.offset + count, 0, '~', redattr) count += 1 continue line = lines[y] s = line[x:x + slot.width] try: self.win.addstr(slot.offset + count, 0, s) except: self.set_error("addstr(%r + %r, %r, %r)" % (slot.offset, count, 0, s)) if x + slot.width >= len(line): x = 0 y += 1 else: self.win.addch(slot.offset + count, slot.width, '\\', redattr) x += slot.width count += 1 def _draw_slot_lit(self, i): slot = self.bufferlist.slots[i] w = slot.window modename = w.mode.name() redattr = color.build_attr(color.pairs('red', 'default')) highlighter = w.buffer.highlights[modename] (x, y) = w.first.xy() j = 0 count = 0 assert len(w.buffer.lines) == len(highlighter.tokens) while count < slot.height: if y < len(w.buffer.lines): while j < len(highlighter.tokens[y]): token = highlighter.tokens[y][j] if token.string.endswith('\n'): tstring = token.string[:-1] else: tstring = token.string assert token.y == y, '%d == %d' % (token.y, y) s_offset = max(x - token.x, 0) x_offset = max(token.x - x, 0) assert x_offset <= slot.width, '%d <= %d' % (x_offset, slot.width) s = tstring[s_offset:] token_done = x_offset + len(s) <= slot.width token_wrap = x_offset + len(s) > slot.width attr = color.build(*token.color) self.win.addstr(slot.offset + count, x_offset, s[:slot.width - x_offset], attr) if token_wrap: self.win.addch(slot.offset + count, slot.width, '\\', redattr) x += slot.width count += 1 if token_done: j += 1 if count >= slot.height: break # we have finished this logical line of tokens j = x = 0 y += 1 count += 1 else: self.win.addstr(slot.offset + count, 0, '~', redattr) count += 1 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 %-18s (%s)--L%d--C%d--%s" status = format % (modflag, name, w.mode.name(), cursor.y+1, cursor.x+1, perc) #format = "%s %-18s (%s)--L%d--C%d--%s %s %s %s" #status = format % (modflag, name, w.mode.name(), cursor.y+1, cursor.x+1, perc, w.first, cursor, w.last) status = status.ljust(slot.width + 1)[:slot.width + 1] self.win.addstr(slot.height + slot.offset, 0, status, 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 _get_mini_lines(self, s): i = self.x - 1 return [s[:i]] lines = [s[:i]] while i < len(s): lines.append(s[i:self.x - 1 + i]) i += self.x - 1 return lines def draw_error(self): lines = self._get_mini_lines(self.error_string) for i in range(0, len(lines)): line = util.pad(lines[i], self.x - 1) self.win.addstr(self.y - len(lines) + i, 0, line) def draw_mini_buffer(self): b = self.mini_buffer lines = self._get_mini_lines(self.mini_prompt + b.lines[0]) for i in range(0, len(lines)): line = util.pad(lines[i], self.x - 1) self.win.addstr(self.y - len(lines) + i, 0, line) def draw_nothing(self): self.win.addstr(self.y-1, 0, util.pad('', self.x - 1)) def open_aes_file(path, nl, name=None): if os.path.isfile(path) or not os.path.exists(path): p = getpass.getpass("Please enter the AES password: ") return buffer2.AesBuffer(path, p, nl, name) else: raise Exception, "can't open %r; unsupported file type" % path def open_plain_file(path, nl, name=None, binary=False): if os.path.isfile(path) or not os.path.exists(path): if binary: return buffer2.Binary32Buffer(path, nl, name) else: return buffer2.FileBuffer(path, nl, name) elif os.path.isdir(path): return buffer2.DirBuffer(path, nl, name) else: raise Exception, "can't open %r; unsupported file type" % path 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.set_defaults(binary=False) parser.add_option('-b', '--binary', dest='binary', action='store_true', help='open file(s) in hex binary mode') 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(args) > 0 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() if not args: args = ['.'] 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 try: b = f(path, nl, name, opts.binary) b.open() except buffer2.BinaryDataException, e: b = f(path, nl, name, True) b.open() buffers.append(b) paths.add(path) names.add(name) # ok, now run our app run(buffers, opts.goto, opts.mode)