mirror of
https://github.com/pkoning2/decstuff.git
synced 2026-01-22 02:25:20 +00:00
3401 lines
113 KiB
Python
Executable File
3401 lines
113 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
"""TECO for Python
|
|
|
|
Copyright (C) 2006, 2014 by Paul Koning
|
|
|
|
This is an implementation of DEC Standard TECO in Python.
|
|
It corresponds to PDP-11/VAX TECO V40, give or take a few details
|
|
that don't really carry over.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import re
|
|
import time
|
|
import traceback
|
|
import glob
|
|
import warnings
|
|
import copy
|
|
import atexit
|
|
import threading
|
|
import tempfile
|
|
|
|
try:
|
|
import curses
|
|
curses.initscr ()
|
|
curses.endwin ()
|
|
curses.def_shell_mode ()
|
|
cursespresent = True
|
|
except ImportError:
|
|
cursespresent = False
|
|
|
|
try:
|
|
import wx
|
|
wxpresent = True
|
|
except ImportError:
|
|
wxpresent = False
|
|
|
|
# char codes
|
|
null = '\000'
|
|
ctrlc = '\003'
|
|
ctrle = '\005'
|
|
bs = '\010'
|
|
bell = '\007'
|
|
tab = '\011'
|
|
lf = '\012'
|
|
vt = '\013'
|
|
ff = '\014'
|
|
cr = '\015'
|
|
crlf = cr + lf
|
|
ctrls = '\023'
|
|
ctrlu = '\025'
|
|
ctrlx = '\030'
|
|
esc = '\033'
|
|
rub = '\177'
|
|
eol = lf + vt + ff # all these are TECO end of line (TODO)
|
|
|
|
# global variables
|
|
screen = None
|
|
display = None
|
|
dsem = None
|
|
exiting = False
|
|
|
|
# Other useful constants
|
|
VERSION = 40
|
|
|
|
# There is no good match for the CPU and OS types. Based on the
|
|
# features, PDP-11 makes sense, since this TECO looks like TECO-11.
|
|
# The implementation of ^B says we'd like to call it RSX/VMS, but
|
|
# that won't work because various code (like TECO.TEC) will use that
|
|
# as a hint to construct filenames with RMS format switches in them,
|
|
# and those look like Unix directory names, so all hell will break
|
|
# loose. The best answer, therefore, is to call it RT-11, which
|
|
# has neither directories nor file version numbers nor filename
|
|
# switches. RSX/VMS has all of those, and RSTS/E has all except
|
|
# versions -- but any of those would give Unix filename handlers
|
|
# conniption fits...
|
|
CPU = 0 # Pretend to be a PDP-11, that's closest
|
|
OS = 7 # OS is Unix, but pretend it's RT-11
|
|
|
|
# unbuffered input: Python Cookbook V2 section 2.23
|
|
try:
|
|
from msvcrt import getch
|
|
rubchr = '\010'
|
|
except ImportError:
|
|
rubchr = '\177'
|
|
def getch ():
|
|
"""Get a character in raw (unechoed, single character) mode.
|
|
"""
|
|
import tty, termios
|
|
fd = sys.stdin.fileno ()
|
|
old_settings = termios.tcgetattr (fd)
|
|
try:
|
|
tty.setraw (fd)
|
|
ch = sys.stdin.read (1)
|
|
finally:
|
|
termios.tcsetattr (fd, termios.TCSADRAIN, old_settings)
|
|
return ch
|
|
|
|
# Enhanced traceback, from Python Cookbook section 8.6, slightly tweaked
|
|
maxstrlen = 200
|
|
def print_exc_plus ():
|
|
'''Print all the usual traceback information, followed by a listing of
|
|
all the local variables in each frame.
|
|
|
|
Variable values are truncated to 200 characters max for readability,
|
|
and converted to printable characters in standard TECO fashion.
|
|
'''
|
|
tb = sys.exc_info ()[2]
|
|
while tb.tb_next:
|
|
tb = tb.tb_next
|
|
stack = [ ]
|
|
f = tb.tb_frame
|
|
while f:
|
|
stack.append (f)
|
|
f = f.f_back
|
|
stack.reverse ()
|
|
endwin ()
|
|
traceback.print_exc ()
|
|
print("Locals by frame, innermost last")
|
|
if stack[0].f_code.co_name == "?":
|
|
del stack[0]
|
|
for frame in stack:
|
|
print()
|
|
print("Frame %s in %s at line %s" % (frame.f_code.co_name,
|
|
frame.f_code.co_filename,
|
|
frame.f_lineno))
|
|
for key, value in list(frame.f_locals.items ()):
|
|
print("\t%20s = " % key, end=' ')
|
|
try:
|
|
value = printable (str (value))
|
|
if len (value) > maxstrlen:
|
|
value = value[:maxstrlen] + "..."
|
|
print(value)
|
|
except:
|
|
print("<ERROR while printing value>")
|
|
|
|
# Transform a generic binary string to a printable one. Try to
|
|
# optimize this because sometimes it is fed big strings.
|
|
# tab through cr are printed as is; other control chars are uparrowed
|
|
# except for esc of course.
|
|
# Taken from Python Cookbook, section 1.18
|
|
_printre = re.compile ("[\000-\007\016-\037\177]")
|
|
_printdict = { }
|
|
for c in range (0o40):
|
|
_printdict[chr (c)] = '^' + chr (c + 64)
|
|
_printdict[esc] = '$'
|
|
_printdict[rub] = "^?"
|
|
|
|
def _makeprintable (m):
|
|
return _printdict[m.group (0)]
|
|
|
|
def printable(s):
|
|
"""Convert the supplied string to a printable string,
|
|
by converting all unusual control characters to uparrow
|
|
form, and escape to $ sign.
|
|
"""
|
|
return _printre.sub (_makeprintable, s)
|
|
|
|
# Error handling
|
|
class err (Exception):
|
|
'''Base class for TECO errors.
|
|
|
|
Specific errors are derived from this class; the class name is
|
|
the three-letter code for the error, and the class doc string
|
|
is the event message string.
|
|
'''
|
|
def __init__ (self, teco, *a):
|
|
self.teco = teco
|
|
self.args = tuple (printable (arg) for arg in a)
|
|
teco.clearargs ()
|
|
|
|
def show (self):
|
|
endwin ()
|
|
detail = self.teco.eh & 3
|
|
if detail == 1:
|
|
print("?%s" % self.__class__.__name__)
|
|
else:
|
|
if self.args:
|
|
msg = self.__class__.__doc__ % self.args
|
|
else:
|
|
msg = self.__class__.__doc__
|
|
print("?%s %s" % (self.__class__.__name__, msg))
|
|
if self.teco.eh & 4:
|
|
print(printable (self.teco.failedcommand ()), "?")
|
|
|
|
class ARG (err): 'Improper Arguments'
|
|
class BNI (err): '> not in iteration'
|
|
class FER (err): 'File Error'
|
|
class FNF (err): 'File not found "%s"'
|
|
class ICE (err): 'Illegal ^E Command in Search Argument'
|
|
class IEC (err): 'Illegal character "%s" after E'
|
|
class IFC (err): 'Illegal character "%s" after F'
|
|
class IFN (err): 'Illegal character "%s" in filename'
|
|
class IIA (err): 'Illegal insert arg'
|
|
class ILL (err): 'Illegal command "%s"'
|
|
class ILN (err): 'Illegal number'
|
|
class INP (err): 'Input error'
|
|
class IPA (err): 'Negative or 0 argument to P'
|
|
class IQC (err): 'Illegal " character'
|
|
class IQN (err): 'Illegal Q-register name "%s"'
|
|
class IRA (err): 'Illegal radix argument to ^R'
|
|
class ISA (err): 'Illegal search arg'
|
|
class ISS (err): 'Illegal search string'
|
|
class IUC (err): 'Illegal character "%s" following ^'
|
|
class MAP (err): "Missing '"
|
|
class MLA (err): 'Missing Left Angle Bracket'
|
|
class MLP (err): 'Missing ('
|
|
class MRA (err): 'Missing Right Angle Bracket'
|
|
class MRP (err): 'Missing )'
|
|
class NAB (err): 'No arg before ^_'
|
|
class NAC (err): 'No arg before ,'
|
|
class NAE (err): 'No arg before ='
|
|
class NAP (err): 'No arg before )'
|
|
class NAQ (err): 'No arg before "'
|
|
class NAS (err): 'No arg before ;'
|
|
class NAU (err): 'No arg before U'
|
|
class NCA (err): 'Negative argument to ,'
|
|
class NFI (err): 'No file for input'
|
|
class NFO (err): 'No file for output'
|
|
class NPA (err): 'Negative or 0 argument to P'
|
|
class NYA (err): 'Numeric argument with Y'
|
|
class NYI (err): 'Not Yet Implemented'
|
|
class OFO (err): 'Output file already open'
|
|
class OUT (err): 'Output error'
|
|
class PES (err): 'Attempt to Pop Empty Stack'
|
|
class POP (err): 'Attempt to move Pointer Off Page with "%s"'
|
|
class SNI (err): '; not in iteration'
|
|
class SRH (err): 'Search failure "%s"'
|
|
class TAG (err): 'Missing Tag !%s!'
|
|
class UTC (err): 'Unterminated command "%s"'
|
|
class UTM (err): 'Unterminated macro'
|
|
class XAB (err): 'Execution aborted'
|
|
class YCA (err): 'Y command aborted'
|
|
|
|
# Other exceptions used for misc purposes
|
|
class ExitLevel (Exception): pass
|
|
class ExitExecution (Exception): pass
|
|
|
|
# text reformatting for screen display
|
|
tabre = re.compile (tab)
|
|
_tabadjust = 0
|
|
def _untabify (m):
|
|
global _curcol, _tabadjust
|
|
pos = m.start () + _tabadjust
|
|
count = 8 - (pos & 7)
|
|
if _curcol > pos:
|
|
_curcol += count - 1
|
|
_tabadjust += count - 1
|
|
return " " * count
|
|
|
|
def untabify (line, curpos, width):
|
|
"""Convert tabs to spaces, and wrap the line as needed into chunks
|
|
of the specified width. Returns the list of chunks, and the row
|
|
and column corresponding to the supplied curpos. There is always
|
|
at least one chunk, which may have an empty string if the supplied
|
|
line was empty.
|
|
|
|
Each chunk is a pair of text and wrap flag. Wrap flag is True
|
|
if this chunk of text is wrapped, i.e., it does not end with a
|
|
carriage return, and False if it is the end of the line.
|
|
|
|
Note that a trailing cr and/or lf is stripped from the input line.
|
|
"""
|
|
global _curcol, _tabadjust
|
|
_curcol = curpos
|
|
_tabadjust = 0
|
|
line = tabre.sub (_untabify, printable (line.rstrip (crlf)))
|
|
currow = 0
|
|
if True: # todo: truncate vs. wrap mode
|
|
lines = [ ]
|
|
while len (line) > width:
|
|
lines.append ((line[:width], True))
|
|
line = line[width:]
|
|
if _curcol > width:
|
|
currow += 1
|
|
_curcol -= width
|
|
lines.append ((line, False))
|
|
return lines, currow, _curcol
|
|
|
|
# Property makers
|
|
def commonprop (name, doc=None):
|
|
"""Define a property that references an attribute of 'teco'
|
|
(the common state object for TECO).
|
|
"""
|
|
def _fget (obj):
|
|
return getattr (obj.teco, name)
|
|
def _fset (obj, val):
|
|
return setattr (obj.teco, name, val)
|
|
_fget.__name__ = "get_%s" % name
|
|
_fset.__name__ = "set_%s" % name
|
|
return property (_fget, _fset, doc=doc)
|
|
|
|
def bufprop (name, doc=None):
|
|
"""Define a property that references an attribute of 'buffer'
|
|
(the text buffer object for TECO).
|
|
"""
|
|
def _fget (obj):
|
|
return getattr (obj.buffer, name)
|
|
def _fset (obj, val):
|
|
return setattr (obj.buffer, name, val)
|
|
_fget.__name__ = "get_%s" % name
|
|
_fset.__name__ = "set_%s" % name
|
|
return property (_fget, _fset, doc=doc)
|
|
|
|
# Global state handling
|
|
class teco (object):
|
|
'''This class defines the global state for TECO, i.e., the state
|
|
that is independent of the macro level. It also points to state
|
|
that is kept in separate classes, such as the buffer, the command
|
|
input, and the command level for the interactive level.
|
|
'''
|
|
def __init__(self):
|
|
self.radix = 10
|
|
self.ed = 0
|
|
self.eh = 0
|
|
self.es = 0
|
|
if wxpresent:
|
|
self.etfixed = 1024 # Display available
|
|
elif cursespresent:
|
|
self.etfixed = 512 # text terminal "watch" available
|
|
else:
|
|
self.etfixed = 0 # neither available
|
|
self.et = 128 + self.etfixed
|
|
self.eu = -1
|
|
self.ev = 0
|
|
setattr (self, ctrlx, 0) # ^X flag
|
|
self.trace = False
|
|
self.laststringlen = 0
|
|
self.qregs = { }
|
|
self.lastsearch = ""
|
|
self.lastfilename = ""
|
|
self.qstack = [ ]
|
|
self.clearargs ()
|
|
self.interactive = command_level (self)
|
|
self.buffer = buffer (self)
|
|
self.cmdhandler = command_handler (self)
|
|
self.screenok = False
|
|
self.incurses = False
|
|
self.watchparams = [ 8, 80, 24, 0, 0, 0, 0, 0 ]
|
|
self.curline = 16
|
|
|
|
buf = bufprop ("text")
|
|
dot = bufprop ("dot")
|
|
end = bufprop ("end")
|
|
|
|
def doop (self, c):
|
|
"""Process a pending arithmetic operation, if any.
|
|
self.arg is left with the current term value.
|
|
"""
|
|
if self.op:
|
|
if self.op in '+-' and self.arg is None:
|
|
self.arg = 0
|
|
if self.num is None:
|
|
raise ILL (self, c)
|
|
if self.op == '+':
|
|
self.arg += self.num
|
|
elif self.op == '-':
|
|
self.arg -= self.num
|
|
elif self.op == '*':
|
|
self.arg *= self.num
|
|
elif self.op == '/':
|
|
if not self.num:
|
|
raise ILL (self, '/')
|
|
self.arg //= self.num
|
|
elif self.op == '&':
|
|
self.arg &= self.num
|
|
elif self.op == '#':
|
|
self.arg |= self.num
|
|
else:
|
|
self.arg = self.num
|
|
|
|
def getterm (self, c):
|
|
"""Get the current term, i.e., the innermost expression
|
|
part in parentheses.
|
|
"""
|
|
self.doop (c)
|
|
ret = self.arg
|
|
self.num = None
|
|
self.op = None
|
|
self.arg = None
|
|
return ret
|
|
|
|
def leftparen (self):
|
|
"""Proces a left parenthesis command, by pushing the current
|
|
partial expression state onto the operation stack.
|
|
"""
|
|
self.opstack.append ((self.arg, self.op))
|
|
self.arg = None
|
|
self.op = None
|
|
|
|
def rightparen (self):
|
|
"""Process a right parenthesis command, by setting the current
|
|
right-hand side to the expression term value, and popping the
|
|
operation stack state for the left hand and operation (if any).
|
|
"""
|
|
if self.opstack:
|
|
try:
|
|
self.num = self.getterm (')')
|
|
except err:
|
|
raise NAP (self)
|
|
self.arg, self.op = self.opstack.pop ()
|
|
else:
|
|
raise MLP (self)
|
|
|
|
def operator (self, c):
|
|
"""Process an arithmetic operation character.
|
|
"""
|
|
if self.num is None:
|
|
if c in "+-" and self.arg is None:
|
|
self.op = c
|
|
return
|
|
else:
|
|
raise ILL (self, c)
|
|
self.doop (c)
|
|
self.num = None
|
|
self.op = c
|
|
|
|
def digit (self, c):
|
|
"""Process a decimal digit. 8 and 9 generate an error if
|
|
the current radix is octal.
|
|
"""
|
|
if self.num is None:
|
|
self.num = 0
|
|
n = int (c)
|
|
if n >= self.radix:
|
|
raise ILN (self)
|
|
self.num = self.num * self.radix + n
|
|
|
|
def getarg (self, c):
|
|
"""Get a complete argument, or None if there isn't one.
|
|
If there are left parentheses that have not yet been matched,
|
|
that is an error. + or - alone are taken to be +1 and -1
|
|
respectively.
|
|
"""
|
|
if self.opstack:
|
|
raise MRP (self)
|
|
if self.op and self.op in '+-' and self.num is None and self.arg is None:
|
|
self.num = 1
|
|
return self.getterm (c)
|
|
|
|
def getoptarg (self, c):
|
|
"""Get an optional argument. Unlike getarg, this is legal
|
|
while we're in an incomplete expression. This method is used
|
|
for commands that do something if given an argument, but return
|
|
a value if not. This way, the second case can be used as
|
|
an element of an expression.
|
|
"""
|
|
if self.op and self.op in '+-' and self.num is None and self.arg is None:
|
|
self.num = 1
|
|
if self.num is None:
|
|
return None
|
|
return self.getarg (c)
|
|
|
|
def setval (self, val):
|
|
"""Set the command result value into the expression state.
|
|
"""
|
|
self.num = val
|
|
self.clearmods ()
|
|
|
|
def bitflag (self, name):
|
|
"""Process a bit-flag type command, i.e., one that is read by
|
|
supplying no argument, set by supplying one, and has its bits
|
|
fiddled by two arguments.
|
|
"""
|
|
n = self.getoptarg (name)
|
|
if n is None:
|
|
self.setval (getattr (self, name))
|
|
else:
|
|
if self.arg2 is not None:
|
|
n = (getattr (self, name) | n) & (~self.arg2)
|
|
if n & 32768:
|
|
# Sign extend upper 16 bits
|
|
n |= -32768
|
|
fixed = getattr (self, name + "fixed", 0)
|
|
self.clearargs ()
|
|
setattr (self, name, n | fixed)
|
|
|
|
def numflag (self, name):
|
|
"""Process a numeric flag type command, i.e., one that is
|
|
read by supplying no argument, and set by supplying the new value.
|
|
"""
|
|
n = self.getoptarg (name)
|
|
if n is None:
|
|
self.setval (getattr (self, name))
|
|
else:
|
|
self.clearargs ()
|
|
setattr (self, name, n)
|
|
|
|
def lineargs(self, c):
|
|
"""Process the argument(s) for a command that references lines.
|
|
If one argument is present, that is taken as a line count
|
|
displacement from dot. If two arguments are present, that is
|
|
the start and end of the the range.
|
|
"""
|
|
m, n = self.arg2, self.getarg (c)
|
|
if n is None:
|
|
n = 1
|
|
if m is None:
|
|
if n > 0:
|
|
m, n = self.dot, self.buffer.line (n)
|
|
else:
|
|
m, n = self.buffer.line (n), self.dot
|
|
else:
|
|
if m < 0 or n > self.end or m > n:
|
|
raise POP (self, c)
|
|
return (m, n)
|
|
|
|
def clearmods (self):
|
|
"""Clear modifier flags (colon and at sign).
|
|
"""
|
|
self.colons = 0
|
|
self.atmod = False
|
|
|
|
def clearargs (self):
|
|
"""Reinitialize all expression state, as well as modifiers.
|
|
"""
|
|
self.opstack = [ ]
|
|
self.arg = None
|
|
self.arg2 = None
|
|
self.num = None
|
|
self.op = None
|
|
self.clearmods ()
|
|
|
|
def failedcommand (self):
|
|
"""Return the last failed interactive command, i.e., the last
|
|
command up to the point where execution was aborted.
|
|
"""
|
|
return self.interactive.failedcommand ()
|
|
|
|
def lastcommand (self):
|
|
"""Return the last interactive command, in full.
|
|
"""
|
|
return self.interactive.lastcommand ()
|
|
|
|
def mainloop (self):
|
|
"""This is the TECO command loop. It fetches the command string,
|
|
handles special immediate action forms, and otherwise executes
|
|
the command as supplied. Errors are handled by catching the
|
|
TECO error exception, and printing the error information as
|
|
controlled by the EH flag.
|
|
"""
|
|
preverror = False
|
|
try:
|
|
while True:
|
|
if not self.cmdhandler.eifile:
|
|
if screen:
|
|
y, x = screen.getmaxyx ()
|
|
screen.untouchwin ()
|
|
screen.move (y - 1, 0)
|
|
screen.clrtoeol ()
|
|
screen.refresh ()
|
|
curses.reset_shell_mode ()
|
|
self.incurses = False
|
|
self.screenok = False
|
|
self.updatedisplay ()
|
|
# clear "in startup mode" flag
|
|
self.et &= ~128
|
|
self.autoverify (self.ev)
|
|
sys.stdout.write ("*")
|
|
sys.stdout.flush ()
|
|
cmdline = self.cmdhandler.teco_cmdstring ()
|
|
self.clearargs ()
|
|
if cmdline and cmdline[-1] != esc:
|
|
# it was an immediate action command -- handle that
|
|
cmd = cmdline[0]
|
|
if cmd == '*':
|
|
try:
|
|
q = self.interactive.qreg (cmdline[1])
|
|
except err as e:
|
|
e.show ()
|
|
else:
|
|
q.setstr (self.lastcommand ())
|
|
continue
|
|
elif cmd == '?':
|
|
if preverror:
|
|
print(printable (self.failedcommand ()))
|
|
continue
|
|
elif cmd == lf:
|
|
cmdline = "lt"
|
|
else:
|
|
cmdline = "-lt"
|
|
try:
|
|
preverror = False
|
|
self.runcommand (cmdline)
|
|
except ExitExecution:
|
|
pass
|
|
except SystemExit:
|
|
endwin ()
|
|
enddisplay ()
|
|
break
|
|
except err as e:
|
|
preverror = True
|
|
e.show ()
|
|
###print_exc_plus () # *** for debug
|
|
# Turn off any EI
|
|
self.cmdhandler.ei ("")
|
|
except:
|
|
print_exc_plus ()
|
|
except:
|
|
print_exc_plus ()
|
|
|
|
def runcommand (self, s):
|
|
"""Execute the specified TECO command string as an interactive
|
|
level command.
|
|
"""
|
|
self.interactive.run (s)
|
|
|
|
def screentext (self, height, width, curlinenum):
|
|
"""Given a screen height and width in characters, and the
|
|
line on which we want 'dot' to appear, determine the text that
|
|
fits on the screen.
|
|
|
|
Returns a three-element tuple: lines, row, and column corresponding
|
|
to dot.
|
|
|
|
Lines is a list; each list element is a pair of text and
|
|
wrap flag. Wrap flag is True if this chunk of text is wrapped,
|
|
i.e., it does not end with a carriage return, and False if
|
|
it is the end of the line.
|
|
"""
|
|
curlinestart = self.buffer.line (0)
|
|
curcol = self.dot - curlinestart
|
|
line = self.buf[curlinestart:self.buffer.line(1)]
|
|
lines, currow, curcol = untabify (line, curcol, width)
|
|
n = 1
|
|
# First add on lines after the current line, up to the
|
|
# window height, if we have that many
|
|
while len (lines) < height:
|
|
start, end = self.buffer.line (n), self.buffer.line (n + 1)
|
|
if start >= self.end:
|
|
break
|
|
line, i, i = untabify (self.buf[start:end], 0, width)
|
|
lines += line
|
|
n += 1
|
|
# Next, add lines before the current line, until we have
|
|
# enough to put the cursor onto the line where we want it,
|
|
# but also try to fill the screen
|
|
n = 0
|
|
while currow < curlinenum or len (lines) < height:
|
|
start, end = self.buffer.line (-n - 1), self.buffer.line (-n)
|
|
if not end:
|
|
break
|
|
line, i, i = untabify (self.buf[start:end], 0, width)
|
|
lines = line + lines
|
|
currow += len (line)
|
|
n += 1
|
|
# Now trim things, since (a) the topmost line may have wrapped
|
|
# so the cursor may be lower than we want it to be, and (b)
|
|
# we now probably have more lines than we want at the end
|
|
# of the screen.
|
|
trim = min (currow - curlinenum, len (lines) - height)
|
|
if trim > 0:
|
|
lines = lines[trim:]
|
|
currow -= trim
|
|
if len (lines) > height:
|
|
lines = lines[:height]
|
|
return lines, currow, curcol
|
|
|
|
def enable_curses (self):
|
|
"""Enable curses (VT100 style screen watch) mode, if available
|
|
in this Python installation. This is a NOP if curses is not
|
|
available.
|
|
"""
|
|
global screen
|
|
if not cursespresent:
|
|
return
|
|
if not screen:
|
|
atexit.register (endwin)
|
|
screen = curses.initscr ()
|
|
curses.noecho ()
|
|
curses.nonl ()
|
|
curses.raw ()
|
|
curses.def_prog_mode ()
|
|
else:
|
|
curses.reset_prog_mode ()
|
|
self.incurses = True
|
|
self.watchparams[2], self.watchparams[1] = screen.getmaxyx ()
|
|
|
|
def watch (self):
|
|
"""Do a screen watch operation, i.e., update the screen to
|
|
reflect the buffer contents around the current dot, in curses mode.
|
|
"""
|
|
if not cursespresent:
|
|
return
|
|
self.enable_curses ()
|
|
curlinenum = self.curline
|
|
width, height = self.watchparams[1], self.watchparams[2]
|
|
lines, currow, curcol = self.screentext (height, width, curlinenum)
|
|
if currow >= height:
|
|
currow = height - 1
|
|
if curcol >= width:
|
|
curcol = width - 1
|
|
for row, line in enumerate (lines):
|
|
line, wrap = line
|
|
screen.addstr (row, 0, line)
|
|
screen.clrtoeol ()
|
|
screen.clrtobot ()
|
|
if not self.screenok:
|
|
screen.clearok (1)
|
|
self.screenok = True
|
|
screen.move (currow, curcol)
|
|
screen.refresh ()
|
|
|
|
# Interface to the display thread
|
|
def startdisplay (self):
|
|
"""Start the wxPython display (GT40 emulation, essentially).
|
|
This is a NOP if wxPython is not available.
|
|
"""
|
|
if not display:
|
|
atexit.register (enddisplay)
|
|
# Release the main thread, allowing it to start the display
|
|
dsem.release ()
|
|
else:
|
|
self.updatedisplay ()
|
|
|
|
def updatedisplay (self):
|
|
"""Refresh the GT40 style display to reflect the current
|
|
buffer state around dot.
|
|
"""
|
|
if display:
|
|
display.show ()
|
|
|
|
def hidedisplay (self):
|
|
"""Turn off (hide) the GT40 display.
|
|
"""
|
|
if display:
|
|
display.show (False)
|
|
|
|
def autoverify (self, flag):
|
|
"""Handle automatic verify for the ES and EV flags. The input
|
|
is the flag in question.
|
|
"""
|
|
if flag:
|
|
if flag == -1:
|
|
flag = 0
|
|
ch = flag & 255
|
|
if ch:
|
|
if ch < 32:
|
|
ch = lf
|
|
else:
|
|
ch = chr (ch)
|
|
flag >>= 8
|
|
start = self.buffer.line (-flag)
|
|
end = self.buffer.line (flag + 1)
|
|
sys.stdout.write (printable (self.buf[start:self.dot]))
|
|
if ch:
|
|
sys.stdout.write (ch)
|
|
sys.stdout.write (printable (self.buf[self.dot:end]))
|
|
sys.stdout.flush ()
|
|
self.screenok = False
|
|
|
|
# A metaclass to allow non-alphanumeric methods to be defined, which
|
|
# is handy when you use method names directly for command character
|
|
# processing. Inspired by the Python Cookbook, chapter 20 intro.
|
|
def _repchar (m):
|
|
return chr (int (m.group (1), 8))
|
|
class Anychar (type):
|
|
def __new__ (cls, cname, cbases, cdict):
|
|
newnames = {}
|
|
charre = re.compile ("char([0-7]{3})")
|
|
for name in cdict:
|
|
newname, cnt = charre.subn (_repchar, name)
|
|
if cnt:
|
|
fun = cdict[name]
|
|
newnames[newname] = fun
|
|
cdict.update (newnames)
|
|
return super (Anychar, cls).__new__ (cls, cname, cbases, cdict)
|
|
|
|
class qreg (object):
|
|
'''Queue register object. Stores text and numeric parts, with
|
|
methods to access each part.
|
|
'''
|
|
def __init__ (self):
|
|
self.num = 0
|
|
self.text = ""
|
|
|
|
def getnum (self):
|
|
return self.num
|
|
|
|
def setnum (self, val):
|
|
self.num = val
|
|
|
|
def getstr (self):
|
|
return self.text
|
|
|
|
def setstr (self, val):
|
|
self.text = val
|
|
|
|
def appendstr (self, val):
|
|
self.text += val
|
|
|
|
# Atexit handlers. These are guarded so they can be called even
|
|
# if the corresponding module isn't present.
|
|
def endwin ():
|
|
"""Close down curses mode.
|
|
"""
|
|
global screen
|
|
if screen:
|
|
try:
|
|
curses.endwin ()
|
|
except:
|
|
pass
|
|
screen = None
|
|
|
|
def enddisplay ():
|
|
"""Stop wxPython display.
|
|
"""
|
|
global display
|
|
if display:
|
|
display.stop ()
|
|
|
|
if wxpresent:
|
|
pointsize = 12
|
|
|
|
class displayApp ():
|
|
"""This class wraps the App main loop for wxPython.
|
|
|
|
Due to limitations in some OS (Mac OS at least), this
|
|
class must be created in the main thread -- the first
|
|
thread of the process. Furthermore, the "start" method
|
|
will create the application and run the main loop, and will
|
|
not return until application exit. So for any other things
|
|
that need to run in the meantime, like user interaction,
|
|
the caller will need to create an additional thread.
|
|
|
|
This class creates a 24 by 80 character window, and initializes
|
|
some common state such as the font used to display text
|
|
in the window.
|
|
"""
|
|
|
|
def __init__ (self, teco):
|
|
self.running = False
|
|
self.teco = teco
|
|
self.displayline = 16
|
|
|
|
def start (self):
|
|
"""Start the wx App main loop.
|
|
This finds a font, then opens up a Frame initially
|
|
sized for 80 by 24 characters.
|
|
"""
|
|
global display
|
|
self.app = wx.App ()
|
|
self.font = wx.Font (pointsize, wx.FONTFAMILY_MODERN,
|
|
wx.FONTSTYLE_NORMAL,
|
|
wx.FONTWEIGHT_NORMAL)
|
|
dc = wx.MemoryDC ()
|
|
dc.SetFont (self.font)
|
|
self.fontextent = dc.GetTextExtent ("a")
|
|
cw, ch = self.fontextent
|
|
self.margin = cw
|
|
cw = cw * 80 + self.margin * 2
|
|
ch = ch * 24 + self.margin * 2
|
|
self.frame = displayframe (self, wx.ID_ANY, "TECO display",
|
|
wx.Size (cw, ch))
|
|
self.frame.Show (True)
|
|
self.running = True
|
|
self.app.MainLoop ()
|
|
# Come here only on application exit.
|
|
display = None
|
|
|
|
def show (self, show = True):
|
|
"""If the display is active, this refreshes it to display
|
|
the current text around dot. If "show" is False, it tells
|
|
the display to go hide itself.
|
|
"""
|
|
if self.running:
|
|
self.frame.doShow = show
|
|
self.frame.doRefresh = True
|
|
wx.WakeUpIdle ()
|
|
|
|
def stop (self):
|
|
"""Stop the display thread by closing the Frame.
|
|
"""
|
|
if self.running:
|
|
self.running = False
|
|
self.frame.AddPendingEvent (wx.CloseEvent (wx.wxEVT_CLOSE_WINDOW))
|
|
|
|
class displayframe (wx.Frame):
|
|
"""Simple text display window class derived from wxFrame.
|
|
It handles repaint, close, and timer events. The timer
|
|
event is used for the blinking text cursor.
|
|
|
|
When instantiated, this class creats the window using the
|
|
supplied size, and starts the cursor blink timer.
|
|
"""
|
|
def __init__ (self, display, id, name, size):
|
|
framestyle = (wx.MINIMIZE_BOX |
|
|
wx.MAXIMIZE_BOX |
|
|
wx.RESIZE_BORDER |
|
|
wx.SYSTEM_MENU |
|
|
wx.CAPTION |
|
|
wx.CLOSE_BOX |
|
|
wx.FULL_REPAINT_ON_RESIZE)
|
|
wx.Frame.__init__ (self, None, id, name, style = framestyle)
|
|
self.SetClientSize (size)
|
|
timerId = 666
|
|
self.Bind (wx.EVT_PAINT, self.OnPaint)
|
|
self.Bind (wx.EVT_CLOSE, self.OnClose)
|
|
self.Bind (wx.EVT_TIMER, self.OnTimer)
|
|
self.Bind (wx.EVT_IDLE, self.OnIdle)
|
|
self.display = display
|
|
self.cursor = None, None, False
|
|
self.timer = wx.Timer (self, timerId)
|
|
self.timer.Start (500)
|
|
self.cursorState = True
|
|
self.doRefresh = False
|
|
self.SetBackgroundColour (wx.WHITE)
|
|
|
|
def OnIdle (self, event = None):
|
|
"""Used to make a refresh happen, if one has been requested.
|
|
"""
|
|
if self.doRefresh:
|
|
self.doRefresh = False
|
|
self.Show (self.doShow)
|
|
if self.doShow:
|
|
self.Refresh ()
|
|
|
|
def OnTimer (self, event = None):
|
|
"""Draw a GT40-TECO style cursor: vertical line with
|
|
a narrow horizontal line across the bottom, essentially
|
|
an upside-down T.
|
|
|
|
If dot is between a CR and LF, the cursor is drawn upside
|
|
down (right side up T) at the left margin.
|
|
"""
|
|
x, y, flip = self.cursor
|
|
if x is not None:
|
|
cw, ch = self.display.fontextent
|
|
if self.cursorState:
|
|
pen = wx.BLACK_PEN
|
|
else:
|
|
pen = wx.WHITE_PEN
|
|
self.cursorState = not self.cursorState
|
|
dc = wx.ClientDC (self)
|
|
dc.SetPen (pen)
|
|
if flip:
|
|
dc.DrawLine (x, y, x, y - ch)
|
|
dc.DrawLine (x - cw / 2, y - ch, x + cw / 2 + 1, y - ch)
|
|
else:
|
|
dc.DrawLine (x, y, x, y - ch)
|
|
dc.DrawLine (x - cw / 2, y, x + cw / 2 + 1, y)
|
|
|
|
def OnPaint (self, event = None):
|
|
"""This is the event handler for window repaint events,
|
|
which is also done on window resize. It fills the window
|
|
with the buffer contents, centered on dot.
|
|
|
|
Line wrap is indicated in GT40 fashion: the continuation line
|
|
segments have a right-pointing arrow in the left margin.
|
|
"""
|
|
dc = wx.PaintDC (self)
|
|
dc.Clear ()
|
|
dc.SetFont (self.display.font)
|
|
w, h = dc.GetSize ()
|
|
w -= 2 * self.display.margin
|
|
h -= 2 * self.display.margin
|
|
cw, ch = self.display.fontextent
|
|
w //= cw
|
|
h //= ch
|
|
lines, currow, curcol = self.display.teco.screentext (h, w, h // 2)
|
|
if curcol > len (lines[currow][0]):
|
|
self.cursor = self.display.margin, \
|
|
(currow + 2) * ch + self.display.margin, \
|
|
True
|
|
else:
|
|
self.cursor = curcol * cw + self.display.margin, \
|
|
(currow + 1) * ch + self.display.margin, \
|
|
False
|
|
wrap = False
|
|
dc.SetPen (wx.BLACK_PEN)
|
|
for row, line in enumerate (lines):
|
|
y = self.display.margin + row * ch
|
|
if wrap:
|
|
y2 = y + ch - ch / 2
|
|
dc.DrawLine (1, y2, cw - 1, y2)
|
|
dc.DrawLines ([ wx.Point (cw / 2, y2 - cw / 3),
|
|
wx.Point (cw - 1, y2),
|
|
wx.Point (cw / 2, y2 + cw / 3 + 1)])
|
|
line, wrap = line
|
|
dc.DrawText (line, self.display.margin, y)
|
|
|
|
def OnClose (self, event = None):
|
|
"""Close the GT40 window, and stop the cursor blink timer.
|
|
"""
|
|
self.timer.Stop ()
|
|
self.cursor = None, None
|
|
self.Destroy ()
|
|
|
|
# Nice hairy regexp for scanning across bracketed constructs looking
|
|
# for the end of a range (conditional or iteration). It doesn't bother
|
|
# looking for parentheses since none of the constructs we have to scan
|
|
# for ("else" or condition end, or iteration end, or label) are allowed
|
|
# inside parentheses -- and it isn't the job of this scanner to catch
|
|
# illegal commands. For the same reason, it doesn't do things like
|
|
# look for missing arguments, or invalid q-reg names, etc.
|
|
#
|
|
# The recipe here goes like this: the first half is for @-modified commands,
|
|
# so it matches string arguments wrapped in matching delimiters.
|
|
# The second half is the corresponding set of rules for non-@-modified
|
|
# commands, so they have escape as the string terminator, except for
|
|
# those oddballs that use something else, like ctrl/a. It also covers
|
|
# the cases of commands that don't take string arguments and thus
|
|
# don't care about @ modifiers
|
|
#
|
|
# The indenting is meant to show grouping. This pattern must be compiled
|
|
# with the "verbose" flag.
|
|
#
|
|
# This pattern is subsequently modified to make three very similar patterns:
|
|
# one to scan across iterations, one to scan across conditionals, and
|
|
# one to search for tags (labels). A single base pattern is used to
|
|
# form all three, so I don't have to keep three almost-identical copies
|
|
# of these things in sync.
|
|
basepat = """
|
|
# First any commands that take string arguments, @ modified flavor
|
|
(?:(?:@(?:\\:*(?:(?:f(?:(?:[cns_](.).*?\\1.*?\\1)|
|
|
(?:[br](.).*?\\2)|
|
|
.))|
|
|
(?:e(?:[bginrw_](.).*?\\3|.))|
|
|
# Control A and Control U in uparrow form
|
|
(?:\\^(?:(?:a(.).*?\\4)|
|
|
(?:u\\.?.(.).*?\\5)|.))|
|
|
# Tag start is included here, so tags are skipped
|
|
# The ! is removed for tag search so tags are
|
|
# not skipped there.
|
|
(?:[\001!inos_](.).*?\\6)|
|
|
(?:\025\\.?.(.).*?\\7))))|
|
|
# Next commands that take string arguments, no @
|
|
(?:(?:f(?:(?:[cns_].*?\033.*?\033)|
|
|
(?:[br].*?\033)|
|
|
.))|
|
|
(?:e(?:[bginrw_].*?\033|.))|
|
|
# Control A, Control U, Control ^ in uparrow form
|
|
(?:\\^(?:(?:a.*?\001)|
|
|
(?:u\\.?..*?\025)|
|
|
(?:\\^.)|.))|
|
|
(?:[inos_].*?\033)|
|
|
(?:\001.*?\001)|
|
|
# At this point we insert one of several subexpressions,
|
|
# depending on what we need: a pattern to skip tags,
|
|
# or a pattern to skip condition starts, or both, or
|
|
# neither
|
|
### insert here
|
|
(?:\025\\.?..*?\033)|
|
|
(?:\036.)|
|
|
(?:[][%gmqux]\\.?.)|
|
|
# The tilde is replaced by the terminator character set
|
|
[^~]))*
|
|
"""
|
|
# These two can be inserted into basepat at "### insert here"
|
|
marker = "### insert here"
|
|
exclpat = "(?:!.*?!)|"
|
|
dqpat = '(?:".)|'
|
|
|
|
# These are the terminator sets, inserted at two places into basepat to
|
|
# specify what set of characters terminates the scan
|
|
iterset = "<>"
|
|
condset = "\"|\'<>"
|
|
tagset = "!<>"
|
|
|
|
# Construct the three patterns
|
|
iterpat = basepat.replace (marker, exclpat + dqpat).replace ('~', iterset)
|
|
condpat = basepat.replace (marker, exclpat).replace ('~', condset)
|
|
tagpat = basepat.replace ("!", "").replace (marker, dqpat).replace ('~', tagset)
|
|
|
|
iterre = re.compile (iterpat, re.IGNORECASE | re.DOTALL | re.VERBOSE)
|
|
condre = re.compile (condpat, re.IGNORECASE | re.DOTALL | re.VERBOSE)
|
|
tagre = re.compile (tagpat, re.IGNORECASE | re.DOTALL | re.VERBOSE)
|
|
|
|
class iter (object):
|
|
'''State for command iterations.
|
|
'''
|
|
def __init__ (self, teco, cmd, count):
|
|
self.start = cmd.cmdpos
|
|
self.count = count
|
|
self.cmd = cmd
|
|
self.teco = teco
|
|
|
|
def again (self, atend = True, delta = 1):
|
|
if self.count:
|
|
self.count -= delta
|
|
if not self.count:
|
|
if not atend:
|
|
self.cmd.skipiter ()
|
|
if self.teco.trace:
|
|
sys.stdout.write ('>')
|
|
sys.stdout.flush ()
|
|
self.cmd.iterstack.pop ()
|
|
return
|
|
self.cmd.cmdpos = self.start
|
|
self.teco.clearargs ()
|
|
|
|
# assorted regular expressions used below:
|
|
|
|
# patterns for \ command, for the three possible radix values
|
|
decre = re.compile (r'[+-]?\d+')
|
|
octre = re.compile (r'[+-]?[0-7]+')
|
|
hexre = re.compile (r'[+-]?[0-9a-f]+', re.IGNORECASE)
|
|
|
|
# Patterns for the string builder, with and without ^x to control-x
|
|
# conversion. Note that a single replacer function is used with
|
|
# either pattern, so bldpat must be a superset of buildpatnoup,
|
|
# and the common groups must come first and in the same order.
|
|
_bldpat = re.compile('''
|
|
(?:(?:(?:\\^[qr])|[\021\022])(.))| # ^Qx or ^Rx
|
|
(?:(?:(?:\\^e)|\005)q(\\.?.))| # ^EQq
|
|
(?:(?:(?:\\^e)|\005)u(\\.?.))| # ^EUq
|
|
(?:(?:(?:\\^v)|\026)(.))| # ^Vx
|
|
(?:(?:(?:\\^w)|\027)(.))| # ^Wx
|
|
(?:\\^(.)) # ^x
|
|
''', re.IGNORECASE |re.DOTALL | re.VERBOSE)
|
|
_bldpatnoup = re.compile('''
|
|
(?:[\021\022](.))| # ^Qx or ^Rx
|
|
(?:\005q(\\.?.))| # ^EQq
|
|
(?:\005u(\\.?.))| # ^EUq
|
|
(?:\026(.))| # ^Vx
|
|
(?:\027(.)) # ^Wx
|
|
''', re.IGNORECASE | re.DOTALL | re.VERBOSE)
|
|
|
|
# pattern for the search string to search regexp converter
|
|
_searchpat = re.compile ('''
|
|
# A regexp special character (must be quoted)
|
|
([][\\\\^$.?+(){}])|
|
|
# ^ES -- One or more spaces/tabs; ^EX -- any char
|
|
# These two do not accept ^N
|
|
((?:\005[sx])|\030)|
|
|
# Check for leading ^N (inverse match)
|
|
(?:(\016)?
|
|
(?:
|
|
# ^EGq -- table match, with optional ^N
|
|
(?:\005g(\\.?.))|
|
|
# All other special match characters
|
|
((?:\005[abcdlrvwx])|\023)))|
|
|
# Check for ^EE -- regexp match (teco.py addition)
|
|
(?:\005e(.+))
|
|
''', re.IGNORECASE | re.DOTALL | re.VERBOSE)
|
|
|
|
# Substitution dictionary for the special match characters
|
|
#
|
|
# These are regexp subexpressions corresponding to TECO match patterns
|
|
_searchdict2 = { ctrle + "s" : "[ \t]+",
|
|
ctrle + "x" : ".",
|
|
ctrlx : "." }
|
|
|
|
# These are rexexp character class expressions, so they go inside
|
|
# [...], or [^...] if ^N was present in the TECO string
|
|
_searchdict5 = { ctrle + "a" : "A-Za-z",
|
|
ctrle + "b" : "\\W",
|
|
ctrle + "c" : "\\w$_.",
|
|
ctrle + "d" : "\d",
|
|
ctrle + "l" : eol,
|
|
ctrle + "r" : "\\w",
|
|
ctrle + "v" : "a-z",
|
|
ctrle + "w" : "A-Z",
|
|
ctrls : "\\W"}
|
|
|
|
class command_level(metaclass=Anychar):
|
|
'''This state handles a single command level (interactive or macro
|
|
execution) for TECO.
|
|
|
|
Any method with a one-character name is the handler for the
|
|
corresponding TECO command. Two-character methods are for two-
|
|
character TECO command names (the dispatch is via the one-character
|
|
method matching the start character; for example method "fb" is
|
|
invoked via method "f").
|
|
|
|
TECO command characters that are not valid Python symbol names
|
|
are represented by methods with "charnnn" in the name. The metaclass
|
|
creates synonyms for those methods with the real name, which is
|
|
the character with octal char code nnn. For example, char042 is
|
|
the " (double quote) command method, and fchar074 is the f< command
|
|
method.
|
|
'''
|
|
def __init__ (self, teco, q = None):
|
|
self.qregs = q or { }
|
|
self.teco = teco
|
|
self.enlist = [ ]
|
|
self.enstring = ""
|
|
self.iterstack = [ ]
|
|
|
|
# Define a bunch of properties for cleaner access to state that
|
|
# is kept in other places.
|
|
# First the ones that are common across all command levels, and
|
|
# are kept by the "teco" class:
|
|
atmod = commonprop ("atmod")
|
|
arg2 = commonprop ("arg2")
|
|
colons = commonprop ("colons")
|
|
ctrlxflag = commonprop (ctrlx)
|
|
edflag = commonprop ("ed")
|
|
etflag = commonprop ("et")
|
|
radix = commonprop ("radix")
|
|
laststringlen = commonprop ("laststringlen")
|
|
lastsearch = commonprop ("lastsearch")
|
|
lastfilename = commonprop ("lastfilename")
|
|
trace = commonprop ("trace")
|
|
buffer = commonprop ("buffer")
|
|
screenok = commonprop ("screenok")
|
|
|
|
# Now the ones that relate to the text buffer, so they are kept
|
|
# by the "buffer" class:
|
|
buf = bufprop ("text")
|
|
dot = bufprop ("dot")
|
|
end = bufprop ("end")
|
|
eoflag = bufprop ("eoflag")
|
|
ffflag = bufprop ("ffflag")
|
|
|
|
def do (self, c):
|
|
"""Execute the single teco command named by the argument
|
|
(a single character, or a two-character string for TECO
|
|
commands that have two character names). The method for that
|
|
command is invoked, if it exists; otherwise error ILL
|
|
(Illegal command) is raised.
|
|
|
|
The command name is passed as argument to the method, which is
|
|
useful when several commands (e.g., all the digits) are
|
|
bound to a single method.
|
|
"""
|
|
c = c.lower ()
|
|
try:
|
|
op = getattr (self, c)
|
|
except AttributeError:
|
|
raise ILL (self.teco, c)
|
|
op (c)
|
|
|
|
def tracechar (self, c):
|
|
"""Show the supplied character (or string) as trace text,
|
|
if tracing is enabled.
|
|
"""
|
|
if self.trace:
|
|
sys.stdout.write (printable (c))
|
|
sys.stdout.flush ()
|
|
|
|
def peeknextcmd (self):
|
|
"""Look at the next command character, without advancing
|
|
the current command pointer.
|
|
"""
|
|
try:
|
|
return self.command[self.cmdpos]
|
|
except IndexError:
|
|
return ""
|
|
|
|
def nextcmd (self):
|
|
"""Get the next command character; if there isn't one,
|
|
error UTC (Unterminated Command) is raised.
|
|
"""
|
|
c = self.peeknextcmd ()
|
|
if not c:
|
|
raise UTC (self.teco)
|
|
self.tracechar (c)
|
|
self.cmdpos += 1
|
|
return c
|
|
|
|
def colon (self):
|
|
"""Return True if colon modifier(s) are present.
|
|
"""
|
|
return self.colons != 0
|
|
|
|
def getarg (self, c, default = None):
|
|
'''Get the command argument. If there is no argument, the
|
|
default argument governs what happens. If no default is
|
|
supplied, the function returns None. If a default value is
|
|
supplied, that value is returned. Otherwise, the default argument
|
|
should be an exception class, and that exception is raised.
|
|
This function is used for cases where the command does not return
|
|
a value; it requires that any argument is complete (matching
|
|
parentheses, right hand side present for a pending operator).
|
|
'''
|
|
ret = self.teco.getarg (c)
|
|
if default is not None:
|
|
self.clearargs ()
|
|
if ret is None:
|
|
# Yuck. If exceptions were new style classes
|
|
# I wouldn't need this ugly mess!
|
|
if type (default) is type (Exception):
|
|
raise default (self.teco)
|
|
ret = default
|
|
return ret
|
|
|
|
def getoptarg (self, c):
|
|
'''Get the command argument. Return None if it was not present.
|
|
This function may be called when we are in the middle of an
|
|
expression; that is intended for the case where a command may return
|
|
a value that is then in turn part of an expression.
|
|
'''
|
|
return self.teco.getoptarg (c)
|
|
|
|
def getargs (self, c, default = None):
|
|
"""Get the command argument pair, as a pair. If there was
|
|
only one argument, the first element of the pair is None.
|
|
If there was no argument at all, or nothing after the comma,
|
|
the supplied default is used for the second element of the pair
|
|
in the same way as for method getarg.
|
|
"""
|
|
arg2 = self.arg2
|
|
return arg2, self.getarg (c, default)
|
|
|
|
def getargc (self, c, default = None):
|
|
"""Get the command argument and the colon modifier, as a pair.
|
|
If there was no argument, the supplied default is used for the
|
|
first element of the pair in the same way as for method getarg.
|
|
"""
|
|
col = self.colon ()
|
|
return self.getarg (c, default), col
|
|
|
|
def getargsc (self, c, default = None):
|
|
"""Get the command argument pair and the colon flag, as a tuple.
|
|
If there was only one argument, the first element of the pair is None.
|
|
If there was no argument at all, or nothing after the comma,
|
|
the supplied default is used for the second element of the pair
|
|
in the same way as for method getarg.
|
|
"""
|
|
arg2 = self.arg2
|
|
col = self.colon ()
|
|
return arg2, self.getarg (c, default), col
|
|
|
|
def setval (self, n):
|
|
"""Set the command result value into the expression state.
|
|
"""
|
|
self.teco.setval (n)
|
|
|
|
def clearargs (self):
|
|
"""Clear the expression state and command modifiers.
|
|
"""
|
|
self.teco.clearargs ()
|
|
|
|
def clearmods (self):
|
|
"""Clear the command modifier flags (colon and at sign).
|
|
"""
|
|
self.teco.clearmods ()
|
|
|
|
def bitflag (self, c):
|
|
"""Process a bit flag command, such as ET. See teco.bitflag
|
|
for details.
|
|
"""
|
|
self.teco.bitflag (c)
|
|
|
|
def numflag (self, c):
|
|
"""Process a numeric flag command, such as EV. See teco.numflag
|
|
for details.
|
|
"""
|
|
self.teco.numflag (c)
|
|
|
|
def strarg (self, c, term = esc):
|
|
"""Return the string argument for the command. If the at sign
|
|
modifier is in effect, the next character in the command string
|
|
is the delimiter. Otherwise, the term argument specifies the
|
|
delimiter, or ESC is used if term is omitted.
|
|
"""
|
|
if self.atmod:
|
|
term = self.nextcmd ()
|
|
self.atmod = False
|
|
s = self.cmdpos
|
|
try:
|
|
e = self.command.index (term, s)
|
|
except ValueError:
|
|
raise UTC (self.teco, c)
|
|
self.cmdpos = e + 1
|
|
self.tracechar (self.command[s:self.cmdpos])
|
|
return self.command[s:e]
|
|
|
|
def strargs (self, c):
|
|
"""Return a pair of string arguments for the command. If the at
|
|
sign modifier is in effect, the next character in the command
|
|
string is the delimiter. Otherwise, the delimiter is ESC.
|
|
"""
|
|
term = esc
|
|
if self.atmod:
|
|
term = self.peeknextcmd ()
|
|
s1 = self.strarg (c)
|
|
return s1, self.strarg (c, term)
|
|
|
|
def makecontrol (self, c):
|
|
"""Return the control character correponding to the supplied
|
|
character; for example, 'a' produces control/A.
|
|
"""
|
|
n = ord (c)
|
|
if 0o100 <= n <= 0o137 or 0o141 <= n <= 0o172:
|
|
return chr (n & 31)
|
|
else:
|
|
raise IUC (self.teco, chr (n))
|
|
|
|
def _strbuildrep (self, m):
|
|
if m.group (1):
|
|
# ^Qx or ^Rx is literally x
|
|
return m.group (1)
|
|
elif m.group (2):
|
|
# ^EQq is text of Q-reg q
|
|
return self.qregstr (m.group (2))
|
|
elif m.group (3):
|
|
# ^EUq is character whose code is in numeric Q-reg q
|
|
return chr (self.qreg (m.group (3)).getnum ())
|
|
elif m.group (4):
|
|
# ^Vx is lowercase x
|
|
return m.group (4).lower ()
|
|
elif m.group (5):
|
|
# ^Vx is uppercase x
|
|
return m.group (5).upper ()
|
|
else:
|
|
# ^x is control-x
|
|
return self.makecontrol (m.group (6))
|
|
|
|
def strbuild (self, s):
|
|
"""TECO string builder. This processes uparrow/char combinations,
|
|
unless bit 0 in ED is set. It also handles string build
|
|
characters such as ^Qx (literal x), ^EQq (text in q-reg q), etc.
|
|
"""
|
|
if self.edflag & 1:
|
|
pat = _bldpatnoup
|
|
else:
|
|
pat = _bldpat
|
|
return pat.sub (self._strbuildrep, s)
|
|
|
|
def _str2rerep (self, m):
|
|
if m.group (1):
|
|
return '\\' + m.group (1)
|
|
elif m.group (2):
|
|
return _searchdict2[m.group (2).lower ()]
|
|
|
|
inverse = m.group (3) is not None
|
|
if m.group (4):
|
|
# ^EGq -- table match
|
|
charset = set (self.qregstr (m.group (4)))
|
|
pfx = sfx = ''
|
|
if not charset:
|
|
return ""
|
|
if not inverse and len (charset) == 1:
|
|
c = ''.join (charset)
|
|
if c in "][\\^$.?+(){}":
|
|
c = '\\' + c
|
|
return c
|
|
if ']' in charset:
|
|
charset -= set (']')
|
|
pfx = ']'
|
|
if '\\' in charset:
|
|
charset -= set ('\\')
|
|
pfx += '\\'
|
|
if '-' in charset:
|
|
sfx = '-'
|
|
charset -= set ('-')
|
|
c = pfx + ''.join (charset) + sfx
|
|
elif m.group (5):
|
|
c = _searchdict5[m.group (5).lower ()]
|
|
else:
|
|
# ^EE -- regexp pattern. Return it exactly as written.
|
|
return m.group (6)
|
|
if inverse:
|
|
return "[^%s]" % c
|
|
else:
|
|
return "[%s]" % c
|
|
|
|
def str2re (self, s):
|
|
"""Convert a TECO search string to the equivalent
|
|
regular expression string.
|
|
"""
|
|
reflags = re.DOTALL
|
|
if self.ctrlxflag == 0:
|
|
reflags |= re.IGNORECASE
|
|
return re.compile (_searchpat.sub (self._str2rerep, s), reflags)
|
|
|
|
def isinteractive (self):
|
|
'''Return True if executing at the interactive level
|
|
as opposed to in a macro.
|
|
'''
|
|
return self is self.teco.interactive
|
|
|
|
def skip (self, pat):
|
|
'''Skip based on a regexp, starting at the current command
|
|
position. Updates command position to be one character beyond
|
|
the end of the match, and returns the character after the match,
|
|
if any. If no match, returns None.
|
|
|
|
The reason for passing over an extra character is that the regexp
|
|
is coded to terminate on one of the characters we want to look
|
|
for -- for example, ! < > for tag search. It would make sense
|
|
to include that set at the end of the regexp, but if you do that
|
|
then the match attempt can take a very long time if there is no
|
|
match. (It seems to take exponential time!) To avoid that,
|
|
the regexp instead describes what we want to skip, and then
|
|
picks up, skips over, and returns the character after that.
|
|
'''
|
|
m = pat.match (self.command, self.cmdpos)
|
|
if not m:
|
|
return None
|
|
self.cmdpos = m.end () + 1
|
|
try:
|
|
return self.command[self.cmdpos - 1]
|
|
except IndexError:
|
|
return None
|
|
|
|
def skipiter (self):
|
|
'''Skip across nested iterations to the end of the current
|
|
iteration.
|
|
'''
|
|
level = 1
|
|
while level > 0:
|
|
tail = self.skip (iterre)
|
|
if not tail:
|
|
raise UTC (self.teco, '<')
|
|
if tail == '<':
|
|
level += 1
|
|
else:
|
|
level -= 1
|
|
|
|
def skipcond (self, c):
|
|
'''Skip across conditional code for the specified end string.
|
|
Nested conditionals are skipped. Nested iterations are
|
|
skipped but scanned, because iterations can overlap conditionals.
|
|
'''
|
|
while True:
|
|
tail = self.skip (condre)
|
|
if not tail:
|
|
raise MAP (self.teco)
|
|
if tail in c:
|
|
break
|
|
else:
|
|
# We stopped on something other than what we wanted to skip to.
|
|
# There are two possibilities: it is the start of an inner
|
|
# range (either condition or iteration), or it is the end
|
|
# of some range.
|
|
#
|
|
# We can't just recursively skip nested ranges because
|
|
# of this warped case:
|
|
# < 0A"A C > '
|
|
#
|
|
# So instead, nested conditions are just skipped, but
|
|
# iterations are handled by stacking a simulated start of
|
|
# iteration with a repeat count of one onto the iteration
|
|
# stack and continuing the scan in-line. The count of
|
|
# one means that this case also works somewhat sanely:
|
|
# "A < xxx ' >
|
|
# I suspect that's not legal, but who knows...
|
|
if tail == '<':
|
|
self.iterstack.append (iter (self.teco, self, 1))
|
|
elif tail == '"':
|
|
self.cmdpos += 1
|
|
self.skipcond ("'")
|
|
elif tail == '>':
|
|
# Found an iteration end. Pop it off the iteration stack
|
|
try:
|
|
self.iterstack.pop ()
|
|
except IndexError:
|
|
raise BNI (self.teco)
|
|
|
|
def findtag (self, c):
|
|
'''Search for the specified tag, starting at the current
|
|
command position. Nested iterations are skipped (not searched),
|
|
so if the tag is in one of those it will not be found.
|
|
'''
|
|
while True:
|
|
tail = self.skip (tagre)
|
|
if not tail:
|
|
raise TAG (self.teco, c)
|
|
if tail == '!':
|
|
term = '!'
|
|
if self.command[self.cmdpos-2] == '@':
|
|
term = self.nextcmd ()
|
|
if self.command.startswith (c + term, self.cmdpos):
|
|
self.cmdpos += len (c) + 1
|
|
return
|
|
try:
|
|
e = self.command.index (term, self.cmdpos)
|
|
self.cmdpos = e + 1
|
|
except ValueError:
|
|
raise UTC (self.teco, '!')
|
|
elif tail == '<':
|
|
p = self.cmdpos
|
|
self.skipiter ()
|
|
else:
|
|
# Found an iteration end. Pop it off the iteration stack
|
|
try:
|
|
self.iterstack.pop ()
|
|
except IndexError:
|
|
raise BNI (self.teco)
|
|
|
|
def run (self, s):
|
|
self.command = s
|
|
self.cmdpos = 0
|
|
self.iterstack = [ ]
|
|
try:
|
|
while self.cmdpos < len (self.command):
|
|
c = self.nextcmd ()
|
|
self.do (c)
|
|
except ExitLevel:
|
|
pass
|
|
except KeyboardInterrupt:
|
|
raise XAB (self.teco)
|
|
|
|
def search (self, s, n, start, end, colon, topiffail = True,
|
|
nextpage = None):
|
|
"""Search in the current buffer for a given string.
|
|
Stop on the abs(n)th occurrence. If n is negative, search
|
|
is backward, starting at 'end'; otherwise it is forward,
|
|
starting at 'start'. The search range is bounded by
|
|
the range (start, end). If 'nextpage' is specified, it is
|
|
called if we run out of stuff to match in the current buffer,
|
|
continuing until end of file. Note that 'nextpage' is only
|
|
meaningful for forward searches (n > 0); it is ignored for
|
|
backward searches.
|
|
|
|
Note: start and end are the range of buffer positions where
|
|
the match is allowed to begin, inclusive.
|
|
|
|
If the search succeeds, dot is set to the end of the
|
|
match string, and the ^S variable is set to the negative of
|
|
the matched length. (I.e., .+^S is the start of the match.)
|
|
Finally, if 'colon' is true, the current arg is set to -1.
|
|
|
|
If the search fails, a bunch of things can happen.
|
|
|
|
If topiffail is True or omitted, and the 16 bit is clear in
|
|
the ED flag, dot is set to zero.
|
|
|
|
If 'colon' is true, the current arg is set to 0.
|
|
The same happens if we're in an iteration, and the next command
|
|
character is a semicolon.
|
|
|
|
Otherwise, if we're in an iteration, a warning message is
|
|
generated and the iteration is exited (as if a semicolon
|
|
had been next). If we're not in an iteration, an error
|
|
?SRH is generated.
|
|
"""
|
|
rep = abs (n)
|
|
if n < 0:
|
|
pos = end
|
|
laststart = None
|
|
# We don't allow paging for reverse searches
|
|
nextpage = None
|
|
else:
|
|
pos = start
|
|
if s:
|
|
s = self.strbuild (s)
|
|
self.lastsearch = s
|
|
else:
|
|
s = self.lastsearch
|
|
re = self.str2re (s)
|
|
# Bind the buffer text to a local variable to help speed
|
|
# up the inner loop
|
|
buf = self.buf
|
|
while rep:
|
|
if n < 0:
|
|
# This is painful. There is no reverse search for
|
|
# regular expressions, so we do it the hard way,
|
|
# by repeatedly matching, stepping backwards one
|
|
# character at a time...
|
|
#
|
|
# Note to self: a different way that's probably faster
|
|
# but harder to do: reverse the string, reverse the
|
|
# regexp pattern, "unreverse" any [...] and [...]+
|
|
# inside the regexp, and do an ordinary search with those.
|
|
# Then some extra work is needed to find any matches
|
|
# that straddle the search start point (i.e., dot).
|
|
tmatch = re.match (buf, pos)
|
|
if tmatch and not (laststart and tmatch.end () > laststart):
|
|
match = tmatch
|
|
laststart = match.start ()
|
|
else:
|
|
if pos:
|
|
pos -= 1
|
|
continue
|
|
match = None
|
|
else:
|
|
match = re.search (buf, pos)
|
|
if match and not start <= match.start () <= end:
|
|
match = None
|
|
if match is None:
|
|
# If we have a nextpage function and we're not
|
|
# at the end of the input file, keep going
|
|
if nextpage and self.eoflag == 0:
|
|
nextpage ()
|
|
buf = self.buf
|
|
start = 0
|
|
pos = 0
|
|
end = self.end
|
|
continue
|
|
if topiffail and (self.edflag & 16) == 0:
|
|
self.buffer.goto (0)
|
|
if colon:
|
|
self.setval (0)
|
|
return False
|
|
elif self.iterstack:
|
|
self.setval (0)
|
|
if self.peeknextcmd () != ';':
|
|
print("%Search fail in iter")
|
|
# pretend there was a ;
|
|
self.do (';')
|
|
return False
|
|
else:
|
|
raise SRH (self.teco, s)
|
|
rep -= 1
|
|
|
|
# We found what we were looking for. "match" is a regexp
|
|
# match object for the matched string.
|
|
self.buffer.goto (match.end ())
|
|
self.laststringlen = -(match.end () - match.start ())
|
|
# Supply the success value if asked for, or if a ; follows
|
|
if colon or self.iterstack and self.peeknextcmd () == ';':
|
|
self.setval (-1)
|
|
self.teco.autoverify (self.teco.es)
|
|
return True
|
|
|
|
def failedcommand (self):
|
|
"""Return the command string up to the point where execution
|
|
was aborted.
|
|
"""
|
|
return self.command[:self.cmdpos]
|
|
|
|
def lastcommand (self):
|
|
"""Return the command string, in full.
|
|
"""
|
|
return self.command
|
|
|
|
def qregname (self):
|
|
"""Parse a Q-register name from the command string. If the
|
|
next character is dot, the name is dot plus the character
|
|
after that; otherwise it is just the next character.
|
|
|
|
Note that the name is not validated here; the caller does that
|
|
if necessary.
|
|
"""
|
|
c = self.nextcmd ()
|
|
if c == '.':
|
|
c += self.nextcmd ()
|
|
return c.lower ()
|
|
|
|
def qdict (self, c):
|
|
"""Return the Q-reg dictionary referenced by the supplied Q-reg
|
|
name. If the Q-reg name begins with dot, this is the local
|
|
Q-reg dictionary for the current command level; otherwise it
|
|
is the TECO-global dictionary.
|
|
|
|
Note that the per-level dictionary is not necessarily unique
|
|
to the level; a colon-modified M command creates a new one,
|
|
an unmodified M command binds to the one of the invoking level.
|
|
"""
|
|
if c.isalnum ():
|
|
return self.teco.qregs
|
|
elif c[0] == '.' and c[1].isalnum ():
|
|
return self.qregs
|
|
else:
|
|
raise IQN (self.teco, c)
|
|
|
|
def qreg (self, c = None):
|
|
"""Return the Q-reg named by the argument, or by the command
|
|
string if the argument is omitted. If the Q-reg does not yet
|
|
exist, it is created (with no text and 0 numeric value).
|
|
"""
|
|
if c:
|
|
c = c.lower ()
|
|
else:
|
|
c = self.qregname ()
|
|
qd = self.qdict (c)
|
|
if c not in qd:
|
|
qd[c] = qreg ()
|
|
return qd[c]
|
|
|
|
def qregstr (self, c = None):
|
|
'''Return the string value of the specified Q-register,
|
|
or the last filename string if *, or the last search string
|
|
if _ was specified for the Q-register name.
|
|
'''
|
|
if c is None:
|
|
t = self.peeknextcmd ()
|
|
if t in "*_":
|
|
c = t
|
|
self.nextcmd ()
|
|
if c == '*':
|
|
return self.lastfilename
|
|
elif c == '_':
|
|
return self.lastsearch
|
|
else:
|
|
return self.qreg (c).getstr ()
|
|
|
|
def setqreg (self, q):
|
|
"""Set the Q-reg named by the command string to be the supplied
|
|
Q-reg. This is used by the ]q (pop Q-reg) command.
|
|
"""
|
|
c = self.qregname ()
|
|
qd = self.qdict (c)
|
|
qd[c] = q
|
|
|
|
# From here on we have the actual command handlers. Their names
|
|
# come in two forms. Commands whose names are alphabetic are
|
|
# given by functions whose names are simply the command name.
|
|
# Other commands are given by functions whose names contain
|
|
# "charnnn" where nnn is the octal character code.
|
|
|
|
# a few control chars, and space, are nop
|
|
def nop (self, c): pass
|
|
char000 = nop # null (^@)
|
|
char070 = nop # bell (^G)
|
|
char012 = nop # line feed (^J)
|
|
char014 = nop # form feed (^L)
|
|
char015 = nop # carriage return (^M)
|
|
char040 = nop # space
|
|
|
|
def char001 (self, c): # ^A
|
|
"""^A command -- print text.
|
|
"""
|
|
s = self.strarg (c, '\001')
|
|
sys.stdout.write (s)
|
|
sys.stdout.flush ()
|
|
self.screenok = False
|
|
self.clearargs ()
|
|
|
|
def char002 (self, c): # ^B
|
|
"""Return the current date. Since we pretend to be RT-11 it
|
|
would make sense to return the RT-11 format date, but that
|
|
utterly falls apart starting with 2004 (32 years from 1972)
|
|
so use the RSX/VMS format instead, which is substantially
|
|
more Y2K-proof.
|
|
"""
|
|
now = time.localtime ()
|
|
self.setval ((now.tm_year - 1900) * 512 + now.tm_mon * 32 + now.tm_mday)
|
|
|
|
def char003 (self, c): # ^C
|
|
"""^C command -- exit TECO if done at the interactive level;
|
|
stop execution and return to interactive prompt otherwise.
|
|
"""
|
|
if self.isinteractive ():
|
|
self.exit ()
|
|
else:
|
|
raise ExitExecution
|
|
|
|
def char004 (self, c): # ^D
|
|
"""^D command -- set radix to decimal.
|
|
"""
|
|
self.radix = 10
|
|
self.clearargs ()
|
|
|
|
def char005 (self, c): # ^E
|
|
"""^E command -- return form feed flag.
|
|
"""
|
|
self.setval (self.ffflag)
|
|
|
|
def char006 (self, c): # ^F
|
|
"""^F command -- return switch register. We just make it zero
|
|
for lack of switches...
|
|
"""
|
|
self.setval (0)
|
|
|
|
def char010 (self, c): # ^H
|
|
"""Return the current time of day. Match ^B, so we'll do
|
|
RSX/VMS format here, too. Amusingly, that happens to be
|
|
the RT-11 format, too.
|
|
"""
|
|
now = time.localtime ()
|
|
self.setval (now.tm_hour * 3600 + now.tm_min * 60 + now.tm_sec)
|
|
|
|
def char011 (self, c): # tab
|
|
"""Tab command -- insert text including the leading tab.
|
|
"""
|
|
s = self.strarg (c)
|
|
self.buffer.insert (tab + s)
|
|
self.clearargs ()
|
|
|
|
def char016 (self, c): # ^N
|
|
"""^N command -- return end of file flag.
|
|
"""
|
|
self.setval (self.eoflag)
|
|
|
|
def char017 (self, c): # ^O
|
|
"""^O command -- set radix to octal.
|
|
"""
|
|
self.radix = 8
|
|
self.clearargs ()
|
|
|
|
def char021 (self, c): # ^Q
|
|
"""^Q command -- return character offset corresponding to
|
|
the line offset supplied as the argument.
|
|
"""
|
|
self.setval (self.buffer.line (self.getarg (c, 1)))
|
|
|
|
def char022 (self, c): # ^R
|
|
"""^R -- Set the radix to the supplied value, which must
|
|
be 8, 10, or 16.
|
|
"""
|
|
r = self.getoptarg (c)
|
|
if r is None:
|
|
self.clearmods ()
|
|
self.setval (self.radix)
|
|
else:
|
|
self.clearargs ()
|
|
if r in (8, 10, 16):
|
|
self.radix = r
|
|
else:
|
|
raise IRA (self.teco)
|
|
|
|
def char023 (self, c): # ^S
|
|
"""^S command -- return the negative of the length of the
|
|
last string matched, set as replacement, or inserted.
|
|
"""
|
|
self.setval (self.laststringlen)
|
|
|
|
def char024 (self, c): # ^T
|
|
"""^T command. If an argument is given, output the character
|
|
with that numeric value (in raw mode if colon is present).
|
|
Otherwise, read a character from input and return its
|
|
numeric code.
|
|
|
|
If bit 5 in ET is set, return -1 if there is no input; otherwise
|
|
wait for it. (TODO)
|
|
"""
|
|
colon = self.colon ()
|
|
n = self.getoptarg (c)
|
|
if n is None:
|
|
self.clearmods ()
|
|
if (self.etflag & 32) and self.teco.cmdhandler.eifile is None:
|
|
# TODO -- nowait char fetch from terminal
|
|
n = -1
|
|
else:
|
|
n = ord (self.teco.cmdhandler.getch ())
|
|
self.setval (n)
|
|
else:
|
|
self.clearargs ()
|
|
if colon:
|
|
sys.stdout.write (chr (n))
|
|
else:
|
|
sys.stdout.write (printable (chr (n)))
|
|
self.screenok = False
|
|
sys.stdout.flush ()
|
|
|
|
def char025 (self, c): # ^U
|
|
"""^U command -- set Q-reg text.
|
|
"""
|
|
q = self.qreg ()
|
|
s = self.strarg (c)
|
|
if self.colon ():
|
|
q.appendstr (s)
|
|
else:
|
|
q.setstr (s)
|
|
self.clearargs ()
|
|
|
|
def char026 (self, c): # ^V
|
|
pass
|
|
|
|
def char027 (self, c): # ^W
|
|
pass
|
|
|
|
char030 = numflag # ^X
|
|
|
|
def char031 (self, c): # ^Y
|
|
"""^Y command -- synonym of ^S+.,.
|
|
"""
|
|
self.clearargs ()
|
|
self.arg2 = self.dot + self.laststringlen
|
|
self.setval (self.dot)
|
|
|
|
def char032 (self, c): # ^Z
|
|
"""^Z command -- This is supposed to be total Q-reg text size.
|
|
Just return 0 for now.
|
|
"""
|
|
self.setval (0)
|
|
|
|
def char033 (self, c): # esc
|
|
"""ESCAPE -- exit the current level if $$, otherwise clear
|
|
out any expression value and modifiers.
|
|
"""
|
|
try:
|
|
if self.peeknextcmd () == esc:
|
|
raise ExitLevel
|
|
except UTC:
|
|
pass
|
|
self.clearargs ()
|
|
|
|
def char036 (self, c): # ^^
|
|
"""^^ command -- return the numeric value of the following character
|
|
in the command string.
|
|
"""
|
|
self.setval (ord (self.nextcmd ()))
|
|
|
|
def char037 (self, c): # ^_
|
|
"""^_ command -- return one's complement of the argument.
|
|
"""
|
|
self.setval (~self.getarg (c, NAB))
|
|
|
|
def char041 (self, c): # !
|
|
"""! command -- tag (O command target) or comment.
|
|
"""
|
|
self.strarg (c, '!')
|
|
|
|
def char042 (self, c): # "
|
|
'''" command -- conditional execution range start.
|
|
'''
|
|
n = self.getarg (c, NAQ)
|
|
if 0 <= n < 0x110000:
|
|
nc = chr (n)
|
|
else:
|
|
nc = ""
|
|
c = self.nextcmd ().lower ()
|
|
if c == 'a':
|
|
cond = nc.isalpha ()
|
|
elif c == 'c':
|
|
cond = nc.isalnum () or nc in "$._"
|
|
elif c == 'd':
|
|
cond = nc.isdigit ()
|
|
elif c in "efu=":
|
|
cond = n == 0
|
|
elif c in "g>":
|
|
cond = n > 0
|
|
elif c in "lst<":
|
|
cond = n < 0
|
|
elif c in "n":
|
|
cond = n != 0
|
|
elif c == 'r':
|
|
cond = nc.isalnum ()
|
|
elif c == 'v':
|
|
cond = nc.islower ()
|
|
elif c == 'w':
|
|
cond = nc.isupper ()
|
|
else:
|
|
raise IQC (self.teco)
|
|
if not cond:
|
|
self.skipcond ("|'")
|
|
self.tracechar (self.command[self.cmdpos - 1])
|
|
|
|
def char045 (self, c): # %
|
|
"""% command -- increment Q-reg numeric value by the specified
|
|
amount, and return the result.
|
|
"""
|
|
n = self.getoptarg (c)
|
|
if n is None:
|
|
n = 1
|
|
q = self.qreg ()
|
|
n += q.getnum ()
|
|
q.setnum (n)
|
|
self.setval (n)
|
|
|
|
def char047 (self, c): # '
|
|
"""' command -- conditional range end.
|
|
"""
|
|
pass
|
|
|
|
def char050 (self, c): # (
|
|
"""( command -- expression grouping.
|
|
"""
|
|
self.teco.leftparen ()
|
|
|
|
def char051 (self, c): # )
|
|
""") command -- expression grouping.
|
|
"""
|
|
self.teco.rightparen ()
|
|
|
|
def operator (self, c):
|
|
"""Arithmetic operators. All the operators are bound to
|
|
this method; the command character (which is the argument)
|
|
distinguishes them.
|
|
"""
|
|
self.teco.operator (c)
|
|
|
|
char043 = operator # #
|
|
char046 = operator # &
|
|
char052 = operator # *
|
|
char053 = operator # +
|
|
char055 = operator # -
|
|
char057 = operator # /
|
|
|
|
def char054 (self, c): # ,
|
|
""", command -- second operand marker.
|
|
"""
|
|
if self.arg2 is None:
|
|
n = self.getarg (c, ARG)
|
|
if n < 0:
|
|
raise NCA (self.teco)
|
|
self.arg2 = n
|
|
else:
|
|
raise ARG (self.teco)
|
|
|
|
def char056 (self, c): # .
|
|
""". command -- current buffer position.
|
|
"""
|
|
self.setval (self.dot)
|
|
|
|
def digit (self, c):
|
|
"""Digits are handled by this method. All digit methods are
|
|
bound to this method, and distinguished by the command
|
|
character argument.
|
|
"""
|
|
self.teco.digit (c)
|
|
|
|
char060 = digit
|
|
char061 = digit
|
|
char062 = digit
|
|
char063 = digit
|
|
char064 = digit
|
|
char065 = digit
|
|
char066 = digit
|
|
char067 = digit
|
|
char070 = digit
|
|
char071 = digit
|
|
|
|
def char072 (self, c): # :
|
|
""": command -- modifier.
|
|
"""
|
|
self.colons = 1
|
|
if self.peeknextcmd () == ':':
|
|
self.nextcmd ()
|
|
self.colons = 2
|
|
|
|
def char073 (self, c): # ;
|
|
"""; command -- iteration exit.
|
|
"""
|
|
n, colon = self.getargc (c, NAS)
|
|
if not self.iterstack:
|
|
raise SNI (self.teco)
|
|
if (not colon and n >= 0) or (colon and n < 0):
|
|
self.skipiter ()
|
|
self.tracechar ('>')
|
|
self.iterstack.pop ()
|
|
|
|
def char074 (self, c): # <
|
|
"""< command -- iteration start.
|
|
"""
|
|
n = self.getarg (c)
|
|
self.clearargs ()
|
|
if n is None:
|
|
self.iterstack.append (iter (self.teco, self, 0))
|
|
elif n <= 0:
|
|
self.skipiter ()
|
|
self.tracechar ('>')
|
|
else:
|
|
self.iterstack.append (iter (self.teco, self, n))
|
|
|
|
def char075 (self, c): # =
|
|
"""= command -- display the argument in decimal.
|
|
== command -- display the argument in octal.
|
|
=== command -- display the argument in hex.
|
|
|
|
CRLF is added after the value unless : modifier is given.
|
|
"""
|
|
n, colon = self.getargc (c, NAE)
|
|
if colon:
|
|
term = ""
|
|
else:
|
|
term = lf
|
|
if self.peeknextcmd () == '=':
|
|
self.nextcmd ()
|
|
if self.peeknextcmd () == '=':
|
|
self.nextcmd ()
|
|
sys.stdout.write ("%x%s" % (n, term))
|
|
else:
|
|
sys.stdout.write ("%o%s" % (n, term))
|
|
else:
|
|
sys.stdout.write ("%d%s" % (n, term))
|
|
self.screenok = False
|
|
sys.stdout.flush ()
|
|
|
|
def char076 (self, c): # >
|
|
"""< command -- iteration end.
|
|
"""
|
|
if not self.iterstack:
|
|
raise BNI (self.teco)
|
|
self.clearargs ()
|
|
self.iterstack[-1].again ()
|
|
|
|
def char077 (self, c): # ?
|
|
"""? command -- toggle trace flag.
|
|
"""
|
|
self.trace = not self.trace
|
|
|
|
def char100 (self, c): # @
|
|
"""@ command -- modifier (explicitly supplied string delimiter).
|
|
"""
|
|
self.atmod = True
|
|
|
|
def a (self, c):
|
|
"""A command -- append (if no argument) or return numeric value
|
|
of character in the text buffer (if argument is given, which is
|
|
the offset from dot).
|
|
"""
|
|
n, colon = self.getargc (c)
|
|
if n is None:
|
|
self.clearargs ()
|
|
ret = self.buffer.append ()
|
|
if colon:
|
|
self.setval (ret)
|
|
else:
|
|
pos = self.dot + n
|
|
if 0 <= pos < self.end:
|
|
self.setval (ord (self.buf[pos]))
|
|
else:
|
|
self.setval (-1)
|
|
|
|
def b (self, c):
|
|
"""B command -- zero (start of buffer).
|
|
"""
|
|
self.setval (0)
|
|
|
|
def c (self, c):
|
|
"""C command -- move forward n characters.
|
|
"""
|
|
newpos = self.dot + self.getarg (c, 1)
|
|
if 0 <= newpos <= self.end:
|
|
self.buffer.goto (newpos)
|
|
else:
|
|
raise POP (self.teco, c)
|
|
|
|
def d (self, c):
|
|
"""D command -- delete n characters. If two arguments are
|
|
given, delete that range of characters.
|
|
"""
|
|
# TODO: two args
|
|
m, n = self.getargs (c, 1)
|
|
if m is None:
|
|
end = self.dot + n
|
|
if 0 <= end <= self.end:
|
|
if n < 0:
|
|
self.buffer.goto (end)
|
|
n = -n
|
|
if n:
|
|
self.buffer.delete (n)
|
|
else:
|
|
raise POP (self.teco, c)
|
|
else:
|
|
if 0 <= m <= n <= self.end:
|
|
self.buffer.goto (m)
|
|
self.buffer.delete (n - m)
|
|
else:
|
|
raise POP (self.teco, c)
|
|
self.clearmods ()
|
|
|
|
def e (self, c):
|
|
"""E command -- two character command starting with E.
|
|
"""
|
|
c = self.nextcmd ()
|
|
try:
|
|
self.do ('e' + c)
|
|
except ILL:
|
|
raise IEC (self.teco, c)
|
|
|
|
def ea (self, c):
|
|
"""EA command -- switch to alternate output stream.
|
|
"""
|
|
self.buffer.ea ()
|
|
|
|
def eb (self, c):
|
|
"""EB command -- open file for editing (input/output, with
|
|
backup).
|
|
"""
|
|
fn = self.strbuild (self.strarg (c))
|
|
colon = self.colon ()
|
|
self.clearargs ()
|
|
ret = self.buffer.eb (fn, colon)
|
|
if colon:
|
|
self.setval (ret)
|
|
|
|
def ec (self, c):
|
|
"""EC command -- finish with the current file. If input and
|
|
output files are open, write remainder of the input to output,
|
|
then close both.
|
|
"""
|
|
self.buffer.ec ()
|
|
self.clearargs ()
|
|
|
|
ed = bitflag
|
|
eh = numflag
|
|
|
|
def ef (self, c):
|
|
"""EF command -- close current output file, without any writing.
|
|
"""
|
|
self.buffer.ef ()
|
|
|
|
def eg (self, c):
|
|
"""EG command -- OS dependent action.
|
|
|
|
Right now this does nothing.
|
|
"""
|
|
cmd = self.strbuild (self.strarg (c))
|
|
colon = self.colon ()
|
|
# TODO: do something
|
|
if colon:
|
|
self.setval (0)
|
|
|
|
def ei (self, c):
|
|
"""EI command -- read command input from a file.
|
|
"""
|
|
fn = self.strbuild (self.strarg (c))
|
|
colon = self.colon ()
|
|
self.clearargs ()
|
|
ret = self.teco.cmdhandler.ei (fn, colon)
|
|
if colon:
|
|
self.setval (ret)
|
|
|
|
def ej (self, c):
|
|
"""EJ command -- return environment parameters.
|
|
"""
|
|
n = self.getarg (c, 0)
|
|
if n == -1:
|
|
self.setval (CPU * 256 + OS)
|
|
elif n == 0:
|
|
# Don't return too big a number or some things get confused
|
|
# We use the parent pid, because that's fairly constant
|
|
# for a given session -- just like the "job" number for
|
|
# classic DEC operating systems.
|
|
self.setval (os.getppid () & 255)
|
|
elif n == 1:
|
|
self.setval (0)
|
|
elif n == 2:
|
|
self.setval (os.getuid ())
|
|
else:
|
|
raise ARG (self.teco)
|
|
|
|
def ek (self, c):
|
|
"""EK command -- discard output file.
|
|
"""
|
|
self.buffer.ek ()
|
|
|
|
def en (self, c):
|
|
"""EN command -- set wildcard pattern to match, or return
|
|
next match value.
|
|
"""
|
|
cmd = self.strbuild (self.strarg (c))
|
|
colon = self.colon ()
|
|
if len (cmd):
|
|
self.enlist = glob.glob (cmd)
|
|
self.enstring = cmd
|
|
elif self.enlist:
|
|
self.lastfilename = self.enlist[0]
|
|
del self.enlist[0]
|
|
if colon:
|
|
self.setval (-1)
|
|
else:
|
|
if colon:
|
|
self.setval (0)
|
|
else:
|
|
raise FNF (self.teco, self.enstring)
|
|
|
|
def eo (self, c):
|
|
"""EO command -- return TECO version number.
|
|
"""
|
|
self.setval (VERSION)
|
|
|
|
def ep (self, c):
|
|
"""EP command -- set alternate input stream.
|
|
"""
|
|
self.buffer.ep ()
|
|
|
|
def er (self, c):
|
|
"""ER command -- open input file.
|
|
"""
|
|
fn = self.strbuild (self.strarg (c))
|
|
colon = self.colon ()
|
|
self.clearargs ()
|
|
ret = self.buffer.er (fn, colon)
|
|
if colon:
|
|
self.setval (ret)
|
|
|
|
es = numflag
|
|
et = bitflag
|
|
eu = numflag
|
|
ev = numflag
|
|
|
|
def ew (self, c):
|
|
"""EW command -- open output file.
|
|
"""
|
|
fn = self.strbuild (self.strarg (c))
|
|
colon = self.colon ()
|
|
self.clearargs ()
|
|
ret = self.buffer.ew (fn, colon)
|
|
if colon:
|
|
self.setval (ret)
|
|
|
|
def ex (self, c):
|
|
"""EX command -- finish with files (effect of EC command)
|
|
then exit TECO.
|
|
"""
|
|
self.ec (c)
|
|
self.exit ()
|
|
|
|
def exit (self):
|
|
# Release the main thread, allowing it to start the display
|
|
global exiting
|
|
exiting = True
|
|
if dsem:
|
|
dsem.release ()
|
|
sys.exit ()
|
|
|
|
def ey (self, c = None):
|
|
"""EY command -- yank unconditionally.
|
|
"""
|
|
self.buffer.yank (False)
|
|
|
|
def f (self, c):
|
|
"""F command -- two character command starting with F.
|
|
"""
|
|
c = self.nextcmd ()
|
|
try:
|
|
self.do ('f' + c)
|
|
except ILL:
|
|
raise IFC (self.teco, c)
|
|
|
|
def fchar047 (self, c): # f'
|
|
"""F' command -- 'flow' to end of conditional.
|
|
"""
|
|
self.clearargs ()
|
|
self.skipcond ("'")
|
|
self.tracechar ("'")
|
|
|
|
def fchar074 (self, c): # f<
|
|
"""F< command -- 'flow' to start of iteration. Not like
|
|
'continue' in C, it starts another iteration without
|
|
decrementing the count of iterations to do.
|
|
"""
|
|
if not self.iterstack:
|
|
raise BNI (self.teco)
|
|
self.iterstack[-1].again (False, 0)
|
|
|
|
def fchar076 (self, c): # f>
|
|
"""F> command -- 'flow' to end of iteration. Like
|
|
'continue' in C, it decrements the count of iterations
|
|
left to do, and does another if there are any left.
|
|
"""
|
|
if not self.iterstack:
|
|
raise ExitLevel
|
|
self.iterstack[-1].again (False)
|
|
|
|
def fb (self, c):
|
|
"""FB command -- bounded search.
|
|
"""
|
|
s = self.strarg (c)
|
|
m, n, colon = self.getargsc (c)
|
|
if m is None:
|
|
m = self.dot
|
|
n = m + self.buffer.line (n)
|
|
count = 1
|
|
if m > n:
|
|
count = -1
|
|
m, n = n, m
|
|
self.search (s, count, m, n, colon, False)
|
|
|
|
def fc (self, c):
|
|
"""FC command -- bounded search and replace.
|
|
"""
|
|
s, rep = self.strargs (c)
|
|
m, n, colon = self.getargsc (c)
|
|
self.clearargs ()
|
|
if m is None:
|
|
m = self.dot
|
|
n = m + self.buffer.line (n)
|
|
count = 1
|
|
if m > n:
|
|
count = -1
|
|
m, n = n, m
|
|
if self.search (s, count, m, n, colon, False):
|
|
self.buffer.goto (self.dot + self.laststringlen)
|
|
self.buffer.delete (-self.laststringlen)
|
|
self.buffer.insert (rep)
|
|
|
|
def fr (self, c):
|
|
"""FR command -- replace string previously matched or
|
|
inserted with the specified string.
|
|
"""
|
|
rep = self.strarg (c)
|
|
self.buffer.goto (self.dot + self.laststringlen)
|
|
self.buffer.delete (-self.laststringlen)
|
|
self.buffer.insert (rep)
|
|
|
|
def fs (self, c):
|
|
"""FS command -- search and replace.
|
|
"""
|
|
s, rep = self.strargs (c)
|
|
colons = self.colons
|
|
topiffail = colons < 2
|
|
m, n, colon = self.getargsc (c, 1)
|
|
if not n:
|
|
raise ISA (self.teco)
|
|
if m == 0:
|
|
m = None
|
|
topiffail = False
|
|
if n < 0:
|
|
start, end = 0, self.dot
|
|
if m is not None:
|
|
start = end - abs (m)
|
|
else:
|
|
start, end = self.dot, self.end
|
|
if m is not None:
|
|
end = start + abs (m)
|
|
if colons > 1:
|
|
start = self.dot
|
|
end = self.dot
|
|
nextpage = None
|
|
if c == "fn":
|
|
nextpage = self.buffer.page
|
|
elif c == "f_":
|
|
nextpage = self.y
|
|
if self.search (s, n, start, end, colon, topiffail, nextpage):
|
|
self.buffer.goto (self.dot + self.laststringlen)
|
|
self.buffer.delete (-self.laststringlen)
|
|
self.buffer.insert (rep)
|
|
|
|
fn = fs
|
|
fchar137 = fs # f_
|
|
|
|
def fchar174 (self, c): # f|
|
|
"""F| command -- 'flow' to the Else part of a conditional.
|
|
Exits the condition if there isn't an else part.
|
|
"""
|
|
self.clearargs ()
|
|
self.skipcond ("|'")
|
|
|
|
def g (self, c):
|
|
"""G command -- get text from the specified Q-register and
|
|
put it in the text buffer. Print the text if colon-modified.
|
|
Special name * means the last file spec, _ means the last
|
|
search string.
|
|
"""
|
|
c = self.peeknextcmd ()
|
|
s = self.qregstr ()
|
|
if self.colon ():
|
|
sys.stdout.write (s)
|
|
sys.stdout.flush ()
|
|
self.screenok = False
|
|
else:
|
|
self.buffer.insert (s)
|
|
self.clearargs ()
|
|
|
|
def h (self, c):
|
|
"""H command -- represents the wHole buffer.
|
|
Synonym for B,Z.
|
|
"""
|
|
self.clearargs ()
|
|
self.arg2 = 0
|
|
self.setval (self.end)
|
|
|
|
def i (self,c):
|
|
"""I command -- insert a string. If no string argument is
|
|
supplied, inserts the character whose numeric value is
|
|
given as the numeric argument.
|
|
"""
|
|
n = self.getarg (c)
|
|
s = self.strarg (c)
|
|
if len (s) == 0:
|
|
if n is not None:
|
|
s = chr (n)
|
|
elif n is not None:
|
|
raise IIA (self.teco)
|
|
self.buffer.insert (s)
|
|
self.clearargs ()
|
|
|
|
def j (self, c):
|
|
"""J command -- move to the specified offset in the buffer.
|
|
"""
|
|
self.buffer.goto (self.getarg (c, 0))
|
|
|
|
def k (self, c):
|
|
"""K command -- delete n lines. If two arguments are given,
|
|
delete the range of characters between those two positions.
|
|
"""
|
|
m, n = self.teco.lineargs (c)
|
|
self.buffer.goto (m)
|
|
self.buffer.delete (n - m)
|
|
self.clearargs ()
|
|
|
|
def l (self, c):
|
|
"""L command -- move the specified number of lines.
|
|
"""
|
|
self.buffer.goto (self.buffer.line (self.getarg (c, 1)))
|
|
|
|
def m (self, c):
|
|
"""M command -- macro execution. Executes the TECO commands
|
|
in the specified Q-register. If colon-modified, the new
|
|
execution level gets its own set of local Q-registers
|
|
(ones whose names start with . ) -- otherwise the new level
|
|
shares the local Q-registers of the current level.
|
|
"""
|
|
q = self.qreg ()
|
|
if self.colon ():
|
|
i = command_level (self.teco, self.qregs)
|
|
self.clearmods ()
|
|
else:
|
|
i = command_level (self.teco)
|
|
i.run (q.getstr ())
|
|
|
|
def o (self, c):
|
|
"""O command -- go to the specified tag in the command string.
|
|
If an argument is supplied, go to that tag in the list of tags
|
|
given in the string argument, e.g., 2Ofoo,bar,baz$ goes to
|
|
tag !baz!. If the argument is out of range, execution just
|
|
continues.
|
|
"""
|
|
tag = self.strbuild (self.strarg (c))
|
|
n = self.getarg (c)
|
|
self.clearargs ()
|
|
if not len (tag):
|
|
raise ILL (self.teco, c)
|
|
if n is not None:
|
|
n -= 1
|
|
tags = tag.split (',')
|
|
if not 0 <= n < len (tags):
|
|
return
|
|
tag = tags[n]
|
|
if self.iterstack:
|
|
self.cmdpos = self.iterstack[-1].start
|
|
else:
|
|
self.cmdpos = 0
|
|
self.findtag (tag)
|
|
|
|
def p (self, c):
|
|
"""P command -- page ahead the specified number of pages
|
|
in the input file, writing to the output file in the process.
|
|
|
|
PW command -- write the current buffer to the output file.
|
|
"""
|
|
m, n, colon = self.getargsc (c, 1)
|
|
c2 = self.peeknextcmd ().lower ()
|
|
if m is not None or c2 == 'w':
|
|
if c2 == 'w':
|
|
self.nextcmd ()
|
|
if m is not None:
|
|
part = m, n
|
|
repeat = 1
|
|
else:
|
|
part = None
|
|
repeat = n
|
|
if n <= 0:
|
|
raise IPA (self.teco)
|
|
for i in range (repeat):
|
|
self.buffer.writepage (part)
|
|
else:
|
|
if n <= 0:
|
|
raise IPA (self.teco)
|
|
for i in range (n):
|
|
ret = self.buffer.page ()
|
|
if colon:
|
|
self.setval (ret)
|
|
|
|
def q (self, c):
|
|
"""Q command -- return the numeric value in the specified Q-register.
|
|
If an argument is given, return the ASCII value of the
|
|
character in the text part of the Q-register at the specified
|
|
offset (counting from zero).
|
|
"""
|
|
colon = self.colon ()
|
|
n = self.getoptarg (c)
|
|
q = self.qreg ()
|
|
if colon:
|
|
self.clearmods ()
|
|
self.setval (len (q.getstr ()))
|
|
elif n is not None:
|
|
self.clearargs ()
|
|
qstr = q.getstr ()
|
|
if 0 <= n < len (qstr):
|
|
n = ord (qstr[n])
|
|
else:
|
|
n = -1
|
|
self.setval (n)
|
|
else:
|
|
self.clearmods ()
|
|
self.setval (q.getnum ())
|
|
|
|
def r (self, c):
|
|
"""R command -- move backward by the specified number of
|
|
character positions.
|
|
"""
|
|
newpos = self.dot - self.getarg (c, 1)
|
|
if 0 <= newpos <= self.end:
|
|
self.buffer.goto (newpos)
|
|
else:
|
|
raise POP (self.teco, c)
|
|
|
|
def s (self, c):
|
|
"""S command -- search for a string. If colon modified,
|
|
return -1 if ok, 0 if no match. If :: modified, it's a
|
|
match operation rather than a search (pointer never moves).
|
|
"""
|
|
s = self.strarg (c)
|
|
colons = self.colons
|
|
topiffail = colons < 2
|
|
m, n, colon = self.getargsc (c, 1)
|
|
if not n:
|
|
raise ISA (self.teco)
|
|
if m == 0:
|
|
m = None
|
|
topiffail = False
|
|
if n < 0:
|
|
start, end = 0, self.dot
|
|
if m is not None:
|
|
start = end - abs (m)
|
|
else:
|
|
start, end = self.dot, self.end
|
|
if m is not None:
|
|
end = start + abs (m)
|
|
if colons > 1:
|
|
start = self.dot
|
|
end = start
|
|
nextpage = None
|
|
if c == "n":
|
|
nextpage = self.buffer.page
|
|
elif c == "_":
|
|
nextpage = self.y
|
|
elif c == "e_":
|
|
nextpage = self.ey
|
|
self.search (s, n, start, end, colon, topiffail, nextpage)
|
|
|
|
echar137 = s # e_
|
|
n = s
|
|
char137 = s # _
|
|
|
|
def t (self, c):
|
|
"""T command -- type the specified number of lines.
|
|
"""
|
|
m, n = self.teco.lineargs (c)
|
|
sys.stdout.write (printable (self.buf[m:n]))
|
|
sys.stdout.flush ()
|
|
self.screenok = False
|
|
self.clearargs ()
|
|
|
|
def u (self, c):
|
|
"""U command -- set the numeric part of the Q-register
|
|
to the specified value.
|
|
"""
|
|
q = self.qreg ()
|
|
q.setnum (self.getarg (c, NAU))
|
|
|
|
def v (self, c):
|
|
"""V command -- display the current line, with n lines to each
|
|
side of it if argument n is supplied, or m before and n after
|
|
if argument pair m,n is given.
|
|
"""
|
|
m, n = self.getargs (c, 1)
|
|
start = self.buffer.line (1 - (m or n))
|
|
end = self.buffer.line (n)
|
|
sys.stdout.write (printable (self.buf[start:end]))
|
|
sys.stdout.flush ()
|
|
self.teco.screenok = False
|
|
|
|
def w (self, c):
|
|
"""W command -- watch the buffer contents.
|
|
|
|
If wxPython is available, a wxPython window is opened showing
|
|
the current buffer around dot, which will be updated as the
|
|
buffer changes or dot moves, until further notice. 0W will
|
|
stop the display.
|
|
|
|
If wxPython is not available but curses is, the buffer contents
|
|
will be displayed using screen control sequences on the current
|
|
terminal. It is updated only when another W command is issued.
|
|
|
|
Also, in that mode, :W does lots of magical things; refer to
|
|
the manual for all the details.
|
|
"""
|
|
m, n, colon = self.getargsc (c)
|
|
if wxpresent:
|
|
if n is None:
|
|
self.teco.startdisplay ()
|
|
elif n == 0:
|
|
self.teco.hidedisplay ()
|
|
elif cursespresent:
|
|
if colon:
|
|
if n is None:
|
|
n = 0
|
|
if n & -256:
|
|
# insert until...
|
|
if not n & 1:
|
|
self.teco.watch ()
|
|
term = m and [ m & 255, m >> 8 ]
|
|
if n & 2:
|
|
m.append (9)
|
|
while True:
|
|
ch = screen.getch ()
|
|
if ch == 3:
|
|
if self.etflag & 32768:
|
|
self.etflag &= -32768
|
|
else:
|
|
raise XAB (self.teco)
|
|
if (n & 64) or \
|
|
(ch != 9 and ch < 32) or ch > 126 or \
|
|
(term and ch in term):
|
|
break
|
|
c = chr (ch)
|
|
if n & 4:
|
|
c = c.upper ()
|
|
self.buffer.insert (c)
|
|
if not n & 32:
|
|
self.teco.watch ()
|
|
self.setval (ch)
|
|
return
|
|
if not 0 <= n <= 7:
|
|
raise ARG (self.teco)
|
|
if m is None:
|
|
self.setval (self.teco.watchparams[n])
|
|
else:
|
|
if n:
|
|
self.teco.watchparams[n] = m
|
|
else:
|
|
if n is None:
|
|
endwin ()
|
|
else:
|
|
if n == 0:
|
|
n = 16
|
|
if n > 0:
|
|
self.teco.curline = n
|
|
else:
|
|
if n == -1000:
|
|
self.screenok = True
|
|
self.teco.watch ()
|
|
else:
|
|
raise ILL (self.teco, c)
|
|
|
|
def x (self, c):
|
|
"""X command -- set the text part of the specified Q-register
|
|
from text in the buffer. If one numeric argument is present,
|
|
that is a number of lines. If two arguments are given, it
|
|
is the range of character positions in the buffer. If colon-
|
|
modified, the new text is appended to the existing Q-register
|
|
contents rather than replacing it.
|
|
"""
|
|
colon = self.colon ()
|
|
m, n = self.teco.lineargs (c)
|
|
q = self.qreg ()
|
|
text = self.buf[m:n]
|
|
if colon:
|
|
q.appendstr (text)
|
|
else:
|
|
q.setstr (text)
|
|
self.clearargs ()
|
|
|
|
def y (self, c = None):
|
|
"""Y command -- read the next page. If the buffer is not
|
|
empty, and there is an output file, refuse the operation unless
|
|
bit 1 is set in ED.
|
|
"""
|
|
self.buffer.yank ((self.edflag & 2) == 0)
|
|
|
|
def z (self, c):
|
|
"""Z command -- the number of characters in the buffer, or in
|
|
other words, the character position corresponding to the
|
|
end of the buffer.
|
|
"""
|
|
self.setval (self.end)
|
|
|
|
def char133 (self, c): # [
|
|
"""[ command -- push the specified Q-register onto the
|
|
Q-register stack.
|
|
"""
|
|
# We have to make a copy of the Q register so that any later
|
|
# changes to the existing one are not also reflected in the
|
|
# pushed copy. A shallow copy suffices.
|
|
self.teco.qstack.append (copy.copy (self.qreg ()))
|
|
|
|
def char134 (self, c): # \
|
|
r"""\ command -- number/string conversion.
|
|
|
|
If an argument is supplied, convert that according to the
|
|
current radix, and insert it into the buffer. Note that ^S
|
|
is not updated to reflect that insertion.
|
|
|
|
If no argument is present, parse a number from the current
|
|
buffer position according to the current radix, and return that
|
|
number as a result. Dot is moved across whatever was parsed.
|
|
"""
|
|
n = self.getoptarg (c)
|
|
if n is None:
|
|
self.clearmods ()
|
|
if self.radix == 8:
|
|
m = octre.match (self.buf, self.dot)
|
|
elif self.radix == 10:
|
|
m = decre.match (self.buf, self.dot)
|
|
else:
|
|
m = hexre.match (self.buf, self.dot)
|
|
if m is None:
|
|
n = 0
|
|
else:
|
|
n = int (m.group (0), self.radix)
|
|
self.dot = m.end ()
|
|
self.setval (n)
|
|
else:
|
|
self.clearargs ()
|
|
if self.radix == 8:
|
|
s = "%o" % n
|
|
elif self.radix == 10:
|
|
s = "%d" % n
|
|
else:
|
|
s = "%x" % n
|
|
self.buffer.insert (s)
|
|
|
|
def char135 (self, c): # ]
|
|
"""] command -- pop the Q-register stack into the specified
|
|
Q-register.
|
|
"""
|
|
colon = self.colon ()
|
|
if self.teco.qstack:
|
|
q = self.teco.qstack.pop ()
|
|
self.setqreg (q)
|
|
if colon:
|
|
self.setval (-1)
|
|
elif colon:
|
|
self.nextcmd ()
|
|
self.setval (0)
|
|
else:
|
|
raise PES (self.teco)
|
|
|
|
def char136 (self, c): # ^
|
|
"""^ command -- take the next character as a control character,
|
|
for example '^S' is the same as 'control-S'.
|
|
"""
|
|
self.do (self.makecontrol (self.nextcmd ()))
|
|
|
|
def char174 (self, c): # |
|
|
"""| command -- marks the start of the 'else' part of
|
|
a conditional execution block.
|
|
"""
|
|
self.skipcond ("'")
|
|
|
|
class inputstream (object):
|
|
def __init__ (self, teco):
|
|
self.teco = teco
|
|
self.pages = ""
|
|
self.eoflag = -1
|
|
self.ffflag = 0
|
|
self.infile = False
|
|
|
|
def open (self, fn, colon):
|
|
fn = os.path.expanduser (fn)
|
|
try:
|
|
infile = open (fn, "rt", encoding = "utf8", errors = "ignore")
|
|
self.teco.lastfilename = fn
|
|
except IOError as err:
|
|
if colon:
|
|
return 0
|
|
if err.errno == 2:
|
|
raise FNF (self.teco, fn)
|
|
else:
|
|
raise FER (self.teco)
|
|
try:
|
|
indata = infile.read ()
|
|
except IOError:
|
|
raise INP (self.teco)
|
|
infile.close ()
|
|
self.pages = indata.split (ff)
|
|
self.infile = True # input file is "open"
|
|
self.infn = fn
|
|
self.eoflag = 0
|
|
return -1
|
|
|
|
def readpage (self):
|
|
if self.pages:
|
|
ret = self.pages[0].replace (lf, crlf)
|
|
del self.pages[0]
|
|
if len (self.pages):
|
|
self.ffflag = -1
|
|
self.eoflag = 0
|
|
else:
|
|
self.ffflag = 0
|
|
self.eoflag = -1
|
|
return ret, -1
|
|
else:
|
|
self.ffflag = 0
|
|
self.eoflag = -1
|
|
return "", 0
|
|
|
|
class outputstream (object):
|
|
def __init__ (self, teco):
|
|
self.teco = teco
|
|
self.outfile = None
|
|
|
|
def open (self, fn, colon, scheck = True):
|
|
if self.outfile:
|
|
raise OFO (self.teco)
|
|
fn = os.path.expanduser (fn)
|
|
if scheck and not fn.lower ().endswith (".tmp") and os.path.isfile (fn):
|
|
print('%%Superseding existing file "%s"' % fn)
|
|
fd, self.tempfn = tempfile.mkstemp (text = True)
|
|
try:
|
|
self.outfile = open (fd, "wt", encoding = "utf8", errors = "ignore")
|
|
self.teco.lastfilename = fn
|
|
except IOError as err:
|
|
if colon:
|
|
return 0
|
|
raise FER (self.teco)
|
|
self.outfn = fn
|
|
return -1
|
|
|
|
class buffer (object):
|
|
'''This class defines the TECO text buffer, and methods to manipulate
|
|
its contents.
|
|
'''
|
|
def __init__ (self, teco):
|
|
self.text = ""
|
|
self.dot = 0
|
|
self.teco = teco
|
|
self.ebflag = False
|
|
self.inputs = [ None, None ]
|
|
self.istream = 0
|
|
self.outputs = [ None, None ]
|
|
self.ostream = 0
|
|
|
|
laststringlen = commonprop ("laststringlen")
|
|
|
|
def insert (self, text):
|
|
self.text = self.text[:self.dot] + text + self.text[self.dot:]
|
|
self.dot += len (text)
|
|
self.laststringlen = -len (text)
|
|
|
|
def delete (self, len):
|
|
self.text = self.text[:self.dot] + self.text[self.dot + len:]
|
|
|
|
def goto (self, pos):
|
|
if pos < 0: pos = 0
|
|
if pos > len (self.text): pos = len (self.text)
|
|
self.dot = pos
|
|
|
|
def _end (self):
|
|
return len (self.text)
|
|
|
|
end = property (_end)
|
|
|
|
def _ffflag (self):
|
|
infile = self.inputs[self.istream]
|
|
if infile:
|
|
return infile.ffflag
|
|
else:
|
|
return 0
|
|
|
|
ffflag = property (_ffflag)
|
|
|
|
def _eoflag (self):
|
|
infile = self.inputs[self.istream]
|
|
if infile:
|
|
return infile.eoflag
|
|
else:
|
|
return -1
|
|
|
|
eoflag = property (_eoflag)
|
|
|
|
def line (self, linecnt):
|
|
pos = self.dot
|
|
if linecnt > 0:
|
|
while linecnt > 0:
|
|
try:
|
|
pos = self.text.index (lf, pos) + 1
|
|
except ValueError:
|
|
return len (self.text)
|
|
linecnt -= 1
|
|
return pos
|
|
else:
|
|
while linecnt <= 0:
|
|
try:
|
|
pos = self.text.rindex (lf, 0, pos)
|
|
except ValueError:
|
|
return 0
|
|
linecnt += 1
|
|
return pos + 1
|
|
|
|
def ea (self):
|
|
self.ostream = 1
|
|
|
|
def eb (self, fn,colon):
|
|
if self.outputs[self.ostream]:
|
|
raise OFO (self.teco)
|
|
ret = self.er (fn, colon)
|
|
if ret == -1:
|
|
ret = self.ew (fn, colon, False)
|
|
if ret == -1:
|
|
self.ebflag = True
|
|
return ret
|
|
|
|
def ec (self):
|
|
infile = self.inputs[self.istream]
|
|
outfile = self.outputs[self.ostream]
|
|
if outfile:
|
|
if infile:
|
|
while self.page () < 0:
|
|
pass
|
|
else:
|
|
self.writepage ()
|
|
self.text = ""
|
|
self.ef ()
|
|
elif self.end:
|
|
raise NFO (self.teco)
|
|
|
|
def ef (self):
|
|
infile = self.inputs[self.istream]
|
|
self.inputs[self.istream] = None
|
|
outfile = self.outputs[self.ostream]
|
|
if outfile:
|
|
if self.ebflag:
|
|
try:
|
|
os.remove (infile.infn + '~')
|
|
except:
|
|
pass
|
|
os.rename (infile.infn, infile.infn + '~')
|
|
self.ebflag = False
|
|
outfile.outfile.close ()
|
|
self.outputs[self.ostream] = None
|
|
os.rename (outfile.tempfn, outfile.outfn)
|
|
|
|
def ek (self):
|
|
outfile = self.outputs[self.ostream]
|
|
if outfile:
|
|
outfile.outfile.close ()
|
|
self.outputs[self.ostream] = None
|
|
os.remove (outfile.tempfn)
|
|
self.ebflag = False
|
|
|
|
def ep (self):
|
|
self.istream = 1
|
|
|
|
def er (self, fn, colon):
|
|
""" This opens an input file, reads the whole file, and breaks it
|
|
into pages.
|
|
I suppose that isn't really all that elegant, but unless the
|
|
file is humongous, it's fast enough these days, and it produces
|
|
the correct result.
|
|
"""
|
|
if not len (fn):
|
|
self.istream = 0
|
|
return True
|
|
if not self.inputs[self.istream]:
|
|
self.inputs[self.istream] = inputstream (self.teco)
|
|
return self.inputs[self.istream].open (fn, colon)
|
|
|
|
def ew (self, fn, colon, scheck = True):
|
|
"""This opens an output file. It creates the output file using
|
|
a temporary name, in the directory specified. The actual
|
|
desired name is saved, and will be set when the file is closed.
|
|
"""
|
|
if not len (fn):
|
|
self.ostream = 0
|
|
return True
|
|
if not self.outputs[self.ostream]:
|
|
self.outputs[self.ostream] = outputstream (self.teco)
|
|
return self.outputs[self.ostream].open (fn, colon, scheck)
|
|
|
|
def yank (self, protect = True):
|
|
"""Read another page into the text buffer. If 'protect' is
|
|
True or omitted, the operation is rejected if there is an
|
|
output file and the buffer is non-empty.
|
|
Returns -1 if there was more data to read, 0 if we were
|
|
at end of file already.
|
|
"""
|
|
if protect and self.outputs[self.ostream] and self.text:
|
|
raise YCA (self.teco)
|
|
self.text = ""
|
|
ret = self.append ()
|
|
self.goto (0)
|
|
return ret
|
|
|
|
def append (self):
|
|
"""Append another page to the text buffer.
|
|
"""
|
|
infile = self.inputs[self.istream]
|
|
if not infile:
|
|
raise NFI (self.teco)
|
|
newstr, ret = infile.readpage ()
|
|
self.text += newstr
|
|
return ret
|
|
|
|
def writepage (self, part = None):
|
|
outfile = self.outputs[self.ostream]
|
|
if not outfile:
|
|
raise NFO (self.teco)
|
|
if part:
|
|
start, end = part
|
|
outfile.outfile.write (self.text[start:end].replace (crlf, lf))
|
|
else:
|
|
outfile.outfile.write (self.text.replace (crlf, lf))
|
|
|
|
def page (self):
|
|
"""Write out the current page, and read the next.
|
|
"""
|
|
infile = self.inputs[self.istream]
|
|
outfile = self.outputs[self.ostream]
|
|
self.writepage ()
|
|
if infile and infile.ffflag:
|
|
outfile.outfile.write (ff)
|
|
return self.yank (False)
|
|
|
|
class command_handler (object):
|
|
'''Class for handling TECO input.
|
|
|
|
It handles terminal input as well as input from EI files,
|
|
either single characters, or a complete TECO command with
|
|
the usual special character processing.
|
|
'''
|
|
def __init__ (self, teco):
|
|
self.eifile = None
|
|
self.teco = teco
|
|
|
|
etflag = commonprop ("et")
|
|
|
|
def ei (self, fn, colon = False):
|
|
"""EI file opener.
|
|
|
|
If the supplied file name does not contain a directory spec,
|
|
we look for the file in several places. The list of places to
|
|
look is given by environment variable TECO_PATH, if defined,
|
|
or PATH, if that is defined, or else Python's built in
|
|
default path. The first match is used; if all choices fail
|
|
then the EI fails (with a return status of 0 if colon-modified,
|
|
or error FNF otherwise).
|
|
"""
|
|
if fn:
|
|
fn = os.path.expanduser (fn)
|
|
if self.eifile:
|
|
self.eifile.close ()
|
|
f = None
|
|
if not os.path.dirname (fn):
|
|
fn
|
|
for d in (os.environ.get("TECO_PATH",None) or
|
|
os.environ.get("PATH",None) or
|
|
os.defpath).split(os.pathsep):
|
|
realfn = os.path.join (d, fn)
|
|
try:
|
|
f = open (realfn, "r", encoding = "utf8",
|
|
errors = "ignore")
|
|
break
|
|
except IOError as err:
|
|
if err.errno == 2:
|
|
pass
|
|
else:
|
|
raise FER (self.teco)
|
|
else:
|
|
try:
|
|
f = open (fn, "r", encoding = "utf8", errors = "ignore")
|
|
realfn = fn
|
|
except IOError as err:
|
|
if err.errno == 2:
|
|
pass
|
|
else:
|
|
raise FER (self.teco)
|
|
if f:
|
|
self.eifile = f
|
|
elif colon:
|
|
return 0
|
|
else:
|
|
raise FNF (self.teco, fn)
|
|
else:
|
|
if self.eifile:
|
|
self.eifile.close ()
|
|
self.eifile = None
|
|
return -1
|
|
|
|
def getch (self, trap_ctrlc = True):
|
|
if self.eifile:
|
|
try:
|
|
c = self.eifile.read (1)
|
|
except IOError:
|
|
self.eifile = None
|
|
raise INP (self.teco)
|
|
if len (c):
|
|
if c == lf:
|
|
c = cr
|
|
return c
|
|
self.eifile.close ()
|
|
self.eifile = None
|
|
if screen and self.teco.incurses:
|
|
c = screen.getch ()
|
|
if c < 256:
|
|
c = chr (c)
|
|
else:
|
|
c = '\000'
|
|
else:
|
|
c = getch ()
|
|
if c == ctrlc and trap_ctrlc:
|
|
if self.etflag & 32768:
|
|
self.etflag &= 32767
|
|
else:
|
|
raise XAB (self.teco)
|
|
if c == cr:
|
|
sys.stdout.write (crlf)
|
|
elif c != rubchr:
|
|
sys.stdout.write (printable (c))
|
|
sys.stdout.flush ()
|
|
return c
|
|
|
|
def teco_cmdstring (self):
|
|
'''Get a command string using the TECO input conventions.
|
|
Text is accumulated until a double escape is seen. Control/U
|
|
and rubout are processed. Double control/G exits with a null
|
|
command (which will cause the main loop to prompt again).
|
|
'''
|
|
buf = ""
|
|
bellflag = False
|
|
escflag = False
|
|
immediate = True
|
|
while True:
|
|
c = self.getch (False)
|
|
if not self.eifile:
|
|
# Most special characters are only special if they
|
|
# come from the terminal, not from an EI file.
|
|
if immediate:
|
|
if c in "\010\012?":
|
|
print()
|
|
return c
|
|
elif c == '*':
|
|
c = self.getch (False)
|
|
if c.isalnum ():
|
|
print()
|
|
return '*' + c
|
|
buf = '*'
|
|
immediate = False
|
|
if c == bell:
|
|
if bellflag:
|
|
print()
|
|
return ""
|
|
bellflag = True
|
|
elif bellflag:
|
|
bellflag = False
|
|
if c == ' ':
|
|
buf = buf[:-1]
|
|
try:
|
|
start = buf.rindex (lf)
|
|
except ValueError:
|
|
start = 0
|
|
print()
|
|
sys.stdout.write (printable (buf[start:]))
|
|
sys.stdout.flush ()
|
|
continue
|
|
elif c == '*':
|
|
buf = buf[:-1]
|
|
print()
|
|
sys.stdout.write (printable (buf))
|
|
sys.stdout.flush ()
|
|
continue
|
|
if c == ctrlu:
|
|
print()
|
|
try:
|
|
ls = buf.rindex (lf)
|
|
buf = buf[:ls + 1]
|
|
except ValueError:
|
|
buf = ""
|
|
continue
|
|
elif c == rubchr:
|
|
if len (buf):
|
|
sys.stdout.write ("\010 \010")
|
|
if ord (buf[-1]) < 32 and buf[-1] != esc:
|
|
sys.stdout.write ("\010 \010")
|
|
buf = buf[:-1]
|
|
sys.stdout.flush ()
|
|
continue
|
|
if c == esc:
|
|
buf += c
|
|
if escflag:
|
|
if not self.eifile:
|
|
print()
|
|
return buf
|
|
escflag = True
|
|
continue
|
|
else:
|
|
escflag = False
|
|
if c == cr:
|
|
buf += crlf
|
|
else:
|
|
buf += c
|
|
|
|
# Here's a rather primitive (but functional) teco command handler.
|
|
# This one is used if we can't find a teco.tec anywhere.
|
|
defmacro = \
|
|
"""0ed 0^x ^d z"e @o/end/' j
|
|
::@s/tec/"s 0u1 :@s*/i*"s -1u1 @fr// <::@s/^ea/; @fr//>'
|
|
j :@s/^es/"f hk @o/end/' b,.k :@s/=/"f hx1 hk
|
|
q1"f @eb/^eq1/ y @o/end/'
|
|
@er/^eq1/ y @o/end/'
|
|
@fr// .,zx1 @er/^eq1/ b,.x1 @ew/^eq1/ hk y @o/end/'
|
|
::@s/mun/"s :@s/^es/"f @^a/?How can I MUNG nothing?
|
|
/ ^c' b,.k :@s/,/"s @fr// 1+' 0"e zj' b,.x1 b,.k @ei/^eq1/ @o/end/'
|
|
::@s/mak/"f @^a/?Illegal command "/ ht @^a/"
|
|
/ ^c' :@s/^es/"f @^a/?How can I MAKE nothing?
|
|
/ ^c' b,.k z-4"e ::@s/love/"s @^a/Not war?
|
|
/ j'' hx1 hk @ew/^eq1/'
|
|
!end!"""
|
|
|
|
def main ():
|
|
global t
|
|
t = teco ()
|
|
arg0 = os.path.basename (sys.argv[0]).lower ()
|
|
if arg0.endswith (".py"):
|
|
arg0 = arg0[:-3]
|
|
elif arg0.endswith (".pyc"):
|
|
arg0 = arg0[:-4]
|
|
cmdline = " ".join ([ arg0 ] + sys.argv[1:])
|
|
t.buf = cmdline
|
|
if wxpresent:
|
|
# Need to create a new thread for interaction, then this
|
|
# (main) thread will own the window.
|
|
thr = threading.Thread (target = main2)
|
|
thr.start ()
|
|
global display, dsem
|
|
dsem = threading.Semaphore (value = 0)
|
|
# Wait for someone to ask for a display
|
|
dsem.acquire ()
|
|
if exiting:
|
|
return
|
|
# Now start the display
|
|
display = displayApp (t)
|
|
display.start ()
|
|
else:
|
|
main2 ()
|
|
|
|
def main2 ():
|
|
#t.trace = True # *** for debug
|
|
if not t.cmdhandler.ei ("teco.tec", True):
|
|
try:
|
|
t.runcommand (defmacro)
|
|
except err as e:
|
|
e.show ()
|
|
t.mainloop ()
|
|
|
|
if __name__ == "__main__":
|
|
main ()
|