multiSSH3 5.46__tar.gz → 5.48__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of multiSSH3 might be problematic. Click here for more details.
- {multissh3-5.46 → multissh3-5.48}/PKG-INFO +1 -1
- {multissh3-5.46 → multissh3-5.48}/multiSSH3.egg-info/PKG-INFO +1 -1
- {multissh3-5.46 → multissh3-5.48}/multiSSH3.egg-info/SOURCES.txt +6 -1
- {multissh3-5.46 → multissh3-5.48}/multiSSH3.py +63 -21
- multissh3-5.48/test/test.py +316 -0
- multissh3-5.48/test/testCurses.py +455 -0
- multissh3-5.48/test/testCursesOld.py +289 -0
- multissh3-5.48/test/testPerfCompact.py +31 -0
- multissh3-5.48/test/testPerfExpand.py +14 -0
- {multissh3-5.46 → multissh3-5.48}/README.md +0 -0
- {multissh3-5.46 → multissh3-5.48}/multiSSH3.egg-info/dependency_links.txt +0 -0
- {multissh3-5.46 → multissh3-5.48}/multiSSH3.egg-info/entry_points.txt +0 -0
- {multissh3-5.46 → multissh3-5.48}/multiSSH3.egg-info/requires.txt +0 -0
- {multissh3-5.46 → multissh3-5.48}/multiSSH3.egg-info/top_level.txt +0 -0
- {multissh3-5.46 → multissh3-5.48}/setup.cfg +0 -0
- {multissh3-5.46 → multissh3-5.48}/setup.py +0 -0
|
@@ -6,4 +6,9 @@ multiSSH3.egg-info/SOURCES.txt
|
|
|
6
6
|
multiSSH3.egg-info/dependency_links.txt
|
|
7
7
|
multiSSH3.egg-info/entry_points.txt
|
|
8
8
|
multiSSH3.egg-info/requires.txt
|
|
9
|
-
multiSSH3.egg-info/top_level.txt
|
|
9
|
+
multiSSH3.egg-info/top_level.txt
|
|
10
|
+
test/test.py
|
|
11
|
+
test/testCurses.py
|
|
12
|
+
test/testCursesOld.py
|
|
13
|
+
test/testPerfCompact.py
|
|
14
|
+
test/testPerfExpand.py
|
|
@@ -34,6 +34,7 @@ import uuid
|
|
|
34
34
|
import tempfile
|
|
35
35
|
import math
|
|
36
36
|
from itertools import count
|
|
37
|
+
import queue
|
|
37
38
|
|
|
38
39
|
try:
|
|
39
40
|
# Check if functiools.cache is available
|
|
@@ -46,7 +47,7 @@ except AttributeError:
|
|
|
46
47
|
# If neither is available, use a dummy decorator
|
|
47
48
|
def cache_decorator(func):
|
|
48
49
|
return func
|
|
49
|
-
version = '5.
|
|
50
|
+
version = '5.48'
|
|
50
51
|
VERSION = version
|
|
51
52
|
COMMIT_DATE = '2025-01-30'
|
|
52
53
|
|
|
@@ -90,31 +91,68 @@ def signal_handler(sig, frame):
|
|
|
90
91
|
os.system(f'pkill -ef {os.path.basename(__file__)}')
|
|
91
92
|
sys.exit(0)
|
|
92
93
|
|
|
94
|
+
# def input_with_timeout_and_countdown(timeout, prompt='Please enter your selection'):
|
|
95
|
+
# """
|
|
96
|
+
# Read an input from the user with a timeout and a countdown.
|
|
97
|
+
|
|
98
|
+
# Parameters:
|
|
99
|
+
# timeout (int): The timeout value in seconds.
|
|
100
|
+
# prompt (str): The prompt message to display to the user. Default is 'Please enter your selection'.
|
|
101
|
+
|
|
102
|
+
# Returns:
|
|
103
|
+
# str or None: The user input if received within the timeout, or None if no input is received.
|
|
104
|
+
# """
|
|
105
|
+
# import select
|
|
106
|
+
# # Print the initial prompt with the countdown
|
|
107
|
+
# eprint(f"{prompt} [{timeout}s]: ", end='', flush=True)
|
|
108
|
+
# # Loop until the timeout
|
|
109
|
+
# for remaining in range(timeout, 0, -1):
|
|
110
|
+
# # If there is an input, return it
|
|
111
|
+
# # this only works on linux
|
|
112
|
+
# if sys.stdin in select.select([sys.stdin], [], [], 0)[0]:
|
|
113
|
+
# return input().strip()
|
|
114
|
+
# # Print the remaining time
|
|
115
|
+
# eprint(f"\r{prompt} [{remaining}s]: ", end='', flush=True)
|
|
116
|
+
# # Wait a second
|
|
117
|
+
# time.sleep(1)
|
|
118
|
+
# # If there is no input, return None
|
|
119
|
+
# return None
|
|
120
|
+
|
|
93
121
|
def input_with_timeout_and_countdown(timeout, prompt='Please enter your selection'):
|
|
94
122
|
"""
|
|
95
|
-
Read
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
timeout (int): The timeout value in seconds.
|
|
99
|
-
prompt (str): The prompt message to display to the user. Default is 'Please enter your selection'.
|
|
100
|
-
|
|
101
|
-
Returns:
|
|
102
|
-
str or None: The user input if received within the timeout, or None if no input is received.
|
|
123
|
+
Read input from the user with a timeout (cross-platform).
|
|
124
|
+
If the user does not enter any input within `timeout` seconds, return None.
|
|
125
|
+
Otherwise, return the input string.
|
|
103
126
|
"""
|
|
104
|
-
|
|
105
|
-
|
|
127
|
+
# Queue to receive user input from the background thread
|
|
128
|
+
input_queue = queue.Queue()
|
|
129
|
+
def read_input():
|
|
130
|
+
# Read line from stdin and put it in the queue
|
|
131
|
+
user_input = sys.stdin.readline()
|
|
132
|
+
input_queue.put(user_input)
|
|
133
|
+
# Start a thread that will block on input()
|
|
134
|
+
input_thread = threading.Thread(target=read_input, daemon=True)
|
|
135
|
+
input_thread.start()
|
|
136
|
+
# Print the initial prompt
|
|
106
137
|
eprint(f"{prompt} [{timeout}s]: ", end='', flush=True)
|
|
107
|
-
#
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if
|
|
111
|
-
|
|
112
|
-
|
|
138
|
+
# Countdown loop
|
|
139
|
+
start_time = time.time()
|
|
140
|
+
while True:
|
|
141
|
+
# Check if the input thread has finished (i.e., user pressed Enter)
|
|
142
|
+
if not input_queue.empty():
|
|
143
|
+
# We got user input
|
|
144
|
+
user_input = input_queue.get().strip()
|
|
145
|
+
eprint() # move to the next line
|
|
146
|
+
return user_input
|
|
147
|
+
elapsed = int(time.time() - start_time)
|
|
148
|
+
remaining = timeout - elapsed
|
|
149
|
+
if remaining <= 0:
|
|
150
|
+
# Time is up, no input
|
|
151
|
+
eprint() # move to the next line
|
|
152
|
+
return None
|
|
153
|
+
# Update prompt countdown
|
|
113
154
|
eprint(f"\r{prompt} [{remaining}s]: ", end='', flush=True)
|
|
114
|
-
# Wait a second
|
|
115
155
|
time.sleep(1)
|
|
116
|
-
# If there is no input, return None
|
|
117
|
-
return None
|
|
118
156
|
|
|
119
157
|
@cache_decorator
|
|
120
158
|
def getIP(hostname: str,local=False):
|
|
@@ -1879,7 +1917,11 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
|
|
|
1879
1917
|
# with open('keylog.txt','a') as f:
|
|
1880
1918
|
# f.write(str(key)+'\n')
|
|
1881
1919
|
if key == 410: # 410 is the key code for resize
|
|
1882
|
-
return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Terminal resize requested')
|
|
1920
|
+
return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Terminal resize requested')
|
|
1921
|
+
# if the user pressed ctrl + d and the last line is empty, we will exit by adding 'exit\n' to the last line
|
|
1922
|
+
elif key == 4 and not __keyPressesIn[-1]:
|
|
1923
|
+
__keyPressesIn[-1].extend('exit\n')
|
|
1924
|
+
__keyPressesIn.append([])
|
|
1883
1925
|
elif key == 95 and not __keyPressesIn[-1]: # 95 is the key code for _
|
|
1884
1926
|
# if last line is empty, we will reconfigure the wh to be smaller
|
|
1885
1927
|
if min_line_len != 1:
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import string
|
|
3
|
+
|
|
4
|
+
def validate_expand_hostname(hostname):
|
|
5
|
+
# Assuming this function is already implemented.
|
|
6
|
+
return [hostname]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def old_expand_hostname(text,validate=True):
|
|
10
|
+
'''
|
|
11
|
+
Expand the hostname range in the text.
|
|
12
|
+
Will search the string for a range ( [] encloused and non enclosed number ranges).
|
|
13
|
+
Will expand the range, validate them using validate_expand_hostname and return a list of expanded hostnames
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
text (str): The text to be expanded
|
|
17
|
+
validate (bool, optional): Whether to validate the hostname. Defaults to True.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
set: A set of expanded hostnames
|
|
21
|
+
'''
|
|
22
|
+
expandinghosts = [text]
|
|
23
|
+
expandedhosts = set()
|
|
24
|
+
# all valid alphanumeric characters
|
|
25
|
+
alphanumeric = string.digits + string.ascii_letters
|
|
26
|
+
while len(expandinghosts) > 0:
|
|
27
|
+
hostname = expandinghosts.pop()
|
|
28
|
+
match = re.search(r'\[(.*?-.*?)\]', hostname)
|
|
29
|
+
if not match:
|
|
30
|
+
expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
|
|
31
|
+
continue
|
|
32
|
+
try:
|
|
33
|
+
range_start, range_end = match.group(1).split('-')
|
|
34
|
+
except ValueError:
|
|
35
|
+
expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
|
|
36
|
+
continue
|
|
37
|
+
range_start = range_start.strip()
|
|
38
|
+
range_end = range_end.strip()
|
|
39
|
+
if not range_end:
|
|
40
|
+
if range_start.isdigit():
|
|
41
|
+
range_end = '9'
|
|
42
|
+
elif range_start.isalpha() and range_start.islower():
|
|
43
|
+
range_end = 'z'
|
|
44
|
+
elif range_start.isalpha() and range_start.isupper():
|
|
45
|
+
range_end = 'Z'
|
|
46
|
+
else:
|
|
47
|
+
expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
|
|
48
|
+
continue
|
|
49
|
+
if not range_start:
|
|
50
|
+
if range_end.isdigit():
|
|
51
|
+
range_start = '0'
|
|
52
|
+
elif range_end.isalpha() and range_end.islower():
|
|
53
|
+
range_start = 'a'
|
|
54
|
+
elif range_end.isalpha() and range_end.isupper():
|
|
55
|
+
range_start = 'A'
|
|
56
|
+
else:
|
|
57
|
+
expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
|
|
58
|
+
continue
|
|
59
|
+
if range_start.isdigit() and range_end.isdigit():
|
|
60
|
+
padding_length = min(len(range_start), len(range_end))
|
|
61
|
+
format_str = "{:0" + str(padding_length) + "d}"
|
|
62
|
+
for i in range(int(range_start), int(range_end) + 1):
|
|
63
|
+
formatted_i = format_str.format(i)
|
|
64
|
+
if '[' in hostname:
|
|
65
|
+
expandinghosts.append(hostname.replace(match.group(0), formatted_i, 1))
|
|
66
|
+
else:
|
|
67
|
+
expandedhosts.update(validate_expand_hostname(hostname.replace(match.group(0), formatted_i, 1)) if validate else [hostname])
|
|
68
|
+
else:
|
|
69
|
+
if all(c in string.hexdigits for c in range_start + range_end):
|
|
70
|
+
for i in range(int(range_start, 16), int(range_end, 16)+1):
|
|
71
|
+
if '[' in hostname:
|
|
72
|
+
expandinghosts.append(hostname.replace(match.group(0), format(i, 'x'), 1))
|
|
73
|
+
else:
|
|
74
|
+
expandedhosts.update(validate_expand_hostname(hostname.replace(match.group(0), format(i, 'x'), 1)) if validate else [hostname])
|
|
75
|
+
else:
|
|
76
|
+
try:
|
|
77
|
+
start_index = alphanumeric.index(range_start)
|
|
78
|
+
end_index = alphanumeric.index(range_end)
|
|
79
|
+
for i in range(start_index, end_index + 1):
|
|
80
|
+
if '[' in hostname:
|
|
81
|
+
expandinghosts.append(hostname.replace(match.group(0), alphanumeric[i], 1))
|
|
82
|
+
else:
|
|
83
|
+
expandedhosts.update(validate_expand_hostname(hostname.replace(match.group(0), alphanumeric[i], 1)) if validate else [hostname])
|
|
84
|
+
except ValueError:
|
|
85
|
+
expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
|
|
86
|
+
return expandedhosts
|
|
87
|
+
|
|
88
|
+
def expand_hostname(text, validate=True):
|
|
89
|
+
'''
|
|
90
|
+
Expand the hostname range in the text.
|
|
91
|
+
Will search the string for a range ( [] enclosed and non-enclosed number ranges).
|
|
92
|
+
Will expand the range, validate them using validate_expand_hostname and return a list of expanded hostnames
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
text (str): The text to be expanded
|
|
96
|
+
validate (bool, optional): Whether to validate the hostname. Defaults to True.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
set: A set of expanded hostnames
|
|
100
|
+
'''
|
|
101
|
+
expandinghosts = [text]
|
|
102
|
+
expandedhosts = set()
|
|
103
|
+
# all valid alphanumeric characters
|
|
104
|
+
alphanumeric = string.digits + string.ascii_letters
|
|
105
|
+
while len(expandinghosts) > 0:
|
|
106
|
+
hostname = expandinghosts.pop()
|
|
107
|
+
match = re.search(r'\[(.*?)]', hostname)
|
|
108
|
+
if not match:
|
|
109
|
+
expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
|
|
110
|
+
continue
|
|
111
|
+
group = match.group(1)
|
|
112
|
+
parts = group.split(',')
|
|
113
|
+
for part in parts:
|
|
114
|
+
part = part.strip()
|
|
115
|
+
if '-' in part:
|
|
116
|
+
try:
|
|
117
|
+
range_start,_, range_end = part.partition('-')
|
|
118
|
+
except ValueError:
|
|
119
|
+
expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
|
|
120
|
+
continue
|
|
121
|
+
range_start = range_start.strip()
|
|
122
|
+
range_end = range_end.strip()
|
|
123
|
+
if range_start.isdigit() and range_end.isdigit():
|
|
124
|
+
padding_length = min(len(range_start), len(range_end))
|
|
125
|
+
format_str = "{:0" + str(padding_length) + "d}"
|
|
126
|
+
for i in range(int(range_start), int(range_end) + 1):
|
|
127
|
+
formatted_i = format_str.format(i)
|
|
128
|
+
expandinghosts.append(hostname.replace(match.group(0), formatted_i, 1))
|
|
129
|
+
elif all(c in string.hexdigits for c in range_start + range_end):
|
|
130
|
+
for i in range(int(range_start, 16), int(range_end, 16) + 1):
|
|
131
|
+
expandinghosts.append(hostname.replace(match.group(0), format(i, 'x'), 1))
|
|
132
|
+
else:
|
|
133
|
+
try:
|
|
134
|
+
start_index = alphanumeric.index(range_start)
|
|
135
|
+
end_index = alphanumeric.index(range_end)
|
|
136
|
+
for i in range(start_index, end_index + 1):
|
|
137
|
+
expandinghosts.append(hostname.replace(match.group(0), alphanumeric[i], 1))
|
|
138
|
+
except ValueError:
|
|
139
|
+
expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
|
|
140
|
+
else:
|
|
141
|
+
expandinghosts.append(hostname.replace(match.group(0), part, 1))
|
|
142
|
+
return expandedhosts
|
|
143
|
+
|
|
144
|
+
# Example usage
|
|
145
|
+
# text = "test[2-1c]"
|
|
146
|
+
# print(expand_hostname(text))
|
|
147
|
+
# print(old_expand_hostname(text))
|
|
148
|
+
|
|
149
|
+
# Test cases for expand_hostname and old_expand_hostname functions
|
|
150
|
+
|
|
151
|
+
def run_tests():
|
|
152
|
+
test_cases = [
|
|
153
|
+
# Simple numeric range
|
|
154
|
+
("test[1-3]", {"test1", "test2", "test3"}),
|
|
155
|
+
|
|
156
|
+
# Simple letter range
|
|
157
|
+
("host[a-c]", {"hosta", "hostb", "hostc"}),
|
|
158
|
+
|
|
159
|
+
# Simple hexadecimal range
|
|
160
|
+
("hex[0-3]", {"hex0", "hex1", "hex2", "hex3"}),
|
|
161
|
+
|
|
162
|
+
# Numeric range with padding
|
|
163
|
+
("server[001-003]", {"server001", "server002", "server003"}),
|
|
164
|
+
|
|
165
|
+
# Letter and numeric mixed ranges
|
|
166
|
+
("mixed[a-b,1-2]", {"mixeda", "mixedb", "mixed1", "mixed2"}),
|
|
167
|
+
|
|
168
|
+
# Multiple ranges with different characters
|
|
169
|
+
("complex[a-c,1-2,x]", {"complexa", "complexb", "complexc", "complex1", "complex2", "complexx"}),
|
|
170
|
+
|
|
171
|
+
# Hexadecimal range with uppercase letters
|
|
172
|
+
("hexrange[A-C]", {"hexrangea", "hexrangeb", "hexrangec"}),
|
|
173
|
+
|
|
174
|
+
# Multiple numeric ranges
|
|
175
|
+
("num[1-2,5-6]", {"num1", "num2", "num5", "num6"}),
|
|
176
|
+
|
|
177
|
+
# Overlapping numeric range
|
|
178
|
+
("overlap[3-6,5-8]", {"overlap3", "overlap4", "overlap5", "overlap6", "overlap7", "overlap8"}),
|
|
179
|
+
|
|
180
|
+
# Invalid input without range brackets
|
|
181
|
+
("invalid_host", {"invalid_host"}),
|
|
182
|
+
|
|
183
|
+
# No content inside brackets
|
|
184
|
+
("empty[]", {"empty"}),
|
|
185
|
+
|
|
186
|
+
# Invalid range format with non-numeric or non-alphanumeric character
|
|
187
|
+
("invalid[@-%]", {"invalid[@-%]"}),
|
|
188
|
+
|
|
189
|
+
# No valid starting character for range
|
|
190
|
+
#("start[0-9A-Z]", {"start0", "start1", "start2", "start3", "start4", "start5", "start6", "start7", "start8", "start9"}),
|
|
191
|
+
|
|
192
|
+
# Empty input
|
|
193
|
+
("", {""}),
|
|
194
|
+
|
|
195
|
+
# Simple hostname without any ranges
|
|
196
|
+
("hostname", {"hostname"}),
|
|
197
|
+
("hostname[x-z]", {"hostnamex","hostnamey","hostnamez"}),
|
|
198
|
+
|
|
199
|
+
# Mixed valid and invalid ranges
|
|
200
|
+
#("mix[1-3, x-]", {"mix1", "mix2", "mix3", "mix[ x-]"}),
|
|
201
|
+
|
|
202
|
+
# Single digit to single letter range
|
|
203
|
+
("combo[5-a]", {'combo8', 'combo6', 'combo9', 'comboa', 'combo5', 'combo7'}),
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
for idx, (input_str, expected) in enumerate(test_cases):
|
|
207
|
+
result = expand_hostname(input_str, validate=False)
|
|
208
|
+
#old_result = old_expand_hostname(input_str, validate=False)
|
|
209
|
+
assert result == expected, f"Test {idx + 1} failed in expand_hostname(). Expected: {expected}, Got: {result}"
|
|
210
|
+
#assert old_result == expected, f"Test {idx + 1} failed in old_expand_hostname(). Expected: {expected}, Got: {old_result}"
|
|
211
|
+
print(f"Test {idx + 1} passed.")
|
|
212
|
+
|
|
213
|
+
import re
|
|
214
|
+
|
|
215
|
+
def tokenize_hostname(hostname):
|
|
216
|
+
"""
|
|
217
|
+
Tokenize the hostname into a list of tokens.
|
|
218
|
+
Tokens will be seperated by symbols or numbers.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
hostname (str): The hostname to tokenize.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
list: A list of tokens (hashed).
|
|
225
|
+
"""
|
|
226
|
+
# Split the hostname into tokens
|
|
227
|
+
tokens = re.findall(r"([a-zA-Z]+|\d+)", hostname)
|
|
228
|
+
# Hash the tokens
|
|
229
|
+
return [hash(token) for token in tokens]
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def compact_hostnames(hostnames):
|
|
234
|
+
patterns = defaultdict(list)
|
|
235
|
+
for hostname in hostnames:
|
|
236
|
+
parts = parse_hostname(hostname)
|
|
237
|
+
pattern = get_pattern(parts)
|
|
238
|
+
patterns[pattern].append(parts)
|
|
239
|
+
|
|
240
|
+
# Function to compact a list of numbers
|
|
241
|
+
def compact_numbers(numbers):
|
|
242
|
+
sorted_nums = sorted(set(numbers))
|
|
243
|
+
if len(sorted_nums) == 1:
|
|
244
|
+
return str(sorted_nums[0])
|
|
245
|
+
ranges = []
|
|
246
|
+
start = prev = sorted_nums[0]
|
|
247
|
+
for number in sorted_nums[1:]:
|
|
248
|
+
if number != prev + 1:
|
|
249
|
+
if start == prev:
|
|
250
|
+
ranges.append(str(start))
|
|
251
|
+
else:
|
|
252
|
+
ranges.append(f"{start}-{prev}")
|
|
253
|
+
start = number
|
|
254
|
+
prev = number
|
|
255
|
+
if start == prev:
|
|
256
|
+
ranges.append(str(start))
|
|
257
|
+
else:
|
|
258
|
+
ranges.append(f"{start}-{prev}")
|
|
259
|
+
return "[" + ",".join(ranges) + "]"
|
|
260
|
+
|
|
261
|
+
results = []
|
|
262
|
+
for pattern, parts_list in patterns.items():
|
|
263
|
+
segment_lists = OrderedDict()
|
|
264
|
+
for parts in parts_list:
|
|
265
|
+
for i, part in enumerate(parts):
|
|
266
|
+
if part.isdigit():
|
|
267
|
+
if i not in segment_lists:
|
|
268
|
+
segment_lists[i] = []
|
|
269
|
+
segment_lists[i].append(int(part))
|
|
270
|
+
|
|
271
|
+
# Construct the resulting compacted hostname
|
|
272
|
+
result_parts = []
|
|
273
|
+
last_index = 0
|
|
274
|
+
for index, num_list in segment_lists.items():
|
|
275
|
+
# Add the preceding static text
|
|
276
|
+
result_parts.append("".join(pattern[last_index:index]))
|
|
277
|
+
last_index = index + 1
|
|
278
|
+
# Add the compacted number range
|
|
279
|
+
result_parts.append(compact_numbers(num_list))
|
|
280
|
+
# Add any trailing static text
|
|
281
|
+
result_parts.append("".join(pattern[last_index:]))
|
|
282
|
+
results.append("".join(result_parts))
|
|
283
|
+
|
|
284
|
+
return ",".join(results)
|
|
285
|
+
|
|
286
|
+
# Test this updated function with your test cases.
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
# Run the tests
|
|
290
|
+
run_tests()
|
|
291
|
+
|
|
292
|
+
# servera,serverb,serverc=server[a-c]
|
|
293
|
+
# server15,server16,server17=server[15-17]
|
|
294
|
+
# server-1,server-2,server-3=server-[1-3]
|
|
295
|
+
# server-1-2,server-1-1,server-2-1,server-2-2=server-[1-2]-[1-2]
|
|
296
|
+
# server-1-2,server-1-1,server-2-2=server-1-[1-2],server-2-2
|
|
297
|
+
# test1-a,test2-a=test[1-2]-a
|
|
298
|
+
|
|
299
|
+
# Test cases
|
|
300
|
+
test_cases = [
|
|
301
|
+
(['server15', 'server16', 'server17'], 'server[15-17]'),
|
|
302
|
+
(['server-1', 'server-2', 'server-3'], 'server-[1-3]'),
|
|
303
|
+
(['server-1-2', 'server-1-1', 'server-2-1', 'server-2-2'], 'server-[1-2]-[1-2]'),
|
|
304
|
+
(['server-1-2', 'server-1-1', 'server-2-2'], 'server-1-[1-2],server-2-2'),
|
|
305
|
+
(['test1-a', 'test2-a'], 'test[1-2]-a'),
|
|
306
|
+
(['sub-s1', 'sub-s2'], 'sub-s[1-2]'),
|
|
307
|
+
]
|
|
308
|
+
|
|
309
|
+
for hostnames, expected in test_cases:
|
|
310
|
+
result = compact_hostnames(hostnames)
|
|
311
|
+
print(f"Hostnames: {hostnames}")
|
|
312
|
+
print(f"Compacted: {result}")
|
|
313
|
+
print(f"Expected: {expected}")
|
|
314
|
+
print(f"Pass: {result == expected}\n")
|
|
315
|
+
|
|
316
|
+
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import curses
|
|
2
|
+
import re
|
|
3
|
+
import math
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
# Global dictionary to store color pairs
|
|
7
|
+
__curses_global_color_pairs = {(-1,-1):1}
|
|
8
|
+
__curses_current_color_pair_index = 2 # Start from 1, as 0 is the default color pair
|
|
9
|
+
__curses_color_table = {}
|
|
10
|
+
__curses_current_color_index = 10
|
|
11
|
+
# Mapping of ANSI 4-bit colors to curses colors
|
|
12
|
+
ANSI_TO_CURSES_COLOR = {
|
|
13
|
+
30: curses.COLOR_BLACK,
|
|
14
|
+
31: curses.COLOR_RED,
|
|
15
|
+
32: curses.COLOR_GREEN,
|
|
16
|
+
33: curses.COLOR_YELLOW,
|
|
17
|
+
34: curses.COLOR_BLUE,
|
|
18
|
+
35: curses.COLOR_MAGENTA,
|
|
19
|
+
36: curses.COLOR_CYAN,
|
|
20
|
+
37: curses.COLOR_WHITE,
|
|
21
|
+
90: curses.COLOR_BLACK, # Bright Black (usually gray)
|
|
22
|
+
91: curses.COLOR_RED, # Bright Red
|
|
23
|
+
92: curses.COLOR_GREEN, # Bright Green
|
|
24
|
+
93: curses.COLOR_YELLOW, # Bright Yellow
|
|
25
|
+
94: curses.COLOR_BLUE, # Bright Blue
|
|
26
|
+
95: curses.COLOR_MAGENTA, # Bright Magenta
|
|
27
|
+
96: curses.COLOR_CYAN, # Bright Cyan
|
|
28
|
+
97: curses.COLOR_WHITE # Bright White
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
def __approximate_color_8bit(color):
|
|
32
|
+
"""
|
|
33
|
+
Approximate an 8-bit color (0-255) to the nearest curses color.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
color: 8-bit color code
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Curses color code
|
|
40
|
+
"""
|
|
41
|
+
if color < 8: # Standard and bright colors
|
|
42
|
+
return ANSI_TO_CURSES_COLOR.get(color % 8 + 30, curses.COLOR_WHITE)
|
|
43
|
+
elif 8 <= color < 16: # Bright colors
|
|
44
|
+
return ANSI_TO_CURSES_COLOR.get(color % 8 + 90, curses.COLOR_WHITE)
|
|
45
|
+
elif 16 <= color <= 231: # Color cube
|
|
46
|
+
# Convert 216-color cube index to RGB
|
|
47
|
+
color -= 16
|
|
48
|
+
r = (color // 36) % 6 * 51
|
|
49
|
+
g = (color // 6) % 6 * 51
|
|
50
|
+
b = color % 6 * 51
|
|
51
|
+
return __approximate_color_24bit(r, g, b) # Map to the closest curses color
|
|
52
|
+
elif 232 <= color <= 255: # Grayscale
|
|
53
|
+
gray = (color - 232) * 10 + 8
|
|
54
|
+
return __approximate_color_24bit(gray, gray, gray)
|
|
55
|
+
else:
|
|
56
|
+
return curses.COLOR_WHITE # Fallback to white for unexpected values
|
|
57
|
+
|
|
58
|
+
def __approximate_color_24bit(r, g, b):
|
|
59
|
+
"""
|
|
60
|
+
Approximate a 24-bit RGB color to the nearest curses color.
|
|
61
|
+
Will initiate a curses color if curses.can_change_color() is True.
|
|
62
|
+
|
|
63
|
+
Globals:
|
|
64
|
+
__curses_color_table: Dictionary of RGB color to curses color code
|
|
65
|
+
__curses_current_color_index: Current index of the
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
r: Red component (0-255)
|
|
69
|
+
g: Green component (0-255)
|
|
70
|
+
b: Blue component (0-255)
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Curses color code
|
|
74
|
+
"""
|
|
75
|
+
if curses.can_change_color():
|
|
76
|
+
global __curses_color_table,__curses_current_color_index
|
|
77
|
+
# Initiate a new color if it does not exist
|
|
78
|
+
if (r, g, b) not in __curses_color_table:
|
|
79
|
+
if __curses_current_color_index >= curses.COLORS:
|
|
80
|
+
eprint("Warning: Maximum number of colors reached. Wrapping around.")
|
|
81
|
+
__curses_current_color_index = 10
|
|
82
|
+
curses.init_color(__curses_current_color_index, int(r/255*1000), int(g/255*1000), int(b/255*1000))
|
|
83
|
+
__curses_color_table[(r, g, b)] = __curses_current_color_index
|
|
84
|
+
__curses_current_color_index += 1
|
|
85
|
+
return __curses_color_table[(r, g, b)]
|
|
86
|
+
# Fallback to 8-bit color approximation
|
|
87
|
+
colors = {
|
|
88
|
+
curses.COLOR_BLACK: (0, 0, 0),
|
|
89
|
+
curses.COLOR_RED: (255, 0, 0),
|
|
90
|
+
curses.COLOR_GREEN: (0, 255, 0),
|
|
91
|
+
curses.COLOR_YELLOW: (255, 255, 0),
|
|
92
|
+
curses.COLOR_BLUE: (0, 0, 255),
|
|
93
|
+
curses.COLOR_MAGENTA: (255, 0, 255),
|
|
94
|
+
curses.COLOR_CYAN: (0, 255, 255),
|
|
95
|
+
curses.COLOR_WHITE: (255, 255, 255),
|
|
96
|
+
}
|
|
97
|
+
best_match = curses.COLOR_WHITE
|
|
98
|
+
min_distance = float("inf")
|
|
99
|
+
for color, (cr, cg, cb) in colors.items():
|
|
100
|
+
distance = math.sqrt((r - cr) ** 2 + (g - cg) ** 2 + (b - cb) ** 2)
|
|
101
|
+
if distance < min_distance:
|
|
102
|
+
min_distance = distance
|
|
103
|
+
best_match = color
|
|
104
|
+
return best_match
|
|
105
|
+
|
|
106
|
+
def __get_curses_color_pair(fg, bg):
|
|
107
|
+
"""
|
|
108
|
+
Use curses color int values to create a curses color pair.
|
|
109
|
+
|
|
110
|
+
Globals:
|
|
111
|
+
__curses_global_color_pairs: Dictionary of color pairs
|
|
112
|
+
__curses_current_color_pair_index: Current index of the color pair
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
fg: Foreground color code
|
|
116
|
+
bg: Background color code
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Curses color pair code
|
|
120
|
+
"""
|
|
121
|
+
global __curses_global_color_pairs, __curses_current_color_pair_index
|
|
122
|
+
if (fg, bg) not in __curses_global_color_pairs:
|
|
123
|
+
if __curses_current_color_pair_index >= curses.COLOR_PAIRS:
|
|
124
|
+
print("Warning: Maximum number of color pairs reached, wrapping around.")
|
|
125
|
+
__curses_current_color_pair_index = 1
|
|
126
|
+
curses.init_pair(__curses_current_color_pair_index, fg, bg)
|
|
127
|
+
__curses_global_color_pairs[(fg, bg)] = __curses_current_color_pair_index
|
|
128
|
+
__curses_current_color_pair_index += 1
|
|
129
|
+
return curses.color_pair(__curses_global_color_pairs[(fg, bg)])
|
|
130
|
+
|
|
131
|
+
def __parse_ansi_escape_sequence_to_curses_attr(escape_code,color_pair_list = None):
|
|
132
|
+
"""
|
|
133
|
+
Parse ANSI escape codes to extract foreground and background colors.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
escape_code: ANSI escape sequence for color
|
|
137
|
+
color_pair_list: List of [foreground, background, color_pair] curses color pair values
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Curses color pair / attribute code
|
|
141
|
+
"""
|
|
142
|
+
if not escape_code:
|
|
143
|
+
return 1
|
|
144
|
+
if not color_pair_list:
|
|
145
|
+
color_pair_list = [-1,-1,1]
|
|
146
|
+
color_match = escape_code.lstrip("\x1b[").rstrip("m").split(";")
|
|
147
|
+
color_match = [x if x else '0' for x in color_match] # Replace empty strings with '0' (reset)
|
|
148
|
+
if color_match:
|
|
149
|
+
processed_index = -1
|
|
150
|
+
for i, param in enumerate(color_match):
|
|
151
|
+
if processed_index >= i:
|
|
152
|
+
# if the index has been processed, skip
|
|
153
|
+
continue
|
|
154
|
+
if param.isdigit():
|
|
155
|
+
if int(param) == 0:
|
|
156
|
+
color_pair_list[0] = -1
|
|
157
|
+
color_pair_list[1] = -1
|
|
158
|
+
color_pair_list[2] = 1
|
|
159
|
+
elif int(param) == 38:
|
|
160
|
+
if i + 1 >= len(color_match):
|
|
161
|
+
# Invalid color code, skip
|
|
162
|
+
continue
|
|
163
|
+
if color_match[i + 1] == "5":
|
|
164
|
+
# 8-bit foreground color
|
|
165
|
+
if i + 2 >= len(color_match) or not color_match[i + 2].isdigit():
|
|
166
|
+
# Invalid color code, skip
|
|
167
|
+
processed_index = i + 1
|
|
168
|
+
continue
|
|
169
|
+
color_pair_list[0] = __approximate_color_8bit(int(color_match[i + 2]))
|
|
170
|
+
color_pair_list[2] = __get_curses_color_pair(color_pair_list[0], color_pair_list[1])
|
|
171
|
+
processed_index = i + 2
|
|
172
|
+
elif color_match[i + 1] == "2":
|
|
173
|
+
# 24-bit foreground color
|
|
174
|
+
if i + 4 >= len(color_match) or not all(x.isdigit() for x in color_match[i + 2:i + 5]):
|
|
175
|
+
# Invalid color code, skip
|
|
176
|
+
processed_index = i + 1
|
|
177
|
+
continue
|
|
178
|
+
color_pair_list[0] = __approximate_color_24bit(int(color_match[i + 2]), int(color_match[i + 3]), int(color_match[i + 4]))
|
|
179
|
+
color_pair_list[2] = __get_curses_color_pair(color_pair_list[0], color_pair_list[1])
|
|
180
|
+
processed_index = i + 4
|
|
181
|
+
elif int(param) == 48:
|
|
182
|
+
if i + 1 >= len(color_match):
|
|
183
|
+
# Invalid color code, skip
|
|
184
|
+
continue
|
|
185
|
+
if color_match[i + 1] == "5":
|
|
186
|
+
# 8-bit background color
|
|
187
|
+
if i + 2 >= len(color_match) or not color_match[i + 2].isdigit():
|
|
188
|
+
# Invalid color code, skip
|
|
189
|
+
processed_index = i + 1
|
|
190
|
+
continue
|
|
191
|
+
color_pair_list[1] = __approximate_color_8bit(int(color_match[i + 2]))
|
|
192
|
+
color_pair_list[2] = __get_curses_color_pair(color_pair_list[0], color_pair_list[1])
|
|
193
|
+
processed_index = i + 2
|
|
194
|
+
elif color_match[i + 1] == "2":
|
|
195
|
+
# 24-bit background color
|
|
196
|
+
if i + 4 >= len(color_match) or not all(x.isdigit() for x in color_match[i + 2:i + 5]):
|
|
197
|
+
# Invalid color code, skip
|
|
198
|
+
processed_index = i + 1
|
|
199
|
+
continue
|
|
200
|
+
color_pair_list[1] = __approximate_color_24bit(int(color_match[i + 2]), int(color_match[i + 3]), int(color_match[i + 4]))
|
|
201
|
+
color_pair_list[2] = __get_curses_color_pair(color_pair_list[0], color_pair_list[1])
|
|
202
|
+
processed_index = i + 4
|
|
203
|
+
elif 30 <= int(param) <= 37 or 90 <= int(param) <= 97:
|
|
204
|
+
# 4-bit foreground color
|
|
205
|
+
color_pair_list[0] = ANSI_TO_CURSES_COLOR.get(int(param), curses.COLOR_WHITE)
|
|
206
|
+
color_pair_list[2] = __get_curses_color_pair(color_pair_list[0], color_pair_list[1])
|
|
207
|
+
elif 40 <= int(param) <= 47 or 100 <= int(param) <= 107:
|
|
208
|
+
# 4-bit background color
|
|
209
|
+
color_pair_list[1] = ANSI_TO_CURSES_COLOR.get(int(param)-10, curses.COLOR_BLACK)
|
|
210
|
+
color_pair_list[2] = __get_curses_color_pair(color_pair_list[0], color_pair_list[1])
|
|
211
|
+
elif int(param) == 1:
|
|
212
|
+
color_pair_list[2] = color_pair_list[2] | curses.A_BOLD
|
|
213
|
+
elif int(param) == 2:
|
|
214
|
+
color_pair_list[2] = color_pair_list[2] | curses.A_DIM
|
|
215
|
+
elif int(param) == 4:
|
|
216
|
+
color_pair_list[2] = color_pair_list[2] | curses.A_UNDERLINE
|
|
217
|
+
elif int(param) == 5:
|
|
218
|
+
color_pair_list[2] = color_pair_list[2] | curses.A_BLINK
|
|
219
|
+
elif int(param) == 7:
|
|
220
|
+
color_pair_list[2] = color_pair_list[2] | curses.A_REVERSE
|
|
221
|
+
elif int(param) == 8:
|
|
222
|
+
color_pair_list[2] = color_pair_list[2] | curses.A_INVIS
|
|
223
|
+
elif int(param) == 21:
|
|
224
|
+
color_pair_list[2] = color_pair_list[2] & ~curses.A_BOLD
|
|
225
|
+
elif int(param) == 22:
|
|
226
|
+
color_pair_list[2] = color_pair_list[2] & ~curses.A_DIM
|
|
227
|
+
elif int(param) == 24:
|
|
228
|
+
color_pair_list[2] = color_pair_list[2] & ~curses.A_UNDERLINE
|
|
229
|
+
elif int(param) == 25:
|
|
230
|
+
color_pair_list[2] = color_pair_list[2] & ~curses.A_BLINK
|
|
231
|
+
elif int(param) == 27:
|
|
232
|
+
color_pair_list[2] = color_pair_list[2] & ~curses.A_REVERSE
|
|
233
|
+
elif int(param) == 28:
|
|
234
|
+
color_pair_list[2] = color_pair_list[2] & ~curses.A_INVIS
|
|
235
|
+
elif int(param) == 39:
|
|
236
|
+
color_pair_list[0] = -1
|
|
237
|
+
color_pair_list[2] = __get_curses_color_pair(color_pair_list[0], color_pair_list[1])
|
|
238
|
+
elif int(param) == 49:
|
|
239
|
+
color_pair_list[1] = -1
|
|
240
|
+
color_pair_list[2] = __get_curses_color_pair(color_pair_list[0], color_pair_list[1])
|
|
241
|
+
else:
|
|
242
|
+
color_pair_list[0] = -1
|
|
243
|
+
color_pair_list[1] = -1
|
|
244
|
+
color_pair_list[2] = 1
|
|
245
|
+
return color_pair_list[2]
|
|
246
|
+
|
|
247
|
+
def _curses_add_string_to_window(window, line = '', y = 0, x = 0, number_of_char_to_write = -1, color_pair_list = [-1,-1,1],fill_char=' ',parse_ansi_colors = True,centered = False,lead_str = '', trail_str = '',box_ansi_color = None, keep_top_n_lines = 0):
|
|
248
|
+
"""
|
|
249
|
+
Add a string to a curses window with / without ANSI color escape sequences translated to curses color pairs.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
window: curses window object
|
|
253
|
+
line: The line to add
|
|
254
|
+
y: Line position in the window. Use -1 to scroll the window up 1 line and add the line at the bottom
|
|
255
|
+
x: Column position in the window
|
|
256
|
+
number_of_char_to_write: Number of characters to write. -1 for all remaining space in line, 0 for no characters, and a positive integer for a specific number of characters.
|
|
257
|
+
color_pair_list: List of [foreground, background, color_pair] curses color pair values
|
|
258
|
+
fill_char: Character to fill the remaining space in the line
|
|
259
|
+
parse_ansi_colors: Parse ASCII color codes
|
|
260
|
+
centered: Center the text in the window
|
|
261
|
+
lead_str: Leading string to add to the line
|
|
262
|
+
trail_str: Trailing string to add to the line
|
|
263
|
+
box_ansi_color: ANSI color escape sequence for the box color
|
|
264
|
+
keep_top_n_lines: Number of lines to keep at the top of the window
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
None
|
|
268
|
+
"""
|
|
269
|
+
if window.getmaxyx()[0] == 0 or window.getmaxyx()[1] == 0 or x >= window.getmaxyx()[1]:
|
|
270
|
+
return
|
|
271
|
+
if x < 0:
|
|
272
|
+
x = window.getmaxyx()[1] + x
|
|
273
|
+
if number_of_char_to_write == -1:
|
|
274
|
+
numChar = window.getmaxyx()[1] - x -1
|
|
275
|
+
elif number_of_char_to_write == 0:
|
|
276
|
+
return
|
|
277
|
+
elif number_of_char_to_write + x > window.getmaxyx()[1]:
|
|
278
|
+
numChar = window.getmaxyx()[1] - x -1
|
|
279
|
+
else:
|
|
280
|
+
numChar = number_of_char_to_write
|
|
281
|
+
if numChar < 0:
|
|
282
|
+
return
|
|
283
|
+
if y < 0 or y >= window.getmaxyx()[0]:
|
|
284
|
+
if keep_top_n_lines > window.getmaxyx()[0] -1:
|
|
285
|
+
keep_top_n_lines = window.getmaxyx()[0] -1
|
|
286
|
+
if keep_top_n_lines < 0:
|
|
287
|
+
keep_top_n_lines = 0
|
|
288
|
+
window.move(keep_top_n_lines,0)
|
|
289
|
+
window.deleteln()
|
|
290
|
+
y = window.getmaxyx()[0] - 1
|
|
291
|
+
line = line.replace('\n', ' ').replace('\r', ' ')
|
|
292
|
+
if parse_ansi_colors:
|
|
293
|
+
segments = re.split(r"(\x1b\[[\d;]*m)", line) # Split line by ANSI escape codes
|
|
294
|
+
else:
|
|
295
|
+
segments = [line]
|
|
296
|
+
charsWritten = 0
|
|
297
|
+
boxAttr = __parse_ansi_escape_sequence_to_curses_attr(box_ansi_color)
|
|
298
|
+
# first add the lead_str
|
|
299
|
+
window.addnstr(y, x, lead_str, numChar, boxAttr)
|
|
300
|
+
charsWritten = min(len(lead_str), numChar)
|
|
301
|
+
# process centering
|
|
302
|
+
if centered:
|
|
303
|
+
fill_length = numChar - len(lead_str) - len(trail_str) - sum([len(segment) for segment in segments if not segment.startswith("\x1b[")])
|
|
304
|
+
window.addnstr(y, x + charsWritten, fill_char * (fill_length // 2 // len(fill_char)), numChar - charsWritten, boxAttr)
|
|
305
|
+
charsWritten += min(len(fill_char * (fill_length // 2)), numChar - charsWritten)
|
|
306
|
+
# add the segments
|
|
307
|
+
for segment in segments:
|
|
308
|
+
if not segment:
|
|
309
|
+
continue
|
|
310
|
+
if parse_ansi_colors and segment.startswith("\x1b["):
|
|
311
|
+
# Parse ANSI escape sequence
|
|
312
|
+
newAttr = __parse_ansi_escape_sequence_to_curses_attr(segment,color_pair_list)
|
|
313
|
+
else:
|
|
314
|
+
# Add text with current color
|
|
315
|
+
if charsWritten < numChar:
|
|
316
|
+
window.addnstr(y, x + charsWritten, segment, numChar - charsWritten, color_pair_list[2])
|
|
317
|
+
charsWritten += min(len(segment), numChar - charsWritten)
|
|
318
|
+
# if we have finished printing segments but we still have space, we will fill it with fill_char
|
|
319
|
+
if charsWritten + len(trail_str) < numChar:
|
|
320
|
+
fillStr = fill_char * ((numChar - charsWritten - len(trail_str))//len(fill_char))
|
|
321
|
+
#fillStr = f'{color_pair_list}'
|
|
322
|
+
window.addnstr(y, x + charsWritten, fillStr + trail_str, numChar - charsWritten, boxAttr)
|
|
323
|
+
charsWritten += numChar - charsWritten
|
|
324
|
+
else:
|
|
325
|
+
window.addnstr(y, x + charsWritten, trail_str, numChar - charsWritten, boxAttr)
|
|
326
|
+
|
|
327
|
+
def _get_hosts_to_display (hosts, max_num_hosts, hosts_to_display = None):
|
|
328
|
+
'''
|
|
329
|
+
Generate a list for the hosts to be displayed on the screen. This is used to display as much relevant information as possible.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
hosts (list): A list of Host objects
|
|
333
|
+
max_num_hosts (int): The maximum number of hosts to be displayed
|
|
334
|
+
hosts_to_display (list, optional): The hosts that are currently displayed. Defaults to None.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
list: A list of Host objects to be displayed
|
|
338
|
+
'''
|
|
339
|
+
# We will sort the hosts by running -> failed -> finished -> waiting
|
|
340
|
+
# running: returncode is None and output is not empty (output will be appened immediately after the command is run)
|
|
341
|
+
# failed: returncode is not None and returncode is not 0
|
|
342
|
+
# finished: returncode is not None and returncode is 0
|
|
343
|
+
# waiting: returncode is None and output is empty
|
|
344
|
+
running_hosts = [host for host in hosts if host.returncode is None and host.output]
|
|
345
|
+
failed_hosts = [host for host in hosts if host.returncode is not None and host.returncode != 0]
|
|
346
|
+
finished_hosts = [host for host in hosts if host.returncode is not None and host.returncode == 0]
|
|
347
|
+
waiting_hosts = [host for host in hosts if host.returncode is None and not host.output]
|
|
348
|
+
new_hosts_to_display = (running_hosts + failed_hosts + finished_hosts + waiting_hosts)[:max_num_hosts]
|
|
349
|
+
if not hosts_to_display:
|
|
350
|
+
return new_hosts_to_display , {'running':len(running_hosts), 'failed':len(failed_hosts), 'finished':len(finished_hosts), 'waiting':len(waiting_hosts)}
|
|
351
|
+
# we will compare the new_hosts_to_display with the old one, if some hosts are not in their original position, we will change its printedLines to 0
|
|
352
|
+
for i, host in enumerate(new_hosts_to_display):
|
|
353
|
+
if host not in hosts_to_display:
|
|
354
|
+
host.printedLines = 0
|
|
355
|
+
elif i != hosts_to_display.index(host):
|
|
356
|
+
host.printedLines = 0
|
|
357
|
+
return new_hosts_to_display , {'running':len(running_hosts), 'failed':len(failed_hosts), 'finished':len(finished_hosts), 'waiting':len(waiting_hosts)}
|
|
358
|
+
|
|
359
|
+
TEST_LINE = (
|
|
360
|
+
"Standard \x1b[31m Red\x1b[0m, ",
|
|
361
|
+
"Bold \x1b[91m \x1b[1mRed\x1b[0m, ",
|
|
362
|
+
"Standard \x1b[32m Green\x1b[0m, ",
|
|
363
|
+
"Bright \x1b[92m Green\x1b[0m, ",
|
|
364
|
+
"Standard \x1b[33m Yellow\x1b[0m, ",
|
|
365
|
+
"Bright \x1b[93m Yellow\x1b[0m, ",
|
|
366
|
+
"Standard \x1b[34m Blue\x1b[0m, ",
|
|
367
|
+
"Bright \x1b[94m Blue\x1b[0m, ",
|
|
368
|
+
"Standard \x1b[35m Magenta\x1b[0m, ",
|
|
369
|
+
"Bright \x1b[95m Magenta\x1b[0m, ",
|
|
370
|
+
"Standard \x1b[36m Cyan\x1b[0m, ",
|
|
371
|
+
"Bright \x1b[96m Cyan\x1b[0m, ",
|
|
372
|
+
"Standard \x1b[37m White\x1b[0m, ",
|
|
373
|
+
"Bright \x1b[97m White\x1b[0m, ",
|
|
374
|
+
"8-bit \x1b[38;5;196m Red\x1b[0m, ",
|
|
375
|
+
"8-bit \x1b[38;5;82m Green\x1b[0m, ",
|
|
376
|
+
"8-bit \x1b[38;5;27m Blue\x1b[0m, ",
|
|
377
|
+
"8-bit \x1b[38;5;226m Yellow\x1b[0m, ",
|
|
378
|
+
"8-bit \x1b[38;5;201m Magenta\x1b[0m, ",
|
|
379
|
+
"8-bit \x1b[38;5;51m Cyan\x1b[0m, ",
|
|
380
|
+
"8-bit \x1b[38;5;15m White\x1b[0m, ",
|
|
381
|
+
"8-bit \x1b[38;5;0m Black\x1b[0m, ",
|
|
382
|
+
"8-bit \x1b[38;5;4m Blue\x1b[0m, ",
|
|
383
|
+
"24-bit \x1b[38;2;128;128;128m Gray\x1b[0m.",
|
|
384
|
+
"24-bit \x1b[38;2;255;165;0m Orange\x1b[0m.",
|
|
385
|
+
"24-bit \x1b[38;2;255;0;255m Pink\x1b[0m.",
|
|
386
|
+
"24-bit \x1b[38;2;0;255;255m Cyan\x1b[0m.",
|
|
387
|
+
"24-bit \x1b[38;2;255;255;255m White\x1b[0m.",
|
|
388
|
+
"24-bit \x1b[38;2;0;0;0m Black\x1b[0m.",
|
|
389
|
+
"24-bit \x1b[38;2;128;128;128m Gray\x1b[0m.",
|
|
390
|
+
"24-bit \x1b[38;2;255;0;0m Red\x1b[0m.",
|
|
391
|
+
"24-bit \x1b[38;2;0;255;0m Green\x1b[0m.",
|
|
392
|
+
"24-bit \x1b[38;2;0;0;255m Blue\x1b[0m.",
|
|
393
|
+
"24-bit \x1b[38;2;255;255;0m Yellow\x1b[0m.",
|
|
394
|
+
"24-bit \x1b[38;2;255;0;255m Magenta\x1b[0m.",
|
|
395
|
+
"24-bit \x1b[38;2;0;255;255m Cyan colored dot.",
|
|
396
|
+
"Following string without reset color.",
|
|
397
|
+
"Following string with \x1b[31m Red color.",
|
|
398
|
+
"Following string with \x1b[31m Red color and \x1b[32m Green color.",
|
|
399
|
+
"Reset color. \x1b[0m Following string with default color.",
|
|
400
|
+
"Bold \x1b[1m Following string with bold text.\x1b[0m",
|
|
401
|
+
"Dim \x1b[2m Following string with dim text.\x1b[0m",
|
|
402
|
+
"Underline \x1b[4m Following string with underline text.\x1b[0m",
|
|
403
|
+
"Blink \x1b[5m Following string with blink text.\x1b[0m",
|
|
404
|
+
"Reverse \x1b[7m Following string with reverse text.\x1b[0m",
|
|
405
|
+
"Invisible \x1b[8m Following string with invisible text.\x1b[0m",
|
|
406
|
+
"Reset color. \x1b[0m Following string with default color.\x1b[0m",
|
|
407
|
+
"Bold \x1b[1m Following string with \x1b[22m not bold text.\x1b[0m",
|
|
408
|
+
"RedGreen \x1b[31;32m Green\x1b[0m, ",
|
|
409
|
+
)
|
|
410
|
+
# Example usage in a curses application
|
|
411
|
+
def main(stdscr):
|
|
412
|
+
curses.start_color()
|
|
413
|
+
curses.use_default_colors()
|
|
414
|
+
curses.init_pair(1, -1, -1)
|
|
415
|
+
parsed_attr = __parse_ansi_escape_sequence_to_curses_attr('\x1b[31m')
|
|
416
|
+
stdscr.addnstr(0, 0, f'{parsed_attr}',stdscr.getmaxyx()[1], parsed_attr)
|
|
417
|
+
# get curses.A_BOLD
|
|
418
|
+
stdscr.addnstr(1, 0, f'{curses.A_BOLD}',stdscr.getmaxyx()[1], curses.A_BOLD)
|
|
419
|
+
stdscr.addnstr(2, 0, f'{parsed_attr | curses.A_BOLD}',stdscr.getmaxyx()[1], parsed_attr | curses.A_BOLD)
|
|
420
|
+
stdscr.addnstr(3, 0, f'{curses.A_DIM}',stdscr.getmaxyx()[1], parsed_attr | curses.A_DIM)
|
|
421
|
+
stdscr.addnstr(4, 0, f'{parsed_attr | curses.A_BOLD | curses.A_DIM}',stdscr.getmaxyx()[1], parsed_attr | curses.A_BOLD| curses.A_DIM)
|
|
422
|
+
stdscr.refresh()
|
|
423
|
+
stdscr.getch()
|
|
424
|
+
stdscr.clear()
|
|
425
|
+
#stdscr.idlok(True)
|
|
426
|
+
#stdscr.scrollok(True)
|
|
427
|
+
screen_size = stdscr.getmaxyx()
|
|
428
|
+
|
|
429
|
+
# Example line with ANSI color escape codes (including 8-bit and 24-bit colors)
|
|
430
|
+
color_pair_list = [-1,-1,1]
|
|
431
|
+
for i, line in enumerate(TEST_LINE):
|
|
432
|
+
_curses_add_string_to_window(window=stdscr, line=line, y = i,color_pair_list=color_pair_list,fill_char='-',centered=True,lead_str='|',trail_str='?|',box_ansi_color='\x1b[43;31;5m',keep_top_n_lines=2)
|
|
433
|
+
|
|
434
|
+
#stdscr.addnstr(i+ 1, 0, 'test',10, 4)
|
|
435
|
+
stdscr.refresh()
|
|
436
|
+
stdscr.getch()
|
|
437
|
+
#stdscr.scroll(1)
|
|
438
|
+
stdscr.move(0,0)
|
|
439
|
+
stdscr.deleteln()
|
|
440
|
+
stdscr.refresh()
|
|
441
|
+
stdscr.getch()
|
|
442
|
+
|
|
443
|
+
if __name__ == "__main__":
|
|
444
|
+
for i, color in enumerate(TEST_LINE):
|
|
445
|
+
print(f'{i}: {color}')
|
|
446
|
+
curses.wrapper(main)
|
|
447
|
+
|
|
448
|
+
#print(__curses_color_table)
|
|
449
|
+
#print(__curses_global_color_pairs)
|
|
450
|
+
for color in __curses_color_table:
|
|
451
|
+
print(f'{color}: {__curses_color_table[color]}, {curses.color_content(__curses_color_table[color])}')
|
|
452
|
+
for color_pair in __curses_global_color_pairs:
|
|
453
|
+
print(f'{color_pair}: {__curses_global_color_pairs[color_pair]}')
|
|
454
|
+
# for i in range(curses.COLORS):
|
|
455
|
+
# print(f'{i}: {curses.color_content(i)}')
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import curses
|
|
2
|
+
import re
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
# Global dictionary to store color pairs
|
|
6
|
+
__curses_global_color_pairs = {(-1,-1):1}
|
|
7
|
+
__curses_current_color_pair_index = 2 # Start from 1, as 0 is the default color pair
|
|
8
|
+
__curses_color_table = {}
|
|
9
|
+
__curses_current_color_index = 10
|
|
10
|
+
# Mapping of ANSI 4-bit colors to curses colors
|
|
11
|
+
ANSI_TO_CURSES_COLOR = {
|
|
12
|
+
30: curses.COLOR_BLACK,
|
|
13
|
+
31: curses.COLOR_RED,
|
|
14
|
+
32: curses.COLOR_GREEN,
|
|
15
|
+
33: curses.COLOR_YELLOW,
|
|
16
|
+
34: curses.COLOR_BLUE,
|
|
17
|
+
35: curses.COLOR_MAGENTA,
|
|
18
|
+
36: curses.COLOR_CYAN,
|
|
19
|
+
37: curses.COLOR_WHITE,
|
|
20
|
+
90: curses.COLOR_BLACK, # Bright Black (usually gray)
|
|
21
|
+
91: curses.COLOR_RED, # Bright Red
|
|
22
|
+
92: curses.COLOR_GREEN, # Bright Green
|
|
23
|
+
93: curses.COLOR_YELLOW, # Bright Yellow
|
|
24
|
+
94: curses.COLOR_BLUE, # Bright Blue
|
|
25
|
+
95: curses.COLOR_MAGENTA, # Bright Magenta
|
|
26
|
+
96: curses.COLOR_CYAN, # Bright Cyan
|
|
27
|
+
97: curses.COLOR_WHITE # Bright White
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
def __approximate_color_8bit(color):
|
|
31
|
+
"""
|
|
32
|
+
Approximate an 8-bit color (0-255) to the nearest curses color.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
color: 8-bit color code
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Curses color code
|
|
39
|
+
"""
|
|
40
|
+
if color < 8: # Standard and bright colors
|
|
41
|
+
return ANSI_TO_CURSES_COLOR.get(color % 8 + 30, curses.COLOR_WHITE)
|
|
42
|
+
elif 8 <= color < 16: # Bright colors
|
|
43
|
+
return ANSI_TO_CURSES_COLOR.get(color % 8 + 90, curses.COLOR_WHITE)
|
|
44
|
+
elif 16 <= color <= 231: # Color cube
|
|
45
|
+
# Convert 216-color cube index to RGB
|
|
46
|
+
color -= 16
|
|
47
|
+
r = (color // 36) % 6 * 51
|
|
48
|
+
g = (color // 6) % 6 * 51
|
|
49
|
+
b = color % 6 * 51
|
|
50
|
+
return __approximate_color_24bit(r, g, b) # Map to the closest curses color
|
|
51
|
+
elif 232 <= color <= 255: # Grayscale
|
|
52
|
+
gray = (color - 232) * 10 + 8
|
|
53
|
+
return __approximate_color_24bit(gray, gray, gray)
|
|
54
|
+
else:
|
|
55
|
+
return curses.COLOR_WHITE # Fallback to white for unexpected values
|
|
56
|
+
|
|
57
|
+
def __approximate_color_24bit(r, g, b):
|
|
58
|
+
"""
|
|
59
|
+
Approximate a 24-bit RGB color to the nearest curses color.
|
|
60
|
+
Will initiate a curses color if curses.can_change_color() is True.
|
|
61
|
+
|
|
62
|
+
Globals:
|
|
63
|
+
__curses_color_table: Dictionary of RGB color to curses color code
|
|
64
|
+
__curses_current_color_index: Current index of the
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
r: Red component (0-255)
|
|
68
|
+
g: Green component (0-255)
|
|
69
|
+
b: Blue component (0-255)
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Curses color code
|
|
73
|
+
"""
|
|
74
|
+
if curses.can_change_color():
|
|
75
|
+
global __curses_color_table,__curses_current_color_index
|
|
76
|
+
# Initiate a new color if it does not exist
|
|
77
|
+
if (r, g, b) not in __curses_color_table:
|
|
78
|
+
if __curses_current_color_index >= curses.COLORS:
|
|
79
|
+
eprint("Warning: Maximum number of colors reached. Wrapping around.")
|
|
80
|
+
__curses_current_color_index = 10
|
|
81
|
+
curses.init_color(__curses_current_color_index, int(r/255*1000), int(g/255*1000), int(b/255*1000))
|
|
82
|
+
__curses_color_table[(r, g, b)] = __curses_current_color_index
|
|
83
|
+
__curses_current_color_index += 1
|
|
84
|
+
return __curses_color_table[(r, g, b)]
|
|
85
|
+
# Fallback to 8-bit color approximation
|
|
86
|
+
colors = {
|
|
87
|
+
curses.COLOR_BLACK: (0, 0, 0),
|
|
88
|
+
curses.COLOR_RED: (255, 0, 0),
|
|
89
|
+
curses.COLOR_GREEN: (0, 255, 0),
|
|
90
|
+
curses.COLOR_YELLOW: (255, 255, 0),
|
|
91
|
+
curses.COLOR_BLUE: (0, 0, 255),
|
|
92
|
+
curses.COLOR_MAGENTA: (255, 0, 255),
|
|
93
|
+
curses.COLOR_CYAN: (0, 255, 255),
|
|
94
|
+
curses.COLOR_WHITE: (255, 255, 255),
|
|
95
|
+
}
|
|
96
|
+
best_match = curses.COLOR_WHITE
|
|
97
|
+
min_distance = float("inf")
|
|
98
|
+
for color, (cr, cg, cb) in colors.items():
|
|
99
|
+
distance = math.sqrt((r - cr) ** 2 + (g - cg) ** 2 + (b - cb) ** 2)
|
|
100
|
+
if distance < min_distance:
|
|
101
|
+
min_distance = distance
|
|
102
|
+
best_match = color
|
|
103
|
+
return best_match
|
|
104
|
+
|
|
105
|
+
def __parse_ansi_escape_sequence_to_curses_color(escape_code):
|
|
106
|
+
"""
|
|
107
|
+
Parse ANSI escape codes to extract foreground and background colors.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
escape_code: ANSI escape sequence for color
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Tuple of (foreground, background) curses color pairs.
|
|
114
|
+
If the escape code is a reset code, return (-1, -1).
|
|
115
|
+
None values indicate that the color should not be changed.
|
|
116
|
+
"""
|
|
117
|
+
if not escape_code:
|
|
118
|
+
return None, None
|
|
119
|
+
color_match = re.match(r"\x1b\[(\d+)(?:;(\d+))?(?:;(\d+))?(?:;(\d+);(\d+);(\d+))?m", escape_code)
|
|
120
|
+
if color_match:
|
|
121
|
+
params = color_match.groups()
|
|
122
|
+
if params[0] == "0" and not any(params[1:]): # Reset code
|
|
123
|
+
return -1, -1
|
|
124
|
+
if params[0] == "38" and params[1] == "5": # 8-bit foreground
|
|
125
|
+
return __approximate_color_8bit(int(params[2])), None
|
|
126
|
+
elif params[0] == "38" and params[1] == "2": # 24-bit foreground
|
|
127
|
+
return __approximate_color_24bit(int(params[3]), int(params[4]), int(params[5])), None
|
|
128
|
+
elif params[0] == "48" and params[1] == "5": # 8-bit background
|
|
129
|
+
return None , __approximate_color_8bit(int(params[2]))
|
|
130
|
+
elif params[0] == "48" and params[1] == "2": # 24-bit background
|
|
131
|
+
return None, __approximate_color_24bit(int(params[3]), int(params[4]), int(params[5]))
|
|
132
|
+
else:
|
|
133
|
+
fg = None
|
|
134
|
+
bg = None
|
|
135
|
+
if params[0] and params[0].isdigit(): # 4-bit color
|
|
136
|
+
fg = ANSI_TO_CURSES_COLOR.get(int(params[0]), curses.COLOR_WHITE)
|
|
137
|
+
if params[1] and params[1].isdigit():
|
|
138
|
+
bg = ANSI_TO_CURSES_COLOR.get(int(params[1]), curses.COLOR_BLACK)
|
|
139
|
+
return fg, bg
|
|
140
|
+
return None, None
|
|
141
|
+
|
|
142
|
+
def __get_curses_color_pair(fg, bg):
|
|
143
|
+
"""
|
|
144
|
+
Use curses color int values to create a curses color pair.
|
|
145
|
+
|
|
146
|
+
Globals:
|
|
147
|
+
__curses_global_color_pairs: Dictionary of color pairs
|
|
148
|
+
__curses_current_color_pair_index: Current index of the color pair
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
fg: Foreground color code
|
|
152
|
+
bg: Background color code
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Curses color pair code
|
|
156
|
+
"""
|
|
157
|
+
global __curses_global_color_pairs, __curses_current_color_pair_index
|
|
158
|
+
if (fg, bg) not in __curses_global_color_pairs:
|
|
159
|
+
if __curses_current_color_pair_index >= curses.COLOR_PAIRS:
|
|
160
|
+
print("Warning: Maximum number of color pairs reached, wrapping around.")
|
|
161
|
+
__curses_current_color_pair_index = 1
|
|
162
|
+
curses.init_pair(__curses_current_color_pair_index, fg, bg)
|
|
163
|
+
__curses_global_color_pairs[(fg, bg)] = __curses_current_color_pair_index
|
|
164
|
+
__curses_current_color_pair_index += 1
|
|
165
|
+
return __curses_global_color_pairs[(fg, bg)]
|
|
166
|
+
|
|
167
|
+
def _add_line_with_ascii_colors(window, y, x, line, n, color_pair_list = [-1,-1,1]):
|
|
168
|
+
"""
|
|
169
|
+
Add a line to a curses window with ANSI escape sequences translated to curses color pairs.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
window: curses window object
|
|
173
|
+
y: Line position in the window
|
|
174
|
+
x: Column position in the window
|
|
175
|
+
line: The string containing ANSI escape sequences for color
|
|
176
|
+
n: Maximum number of characters to write
|
|
177
|
+
host: The host object
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
None
|
|
181
|
+
"""
|
|
182
|
+
segments = re.split(r"(\x1b\[[\d;]*m)", line) # Split line by ANSI escape codes
|
|
183
|
+
current_x = x
|
|
184
|
+
for segment in segments:
|
|
185
|
+
if segment.startswith("\x1b["):
|
|
186
|
+
# Parse ANSI escape sequence
|
|
187
|
+
newFrontColor, newBackColor = __parse_ansi_escape_sequence_to_curses_color(segment)
|
|
188
|
+
if newFrontColor is not None:
|
|
189
|
+
color_pair_list[0] = newFrontColor
|
|
190
|
+
if newBackColor is not None:
|
|
191
|
+
color_pair_list[1] = newBackColor
|
|
192
|
+
color_pair_list[2] = __get_curses_color_pair(color_pair_list[0], color_pair_list[1])
|
|
193
|
+
pair = str(color_pair_list[2])
|
|
194
|
+
window.addstr(y, current_x, pair)
|
|
195
|
+
current_x += len(pair)
|
|
196
|
+
else:
|
|
197
|
+
# Add text with current color
|
|
198
|
+
if current_x < x + n:
|
|
199
|
+
#eprint(f"\ny: {y}, x: {current_x}, segment: {segment}, n: {n}, color_pair: {color_pair_list[2]}\n")
|
|
200
|
+
window.addnstr(y, current_x, segment, n - (current_x - x), curses.color_pair(color_pair_list[2]))
|
|
201
|
+
current_x += len(segment)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
TEST_LINE = (
|
|
205
|
+
"Standard \x1b[31mRed\x1b[0m, ",
|
|
206
|
+
"Bright \x1b[91mRed\x1b[0m, ",
|
|
207
|
+
"Standard \x1b[32mGreen\x1b[0m, ",
|
|
208
|
+
"Bright \x1b[92mGreen\x1b[0m, ",
|
|
209
|
+
"Standard \x1b[33mYellow\x1b[0m, ",
|
|
210
|
+
"Bright \x1b[93mYellow\x1b[0m, ",
|
|
211
|
+
"Standard \x1b[34mBlue\x1b[0m, ",
|
|
212
|
+
"Bright \x1b[94mBlue\x1b[0m, ",
|
|
213
|
+
"Standard \x1b[35mMagenta\x1b[0m, ",
|
|
214
|
+
"Bright \x1b[95mMagenta\x1b[0m, ",
|
|
215
|
+
"Standard \x1b[36mCyan\x1b[0m, ",
|
|
216
|
+
"Bright \x1b[96mCyan\x1b[0m, ",
|
|
217
|
+
"Standard \x1b[37mWhite\x1b[0m, ",
|
|
218
|
+
"Bright \x1b[97mWhite\x1b[0m, ",
|
|
219
|
+
"8-bit \x1b[38;5;196mRed\x1b[0m, ",
|
|
220
|
+
"8-bit \x1b[38;5;82mGreen\x1b[0m, ",
|
|
221
|
+
"8-bit \x1b[38;5;27mBlue\x1b[0m, ",
|
|
222
|
+
"8-bit \x1b[38;5;226mYellow\x1b[0m, ",
|
|
223
|
+
"8-bit \x1b[38;5;201mMagenta\x1b[0m, ",
|
|
224
|
+
"8-bit \x1b[38;5;51mCyan\x1b[0m, ",
|
|
225
|
+
"8-bit \x1b[38;5;15mWhite\x1b[0m, ",
|
|
226
|
+
"8-bit \x1b[38;5;0mBlack\x1b[0m, ",
|
|
227
|
+
"8-bit \x1b[38;5;4mBlue\x1b[0m, ",
|
|
228
|
+
"24-bit \x1b[38;2;128;128;128mGray\x1b[0m.",
|
|
229
|
+
"24-bit \x1b[38;2;255;165;0mOrange\x1b[0m.",
|
|
230
|
+
"24-bit \x1b[38;2;255;0;255mPink\x1b[0m.",
|
|
231
|
+
"24-bit \x1b[38;2;0;255;255mCyan\x1b[0m.",
|
|
232
|
+
"24-bit \x1b[38;2;255;255;255mWhite\x1b[0m.",
|
|
233
|
+
"24-bit \x1b[38;2;0;0;0mBlack\x1b[0m.",
|
|
234
|
+
"24-bit \x1b[38;2;128;128;128mGray\x1b[0m.",
|
|
235
|
+
"24-bit \x1b[38;2;255;0;0mRed\x1b[0m.",
|
|
236
|
+
"24-bit \x1b[38;2;0;255;0mGreen\x1b[0m.",
|
|
237
|
+
"24-bit \x1b[38;2;0;0;255mBlue\x1b[0m.",
|
|
238
|
+
"24-bit \x1b[38;2;255;255;0mYellow\x1b[0m.",
|
|
239
|
+
"24-bit \x1b[38;2;255;0;255mMagenta\x1b[0m.",
|
|
240
|
+
"24-bit \x1b[38;2;0;255;255mCyan colored dot.",
|
|
241
|
+
"Following string without reset color.",
|
|
242
|
+
"Following string with \x1b[31mRed color.",
|
|
243
|
+
"Following string with \x1b[31mRed color and \x1b[32mGreen color.",
|
|
244
|
+
"Reset color. \x1b[0mFollowing string with default color."
|
|
245
|
+
)
|
|
246
|
+
# Example usage in a curses application
|
|
247
|
+
def main(stdscr):
|
|
248
|
+
curses.start_color()
|
|
249
|
+
curses.use_default_colors()
|
|
250
|
+
curses.init_pair(1, -1, -1)
|
|
251
|
+
frontColor, backColor = __parse_ansi_escape_sequence_to_curses_color('\x1b[31;33m')
|
|
252
|
+
stdscr.addnstr(0, 0, f'frontColor,backColor:{frontColor},{backColor}',stdscr.getmaxyx()[1], 1)
|
|
253
|
+
stdscr.addnstr(1, 0, f'{curses.color_content(frontColor)}, {curses.color_content(backColor)}',stdscr.getmaxyx()[1], 1)
|
|
254
|
+
stdscr.refresh()
|
|
255
|
+
color_pair = __get_curses_color_pair(frontColor, backColor)
|
|
256
|
+
stdscr.addnstr(2, 0, f'color_pair {color_pair}',stdscr.getmaxyx()[1], color_pair)
|
|
257
|
+
stdscr.addnstr(3, 0, f'{curses.color_pair(color_pair)}',stdscr.getmaxyx()[1], color_pair)
|
|
258
|
+
stdscr.refresh()
|
|
259
|
+
stdscr.getch()
|
|
260
|
+
stdscr.clear()
|
|
261
|
+
#stdscr.idlok(True)
|
|
262
|
+
#stdscr.scrollok(True)
|
|
263
|
+
screen_size = stdscr.getmaxyx()
|
|
264
|
+
|
|
265
|
+
# Example line with ANSI color escape codes (including 8-bit and 24-bit colors)
|
|
266
|
+
color_pair_list = [-1,-1,1]
|
|
267
|
+
for i, line in enumerate(TEST_LINE):
|
|
268
|
+
_add_line_with_ascii_colors(stdscr, i, 0, line, screen_size[1], color_pair_list)
|
|
269
|
+
|
|
270
|
+
stdscr.refresh()
|
|
271
|
+
stdscr.getch()
|
|
272
|
+
#stdscr.scroll(1)
|
|
273
|
+
stdscr.move(0,0)
|
|
274
|
+
stdscr.deleteln()
|
|
275
|
+
stdscr.refresh()
|
|
276
|
+
stdscr.getch()
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
if __name__ == "__main__":
|
|
280
|
+
for i, color in enumerate(TEST_LINE):
|
|
281
|
+
print(f'{i}: {color}')
|
|
282
|
+
curses.wrapper(main)
|
|
283
|
+
|
|
284
|
+
#print(__curses_color_table)
|
|
285
|
+
#print(__curses_global_color_pairs)
|
|
286
|
+
for color in __curses_color_table:
|
|
287
|
+
print(f'{color}: {__curses_color_table[color]}, {curses.color_content(__curses_color_table[color])}')
|
|
288
|
+
for color_pair in __curses_global_color_pairs:
|
|
289
|
+
print(f'{color_pair}: {__curses_global_color_pairs[color_pair]}')
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import multiSSH3
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
print(multiSSH3.compact_hostnames(frozenset([f'PC{i}-{j:03d}' for i in range(8,11) for j in range(1, 3) ])))
|
|
5
|
+
|
|
6
|
+
# bigZeroPaddedHosts = frozenset([f'PC{i:02d}-{j:02d}' for i in range(1, 200) for j in range(1, 3102)] + ['3-3PC','nebulamaster'])
|
|
7
|
+
# print(f'len of bigZeroPaddedHosts: {len(bigZeroPaddedHosts)}')
|
|
8
|
+
# startTime = time.perf_counter()
|
|
9
|
+
# print(compact_hostnames(bigZeroPaddedHosts))
|
|
10
|
+
# print(f'Time: {time.perf_counter() - startTime}')
|
|
11
|
+
|
|
12
|
+
# hugeHosts = frozenset([f'PC{i}-{j:03d}-{k:05d}' for i in range(3, 40) for j in range(1, 50) for k in range(1, 100)] + ['3-3-3PC','nebulamaster'])
|
|
13
|
+
# print(f'len of hugeHosts: {len(hugeHosts)}')
|
|
14
|
+
# startTime = time.perf_counter()
|
|
15
|
+
# print(compact_hostnames(hugeHosts))
|
|
16
|
+
# print(f'Time: {time.perf_counter() - startTime}')
|
|
17
|
+
|
|
18
|
+
# # %%
|
|
19
|
+
# hugeHosts = frozenset([f'PC{i:01d}-{j:03d}-{k:03d}' for i in range(1, 100) for j in range(1, 50) for k in range(1, 100) if j != 8 if i != 35] + ['3-3-3PC','nebulamaster'])
|
|
20
|
+
# startTime = time.perf_counter()
|
|
21
|
+
# print(f'len of skipping hugerHosts: {len(hugeHosts)}')
|
|
22
|
+
# print(compact_hostnames(hugeHosts))
|
|
23
|
+
# print(f'Time: {time.perf_counter() - startTime}')
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
ipRangeHosts = frozenset([f'10.{i}.{j}.{k}' for i in range(6, 13) for j in range(100, 255) for k in range(1, 255)] +[f'192.168.{j}.{k}' for k in range(100, 200) for j in range(1, 255) ]+ ['localhost'])
|
|
27
|
+
print(f'len of ipRangeHosts: {len(ipRangeHosts)}')
|
|
28
|
+
startTime = time.perf_counter()
|
|
29
|
+
print(multiSSH3.compact_hostnames(ipRangeHosts))
|
|
30
|
+
print(f'Time: {time.perf_counter() - startTime}')
|
|
31
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import multiSSH3
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
ipRangeHosts = frozenset(['admin[1-10]@10.251.*.1-253','localhost'])
|
|
6
|
+
print(f'len of ipRangeHosts: {len(ipRangeHosts)}')
|
|
7
|
+
startTime = time.perf_counter()
|
|
8
|
+
results = multiSSH3.expand_hostnames(ipRangeHosts)
|
|
9
|
+
print(f'Time: {time.perf_counter() - startTime}')
|
|
10
|
+
print(len(results))
|
|
11
|
+
for i, result in enumerate(results.items()):
|
|
12
|
+
print(result)
|
|
13
|
+
if i > 10:
|
|
14
|
+
break
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|