├── .gitignore ├── LICENSE ├── README.md ├── calculon ├── __init__.py ├── colour.py ├── config │ ├── default.cfg │ └── example.cfg ├── display.py ├── env.py ├── load.py ├── main.py ├── repl.py └── voltron_integration.py ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | ### Python ### 2 | *.py[cod] 3 | 4 | # C extensions 5 | *.so 6 | 7 | # Packages 8 | *.egg 9 | *.egg-info 10 | dist 11 | build 12 | eggs 13 | parts 14 | bin 15 | var 16 | sdist 17 | develop-eggs 18 | .installed.cfg 19 | lib 20 | lib64 21 | __pycache__ 22 | 23 | # Installer logs 24 | pip-log.txt 25 | 26 | # Unit test / coverage reports 27 | .coverage 28 | .tox 29 | nosetests.xml 30 | 31 | # Translations 32 | *.mo 33 | 34 | # Mr Developer 35 | .mr.developer.cfg 36 | .project 37 | .pydevproject 38 | 39 | # Rope 40 | .ropeproject 41 | 42 | .DS_Store 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 snare 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | calculon 2 | ======== 3 | 4 | A terminal-based programmer's calculator 5 | ---------------------------------------- 6 | 7 | I haven't found many decent programmer's calculators for Mac and I spend a fair bit of time copying and pasting between Calculator.app, a Python REPL and a debugger, so I figured I'd have a go at writing a quick terminal-based calculator in Python. The result is Calculon. 8 | 9 | Calculon is a programmer's calculator based on an embedded Python REPL. It's split into two components - the display and the REPL - each of which are run in a separate terminal. There are two options for the REPL - either the embedded Python REPL (based on the Python `code` module) or an instance of `bpython`. 10 | 11 | Here is Calculon running with the embedded Python REPL in two panes of an iTerm window: 12 | 13 | [![calculon example](http://i.imgur.com/XEFqWr1.png)](#example) 14 | 15 | And here is Calculon running with the `bpython` REPL with a narrower display: 16 | 17 | [![calculon example2](http://i.imgur.com/F5BJYAu.png)](#example2) 18 | 19 | Installation 20 | ------------ 21 | 22 | $ python setup.py install 23 | 24 | Configuration 25 | ------------- 26 | 27 | An example config (`example.cfg`) is included with the source. Copy it to `~/.calculon/config` and edit it if you want to customise the display, otherwise the defaults in the `defaults.cfg` will be used. 28 | 29 | Usage 30 | ----- 31 | 32 | To run the display: 33 | 34 | $ calculon display 35 | 36 | To run the embedded REPL: 37 | 38 | $ calculon console 39 | 40 | Or, to connect to the display from within a `bpython` instance: 41 | 42 | $ bpython 43 | >>> import calculon.load 44 | 45 | From here, any Python code entered into the REPL that results in a numeric value will be rendered in the display. For example: 46 | 47 | ![format_example_1](http://i.imgur.com/Njn9RRJ.png) 48 | 49 | The result, 0x9a4, will be rendered in the display. When using the embedded REPL (not `bpython`), any numeric results in the REPL will be formatted using the format string defined in the config. By default, this is white coloured hex numbers. The format string can be customised in the configuration, or set in the REPL on the fly like this: 50 | 51 | ![format_example_2](http://i.imgur.com/y46S1c9.png) 52 | 53 | Calculon adds some hackery to the REPL for watching variables. Calling `watch ` will add the given expression to a list of expressions that are tracked and updated every time they change. For example: 54 | 55 | >>> watch a 56 | >>> watch b 57 | >>> watch a + b 58 | 59 | Now when these variables are updated: 60 | 61 | >>> a = 1234 62 | >>> b = 1234 63 | 64 | Their values will be tracked and the expressions reevaluated. Expressions can be removed from this display with the `unwatch` keyword: 65 | 66 | >>> unwatch 0 67 | 68 | Where 0 is the ID displayed at the end (or beginning, when right aligned) of the line. 69 | 70 | Calculon can also connect to [Voltron](https://github.com/snare/voltron) using its [REPL client](https://github.com/snare/voltron/wiki/REPL-Client), inspect register state and memory, and execute debugger commands. 71 | 72 | Inspecting registers: 73 | 74 | >>> V.rip 75 | 0x100000d20 76 | 77 | Memory: 78 | 79 | >>> V[V.rbp] 80 | 'x' 81 | >>> V[V.rbp:V.rbp + 32] 82 | 'x\xee\xbf_\xff\x7f\x00\x00\xfd\xf5\xad\x85\xff\x7f\x00\x00' 83 | 84 | Values from Voltron can now be included in `watch` expressions, and will be updated when the Voltron views are updated (ie. when the debugger is stepped or execution is started and stopped again): 85 | 86 | >>> watch V.rip 87 | 88 | Calculon can also execute debugger commands from the REPL: 89 | 90 | >>> print V("reg read") 91 | General Purpose Registers: 92 | rax = 0x0000000100000d00 inferior`test_function 93 | rbx = 0x0000000000000000 94 | rcx = 0x00007fff5fbff9b0 95 | rdx = 0x00007fff5fbff8b0 96 | rdi = 0x0000000100000f7b "Usage: inferior < sleep | loop | function | crash >\n" 97 | rsi = 0x00007fff5fbff8a0 98 | rbp = 0x00007fff5fbff880 99 | rsp = 0x00007fff5fbff818 100 | r8 = 0x0000000000000000 101 | r9 = 0x00007fff7164b0c8 atexit_mutex + 24 102 | r10 = 0x00000000ffffffff 103 | r11 = 0x0000000100001008 (void *)0x0000000000000000 104 | r12 = 0x0000000000000000 105 | r13 = 0x0000000000000000 106 | r14 = 0x0000000000000000 107 | r15 = 0x0000000000000000 108 | rip = 0x00007fff8c2a7148 libdyld.dylib`dyld_stub_binder 109 | rflags = 0x0000000000000246 110 | cs = 0x000000000000002b 111 | fs = 0x0000000000000000 112 | gs = 0x0000000000000000 113 | 114 | Credits 115 | ------- 116 | [richo](https://github.com/richo) deserves many beers for his efforts -------------------------------------------------------------------------------- /calculon/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from .main import * 4 | from .display import * 5 | from .env import * 6 | from .repl import formatter 7 | 8 | disp = None 9 | V = None 10 | -------------------------------------------------------------------------------- /calculon/colour.py: -------------------------------------------------------------------------------- 1 | ESCAPES = { 2 | # reset 3 | 'reset': 0, 4 | 5 | # colours 6 | 'grey': 30, 7 | 'red': 31, 8 | 'green': 32, 9 | 'yellow': 33, 10 | 'blue': 34, 11 | 'magenta': 35, 12 | 'cyan': 36, 13 | 'white': 37, 14 | 15 | # background 16 | 'b_grey': 40, 17 | 'b_red': 41, 18 | 'b_green': 42, 19 | 'b_yellow': 43, 20 | 'b_blue': 44, 21 | 'b_magenta': 45, 22 | 'b_cyan': 46, 23 | 'b_white': 47, 24 | 25 | # attributes 26 | 'a_bold': 1, 27 | 'a_dark': 2, 28 | 'a_underline': 4, 29 | 'a_blink': 5, 30 | 'a_reverse': 7, 31 | 'a_concealed': 8 32 | } 33 | ESC_TEMPLATE = '\033[{}m' 34 | 35 | def escapes(): 36 | return ESCAPES 37 | 38 | def get_esc(name): 39 | return ESCAPES[name] 40 | 41 | def fmt_esc(name): 42 | return ESC_TEMPLATE.format(escapes()[name]) 43 | 44 | FMT_ESCAPES = dict((k, fmt_esc(k)) for k in ESCAPES) 45 | -------------------------------------------------------------------------------- /calculon/config/default.cfg: -------------------------------------------------------------------------------- 1 | # 2 | # default.cfg 3 | # 4 | # Default config. Probably don't mess with this. 5 | # 6 | 7 | { 8 | "bits": 64, 9 | "formats": ["h","d","o","a","b"], 10 | "variables": {}, 11 | "bin_mode": "auto", 12 | "align": "left", 13 | "padding": { 14 | "left": 2, "right": 2, 15 | "top": 2, "bottom": 1, 16 | "bintop": 1, "binbottom": 0, 17 | "vartop": 1, "varbottom": 0, 18 | "label": 2 19 | }, 20 | "attrs": { 21 | "header": ["bold", "black", "on_green"], 22 | "binlabel": ["bold", "red"], 23 | "vallabel": ["bold", "red"], 24 | "bval": ["green"], 25 | "hval": ["white"], 26 | "dval": ["magenta"], 27 | "oval": ["magenta"], 28 | "aval": ["cyan"], 29 | "uval": ["cyan"], 30 | "err": ["bold", "black", "on_green"], 31 | "expr": ["magenta"], 32 | "exprlabel":["bold", "red"] 33 | }, 34 | "repl_format": "{t.white}0x{v:x}{t.normal}" 35 | } 36 | -------------------------------------------------------------------------------- /calculon/config/example.cfg: -------------------------------------------------------------------------------- 1 | # 2 | # example.cfg 3 | # 4 | # Sample calculon config file. Copy this to "~/.calculon/config". 5 | # 6 | 7 | { 8 | # rounded up to nearest 16 or 32 depending on binary display width 9 | "bits": 64, 10 | 11 | # formats for main output display - hex, decimal, octal, ascii, binary 12 | "formats": ["h","d","o","a","b"], 13 | 14 | # variables to watch 15 | "variables": { 16 | "a": {"format": "h"}, 17 | "b": {"format": "h"} 18 | }, 19 | 20 | # binary display mode - wide or narrow 21 | "bin_mode": "auto", 22 | 23 | # alignment - left or right 24 | "align": "left", 25 | 26 | # display padding 27 | "padding": { 28 | # sides of entire display 29 | "left": 2, "right": 2, 30 | # above/below entire display 31 | "top": 2, "bottom": 1, 32 | # above/below binary display 33 | "bintop": 1, "binbottom": 0, 34 | # above/below variables 35 | "vartop": 1, "varbottom": 0, 36 | # before/after variables 37 | "label": 2 38 | }, 39 | 40 | # Attributes are lists made up of the following values: 41 | # Colours: black, white, red, green, blue, yellow, cyan, magenta 42 | # Backgrounds: on_black, on_white, on_red, on_green, on_blue, on_yellow, on_cyan, on_magenta 43 | # Attrs: altcharset, blink, bold, dim, normal, reverse, standout, underline 44 | 45 | "attrs": { 46 | "header": ["bold", "black", "on_green"], 47 | "binlabel": ["bold", "red"], 48 | "vallabel": ["bold", "red"], 49 | "bval": ["green"], 50 | "hval": ["white"], 51 | "dval": ["magenta"], 52 | "oval": ["magenta"], 53 | "aval": ["cyan"], 54 | "uval": ["cyan"], 55 | "err": ["bold", "black", "on_green"], 56 | "expr": ["magenta"], 57 | "exprlabel":["bold", "red"] 58 | }, 59 | 60 | # format string to use to format any numbers that fall out of the REPL 61 | # t is a blessed.Terminal 62 | # v is the value (int) 63 | "repl_format": "{t.white}0x{v:x}{t.normal}" 64 | } 65 | -------------------------------------------------------------------------------- /calculon/display.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | import sys 3 | import string 4 | import struct 5 | import Pyro4 6 | import os 7 | import time 8 | from blessed import Terminal 9 | 10 | from .env import * 11 | 12 | BIN_MODE_WIDTH_WIDE = 84 13 | BIN_MODE_WIDTH_NARROW = 44 14 | BIN_MODE_ROW_WIDE = 32 15 | BIN_MODE_ROW_NARROW = 16 16 | 17 | BASE_FMT = { 18 | 'h': '0x{0:X}', 19 | 'd': '{0:d}', 20 | 'o': '{0:o}', 21 | 'b': '{:0=%db}' 22 | } 23 | 24 | VALID_FORMATS = ['h','d','o','a','u','b'] 25 | 26 | needs_redraw = False 27 | 28 | def sigwinch_handler(n, frame): 29 | global needs_redraw 30 | needs_redraw = True 31 | 32 | # this kinda sucks, maybe do it without system 33 | class HiddenCursor(object): 34 | def __enter__(self): 35 | os.system('tput civis') 36 | 37 | def __exit__(self, type, value, traceback): 38 | os.system('tput cnorm') 39 | 40 | 41 | class CalculonDisplay (object): 42 | def __init__(self): 43 | self.term = Terminal() 44 | print(self.term.enter_fullscreen()) 45 | 46 | # parse config 47 | self.config = self.init_config(config) 48 | self.bin_mode = self.config['bin_mode'] 49 | self.cur_bin_mode = None 50 | self.bits = self.config['bits'] 51 | self.formats = self.config['formats'] 52 | self.align = self.config['align'] 53 | self.padding = self.config['padding'] 54 | self.attrs = self.config['attrs'] 55 | 56 | self.header = 'calculon' 57 | self.show_header = True 58 | 59 | # Watched variables 60 | self.lastval = 0 61 | 62 | # Watched expressions 63 | self.exprs = [] 64 | 65 | self.draw_state = { 66 | 'header': True, 'value': True, 'vallabel': True, 'binlabel': True, 67 | 'varlabel': True, 'varvalue': True, 'exprlabel': True, 'exprvalue': True, 68 | 'all': True 69 | } 70 | 71 | # set initial value 72 | self.update_value(0) 73 | 74 | @Pyro4.expose 75 | def are_you_there(self): 76 | return True 77 | 78 | def init_config(self, config): 79 | # update text attributes 80 | for sec in config['attrs']: 81 | config['attrs'][sec] = ''.join(['{t.' + x + '}' for x in config['attrs'][sec]]) 82 | 83 | return config 84 | 85 | def set_win(self, win, repl_win): 86 | self.win = win 87 | self.repl_win = repl_win 88 | self.update_value(0) 89 | self.redraw() 90 | 91 | def update_bin_mode(self): 92 | # detect bin display mode 93 | old_mode = self.cur_bin_mode 94 | if self.bin_mode == "auto": 95 | self.cur_bin_mode = "wide" if self.term.width >= BIN_MODE_WIDTH_WIDE else "narrow" 96 | if self.cur_bin_mode != old_mode: 97 | self.draw_state['all'] = True 98 | 99 | # round up bits to nearest row 100 | self.bin_row = BIN_MODE_ROW_NARROW if self.cur_bin_mode == "narrow" else BIN_MODE_ROW_WIDE 101 | if self.bits % self.bin_row > 0: 102 | self.bits += self.bin_row - (self.bits % self.bin_row) 103 | 104 | @Pyro4.expose 105 | def update_value(self, value): 106 | self.lastval = value 107 | self.draw_state['value'] = True 108 | self.redraw() 109 | 110 | @Pyro4.expose 111 | def set_exprs(self, values): 112 | self.exprs = values 113 | self.draw_state['exprvalue'] = True 114 | self.draw_state['exprlabel'] = True 115 | self.redraw() 116 | 117 | @Pyro4.expose 118 | def redraw(self, all=False): 119 | global needs_redraw 120 | 121 | self.update_bin_mode() 122 | 123 | if all or needs_redraw: 124 | self.draw_state['all'] = True 125 | needs_redraw = False 126 | if self.draw_state['all']: 127 | print(self.term.clear()) 128 | if self.draw_state['header'] or self.draw_state['all']: 129 | self.draw_header() 130 | self.draw_state['header'] = False 131 | if self.draw_state['value'] or self.draw_state['all']: 132 | self.clear_value() 133 | self.draw_value() 134 | self.draw_binary() 135 | self.draw_state['value'] = False 136 | if self.draw_state['vallabel'] or self.draw_state['all']: 137 | self.draw_value_labels() 138 | self.draw_binary_labels() 139 | self.draw_state['vallabel'] = False 140 | if self.draw_state['exprlabel'] or self.draw_state['all']: 141 | self.draw_expr_labels() 142 | self.draw_state['exprlabel'] = False 143 | if self.draw_state['exprvalue'] or self.draw_state['all']: 144 | self.clear_exprs() 145 | self.draw_exprs() 146 | self.draw_expr_labels() 147 | self.draw_state['exprvalue'] = False 148 | self.draw_state['all'] = False 149 | 150 | def get_value_formats(self): 151 | return [ x for x in self.formats if x in VALID_FORMATS and x != 'b' ] 152 | 153 | def num_rows(self): 154 | return self.offset_exprs() + self.num_rows_exprs() + self.padding['bottom'] 155 | 156 | def num_cols(self): 157 | if self.cur_bin_mode == "wide": 158 | c = BIN_MODE_WIDTH_WIDE + self.padding['left'] + self.padding['right'] 159 | else: 160 | c = BIN_MODE_WIDTH_NARROW + self.padding['left'] + self.padding['right'] 161 | return c 162 | 163 | def num_rows_val(self): 164 | return len(self.get_value_formats()) 165 | 166 | def num_rows_bin(self): 167 | return int(self.bits / self.bin_row + self.padding['bintop'] + self.padding['binbottom']) 168 | 169 | def num_rows_exprs(self): 170 | n = len(self.exprs) 171 | if n > 0: 172 | n += self.padding['vartop'] + self.padding['varbottom'] 173 | return n 174 | 175 | def offset_val(self): 176 | return self.padding['top'] 177 | 178 | def offset_bin(self): 179 | return self.offset_val() + self.num_rows_val() 180 | 181 | def offset_exprs(self): 182 | return self.offset_bin() + self.num_rows_bin() 183 | 184 | def draw_str(self, str, attr='', x=0, y=0): 185 | print((self.term.normal + self.term.move(y, x) + attr + str).format(t=self.term)) 186 | 187 | def draw_header(self): 188 | if self.show_header: 189 | self.draw_str(' ' * self.term.width, self.attrs['header'], 0, 0) 190 | self.draw_str(self.header, self.attrs['header'], self.padding['left'] ) 191 | 192 | def clear_value(self, varname=None): 193 | y = self.padding['top'] 194 | for fmt in self.get_value_formats(): 195 | w = self.num_cols() - self.padding['left'] - len(' ' + fmt) - self.padding['right'] - self.padding['label']*2 196 | x = self.padding['left'] + len(' ' + fmt) 197 | if varname: 198 | w -= len(varname) 199 | if self.align == 'right': 200 | x += len(varname) 201 | self.draw_str(' '*w, '', x, y) 202 | y += 1 203 | 204 | def draw_value(self, varname=None): 205 | y = self.padding['top'] 206 | for fmt in self.get_value_formats(): 207 | self.draw_value_at_row(self.lastval, fmt, y) 208 | y += 1 209 | 210 | def draw_value_at_row(self, value, fmt, row, label=None): 211 | if value == None: 212 | fmtd = '' 213 | attr = self.attrs['err'] 214 | else: 215 | fmtd = '' 216 | if fmt in ['h', 'd', 'o']: 217 | fmtd = BASE_FMT[fmt].format(value) 218 | attr = self.attrs[fmt + 'val'] 219 | elif fmt == 'a': 220 | s = ('{0:0=%dX}' % (self.bits/4)).format(value) 221 | a = [chr(int(s[i:i+2],16)) for i in range(0, len(s), 2)] 222 | for c in a: 223 | if c not in string.printable or c == '\n': 224 | fmtd += '.' 225 | else: 226 | fmtd += c 227 | if c in (r'{', r'}'): 228 | fmtd += c 229 | attr = self.attrs['aval'] 230 | elif fmt == 'u': 231 | # s = ('{0:0=%dX}' % (self.bits/4)).format(value) 232 | # a = [(chr(int(s[i:i+2],16)) + chr(int(s[i+2:i+4],16))).decode('utf-16') for i in range(0, len(s), 4)] 233 | attr = self.attrs['uval'] 234 | if self.align == 'right': 235 | col = self.num_cols() - self.padding['right'] - self.padding['label'] - len(fmtd) - 2 236 | self.draw_str(fmtd, attr, col, row) 237 | elif self.align == 'left': 238 | col = self.padding['left'] + len(' ' + fmt) + self.padding['label'] 239 | self.draw_str(fmtd, attr, col, row) 240 | 241 | def draw_value_labels(self): 242 | y = self.padding['top'] 243 | for fmt in self.get_value_formats(): 244 | self.draw_labels_at_row(fmt, y) 245 | y += 1 246 | 247 | def draw_labels_at_row(self, fmt, row, label=None): 248 | if self.align == 'right': 249 | col = self.num_cols() - self.padding['right'] - self.padding['label'] 250 | self.draw_str(fmt, self.attrs['vallabel'], col, row) 251 | if label != None: 252 | self.draw_str(label, self.attrs['vallabel'], self.padding['left'], row) 253 | elif self.align == 'left': 254 | col = self.padding['left'] 255 | self.draw_str(fmt, self.attrs['vallabel'], col, row) 256 | if label != None: 257 | self.draw_str(label, self.attrs['vallabel'], self.num_cols() - self.padding['right'] - len(label), row) 258 | 259 | def draw_binary(self): 260 | s = (BASE_FMT['b'] % self.bits).format(self.lastval) 261 | if len(s) > self.bits: 262 | s = s[len(s)-self.bits:] 263 | y = len(self.get_value_formats()) + self.padding['top'] + self.padding['bintop'] 264 | x = self.padding['left'] 265 | p = 0 266 | if self.lastval >= 1< 2: 23 | long = int 24 | 25 | def constant_factory(value): 26 | return functools.partial(next, itertools.repeat(value)) 27 | 28 | last_result = defaultdict(constant_factory(None)) 29 | last_line = "" 30 | repl = None 31 | watched_exprs = [] 32 | exprs = [] 33 | 34 | t = Terminal() 35 | 36 | lock = threading.Lock() 37 | 38 | 39 | def warn(msg): 40 | sys.stderr.write("Warning: %s\n" % msg) 41 | 42 | 43 | def safe_eval(expr): 44 | try: 45 | return expr() 46 | except Exception as e: 47 | warn(e) 48 | return 0 49 | 50 | 51 | def formatter(v): 52 | """ 53 | Default REPL formatter. 54 | """ 55 | if config.repl_format: 56 | return config.repl_format.format(v=v, t=t) 57 | else: 58 | return v 59 | 60 | 61 | def displayhook(v): 62 | try: 63 | print(formatter(v)) 64 | except: 65 | print(v) 66 | 67 | if isinstance(v, int): 68 | calculon.disp.update_value(v) 69 | 70 | 71 | class CalculonInterpreter(code.InteractiveInterpreter): 72 | def runsource(self, source, filename='', symbol='single', encode=True): 73 | global last_result, last_line, repl, env 74 | 75 | def update_display_exprs(): 76 | global exprs 77 | exprs = [(safe_eval(expr), fmt, label) for expr, fmt, label in watched_exprs] 78 | calculon.disp.set_exprs(exprs) 79 | 80 | lock.acquire() 81 | 82 | eval_source = True 83 | 84 | try: 85 | # if the code starts with an operator, prepend the _ variable 86 | tokens = tokenize.generate_tokens(lambda: source) 87 | for tokenType, tokenString, (startRow, startCol), (endRow, endCol), line in tokens: 88 | if tokenType == token.OP: 89 | source = '_ ' + source 90 | elif tokenType == token.NAME and tokenString == 'watch': 91 | toks = source.split() 92 | if len(toks) == 1: 93 | warn("syntax: watch [as ] ") 94 | return False 95 | 96 | # Special case `watch as d 97 | if toks[1] == "as": 98 | if len(toks) < 4: 99 | warn("syntax: watch [as ] ") 100 | return False 101 | fmt = toks[2] 102 | toks = toks[3:] 103 | else: 104 | fmt = 'h' 105 | toks = toks[1:] 106 | 107 | if fmt not in VALID_FORMATS: 108 | warn("invalid format: %s" % fmt) 109 | return False 110 | 111 | # We handle our code here, so we don't need to actually let the 112 | # backend poke anything 113 | expr = ' '.join(toks) 114 | try: 115 | thunk = eval("lambda: %s" % expr, self.locals) 116 | thunk() 117 | except Exception as e: 118 | warn(str(e)) 119 | return False 120 | 121 | watch_expr(thunk, expr, fmt) 122 | eval_source = False 123 | elif tokenType == token.NAME and tokenString == 'unwatch': 124 | toks = source.split() 125 | if len(toks) != 2: 126 | warn("syntax: unwatch ") 127 | return False 128 | try: 129 | exprid = int(toks[1]) 130 | except ValueError: 131 | warn("syntax: unwatch ") 132 | return False 133 | unwatch_expr(exprid) 134 | eval_source = False 135 | break 136 | 137 | # if we got an empty source line, re-evaluate the last line 138 | if len(source) == 0: 139 | source = last_line 140 | else: 141 | last_line = source 142 | 143 | temp_stdout = None 144 | if eval_source: 145 | # compile code 146 | try: 147 | code = self.compile(source, filename, symbol) 148 | except (OverflowError, SyntaxError, ValueError): 149 | self.showsyntaxerror(filename) 150 | return False 151 | if code is None: 152 | return True 153 | 154 | # if we got a valid code object, run it 155 | stdout = sys.stdout 156 | temp_stdout = six.StringIO() 157 | sys.stdout = temp_stdout 158 | self.runcode(code) 159 | sys.stdout = stdout 160 | 161 | # push functions and data into locals if they're not there 162 | if 'disp' not in self.locals: 163 | self.locals['disp'] = disp 164 | self.locals['swap'] = swap 165 | self.locals['switch'] = swap 166 | self.locals['_watch_expr'] = watch_expr 167 | self.locals['_unwatch_expr'] = unwatch_expr 168 | if calculon.V: 169 | calculon.V.callback = update_display_exprs 170 | calculon.V.start_watcher() 171 | self.locals['V'] = calculon.V 172 | 173 | # make sure there's a valid connection to the display 174 | try: 175 | calculon.disp.are_you_there() 176 | except: 177 | # reload the environment just in case the display has been started/restarted 178 | env = load_env() 179 | calculon.disp = Pyro4.Proxy(env.main_dir.uri.content) 180 | try: 181 | calculon.disp.are_you_there() 182 | except: 183 | calculon.disp = None 184 | if calculon.V: 185 | calculon.V.disp = calculon.disp 186 | 187 | # update value from last operation 188 | if calculon.formatter and temp_stdout: 189 | try: 190 | # only print with the formatter if the output is an int and nothing else 191 | output = temp_stdout.getvalue().strip() 192 | if output.endswith('L'): 193 | output = output[:-1] 194 | int(output) 195 | print(calculon.formatter(self.locals['__builtins__']['_'])) 196 | except Exception as e: 197 | # otherwise just print whatever came out of `exec` 198 | print(temp_stdout.getvalue(), end='') 199 | else: 200 | if temp_stdout: 201 | print(temp_stdout.getvalue(), end='') 202 | if calculon.disp: 203 | try: 204 | result = self.locals['__builtins__']['_'] 205 | if type(result) in [int, long] and result != last_result['_']: 206 | calculon.disp.update_value(result) 207 | last_result['_'] = result 208 | except KeyError as e: 209 | self.locals['__builtins__']['_'] = 0 210 | 211 | update_display_exprs() 212 | finally: 213 | lock.release() 214 | 215 | return False 216 | 217 | 218 | def watch_expr(expr, label, format='h'): 219 | watched_exprs.append((expr, format, label)) 220 | 221 | 222 | def unwatch_expr(idx): 223 | global exprs 224 | del watched_exprs[idx] 225 | del exprs[idx] 226 | calculon.disp.set_exprs(exprs) 227 | calculon.disp.redraw(True) 228 | 229 | 230 | def swap(value): 231 | h = hex(value)[2:] 232 | h = h.replace('L', '') 233 | if len(h) % 2 > 0: 234 | h = '0' + h 235 | bytes = re.findall('..', h) 236 | bytes.reverse() 237 | return int(''.join(bytes), 16) 238 | -------------------------------------------------------------------------------- /calculon/voltron_integration.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import socket 3 | import threading 4 | from requests.exceptions import ConnectionError 5 | 6 | import calculon 7 | from .env import * 8 | 9 | try: 10 | import voltron 11 | from voltron.repl import REPLClient 12 | HAS_VOLTRON = True 13 | except ImportError: 14 | HAS_VOLTRON = False 15 | 16 | 17 | if HAS_VOLTRON: 18 | class VoltronWatcher(threading.Thread): 19 | def __init__(self, callback=None): 20 | super(VoltronWatcher, self).__init__() 21 | self.callback = callback 22 | 23 | def run(self, *args, **kwargs): 24 | if self.callback: 25 | self.done = False 26 | self.client = voltron.core.Client() 27 | while not self.done: 28 | try: 29 | res = self.client.perform_request('version', block=True, timeout=1) 30 | if res.is_success: 31 | self.callback() 32 | except Exception as e: 33 | done = True 34 | 35 | class VoltronProxy(REPLClient): 36 | _instance = None 37 | exception = False 38 | watcher = None 39 | disp = None 40 | 41 | def __init__(self, callback=None): 42 | self.callback = callback 43 | self.start_watcher() 44 | calculon.voltron_proxy = self 45 | super(VoltronProxy, self).__init__() 46 | 47 | def start_watcher(self): 48 | if not self.watcher and self.callback: 49 | self.watcher = VoltronWatcher(self.callback) 50 | self.watcher.start() 51 | 52 | def stop_watcher(self): 53 | if self.watcher: 54 | calculon.V.watcher.done = True 55 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="calculon-calc", 5 | version="0.2", 6 | author="snare", 7 | author_email="snare@ho.ax", 8 | description=("A terminal-based programmer's calculator endowed with unholy acting talent by the Robot Devil"), 9 | license="Buy snare a beer", 10 | keywords="calculator programmer hex 64-bit", 11 | url="https://github.com/snare/calculon", 12 | packages=['calculon'], 13 | install_requires=['Pyro4', 'blessed', 'scruffington'], 14 | package_data={'calculon': ['config/*']}, 15 | entry_points={'console_scripts': ['calculon=calculon:main']}, 16 | zip_safe=False 17 | ) 18 | --------------------------------------------------------------------------------