multiSSH3 5.27__tar.gz → 5.30__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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: multiSSH3
3
- Version: 5.27
3
+ Version: 5.30
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: multiSSH3
3
- Version: 5.27
3
+ Version: 5.30
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
@@ -24,6 +24,7 @@ import shutil
24
24
  import getpass
25
25
  import uuid
26
26
  import tempfile
27
+ import math
27
28
 
28
29
  try:
29
30
  # Check if functiools.cache is available
@@ -36,7 +37,7 @@ except AttributeError:
36
37
  # If neither is available, use a dummy decorator
37
38
  def cache_decorator(func):
38
39
  return func
39
- version = '5.27'
40
+ version = '5.30'
40
41
  VERSION = version
41
42
 
42
43
  CONFIG_FILE = '/etc/multiSSH3.config.json'
@@ -179,12 +180,17 @@ class Host:
179
180
  self.uuid = uuid
180
181
  self.identity_file = identity_file
181
182
  self.ip = ip if ip else getIP(name)
183
+ self.current_color_pair = [-1, -1, 1]
182
184
 
183
185
  def __iter__(self):
184
186
  return zip(['name', 'command', 'returncode', 'stdout', 'stderr'], [self.name, self.command, self.returncode, self.stdout, self.stderr])
185
187
  def __repr__(self):
186
188
  # return the complete data structure
187
- return f"Host(name={self.name}, command={self.command}, returncode={self.returncode}, stdout={self.stdout}, stderr={self.stderr}, output={self.output}, printedLines={self.printedLines}, files={self.files}, ipmi={self.ipmi}, interface_ip_prefix={self.interface_ip_prefix}, scp={self.scp}, gatherMode={self.gatherMode}, extraargs={self.extraargs}, resolvedName={self.resolvedName}, i={self.i}, uuid={self.uuid}), identity_file={self.identity_file}"
189
+ return f"Host(name={self.name}, command={self.command}, returncode={self.returncode}, stdout={self.stdout}, stderr={self.stderr}, \
190
+ output={self.output}, printedLines={self.printedLines}, files={self.files}, ipmi={self.ipmi}, \
191
+ interface_ip_prefix={self.interface_ip_prefix}, scp={self.scp}, gatherMode={self.gatherMode}, \
192
+ extraargs={self.extraargs}, resolvedName={self.resolvedName}, i={self.i}, uuid={self.uuid}), \
193
+ identity_file={self.identity_file}, ip={self.ip}, current_color_pair={self.current_color_pair}"
188
194
  def __str__(self):
189
195
  return f"Host(name={self.name}, command={self.command}, returncode={self.returncode}, stdout={self.stdout}, stderr={self.stderr})"
190
196
 
@@ -341,7 +347,30 @@ if True:
341
347
  __keyPressesIn = [[]]
342
348
  _emo = False
343
349
  _etc_hosts = __configs_from_file.get('_etc_hosts', __build_in_default_config['_etc_hosts'])
344
-
350
+ __curses_global_color_pairs = {(-1,-1):1}
351
+ __curses_current_color_pair_index = 2 # Start from 1, as 0 is the default color pair
352
+ __curses_color_table = {}
353
+ __curses_current_color_index = 10
354
+
355
+ # Mapping of ANSI 4-bit colors to curses colors
356
+ ANSI_TO_CURSES_COLOR = {
357
+ 30: curses.COLOR_BLACK,
358
+ 31: curses.COLOR_RED,
359
+ 32: curses.COLOR_GREEN,
360
+ 33: curses.COLOR_YELLOW,
361
+ 34: curses.COLOR_BLUE,
362
+ 35: curses.COLOR_MAGENTA,
363
+ 36: curses.COLOR_CYAN,
364
+ 37: curses.COLOR_WHITE,
365
+ 90: curses.COLOR_BLACK, # Bright Black (usually gray)
366
+ 91: curses.COLOR_RED, # Bright Red
367
+ 92: curses.COLOR_GREEN, # Bright Green
368
+ 93: curses.COLOR_YELLOW, # Bright Yellow
369
+ 94: curses.COLOR_BLUE, # Bright Blue
370
+ 95: curses.COLOR_MAGENTA, # Bright Magenta
371
+ 96: curses.COLOR_CYAN, # Bright Cyan
372
+ 97: curses.COLOR_WHITE # Bright White
373
+ }
345
374
  # ------------ Exportable Help Functions ----------------
346
375
  # check if command sshpass is available
347
376
  _binPaths = {}
@@ -998,9 +1027,6 @@ def __expand_hostnames(hosts) -> dict:
998
1027
  username, host = host.split('@',1)
999
1028
  username = username.strip()
1000
1029
  host = host.strip()
1001
- username, host = host.split('@',1)
1002
- username = username.strip()
1003
- host = host.strip()
1004
1030
  # first we check if the hostname is an range of IP addresses
1005
1031
  # This is done by checking if the hostname follows four fields of
1006
1032
  # "(((\d{1,3}|x|\*|\?)(-(\d{1,3}|x|\*|\?))?)|(\[(\d{1,3}|x|\*|\?)(-(\d{1,3}|x|\*|\?))?\]))"
@@ -1424,6 +1450,227 @@ def start_run_on_hosts(hosts, timeout=60,password=None,max_connections=4 * os.cp
1424
1450
  return threads
1425
1451
 
1426
1452
  # ------------ Display Block ----------------
1453
+ def __approximate_color_8bit(color):
1454
+ """
1455
+ Approximate an 8-bit color (0-255) to the nearest curses color.
1456
+
1457
+ Args:
1458
+ color: 8-bit color code
1459
+
1460
+ Returns:
1461
+ Curses color code
1462
+ """
1463
+ if color < 8: # Standard and bright colors
1464
+ return ANSI_TO_CURSES_COLOR.get(color % 8 + 30, curses.COLOR_WHITE)
1465
+ elif 8 <= color < 16: # Bright colors
1466
+ return ANSI_TO_CURSES_COLOR.get(color % 8 + 90, curses.COLOR_WHITE)
1467
+ elif 16 <= color <= 231: # Color cube
1468
+ # Convert 216-color cube index to RGB
1469
+ color -= 16
1470
+ r = (color // 36) % 6 * 51
1471
+ g = (color // 6) % 6 * 51
1472
+ b = color % 6 * 51
1473
+ return __approximate_color_24bit(r, g, b) # Map to the closest curses color
1474
+ elif 232 <= color <= 255: # Grayscale
1475
+ gray = (color - 232) * 10 + 8
1476
+ return __approximate_color_24bit(gray, gray, gray)
1477
+ else:
1478
+ return curses.COLOR_WHITE # Fallback to white for unexpected values
1479
+
1480
+ def __approximate_color_24bit(r, g, b):
1481
+ """
1482
+ Approximate a 24-bit RGB color to the nearest curses color.
1483
+ Will initiate a curses color if curses.can_change_color() is True.
1484
+
1485
+ Globals:
1486
+ __curses_color_table: Dictionary of RGB color to curses color code
1487
+ __curses_current_color_index: Current index of the
1488
+
1489
+ Args:
1490
+ r: Red component (0-255)
1491
+ g: Green component (0-255)
1492
+ b: Blue component (0-255)
1493
+
1494
+ Returns:
1495
+ Curses color code
1496
+ """
1497
+ if curses.can_change_color():
1498
+ global __curses_color_table,__curses_current_color_index
1499
+ # Initiate a new color if it does not exist
1500
+ if (r, g, b) not in __curses_color_table:
1501
+ if __curses_current_color_index >= curses.COLORS:
1502
+ eprint("Warning: Maximum number of colors reached. Wrapping around.")
1503
+ __curses_current_color_index = 10
1504
+ curses.init_color(__curses_current_color_index, int(r/255*1000), int(g/255*1000), int(b/255*1000))
1505
+ __curses_color_table[(r, g, b)] = __curses_current_color_index
1506
+ __curses_current_color_index += 1
1507
+ return __curses_color_table[(r, g, b)]
1508
+ # Fallback to 8-bit color approximation
1509
+ colors = {
1510
+ curses.COLOR_BLACK: (0, 0, 0),
1511
+ curses.COLOR_RED: (255, 0, 0),
1512
+ curses.COLOR_GREEN: (0, 255, 0),
1513
+ curses.COLOR_YELLOW: (255, 255, 0),
1514
+ curses.COLOR_BLUE: (0, 0, 255),
1515
+ curses.COLOR_MAGENTA: (255, 0, 255),
1516
+ curses.COLOR_CYAN: (0, 255, 255),
1517
+ curses.COLOR_WHITE: (255, 255, 255),
1518
+ }
1519
+ best_match = curses.COLOR_WHITE
1520
+ min_distance = float("inf")
1521
+ for color, (cr, cg, cb) in colors.items():
1522
+ distance = math.sqrt((r - cr) ** 2 + (g - cg) ** 2 + (b - cb) ** 2)
1523
+ if distance < min_distance:
1524
+ min_distance = distance
1525
+ best_match = color
1526
+ return best_match
1527
+
1528
+ def __parse_ansi_escape_sequence_to_curses_color(escape_code):
1529
+ """
1530
+ Parse ANSI escape codes to extract foreground and background colors.
1531
+
1532
+ Args:
1533
+ escape_code: ANSI escape sequence for color
1534
+
1535
+ Returns:
1536
+ Tuple of (foreground, background) curses color pairs.
1537
+ If the escape code is a reset code, return (-1, -1).
1538
+ None values indicate that the color should not be changed.
1539
+ """
1540
+ if not escape_code:
1541
+ return None, None
1542
+ color_match = re.match(r"\x1b\[(\d+)(?:;(\d+))?(?:;(\d+))?(?:;(\d+);(\d+);(\d+))?m", escape_code)
1543
+ if color_match:
1544
+ params = color_match.groups()
1545
+ if params[0] == "0" and not any(params[1:]): # Reset code
1546
+ return -1, -1
1547
+ if params[0] == "38" and params[1] == "5": # 8-bit foreground
1548
+ return __approximate_color_8bit(int(params[2])), None
1549
+ elif params[0] == "38" and params[1] == "2": # 24-bit foreground
1550
+ return __approximate_color_24bit(int(params[3]), int(params[4]), int(params[5])), None
1551
+ elif params[0] == "48" and params[1] == "5": # 8-bit background
1552
+ return None , __approximate_color_8bit(int(params[2]))
1553
+ elif params[0] == "48" and params[1] == "2": # 24-bit background
1554
+ return None, __approximate_color_24bit(int(params[3]), int(params[4]), int(params[5]))
1555
+ else:
1556
+ fg = None
1557
+ bg = None
1558
+ if params[0] and params[0].isdigit(): # 4-bit color
1559
+ fg = ANSI_TO_CURSES_COLOR.get(int(params[0]), curses.COLOR_WHITE)
1560
+ if params[1] and params[1].isdigit():
1561
+ bg = ANSI_TO_CURSES_COLOR.get(int(params[1]), curses.COLOR_BLACK)
1562
+ return fg, bg
1563
+ return None, None
1564
+
1565
+ def __get_curses_color_pair(fg, bg):
1566
+ """
1567
+ Use curses color int values to create a curses color pair.
1568
+
1569
+ Globals:
1570
+ __curses_global_color_pairs: Dictionary of color pairs
1571
+ __curses_current_color_pair_index: Current index of the color pair
1572
+
1573
+ Args:
1574
+ fg: Foreground color code
1575
+ bg: Background color code
1576
+
1577
+ Returns:
1578
+ Curses color pair code
1579
+ """
1580
+ global __curses_global_color_pairs, __curses_current_color_pair_index
1581
+ if (fg, bg) not in __curses_global_color_pairs:
1582
+ if __curses_current_color_pair_index >= curses.COLOR_PAIRS:
1583
+ print("Warning: Maximum number of color pairs reached, wrapping around.")
1584
+ __curses_current_color_pair_index = 1
1585
+ curses.init_pair(__curses_current_color_pair_index, fg, bg)
1586
+ __curses_global_color_pairs[(fg, bg)] = __curses_current_color_pair_index
1587
+ __curses_current_color_pair_index += 1
1588
+ return curses.color_pair(__curses_global_color_pairs[(fg, bg)])
1589
+
1590
+ 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_ascii_colors = True,centered = False,lead_str = '', trail_str = '',box_ansi_color = None):
1591
+ """
1592
+ Add a string to a curses window with / without ANSI color escape sequences translated to curses color pairs.
1593
+
1594
+ Args:
1595
+ window: curses window object
1596
+ line: The line to add
1597
+ y: Line position in the window. Use -1 to scroll the window up 1 line and add the line at the bottom
1598
+ x: Column position in the window
1599
+ 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.
1600
+ color_pair_list: List of [foreground, background, color_pair] curses color pair values
1601
+ fill_char: Character to fill the remaining space in the line
1602
+ parse_ascii_colors: Parse ASCII color codes
1603
+ centered: Center the text in the window
1604
+ lead_str: Leading string to add to the line
1605
+ trail_str: Trailing string to add to the line
1606
+
1607
+ Returns:
1608
+ None
1609
+ """
1610
+ if window.getmaxyx()[0] == 0 or window.getmaxyx()[1] == 0 or x >= window.getmaxyx()[1]:
1611
+ return
1612
+ if x < 0:
1613
+ x = window.getmaxyx()[1] + x
1614
+ if number_of_char_to_write == -1:
1615
+ numChar = window.getmaxyx()[1] - x -1
1616
+ elif number_of_char_to_write == 0:
1617
+ return
1618
+ elif number_of_char_to_write + x > window.getmaxyx()[1]:
1619
+ numChar = window.getmaxyx()[1] - x -1
1620
+ else:
1621
+ numChar = number_of_char_to_write
1622
+ if numChar < 0:
1623
+ return
1624
+ if y < 0 or y >= window.getmaxyx()[0]:
1625
+ window.move(0, 0)
1626
+ window.deleteln()
1627
+ y = window.getmaxyx()[0] - 1
1628
+ if parse_ascii_colors:
1629
+ segments = re.split(r"(\x1b\[[\d;]*m)", line) # Split line by ANSI escape codes
1630
+ else:
1631
+ segments = [line]
1632
+ charsWritten = 0
1633
+ boxFrontColor, boxBackColor = color_pair_list[0], color_pair_list[1]
1634
+ newBoxFrontColor, newBoxBackColor = __parse_ansi_escape_sequence_to_curses_color(box_ansi_color)
1635
+ if newBoxFrontColor:
1636
+ boxFrontColor = newBoxFrontColor
1637
+ if newBoxBackColor:
1638
+ boxBackColor = newBoxBackColor
1639
+ boxColorPair = __get_curses_color_pair(boxFrontColor, boxBackColor)
1640
+ # first add the lead_str
1641
+ window.addnstr(y, x, lead_str, numChar, boxColorPair)
1642
+ charsWritten = min(len(lead_str), numChar)
1643
+ # process centering
1644
+ if centered:
1645
+ fill_length = numChar - len(lead_str) - len(trail_str) - sum([len(segment) for segment in segments if not segment.startswith("\x1b[")])
1646
+ window.addnstr(y, x + charsWritten, fill_char * (fill_length // 2), numChar - charsWritten, boxColorPair)
1647
+ charsWritten += min(fill_length // 2, numChar - charsWritten)
1648
+ # add the segments
1649
+ for segment in segments:
1650
+ if parse_ascii_colors and segment.startswith("\x1b["):
1651
+ # Parse ANSI escape sequence
1652
+ newFrontColor, newBackColor = __parse_ansi_escape_sequence_to_curses_color(segment)
1653
+ if newFrontColor is not None:
1654
+ color_pair_list[0] = newFrontColor
1655
+ if newBackColor is not None:
1656
+ color_pair_list[1] = newBackColor
1657
+ color_pair_list[2] = __get_curses_color_pair(color_pair_list[0], color_pair_list[1])
1658
+ #window.addnstr(y, x + charsWritten, str(color_pair_list[2]), numChar - charsWritten, color_pair_list[2])
1659
+ #charsWritten += min(len(str(color_pair_list[2])), numChar - charsWritten)
1660
+ else:
1661
+ # Add text with current color
1662
+ if charsWritten < numChar:
1663
+ window.addnstr(y, x + charsWritten, segment, numChar - charsWritten, color_pair_list[2])
1664
+ charsWritten += min(len(segment), numChar - charsWritten)
1665
+ # if we have finished printing segments but we still have space, we will fill it with fill_char
1666
+ if charsWritten + len(trail_str) < numChar:
1667
+ fillStr = fill_char * (numChar - charsWritten - len(trail_str))
1668
+ #fillStr = f'{color_pair_list}'
1669
+ window.addnstr(y, x + charsWritten, fillStr + trail_str, numChar - charsWritten, boxColorPair)
1670
+ charsWritten += numChar - charsWritten
1671
+ else:
1672
+ window.addnstr(y, x + charsWritten, trail_str, numChar - charsWritten, boxColorPair)
1673
+
1427
1674
  def _get_hosts_to_display (hosts, max_num_hosts, hosts_to_display = None):
1428
1675
  '''
1429
1676
  Generate a list for the hosts to be displayed on the screen. This is used to display as much relevant information as possible.
@@ -1519,6 +1766,7 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
1519
1766
  stdscr.nodelay(True)
1520
1767
  # we generate a stats window at the top of the screen
1521
1768
  stat_window = curses.newwin(1, max_x, 0, 0)
1769
+ stat_window.leaveok(True)
1522
1770
  # We create a window for each host
1523
1771
  host_windows = []
1524
1772
  for i, host in enumerate(hosts_to_display):
@@ -1529,13 +1777,18 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
1529
1777
  #print(f"Creating a window at {y},{x}")
1530
1778
  # We create the window
1531
1779
  host_window = curses.newwin(host_window_height, host_window_width, y, x)
1780
+ host_window.idlok(True)
1781
+ host_window.scrollok(True)
1782
+ host_window.leaveok(True)
1532
1783
  host_windows.append(host_window)
1533
1784
  # If there is space left, we will draw the bottom border
1534
1785
  bottom_border = None
1535
1786
  if y + host_window_height < org_dim[0]:
1536
1787
  bottom_border = curses.newwin(1, max_x, y + host_window_height, 0)
1788
+ bottom_border.leaveok(True)
1537
1789
  #bottom_border.clear()
1538
- bottom_border.addstr(0, 0, '-' * (max_x - 1))
1790
+ #bottom_border.addnstr(0, 0, '-' * (max_x - 1), max_x - 1)
1791
+ _curses_add_string_to_window(window=bottom_border, y=0, line='-' * (max_x - 1),fill_char='-')
1539
1792
  bottom_border.refresh()
1540
1793
  while host_stats['running'] > 0 or host_stats['waiting'] > 0:
1541
1794
  # Check for keypress
@@ -1637,11 +1890,13 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
1637
1890
  # encodedLine = encodedLine[:curserPosition] + ' ' + encodedLine[curserPosition:]
1638
1891
  stats = '┍'+ f"Send CMD: {encodedLine}"[:max_x - 2].center(max_x - 2, "━")
1639
1892
  if bottom_border:
1640
- bottom_stats = '└'+ f" Total: {len(hosts)} Running: {host_stats['running']} Failed: {host_stats['failed']} Finished: {host_stats['finished']} Waiting: {host_stats['waiting']} "[:max_x - 2].center(max_x - 2, "─")
1893
+ target_length = max_x - 2 + len('\x1b[33m\x1b[0m\x1b[31m\x1b[0m\x1b[32m\x1b[0m')
1894
+ bottom_stats = '└'+ f" Total: {len(hosts)} Running: \x1b[33m{host_stats['running']}\x1b[0m Failed: \x1b[31m{host_stats['failed']}\x1b[0m Finished: \x1b[32m{host_stats['finished']}\x1b[0m Waiting: {host_stats['waiting']} "[:target_length].center(target_length, "─")
1641
1895
  if bottom_stats != old_bottom_stat:
1642
1896
  old_bottom_stat = bottom_stats
1643
1897
  #bottom_border.clear()
1644
- bottom_border.addstr(0, 0, bottom_stats)
1898
+ #bottom_border.addnstr(0, 0, bottom_stats, max_x - 1)
1899
+ _curses_add_string_to_window(window=bottom_border, y=0, line=bottom_stats)
1645
1900
  bottom_border.refresh()
1646
1901
  if stats != old_stat or curserPosition != old_cursor_position:
1647
1902
  old_stat = stats
@@ -1654,9 +1909,9 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
1654
1909
  #stat_window.clear()
1655
1910
  #stat_window.addstr(0, 0, stats)
1656
1911
  # add the line with curser that inverses the color at the curser position
1657
- stat_window.addstr(0, 0, stats[:curserPositionStats], curses.color_pair(1))
1658
- stat_window.addstr(0, curserPositionStats, stats[curserPositionStats], curses.color_pair(2))
1659
- stat_window.addstr(0, curserPositionStats + 1, stats[curserPositionStats + 1:], curses.color_pair(1))
1912
+ stat_window.addstr(0, 0, stats[:curserPositionStats])
1913
+ stat_window.addch(0,curserPositionStats, stats[curserPositionStats], curses.A_REVERSE)
1914
+ stat_window.addnstr(0, curserPositionStats + 1, stats[curserPositionStats + 1:], max_x - 1 - curserPositionStats)
1660
1915
  stat_window.refresh()
1661
1916
  # set the maximum refresh rate to 100 Hz
1662
1917
  if time.perf_counter() - last_refresh_time < 0.01:
@@ -1670,17 +1925,19 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
1670
1925
  #host_window.clear()
1671
1926
  # we will try to center the name of the host with ┼ at the beginning and end and ─ in between
1672
1927
  linePrintOut = f'┼{(host.name+":["+host.command+"]")[:host_window_width - 2].center(host_window_width - 1, "─")}'.replace('\n', ' ').replace('\r', ' ').strip()
1673
- host_window.addstr(0, 0, linePrintOut)
1928
+ host_window.addnstr(0, 0, linePrintOut, host_window_width - 1)
1929
+ #_add_line_with_ascii_colors(window=host_window, y=0, x=0, line=linePrintOut, n=host_window_width - 1, color_pair_list = host.current_color_pair)
1674
1930
  # we will display the latest outputs of the host as much as we can
1675
1931
  for i, line in enumerate(host.output[-(host_window_height - 1):]):
1676
1932
  # print(f"Printng a line at {i + 1} with length of {len('│'+line[:host_window_width - 1])}")
1677
1933
  # time.sleep(10)
1678
- linePrintOut = ('│'+line[:host_window_width - 2].replace('\n', ' ').replace('\r', ' ')).strip().ljust(host_window_width - 1, ' ')
1679
- host_window.addstr(i + 1, 0, linePrintOut)
1934
+ linePrintOut = ('│'+line[:host_window_width - 2].replace('\n', ' ').replace('\r', ' ')).strip()
1935
+ #host_window.addnstr(i + 1, 0, linePrintOut, host_window_width - 1)
1936
+ _curses_add_string_to_window(window=host_window, y=i + 1, line=linePrintOut, color_pair_list=host.current_color_pair)
1680
1937
  # we draw the rest of the available lines
1681
1938
  for i in range(len(host.output), host_window_height - 1):
1682
1939
  # print(f"Printng a line at {i + 1} with length of {len('│')}")
1683
- host_window.addstr(i + 1, 0, '│'.ljust(host_window_width - 1, ' '))
1940
+ host_window.addnstr(i + 1, 0, '│'.ljust(host_window_width - 1, ' '), host_window_width - 1)
1684
1941
  host.printedLines = len(host.output)
1685
1942
  host_window.refresh()
1686
1943
  except Exception as e:
@@ -1710,33 +1967,39 @@ def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_C
1710
1967
  '''
1711
1968
  # We create all the windows we need
1712
1969
  # We initialize the color pair
1713
- curses.start_color()
1714
1970
  curses.curs_set(0)
1715
- curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLACK)
1716
- curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_WHITE)
1717
- curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK)
1718
- curses.init_pair(4, curses.COLOR_GREEN, curses.COLOR_BLACK)
1719
- curses.init_pair(5, curses.COLOR_YELLOW, curses.COLOR_BLACK)
1720
- curses.init_pair(6, curses.COLOR_BLUE, curses.COLOR_BLACK)
1721
- curses.init_pair(7, curses.COLOR_MAGENTA, curses.COLOR_BLACK)
1722
- curses.init_pair(8, curses.COLOR_CYAN, curses.COLOR_BLACK)
1723
- curses.init_pair(9, curses.COLOR_WHITE, curses.COLOR_RED)
1724
- curses.init_pair(10, curses.COLOR_WHITE, curses.COLOR_GREEN)
1725
- curses.init_pair(11, curses.COLOR_WHITE, curses.COLOR_YELLOW)
1726
- curses.init_pair(12, curses.COLOR_WHITE, curses.COLOR_BLUE)
1727
- curses.init_pair(13, curses.COLOR_WHITE, curses.COLOR_MAGENTA)
1728
- curses.init_pair(14, curses.COLOR_WHITE, curses.COLOR_CYAN)
1729
- curses.init_pair(15, curses.COLOR_BLACK, curses.COLOR_RED)
1730
- curses.init_pair(16, curses.COLOR_BLACK, curses.COLOR_GREEN)
1731
- curses.init_pair(17, curses.COLOR_BLACK, curses.COLOR_YELLOW)
1732
- curses.init_pair(18, curses.COLOR_BLACK, curses.COLOR_BLUE)
1733
- curses.init_pair(19, curses.COLOR_BLACK, curses.COLOR_MAGENTA)
1734
- curses.init_pair(20, curses.COLOR_BLACK, curses.COLOR_CYAN)
1735
-
1971
+ curses.start_color()
1972
+ curses.use_default_colors()
1973
+ curses.init_pair(1, -1, -1)
1736
1974
  # do not generate display if the output window have a size of zero
1737
1975
  if stdscr.getmaxyx()[0] < 2 or stdscr.getmaxyx()[1] < 2:
1738
1976
  return
1739
-
1977
+ stdscr.idlok(True)
1978
+ stdscr.scrollok(True)
1979
+ stdscr.leaveok(True)
1980
+ # generate some debug information before display initialization
1981
+ try:
1982
+ stdscr.clear()
1983
+ _curses_add_string_to_window(window=stdscr, y=0, line='Initializing display...', n=stdscr.getmaxyx()[1] - 1)
1984
+ # print the size
1985
+ _curses_add_string_to_window(window=stdscr, y=1, line=f"Terminal size: {stdscr.getmaxyx()}", n=stdscr.getmaxyx()[1] - 1)
1986
+ # print the number of hosts
1987
+ _curses_add_string_to_window(window=stdscr, y=2, line=f"Number of hosts: {len(hosts)}", n=stdscr.getmaxyx()[1] - 1)
1988
+ # print the number of threads
1989
+ _curses_add_string_to_window(window=stdscr, y=3, line=f"Number of threads: {len(threads)}", n=stdscr.getmaxyx()[1] - 1)
1990
+ # print the minimum character length
1991
+ _curses_add_string_to_window(window=stdscr, y=4, line=f"Minimum character length: {min_char_len}", n=stdscr.getmaxyx()[1] - 1)
1992
+ # print the minimum line length
1993
+ _curses_add_string_to_window(window=stdscr, y=5, line=f"Minimum line length: {min_line_len}", n=stdscr.getmaxyx()[1] - 1)
1994
+ # print the single window mode
1995
+ _curses_add_string_to_window(window=stdscr, y=6, line=f"Single window mode: {single_window}", n=stdscr.getmaxyx()[1] - 1)
1996
+ # print COLORS and COLOR_PAIRS count
1997
+ _curses_add_string_to_window(window=stdscr, y=7, line=f"len(COLORS): {curses.COLORS} len(COLOR_PAIRS): {curses.COLOR_PAIRS}", n=stdscr.getmaxyx()[1] - 1)
1998
+ # print if can change color
1999
+ _curses_add_string_to_window(window=stdscr, y=8, line=f"Real color capability: {curses.can_change_color()}", n=stdscr.getmaxyx()[1] - 1)
2000
+ stdscr.refresh()
2001
+ except:
2002
+ pass
1740
2003
  params = (-1,0 , min_char_len, min_line_len, single_window,'new config')
1741
2004
  while params:
1742
2005
  params = __generate_display(stdscr, hosts, *params)
@@ -2,7 +2,7 @@ from setuptools import setup
2
2
 
3
3
  setup(
4
4
  name='multiSSH3',
5
- version='5.27',
5
+ version='5.30',
6
6
  description='Run commands on multiple hosts via SSH',
7
7
  long_description=open('README.md').read(),
8
8
  long_description_content_type='text/markdown',
File without changes
File without changes
File without changes