-
Notifications
You must be signed in to change notification settings - Fork 0
/
handy.py
461 lines (366 loc) · 14.5 KB
/
handy.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
from blessed import Terminal
import pyperclip
from datetime import datetime
import time
import threading
import math
import re
import sys
running_on_windows = True
#define getch on platforms that don't support it
try:
from msvcrt import getch
except ImportError:
running_on_windows = False
def getch():
import tty
import termios
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
try:
tty.setraw(fd)
return sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old)
term = Terminal()
command_history = []
cur_input = ""
pending_input = []
last_curinput_linecount = 1
handy_name = "handy.txt"
last_size_x = 0
last_size_y = 0
redraw_event = threading.Event()
pending_input_lock = threading.Lock()
full_redraw_lock = threading.Lock()
full_redraw_pending = False
#helper functions that might be unnecessary if these are the same char codes on mac
def is_exit(val):
return val == '\x03'
def is_tab(val):
return val == '\t'
def is_paste(val):
return val == '\x16'
def is_backspace(val):
if running_on_windows:
return val == '\x08'
else:
return val == '\x7f'
def is_enter(val):
return val == '\r' or val == '\n'
def is_printable(val):
return val >= ' ' and val <= '~'
def is_escape_code(val):
return val == '\x1b'
def is_null(val):
return val == '\x00'
def get_keyboard_input():
while 1:
input = getch()
if running_on_windows:
try:
input = input.decode("utf-8")
except(UnicodeDecodeError):
continue
if is_escape_code(input):
getch()
getch()
elif is_null(input):
getch()
else:
pending_input_lock.acquire()
pending_input.append(input)
pending_input_lock.release()
redraw_event.set()
def check_for_terminal_resize():
global last_size_x
global last_size_y
global full_redraw_pending
while 1:
if last_size_x != term.width or last_size_y != term.height:
last_size_x = term.width
last_size_y = term.height
full_redraw_lock.acquire()
full_redraw_pending = True
full_redraw_lock.release()
redraw_event.set()
time.sleep(2.0)
# only redraws cur input to prevent flickering, triggers full redraw if curinput needs to grow to another line
def redraw_curinput(term, last_linecount):
trimmed_input = cur_input.replace("\n", "").replace("\r", "")
input_len = len(trimmed_input) +2
input_lines = int(input_len / term.width) + 1
if input_lines != last_linecount:
redraw(term)
return input_lines
else:
output_len = input_len
space_to_clear = term.width*input_lines - output_len
output = trimmed_input
for i in range(0, space_to_clear):
output += " "
cursor_x = (input_len) % term.width
print(term.move_xy(0,term.height-input_lines) + "> "+ output, end="", flush=True)
print(term.move_xy(cursor_x,term.height),end="", flush=True)
return input_lines
def redraw(term):
print(term.clear)
trimmed_input = cur_input.replace("\n", "").replace("\r", "")
input_len = len(trimmed_input) + len("> ")
input_lines = int(input_len / term.width) + 1
cursor_y = term.height - input_lines-1
first_command = True
for command in reversed(command_history):
if command == "\n":
continue
cmd_string = command.replace("\n", "")
string_len = len(cmd_string)
string_height = int(string_len / term.width) +1
#handle truncating multi line blocks near the top of visible history
shows_date = True
if cursor_y < string_height:
diff = string_height-cursor_y
line_len = term.width
cmd_string = cmd_string[line_len*(diff)+1:]
string_height -= diff
shows_date = False
if string_height > 1:
cursor_y -= string_height-1
if first_command:
print(term.move_xy(0,cursor_y) + term.reverse(cmd_string))
first_command = False
else:
if shows_date:
date_len = len(date_string())
print(term.move_xy(0,cursor_y) + term.bright_red(cmd_string[0:date_len]) + cmd_string[date_len:], flush=False)
else:
print(term.move_xy(0, cursor_y) + cmd_string, flush=False)
cursor_y-=1
if cursor_y <= 0:
break
print(term.move_xy(0,0) + term.center(handy_name.replace(".txt","").capitalize()), flush=False)
redraw_curinput(term, input_lines)
def full_match(reg_pattern, input_str):
match = re.match(reg_pattern, input_str)
if match is None:
return False
return match and match.end() == len(input_str)
def is_hex(input_str):
return full_match("[a-f0-9]+",input_str.lower())
def is_binary(input_str):
return full_match("[0-1]+", input_str.lower())
def is_decimal(input_str):
return full_match("\-?[0-9]+", input_str.lower())
def is_numeric(input_str):
lc_input = input_str.lower()
if lc_input.startswith('0x') and is_hex(lc_input[2:]):
return len(lc_input) > 2
if lc_input.startswith('0b') and is_binary(lc_input[2:]):
return len(lc_input) > 2
return is_decimal(input_str)
def convert_to_decimal(val):
if is_decimal(val):
return (int(trim_prefix(val)))
if val.startswith("0x") and is_hex(trim_prefix(val)):
return (int(val,16))
if val.startswith("0b") and is_binary(trim_prefix(val)):
return (int(val,2))
def trim_prefix(val):
if val.startswith("0x"):
return val[2:]
if val.startswith("0b"):
return val[2:]
return val
def parseNumericValue(valueStr):
lc_val = valueStr.lower()
radix = 10
if (lc_val.startswith('0x')):
if is_hex(lc_val[2:]):
radix = 16
else:
return "Invalid Hex Number"
if (lc_val.startswith('0b')):
if is_binary(lc_val[2:]):
radix = 2
else:
return "Invalid Binary Number"
if radix == 10 and is_decimal(lc_val) == False:
return "Invalid Decimal Number"
out_str = ""
if radix == 16:
out_str = str(int(lc_val,16)) + ", " + str(bin(int(lc_val,16)))
elif radix == 2:
out_str = str(int(lc_val,2)) + ", " + str(hex(int(lc_val,2)))
else:
#if it's a negative integer, we need to convert it first to two's complement, then display hex/binary
if valueStr[0] == '-':
pos_int = int(lc_val[1:], 10) #drop the sign
if pos_int > 2147483648: #if we're outside the range of a 32 bit signed int, bail
out_str = "Signed Int > 32 bits, Skipping Conversion"
return out_str
bin_str_noprefix = format(pos_int, "032b") #format num as a 0 padded 32 bit binary string
flipped_bin_str = bin_str_noprefix.replace("0", "@").replace("1", "0").replace("@","1") #janky bit flip
out_val = int(flipped_bin_str, 2) #convert back to int
out_val +=1
out_str = format(out_val, "#010x") + ", " + format(out_val, "#034b") #need 34 digits (32 + 2 for "0b")
else:
out_str = str(hex(int(lc_val,10))) + ", " + str(bin(int(lc_val, 10)))
return out_str
def parse_input_pattern(split_input, input_str):
#first we need to mark when a comment is added (if it is), since anything after that is moot
comments_start = len(split_input)-1
str_idx = 0
for cur_str in split_input:
if cur_str.startswith('//'):
comments_start = str_idx
break
str_idx +=1
found_error = False
input_str_comment_start = input_str.find("//")
if input_str_comment_start == -1:
input_str_comment_start = len(input_str)
for i in range(0, comments_start):
#comments_start can be modified at loop time
if i > comments_start:
break
cur_str = split_input[i]
if cur_str == "+" or cur_str == "-" or cur_str == "<<" or cur_str == ">>":
left = (split_input[i-1])
right = (split_input[i+1])
#if one of the operands isn't the right type, just bail on the whole expression
if is_numeric(left) == False or is_numeric(right) == False:
found_error = True
break
if cur_str == "+":
split_input[i] = (convert_to_decimal(left) + convert_to_decimal(right))
elif cur_str == "-":
split_input[i] = (convert_to_decimal(left) - convert_to_decimal(right))
elif cur_str == ">>":
split_input[i] = (convert_to_decimal(left) >> convert_to_decimal(right))
elif cur_str == "<<":
split_input[i] = (convert_to_decimal(left) << convert_to_decimal(right))
if left.startswith("0x") and is_hex(trim_prefix(left)):
hex_val = hex(int(split_input[i]))
split_input[i] = str(hex_val)
elif left.startswith("0b") and is_binary(trim_prefix(left)):
bin_val = bin(int(split_input[i]))
# if we are bit shifting a binary left-val, I want to preserve length when right shifting
if cur_str == "<<" or cur_str == ">>":
out_len = len(left)
if left[0] == "-" and str(bin_val)[0] != "-":
out_len -=1
format_str = "#0" + str(out_len) + "b"
split_input[i] = format(int(split_input[i]), format_str)
else:
split_input[i] = str(bin_val)
else:
split_input[i] = str(split_input[i])
del split_input[i-1]
del split_input[i]
comments_start -=2
i -= 1
out_str = input_str
if found_error == False:
out_str = input_str[0:input_str_comment_start] + " -> "
for cur_str in split_input:
out_str += cur_str + " "
return out_str
def date_string():
return datetime.now().strftime("%d/%m/%y %H:%M:%S")
def eval(input_str):
if len(input_str) == 0:
return
output_str = date_string() +": "
#handle the "I want to know the char code for this" case
if input_str.startswith("\'") and input_str.endswith('\'') and len(input_str) == 3:
output_str += input_str +" -> "+str(ord(input_str[1])) + ", " + str(hex(ord(input_str[1])))
return output_str
#expand minus sign so the string splits properly, unless it's the first
#shar in the string...since then it might be a negative int for conversion
input_str = input_str.replace("-", " - ")
if input_str[0:3] == " - ":
input_str = input_str.replace(" - ", "-", 1)
input_str = input_str.replace("+", " + ")
input_str = input_str.replace("<<", " << ")
input_str = input_str.replace(">>", " >> ")
split_input = input_str.split()
# there are only really two options, either an input string is a numeric operation
# (meaning it starts with a numeric), or it's a string that we just write to the log
if is_numeric(split_input[0]):
if len(split_input) == 1: #if there's only 1 numeric value, just convert it
output_str += split_input[0] + " -> "
output_str += parseNumericValue(split_input[0])
else: # otherwise we might have an arithmetic expression, or an inline comment, or both
output_str += parse_input_pattern(split_input, input_str)
else: #if the first input isn't numeric, it's just a string
output_str += input_str
return output_str
def resume_session(log_file):
log_file.seek(0)
line = log_file.readline()
while line:
command_history.append(line)
line = log_file.readline()
def log(resolved_cmd, log_file):
if resolved_cmd is None:
return
command_history.append(resolved_cmd)
log_file.write(resolved_cmd + "\n\n")
log_file.flush()
def main():
global handy_name
if len(sys.argv) -1 > 0:
handy_name = sys.argv[1]
if handy_name.endswith(".txt") == False:
handy_name+=".txt"
with open(handy_name, "a+") as handy_file:
resume_session(handy_file)
threading._start_new_thread(check_for_terminal_resize,())
threading._start_new_thread(get_keyboard_input, ())
global cur_input
global last_curinput_linecount
global full_redraw_pending
global pending_input_lock
global last_size_x
global last_size_y
wants_exit = False
try:
with term.fullscreen(), term.cbreak():
last_size_y = term.height
last_size_x = term.width
redraw(term)
while wants_exit == False:
redraw_event.wait(); # waits until input thread or term size thread signals a redraw
redraw_event.clear();
pending_input_lock.acquire()
for key in pending_input:
if is_exit(key):
wants_exit = True
if is_paste(key) or is_tab(key):
cur_input += pyperclip.paste()
elif is_backspace(key):
cur_input = cur_input[0:len(cur_input)-1]
elif is_enter(key) and len(cur_input) > 0:
cur_input += key
elif is_printable(key):
cur_input += key
pending_input.clear()
pending_input_lock.release()
if cur_input.endswith("\n") or cur_input.endswith("\r"):
trimmed_input = cur_input.replace("\n", "").replace("\r", "")
log(eval(trimmed_input), handy_file)
cur_input = ""
redraw(term)
last_curinput_linecount = 1
continue
#if len(cur_input) > 0:
last_curinput_linecount = redraw_curinput(term, last_curinput_linecount)
if full_redraw_pending == True:
full_redraw_lock.acquire();
full_redraw_pending = False;
full_redraw_lock.release();
redraw(term)
except(KeyboardInterrupt, SystemExit):
exit(0)
if __name__ == "__main__":
main()