multiSSH3 5.97__py3-none-any.whl → 5.99__py3-none-any.whl

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.py CHANGED
@@ -84,10 +84,10 @@ except Exception:
84
84
  print('Warning: functools.lru_cache is not available, multiSSH3 will run slower without cache.',file=sys.stderr)
85
85
  def cache_decorator(func):
86
86
  return func
87
- version = '5.97'
87
+ version = '5.99'
88
88
  VERSION = version
89
89
  __version__ = version
90
- COMMIT_DATE = '2025-10-21'
90
+ COMMIT_DATE = '2025-10-24'
91
91
 
92
92
  CONFIG_FILE_CHAIN = ['./multiSSH3.config.json',
93
93
  '~/multiSSH3.config.json',
@@ -377,6 +377,24 @@ ERROR_MESSAGES_TO_IGNORE = [
377
377
  'Killed by signal',
378
378
  'Connection reset by peer',
379
379
  ]
380
+ __DEFAULT_COLOR_PALETTE = {
381
+ 'cyan': (86, 173, 188),
382
+ 'green': (114, 180, 43),
383
+ 'magenta': (140, 107, 200),
384
+ 'red': (196, 38, 94),
385
+ 'white': (227, 227, 221),
386
+ 'yellow': (179, 180, 43),
387
+ 'blue': (106, 126, 200),
388
+ 'bright_black': (102, 102, 102),
389
+ 'bright_blue': (129, 154, 255),
390
+ 'bright_cyan': (102, 217, 239),
391
+ 'bright_green': (126, 226, 46),
392
+ 'bright_magenta': (174, 129, 255),
393
+ 'bright_red': (249, 38, 114),
394
+ 'bright_white': (248, 248, 242),
395
+ 'bright_yellow': (226, 226, 46),
396
+ }
397
+ COLOR_PALETTE = __DEFAULT_COLOR_PALETTE.copy()
380
398
  _DEFAULT_CALLED = True
381
399
  _DEFAULT_RETURN_UNFINISHED = False
382
400
  _DEFAULT_UPDATE_UNREACHABLE_HOSTS = True
@@ -936,6 +954,8 @@ def get_terminal_color_capability():
936
954
  return '24bit'
937
955
  elif "256" in term:
938
956
  return '256'
957
+ elif "16" in term:
958
+ return '16'
939
959
  try:
940
960
  curses.setupterm()
941
961
  colors = curses.tigetnum("colors")
@@ -954,96 +974,120 @@ def get_terminal_color_capability():
954
974
  return 'None'
955
975
 
956
976
  @cache_decorator
957
- def get_xterm256_palette():
958
- palette = []
959
- # 0–15: system colors (we'll just fill with dummy values;
960
- # you could fill in real RGB if you need to)
961
- system_colors = [
962
- (0, 0, 0), (128, 0, 0), (0, 128, 0), (128, 128, 0),
963
- (0, 0, 128), (128, 0, 128), (0, 128, 128), (192, 192, 192),
964
- (128, 128, 128), (255, 0, 0), (0, 255, 0), (255, 255, 0),
965
- (0, 0, 255), (255, 0, 255), (0, 255, 255), (255, 255, 255),
966
- ]
967
- palette.extend(system_colors)
968
- # 16–231: 6x6x6 color cube
969
- levels = [0, 95, 135, 175, 215, 255]
970
- for r in levels:
971
- for g in levels:
972
- for b in levels:
973
- palette.append((r, g, b))
974
- # 232–255: grayscale ramp, 24 steps from 8 to 238
975
- for i in range(24):
976
- level = 8 + i * 10
977
- palette.append((level, level, level))
978
- return palette
977
+ def rgb_to_ansi_color_string(r, g, b):
978
+ """
979
+ Return an ANSI escape sequence setting the foreground to (r,g,b)
980
+ approximated to the terminal's capability, or '' if none.
981
+ """
982
+ cap = get_terminal_color_capability()
983
+ if cap == 'None':
984
+ return ''
985
+ if cap == '24bit':
986
+ return f'\x1b[38;2;{r};{g};{b}m'
987
+ if cap == '256':
988
+ idx = _rgb_to_256_color(r, g, b)
989
+ return f'\x1b[38;5;{idx}m'
990
+ if cap == '16':
991
+ idx = _rgb_to_16_color(r, g, b)
992
+ # 0–7 = 30–37, 8–15 = 90–97
993
+ if idx < 8:
994
+ return f'\x1b[{30 + idx}m'
995
+ else:
996
+ return f'\x1b[{90 + (idx - 8)}m'
997
+ if cap == '8':
998
+ idx = _rgb_to_8_color(r, g, b)
999
+ return f'\x1b[{30 + idx}m'
1000
+ return ''
979
1001
 
980
- @cache_decorator
981
- def rgb_to_xterm_index(r, g, b):
1002
+ def _rgb_to_256_color(r, g, b):
1003
+ """
1004
+ Map (r,g,b) to the 256-color cube or grayscale ramp.
982
1005
  """
983
- Map 24-bit RGB to nearest xterm-256 color index.
984
- r, g, b should be in 0-255.
985
- Returns an int in 0-255.
1006
+ # if it’s already gray, use the 232–255 grayscale ramp
1007
+ if r == g == b:
1008
+ # 24 shades from 232 to 255
1009
+ return 232 + int(round(r / 255 * 23))
1010
+ # else map each channel to 0–5
1011
+ def to6(v):
1012
+ return int(round(v / 255 * 5))
1013
+ r6, g6, b6 = to6(r), to6(g), to6(b)
1014
+ return 16 + 36 * r6 + 6 * g6 + b6
1015
+
1016
+ def _rgb_to_16_color(r, g, b):
986
1017
  """
987
- best_index = 0
1018
+ Pick the nearest of the 16 ANSI standard colors.
1019
+ Returns an index 0-15.
1020
+ """
1021
+ palette = [
1022
+ (0, 0, 0), # 0 black
1023
+ (128, 0, 0), # 1 red
1024
+ (0, 128, 0), # 2 green
1025
+ (128, 128, 0), # 3 yellow
1026
+ (0, 0, 128), # 4 blue
1027
+ (128, 0, 128), # 5 magenta
1028
+ (0, 128, 128), # 6 cyan
1029
+ (192, 192, 192), # 7 white (light gray)
1030
+ (128, 128, 128), # 8 bright black (dark gray)
1031
+ (255, 0, 0), # 9 bright red
1032
+ (0, 255, 0), # 10 bright green
1033
+ (255, 255, 0), # 11 bright yellow
1034
+ (0, 0, 255), # 12 bright blue
1035
+ (255, 0, 255), # 13 bright magenta
1036
+ (0, 255, 255), # 14 bright cyan
1037
+ (255, 255, 255), # 15 bright white
1038
+ ]
1039
+ best_idx = 0
988
1040
  best_dist = float('inf')
989
- for i, (pr, pg, pb) in enumerate(get_xterm256_palette()):
990
- dr = pr - r
991
- dg = pg - g
992
- db = pb - b
993
- dist = dr*dr + dg*dg + db*db
1041
+ for i, (pr, pg, pb) in enumerate(palette):
1042
+ dist = (r - pr)**2 + (g - pg)**2 + (b - pb)**2
994
1043
  if dist < best_dist:
995
1044
  best_dist = dist
996
- best_index = i
997
- return best_index
1045
+ best_idx = i
1046
+ return best_idx
998
1047
 
999
- @cache_decorator
1000
- def hashable_to_color(n, brightness_threshold=500):
1001
- hash_value = hash(str(n))
1048
+ def _rgb_to_8_color(r, g, b):
1049
+ """
1050
+ Reduce to 8 colors by mapping to the 16-color index then clamping 0-7.
1051
+ """
1052
+ return _rgb_to_16_color(r//2, g//2, b//2)
1053
+
1054
+
1055
+ def int_to_color(hash_value, min_brightness=100,max_brightness=220):
1002
1056
  r = (hash_value >> 16) & 0xFF
1003
1057
  g = (hash_value >> 8) & 0xFF
1004
1058
  b = hash_value & 0xFF
1005
- if (r + g + b) < brightness_threshold:
1006
- return hashable_to_color(hash_value, brightness_threshold)
1059
+ brightness = math.sqrt(0.299 * r**2 + 0.587 * g**2 + 0.114 * b**2)
1060
+ if brightness < min_brightness:
1061
+ return int_to_color(hash(str(hash_value)), min_brightness, max_brightness)
1062
+ if brightness > max_brightness:
1063
+ return int_to_color(hash(str(hash_value)), min_brightness, max_brightness)
1007
1064
  return (r, g, b)
1008
1065
 
1009
- __previous_ansi_color_index = -1
1066
+ __previous_color_rgb = ()
1010
1067
  @cache_decorator
1011
- def string_to_unique_ansi_color(string):
1068
+ def int_to_unique_ansi_color(number):
1012
1069
  '''
1013
- Convert a string to a unique ANSI color code
1070
+ Convert a number to a unique ANSI color code
1014
1071
 
1015
1072
  Args:
1016
- string (str): The string to convert
1017
-
1073
+ number (int): The number to convert
1018
1074
  Returns:
1019
1075
  int: The ANSI color code
1020
1076
  '''
1021
- global __previous_ansi_color_index
1077
+ global __previous_color_rgb
1022
1078
  # Use a hash function to generate a consistent integer from the string
1023
1079
  color_capability = get_terminal_color_capability()
1024
- index = None
1025
1080
  if color_capability == 'None':
1026
1081
  return ''
1027
- elif color_capability == '16':
1028
- # Map to one of the 14 colors (31-37, 90-96), avoiding black and white
1029
- index = (hash(string) % 14) + 31
1030
- if index > 37:
1031
- index += 52 # Bright colors (90-97)
1032
- elif color_capability == '8':
1033
- index = (hash(string) % 6) + 31
1034
- r,g,b = hashable_to_color(string)
1035
- if color_capability == '256':
1036
- index = rgb_to_xterm_index(r,g,b)
1037
- if index:
1038
- if index == __previous_ansi_color_index:
1039
- return string_to_unique_ansi_color(hash(string))
1040
- __previous_ansi_color_index = index
1041
- if color_capability == '256':
1042
- return f'\033[38;5;{index}m'
1043
- else:
1044
- return f'\033[{index}m'
1082
+ if color_capability == '24bit':
1083
+ r, g, b = int_to_color(number)
1045
1084
  else:
1046
- return f'\033[38;2;{r};{g};{b}m'
1085
+ # for 256 colors and below, reduce brightness threshold as we do not have many color to work with
1086
+ r, g, b = int_to_color(number, min_brightness=70, max_brightness=190)
1087
+ if sum(abs(a - b) for a, b in zip((r, g, b), __previous_color_rgb)) <= 256:
1088
+ r, g, b = int_to_color(hash(str(number)))
1089
+ __previous_color_rgb = (r, g, b)
1090
+ return rgb_to_ansi_color_string(r, g, b)
1047
1091
 
1048
1092
  #%% ------------ Compacting Hostnames ----------------
1049
1093
  def __tokenize_hostname(hostname):
@@ -2795,7 +2839,7 @@ def mergeOutput(merging_hostnames,outputs_by_hostname,output,diff_display_thresh
2795
2839
  indexes = Counter({hostname: 0 for hostname in merging_hostnames})
2796
2840
  working_index_keys = set(merging_hostnames)
2797
2841
  previousBuddies = set()
2798
- hostnameWrapper = textwrap.TextWrapper(width=line_length - 1, tabsize=4, replace_whitespace=False, drop_whitespace=False, break_on_hyphens=False,initial_indent='├─ ', subsequent_indent='│- ')
2842
+ hostnameWrapper = textwrap.TextWrapper(width=line_length -1, tabsize=4, replace_whitespace=False, drop_whitespace=False, break_on_hyphens=False,initial_indent=' ', subsequent_indent='- ')
2799
2843
  hostnameWrapper.wordsep_simple_re = re.compile(r'([,]+)')
2800
2844
  diff_display_item_count = max(1,int(max(map(len, outputs_by_hostname.values())) * (1 - diff_display_threshold)))
2801
2845
  def get_multiset_index_for_hostname(hostname):
@@ -2830,6 +2874,13 @@ def mergeOutput(merging_hostnames,outputs_by_hostname,output,diff_display_thresh
2830
2874
  # futures[hostname] # ensure it's initialized
2831
2875
  futures = {hostname: get_multiset_index_for_hostname(hostname) for hostname in merging_hostnames}
2832
2876
  currentLines = defaultdict(set)
2877
+ color_cap = get_terminal_color_capability()
2878
+ if color_cap == 'None':
2879
+ green_str = ''
2880
+ reset_str = ''
2881
+ else:
2882
+ green_str = rgb_to_ansi_color_string(*COLOR_PALETTE.get('green', __DEFAULT_COLOR_PALETTE['green']))
2883
+ reset_str = '\033[0m'
2833
2884
  for hostname in merging_hostnames:
2834
2885
  currentLines[outputs_by_hostname[hostname][0]].add(hostname)
2835
2886
  while indexes:
@@ -2867,12 +2918,18 @@ def mergeOutput(merging_hostnames,outputs_by_hostname,output,diff_display_thresh
2867
2918
  if buddy != previousBuddies:
2868
2919
  hostnameStr = ','.join(compact_hostnames(buddy))
2869
2920
  hostnameLines = hostnameWrapper.wrap(hostnameStr)
2870
- hostnameLines = [line.ljust(line_length - 1) + '│' for line in hostnameLines]
2871
- color = string_to_unique_ansi_color(hostnameStr) if len(buddy) < len(merging_hostnames) else ''
2872
- hostnameLines[0] = f"\033[0m{color}{hostnameLines[0]}"
2921
+ # hostnameLines = [line.ljust(line_length) for line in hostnameLines]
2922
+ if color_cap == 'None':
2923
+ hostnameLines[0] = f"{hostnameLines[0]}"
2924
+ elif len(buddy) < len(merging_hostnames):
2925
+ color = int_to_unique_ansi_color(hash(hostnameStr))
2926
+ hostnameLines[0] = f"{color}■{hostnameLines[0]}"
2927
+ hostnameLines[-1] += reset_str
2928
+ else:
2929
+ hostnameLines[0] = f"{green_str}■{reset_str}{hostnameLines[0]}"
2873
2930
  output.extend(hostnameLines)
2874
2931
  previousBuddies = buddy
2875
- output.append(lineToAdd.ljust(line_length - 1) + '│')
2932
+ output.append(lineToAdd)
2876
2933
  currentLines[lineToAdd].difference_update(buddy)
2877
2934
  if not currentLines[lineToAdd]:
2878
2935
  del currentLines[lineToAdd]
@@ -2896,17 +2953,27 @@ def mergeOutput(merging_hostnames,outputs_by_hostname,output,diff_display_thresh
2896
2953
 
2897
2954
  def mergeOutputs(outputs_by_hostname, merge_groups, remaining_hostnames, diff_display_threshold, line_length):
2898
2955
  output = []
2899
- output.append(('┌'+'─'*(line_length-2) + '┐'))
2900
- hostnameWrapper = textwrap.TextWrapper(width=line_length - 1, tabsize=4, replace_whitespace=False, drop_whitespace=False, break_on_hyphens=False,initial_indent='├─ ', subsequent_indent='│- ')
2956
+ color_cap = get_terminal_color_capability()
2957
+ if color_cap == 'None':
2958
+ color_line = ''
2959
+ color_reset = ''
2960
+ green_str = ''
2961
+ else:
2962
+ color_line = rgb_to_ansi_color_string(*COLOR_PALETTE.get('white', __DEFAULT_COLOR_PALETTE['white']))
2963
+ color_reset = '\033[0m'
2964
+ green_str = rgb_to_ansi_color_string(*COLOR_PALETTE.get('green', __DEFAULT_COLOR_PALETTE['green']))
2965
+ output.append(color_line+'─'*(line_length)+color_reset)
2966
+ hostnameWrapper = textwrap.TextWrapper(width=line_length - 1, tabsize=4, replace_whitespace=False, drop_whitespace=False, break_on_hyphens=False,initial_indent=' ', subsequent_indent='- ')
2901
2967
  hostnameWrapper.wordsep_simple_re = re.compile(r'([,]+)')
2902
2968
  for merging_hostnames in merge_groups:
2903
2969
  mergeOutput(merging_hostnames, outputs_by_hostname, output, diff_display_threshold,line_length)
2904
- output.append('\033[0m├'+'─'*(line_length-2) + '┤')
2970
+ output.append(color_line+'─'*(line_length)+color_reset)
2905
2971
  for hostname in remaining_hostnames:
2906
2972
  hostnameLines = hostnameWrapper.wrap(','.join(compact_hostnames([hostname])))
2907
- output.extend(line.ljust(line_length - 1) + '│' for line in hostnameLines)
2908
- output.extend(line.ljust(line_length - 1) + '│' for line in outputs_by_hostname[hostname])
2909
- output.append('\033[0m├'+'─'*(line_length-2) + '┤')
2973
+ hostnameLines[0] = f"{green_str}■{color_reset}{hostnameLines[0]}"
2974
+ output.extend(hostnameLines)
2975
+ output.extend(outputs_by_hostname[hostname])
2976
+ output.append(color_line+'─'*(line_length)+color_reset)
2910
2977
  if output:
2911
2978
  output.pop()
2912
2979
  # if output and output[0] and output[0].startswith('├'):
@@ -2931,26 +2998,40 @@ def get_host_raw_output(hosts, terminal_width):
2931
2998
  outputs_by_hostname = {}
2932
2999
  line_bag_by_hostname = {}
2933
3000
  hostnames_by_line_bag_len = {}
2934
- text_wrapper = textwrap.TextWrapper(width=terminal_width - 2, tabsize=4, replace_whitespace=False, drop_whitespace=False,
2935
- initial_indent=' ', subsequent_indent='│-')
3001
+ text_wrapper = textwrap.TextWrapper(width=terminal_width - 1, tabsize=4, replace_whitespace=False, drop_whitespace=False,
3002
+ initial_indent=' ', subsequent_indent='-')
2936
3003
  max_length = 20
3004
+ color_cap = get_terminal_color_capability()
3005
+ if color_cap == 'None':
3006
+ color_reset_str = ''
3007
+ blue_str = ''
3008
+ cyan_str = ''
3009
+ green_str = ''
3010
+ red_str = ''
3011
+ else:
3012
+ color_reset_str = '\033[0m'
3013
+ blue_str = rgb_to_ansi_color_string(*COLOR_PALETTE.get('blue', __DEFAULT_COLOR_PALETTE['blue']))
3014
+ cyan_str = rgb_to_ansi_color_string(*COLOR_PALETTE.get('bright_cyan', __DEFAULT_COLOR_PALETTE['bright_cyan']))
3015
+ green_str = rgb_to_ansi_color_string(*COLOR_PALETTE.get('bright_green', __DEFAULT_COLOR_PALETTE['bright_green']))
3016
+ red_str = rgb_to_ansi_color_string(*COLOR_PALETTE.get('bright_red', __DEFAULT_COLOR_PALETTE['bright_red']))
2937
3017
  hosts = pre_merge_hosts(hosts)
2938
3018
  for host in hosts:
2939
3019
  max_length = max(max_length, len(max(host.name.split(','), key=len)) + 3)
2940
- hostPrintOut = ["│█ EXECUTED COMMAND:"]
3020
+ hostPrintOut = [f"{cyan_str}█{color_reset_str} EXECUTED COMMAND:"]
2941
3021
  for line in host.command.splitlines():
2942
3022
  hostPrintOut.extend(text_wrapper.wrap(line))
2943
3023
  # hostPrintOut.extend(itertools.chain.from_iterable(text_wrapper.wrap(line) for line in host['command'].splitlines()))
2944
3024
  lineBag = {(0,host.command)}
2945
3025
  prevLine = host.command
2946
3026
  if host.stdout:
2947
- hostPrintOut.append('│▓ STDOUT:')
3027
+ hostPrintOut.append(f'{blue_str}▓{color_reset_str} STDOUT:')
2948
3028
  # for line in host.stdout:
2949
3029
  # if len(line) < terminal_width - 2:
2950
- # hostPrintOut.append(f" {line}")
3030
+ # hostPrintOut.append(f" {line}")
2951
3031
  # else:
2952
3032
  # hostPrintOut.extend(text_wrapper.wrap(line))
2953
- hostPrintOut.extend(f" {line}" for line in host.stdout)
3033
+ hostPrintOut.extend(f" {line}" for line in host.stdout)
3034
+ max_length = max(max_length, max(map(len, host.stdout)))
2954
3035
  # hostPrintOut.extend(text_wrapper.wrap(line) for line in host.stdout)
2955
3036
  lineBag.add((prevLine,1))
2956
3037
  lineBag.add((1,host.stdout[0]))
@@ -2966,22 +3047,26 @@ def get_host_raw_output(hosts, terminal_width):
2966
3047
  elif host.stderr[-1].strip().endswith('No route to host'):
2967
3048
  host.stderr[-1] = 'Cannot find host!'
2968
3049
  if host.stderr:
2969
- hostPrintOut.append('│▒ STDERR:')
3050
+ hostPrintOut.append(f'{red_str}▒{color_reset_str} STDERR:')
2970
3051
  # for line in host.stderr:
2971
3052
  # if len(line) < terminal_width - 2:
2972
- # hostPrintOut.append(f" {line}")
3053
+ # hostPrintOut.append(f" {line}")
2973
3054
  # else:
2974
3055
  # hostPrintOut.extend(text_wrapper.wrap(line))
2975
- hostPrintOut.extend(f" {line}" for line in host.stderr)
3056
+ hostPrintOut.extend(f" {line}" for line in host.stderr)
3057
+ max_length = max(max_length, max(map(len, host.stderr)))
2976
3058
  lineBag.add((prevLine,2))
2977
3059
  lineBag.add((2,host.stderr[0]))
2978
3060
  lineBag.update(host.stderr)
2979
3061
  if len(host.stderr) > 1:
2980
3062
  lineBag.update(zip(host.stderr, host.stderr[1:]))
2981
3063
  prevLine = host.stderr[-1]
2982
- hostPrintOut.append(f"│░ RETURN CODE: {host.returncode}")
3064
+ if host.returncode != 0:
3065
+ codeColor = red_str
3066
+ else:
3067
+ codeColor = green_str
3068
+ hostPrintOut.append(f"{codeColor}░{color_reset_str} RETURN CODE: {host.returncode}")
2983
3069
  lineBag.add((prevLine,f"{host.returncode}"))
2984
- max_length = max(max_length, max(map(len, hostPrintOut)))
2985
3070
  outputs_by_hostname[host.name] = hostPrintOut
2986
3071
  line_bag_by_hostname[host.name] = lineBag
2987
3072
  hostnames_by_line_bag_len.setdefault(len(lineBag), set()).add(host.name)
@@ -3022,6 +3107,7 @@ def form_merge_groups(hostnames_by_line_bag_len, sorted_hostnames_by_line_bag_le
3022
3107
  return merge_groups, remaining_hostnames
3023
3108
 
3024
3109
  def generate_output(hosts, usejson = False, greppable = False,quiet = False,encoding = _encoding,keyPressesIn = [[]]):
3110
+ color_cap = get_terminal_color_capability()
3025
3111
  if quiet:
3026
3112
  # remove hosts with returncode 0
3027
3113
  hosts = [host for host in hosts if host.returncode != 0]
@@ -3029,7 +3115,10 @@ def generate_output(hosts, usejson = False, greppable = False,quiet = False,enco
3029
3115
  if usejson:
3030
3116
  return '{"Success": true}'
3031
3117
  else:
3032
- return 'Success'
3118
+ if color_cap == 'None':
3119
+ return 'Success'
3120
+ else:
3121
+ return '\033[32mSuccess\033[0m'
3033
3122
  if usejson:
3034
3123
  # [print(dict(host)) for host in hosts]
3035
3124
  #print(json.dumps([dict(host) for host in hosts],indent=4))
@@ -3064,23 +3153,32 @@ def generate_output(hosts, usejson = False, greppable = False,quiet = False,enco
3064
3153
  except Exception:
3065
3154
  eprint("Warning: diff_display_threshold should be a float between 0 and 1. Setting to default value of 0.9")
3066
3155
  diff_display_threshold = 0.9
3156
+
3157
+ color_reset_str = '' if color_cap == 'None' else '\033[0m'
3158
+ white_str = '' if color_cap == 'None' else rgb_to_ansi_color_string(*COLOR_PALETTE.get('white', __DEFAULT_COLOR_PALETTE['white']))
3067
3159
  terminal_length = get_terminal_size()[0]
3068
3160
  outputs_by_hostname, line_bag_by_hostname, hostnames_by_line_bag_len, sorted_hostnames_by_line_bag_len_keys, line_length = get_host_raw_output(hosts,terminal_length)
3069
3161
  merge_groups ,remaining_hostnames = form_merge_groups(hostnames_by_line_bag_len, sorted_hostnames_by_line_bag_len_keys, line_bag_by_hostname, diff_display_threshold)
3070
3162
  outputs = mergeOutputs(outputs_by_hostname, merge_groups,remaining_hostnames, diff_display_threshold,line_length)
3071
3163
  if keyPressesIn[-1]:
3072
3164
  CMDsOut = [''.join(cmd).encode(encoding=encoding,errors='backslashreplace').decode(encoding=encoding,errors='backslashreplace').replace('\\n', '↵') for cmd in keyPressesIn if cmd]
3073
- outputs.append("├─ User Inputs:".ljust(line_length -1,'─')+'┤')
3165
+ outputs.append(color_reset_str + " User Inputs:".ljust(line_length,'─'))
3074
3166
  cmdOut = []
3075
3167
  for line in CMDsOut:
3076
3168
  cmdOut.extend(textwrap.wrap(line, width=line_length-1, tabsize=4, replace_whitespace=False, drop_whitespace=False,
3077
- initial_indent=' ', subsequent_indent='│-'))
3078
- outputs.extend(cmd.ljust(line_length -1)+'│' for cmd in cmdOut)
3169
+ initial_indent=' ', subsequent_indent='-'))
3170
+ outputs.extend(cmdOut)
3079
3171
  keyPressesIn[-1].clear()
3080
3172
  if not outputs:
3081
- rtnStr = 'Success' if quiet else ''
3173
+ if quiet:
3174
+ if color_cap == 'None':
3175
+ return 'Success'
3176
+ else:
3177
+ return '\033[32mSuccess\033[0m'
3178
+ else:
3179
+ rtnStr = ''
3082
3180
  else:
3083
- rtnStr = '\n'.join(outputs + [('\033[0m└'+'─'*(line_length-2)+'┘')])
3181
+ rtnStr = '\n'.join(outputs + [white_str + '─' * (line_length) + color_reset_str])
3084
3182
  return rtnStr
3085
3183
 
3086
3184
  def print_output(hosts,usejson = False,quiet = False,greppable = False):
@@ -3126,7 +3224,15 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
3126
3224
  if total_sleeped > 0.1:
3127
3225
  break
3128
3226
  if any([host.returncode is None for host in hosts]):
3129
- curses.wrapper(curses_print, hosts, threads, min_char_len = curses_min_char_len, min_line_len = curses_min_line_len, single_window = single_window)
3227
+ try:
3228
+ curses.wrapper(curses_print, hosts, threads, min_char_len = curses_min_char_len, min_line_len = curses_min_line_len, single_window = single_window)
3229
+ except Exception:
3230
+ try:
3231
+ curses.wrapper(curses_print, hosts, threads, min_char_len = curses_min_char_len, min_line_len = curses_min_line_len, single_window = single_window)
3232
+ except Exception as e:
3233
+ eprint(f"Curses print error: {e}")
3234
+ import traceback
3235
+ print(traceback.format_exc())
3130
3236
  if not returnUnfinished:
3131
3237
  # wait until all hosts have a return code
3132
3238
  while any([host.returncode is None for host in hosts]):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multiSSH3
3
- Version: 5.97
3
+ Version: 5.99
4
4
  Summary: Run commands on multiple hosts via SSH
5
5
  Home-page: https://github.com/yufei-pan/multiSSH3
6
6
  Author: Yufei Pan
@@ -0,0 +1,6 @@
1
+ multiSSH3.py,sha256=Of3ONtNehRGAXWF3E3m8ao7OwmbgEu2YGv524CX0ytw,185227
2
+ multissh3-5.99.dist-info/METADATA,sha256=CLc8mLA6ufLHHF3Xip041K1nvdXcZwcwmsDsARshSoo,18093
3
+ multissh3-5.99.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
4
+ multissh3-5.99.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
5
+ multissh3-5.99.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
6
+ multissh3-5.99.dist-info/RECORD,,
@@ -1,6 +0,0 @@
1
- multiSSH3.py,sha256=AXZqfsQ3c0O0qIGUedwGChpLnkxPX_pDtDEtBj_L7Rk,181554
2
- multissh3-5.97.dist-info/METADATA,sha256=26GBKHydWcT4nR5HO3St3ubhaH6zp2-_ajR6RvOX6rI,18093
3
- multissh3-5.97.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
4
- multissh3-5.97.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
5
- multissh3-5.97.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
6
- multissh3-5.97.dist-info/RECORD,,