pmacs3/application.py

750 lines
26 KiB
Python
Executable File

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