-
Notifications
You must be signed in to change notification settings - Fork 1
/
histmanager-code
1018 lines (939 loc) · 47.2 KB
/
histmanager-code
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
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/python3
import json
import os
import re
import time
import traceback
from shutil import copyfile
import readline
prevenv = None # set by the export() function, accessed by the set_env function()
dbh = os.path.expanduser('~/.bash_history') # default bash history
fp = os.path.expanduser('~/.khist/') # path to project folder
allowed_input_options = [] # to be used for tab autocompletion
ignore_input_case = False # to be used for tab autocompletion
style_codes = None
def read_file(filepath):
"""returns a list containing all the lines of a file"""
f = open(filepath, 'r')
result = f.readlines()
f.close()
return result
def create_file(filepath):
"""[single line wrapper] creates (and) returns a file"""
return open(filepath, 'w')
def write_file(filepath, s):
"""writes provided string to provided file"""
f = create_file(filepath)
f.write(s)
f.close()
def write_dic (filepath, dictionary):
"""writes a dictionary to a file"""
f = open(filepath, 'w')
json.dump(dictionary, f)
f.close()
def read_dic (filepath):
"""reads and returns a dictionary from a file"""
r = {}
with open(filepath) as f:
r = json.load(f)
return r
def lines(env):
"""returns the number of lines in the history of an unarchived environment - by reading its history file"""
return len(read_file(fp + 'history/' + env + '.khtxt'))
def set_input_autocomplete_options(aio, iic):
"""
sets the various words to be utilised for tab completion, and ignore case settings for matching input to valid words
(str list) aio: a list input words to be autocompleted - contains each word that will comprise of one possible tab completion option
(bool) iic: whether or not to ignore input case when matching user input to tab completion words
"""
global allowed_input_options
global ignore_input_case
if ('' in aio):
aio.remove('')
allowed_input_options = [o for o in aio if (type(parse(o)) == type(''))]# sorted(aio)# unnecessary sort?
ignore_input_case = iic
def clear_input_autocomplete_options():
"""resets tab autocomplete words"""
set_input_autocomplete_options([], False)
def autocomplete_input(text, state):# disable/tweak double tab display of all possible options?
"""mandatory completer function to enable tab completion - set using the `readline.set_completer(autocomplete_input)` statement"""
#return [i for i in allowed_input_options if i.lower().startswith(text.lower())][state] #concise version
try:
if (ignore_input_case):
options = [cmnd for cmnd in allowed_input_options if cmnd.lower().startswith(text.lower())]
else:
options = [cmnd for cmnd in allowed_input_options if cmnd.startswith(text)]
if state < len(options):#redundant
return options[state]
else:
return None#redundant
except Exception as e:
dumperror(e, False)
except BaseException as e:
dumperror(e, True)
except:
pass
def rinput(prompt, autocompleteOptions, ignoreCase=True):
"""
[helper function] 'raw input', get user input following project guidelines - tab autocompletion, prompt format, input format, etc
(str) prompt: the input prompt for the user
(str list) autocompleteOptions: set of words to be used for tab autocompletion when fetching this input
(bool) ignoreCase(=True): whether or not to ignore case when matching tab autocompletion words
"""
set_input_autocomplete_options(autocompleteOptions, ignoreCase)
rin = input(prettify("{ENDFORMAT}$${PFORMAT}"+prompt+"${INFORMAT}"))
pprint("{ENDFORMAT}$", endc='')
clear_input_autocomplete_options()
return rin
## get input from the user
# prompt - the prompt to be displayed for taking user input
# acceptableOptions - the list of strings containing acceptable input option. [] indicates all options acceptable ('001' input is acceptable if acceptableOptions=['1'])
# repeatOptions - the list of strings that, if entered, call for a repeated prompt
# repeatPropmt - the prompt to use when prompting for input again
# eReturn - the value to return if the input is invalid. Default is None. Boolean False indicates that error should never occur, and function should keep forcibly prompting until acceptable input is obtained
# ignoreCase - boolean indicating whether to ignore case when parsing acceptableOptions and repeatOptions
## Duck Type / Default Values : no user prompt, all input is acceptable, prompt is never repeated, repeat prompt is same as prompt, None is returned for invalid input
def get_input(prompt='', acceptableOptions=[], repeatOptions=[], repeatPrompt=None, eReturn=None, ignoreCase=True):
"""
gets input from the user, passing it through basic error-proofing conditions
(str) prompt(=''): the input prompt for the user
(str list) acceptableOptions(=[]): set of words that comprise the acceptable input from the user, default configuration allows all options
(str list) repeatOptions(=[]): set of words that indicate erroneous input and a subsequent retry for user's input
(str) repeatPrompt(=prompt): the subsequent input prompt for the user
eReturn: the value to be returned if user's input is beyond both acceptableOptions and repeatOptions. if set to `False`, get_input enters repeated prompting mode - never returning a value until the user enters a valid, 'acceptableOption'
(bool) ignoreCase(=True): whether or not to ignore case when matching acceptableOptions and repeatOptions
"""
# Quirk - if all options are acceptable, ie acceptable options = [], then repeat options are not checked for.
if(repeatPrompt == None):
repeatPrompt = prompt
rin = rinput(prompt, acceptableOptions + repeatOptions, ignoreCase)
if (len(acceptableOptions) != 0):# ie all options aint acceptable
if (is_inside(rin, acceptableOptions, ignoreCase) == None):# ie the input is unacceptable
if (type(eReturn) == type(True) and eReturn == False):# dont return, despite error - force input
return get_input(repeatPrompt, acceptableOptions, repeatOptions, repeatPrompt, False, ignoreCase)
else:
if(is_inside(rin, repeatOptions, ignoreCase) != None):# error handling mandates repeated try for input
return get_input(repeatPrompt, acceptableOptions, repeatOptions, repeatPrompt, eReturn, ignoreCase)
else:# error handling - return an error condition
return eReturn # (probably returns None)
else:
return (is_inside(rin, acceptableOptions, ignoreCase))
return rin
# runs the obtained input through all the validation functions - returning the index of the particular validation function failed (int), if failed. OR returns the (string) input if successful
def smart_input(prompt='', validate=[], convertMap=None, autocompleteOptions=[], ignoreCase=True):
"""
gets input from the user, passing it through variable error proofing conditions - returns either the index of the validation function failed (int), or the valid user input (str)
(str) prompt(=''): the input prompt for the user
(function list) validate: functions that should validate the input text
(dict) convertMap(=None): for menu inputs, a map of the form {'1':'option1', '2':'option2' ... }, representing index:value pairs to convert obtained (index) input to a value before validation, returning
(str list) autocompleteOptions(=[]): set of words to be used for tab autocompletion when fetching input
(bool) ignoreCase(=True): whether or not this input is case insensitive
"""
rin = rinput(prompt, autocompleteOptions, ignoreCase)
if (convertMap != None):
ain = is_inside(str(parse(rin)), convertMap.keys())
if (ain != None):
rin = convertMap[ain]
i = 0
for f in validate:
if (not f(rin)):# if invalid
return i
i = i + 1
return rin# rin passed all validity tests
def get_yn(prompt, sInput='', sReturn=None):# should make sInput functionality optional
"""
gets yes/no input from the user, and returns a corresponding boolean (or None, if input string is the same as the specified special input)
(str) prompt: the prompt for getting user input
(str) sInput(=''): the special input string - the case when None is to be returned instead of a boolean
sReturn(=None): the value to be returned if input corresponds to special input
"""
rin = get_input(prompt+"", acceptableOptions=[sInput, 'y', 'yes', 'n', 'no'], eReturn=False)
if (rin == 'y' or rin == 'yes'):
return True
elif (rin == 'n' or rin == 'no'):
return False
return sReturn #special return - for non y/n input
#convert string into int, iff possible, or float, iff possible. Else return the same string
def parse(text):
"""converts string into int iff possible, or float if possible (and int not possible)"""
result = text
try:
result = int(text)#result = abs(int(text)) menus consider -1 == 1?
except ValueError:
try:
result = float(text)
except ValueError:
pass
return result
# check if target string(may represent float or int too!) is in the given list(int float equality applies for strings!)
# eg is_inside("001.00", ['1']) -> True
def is_inside(target, l, ignoreCase=True):#rewrite (pythonify) - as in autocomplete_input()
"""checks if target string is in l, accounting for different number representations (if strings represent numbers) and case differences"""
if (not ignoreCase):# redundant, never used?
if (target in l):# should technically traverse, use parse ...
return target #True
else:
return None #False
else:
for e in l:
if (parse(e.lower()) == parse(target.lower())):
return e #True
return None #False
# formats text ... "Welcome to ${EMPHASIS}HistManager{ENDFORMAT}$, version ${UNDERLINE}1.0{ENDFORMAT}$"
def prettify(text):
"""
prettifies text by converting ${format-option} to relevant format character
printing pretty text for this project was done by embedding it with formatting options surrounded by curly braces and preceeded (sometimes succeeded) by dollar signs.
eg: '${EMPHASIS}${RED}very strongly highlighted text{ENDFORMAT}$'
"""
for k in style_codes:
text = text.replace("${" + k.upper() + "}", style_codes[k])
normals = ["${END}", "${ENDFORMAT}", "{END}$", "{ENDFORMAT}$", "{NORMAL}$"]
for e in normals:
text = text.replace(e, style_codes['normal'])
return text
# print prettified text, redundant one line veneer function
def pprint(text, endc='\n'):
"""[wrapper function] prints text after formatting"""
print(prettify(text), end=endc)
def get_default_style_codes():
return {
"normal":"\033[0m",
"bold":"\033[1m",#currently unused
"underline":"\033[4m",#currently unused
"INFORMAT":"\033[33m",
"ERRFORMAT":"\033[31m",
"COMMENTARY":"\033[34m",
"EMPHASIS":"\033[1m",
"PFORMAT":"",#currently unused
#could be used later: ?
"black":"\033[30m",
"red":"\033[31m",
"green":"\033[32m",
"yellow":"\033[33m",
"blue":"\033[34m",
"magenta":"\033[35m",
"cyan":"\033[36m",
"white":"\033[37m",
"bblack":"\033[90m",
"bred":"\033[91m",
"bgreen":"\033[92m",
"byellow":"\033[93m",
"bblue":"\033[94m",
"bmagenta":"\033[95m",
"bcyan":"\033[96m",
"bwhite":"\033[97m",
"hblack":"\033[40m",
"hred":"\033[41m",
"hgreen":"\033[42m",
"hyellow":"\033[43m",
"hblue":"\033[44m",
"hmagenta":"\033[45m",
"hcyan":"\033[46m",
"hwhite":"\033[47m",
"bhblack":"\033[100m",
"bhred":"\033[101m",
"bhgreen":"\033[102m",
"bhyellow":"\033[103m",
"bhblue":"\033[104m",
"bhmagenta":"\033[105m",
"bhcyan":"\033[106m",
"bhwhite":"\033[107m"
}
def get_funcmap():
"""[wrapper function] returns the histmanager submenu function {user-code:functon map}"""
return {
'1':newenv,
'2':renamenv,
'3':mergenv,
'4':archivenv,
'5':viewarchives,
'6':unarchivenv,
'7':viewhist
}
def set_env():
"""
Sets the Environment of the Terminal Session
Prompts the user to select an environment. The list of environments is read from the 'environments.khtxt' file. A Number of intermediate steps may occur - calls to settings(), etc, depending on the users commands. Finally an environment is set - the 'currentenv.khtxt' file is updated, and the relevant history is imported into the terminal.
"""
env = get_env()
write_file(fp + 'currentenv.khtxt', env + '\n' + str(lines(env)))
os.rename(fp + 'history/' + env + '.khtxt', dbh)
print('')
pprint("${COMMENTARY}${EMPHASIS}" + env + "{ENDFORMAT}$${COMMENTARY} history sucessfully imported.{ENDFORMAT}$")
return env
# menu is the dictionary read from environments.khtxt
def get_env():
"""
[helper function] fetches user's environment setting choice
"""
d = read_dic(fp + 'environments.khtxt')
menu = display_menu(d)#menu = {"0":"HistManager-Settings", "1":"Master", "2":"Java", "3":"Python3" ... }
d['Master'] = 0 #! just for '' choice to function at input:
rchoice = ''
while (rchoice == ''):
rchoice = get_input("Enter choice: ", acceptableOptions=['', '0.1', '0.2', '0.3', '0.4', '0.5', '0.6', '0.7'] + [k for k in menu] + [v for v in menu.values()], eReturn=False)
if (rchoice == ''):
if (prevenv != None and (is_inside(prevenv, d) != None and d[is_inside(prevenv, d)] >= 0)):
return is_inside(prevenv, d)# is_inside handles case: prevenv 'java' renamed to "Java" (a case change)
if (rchoice == '0' or rchoice == 'HistManager-Settings'):
settings()
return get_env()
numchoice = parse(rchoice)
if (type(numchoice) == type(0)):
return menu[rchoice]
elif (type(numchoice) == type(0.0)):
get_funcmap()[str(int(numchoice*10))]()
pprint("returning to environment menu{ENDFORMAT}$")
return get_env()
return rchoice
def display_menu(menu):
"""[helper function] print the environment menu, and return the map of return values corresponding to the menu printed"""
pprint("\n${EMPHASIS}[Environment Menu]{ENDFORMAT}$ Select an environment for this session:")
pprint("(or enter '0' to go to HistManager-Settings)")
print("\t0) HistManager-Settings")
print("\t1) Master")
returnValues = print_envs(menu, startIndex=2)
returnValues["0"] = "HistManager-Settings"
returnValues["1"] = "Master"
return returnValues
# prints sorted envs from a dictionary
def print_envs(envs, showunarch=True, showarch=False, showsysgenarch=False, startIndex=1):
"""
[helper function] prints specified environments as a menu to the user - returning a map with {option:environment}
(dict) envs: the dictionary representing all the current environments (from environments.khtxt file)
(bool) showunarch(=True): whether to show unarchived environments
(bool) showarch(=False): whether to show archived environments
(bool) showsysgenarch(=False): whether to show system generated archived environments (Error-Env and Pre-Merge), if showing archived environments
"""
returnValues = {}#{"0":"HistManager-Settings", "1":"Master"}
for k in sorted(envs):
if ((not showarch) and envs[k] < 0):
continue
elif (showarch and envs[k] < 0 and not showsysgenarch and ('Error-Env' in k or 'Pre-Merge' in k)):
continue
if ((not showunarch) and envs[k] > 0):
continue
print('\t' + str(startIndex) + ') ' + str(k))
returnValues[str(startIndex)] = str(k)
startIndex = startIndex + 1
return returnValues
def settings():
"""
displays settings menu, and takes appropriate action based on user input
"""
print("")
pprint("${EMPHASIS}[HistManager Settings]{ENDFORMAT}$ Choose an option:")
print("\t1) Set up a new environment")
print("\t2) Rename an environment")
print("\t3) Merge two environments")
print("\t4) Archive an environment")
print("\t5) View all archived environments")
print("\t6) Restore an archived environment")
print("\t7) View environment history")
print("\t8) Leave settings and go back to environment menu")
rchoice = get_input("Enter choice: ", acceptableOptions=['1', '2', '3', '4', '5', '6', '7', '8', '17', '257', '65537', '73', '42', ''], eReturn=False)# add valid options 'exit', 'quit', 'return', etc?
if (rchoice != '8' and rchoice != ''):
pprint("(Press '${EMPHASIS}enter{ENDFORMAT}$' anytime to abort operation and return to HistManager Settings)")
if (int(rchoice) < 8):
get_funcmap()[rchoice]()
else:# PIZZAZZ:
disconfig(rchoice)
pprint("returning to settings menu{ENDFORMAT}$")
settings()
else:
pprint("${COMMENTARY}returning to environment menu{ENDFORMAT}$")
def is_valid_name(key):#, silent=False):
"""[helper function] intput validation function - ensures that the input environment is a valid (environment) name"""
if (not( re.match("^[A-Za-z0-9+_-]*$", key) )):
pprint("${COMMENTARY}environment names must be alphanumeric, please try again{ENDFORMAT}$")
elif (len(key)>=1 and not key[0].isalpha()):
pprint("${COMMENTARY}environment names must begin with letters, please try again{ENDFORMAT}$")
elif ((key.lower() == 'histmanager-settings') or (key.lower() == 'master') or ('archive' in key.lower()) or ('error-env' in key.lower()) or ('pre-merge' in key.lower())):
pprint("${COMMENTARY}can't create an environment named '" + key + "', please try a different name{ENDFORMAT}$")
else:
return True
return False
def doesnt_exist_already(key):
"""[helper function] intput validation function - ensures that the input environment doesn't exist already"""
env = is_inside(key, read_dic(fp + 'environments.khtxt').keys())
if (env != None):# key is already an environment
pprint("${COMMENTARY}environment '" + env + "' already exists (may be archived), please try a different name{ENDFORMAT}$")
return False
return True
def exists_already(key):
"""[helper function] intput validation function - ensures that the input environment exists already"""
env = is_inside(key, read_dic(fp + 'environments.khtxt').keys())
if (env == None):# key is unique
pprint("${COMMENTARY}environment '" + key + "' doesn't exist, please try again{ENDFORMAT}$")
return False
return True
def is_not_master(key):
"""[helper function] intput validation function - ensures that the input environment isn't Master"""
if (key.lower() == 'master'):
pprint("${COMMENTARY}can't modify the Master environment, please try a different environment{ENDFORMAT}$")
return False
return True
def master_flag(key):
"""[helper function] intput validation function - ensures that the input environment isn't master - but doesn't print any error messages"""
if (key.lower() == 'master'):
return False
return True
def is_nonempty(key):
"""[helper function] intput validation function - ensures that the input string is nonempty (isn't blank)"""
if (key == ''):
return False
else:
return True
def is_unarchived(key):
"""[helper function] intput validation function - ensures that the input environment (assumed to exist) is unarchived"""
d = read_dic(fp + 'environments.khtxt')
env = is_inside(key, d.keys())# key refers to env
if (d[env] < 0):# env is archived
pprint("${COMMENTARY}environment '" + env + "' is already archived, please try a different environment{ENDFORMAT}$")
return False
return True
def is_archived(key):
"""[helper function] intput validation function - ensures that the input environment (assumed to exist) is archived"""
d = read_dic(fp + 'environments.khtxt')
env = is_inside(key, d.keys())# key refers to env
if (d[env] > 0):# env is unarchived
pprint("${COMMENTARY}environment '" + env + "' is currently not archived, please try a different environment{ENDFORMAT}$")
return False
return True
def newenv(sysenv=None):
"""
Initialises a new environment
(str) sysenv(=None): if not None, the new environment's name is set to sysenv, and the function is 'silenced' - has no interaction with the terminal output
"""
filepath = fp + 'environments.khtxt'
d = read_dic(filepath)
key = -1
if (sysenv == None):
pprint('\n${EMPHASIS}[New Environment]{ENDFORMAT}$', endc='')
while (type(key) == type(0)):
key = smart_input("\nEnter the name of the new environment: ", [is_nonempty, is_valid_name, doesnt_exist_already])
if (key == 0):# input was empty - ''
return
pprint("${COMMENTARY}Environment '" + key + "' created successfully, ", endc='')#returning to environment menu")
else:# if system call
key = sysenv
d[key] = len(d) + 1
write_dic(filepath, d)
create_file(fp + 'history/' + key + '.khtxt')
def renamenv(sysoname=None, sysnname=None):
"""
Renames an existing environment
(str) oname(=None): if not None, this is the environment that is to be renamed
(str) nname(=None): if not None, this is the new environment name
Either both oname and nname should be provided by the calling code, or neither. If provided, the function is 'silenced' - has no interaction with the terminal output
"""
global prevenv
filepath = fp + 'environments.khtxt'
d = read_dic(filepath)
if (len(d) == 0):
pprint("${COMMENTARY}There are no environments to rename (Master can't be modified), ", endc='')#returning to settings menu")
#settings()
return
if ((sysoname is not None) and (sysnname is not None)):#ie system call
oname = sysoname
nname = sysnname
else:# not a system call
pprint("\n${EMPHASIS}[Rename Environment]{ENDFORMAT}$", endc='')
oname = -1
while (type(oname) == type(0)):
oname = smart_input("\nEnter the current name of the environment you wish to rename: ", [is_nonempty, is_not_master, exists_already], autocompleteOptions=[e for e in d])# order important
if (oname == 0):# input was empty - ''
return
oname = is_inside(oname, d)
nname = -1
while (type(nname) == type(0)):
if (nname == -1):# UNECCESSARY PIZZAZZ???
nname = smart_input("Enter the new name for '" + oname + "': ", [is_nonempty, is_valid_name])
else:
nname = smart_input("\nEnter the new name for '" + oname + "': ", [is_nonempty, is_valid_name])
if (nname == 0):# input was empty - ''
return
envt = is_inside(str(nname), d.keys())
if (envt != None):# nname is already an environment
if (nname.lower() != oname.lower() or nname == oname):
pprint("${COMMENTARY}environment '" + envt + "' already exists (may be archived), please try a different name{ENDFORMAT}$")
nname = -2
val = d[oname]
del d[oname]
d[nname] = val
write_dic(filepath, d)
if (oname == prevenv):
prevenv = nname
if (d[nname] < 0):
os.rename(fp + 'history/' + oname + str(val) + '-archive.khtxt', fp + 'history/' + nname + str(val) + '-archive.khtxt')
opt = ("Archived environment '" + oname + "' successfully renamed to '" + nname + "', ")#returning to environment menu")
else:
os.rename(fp + 'history/' + oname + '.khtxt', fp + 'history/' + nname + '.khtxt')
opt = ("Environment '" + oname + "' successfully renamed to '" + nname + "', ")#returning to environment menu")
if (sysoname == None and sysnname == None):
pprint('${COMMENTARY}'+opt, endc='')
def len_unarchived(envs):
"""[helper function] returns the number of currently unarchived environments"""
result = 0
for e in envs:
if(envs[e] > 0):
result = result + 1
return result
def archivenv(sysenv=None):
"""
Archives an existing environment
(str) sysenv(=None): if not None, this is the environment that is archived, and the function is 'silenced' - has no interaction with the terminal output
"""
filepath = fp + 'environments.khtxt'
d = read_dic(filepath)
if (len_unarchived(d) == 0):
pprint("${COMMENTARY}There are no environments to archive, ", endc='')#returning to settings menu")
return
if (sysenv is not None):#system call
denv = sysenv
else:
pprint("\n${EMPHASIS}[Archive Environment]{ENDFORMAT}$", endc='')
denv = -1
while (type(denv) == type(0)):
denv = smart_input("\nEnter the environment you wish to archive: ", [is_nonempty, is_not_master, exists_already, is_unarchived], autocompleteOptions=[e for e in d if d[e]>0])# order important
if (denv == 0):# input was empty - ''
return
denv = is_inside(denv, d)
d[denv] = d[denv]*(-1)
write_dic(filepath, d)
os.rename(fp + 'history/' + denv + '.khtxt', fp + 'history/' + denv + str(d[denv]) + '-archive.khtxt')
if (sysenv != None):# system call
return denv + str(d[denv]) + '-archive.khtxt'
else:
pprint("${COMMENTARY}Environment '" + denv + "' successfully archived, ", endc='')
def unarchivenv():
"""
Restores an archived environment
"""
filepath = fp + 'environments.khtxt'
d = read_dic(filepath)
if(len(d) - len_unarchived(d) == 0):
pprint("${COMMENTARY}There are no archived environments, ", endc='')
return
pprint("\n${EMPHASIS}[Restore Archived Environment]{ENDFORMAT}$")
print("Select the environment you wish to restore: ")
envmap = print_envs(d, showunarch=False, showarch=True, showsysgenarch=True)
rchoice = -1
while (type(rchoice) == type(0)):
if (rchoice == -1):# UNECCESSARY PIZZAZZ???
rchoice = smart_input("Enter choice: ", [is_nonempty, is_not_master, exists_already, is_archived], convertMap=envmap, autocompleteOptions=[e for e in d if d[e]<0])# order important
else:
rchoice = smart_input("\nEnter choice: ", [is_nonempty, is_not_master, exists_already, is_archived], convertMap=envmap, autocompleteOptions=[e for e in d if d[e]<0])# order important
if (rchoice == 0):# input was empty - ''
return
renv = is_inside(rchoice, d)
d[renv] = d[renv]*(-1)
write_dic(filepath, d)
os.rename(fp + 'history/' + renv + '-' + str(d[renv]) + '-archive.khtxt', fp + 'history/' + renv + '.khtxt')
pprint("${COMMENTARY}Environment '" + renv + "' successfully restored, ", endc='')
def viewarchives():
"""Displays archived environments"""
filepath = fp + 'environments.khtxt'
d = read_dic(filepath)
if(len(d) - len_unarchived(d) == 0):
pprint("${COMMENTARY}There are no archived environments, ", endc='')
return
pprint("\n${EMPHASIS}[View Archived Environments]{ENDFORMAT}$")
showsysgen = get_yn("Show unmodified system generated archives too? (y/n)\t")
if (showsysgen == None):
return
openvs = print_envs(d, showunarch=False, showarch=True, showsysgenarch=showsysgen)
if (len(openvs) == 0):
pprint("${COMMENTARY}There are no user generated archives, ", endc='')
else:
input(prettify("Press enter to return${COMMENTARY}"))
def mergenv():
"""Merges two existing environments"""
d = read_dic(fp + 'environments.khtxt')
if (len_unarchived(d) < 2):
pprint("${COMMENTARY}There aren't enough environments to merge, ", endc='')
return
pprint("\n${EMPHASIS}[Merge Environments]{ENDFORMAT}$")
print("(The first environment will be deleted and its history appended to the second environment)", end='')
oname = -1
while (type(oname) == type(0)):
oname = smart_input("\nEnter the name of the first environment you want to merge: ", [is_nonempty, is_not_master, exists_already, is_unarchived], autocompleteOptions=[e for e in d if d[e]>0])# order important
if (oname == 0):# input was empty - ''
return
env1 = is_inside(oname, d)
nname = -1
while (type(nname) == type(0)):
if (nname == -1):# UNECCESSARY PIZZAZZ???
nname = smart_input("Enter environment to merge '" + oname + "' into: ", [is_nonempty, is_not_master, exists_already, is_unarchived], autocompleteOptions=[e for e in d if d[e]>0])# order important
else:
nname = smart_input("\nEnter environment to merge '" + oname + "' into: ", [is_nonempty, is_not_master, exists_already, is_unarchived], autocompleteOptions=[e for e in d if d[e]>0])# order important
if (nname == 0):# input was empty - ''
return
env2 = is_inside(nname, d)
"""#UNKNOWN ERROR
env1 = -1
while (type(env1) == type(0)):
env1 = smart_input("\nEnter the name of the first environment you want to merge: ", [is_nonempty, is_not_master, exists_already, is_unarchived], autocompleteOptions=[e for e in d if d[e]>0])# order important
if (env1 == 0):# input was empty - ''
return
env1 = is_inside(env1, d)
env2 = -1
while (type(env2) == type(0)):
if (env2 == -1):# UNECCESSARY PIZZAZZ???
#print("1")
env2 = smart_input("Enter the name of the environment you want to merge " + env1 + "'s history into: ", [is_nonempty, is_not_master, exists_already, is_unarchived])#, autocompleteOptions=[e for e in d if d[e]>0])# order important
else:
#print("2")
env2 = smart_input("\nEnter the name of the environment you want to merge " + env1 + "'s history into: ", [is_nonempty, is_not_master, exists_already, is_unarchived])#, autocompleteOptions=[e for e in d if d[e]>0])# order important
if (env2 == 0):# input was empty - ''
return
env2 = is_inside(env2, d)
"""
envnum = 0 # mergecount
for k in sorted(d):
if ("Pre-Merge" in k):
envnum = int(k[-4:])
envnum = str(str(envnum+1).zfill(4))
renamenv(env1, env1 + "-Pre-Merge" + envnum)
renamenv(env2, env2 + "-Pre-Merge" + envnum)
aenv1 = archivenv(env1 + "-Pre-Merge" + envnum)
aenv2 = archivenv(env2 + "-Pre-Merge" + envnum)
newenv(env2)
copyfile(fp + 'history/' + aenv2, fp + 'history/' + env2 + '.khtxt')
fw = open(fp + 'history/' + env2 + '.khtxt', 'a')
alllines = read_file(fp + 'history/' + aenv1)
for l in alllines:
fw.write(l)
fw.close()
pprint("${COMMENTARY}History from '" + env1 + "' has now been merged with '" + env2 + "', ", endc='')
def viewhist(sysenv=None, displaymode=0):
"""
Displays or returns history from an unarchived environment
(str) sysenv(=None): if not None, this is the environment whos history is returned, and the function is 'silenced' - has no interaction with the terminal output; else the history is printed and not returned
(int) displaymode(=0): modes(1:entire history, 2:unique entries only, 3:unique commands only, -1:entire history - sorted alphabetically, -2:unique entries - sorted alphabetically, -1:unique commands - sorted alphabetically, OtherValues:invalid)
"""
filepath = fp + 'environments.khtxt'
d = read_dic(filepath)
#venv = None
if (sysenv is not None):
venv = sysenv
else :
pprint("\n${EMPHASIS}[View History]{ENDFORMAT}$", endc='')
venv = -1
while (type(venv) == type(0)):
venv = smart_input("\nEnter the environment who's history you want to view: ", [is_nonempty, master_flag, exists_already, is_unarchived], autocompleteOptions=([e for e in d if d[e]>0] + ['Master']))# order important
if (venv == 0):# input was empty - ''
return
elif (venv == 1):# master
venv = "Master"
if (venv != "Master"):
venv = is_inside(venv, d)
print("\t1) Show the entire history of \'" + venv + "\'")
print("\t2) Omit any repeated entries found in the history of \'" + venv + "\'")
print("\t3) Omit all repeated commands found in the history of \'" + venv + "\'")
choice = get_input("Enter choice: ", acceptableOptions=['', '1', '2', '3', '-1', '-2', '-3'], eReturn=False)
if (choice == ''):
return
displaymode = int(choice)
lines = read_file(fp + "history/" + venv + ".khtxt")
result = []
for i in range(0, len(lines)):
if (displaymode == 1 or displaymode == -1):
result.append(lines[i][0:-1])
continue
else:
unique = True
for j in range(0, i):
if (displaymode == 2 or displaymode == -2):
if (lines[j].split() == lines[i].split()):
unique = False
break
elif (displaymode == 3 or displaymode == -3):
if (lines[j].split()[0] == lines[i].split()[0]):
unique = False
break
if (unique):
result.append(lines[i][0:-1])
if (displaymode < 0):
result = sorted(result)
if (sysenv == None):
optofile = False
if (len(result) > 250):
optofile = get_yn("History is " + str(len(result)) + " entries long, redirect output to file? (y/n)\t")
if (optofile == None):
return
elif(len(result) == 0):
pprint("${COMMENTARY}History is currently empty, ", endc='')
return
if (optofile):
try:
fname = get_input("Enter unique filename (will be created in home directory):\t")
edit = ''
while(os.path.isfile(os.path.expanduser('~/') + fname) or os.path.isfile(os.path.expanduser('~/') + fname + '.txt')):
overwrite = get_yn("File already exists, overwrite? (y/n)\t")
if (overwrite == None):
return
elif (overwrite):
edit = "(over)"
break
else:
fname = get_input("\nEnter unique filename (will be created in home directory):\t")
if (fname[-4:] != '.txt'):
fname = fname + '.txt'
if (edit == '(over)'):
oconfirm = get_yn("Confirm - overwrite file \'" + fname + "\'? (y/n)\t")
if (oconfirm == None):
return
if (not oconfirm):
pprint("${COMMENTARY}aborting overwrite, ", endc='')
return
f = open(os.path.expanduser('~/') + fname, 'w')
for ele in result:
f.write(ele)
f.write("\n")
f.close()
print("History fetched from environment \'" + venv + "\' successfully " + edit + "written to \'" + fname + "\' in the home directory.")
input(prettify("Press enter to return${COMMENTARY}"))
except Exception:
pprint("${COMMENTARY}Error - unable to create file, ", endc='')#returning to settings menu")
return
else:
print("")
i = 1
for ele in result:
print(" " + ele)
input(prettify("\nPress enter to return${COMMENTARY}"))
else:
return result
def disconfig(rchoice):
"""
Configures display styles - the secret feature!
"""
if (rchoice == '42' or rchoice == '73'):
pprint("\n${EMPHASIS}[Configure Display]{ENDFORMAT}$ Choose a format number to execute: ")
pprint("\t3) Reset display configuration to default")
pprint("\t5) Toggle bold-font of all the ${EMPHASIS}emphasised{ENDFORMAT}$ text")
pprint("\t17) Set the colour of the HistManager ${COMMENTARY}commentary{ENDFORMAT}$")
pprint("\t257) Set the colour of the (typed) ${INFORMAT}input{ENDFORMAT}$ text")
pprint("\t65537) Set the colour of the input ${PFORMAT}prompt{ENDFORMAT}$")
rchoice = get_input("Enter choice: ", acceptableOptions=['3', '5', '17', '257', '65537', ''], eReturn=False)
if (rchoice != ''):
global style_codes
colours = ['normal', 'black', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', 'bblack', 'bgreen', 'byellow', 'bblue', 'bmagenta', 'bcyan', 'bwhite']
if (rchoice == '5'):
if (style_codes['EMPHASIS'] == "\033[1m"):
style_codes['EMPHASIS'] = ''
pprint("${COMMENTARY}Emphasised text will no longer be bold.")
else:
style_codes['EMPHASIS'] = "\033[1m"
pprint("${COMMENTARY}Emphasised text henceforth be bold.")
elif (rchoice == '17'):
nc = rinput("Enter new commentary colour: ", colours, True)
if (nc.lower() in colours):
style_codes['COMMENTARY'] = style_codes[nc.lower()]
pprint("${COMMENTARY}Colour " + nc + " set as commentary colour.")
else:
pprint("${COMMENTARY}invalid colour, ", endc='')
elif (rchoice == '257'):
nc = rinput("Enter new input text colour: ", colours, True)
if (nc.lower() in colours):
style_codes['INFORMAT'] = style_codes[nc.lower()]
pprint("${COMMENTARY}Colour " + nc + " set as input text colour.")
else:
pprint("${COMMENTARY}invalid colour, ", endc='')
elif (rchoice == '65537'):
nc = rinput("Enter new input prompt colour: ", colours, True)
if (nc.lower() in colours):
style_codes['PFORMAT'] = style_codes[nc.lower()]
pprint("${COMMENTARY}Colour " + nc + " set as input prompt colour.")
else:
pprint("${COMMENTARY}invalid colour, ", endc='')
else:
style_codes = get_default_style_codes()
pprint("${COMMENTARY}Thank you for your confidence in the default settings - your changes to them have now been discarded.")
write_dic(fp + 'styles.hmconfig', style_codes)
else:
pprint('${COMMENTARY}', endc='')
def export(silent=False):
"""
Exports history to the environment that was last set into the currentenv.khtxt file, also updates Master.khtxt
(bool) silent(=False): if True, then the function is 'silenced' - has no interaction with the terminal output
"""
global prevenv
fc = read_file(fp + "currentenv.khtxt")
env = fc[0].split()[0]
lines = int(fc[1].split()[0])
if (not silent):
prevenv = env
write_file(fp + "currentenv.khtxt", '')
if (env != 'Master'):
backup(lines, silent)# if silenced, function must be called because error had occured in previous terminal session ...
os.rename(dbh, fp + "history/" + env + ".khtxt")
if (not silent):
if (os.path.isfile(fp + '.bash_history_khbackup')):# if backup file exists, its not first time program is being executed
pprint("${COMMENTARY}Exporting ${EMPHASIS}" + env + "{ENDFORMAT}$${COMMENTARY} history from previous session.${ENDFORMAT}")
else:
pprint("${COMMENTARY}Importing all previous History to the Master Environment.{ENDFORMAT}$")# first ever executing must be with env = "Master"
time.sleep(1)
return
#rewrite without preconditions? (assuming that history is nonempty, and that Master != currentenv)
def get_last_master_entry():
"""[helper function]returns the last entry from the history of Master - assuming that it exists, and that currentenv != Master"""
hist = read_file(fp + 'history/Master.khtxt')
return hist[-1]# hist must be nonempty - return the last entry, of the form "exit\n"
def backup(lines, ercall=False):#def backup(lines):
"""
[helper function] Updates Master ('backs up' history to the Master environment)
(int) lines: the number of lines in .bash_history that had been executed before the last session itself, and don't need updating into master
"""
hist = read_file(dbh)
if (lines < len(hist) and get_last_master_entry() == hist[lines]):
lines += 1
fm = open(fp + 'history/Master.khtxt', 'a')
while (lines < len(hist)):
fm.write(hist[lines])
lines = lines + 1
fm.close()
pprint("${COMMENTARY}Updating Master Environment.{ENDFORMAT}$")#if (ercall): print("Updating Master Environment.")
def safe_export():
"""
An error-proofed version of export, will ensure no errors occur even if environment setting was incorrect/unsuccessful in the last terminal session
"""
f = open(fp + 'currentenv.khtxt', 'r')
ce = f.read()
f.close()
if ((ce != '')): #ie, file ~/.bash_history exists for sure
export()
else:
pprint("${ERRFORMAT}Error: Incorrect setting of Environment in the last Terminal Session{ENDFORMAT}$")
time.sleep(1)
pprint(" ${COMMENTARY}Attempting to Recover Lost History{ENDFORMAT}$")
time.sleep(1)
if (os.path.isfile(dbh)):
pprint(" ${COMMENTARY}Successfully found History from last Terminal Session{ENDFORMAT}$")
envnum = 1
d = read_dic(fp + 'environments.khtxt')
for k in d:
if ("Error-Env" in k):
envnum = envnum + 1
envnum = str(str(envnum).zfill(4))
print(" ", end='')
newenv("Error-Env" + envnum)
write_file(fp + 'currentenv.khtxt', "Error-Env" + envnum + "\n" + "0")
export(True)
archivenv("Error-Env" + envnum)
pprint(" ${COMMENTARY}Exporting History to the (archived) environment \'Error-Env" + envnum + "\'{ENDFORMAT}$")
time.sleep(2.5)
else:
time.sleep(1)
pprint(" ${COMMENTARY}No Commands were executed in the last Terminal Session{ENDFORMAT}$")#Could not find History from the last Terminal Session")
time.sleep(1.5)
def take_backup():
"""
Takes a backup of the entire history (ie, the Master environment)
The '~/.khist/history/Master.khtxt' file is copied to '~/.khist/history/BashHistBackup.bhtxt' and to '~/.khist/.bash_history_khbackup'
"""
copyfile(fp + 'history/Master.khtxt', fp + '.bash_history_khbackup')
copyfile(fp + 'history/Master.khtxt', fp + 'history/BashHistBackup.bhtxt')#deliberate use of different extension, to allow environment with name 'BashHistBackup'
def managehistory():
"""
Function to manage program control flow ('managehistory')
This happens through calls to other functions, in the following sequence (apart from tab autocompletion and output formatting setup):
safe_export -> to export history from previous session, and update Master
take_backup -> to take a backup of the history
set_env -> set the environment for the next session, if possible, based on the users choice
"""
global style_codes
try:
style_codes = read_dic(fp + 'styles.hmconfig')
except Exception as e:
style_codes = get_default_style_codes()
dumperror(e, False, True)
safe_export()
take_backup()
readline.parse_and_bind("tab: complete")
readline.set_completer(autocomplete_input)
set_env()#user selects environment
def dumperror(exc=None, base=None, silent=False):
"""
[helper function] Error handling function called if an Exception is caused at runtime
Error handling includes displaying an appropriate message to the user, and printing debug data to the ~/.khist/errorlog.khtxt file. This includes a timestamp, the stacktrace, and the state of the environments.khtxt and currentenv.khtxt files.
(BaseException) exc(=None): the Exception that occured
(bool) base(=None): if False, the error was caught by `except Exception` clause, if True the error was caught by the `except BaseException` clause
Integrity of the base argument is to be maintained by calling code. (ie, it is a precondition)
"""
try:
tm = time.localtime(time.time())
timestamp = str(tm[0]) + '-' + str(tm[1]) + '-' + str(tm[2]) + ' ' + str(tm[3]) + ':' + str(tm[4]) + ':' + str(tm[5])
ce = read_file(fp + 'currentenv.khtxt')
envs = read_file(fp + 'environments.khtxt')
f = open(fp + 'errorlog.khtxt', 'a')
f.write("\n\n\n")
f.write("==StartError================== " + timestamp + " ==================StartError==\n\n")
f.write(str(type(exc)))
f.write("\n")
if (base):
f.write("Caught at BaseException(2)")
else:
f.write("Caught at Exception(1)")
f.write("\n--------------------------------------------------------------------------\n")
f.write("CurrentEnv - " + str(ce) + "\n")
f.write("Environments - " + str(envs))
f.write("\n--------------------------------------------------------------------------\n")
f.write(traceback.format_exc())
f.write("\n==EndError==================== " + timestamp + " ====================EndError==\n\n\n\n")
f.close()
if (not silent):
print('\033[0m\033[31m')
print("")
print("\tERROR : An Error has Occured. Please Report it to [email protected]")
print("\t Meanwhile, you can try restarting the Terminal to check if the Problem Persists.")
print("\033[0m")
time.sleep(3)
except:
traceback.print_exc()
print('\033[0m\033[31m')
print("-------------------------------------------------------------")
print("Unknown DumpError - Please Report it to [email protected]")
print("-------------------------------------------------------------")
print('')
time.sleep(3)
print('\033[0m')
def main():
"""
Main function - invoked with each call to the script
An error proofed verion of managehistory(), which appropriately handles errors using dumperror()
"""
try:
#write_dic(fp + 'styles.hmconfig', get_default_style_codes())
managehistory()
except Exception as e:
dumperror(e, False)
except BaseException as e:
dumperror(e, True)
except:
traceback.print_exc()
print('\033[0m\033[31m')
print("-----------------------------------------------------------------")
print("Unknown MainError - Please Report it to to [email protected]")
print("-----------------------------------------------------------------")
print('')
print('\033[0m')
time.sleep(3)