Made setuptools package
parent
764d6b5a99
commit
43b9d981a3
|
@ -0,0 +1,5 @@
|
|||
*.pyc
|
||||
*.pyo
|
||||
*.egg-info
|
||||
build
|
||||
dist
|
|
@ -0,0 +1,986 @@
|
|||
#!/usr/bin/python
|
||||
|
||||
# Written in 2015 by Christopher Pavlina.
|
||||
###############################################################################
|
||||
# lerpn - Linux Engineering RPN calculator
|
||||
#
|
||||
# This is a simple RPN calculator using Python and curses. How to use it should
|
||||
# be obvious for those familiar with RPN. The basic commands:
|
||||
#
|
||||
# + - * /
|
||||
#
|
||||
# enter dup
|
||||
# x exchange/swap
|
||||
# bksp drop
|
||||
# ? display help
|
||||
# ctrl-C exit
|
||||
#
|
||||
#
|
||||
# Numbers are displayed in engineering notation with metric prefixes. You
|
||||
# cannot TYPE them this way, however; that would cause too many collisions with
|
||||
# single-key commands IMHO.
|
||||
#
|
||||
# Not all commands are a single key. Less-used commands begin with a
|
||||
# single-quote, then the command name, then 'enter'. For example, the sequence
|
||||
# of keys used to compute the sine of 3.14 would be:
|
||||
#
|
||||
# 3 . 1 4 <enter> ' s i n <enter>
|
||||
#
|
||||
# You can tag numbers to help remember what they are. To do this, first have
|
||||
# the number at the end of the stack. Then, push a string to the stack, by
|
||||
# typing a double-quote ", then the string text, then <enter>. Then, press
|
||||
# lower-case t to combine the number and the tag.
|
||||
#
|
||||
# Basic math can be done to numbers while tagged, but if you perform a binary
|
||||
# operation on two tagged numbers, lerpn won't know which tag to keep.
|
||||
#
|
||||
# To untag a number, use upper-case T to split the number from its tag. You can
|
||||
# then drop the tag.
|
||||
#
|
||||
# This is currently the only application for strings on the stack. Lone strings
|
||||
# are displayed in green to make them obvious - otherwise, you'll be mighty
|
||||
# confused when 2 + 3 = 23 !
|
||||
#
|
||||
###############################################################################
|
||||
# ADDING COMMANDS
|
||||
#
|
||||
# Grep this source for ADDING SINGLE-LETTER COMMANDS or ADDING NAMED COMMANDS.
|
||||
#
|
||||
###############################################################################
|
||||
#
|
||||
# This software is free. Do whatever the fuck you want with it. Copy it, change
|
||||
# it, steal it, strip away this notice, print it and tear it into confetti to
|
||||
# throw at random dudes on the street. Really, I don't care.
|
||||
|
||||
import collections
|
||||
import curses
|
||||
import math
|
||||
import string
|
||||
import os
|
||||
import pipes
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
# Py2/3 bits
|
||||
if not hasattr (math, "log2"):
|
||||
math.log2 = lambda x: math.log(x, 2)
|
||||
|
||||
# Hack warning!
|
||||
# Pint takes too long to load for a general-purpose calculator that has to come
|
||||
# up quickly. Since it's not needed until after the first line has been
|
||||
# entered, I'm throwing up a loader in a thread. Blame nickjohnson if it's bad
|
||||
import threading
|
||||
pint_load_event = threading.Event()
|
||||
pint=None
|
||||
UREG=None
|
||||
def pintloader():
|
||||
global pint, _num_parser, UREG, _Q_
|
||||
try:
|
||||
import pint
|
||||
UREG = pint.UnitRegistry ()
|
||||
UREG.autoconvert_offset_to_baseunit = True
|
||||
def _num_parser(x):
|
||||
# Do not return int!
|
||||
ret = UREG.parse_expression (x)
|
||||
if isinstance (ret, int):
|
||||
return float (ret)
|
||||
else:
|
||||
return ret
|
||||
def _Q_(x):
|
||||
return UREG.parse_expression (x)
|
||||
pint_load_event.set ()
|
||||
except ImportError:
|
||||
pint = None
|
||||
_num_parser = float
|
||||
def _Q_(x):
|
||||
return float (x.partition(" ")[0])
|
||||
pint_load_event.set ()
|
||||
|
||||
def num_parser(x):
|
||||
pint_load_event.wait()
|
||||
if '"' in x:
|
||||
x, delim, tag = x.partition('"')
|
||||
else:
|
||||
tag = None
|
||||
parsed_num = _num_parser(x)
|
||||
if tag is None:
|
||||
return parsed_num
|
||||
else:
|
||||
return Tagged (parsed_num, tag.strip())
|
||||
|
||||
def Q_(*args, **kwargs):
|
||||
pint_load_event.wait()
|
||||
return _Q_(*args, **kwargs)
|
||||
|
||||
pL = threading.Thread(target=pintloader)
|
||||
pL.start()
|
||||
|
||||
# Pyperclip is optional
|
||||
try:
|
||||
import pyperclip
|
||||
except ImportError:
|
||||
pyperclip = None
|
||||
|
||||
FORMAT="eng"
|
||||
SIGFIGS=7
|
||||
ERROR=None
|
||||
|
||||
class UndoStack (list):
|
||||
"""Stack supporting an 'undo' action.
|
||||
This is used as the main RPN stack. You can append() and pop() and [] just
|
||||
like with a normal list. You can also use undopush() to push a duplicate of
|
||||
the stack itself onto a "stack of stacks", and then undo() to restore that.
|
||||
"""
|
||||
|
||||
def __init__ (self, v=None):
|
||||
"""Initialize an UndoStack from an optional source list.
|
||||
"""
|
||||
if v is None:
|
||||
list.__init__(self)
|
||||
self.__sos = []
|
||||
|
||||
else:
|
||||
list.__init__(self, v)
|
||||
self.__sos = []
|
||||
|
||||
def undopush (self):
|
||||
"""Save the current stack state to be restored later with undo()
|
||||
"""
|
||||
self.__sos.append (self[:])
|
||||
|
||||
def undo (self):
|
||||
"""Restore the last saved undo state
|
||||
"""
|
||||
if len (self.__sos) > 1:
|
||||
self[:] = self.__sos.pop ()
|
||||
else:
|
||||
self[:] = []
|
||||
|
||||
class Tagged (object):
|
||||
"""Tagged number object.
|
||||
This behaves like a number, but also contains a string tag. The values
|
||||
are accessible at .num and .tag
|
||||
"""
|
||||
def __init__ (self, num, tag):
|
||||
self.num = num
|
||||
self.tag = tag
|
||||
|
||||
# Add operations to Tagged
|
||||
ops = [
|
||||
("add", lambda x,y: x+y),
|
||||
("sub", lambda x,y: x-y),
|
||||
("mul", lambda x,y: x*y),
|
||||
("truediv", lambda x,y: x/y),
|
||||
("mod", lambda x,y: x%y),
|
||||
("pow", lambda x,y: x**y),
|
||||
]
|
||||
for opname, opfunc in ops:
|
||||
# For the uninitiated, the default-argument parameters create a new
|
||||
# scope for the variable, allowing passing each loop iteration's value
|
||||
# to the closure instead of closing around the single final value.
|
||||
def method (self, other, opfunc=opfunc):
|
||||
if isinstance (other, Tagged):
|
||||
# If both are tagged, just remove the tag.
|
||||
return opfunc (self.num, other.num)
|
||||
if not isinstance (other, float): return NotImplemented
|
||||
return Tagged (opfunc (self.num, other), self.tag)
|
||||
def rmethod (self, other, opfunc=opfunc):
|
||||
if isinstance (other, Tagged):
|
||||
# If both are tagged, just remove the tag.
|
||||
return opfunc (other.num, self.num)
|
||||
if not isinstance (other, float): return NotImplemented
|
||||
return Tagged (opfunc (other, self.num), self.tag)
|
||||
setattr (Tagged, "__%s__" % opname, method)
|
||||
setattr (Tagged, "__r%s__" % opname, rmethod)
|
||||
|
||||
class CursesScrollBox (object):
|
||||
"""Displays some text in a scroll box."""
|
||||
def __init__ (self, width, height):
|
||||
"""Initialize.
|
||||
|
||||
@param width - desired width: positive for absolute width, negative
|
||||
for margin around window edge
|
||||
@param height - desired height: positive for absolute height,
|
||||
negative for margin around window edge
|
||||
"""
|
||||
self.w = width
|
||||
self.h = height
|
||||
self.title = ""
|
||||
self.pos = 0
|
||||
|
||||
def set_text (self, text):
|
||||
self.text = text
|
||||
|
||||
def set_title (self, title):
|
||||
self.title = title
|
||||
|
||||
def show (self):
|
||||
if self.w < 0:
|
||||
self._w = curses.COLS + self.w
|
||||
else:
|
||||
self._w = self.w
|
||||
if self.h < 0:
|
||||
self._h = curses.LINES + self.h
|
||||
else:
|
||||
self._h = self.h
|
||||
self._x = curses.COLS // 2 - self._w // 2
|
||||
self._y = curses.LINES // 2 - self._h // 2
|
||||
|
||||
curses.curs_set (0)
|
||||
curses.doupdate ()
|
||||
self.win = curses.newwin (self._h, self._w, self._y, self._x)
|
||||
|
||||
while self._show ():
|
||||
pass
|
||||
|
||||
curses.curs_set (1)
|
||||
|
||||
def _show (self):
|
||||
self.win.clear ()
|
||||
self.win.move (1, 2)
|
||||
self.win.addstr (self.title)
|
||||
self.win.keypad (1)
|
||||
|
||||
if isinstance(self.text, str):
|
||||
lines = self.text.split("\n")
|
||||
elif isinstance(self.text, list):
|
||||
lines = self.text
|
||||
else:
|
||||
raise TypeError ("Text must be list or str")
|
||||
view_lines = lines[self.pos:self.pos+self._h - 4]
|
||||
|
||||
for y, line in enumerate (view_lines):
|
||||
self.win.move (y + 3, 2)
|
||||
self.win.addstr (line.rstrip())
|
||||
|
||||
if self.pos > 0:
|
||||
self.win.move (2, self._w - 3)
|
||||
self.win.addstr ("+")
|
||||
if self.pos + self._h < len (lines) + 4:
|
||||
self.win.move (self._h - 2, self._w - 3)
|
||||
self.win.addstr ("+")
|
||||
|
||||
scroll_limit = len (lines) - self._h // 2
|
||||
|
||||
self.win.border ()
|
||||
|
||||
try:
|
||||
key = self.win.getch ()
|
||||
except KeyboardInterrupt:
|
||||
return False
|
||||
if key == curses.KEY_UP:
|
||||
self.pos = max(self.pos - 1, 0)
|
||||
elif key == curses.KEY_DOWN:
|
||||
self.pos = min(self.pos + 1, scroll_limit)
|
||||
elif key == curses.KEY_PPAGE:
|
||||
self.pos = max(self.pos - 10, 0)
|
||||
elif key == curses.KEY_NPAGE:
|
||||
self.pos = min(self.pos + 10, scroll_limit)
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
LUT = {
|
||||
-24: "y",
|
||||
-21: "z",
|
||||
-18: "a",
|
||||
-15: "f",
|
||||
-12: "p",
|
||||
-9: "n",
|
||||
-6: u"\xb5",
|
||||
-3: "m",
|
||||
0: "",
|
||||
3: "k",
|
||||
6: "M",
|
||||
9: "G",
|
||||
12: "T",
|
||||
15: "P",
|
||||
18: "E",
|
||||
21: "Z",
|
||||
24: "Y"
|
||||
}
|
||||
def eng(x, sigfigs=7):
|
||||
"""Return x in engineering notation"""
|
||||
if x == 0.0:
|
||||
return "0"
|
||||
elif x < 0.0:
|
||||
return '-'+eng (-x, sigfigs)
|
||||
m = math.floor(math.log10(x)/3)*3
|
||||
coeff = "{0:.{1}g}".format (x * 10.0 ** (-m), sigfigs)
|
||||
lut = LUT.get (m, None)
|
||||
if lut is None:
|
||||
return coeff + "e" + str (m)
|
||||
if lut != '': lut = '_' + lut
|
||||
#if '.' in coeff and lut != '':
|
||||
# return coeff.replace ('.', lut)
|
||||
#else:
|
||||
# return coeff + lut
|
||||
return (coeff + lut)
|
||||
|
||||
def RPN_drawstack (stack, window):
|
||||
"""Draw the stack onto a curses window.
|
||||
"""
|
||||
for i, item in enumerate (stack):
|
||||
row = curses.LINES - len (stack) - 2 + i
|
||||
if row < 1:
|
||||
continue
|
||||
|
||||
tag = None
|
||||
mode = 0
|
||||
|
||||
if isinstance (item, Tagged):
|
||||
tag = item.tag
|
||||
item = item.num
|
||||
|
||||
if hasattr (item, "magnitude"):
|
||||
# TODO: HACK
|
||||
# pint will add a way to format just the abbreviated unit in 0.7.
|
||||
# For now, I'll format a known magnitude and then remove the known
|
||||
# magnitude
|
||||
|
||||
one_magnitude_unit = format (UREG.Quantity (1, item.units), "~")
|
||||
assert one_magnitude_unit.startswith ("1 ")
|
||||
units_abbrev = one_magnitude_unit[2:]
|
||||
|
||||
if FORMAT == "eng":
|
||||
formatted = eng (item.magnitude, SIGFIGS) + " " + units_abbrev
|
||||
else:
|
||||
formatted = "%e %s" % (item.magnitude, units_abbrev)
|
||||
|
||||
elif isinstance (item, float):
|
||||
if FORMAT == "eng":
|
||||
formatted = eng (item, SIGFIGS)
|
||||
else:
|
||||
formatted = "%e" % item
|
||||
|
||||
elif isinstance (item, str):
|
||||
formatted = item
|
||||
mode = curses.color_pair(1)
|
||||
|
||||
else:
|
||||
formatted = repr(item)
|
||||
|
||||
if tag is None:
|
||||
window.addstr (row, 0, "%3d %20s" % (i, formatted), mode)
|
||||
else:
|
||||
window.addstr (row, 0, "%3d %20s : %s" % (i, formatted, tag), mode)
|
||||
|
||||
curses.doupdate ()
|
||||
|
||||
def do_resize (window):
|
||||
"""Handles redrawing the window if it has been resized.
|
||||
"""
|
||||
resize = curses.is_term_resized (curses.LINES, curses.COLS)
|
||||
if resize:
|
||||
y, x = window.getmaxyx ()
|
||||
window.clear ()
|
||||
curses.resizeterm (y, x)
|
||||
curses.doupdate ()
|
||||
|
||||
################################################################################
|
||||
# Command definitions
|
||||
|
||||
def BINARY (f):
|
||||
"""Create a binary operator from a lambda function."""
|
||||
def cmd (stack, args=None):
|
||||
y = stack.pop ()
|
||||
x = stack.pop ()
|
||||
stack.append (f (x, y))
|
||||
return cmd
|
||||
|
||||
def UNARY (f):
|
||||
"""Create a unary operator from a lambda function.
|
||||
The unary operators know how to handle tagged numbers - they reapply the
|
||||
same tag after performing the operation."""
|
||||
def cmd (stack, args=None):
|
||||
x = stack.pop ()
|
||||
if isinstance(x, Tagged):
|
||||
newtagged = Tagged(f(x.num), x.tag)
|
||||
stack.append (newtagged)
|
||||
else:
|
||||
stack.append (f (x))
|
||||
return cmd
|
||||
|
||||
def DEGREE_TRIG (f):
|
||||
"""Create a unary trig operator that accepts degrees.
|
||||
|
||||
This has to be handled specially because pint supports degrees/radians,
|
||||
performing the conversion automatically. We have to avoid double-converting,
|
||||
and also work in the absence of pint.
|
||||
"""
|
||||
def g(x):
|
||||
if hasattr (x, "magnitude"):
|
||||
x_deg = x.to (UREG.degree)
|
||||
else:
|
||||
x_deg = x * math.pi / 180
|
||||
return f (x_deg)
|
||||
return UNARY (g)
|
||||
|
||||
def DEGREE_INVTRIG (f):
|
||||
"""Create a unary inverse trig operator that returns degrees.
|
||||
|
||||
This returns a pint object if pint is installed.
|
||||
"""
|
||||
if pint is not None:
|
||||
def g(x):
|
||||
return UREG.Quantity (f (x), UREG.radian).to (UREG.degree)
|
||||
else:
|
||||
def g(x):
|
||||
return f (x) * 180 / math.pi
|
||||
return UNARY (g)
|
||||
|
||||
def _tag (stack, args=None):
|
||||
"""Pop a string and then a number from the stack, then create a tagged
|
||||
number."""
|
||||
tag = stack.pop ()
|
||||
num = stack.pop ()
|
||||
if not isinstance(tag, str):
|
||||
raise TypeError ("to create tagged number, expect Y: number, X: string")
|
||||
elif not isinstance(num, float) and not hasattr(num, "magnitude"):
|
||||
raise TypeError ("to create tagged number, expect Y: number, X: string ")
|
||||
stack.append (Tagged (num, tag))
|
||||
|
||||
def _untag (stack, args=None):
|
||||
"""Pop a tagged number, then push back the original number and tag."""
|
||||
tagged = stack.pop ()
|
||||
if not isinstance(tagged, Tagged):
|
||||
stack.append (tagged)
|
||||
else:
|
||||
stack.append (tagged.num)
|
||||
|
||||
def _drop (stack, args=None):
|
||||
stack.pop ()
|
||||
def _dup (stack, args=None):
|
||||
if len (stack):
|
||||
stack.append (stack[-1])
|
||||
def _undo (stack, args=None):
|
||||
# Have to run twice, because undopush was called right before this
|
||||
stack.undo ()
|
||||
stack.undo ()
|
||||
def _xchg (stack, args=None):
|
||||
a = stack.pop ()
|
||||
b = stack.pop ()
|
||||
stack.append (a)
|
||||
stack.append (b)
|
||||
def _get (stack, args=None):
|
||||
"""Grab an element from the stack by index and dup it onto the end"""
|
||||
n = stack.pop ()
|
||||
if n >= 0 and n < len (stack):
|
||||
stack.append (stack [int (n)])
|
||||
else:
|
||||
stack.append (0.)
|
||||
|
||||
def _help (stack, args=None):
|
||||
"""Display the command helptexts"""
|
||||
scrollbox = CursesScrollBox (-4, -4)
|
||||
scrollbox.set_title ("HELP :: up/down/pgup/pgdown; other=close")
|
||||
|
||||
lines_left = []
|
||||
lines_right = []
|
||||
|
||||
for key in COMMANDS.keys():
|
||||
val = COMMANDS[key]
|
||||
if isinstance(key, int):
|
||||
lines_left.append (" " * 26)
|
||||
lines_left.append ("%-26s" % val)
|
||||
else:
|
||||
lines_left.append ("%-10s %-15s" % (key, val[1]))
|
||||
|
||||
for key in SINGLE_LETTER_COMMANDS.keys():
|
||||
if not isinstance (key, str):
|
||||
continue
|
||||
if key[0] not in string.digits+string.ascii_letters+string.punctuation:
|
||||
continue
|
||||
val = SINGLE_LETTER_COMMANDS[key]
|
||||
lines_right.append ("%-10s %-15s" % (key, val[1]))
|
||||
|
||||
if len (lines_left) < len (lines_right):
|
||||
lines_left.extend ([""] * (len (lines_right) - len (lines_left)))
|
||||
if len (lines_left) > len (lines_right):
|
||||
lines_right.extend ([""] * (len (lines_left) - len (lines_right)))
|
||||
|
||||
lines = [i + " " + j for i,j in zip(lines_left, lines_right)]
|
||||
scrollbox.set_text (lines)
|
||||
|
||||
scrollbox.show ()
|
||||
|
||||
def _exc (stack, args=None):
|
||||
"""Display the latest exception"""
|
||||
if ERROR is None:
|
||||
return
|
||||
s = traceback.format_tb (ERROR[2])
|
||||
scrollbox = CursesScrollBox (-4, -4)
|
||||
scrollbox.set_title ("LAST BACKTRACE")
|
||||
scrollbox.set_text (s)
|
||||
scrollbox.show ()
|
||||
|
||||
|
||||
def _eng (stack, args=None):
|
||||
global FORMAT, SIGFIGS
|
||||
FORMAT = "eng"
|
||||
|
||||
if args is not None and len(args) == 1:
|
||||
SIGFIGS = int(args[0])
|
||||
else:
|
||||
SIGFIGS = 7
|
||||
|
||||
def _sci (stack, args=None):
|
||||
global FORMAT
|
||||
FORMAT = "sci"
|
||||
|
||||
def _copy (stack, args=None):
|
||||
|
||||
item = stack[-1]
|
||||
if isinstance(item, str):
|
||||
fmt = item
|
||||
elif isinstance(item, float):
|
||||
fmt = str(item)
|
||||
|
||||
elif isinstance(item, Tagged):
|
||||
fmt = str(item.num)
|
||||
|
||||
if pyperclip is not None:
|
||||
pyperclip.copy (fmt)
|
||||
else:
|
||||
xclip = subprocess.Popen (["xclip", "-selection", "CLIPBOARD"], stdin=subprocess.PIPE)
|
||||
xclip.stdin.write (fmt.encode('utf8'))
|
||||
xclip.stdin.close ()
|
||||
xclip.wait ()
|
||||
|
||||
def _paste (stack, args=None):
|
||||
if pyperclip is not None:
|
||||
item = float (pyperclip.paste ())
|
||||
stack.append (item)
|
||||
|
||||
else:
|
||||
clipbd = subprocess.check_output (["xclip", "-selection", "CLIPBOARD", "-o"])
|
||||
item = float(clipbd)
|
||||
stack.append (item)
|
||||
|
||||
def _convert (stack, args=None):
|
||||
if args is not None:
|
||||
units = UREG.parse_units (args)
|
||||
value = stack.pop ()
|
||||
else:
|
||||
units = stack.pop ().units
|
||||
value = stack.pop ()
|
||||
if isinstance (value, Tagged):
|
||||
tag = value.tag
|
||||
value = value.num
|
||||
else:
|
||||
tag = None
|
||||
converted = value.to (units)
|
||||
if tag is not None:
|
||||
converted = Tagged (converted, tag)
|
||||
stack.append (converted)
|
||||
|
||||
def _sh (stack, arg):
|
||||
|
||||
newenv = dict (os.environ)
|
||||
if not stack:
|
||||
newenv["N"] = "0"
|
||||
newenv["F"] = "0"
|
||||
else:
|
||||
num = stack[-1]
|
||||
if isinstance(num, Tagged):
|
||||
num = num.num
|
||||
if isinstance(num, float):
|
||||
newenv["N"] = "%g" % num
|
||||
newenv["F"] = "%g" % num
|
||||
else:
|
||||
newenv["N"] = "%g" % num.magnitude
|
||||
newenv["F"] = "{:g~}".format (num)
|
||||
|
||||
try:
|
||||
p = subprocess.Popen (arg, shell=True, env=newenv, stdout=subprocess.PIPE)
|
||||
data = p.stdout.read ().decode ("utf8")
|
||||
p.wait ()
|
||||
except KeyboardInterrupt:
|
||||
data = ""
|
||||
|
||||
FLOAT_RE = r"[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?"
|
||||
match = re.search (FLOAT_RE, data)
|
||||
if match is not None:
|
||||
try:
|
||||
stack.append (float (match.group(0)))
|
||||
except ValueError as e:
|
||||
pass
|
||||
|
||||
scrollbox = CursesScrollBox (-4, -4)
|
||||
scrollbox.set_title ("COMMAND OUTPUT")
|
||||
scrollbox.set_text (data)
|
||||
scrollbox.show ()
|
||||
|
||||
def _preferred (stack, arg):
|
||||
|
||||
U = UREG.parse_units
|
||||
PREFERRED = [
|
||||
U("V"), U("V/m"),
|
||||
U("ohm"), U("farad"), U("henry"),
|
||||
U("W"), U("J"),
|
||||
U("s"), U("Hz"), U("m"), U("g"),
|
||||
]
|
||||
value = stack.pop ()
|
||||
if isinstance (value, Tagged):
|
||||
tag = value.tag
|
||||
value = value.num
|
||||
else:
|
||||
tag = None
|
||||
|
||||
value_base = value.to_base_units ()
|
||||
dest_unit = None
|
||||
|
||||
for dest in PREFERRED:
|
||||
src = UREG.Quantity(1., dest).to_base_units().units
|
||||
if src == value_base.units:
|
||||
dest_unit = dest
|
||||
break
|
||||
|
||||
if dest_unit is None:
|
||||
converted = value
|
||||
else:
|
||||
converted = value.to (dest_unit)
|
||||
|
||||
if tag is not None:
|
||||
converted = Tagged (converted, tag)
|
||||
stack.append (converted)
|
||||
|
||||
################################################################################
|
||||
# ADDING SINGLE-LETTER COMMANDS
|
||||
#
|
||||
# Insert a compound tuple into the OrderedDict below:
|
||||
# (key, (operator, helptext))
|
||||
#
|
||||
# key: the key returned by curses. For printables, just the character.
|
||||
# operator: a function that takes the current UndoStack, operates on it,
|
||||
# and returns nothing. Any exceptions thrown in here will be
|
||||
# caught and handled, so don't worry about those.
|
||||
# helptext: a string to display on the help screen.
|
||||
#
|
||||
#
|
||||
# Standard unary and binary operators can be made with UNARY() and BINARY(),
|
||||
# which accept a lambda function taking one or two arguments and return an
|
||||
# operator closure around it.
|
||||
|
||||
SINGLE_LETTER_COMMANDS = collections.OrderedDict([
|
||||
("\x7f", (_drop, "drop")),
|
||||
("\x08", (_drop, "drop")),
|
||||
(curses.KEY_BACKSPACE, (_drop, "drop")),
|
||||
|
||||
("\r", (_dup, "dup")),
|
||||
("\n", (_dup, "dup")),
|
||||
(curses.KEY_ENTER, (_dup, "dup")),
|
||||
|
||||
("-", (BINARY (lambda x, y: x - y), "subtract")),
|
||||
("+", (BINARY (lambda x, y: x + y), "add")),
|
||||
("*", (BINARY (lambda x, y: x * y), "multiply")),
|
||||
("/", (BINARY (lambda x, y: x / y), "divide")),
|
||||
("^", (BINARY (lambda x, y: x ** y), "power")),
|
||||
("e", (UNARY (math.exp), "exponential")),
|
||||
("r", (UNARY (math.sqrt), "sq root")),
|
||||
("i", (UNARY (lambda x: 1/x), "reciprocal")),
|
||||
|
||||
("u", (_undo, "undo")),
|
||||
("x", (_xchg, "exchange")),
|
||||
("[", (_get, "dup item by number")),
|
||||
|
||||
("t", (_tag, "attach tag to number")),
|
||||
("T", (_untag, "unTag")),
|
||||
("U", (UNARY (lambda x: x.magnitude), "unUnit")),
|
||||
|
||||
("c", (_copy, "copy to clipboard")),
|
||||
("v", (_paste, "paste from clipboard")),
|
||||
|
||||
("?", (_help, "display help")),
|
||||
])
|
||||
|
||||
################################################################################
|
||||
# ADDING NAMED COMMANDS
|
||||
#
|
||||
# Insert a compound tuple into the OrderedDict below:
|
||||
# (name, (operator, helptext))
|
||||
#
|
||||
# name: the full command name, including beginning single-quote
|
||||
# operator: a function that takes the current UndoStack, operates on it,
|
||||
# and returns nothing. Any exceptions thrown in here will be
|
||||
# caught and handled, so don't worry about those.
|
||||
# helptext: a string to display on the help screen.
|
||||
#
|
||||
#
|
||||
# Standard unary and binary operators can be made with UNARY() and BINARY(),
|
||||
# which accept a lambda function taking one or two arguments and return an
|
||||
# operator closure around it.
|
||||
#
|
||||
#
|
||||
# You can insert section headers into the help text by adding a compound tuple:
|
||||
# (id, header)
|
||||
#
|
||||
# id: any unique integer
|
||||
# header: header text
|
||||
|
||||
COMMANDS = collections.OrderedDict([
|
||||
(1, "==TRIGONOMETRY, RAD=="),
|
||||
("'sin", (UNARY (math.sin), "sin(rad)")),
|
||||
("'cos", (UNARY (math.cos), "cos(rad)")),
|
||||
("'tan", (UNARY (math.tan), "tan(rad)")),
|
||||
("'asin", (UNARY (lambda x: math.asin(x) * Q_("1 rad")), "asin->rad")),
|
||||
("'acos", (UNARY (lambda x: math.acos(x) * Q_("1 rad")), "acos->rad")),
|
||||
("'atan", (UNARY (lambda x: math.atan(x) * Q_("1 rad")), "atan->rad")),
|
||||
(2, "==TRIGONOMETRY, DEG=="),
|
||||
("'sind", (DEGREE_TRIG (math.sin), "sin(deg)")),
|
||||
("'cosd", (DEGREE_TRIG (math.cos), "cos(deg)")),
|
||||
("'tand", (DEGREE_TRIG (math.tan), "tan(deg)")),
|
||||
("'asind", (DEGREE_INVTRIG (math.asin), "asin->deg")),
|
||||
("'acosd", (DEGREE_INVTRIG (math.acos), "acos->deg")),
|
||||
("'atand", (DEGREE_INVTRIG (math.atan), "atan->deg")),
|
||||
("'deg", (UNARY (math.degrees), "rad->deg")),
|
||||
("'rad", (UNARY (math.radians), "deg->rad")),
|
||||
(3, "==HYPERBOLICS=="),
|
||||
("'sinh", (UNARY (math.sinh), "sinh")),
|
||||
("'cosh", (UNARY (math.cosh), "cosh")),
|
||||
("'tanh", (UNARY (math.tanh), "tanh")),
|
||||
("'asinh", (UNARY (math.asinh), "asinh")),
|
||||
("'acosh", (UNARY (math.acosh), "acosh")),
|
||||
("'atanh", (UNARY (math.atanh), "atanh")),
|
||||
(4, "==LOGARITHMS=="),
|
||||
("'log", (UNARY (math.log), "log base e")),
|
||||
("'log10", (UNARY (math.log10), "log base 10")),
|
||||
("'log2", (UNARY (math.log2), "log base 2")),
|
||||
(7, "==UNITS=="),
|
||||
("'mag", (UNARY (lambda x: x.magnitude), "return magnitude without unit")),
|
||||
("'unity", (UNARY (lambda x: UREG.Quantity(1., x.units)), "return 1.0*unit")),
|
||||
("'base", (UNARY (lambda x: x.to_base_units()), "convert to base units")),
|
||||
("'pu", (_preferred, "convert to preferred units")),
|
||||
("'conv", (_convert, "convert Y to units of X, or X to units of command argument")),
|
||||
(5, "==CONSTANTS=="),
|
||||
("'pi", (lambda stack,a=None: stack.append (math.pi), "const PI")),
|
||||
("'e", (lambda stack,a=None: stack.append (math.e), "const E")),
|
||||
("'c", (lambda stack,a=None: stack.append (Q_("2.99792458e8 m/s")), "speed of light, m/s")),
|
||||
("'h", (lambda stack,a=None: stack.append (Q_("6.6260755e-34 J S")), "Planck constant, J s")),
|
||||
("'k", (lambda stack,a=None: stack.append (Q_("1.380658e-23 J/K")), "Boltzmann constant, J/K")),
|
||||
("'elec", (lambda stack,a=None: stack.append (Q_("1.60217733e-19 C")), "Charge of electron, C")),
|
||||
("'e0", (lambda stack,a=None: stack.append (Q_("8.854187817e-12 F/m")), "Permittivity of vacuum, F/m")),
|
||||
("'amu", (lambda stack,a=None: stack.append (Q_("1.6605402e-27 kg")), "Atomic mass unit, kg")),
|
||||
("'Na", (lambda stack,a=None: stack.append (Q_("6.0221367e23 1/mol")), "Avogadro's number, mol^-1")),
|
||||
("'atm", (lambda stack,a=None: stack.append (Q_("101325. Pa")), "Standard atmosphere, Pa")),
|
||||
(6, "==LERPN=="),
|
||||
("'sh", (_sh, "run shell command, $N = magnitude, $F = full number with unit")),
|
||||
("'help", (_help, "display help")),
|
||||
("'exc", (_exc, "display latest exception backtrace")),
|
||||
("'eng", (_eng, "engineering mode, sig figs = 7 or command argument")),
|
||||
("'sci", (_sci, "scientific mode")),
|
||||
])
|
||||
|
||||
def get_single_key_command (key):
|
||||
"""
|
||||
Return a single-key command for the key, or None.
|
||||
"""
|
||||
if key > 127:
|
||||
lookup_key = key
|
||||
else:
|
||||
lookup_key = chr (key)
|
||||
return SINGLE_LETTER_COMMANDS.get (lookup_key, None)
|
||||
|
||||
def do_command (stack, cmd):
|
||||
try:
|
||||
stack.undopush ()
|
||||
cmd[0] (stack)
|
||||
except Exception:
|
||||
stack.undo ()
|
||||
return sys.exc_info ()
|
||||
else:
|
||||
return None
|
||||
|
||||
ENTER_KEYS = [curses.KEY_ENTER, 10, 13]
|
||||
BACKSPACE_KEYS = [curses.KEY_BACKSPACE, 127, 8]
|
||||
|
||||
def do_edit (key, strbuf):
|
||||
"""
|
||||
Line-edit.
|
||||
Returns "BACKSPACE", "ENTER", or None
|
||||
"""
|
||||
|
||||
if key in BACKSPACE_KEYS:
|
||||
if len (strbuf):
|
||||
del strbuf[-1]
|
||||
return "BACKSPACE"
|
||||
|
||||
if key in ENTER_KEYS:
|
||||
return "ENTER"
|
||||
|
||||
try:
|
||||
strbuf.append (chr (key))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def RPN_prompt (stack, error, window):
|
||||
"""Displays the prompt.
|
||||
stack: buffer current UndoStack
|
||||
error: any error text returned from the _previous_ RPN_prompt, or None
|
||||
window: curses window
|
||||
|
||||
returns any error text from the command, or None.
|
||||
"""
|
||||
|
||||
if error is not None:
|
||||
global ERROR
|
||||
ERROR = error
|
||||
window.addstr (0, 0, "ERROR: %s" % str (error[1]))
|
||||
|
||||
STATES = [
|
||||
"FIRSTCHAR", # first character determines the mode
|
||||
"NUMERIC", # parsing numeric
|
||||
"NUMUNIT", # parsing unit inside numeric
|
||||
"COMMAND", # parsing command
|
||||
"STRING", # parsing string
|
||||
]
|
||||
state = "FIRSTCHAR"
|
||||
strbuf = []
|
||||
|
||||
while True:
|
||||
assert state in STATES
|
||||
RPN_drawstack (stack, window)
|
||||
window.move (curses.LINES - 1, 0)
|
||||
window.addstr ("? ")
|
||||
for i in strbuf:
|
||||
window.addstr (i)
|
||||
window.clrtoeol ()
|
||||
curses.doupdate ()
|
||||
|
||||
try:
|
||||
key = window.getch ()
|
||||
except KeyboardInterrupt:
|
||||
sys.exit (0)
|
||||
try:
|
||||
char = chr (key)
|
||||
except ValueError:
|
||||
char = None
|
||||
|
||||
if state == "FIRSTCHAR":
|
||||
|
||||
# Try single-key commands first
|
||||
cmd = get_single_key_command (key)
|
||||
if cmd is not None:
|
||||
return do_command (stack, cmd)
|
||||
|
||||
if char is None:
|
||||
continue
|
||||
|
||||
if char in string.digits + "_.":
|
||||
strbuf.append (char)
|
||||
state = "NUMERIC"
|
||||
|
||||
if char == "'":
|
||||
strbuf.append (char)
|
||||
state = "COMMAND"
|
||||
|
||||
elif char == '"':
|
||||
strbuf.append (char)
|
||||
state = "STRING"
|
||||
|
||||
elif state == "NUMERIC":
|
||||
|
||||
cmd = get_single_key_command (key)
|
||||
|
||||
# Make exception for 'e' so that we can type sci notation
|
||||
if char == "e" or char == "E" or (("e" in strbuf or "E" in strbuf) and char == "-"):
|
||||
cmd = None
|
||||
|
||||
edit_type = do_edit (key, strbuf)
|
||||
if edit_type == "ENTER" or (cmd is not None and edit_type != "BACKSPACE"):
|
||||
if cmd is not None and strbuf and edit_type != "ENTER":
|
||||
# Do not include command in number
|
||||
del strbuf[-1]
|
||||
try:
|
||||
if strbuf[0] == "_":
|
||||
strbuf[0] = "-"
|
||||
f = num_parser (''.join(strbuf))
|
||||
except Exception:
|
||||
return sys.exc_info ()
|
||||
else:
|
||||
stack.append (f)
|
||||
|
||||
if edit_type != "ENTER" and cmd is not None:
|
||||
return do_command (stack, cmd)
|
||||
else:
|
||||
return None
|
||||
|
||||
if strbuf and strbuf[-1] == " ":
|
||||
state = "NUMUNIT"
|
||||
|
||||
if not strbuf:
|
||||
state = "FIRSTCHAR"
|
||||
|
||||
if char is None:
|
||||
continue
|
||||
|
||||
elif state == "NUMUNIT":
|
||||
# Like numeric, but since units can contain letters that have
|
||||
# "single-unit commands" in them, ignore those.
|
||||
|
||||
if do_edit (key, strbuf) == "ENTER":
|
||||
try:
|
||||
if strbuf[0] == "_":
|
||||
strbuf[0] = "-"
|
||||
f = num_parser (''.join(strbuf))
|
||||
except Exception:
|
||||
return sys.exc_info ()
|
||||
else:
|
||||
stack.append (f)
|
||||
return
|
||||
|
||||
if not strbuf:
|
||||
state = "FIRSTCHAR"
|
||||
|
||||
elif state == "COMMAND":
|
||||
|
||||
if do_edit (key, strbuf) == "ENTER":
|
||||
command_string = ''.join (strbuf)
|
||||
cmd_and_args = command_string.partition(" ")
|
||||
cmd = COMMANDS.get (cmd_and_args[0], None)
|
||||
if cmd is not None:
|
||||
try:
|
||||
stack.undopush ()
|
||||
cmd[0] (stack, cmd_and_args[2])
|
||||
except Exception:
|
||||
stack.undo ()
|
||||
return sys.exc_info ()
|
||||
else:
|
||||
return None
|
||||
elif not len (strbuf):
|
||||
return None
|
||||
else:
|
||||
try:
|
||||
raise Exception ("Command not found: %s" % ''.join(strbuf))
|
||||
except Exception:
|
||||
return sys.exc_info ()
|
||||
|
||||
if not strbuf:
|
||||
state = "FIRSTCHAR"
|
||||
|
||||
elif state == "STRING":
|
||||
if do_edit (key, strbuf) == "ENTER":
|
||||
stack.append (''.join (strbuf).lstrip('"'))
|
||||
return None
|
||||
|
||||
if not strbuf:
|
||||
state = "FIRSTCHAR"
|
||||
|
||||
def main (stdscr):
|
||||
curses.nonl ()
|
||||
curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK)
|
||||
error = None
|
||||
stack = UndoStack ()
|
||||
while True:
|
||||
stdscr.clear ()
|
||||
RPN_drawstack (stack, stdscr)
|
||||
error = RPN_prompt (stack, error, stdscr)
|
||||
|
||||
if __name__ == "__main__":
|
||||
curses.wrapper (main)
|
|
@ -4,8 +4,8 @@ It's a curses RPN calculator written in straight Python. Not actually Linux-depe
|
|||
despite the (old) name. Should work on both Python 2 and 3, though I only regularly
|
||||
use/test it in Py3.
|
||||
|
||||
Very simple. All one file, no installer - just shove it in your $PATH
|
||||
somewhere. And don't yell at me when it breaks your computer.
|
||||
To install, just do `python setup.py install --user`. To test-drive, you should be able
|
||||
to call `python ./lerpn` from in the root of the source tree.
|
||||
|
||||
See the code for documentation.
|
||||
|
||||
|
|
984
lerpn
984
lerpn
|
@ -1,986 +1,6 @@
|
|||
#!/usr/bin/python
|
||||
|
||||
# Written in 2015 by Christopher Pavlina.
|
||||
###############################################################################
|
||||
# lerpn - Linux Engineering RPN calculator
|
||||
#
|
||||
# This is a simple RPN calculator using Python and curses. How to use it should
|
||||
# be obvious for those familiar with RPN. The basic commands:
|
||||
#
|
||||
# + - * /
|
||||
#
|
||||
# enter dup
|
||||
# x exchange/swap
|
||||
# bksp drop
|
||||
# ? display help
|
||||
# ctrl-C exit
|
||||
#
|
||||
#
|
||||
# Numbers are displayed in engineering notation with metric prefixes. You
|
||||
# cannot TYPE them this way, however; that would cause too many collisions with
|
||||
# single-key commands IMHO.
|
||||
#
|
||||
# Not all commands are a single key. Less-used commands begin with a
|
||||
# single-quote, then the command name, then 'enter'. For example, the sequence
|
||||
# of keys used to compute the sine of 3.14 would be:
|
||||
#
|
||||
# 3 . 1 4 <enter> ' s i n <enter>
|
||||
#
|
||||
# You can tag numbers to help remember what they are. To do this, first have
|
||||
# the number at the end of the stack. Then, push a string to the stack, by
|
||||
# typing a double-quote ", then the string text, then <enter>. Then, press
|
||||
# lower-case t to combine the number and the tag.
|
||||
#
|
||||
# Basic math can be done to numbers while tagged, but if you perform a binary
|
||||
# operation on two tagged numbers, lerpn won't know which tag to keep.
|
||||
#
|
||||
# To untag a number, use upper-case T to split the number from its tag. You can
|
||||
# then drop the tag.
|
||||
#
|
||||
# This is currently the only application for strings on the stack. Lone strings
|
||||
# are displayed in green to make them obvious - otherwise, you'll be mighty
|
||||
# confused when 2 + 3 = 23 !
|
||||
#
|
||||
###############################################################################
|
||||
# ADDING COMMANDS
|
||||
#
|
||||
# Grep this source for ADDING SINGLE-LETTER COMMANDS or ADDING NAMED COMMANDS.
|
||||
#
|
||||
###############################################################################
|
||||
#
|
||||
# This software is free. Do whatever the fuck you want with it. Copy it, change
|
||||
# it, steal it, strip away this notice, print it and tear it into confetti to
|
||||
# throw at random dudes on the street. Really, I don't care.
|
||||
|
||||
import collections
|
||||
import curses
|
||||
import math
|
||||
import string
|
||||
import os
|
||||
import pipes
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import traceback
|
||||
import LerpnApp
|
||||
|
||||
# Py2/3 bits
|
||||
if not hasattr (math, "log2"):
|
||||
math.log2 = lambda x: math.log(x, 2)
|
||||
|
||||
# Hack warning!
|
||||
# Pint takes too long to load for a general-purpose calculator that has to come
|
||||
# up quickly. Since it's not needed until after the first line has been
|
||||
# entered, I'm throwing up a loader in a thread. Blame nickjohnson if it's bad
|
||||
import threading
|
||||
pint_load_event = threading.Event()
|
||||
pint=None
|
||||
UREG=None
|
||||
def pintloader():
|
||||
global pint, _num_parser, UREG, _Q_
|
||||
try:
|
||||
import pint
|
||||
UREG = pint.UnitRegistry ()
|
||||
UREG.autoconvert_offset_to_baseunit = True
|
||||
def _num_parser(x):
|
||||
# Do not return int!
|
||||
ret = UREG.parse_expression (x)
|
||||
if isinstance (ret, int):
|
||||
return float (ret)
|
||||
else:
|
||||
return ret
|
||||
def _Q_(x):
|
||||
return UREG.parse_expression (x)
|
||||
pint_load_event.set ()
|
||||
except ImportError:
|
||||
pint = None
|
||||
_num_parser = float
|
||||
def _Q_(x):
|
||||
return float (x.partition(" ")[0])
|
||||
pint_load_event.set ()
|
||||
|
||||
def num_parser(x):
|
||||
pint_load_event.wait()
|
||||
if '"' in x:
|
||||
x, delim, tag = x.partition('"')
|
||||
else:
|
||||
tag = None
|
||||
parsed_num = _num_parser(x)
|
||||
if tag is None:
|
||||
return parsed_num
|
||||
else:
|
||||
return Tagged (parsed_num, tag.strip())
|
||||
|
||||
def Q_(*args, **kwargs):
|
||||
pint_load_event.wait()
|
||||
return _Q_(*args, **kwargs)
|
||||
|
||||
pL = threading.Thread(target=pintloader)
|
||||
pL.start()
|
||||
|
||||
# Pyperclip is optional
|
||||
try:
|
||||
import pyperclip
|
||||
except ImportError:
|
||||
pyperclip = None
|
||||
|
||||
FORMAT="eng"
|
||||
SIGFIGS=7
|
||||
ERROR=None
|
||||
|
||||
class UndoStack (list):
|
||||
"""Stack supporting an 'undo' action.
|
||||
This is used as the main RPN stack. You can append() and pop() and [] just
|
||||
like with a normal list. You can also use undopush() to push a duplicate of
|
||||
the stack itself onto a "stack of stacks", and then undo() to restore that.
|
||||
"""
|
||||
|
||||
def __init__ (self, v=None):
|
||||
"""Initialize an UndoStack from an optional source list.
|
||||
"""
|
||||
if v is None:
|
||||
list.__init__(self)
|
||||
self.__sos = []
|
||||
|
||||
else:
|
||||
list.__init__(self, v)
|
||||
self.__sos = []
|
||||
|
||||
def undopush (self):
|
||||
"""Save the current stack state to be restored later with undo()
|
||||
"""
|
||||
self.__sos.append (self[:])
|
||||
|
||||
def undo (self):
|
||||
"""Restore the last saved undo state
|
||||
"""
|
||||
if len (self.__sos) > 1:
|
||||
self[:] = self.__sos.pop ()
|
||||
else:
|
||||
self[:] = []
|
||||
|
||||
class Tagged (object):
|
||||
"""Tagged number object.
|
||||
This behaves like a number, but also contains a string tag. The values
|
||||
are accessible at .num and .tag
|
||||
"""
|
||||
def __init__ (self, num, tag):
|
||||
self.num = num
|
||||
self.tag = tag
|
||||
|
||||
# Add operations to Tagged
|
||||
ops = [
|
||||
("add", lambda x,y: x+y),
|
||||
("sub", lambda x,y: x-y),
|
||||
("mul", lambda x,y: x*y),
|
||||
("truediv", lambda x,y: x/y),
|
||||
("mod", lambda x,y: x%y),
|
||||
("pow", lambda x,y: x**y),
|
||||
]
|
||||
for opname, opfunc in ops:
|
||||
# For the uninitiated, the default-argument parameters create a new
|
||||
# scope for the variable, allowing passing each loop iteration's value
|
||||
# to the closure instead of closing around the single final value.
|
||||
def method (self, other, opfunc=opfunc):
|
||||
if isinstance (other, Tagged):
|
||||
# If both are tagged, just remove the tag.
|
||||
return opfunc (self.num, other.num)
|
||||
if not isinstance (other, float): return NotImplemented
|
||||
return Tagged (opfunc (self.num, other), self.tag)
|
||||
def rmethod (self, other, opfunc=opfunc):
|
||||
if isinstance (other, Tagged):
|
||||
# If both are tagged, just remove the tag.
|
||||
return opfunc (other.num, self.num)
|
||||
if not isinstance (other, float): return NotImplemented
|
||||
return Tagged (opfunc (other, self.num), self.tag)
|
||||
setattr (Tagged, "__%s__" % opname, method)
|
||||
setattr (Tagged, "__r%s__" % opname, rmethod)
|
||||
|
||||
class CursesScrollBox (object):
|
||||
"""Displays some text in a scroll box."""
|
||||
def __init__ (self, width, height):
|
||||
"""Initialize.
|
||||
|
||||
@param width - desired width: positive for absolute width, negative
|
||||
for margin around window edge
|
||||
@param height - desired height: positive for absolute height,
|
||||
negative for margin around window edge
|
||||
"""
|
||||
self.w = width
|
||||
self.h = height
|
||||
self.title = ""
|
||||
self.pos = 0
|
||||
|
||||
def set_text (self, text):
|
||||
self.text = text
|
||||
|
||||
def set_title (self, title):
|
||||
self.title = title
|
||||
|
||||
def show (self):
|
||||
if self.w < 0:
|
||||
self._w = curses.COLS + self.w
|
||||
else:
|
||||
self._w = self.w
|
||||
if self.h < 0:
|
||||
self._h = curses.LINES + self.h
|
||||
else:
|
||||
self._h = self.h
|
||||
self._x = curses.COLS // 2 - self._w // 2
|
||||
self._y = curses.LINES // 2 - self._h // 2
|
||||
|
||||
curses.curs_set (0)
|
||||
curses.doupdate ()
|
||||
self.win = curses.newwin (self._h, self._w, self._y, self._x)
|
||||
|
||||
while self._show ():
|
||||
pass
|
||||
|
||||
curses.curs_set (1)
|
||||
|
||||
def _show (self):
|
||||
self.win.clear ()
|
||||
self.win.move (1, 2)
|
||||
self.win.addstr (self.title)
|
||||
self.win.keypad (1)
|
||||
|
||||
if isinstance(self.text, str):
|
||||
lines = self.text.split("\n")
|
||||
elif isinstance(self.text, list):
|
||||
lines = self.text
|
||||
else:
|
||||
raise TypeError ("Text must be list or str")
|
||||
view_lines = lines[self.pos:self.pos+self._h - 4]
|
||||
|
||||
for y, line in enumerate (view_lines):
|
||||
self.win.move (y + 3, 2)
|
||||
self.win.addstr (line.rstrip())
|
||||
|
||||
if self.pos > 0:
|
||||
self.win.move (2, self._w - 3)
|
||||
self.win.addstr ("+")
|
||||
if self.pos + self._h < len (lines) + 4:
|
||||
self.win.move (self._h - 2, self._w - 3)
|
||||
self.win.addstr ("+")
|
||||
|
||||
scroll_limit = len (lines) - self._h // 2
|
||||
|
||||
self.win.border ()
|
||||
|
||||
try:
|
||||
key = self.win.getch ()
|
||||
except KeyboardInterrupt:
|
||||
return False
|
||||
if key == curses.KEY_UP:
|
||||
self.pos = max(self.pos - 1, 0)
|
||||
elif key == curses.KEY_DOWN:
|
||||
self.pos = min(self.pos + 1, scroll_limit)
|
||||
elif key == curses.KEY_PPAGE:
|
||||
self.pos = max(self.pos - 10, 0)
|
||||
elif key == curses.KEY_NPAGE:
|
||||
self.pos = min(self.pos + 10, scroll_limit)
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
LUT = {
|
||||
-24: "y",
|
||||
-21: "z",
|
||||
-18: "a",
|
||||
-15: "f",
|
||||
-12: "p",
|
||||
-9: "n",
|
||||
-6: u"\xb5",
|
||||
-3: "m",
|
||||
0: "",
|
||||
3: "k",
|
||||
6: "M",
|
||||
9: "G",
|
||||
12: "T",
|
||||
15: "P",
|
||||
18: "E",
|
||||
21: "Z",
|
||||
24: "Y"
|
||||
}
|
||||
def eng(x, sigfigs=7):
|
||||
"""Return x in engineering notation"""
|
||||
if x == 0.0:
|
||||
return "0"
|
||||
elif x < 0.0:
|
||||
return '-'+eng (-x, sigfigs)
|
||||
m = math.floor(math.log10(x)/3)*3
|
||||
coeff = "{0:.{1}g}".format (x * 10.0 ** (-m), sigfigs)
|
||||
lut = LUT.get (m, None)
|
||||
if lut is None:
|
||||
return coeff + "e" + str (m)
|
||||
if lut != '': lut = '_' + lut
|
||||
#if '.' in coeff and lut != '':
|
||||
# return coeff.replace ('.', lut)
|
||||
#else:
|
||||
# return coeff + lut
|
||||
return (coeff + lut)
|
||||
|
||||
def RPN_drawstack (stack, window):
|
||||
"""Draw the stack onto a curses window.
|
||||
"""
|
||||
for i, item in enumerate (stack):
|
||||
row = curses.LINES - len (stack) - 2 + i
|
||||
if row < 1:
|
||||
continue
|
||||
|
||||
tag = None
|
||||
mode = 0
|
||||
|
||||
if isinstance (item, Tagged):
|
||||
tag = item.tag
|
||||
item = item.num
|
||||
|
||||
if hasattr (item, "magnitude"):
|
||||
# TODO: HACK
|
||||
# pint will add a way to format just the abbreviated unit in 0.7.
|
||||
# For now, I'll format a known magnitude and then remove the known
|
||||
# magnitude
|
||||
|
||||
one_magnitude_unit = format (UREG.Quantity (1, item.units), "~")
|
||||
assert one_magnitude_unit.startswith ("1 ")
|
||||
units_abbrev = one_magnitude_unit[2:]
|
||||
|
||||
if FORMAT == "eng":
|
||||
formatted = eng (item.magnitude, SIGFIGS) + " " + units_abbrev
|
||||
else:
|
||||
formatted = "%e %s" % (item.magnitude, units_abbrev)
|
||||
|
||||
elif isinstance (item, float):
|
||||
if FORMAT == "eng":
|
||||
formatted = eng (item, SIGFIGS)
|
||||
else:
|
||||
formatted = "%e" % item
|
||||
|
||||
elif isinstance (item, str):
|
||||
formatted = item
|
||||
mode = curses.color_pair(1)
|
||||
|
||||
else:
|
||||
formatted = repr(item)
|
||||
|
||||
if tag is None:
|
||||
window.addstr (row, 0, "%3d %20s" % (i, formatted), mode)
|
||||
else:
|
||||
window.addstr (row, 0, "%3d %20s : %s" % (i, formatted, tag), mode)
|
||||
|
||||
curses.doupdate ()
|
||||
|
||||
def do_resize (window):
|
||||
"""Handles redrawing the window if it has been resized.
|
||||
"""
|
||||
resize = curses.is_term_resized (curses.LINES, curses.COLS)
|
||||
if resize:
|
||||
y, x = window.getmaxyx ()
|
||||
window.clear ()
|
||||
curses.resizeterm (y, x)
|
||||
curses.doupdate ()
|
||||
|
||||
################################################################################
|
||||
# Command definitions
|
||||
|
||||
def BINARY (f):
|
||||
"""Create a binary operator from a lambda function."""
|
||||
def cmd (stack, args=None):
|
||||
y = stack.pop ()
|
||||
x = stack.pop ()
|
||||
stack.append (f (x, y))
|
||||
return cmd
|
||||
|
||||
def UNARY (f):
|
||||
"""Create a unary operator from a lambda function.
|
||||
The unary operators know how to handle tagged numbers - they reapply the
|
||||
same tag after performing the operation."""
|
||||
def cmd (stack, args=None):
|
||||
x = stack.pop ()
|
||||
if isinstance(x, Tagged):
|
||||
newtagged = Tagged(f(x.num), x.tag)
|
||||
stack.append (newtagged)
|
||||
else:
|
||||
stack.append (f (x))
|
||||
return cmd
|
||||
|
||||
def DEGREE_TRIG (f):
|
||||
"""Create a unary trig operator that accepts degrees.
|
||||
|
||||
This has to be handled specially because pint supports degrees/radians,
|
||||
performing the conversion automatically. We have to avoid double-converting,
|
||||
and also work in the absence of pint.
|
||||
"""
|
||||
def g(x):
|
||||
if hasattr (x, "magnitude"):
|
||||
x_deg = x.to (UREG.degree)
|
||||
else:
|
||||
x_deg = x * math.pi / 180
|
||||
return f (x_deg)
|
||||
return UNARY (g)
|
||||
|
||||
def DEGREE_INVTRIG (f):
|
||||
"""Create a unary inverse trig operator that returns degrees.
|
||||
|
||||
This returns a pint object if pint is installed.
|
||||
"""
|
||||
if pint is not None:
|
||||
def g(x):
|
||||
return UREG.Quantity (f (x), UREG.radian).to (UREG.degree)
|
||||
else:
|
||||
def g(x):
|
||||
return f (x) * 180 / math.pi
|
||||
return UNARY (g)
|
||||
|
||||
def _tag (stack, args=None):
|
||||
"""Pop a string and then a number from the stack, then create a tagged
|
||||
number."""
|
||||
tag = stack.pop ()
|
||||
num = stack.pop ()
|
||||
if not isinstance(tag, str):
|
||||
raise TypeError ("to create tagged number, expect Y: number, X: string")
|
||||
elif not isinstance(num, float) and not hasattr(num, "magnitude"):
|
||||
raise TypeError ("to create tagged number, expect Y: number, X: string ")
|
||||
stack.append (Tagged (num, tag))
|
||||
|
||||
def _untag (stack, args=None):
|
||||
"""Pop a tagged number, then push back the original number and tag."""
|
||||
tagged = stack.pop ()
|
||||
if not isinstance(tagged, Tagged):
|
||||
stack.append (tagged)
|
||||
else:
|
||||
stack.append (tagged.num)
|
||||
|
||||
def _drop (stack, args=None):
|
||||
stack.pop ()
|
||||
def _dup (stack, args=None):
|
||||
if len (stack):
|
||||
stack.append (stack[-1])
|
||||
def _undo (stack, args=None):
|
||||
# Have to run twice, because undopush was called right before this
|
||||
stack.undo ()
|
||||
stack.undo ()
|
||||
def _xchg (stack, args=None):
|
||||
a = stack.pop ()
|
||||
b = stack.pop ()
|
||||
stack.append (a)
|
||||
stack.append (b)
|
||||
def _get (stack, args=None):
|
||||
"""Grab an element from the stack by index and dup it onto the end"""
|
||||
n = stack.pop ()
|
||||
if n >= 0 and n < len (stack):
|
||||
stack.append (stack [int (n)])
|
||||
else:
|
||||
stack.append (0.)
|
||||
|
||||
def _help (stack, args=None):
|
||||
"""Display the command helptexts"""
|
||||
scrollbox = CursesScrollBox (-4, -4)
|
||||
scrollbox.set_title ("HELP :: up/down/pgup/pgdown; other=close")
|
||||
|
||||
lines_left = []
|
||||
lines_right = []
|
||||
|
||||
for key in COMMANDS.keys():
|
||||
val = COMMANDS[key]
|
||||
if isinstance(key, int):
|
||||
lines_left.append (" " * 26)
|
||||
lines_left.append ("%-26s" % val)
|
||||
else:
|
||||
lines_left.append ("%-10s %-15s" % (key, val[1]))
|
||||
|
||||
for key in SINGLE_LETTER_COMMANDS.keys():
|
||||
if not isinstance (key, str):
|
||||
continue
|
||||
if key[0] not in string.digits+string.ascii_letters+string.punctuation:
|
||||
continue
|
||||
val = SINGLE_LETTER_COMMANDS[key]
|
||||
lines_right.append ("%-10s %-15s" % (key, val[1]))
|
||||
|
||||
if len (lines_left) < len (lines_right):
|
||||
lines_left.extend ([""] * (len (lines_right) - len (lines_left)))
|
||||
if len (lines_left) > len (lines_right):
|
||||
lines_right.extend ([""] * (len (lines_left) - len (lines_right)))
|
||||
|
||||
lines = [i + " " + j for i,j in zip(lines_left, lines_right)]
|
||||
scrollbox.set_text (lines)
|
||||
|
||||
scrollbox.show ()
|
||||
|
||||
def _exc (stack, args=None):
|
||||
"""Display the latest exception"""
|
||||
if ERROR is None:
|
||||
return
|
||||
s = traceback.format_tb (ERROR[2])
|
||||
scrollbox = CursesScrollBox (-4, -4)
|
||||
scrollbox.set_title ("LAST BACKTRACE")
|
||||
scrollbox.set_text (s)
|
||||
scrollbox.show ()
|
||||
|
||||
|
||||
def _eng (stack, args=None):
|
||||
global FORMAT, SIGFIGS
|
||||
FORMAT = "eng"
|
||||
|
||||
if args is not None and len(args) == 1:
|
||||
SIGFIGS = int(args[0])
|
||||
else:
|
||||
SIGFIGS = 7
|
||||
|
||||
def _sci (stack, args=None):
|
||||
global FORMAT
|
||||
FORMAT = "sci"
|
||||
|
||||
def _copy (stack, args=None):
|
||||
|
||||
item = stack[-1]
|
||||
if isinstance(item, str):
|
||||
fmt = item
|
||||
elif isinstance(item, float):
|
||||
fmt = str(item)
|
||||
|
||||
elif isinstance(item, Tagged):
|
||||
fmt = str(item.num)
|
||||
|
||||
if pyperclip is not None:
|
||||
pyperclip.copy (fmt)
|
||||
else:
|
||||
xclip = subprocess.Popen (["xclip", "-selection", "CLIPBOARD"], stdin=subprocess.PIPE)
|
||||
xclip.stdin.write (fmt.encode('utf8'))
|
||||
xclip.stdin.close ()
|
||||
xclip.wait ()
|
||||
|
||||
def _paste (stack, args=None):
|
||||
if pyperclip is not None:
|
||||
item = float (pyperclip.paste ())
|
||||
stack.append (item)
|
||||
|
||||
else:
|
||||
clipbd = subprocess.check_output (["xclip", "-selection", "CLIPBOARD", "-o"])
|
||||
item = float(clipbd)
|
||||
stack.append (item)
|
||||
|
||||
def _convert (stack, args=None):
|
||||
if args is not None:
|
||||
units = UREG.parse_units (args)
|
||||
value = stack.pop ()
|
||||
else:
|
||||
units = stack.pop ().units
|
||||
value = stack.pop ()
|
||||
if isinstance (value, Tagged):
|
||||
tag = value.tag
|
||||
value = value.num
|
||||
else:
|
||||
tag = None
|
||||
converted = value.to (units)
|
||||
if tag is not None:
|
||||
converted = Tagged (converted, tag)
|
||||
stack.append (converted)
|
||||
|
||||
def _sh (stack, arg):
|
||||
|
||||
newenv = dict (os.environ)
|
||||
if not stack:
|
||||
newenv["N"] = "0"
|
||||
newenv["F"] = "0"
|
||||
else:
|
||||
num = stack[-1]
|
||||
if isinstance(num, Tagged):
|
||||
num = num.num
|
||||
if isinstance(num, float):
|
||||
newenv["N"] = "%g" % num
|
||||
newenv["F"] = "%g" % num
|
||||
else:
|
||||
newenv["N"] = "%g" % num.magnitude
|
||||
newenv["F"] = "{:g~}".format (num)
|
||||
|
||||
try:
|
||||
p = subprocess.Popen (arg, shell=True, env=newenv, stdout=subprocess.PIPE)
|
||||
data = p.stdout.read ().decode ("utf8")
|
||||
p.wait ()
|
||||
except KeyboardInterrupt:
|
||||
data = ""
|
||||
|
||||
FLOAT_RE = r"[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?"
|
||||
match = re.search (FLOAT_RE, data)
|
||||
if match is not None:
|
||||
try:
|
||||
stack.append (float (match.group(0)))
|
||||
except ValueError as e:
|
||||
pass
|
||||
|
||||
scrollbox = CursesScrollBox (-4, -4)
|
||||
scrollbox.set_title ("COMMAND OUTPUT")
|
||||
scrollbox.set_text (data)
|
||||
scrollbox.show ()
|
||||
|
||||
def _preferred (stack, arg):
|
||||
|
||||
U = UREG.parse_units
|
||||
PREFERRED = [
|
||||
U("V"), U("V/m"),
|
||||
U("ohm"), U("farad"), U("henry"),
|
||||
U("W"), U("J"),
|
||||
U("s"), U("Hz"), U("m"), U("g"),
|
||||
]
|
||||
value = stack.pop ()
|
||||
if isinstance (value, Tagged):
|
||||
tag = value.tag
|
||||
value = value.num
|
||||
else:
|
||||
tag = None
|
||||
|
||||
value_base = value.to_base_units ()
|
||||
dest_unit = None
|
||||
|
||||
for dest in PREFERRED:
|
||||
src = UREG.Quantity(1., dest).to_base_units().units
|
||||
if src == value_base.units:
|
||||
dest_unit = dest
|
||||
break
|
||||
|
||||
if dest_unit is None:
|
||||
converted = value
|
||||
else:
|
||||
converted = value.to (dest_unit)
|
||||
|
||||
if tag is not None:
|
||||
converted = Tagged (converted, tag)
|
||||
stack.append (converted)
|
||||
|
||||
################################################################################
|
||||
# ADDING SINGLE-LETTER COMMANDS
|
||||
#
|
||||
# Insert a compound tuple into the OrderedDict below:
|
||||
# (key, (operator, helptext))
|
||||
#
|
||||
# key: the key returned by curses. For printables, just the character.
|
||||
# operator: a function that takes the current UndoStack, operates on it,
|
||||
# and returns nothing. Any exceptions thrown in here will be
|
||||
# caught and handled, so don't worry about those.
|
||||
# helptext: a string to display on the help screen.
|
||||
#
|
||||
#
|
||||
# Standard unary and binary operators can be made with UNARY() and BINARY(),
|
||||
# which accept a lambda function taking one or two arguments and return an
|
||||
# operator closure around it.
|
||||
|
||||
SINGLE_LETTER_COMMANDS = collections.OrderedDict([
|
||||
("\x7f", (_drop, "drop")),
|
||||
("\x08", (_drop, "drop")),
|
||||
(curses.KEY_BACKSPACE, (_drop, "drop")),
|
||||
|
||||
("\r", (_dup, "dup")),
|
||||
("\n", (_dup, "dup")),
|
||||
(curses.KEY_ENTER, (_dup, "dup")),
|
||||
|
||||
("-", (BINARY (lambda x, y: x - y), "subtract")),
|
||||
("+", (BINARY (lambda x, y: x + y), "add")),
|
||||
("*", (BINARY (lambda x, y: x * y), "multiply")),
|
||||
("/", (BINARY (lambda x, y: x / y), "divide")),
|
||||
("^", (BINARY (lambda x, y: x ** y), "power")),
|
||||
("e", (UNARY (math.exp), "exponential")),
|
||||
("r", (UNARY (math.sqrt), "sq root")),
|
||||
("i", (UNARY (lambda x: 1/x), "reciprocal")),
|
||||
|
||||
("u", (_undo, "undo")),
|
||||
("x", (_xchg, "exchange")),
|
||||
("[", (_get, "dup item by number")),
|
||||
|
||||
("t", (_tag, "attach tag to number")),
|
||||
("T", (_untag, "unTag")),
|
||||
("U", (UNARY (lambda x: x.magnitude), "unUnit")),
|
||||
|
||||
("c", (_copy, "copy to clipboard")),
|
||||
("v", (_paste, "paste from clipboard")),
|
||||
|
||||
("?", (_help, "display help")),
|
||||
])
|
||||
|
||||
################################################################################
|
||||
# ADDING NAMED COMMANDS
|
||||
#
|
||||
# Insert a compound tuple into the OrderedDict below:
|
||||
# (name, (operator, helptext))
|
||||
#
|
||||
# name: the full command name, including beginning single-quote
|
||||
# operator: a function that takes the current UndoStack, operates on it,
|
||||
# and returns nothing. Any exceptions thrown in here will be
|
||||
# caught and handled, so don't worry about those.
|
||||
# helptext: a string to display on the help screen.
|
||||
#
|
||||
#
|
||||
# Standard unary and binary operators can be made with UNARY() and BINARY(),
|
||||
# which accept a lambda function taking one or two arguments and return an
|
||||
# operator closure around it.
|
||||
#
|
||||
#
|
||||
# You can insert section headers into the help text by adding a compound tuple:
|
||||
# (id, header)
|
||||
#
|
||||
# id: any unique integer
|
||||
# header: header text
|
||||
|
||||
COMMANDS = collections.OrderedDict([
|
||||
(1, "==TRIGONOMETRY, RAD=="),
|
||||
("'sin", (UNARY (math.sin), "sin(rad)")),
|
||||
("'cos", (UNARY (math.cos), "cos(rad)")),
|
||||
("'tan", (UNARY (math.tan), "tan(rad)")),
|
||||
("'asin", (UNARY (lambda x: math.asin(x) * Q_("1 rad")), "asin->rad")),
|
||||
("'acos", (UNARY (lambda x: math.acos(x) * Q_("1 rad")), "acos->rad")),
|
||||
("'atan", (UNARY (lambda x: math.atan(x) * Q_("1 rad")), "atan->rad")),
|
||||
(2, "==TRIGONOMETRY, DEG=="),
|
||||
("'sind", (DEGREE_TRIG (math.sin), "sin(deg)")),
|
||||
("'cosd", (DEGREE_TRIG (math.cos), "cos(deg)")),
|
||||
("'tand", (DEGREE_TRIG (math.tan), "tan(deg)")),
|
||||
("'asind", (DEGREE_INVTRIG (math.asin), "asin->deg")),
|
||||
("'acosd", (DEGREE_INVTRIG (math.acos), "acos->deg")),
|
||||
("'atand", (DEGREE_INVTRIG (math.atan), "atan->deg")),
|
||||
("'deg", (UNARY (math.degrees), "rad->deg")),
|
||||
("'rad", (UNARY (math.radians), "deg->rad")),
|
||||
(3, "==HYPERBOLICS=="),
|
||||
("'sinh", (UNARY (math.sinh), "sinh")),
|
||||
("'cosh", (UNARY (math.cosh), "cosh")),
|
||||
("'tanh", (UNARY (math.tanh), "tanh")),
|
||||
("'asinh", (UNARY (math.asinh), "asinh")),
|
||||
("'acosh", (UNARY (math.acosh), "acosh")),
|
||||
("'atanh", (UNARY (math.atanh), "atanh")),
|
||||
(4, "==LOGARITHMS=="),
|
||||
("'log", (UNARY (math.log), "log base e")),
|
||||
("'log10", (UNARY (math.log10), "log base 10")),
|
||||
("'log2", (UNARY (math.log2), "log base 2")),
|
||||
(7, "==UNITS=="),
|
||||
("'mag", (UNARY (lambda x: x.magnitude), "return magnitude without unit")),
|
||||
("'unity", (UNARY (lambda x: UREG.Quantity(1., x.units)), "return 1.0*unit")),
|
||||
("'base", (UNARY (lambda x: x.to_base_units()), "convert to base units")),
|
||||
("'pu", (_preferred, "convert to preferred units")),
|
||||
("'conv", (_convert, "convert Y to units of X, or X to units of command argument")),
|
||||
(5, "==CONSTANTS=="),
|
||||
("'pi", (lambda stack,a=None: stack.append (math.pi), "const PI")),
|
||||
("'e", (lambda stack,a=None: stack.append (math.e), "const E")),
|
||||
("'c", (lambda stack,a=None: stack.append (Q_("2.99792458e8 m/s")), "speed of light, m/s")),
|
||||
("'h", (lambda stack,a=None: stack.append (Q_("6.6260755e-34 J S")), "Planck constant, J s")),
|
||||
("'k", (lambda stack,a=None: stack.append (Q_("1.380658e-23 J/K")), "Boltzmann constant, J/K")),
|
||||
("'elec", (lambda stack,a=None: stack.append (Q_("1.60217733e-19 C")), "Charge of electron, C")),
|
||||
("'e0", (lambda stack,a=None: stack.append (Q_("8.854187817e-12 F/m")), "Permittivity of vacuum, F/m")),
|
||||
("'amu", (lambda stack,a=None: stack.append (Q_("1.6605402e-27 kg")), "Atomic mass unit, kg")),
|
||||
("'Na", (lambda stack,a=None: stack.append (Q_("6.0221367e23 1/mol")), "Avogadro's number, mol^-1")),
|
||||
("'atm", (lambda stack,a=None: stack.append (Q_("101325. Pa")), "Standard atmosphere, Pa")),
|
||||
(6, "==LERPN=="),
|
||||
("'sh", (_sh, "run shell command, $N = magnitude, $F = full number with unit")),
|
||||
("'help", (_help, "display help")),
|
||||
("'exc", (_exc, "display latest exception backtrace")),
|
||||
("'eng", (_eng, "engineering mode, sig figs = 7 or command argument")),
|
||||
("'sci", (_sci, "scientific mode")),
|
||||
])
|
||||
|
||||
def get_single_key_command (key):
|
||||
"""
|
||||
Return a single-key command for the key, or None.
|
||||
"""
|
||||
if key > 127:
|
||||
lookup_key = key
|
||||
else:
|
||||
lookup_key = chr (key)
|
||||
return SINGLE_LETTER_COMMANDS.get (lookup_key, None)
|
||||
|
||||
def do_command (stack, cmd):
|
||||
try:
|
||||
stack.undopush ()
|
||||
cmd[0] (stack)
|
||||
except Exception:
|
||||
stack.undo ()
|
||||
return sys.exc_info ()
|
||||
else:
|
||||
return None
|
||||
|
||||
ENTER_KEYS = [curses.KEY_ENTER, 10, 13]
|
||||
BACKSPACE_KEYS = [curses.KEY_BACKSPACE, 127, 8]
|
||||
|
||||
def do_edit (key, strbuf):
|
||||
"""
|
||||
Line-edit.
|
||||
Returns "BACKSPACE", "ENTER", or None
|
||||
"""
|
||||
|
||||
if key in BACKSPACE_KEYS:
|
||||
if len (strbuf):
|
||||
del strbuf[-1]
|
||||
return "BACKSPACE"
|
||||
|
||||
if key in ENTER_KEYS:
|
||||
return "ENTER"
|
||||
|
||||
try:
|
||||
strbuf.append (chr (key))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def RPN_prompt (stack, error, window):
|
||||
"""Displays the prompt.
|
||||
stack: buffer current UndoStack
|
||||
error: any error text returned from the _previous_ RPN_prompt, or None
|
||||
window: curses window
|
||||
|
||||
returns any error text from the command, or None.
|
||||
"""
|
||||
|
||||
if error is not None:
|
||||
global ERROR
|
||||
ERROR = error
|
||||
window.addstr (0, 0, "ERROR: %s" % str (error[1]))
|
||||
|
||||
STATES = [
|
||||
"FIRSTCHAR", # first character determines the mode
|
||||
"NUMERIC", # parsing numeric
|
||||
"NUMUNIT", # parsing unit inside numeric
|
||||
"COMMAND", # parsing command
|
||||
"STRING", # parsing string
|
||||
]
|
||||
state = "FIRSTCHAR"
|
||||
strbuf = []
|
||||
|
||||
while True:
|
||||
assert state in STATES
|
||||
RPN_drawstack (stack, window)
|
||||
window.move (curses.LINES - 1, 0)
|
||||
window.addstr ("? ")
|
||||
for i in strbuf:
|
||||
window.addstr (i)
|
||||
window.clrtoeol ()
|
||||
curses.doupdate ()
|
||||
|
||||
try:
|
||||
key = window.getch ()
|
||||
except KeyboardInterrupt:
|
||||
sys.exit (0)
|
||||
try:
|
||||
char = chr (key)
|
||||
except ValueError:
|
||||
char = None
|
||||
|
||||
if state == "FIRSTCHAR":
|
||||
|
||||
# Try single-key commands first
|
||||
cmd = get_single_key_command (key)
|
||||
if cmd is not None:
|
||||
return do_command (stack, cmd)
|
||||
|
||||
if char is None:
|
||||
continue
|
||||
|
||||
if char in string.digits + "_.":
|
||||
strbuf.append (char)
|
||||
state = "NUMERIC"
|
||||
|
||||
if char == "'":
|
||||
strbuf.append (char)
|
||||
state = "COMMAND"
|
||||
|
||||
elif char == '"':
|
||||
strbuf.append (char)
|
||||
state = "STRING"
|
||||
|
||||
elif state == "NUMERIC":
|
||||
|
||||
cmd = get_single_key_command (key)
|
||||
|
||||
# Make exception for 'e' so that we can type sci notation
|
||||
if char == "e" or char == "E" or (("e" in strbuf or "E" in strbuf) and char == "-"):
|
||||
cmd = None
|
||||
|
||||
edit_type = do_edit (key, strbuf)
|
||||
if edit_type == "ENTER" or (cmd is not None and edit_type != "BACKSPACE"):
|
||||
if cmd is not None and strbuf and edit_type != "ENTER":
|
||||
# Do not include command in number
|
||||
del strbuf[-1]
|
||||
try:
|
||||
if strbuf[0] == "_":
|
||||
strbuf[0] = "-"
|
||||
f = num_parser (''.join(strbuf))
|
||||
except Exception:
|
||||
return sys.exc_info ()
|
||||
else:
|
||||
stack.append (f)
|
||||
|
||||
if edit_type != "ENTER" and cmd is not None:
|
||||
return do_command (stack, cmd)
|
||||
else:
|
||||
return None
|
||||
|
||||
if strbuf and strbuf[-1] == " ":
|
||||
state = "NUMUNIT"
|
||||
|
||||
if not strbuf:
|
||||
state = "FIRSTCHAR"
|
||||
|
||||
if char is None:
|
||||
continue
|
||||
|
||||
elif state == "NUMUNIT":
|
||||
# Like numeric, but since units can contain letters that have
|
||||
# "single-unit commands" in them, ignore those.
|
||||
|
||||
if do_edit (key, strbuf) == "ENTER":
|
||||
try:
|
||||
if strbuf[0] == "_":
|
||||
strbuf[0] = "-"
|
||||
f = num_parser (''.join(strbuf))
|
||||
except Exception:
|
||||
return sys.exc_info ()
|
||||
else:
|
||||
stack.append (f)
|
||||
return
|
||||
|
||||
if not strbuf:
|
||||
state = "FIRSTCHAR"
|
||||
|
||||
elif state == "COMMAND":
|
||||
|
||||
if do_edit (key, strbuf) == "ENTER":
|
||||
command_string = ''.join (strbuf)
|
||||
cmd_and_args = command_string.partition(" ")
|
||||
cmd = COMMANDS.get (cmd_and_args[0], None)
|
||||
if cmd is not None:
|
||||
try:
|
||||
stack.undopush ()
|
||||
cmd[0] (stack, cmd_and_args[2])
|
||||
except Exception:
|
||||
stack.undo ()
|
||||
return sys.exc_info ()
|
||||
else:
|
||||
return None
|
||||
elif not len (strbuf):
|
||||
return None
|
||||
else:
|
||||
try:
|
||||
raise Exception ("Command not found: %s" % ''.join(strbuf))
|
||||
except Exception:
|
||||
return sys.exc_info ()
|
||||
|
||||
if not strbuf:
|
||||
state = "FIRSTCHAR"
|
||||
|
||||
elif state == "STRING":
|
||||
if do_edit (key, strbuf) == "ENTER":
|
||||
stack.append (''.join (strbuf).lstrip('"'))
|
||||
return None
|
||||
|
||||
if not strbuf:
|
||||
state = "FIRSTCHAR"
|
||||
|
||||
def main (stdscr):
|
||||
curses.nonl ()
|
||||
curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK)
|
||||
error = None
|
||||
stack = UndoStack ()
|
||||
while True:
|
||||
stdscr.clear ()
|
||||
RPN_drawstack (stack, stdscr)
|
||||
error = RPN_prompt (stack, error, stdscr)
|
||||
|
||||
if __name__ == "__main__":
|
||||
curses.wrapper (main)
|
||||
curses.wrapper (LerpnApp.main)
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
from setuptools import setup
|
||||
|
||||
setup( name='lerpn',
|
||||
version='0.1',
|
||||
description='Linux? Engineering RPN calculator',
|
||||
url='https://github.com/cpavlina/lerpn',
|
||||
author='Chris Pavlina',
|
||||
author_email='pavlina.chris@gmail.com',
|
||||
license='any',
|
||||
packages=['LerpnApp'],
|
||||
scripts=['lerpn'],
|
||||
zip_safe=False)
|
||||
|
Loading…
Reference in New Issue