pmacs3/application.py

1301 lines
46 KiB
Python
Raw Normal View History

2007-03-06 10:05:38 -05:00
#!/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
2007-03-06 10:05:38 -05:00
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
2007-03-06 10:05:38 -05:00
class PmcError(Exception): pass
class RunError(PmcError): pass
class FileError(PmcError): pass
class DrawError(PmcError): pass
2007-06-13 22:18:41 -04:00
class Application(object):
def __init__(self, stdscr, buffers=[], **kwargs):
2007-03-06 10:05:38 -05:00
# initalize curses primitives
self.stdscr = stdscr
2008-11-08 10:30:04 -05:00
self.y, self.x = self.stdscr.getmaxyx()
2007-03-06 10:05:38 -05:00
# we have to do this early so we can log errors
self.log = buffer.LogBuffer()
2007-03-06 10:05:38 -05:00
# initialize some basic stuff
self.config = {}
2007-03-06 10:05:38 -05:00
self.highlighted_ranges = []
self.highlight_mark = False
2007-07-03 12:53:14 -04:00
self.mini_active = False
self.mini_buffer = None
self.mini_prompt = ""
self.error_string = ""
self.error_timestamp = None
2008-11-08 10:30:04 -05:00
self.need_draw = True
2007-07-03 12:53:14 -04:00
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')
2008-11-08 10:30:04 -05:00
2008-03-16 02:16:41 -04:00
# let's prepopulate some default token colors
2009-04-05 13:05:42 -04:00
self.cached_colors = {}
2008-03-16 02:16:41 -04:00
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,
2008-03-16 02:16:41 -04:00
}
self.default_color = ('default', 'default',)
2007-03-06 10:05:38 -05:00
2008-11-08 12:44:59 -05:00
# initialize configuration defaults
self._load_config_defaults()
2007-03-06 10:05:38 -05:00
# initialize our colors
if curses.has_colors():
curses.start_color()
try:
curses.use_default_colors()
color.default_color = True
except:
color.default_color = False
2009-01-15 19:07:53 -05:00
color.init()
2007-03-06 10:05:38 -05:00
# initialize our modes
2007-10-18 17:07:35 -04:00
# 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 = {}
2007-03-06 10:05:38 -05:00
# initialize our methods
self.methods = {}
2008-11-12 10:52:01 -05:00
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',
2008-11-12 10:52:01 -05:00
)
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()
2007-03-06 10:05:38 -05:00
# ok, now let's load all the "standard" modes
mode.install(self)
2009-07-24 00:15:36 -04:00
names = (
'blame', 'c', 'console', 'consolemini', 'css', 'diff', 'dir',
'elisp', 'hex', 'html', 'java', 'javap', 'javascript', 'lisp', 'make',
2009-07-24 00:15:36 -04:00
'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', 'twine', 'idris',
)
for name in names:
exec("import mode.%s; mode.%s.install(self)" % (name, name))
2007-03-06 10:05:38 -05:00
# create all the insert methods for the character ranges we like
for c in string.ascii_letters + string.digits + string.punctuation:
2008-11-08 19:06:22 -05:00
obj = method.InsertString(c)
self.methods[obj.name] = obj
obj = method.OverwriteChar(c)
self.methods[obj.name] = obj
2007-03-06 10:05:38 -05:00
# buffer list stuff
2008-11-08 13:32:32 -05:00
height = self.y - 1
width = self.x
self.bufferlist = BufferList(height, width)
self.active_slot = 0
self.complete_slot = None
2007-10-18 11:28:55 -04:00
# run user custom code here
self.rcerror = None
2007-10-18 11:28:55 -04:00
self.loadrc()
2007-03-06 10:05:38 -05:00
# initialize our buffers
2008-10-29 13:17:16 -04:00
# 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))
2007-03-06 10:05:38 -05:00
# build windows for our buffers
for b in buffers_:
2008-10-29 13:17:16 -04:00
if b.modename:
Window(b, self)
2007-06-24 09:51:43 -04:00
else:
Window(b, self, mode_name=kwargs.get('init_mode'))
2007-03-06 10:05:38 -05:00
self.bufferlist.add_buffer(b)
self.bufferlist.set_slot(self.active_slot, buffers_[0])
2007-03-06 10:05:38 -05:00
# 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:
2007-06-04 23:05:33 -04:00
w = self.bufferlist.slots[0].window
2008-03-18 11:03:11 -04:00
self.methods['goto-line'].execute(w, lineno=jump_to_line)
2007-03-06 10:05:38 -05:00
# initialize our kill ring and last action
2007-07-02 20:29:27 -04:00
self.kill_ring = []
self.kill_commands = ['kill', 'kill-region']
self.last_action = None
self.last_search = None
2007-03-06 10:05:38 -05:00
self.last_replace_before = None
2007-07-02 20:29:27 -04:00
self.last_replace_after = None
self.registers = {}
self.arg_history = {'default': []}
2007-03-06 10:05:38 -05:00
# 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': {},
}
2007-03-06 10:05:38 -05:00
# 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))
2007-03-06 10:05:38 -05:00
# set up curses
self.win = curses.newwin(self.y, self.x, 0, 0)
self.win.leaveok(0)
2007-03-06 10:05:38 -05:00
curses.meta(1)
curses.cbreak()
2007-08-12 18:04:00 -04:00
curses.noecho()
curses.nonl()
curses.halfdelay(1)
curses.def_prog_mode()
2007-03-06 10:05:38 -05:00
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']
2008-12-05 00:41:02 -05:00
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
2008-12-05 00:41:02 -05:00
self.config['default_color'] = ('default', 'default',)
self.config['margin'] = 79
2008-12-05 00:41:02 -05:00
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
2008-05-05 09:21:00 -04:00
lines = []
2008-05-05 13:47:12 -04:00
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.
2008-05-30 14:13:26 -04:00
numcols = max(self.bufferlist.slots[n].width // (maxlen + 2), 1)
2008-05-05 13:47:12 -04:00
numrows = clen - ((clen // numcols) * (numcols - 1))
for i in xrange(0, numrows):
2008-05-05 13:47:12 -04:00
names = []
index = i * numcols
for j in xrange(0, numcols):
2008-05-05 13:47:12 -04:00
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
2008-11-08 12:44:59 -05:00
def setmode(self, name, cls, paths=[], bases=[], exts=[], detection=[]):
2007-10-18 12:05:21 -04:00
self.modes[name] = cls
2009-04-09 01:01:58 -04:00
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
2007-03-06 10:05:38 -05:00
2009-04-09 01:01:58 -04:00
# we are evil
def eval(self, s): return eval(s)
def globals(self): return globals()
def locals(self): return locals()
# slots
2007-03-06 10:05:38 -05:00
def add_slot(self):
2007-06-04 23:05:33 -04:00
b = self.bufferlist.slots[self.active_slot].window.buffer
2007-06-04 03:29:37 -04:00
n = self.bufferlist.add_slot()
self.bufferlist.set_slot(n, b)
def remove_slot(self, n):
2008-12-05 00:41:02 -05:00
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))
2007-06-04 03:29:37 -04:00
self.bufferlist.remove_slot(n)
if self.active_slot > n:
self.active_slot = max(0, self.active_slot - 1)
2007-03-06 10:05:38 -05:00
def single_slot(self):
while len(self.bufferlist.slots) > 1:
if self.active_slot == 0:
self.remove_slot(1)
else:
self.remove_slot(0)
2007-06-04 03:29:37 -04:00
def get_window_height_width(self, i):
2008-12-05 00:41:02 -05:00
slots = self.bufferlist.slots
assert i >= 0 and i < len(slots), "invalid slot: %d" % i
2008-12-05 00:41:02 -05:00
slot = slots[i]
2007-03-06 10:05:38 -05:00
return (slot.height, slot.width)
# files and stuff
def close_buffer(self, b):
2008-11-08 12:44:59 -05:00
blist = self.bufferlist
blist.remove_buffer(b)
b.close()
2008-11-08 12:44:59 -05:00
active_slot = blist.slots[self.active_slot]
for i in xrange(0, len(blist.slots)):
2008-11-08 12:44:59 -05:00
if blist.slots[i].is_empty():
if blist.hidden_buffers:
blist.set_slot(i, blist.hidden_buffers[0])
2009-03-25 21:28:38 -04:00
elif active_slot.window:
2008-11-08 12:44:59 -05:00
blist.set_slot(i, active_slot.window.buffer)
2009-03-25 21:28:38 -04:00
else:
blist.set_slot(i, None)
2009-01-28 16:28:33 -05:00
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):
2008-10-29 10:58:06 -04:00
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()
2010-09-30 18:18:57 -04:00
#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
2007-03-06 10:05:38 -05:00
# 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
2008-11-08 12:44:59 -05:00
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
2007-03-06 10:05:38 -05:00
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)
2008-05-30 14:13:26 -04:00
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 = ''
2007-03-06 10:05:38 -05:00
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
2007-03-06 10:05:38 -05:00
if self.mini_buffer_is_open():
self.mini_buffer.close()
self.mini_buffer = None
self.mini_prompt = ""
assert not self.mini_active
2007-03-06 10:05:38 -05:00
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):
2008-11-08 12:44:59 -05:00
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)
2007-03-06 10:05:38 -05:00
def window(self):
2007-06-04 23:05:33 -04:00
return self.bufferlist.slots[self.active_slot].window
2007-03-06 10:05:38 -05:00
def active_window(self):
if self.mini_active:
2007-06-04 23:05:33 -04:00
return self.mini_buffer.windows[0]
2007-03-06 10:05:38 -05:00
else:
2008-11-08 12:44:59 -05:00
slot = self.active_slot
assert 0 <= slot and slot < len(self.bufferlist.slots), \
"0 <= %d < %d" % (slot, len(self.bufferlist.slots))
2007-06-04 03:29:37 -04:00
i = self.active_slot
return self.bufferlist.slots[i].window
2007-03-06 10:05:38 -05:00
# buffer handling
2008-11-08 12:44:59 -05:00
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
2007-03-06 10:05:38 -05:00
f = open(path, 'w')
f.write(data)
f.close()
2008-11-08 12:44:59 -05:00
# 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)
2007-03-06 10:05:38 -05:00
self.add_buffer(b)
if switch_to:
self.switch_buffer(b)
return b
2007-03-06 10:05:38 -05:00
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)
2008-10-29 10:58:06 -04:00
b = buffer.data.DataBuffer(name, data)
2007-03-06 10:05:38 -05:00
if modename is not None:
b.modename = modename
Window(b, self, height=0, width=0)
2007-03-06 10:05:38 -05:00
self.add_buffer(b)
if switch_to:
self.switch_buffer(b)
return b
2009-03-16 14:22:23 -04:00
# 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)
2007-03-06 10:05:38 -05:00
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)
2007-03-06 10:05:38 -05:00
# error string handling
def set_msg(self, s):
2007-03-06 10:05:38 -05:00
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)
2007-03-06 10:05:38 -05:00
def clear_error(self):
self.error_string = u""
2007-03-06 10:05:38 -05:00
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()
2007-03-06 10:05:38 -05:00
def resize_event(self):
2007-07-28 23:08:42 -04:00
(self.y, self.x) = self.stdscr.getmaxyx()
2007-03-06 10:05:38 -05:00
self.resize_slots()
def resize_slots(self):
n = len(self.bufferlist.slots)
2007-06-04 10:06:04 -04:00
assert n > 0
2007-03-06 10:05:38 -05:00
x = self.x - 1
y_sum = self.y - n
2007-07-28 23:08:42 -04:00
self.bufferlist.resize(y_sum, x)
2007-03-06 10:05:38 -05:00
# 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):
2007-03-06 10:05:38 -05:00
self.kill_ring[-1] = self.kill_ring[-1] + s
else:
self.kill_ring.append(s)
maxnum = self.config.get('max_num_kills')
2008-11-08 12:44:59 -05:00
if maxnum and len(self.kill_ring) > maxnum:
2007-03-06 10:05:38 -05:00
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:
2007-06-04 03:29:37 -04:00
self.window().undo()
2007-03-06 10:05:38 -05:00
except Exception, e:
self.set_error("%s" % (e))
def redo(self):
try:
2007-06-04 03:29:37 -04:00
self.window().redo()
2007-03-06 10:05:38 -05:00
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)
2007-10-18 11:28:55 -04:00
# 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!
2007-03-06 10:05:38 -05:00
def run(self):
self.done = False
self.draw()
if os.getenv('PMC_EARLY_OUT'):
return
2007-03-06 10:05:38 -05:00
while not self.done:
i = self.win.getch()
2008-05-29 16:35:21 -04:00
2009-04-09 01:01:58 -04:00
# if we get a resize event, wait for things to stabilize
2007-03-06 10:05:38 -05:00
if i == curses.KEY_RESIZE:
2009-04-09 01:01:58 -04:00
while i == curses.KEY_RESIZE: i = self.win.getch()
2007-03-06 10:05:38 -05:00
self.resize_event()
2008-11-08 10:30:04 -05:00
self.need_draw = True
2009-04-09 01:01:58 -04:00
# add the keycodes to our input handler
2007-03-06 10:05:38 -05:00
try:
self.input.parse(i)
except Exception, e:
self.set_error(str(e))
2008-11-08 10:30:04 -05:00
2009-04-09 01:01:58 -04:00
# if the mode has parsed keycodes into a key, we (possibly) handle
# some actions, and refresh the screen
2008-11-08 10:30:04 -05:00
while self.input.tokens:
2009-04-09 01:01:58 -04:00
self.need_draw = True
2007-03-06 10:05:38 -05:00
t = self.input.tokens.pop(0)
self.active_window().mode.handle_token(t)
2008-05-29 16:35:21 -04:00
2008-11-08 10:30:04 -05:00
if self.need_draw:
self.draw()
2008-11-08 10:30:04 -05:00
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()
2007-03-06 10:05:38 -05:00
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
2007-03-06 10:05:38 -05:00
# running external programs
2009-03-16 10:42:17 -04:00
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()
2007-03-06 10:05:38 -05:00
# full screen drawer
def draw(self):
2007-07-28 23:08:42 -04:00
try:
n = len(self.get_minibuffer_lines())
assert n > 0
if n != self.bufferlist.mini_height:
self.bufferlist.resize_mini(n)
2007-09-26 11:56:07 -04:00
self.draw_slots()
self.draw_minibuffer()
2007-07-28 23:08:42 -04:00
self.draw_cursor()
2008-04-06 22:31:13 -04:00
self.win.refresh()
except:
raise #XYZ
2007-07-28 23:08:42 -04:00
# 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()
2007-03-06 10:05:38 -05:00
# 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
2009-04-09 01:01:58 -04:00
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:
2009-04-09 01:01:58 -04:00
x = 0
y += 1
else:
x += swidth
2009-04-09 01:01:58 -04:00
count += 1
return vx, vy
2009-04-09 01:01:58 -04:00
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)))
2007-03-06 10:05:38 -05:00
# sub-drawing methods
def draw_slots(self):
self.win.erase()
for i in xrange(0, len(self.bufferlist.slots)):
#slot = self.bufferlist.slots[i]
2007-03-06 10:05:38 -05:00
self.draw_slot(i)
self.draw_status_bar(i)
2007-07-28 23:08:42 -04:00
def highlight_char(self, sy, sx, fg='default', bg='default'):
2007-06-14 06:10:20 -04:00
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)
2007-06-14 06:10:20 -04:00
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)
2007-06-04 03:29:37 -04:00
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))
2007-06-04 03:29:37 -04:00
slot = self.bufferlist.slots[i]
if slot.window is None:
2007-03-06 10:05:38 -05:00
return
2007-06-04 03:29:37 -04:00
w = slot.window
2009-02-02 09:44:32 -05:00
# draw the header
2009-02-05 10:47:20 -05:00
if w.mode.header:
rstrs = w.mode.get_header()
assert len(rstrs) >= w.mode.header
for j in xrange(0, w.mode.header):
2009-03-06 11:39:47 -05:00
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)
2009-01-28 16:28:33 -05:00
2009-02-02 09:44:32 -05:00
# draw the actual slot
self._draw_slot(i)
2007-03-06 10:05:38 -05:00
2007-06-14 06:10:20 -04:00
# highlighted regions
for hr in self.highlighted_ranges:
(high_w, p1, p2, fg, bg) = hr
2007-07-03 12:53:14 -04:00
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)
2007-06-14 06:10:20 -04:00
2007-07-03 12:53:14 -04:00
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
2009-02-02 09:44:32 -05:00
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)
2007-03-06 10:05:38 -05:00
def draw_status_bar(self, slotname):
slot = self.bufferlist.slots[slotname]
2007-06-04 03:29:37 -04:00
if slot.window is None:
2007-03-06 10:05:38 -05:00
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)
2007-03-06 10:05:38 -05:00
# 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]
2008-04-23 17:21:50 -04:00
try:
self.addstr(self.y - len(lines) + i, 0, line, attr)
2008-04-23 17:21:50 -04:00
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
2007-09-26 11:56:07 -04:00
def get_minibuffer_lines(self):
lines2 = []
if self.error_string:
2008-11-08 12:44:59 -05:00
maxlen = self.config['max_error_len']
if len(self.error_string) < maxlen:
s = self.error_string
else:
2008-11-08 12:44:59 -05:00
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
2007-03-06 10:05:38 -05:00
def open_aes_file(path, name=None, binary=False):
2007-07-20 10:51:36 -04:00
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)
2007-07-19 14:37:39 -04:00
else:
raise Exception, "can't open %r; unsupported file type" % path
def open_plain_file(path, name=None, binary=False):
2007-07-20 10:51:36 -04:00
if os.path.isfile(path) or not os.path.exists(path):
if binary:
return buffer.Binary32Buffer(path, name)
else:
return buffer.FileBuffer(path, name)
2007-07-19 14:37:39 -04:00
elif os.path.isdir(path):
2008-10-29 10:58:06 -04:00
return buffer.fs.DirBuffer(path, name)
2007-07-19 14:37:39 -04:00
else:
raise Exception, "can't open %r; unsupported file type" % path
2007-03-06 10:05:38 -05:00
def run_app(stdscr, buffers, **kwargs):
2008-11-08 10:30:04 -05:00
curses.def_shell_mode()
a = Application(stdscr, buffers, **kwargs)
2009-03-05 03:08:33 -05:00
metax = a.methods['meta-x']
for cmd in kwargs.get('init_cmds', []):
metax.execute(a.active_window(), method=cmd)
2008-11-08 10:30:04 -05:00
a.run()
return 0
2008-11-08 10:30:04 -05:00
2007-03-06 10:05:38 -05:00
if __name__ == "__main__":
ciphers = {'none': open_plain_file,
'aes': open_aes_file}
2007-03-06 10:05:38 -05:00
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
2007-03-06 10:05:38 -05:00
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)
2007-03-06 10:05:38 -05:00
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')
2007-03-06 10:05:38 -05:00
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')
2007-03-06 10:05:38 -05:00
(opts, args) = parser.parse_args(argv)
2007-03-06 10:05:38 -05:00
# if debugging, disable error handling to produce backtraces
if opts.debug:
2007-10-21 20:55:29 -04:00
mode.DEBUG = True
2007-03-06 10:05:38 -05:00
# 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'
2007-03-06 10:05:38 -05:00
# 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
2007-06-04 23:05:33 -04:00
2007-03-06 10:05:38 -05:00
# 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()
2008-04-06 22:31:13 -04:00
# 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)
2007-03-06 10:05:38 -05:00
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()
2007-03-06 10:05:38 -05:00
buffers.append(b)
paths.add(path)
names.add(name)
2008-11-08 10:30:04 -05:00
# save terminal state so we can restore it when the program exits
attr = termios.tcgetattr(sys.stdin)
keyinput.disable_control_chars()
2007-03-06 10:05:38 -05:00
# ok, now run our app
2008-11-08 10:30:04 -05:00
try:
2009-10-01 20:06:17 -04:00
curses.wrapper(run_app, buffers, jump_to_line=opts.goto,
init_mode=opts.mode, init_cmds=opts.cmds)
err = 0
2008-11-08 10:30:04 -05:00
except:
# restore terminal state before printing an error
termios.tcsetattr(sys.stdin, termios.TCSANOW, attr)
2008-11-08 10:30:04 -05:00
traceback.print_exc()
err = 1
2008-11-08 10:30:04 -05:00
# restore terminal state before exiting
2008-11-08 10:30:04 -05:00
termios.tcsetattr(sys.stdin, termios.TCSANOW, attr)
sys.exit(err)