#!/usr/bin/env python import curses from getpass import getpass import locale import os import string from subprocess import Popen, PIPE, STDOUT import sys import termios import time import traceback import buffer import buffer.about import buffer.colors import buffer.console import buffer.data import buffer.fs import buffer.aes from bufferlist import BufferList import color import completer import keyinput import method from minibuffer import MiniBuffer, MiniBufferError import mode import util from window import Window class PmcError(Exception): pass class RunError(PmcError): pass class FileError(PmcError): pass class DrawError(PmcError): pass class Application(object): def __init__(self, stdscr, buffers=[], **kwargs): # initalize curses primitives self.stdscr = stdscr self.y, self.x = self.stdscr.getmaxyx() # we have to do this early so we can log errors self.log = buffer.LogBuffer() # initialize some basic stuff self.config = {} self.highlighted_ranges = [] self.highlight_mark = False self.mini_active = False self.mini_buffer = None self.mini_prompt = "" self.error_string = "" self.error_timestamp = None self.need_draw = True self.input = keyinput.Handler() # white is for delimiters, operators, numbers default = ('default', 'default') # magenta is for keywords/builtins, translation, globs #lo_magenta = ('magenta202', 'default') hi_magenta = ('magenta505', 'default') # red is for comments, pods, endblocks #lo_red = ('red300', 'default') hi_red = ('red511', 'default') # orange are for arrays and hashes #hi_orange = ('yellow531', 'default') #lo_orange = ('yellow520', 'default') # yellow is for scalars and prototypes #hi_yellow = ('yellow551', 'default') #lo_yellow = ('yellow330', 'default') # green is for strings and hash keys lo_green = ('green030', 'default') hi_green = ('green050', 'default') # cyan is for quotes, evals, regexes, subs #lo_cyan = ('cyan033', 'default') hi_cyan = ('cyan155', 'default') # blue is unused #lo_blue = ('blue113', 'default') hi_blue = ('blue225', 'default') # let's prepopulate some default token colors self.cached_colors = {} self.token_colors = { 'spaces': default, 'eol': default, 'comment': hi_red, 'comment.start': hi_red, 'comment.data': hi_red, 'comment.null': hi_red, 'comment.end': hi_red, 'continuation': hi_red, 'escaped': hi_magenta, 'string.start': lo_green, 'string.data': hi_green, 'string.null': hi_green, 'string.hex': hi_magenta, 'string.octal': hi_magenta, 'string.escaped': hi_magenta, 'string.end': lo_green, 'char': hi_green, #'integer': ('white533', 'default'), #'float': ('white533', 'default'), #'number': ('white533', 'default'), 'integer': default, 'float': default, 'number': default, 'label': hi_magenta, 'keyword': hi_cyan, 'reserved': hi_cyan, 'function': hi_blue, 'builtin': hi_magenta, 'method': hi_cyan, 'bareword': default, 'delimiter': default, 'operator': default, } self.default_color = ('default', 'default',) # initialize configuration defaults self._load_config_defaults() # initialize our colors if curses.has_colors(): curses.start_color() try: curses.use_default_colors() color.default_color = True except: color.default_color = False color.init() # 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 = {} # initialize our methods self.methods = {} names = ( 'method', 'method.svn', 'method.cvs', 'method.search', 'method.buffers', 'method.move', 'method.shell', 'method.introspect', 'method.help', 'method.numbers', 'method.spell', 'method.hg', 'method.utf8', 'method.tags', 'method.git', ) for name in names: self.load_pmacs_methods(name) #exec("import %s" % name) #mod = eval(name) #for mname in dir(mod): # if mname.startswith('_'): # continue # cls = eval("%s.%s" % (name, mname)) # if hasattr(cls, '_is_method') and cls._is_method: # self.methods[cls._name()] = cls() # ok, now let's load all the "standard" modes mode.install(self) names = ( 'blame', 'c', 'console', 'consolemini', 'css', 'diff', 'dir', 'elisp', 'hex', 'html', 'java', 'javap', 'javascript', 'lisp', 'make', 'mini', 'mutt', 'nasm', 'ocaml', 'perl', 'python', 'replace', 'rst', 'scheme', 'search', 'sh', 'sql', 'tt', 'text', 'text2', 'which', 'xml', 'cheetah', 'colortext', 'latex', 'insertmini', 'conf', 'haskell', 'erlang', 'iperl', 'iperlmini', 'ipython', 'ipythonmini', 'awk', 'shell', 'shellmini', 'fstab', 'yacc', 'pipe', 'mbox', 'error', 'lua', 'lily', 'forth', 'ebnf', 'colortest', 'go', 'inform6', 'scala', 'markdown', 'roy', ) for name in names: exec("import mode.%s; mode.%s.install(self)" % (name, name)) # create all the insert methods for the character ranges we like for c in string.ascii_letters + string.digits + string.punctuation: obj = method.InsertString(c) self.methods[obj.name] = obj obj = method.OverwriteChar(c) self.methods[obj.name] = obj # buffer list stuff height = self.y - 1 width = self.x self.bufferlist = BufferList(height, width) self.active_slot = 0 self.complete_slot = None # run user custom code here self.rcerror = None self.loadrc() # initialize our buffers # note that only the first buffer will be initially visible buffers_ = list(buffers) buffers_.append(buffer.about.AboutBuffer()) buffers_.append(self.log) if self.rcerror: buffers_.insert(0, buffer.data.DataBuffer('*RcError*', self.rcerror)) # build windows for our buffers for b in buffers_: if b.modename: Window(b, self) else: Window(b, self, mode_name=kwargs.get('init_mode')) self.bufferlist.add_buffer(b) self.bufferlist.set_slot(self.active_slot, buffers_[0]) # see if the user has requested that we go to a particular line jump_to_line = kwargs.get('jump_to_line') if not self.rcerror and jump_to_line: w = self.bufferlist.slots[0].window self.methods['goto-line'].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 = {} self.arg_history = {'default': []} # this is used to maintain state about various things (ctags, version # control, etc) which is shared across several different buffers. # this is so it will be correctly inherited by new buffers, and also # so that when buffers are closed this state won't be lost. self.state = { 'tags': {}, 'vc': {}, } # initialize tab handlers completer.set_completer('path', completer.FileCompleter(self)) completer.set_completer('buffer', completer.BufferCompleter(self)) completer.set_completer('command', completer.CommandCompleter(self)) completer.set_completer('shell', completer.ShellCompleter(self)) completer.set_completer('config', completer.ConfigCompleter(self)) completer.set_completer('method', completer.MethodCompleter(self)) completer.set_completer('register', completer.RegisterCompleter(self)) completer.set_completer('mode', completer.ModeCompleter(self)) completer.set_completer('token', completer.TokenCompleter(self)) # set up curses self.win = curses.newwin(self.y, self.x, 0, 0) self.win.leaveok(0) curses.meta(1) curses.cbreak() curses.noecho() curses.nonl() curses.halfdelay(1) curses.def_prog_mode() def load_pmacs_methods(self, name): exec("import %s" % name) mod = eval(name) for mname in dir(mod): if mname.startswith('_'): continue cls = eval("%s.%s" % (name, mname)) if hasattr(cls, '_is_method') and cls._is_method: self.methods[cls._name()] = cls() def _load_config_defaults(self): self.config['ignore_suffix'] = ['~', '-', 'CVS', '.svn', '.git', '.hg'] self.config['error_timeout'] = -1 self.config['max_error_len'] = 192 self.config['max_num_kills'] = 64 self.config['word_letters'] = string.ascii_letters + string.digits self.config['default_color'] = ('default', 'default',) self.config['margin'] = 79 self.config['margin_color'] = 'blue' self.config['use_last_replace'] = False def completion_window_is_open(self): n = self.complete_slot if n is None: pass elif n >= len(self.bufferlist.slots): self.complete_slot = None elif self.bufferlist.slots[n].window is None: self.complete_slot = None elif not hasattr(self.bufferlist.slots[n].window.buffer, '_completion'): self.complete_slot = None else: return True return False def get_completion_window(self): return self.bufferlist.slots[self.complete_slot].window def open_completion_buffer(self, s, candidates): opened = False previous = None if len(self.bufferlist.slots) == 1: self.add_slot() opened = True n = len(self.bufferlist.slots) - 1 if self.active_slot == n: n -= 1 if not opened: previous = self.bufferlist.slots[n].window.buffer lines = [] clen = len(candidates) if clen > self.bufferlist.slots[n].height: maxlen = 0 for c in candidates: maxlen = max(maxlen, len(c)) # NOTE: this is not an optimal packing, but it's fast and easy to # understand. i encourage someone else to write something better. numcols = max(self.bufferlist.slots[n].width // (maxlen + 2), 1) numrows = clen - ((clen // numcols) * (numcols - 1)) for i in xrange(0, numrows): names = [] index = i * numcols for j in xrange(0, numcols): if index + j < clen: names.append('%-*s' % (maxlen, candidates[index + j])) else: break lines.append(' '.join(names)) else: lines = list(candidates) data = '\n'.join(lines) b = self.data_buffer("*Completions*", data, switch_to=False) b._completion = s b._opened = opened b._previous = previous self.bufferlist.set_slot(n, b) self.complete_slot = n def close_completion_buffer(self): w = self.get_completion_window() opened = w.buffer._opened if opened: self.bufferlist.remove_slot(self.complete_slot) else: self.bufferlist.set_slot(self.complete_slot, w.buffer._previous) self.close_buffer(w.buffer) self.complete_slot = None def set_completer(self, datatype, comp): completer.set_completer(datatype, comp) # this sets up a mode, as well as optionally adding information on when to # auto-load the mode def setmode(self, name, cls, paths=[], bases=[], exts=[], detection=[]): self.modes[name] = cls for p in paths: self.mode_paths[p] = name for b in bases: self.mode_basenames[b] = name for e in exts: self.mode_extensions[e] = name for d in detection: self.mode_detection[d] = name # we are evil def eval(self, s): return eval(s) def globals(self): return globals() def locals(self): return locals() # slots def add_slot(self): 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): slots = self.bufferlist.slots assert len(slots) > 1, "oh no you didn't!" assert n >= 0 and n < len(slots), "invalid slot %r %r" % (n, len(slots)) self.bufferlist.remove_slot(n) if self.active_slot > n: 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): slots = self.bufferlist.slots assert i >= 0 and i < len(slots), "invalid slot: %d" % i slot = slots[i] return (slot.height, slot.width) # files and stuff def close_buffer(self, b): blist = self.bufferlist blist.remove_buffer(b) b.close() active_slot = blist.slots[self.active_slot] for i in xrange(0, len(blist.slots)): if blist.slots[i].is_empty(): if blist.hidden_buffers: blist.set_slot(i, blist.hidden_buffers[0]) elif active_slot.window: blist.set_slot(i, active_slot.window.buffer) else: blist.set_slot(i, None) assert blist.slots[i].window is not None def close_buffer_by_name(self, name): if self.has_buffer_name(name): self.close_buffer(self.get_buffer_by_name(name)) def make_name(self, name): return util.make_name(name, self.has_buffer_name) #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 #return name def open_path(self, path, binary=False, cipher=None, password=None): path = util.literal_path(path) b = self.get_buffer_by_path(path) if b is None: name = self.make_name(os.path.basename(path)) mode_name = None if cipher is None: if not os.path.exists(path) or os.path.isfile(path): if binary: b = buffer.Binary32Buffer(path, name=name) else: b = buffer.FileBuffer(path, name=name) elif os.path.isdir(path): b = buffer.fs.DirBuffer(path, name=name) mode_name = 'dir' else: raise FileError("not a file or dir: %r" % path) elif cipher == 'aes': if not password: raise RunError("password is required") if not os.path.exists(path) or os.path.isfile(path): b = buffer.aes.AesBuffer(path, password, name=name) else: raise FileError("not a file or dir: %r" % path) try: b.open() #except buffer.BinaryDataException: except: if binary: raise else: binary = True b = buffer.Binary32Buffer(path, name=name) b.open() if mode_name is None: mode_name = 'hex' 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, cb, method=None, tabber=None, modename=None, startvalue=None, queue='default'): parentw = self.bufferlist.slots[self.active_slot].window if self.mini_buffer_is_open(): self.close_mini_buffer() self.mini_prompt = prompt self.mini_buffer = MiniBuffer(cb, self, method, tabber, modename, queue, parentw) try: w = self.x - 1 - len(self.mini_prompt) - 1 Window(self.mini_buffer, self, height=1, width=w) if startvalue: self.mini_buffer.set_data(startvalue) self.arg_history.setdefault(queue, []) self.arg_history[queue].append(startvalue or '') self.mini_buffer.hindex = len(self.arg_history[queue]) - 1 self.mini_active = True except 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): blist = self.bufferlist assert 0 <= self.active_slot and self.active_slot < len(blist.slots) self.active_slot = (self.active_slot + 1) % len(blist.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: slot = self.active_slot assert 0 <= slot and slot < len(self.bufferlist.slots), \ "0 <= %d < %d" % (slot, len(self.bufferlist.slots)) i = self.active_slot return self.bufferlist.slots[i].window # buffer handling def new_file_buffer(self, path, data, switch_to=True): assert not self.has_buffer_name(path), '%r is already open' % path # touch the file f = open(path, 'w') f.write(data) f.close() # create the buffer b = buffer.FileBuffer(path) try: b.open() except buffer.BinaryDataException: b = buffer.Binary32Buffer(path) b.open() b.modename = 'hex' Window(b, self, height=0, width=0) self.add_buffer(b) if switch_to: self.switch_buffer(b) return 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.data.DataBuffer(name, data) if modename is not None: b.modename = modename Window(b, self, height=0, width=0) self.add_buffer(b) if switch_to: self.switch_buffer(b) return b # NOTE: make sure that data has escaped \, [, and ] properly, or else you # will get undesirable results def color_data_buffer(self, name, data, switch_to=True, modename='colortext'): if self.has_buffer_name(name): b = self.bufferlist.buffer_names[name] self.remove_buffer(b) b = buffer.colors.ColorDataBuffer(name, data) if modename is not None: b.modename = modename 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 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.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(b, self, height=slot.height, width=slot.width) # error string handling def set_msg(self, s): self.error_string = s self.error_timestamp = time.time() def set_error(self, s): self.set_msg(s) self.log.append_lines([s, u""], act=buffer.ACT_NONE, force=True) def clear_error(self): self.error_string = u"" self.error_timestamp = None def try_manual_resize(self): y, x = self.stdscr.getmaxyx() if y != self.y or x != self.x: self.y, self.x = y, x self.resize_slots() 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 - 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) maxnum = self.config.get('max_num_kills') if maxnum and len(self.kill_ring) > maxnum: 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() # build application-centric paths def getpath(self, *parts): return os.path.join(os.getenv('HOME'), '.pmc', *parts) # create directories under the application path def mkdirs(self, *parts): path = os.path.join(self.getpath(), *parts) if not os.path.exists(path): os.makedirs(path) #for part in parts: # path = os.path.join(path, part) # if not os.path.exists(path): # os.mkdir(path) # load user configuration NOW def loadrc(self): path = os.path.join(os.getenv('HOME'), '.pmc', 'conf') if not os.path.exists(path): return try: f = open(path, 'r') exec(f) f.close() except: s = traceback.format_exc() self.rcerror = 'There was an error during startup:\n\n' + s # after actions get handled, do some stuff def post_action_hook(self, act): self.last_action = act.name if self.highlight_mark: if act.name == 'set-mark' or act.metadata.get('is_move'): pass else: self.highlight_mark = False # UTF-8 aware way to write to the screen def addstr(self, y, x, s, attr=curses.A_NORMAL): self.win.addstr(y, x, s.encode('utf-8'), attr) # the mighty run-loop! def run(self): self.done = False self.draw() if os.getenv('PMC_EARLY_OUT'): return while not self.done: i = self.win.getch() # if we get a resize event, wait for things to stabilize if i == curses.KEY_RESIZE: while i == curses.KEY_RESIZE: i = self.win.getch() self.resize_event() self.need_draw = True # add the keycodes to our input handler try: self.input.parse(i) except Exception, e: self.set_error(str(e)) # if the mode has parsed keycodes into a key, we (possibly) handle # some actions, and refresh the screen while self.input.tokens: self.need_draw = True t = self.input.tokens.pop(0) self.active_window().mode.handle_token(t) if self.need_draw: self.draw() self.need_draw = False # clean up, clean up for b in self.bufferlist.buffers: b.close() # clear the error line; it might look confusing to the user try: self.addstr(self.y-1, 0, ' ' * self.x) except: pass self.win.refresh() return # highlighting def add_highlighted_range(self, hr): self.highlighted_ranges.append(hr) def clear_highlighted_ranges(self, name=None): if name is None: self.highlighted_ranges = [] else: i = 0 while i < len(self.highlighted_ranges): if self.highlighted_ranges[i].name == name: del self.highlighted_ranges[i] else: i += 1 # running external programs def run_pipe(self, args, b, name='*Output*', switch=True, modename=None): #pipe = Popen(args=args, stdin=PIPE, stdout=PIPE, stderr=STDOUT) data = b.make_string().encode('utf-8') return self.run_pipe2(args, data, name, switch, modename) def run_pipe2(self, args, data, name='*Output*', switch=True, modename=None): pipe = Popen(args=args, stdin=PIPE, stdout=PIPE, stderr=STDOUT) try: pipe.stdin.write(data) pipe.stdin.close() except IOError: # sometimes the program will stop reading from stdin before we're # done. this is fine and we should ignore the IOError. pass output = pipe.stdout.read() status = pipe.wait() if callable(switch): switch_to = switch(status) else: switch_to = bool(switch) self.data_buffer(name, output.decode('utf-8'), switch_to=switch_to, modename=modename) return status def run_external(self, *args): curses.reset_shell_mode() try: pipe = Popen(args) pipe.wait() except OSError, e: self.set_error("%s: %s" % (args[0], e)) curses.reset_prog_mode() self.win.redrawwin() self.draw() # full screen drawer def draw(self): try: n = len(self.get_minibuffer_lines()) assert n > 0 if n != self.bufferlist.mini_height: self.bufferlist.resize_mini(n) self.draw_slots() self.draw_minibuffer() self.draw_cursor() self.win.refresh() except: raise #XYZ # ok, so there was a problem... # let's see if the screen changed sizes and if so, resize our slots self.resize_event() # clear the error message if appropriate tout = self.config.get('error_timeout', 0) tstamp = self.error_timestamp if tstamp and tout and time.time() - tstamp > tout: self.clear_error() self.try_manual_resize() # NOTE: this is taken from the cursor code def map_point(self, slot, p): w = slot.window swidth = slot.width - w.mode.lmargin - w.mode.rmargin blen = len(w.buffer.lines) count = w.mode.header x, y = w.first.xy() vy, vx = None, None while count < slot.height: line = w.buffer.lines[y] l = len(line) if p.y == y and p.x >= x and p.x <= x + swidth: vy, vx = slot.y_offset + count, p.x - x + w.mode.lmargin if vx == swidth and p.x < l: vx = 0 vy += 1 break if y >= blen or x + swidth >= l: x = 0 y += 1 else: x += swidth count += 1 return vx, vy def draw_cursor(self): if self.mini_active: b = self.mini_buffer w = b.windows[0] p = w.logical_cursor() x = p.x + len(self.mini_prompt) y = p.y if y >= len(b.lines): return lines = self.get_minibuffer_lines() while x > self.x - 1: y += 1 x -= self.x - 1 vy, vx = self.y - len(lines) + y, x 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() vx, vy = self.map_point(slot, p) if vy is None or vx is None: return try: self.win.move(vy, vx) except: raise DrawError("(%r=%r) no (%r)" % ((vx, vy), p, (self.x, self.y))) # sub-drawing methods def draw_slots(self): self.win.erase() for i in xrange(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 = chr(junk & 255) attr = color.build(fg, bg) try: #self.addstr(sy, sx, char, attr) self.win.addstr(sy, sx, char, attr) except curses.error: raise DrawError("(%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 xrange(sx1, sx2): self.highlight_char(sy, x, fg, bg) def highlight_simple_range(self, slot, y1, x1, x2, fg, bg): count = slot.window.mode.header tx1, tx2 = slot.window.mode.lmargin, slot.width - 1 x, y = slot.window.first.xy() while count < slot.height: if y1 == y and x1 < slot.width + x: sy = slot.y_offset + count sx1 = x1 - x + slot.window.mode.lmargin sx2 = x2 - x + slot.window.mode.lmargin if x1 <= x: if x2 < slot.width + x: self.highlight_chars(sy, tx1, sx2, fg, bg) break else: self.highlight_chars(sy, tx1, tx2, fg, bg) else: if x2 < slot.width + x: self.highlight_chars(sy, sx1, sx2, fg, bg) break else: self.highlight_chars(sy, sx1, tx2, fg, bg) if x + slot.width > len(slot.window.buffer.lines[y]): x = 0 y += 1 else: x += slot.width - 1 count += 1 def highlight_complex_range(self, slot, p1, p2, fg, bg): count = slot.window.mode.header tx1, tx2 = slot.window.mode.lmargin, slot.width - 1 x, y = slot.window.first.xy() while count < slot.height: if p1.y <= y: sy = slot.y_offset + count if p1.y == y: if p1.x > slot.width + x: pass elif p1.x > x: self.highlight_chars(sy, p1.x - x + tx1, tx2, fg, bg) else: self.highlight_chars(sy, tx1, tx2, fg, bg) elif p2.y > y: self.highlight_chars(sy, tx1, tx2, fg, bg) elif p2.y == y and p2.x >= x and p2.x < slot.width + x: if slot.width > p2.x - x: self.highlight_chars(sy, tx1, p2.x - x + tx1, fg, bg) break else: self.highlight_chars(sy, tx1, tx2, fg, bg) if x + slot.width > len(slot.window.buffer.lines[y]): x = 0 y += 1 else: x += slot.width - 1 count += 1 def highlight_range(self, slot, p1, p2, fg, bg): if p1.y == p2.y: return self.highlight_simple_range(slot, p1.y, p1.x, p2.x, fg, bg) else: return self.highlight_complex_range(slot, p1, p2, fg, bg) def draw_slot(self, i): assert self.active_slot < len(self.bufferlist.slots), \ "strange: %d < %d" % (self.active_slot, len(self.bufferlist.slots)) assert i < len(self.bufferlist.slots), \ "puzzling: %d < %d" % (i, len(self.bufferlist.slots)) slot = self.bufferlist.slots[i] if slot.window is None: return w = slot.window # draw the header if w.mode.header: rstrs = w.mode.get_header() assert len(rstrs) >= w.mode.header for j in xrange(0, w.mode.header): k = 0 for rstr in rstrs[j]: #rstr.draw(self.win, slot.y_offset + j, slot.x_offset + k, slot.width) rstr.draw(self.win, slot.y_offset + j, slot.x_offset + k) #k += len(rstr.string) k += rstr.width(w) # draw the actual slot self._draw_slot(i) # highlighted regions for hr in self.highlighted_ranges: (high_w, p1, p2, fg, bg) = hr if w is high_w and p2 >= w.first and p1 <= w.last: self.highlight_range(slot, p1, p2, fg, bg) if (self.active_slot == i and not self.highlighted_ranges and self.highlight_mark): fg, bg = 'black', 'cyan' cursor = w.logical_cursor() mark = w.mark if mark is None: return if mark <= cursor: p1, p2 = mark, cursor else: p1, p2 = cursor, mark if p1 < w.first: p1 = w.first if p2 > w.last: p2 = w.last self.highlight_range(slot, p1, p2, fg, bg) if w.margins_visible: shade = util.get_margin_color(w, 'blue') limit = util.get_margin_limit(w, 80) if limit < self.x: for j in xrange(0, slot.height): #char = chr(self.win.inch(j + slot.y_offset, limit) & 255) #attr = color.build('default', shade, 'bold') #self.addstr(j + slot.y_offset, limit + w.mode.lmargin, char, attr) self.highlight_char(j + slot.y_offset, limit + w.mode.lmargin, fg='default', bg=shade) def _draw_slot(self, i): slot = self.bufferlist.slots[i] w = slot.window x, y = w.first.xy() lm, rm = w.mode.lmargin, w.mode.rmargin count = w.mode.header swidth = slot.width - lm - rm lit = w.mode.name in w.buffer.highlights ended = False # figure out which "physical line" is the first to be shown. note that # the cursor shouldn't be in the last column unless it's the end of a # line. k = x // swidth while count < slot.height: if lit: rlines = w.render_line_lit(y, swidth) else: rlines = w.render_line_raw(y, swidth) for j in xrange(k, len(rlines)): y2 = slot.y_offset + count if lm: i = 0 lcont = j > 0 rstrs = w.mode.get_lmargin(w, y, x, ended, lcont) for rstr in rstrs: rstr.draw(self.win, y2, i) i += rstr.width(w) i = lm for rstr in rlines[j]: rstr.draw(self.win, y2, i) i += rstr.width(w) if rm: i = slot.width - rm rcont = j < len(rlines) - 1 rstrs = w.mode.get_rmargin(w, y, x, ended, rcont) for rstr in rstrs: rstr.draw(self.win, y2, i) i += rstr.width(w) count += 1 if count >= slot.height: break k = 0 y += 1 ended = ended or y >= len(w.buffer.lines) def draw_status_bar(self, slotname): slot = self.bufferlist.slots[slotname] if slot.window is None: return status = slot.window.mode.get_status_bar() status = status.ljust(slot.width)[:slot.width] self.addstr(slot.height + slot.y_offset, 0, status, curses.A_REVERSE) # input bar drawing def draw_minibuffer(self): lines = self.get_minibuffer_lines() attr = color.build('default', 'default') if self.error_string: attr = color.build('default', 'default') for i in xrange(0, len(lines)): line = lines[i] try: self.addstr(self.y - len(lines) + i, 0, line, attr) except: raise if self.error_string or not self.mini_buffer_is_open(): return pattr = color.build('cyan', 'default', 'bold') plines = self.get_minibuffer_x_lines(self.mini_prompt) for i in xrange(0, len(plines)): pline = plines[i] try: self.addstr(self.y - len(lines) + i, 0, pline, pattr) except: pass def get_minibuffer_x_lines(self, s): i = 0 lines = [] while i < len(s): lines.append(s[i:i + self.x - 1]) i += self.x - 1 return lines def get_minibuffer_lines(self): lines2 = [] if self.error_string: maxlen = self.config['max_error_len'] if len(self.error_string) < maxlen: s = self.error_string else: s = self.error_string[:maxlen] + '...' elif self.mini_buffer_is_open(): s = self.mini_prompt + self.mini_buffer.lines[0] lines2 = self.mini_buffer.lines[1:] else: return [' ' * (self.x - 1)] lines = self.get_minibuffer_x_lines(s) lines.extend(lines2) return lines def open_aes_file(path, name=None, binary=False): if os.path.isfile(path) or not os.path.exists(path): p = getpass("Please enter the AES password: ") return buffer.aes.AesBuffer(path, p, name) else: raise Exception, "can't open %r; unsupported file type" % path def open_plain_file(path, name=None, binary=False): if os.path.isfile(path) or not os.path.exists(path): if binary: return buffer.Binary32Buffer(path, name) else: return buffer.FileBuffer(path, name) elif os.path.isdir(path): return buffer.fs.DirBuffer(path, name) else: raise Exception, "can't open %r; unsupported file type" % path def run_app(stdscr, buffers, **kwargs): curses.def_shell_mode() a = Application(stdscr, buffers, **kwargs) metax = a.methods['meta-x'] for cmd in kwargs.get('init_cmds', []): metax.execute(a.active_window(), method=cmd) a.run() return 0 if __name__ == "__main__": ciphers = {'none': open_plain_file, 'aes': open_aes_file} locale.setlocale(locale.LC_ALL, '') # preprocess args argv = list(sys.argv[1:]) goto_line = None i = 0 while i < len(argv): if argv[i] == '-nw': del argv[i] elif argv[i].startswith('+'): goto_line = int(argv.pop(i)) else: i += 1 # set up the option parser, and set some defaults 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(pipe=False) parser.set_defaults(binary=False) parser.set_defaults(cmds=[]) def exec_cb(option, opt, value, parser): parser.values.cmds.append(value) parser.add_option('-b', '--binary', dest='binary', action='store_true', help='open file(s) in hex binary mode') parser.add_option('-p', '--pipe', dest='pipe', action='store_true', help='read data from STDIN into buffer') 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('-m', '--mode', dest='mode', metavar='MODE', help='open arguments in MODE') parser.add_option('-x', '--exec', action='callback', callback=exec_cb, type='string', metavar='CMD', help='run CMD after launching') (opts, args) = parser.parse_args(argv) # if debugging, disable error handling to produce backtraces if opts.debug: mode.DEBUG = True # override $TERM if we need to if os.getenv('PMC_TERM'): os.putenv('TERM', os.getenv('PMC_TERM')) # load an optional init file try: exec(open(os.path.join(os.getenv('HOME'), '.pmc', 'init'), 'r')) except: pass # if -b but no -m, then use -m hex if opts.binary and not opts.mode: opts.mode = 'hex' # 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 goto_line: opts.goto = goto_line # 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 = set() paths = set() # if we used a pipe to read some data, that will be our first buffer if opts.pipe: data = sys.stdin.read() # since we just read until EOF from stdin, we need to reset STDIN using # the TTY. tty = open('/dev/tty', 'r') os.dup2(tty.fileno(), sys.stdin.fileno()) # ok, so now create the actual buffer b = buffer.data.DataBuffer('*Pipe*', data) b.open() buffers.append(b) for path in args: path = os.path.abspath(os.path.realpath(util.expand_tilde(path))) if path in paths: continue # find a free name that doesn't conflict with any others name = util.make_name(os.path.basename(path), lambda x: x in names) try: b = f(path, name, opts.binary) b.open() except buffer.BinaryDataException, e: if not opts.mode: opts.mode = 'hex' b = f(path, name, True) b.open() buffers.append(b) paths.add(path) names.add(name) # save terminal state so we can restore it when the program exits attr = termios.tcgetattr(sys.stdin) keyinput.disable_control_chars() # ok, now run our app try: curses.wrapper(run_app, buffers, jump_to_line=opts.goto, init_mode=opts.mode, init_cmds=opts.cmds) err = 0 except: # restore terminal state before printing an error termios.tcsetattr(sys.stdin, termios.TCSANOW, attr) traceback.print_exc() err = 1 # restore terminal state before exiting termios.tcsetattr(sys.stdin, termios.TCSANOW, attr) sys.exit(err)