multiSSH3 5.30__tar.gz → 5.33__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.30
3
+ Version: 5.33
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.30
3
+ Version: 5.33
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
@@ -37,7 +37,7 @@ except AttributeError:
37
37
  # If neither is available, use a dummy decorator
38
38
  def cache_decorator(func):
39
39
  return func
40
- version = '5.30'
40
+ version = '5.33'
41
41
  VERSION = version
42
42
 
43
43
  CONFIG_FILE = '/etc/multiSSH3.config.json'
@@ -166,6 +166,7 @@ class Host:
166
166
  self.stdout = [] # the stdout of the command
167
167
  self.stderr = [] # the stderr of the command
168
168
  self.printedLines = -1 # the number of lines printed on the screen
169
+ self.lineNumToReprintSet = set() # whether to reprint the last line
169
170
  self.lastUpdateTime = time.time() # the last time the output was updated
170
171
  self.files = files # the files to be copied to the host
171
172
  self.ipmi = ipmi # whether to use ipmi to connect to the host
@@ -187,7 +188,7 @@ class Host:
187
188
  def __repr__(self):
188
189
  # return the complete data structure
189
190
  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
+ output={self.output}, printedLines={self.printedLines}, lineNumToReprintSet={self.lineNumToReprintSet}, files={self.files}, ipmi={self.ipmi}, \
191
192
  interface_ip_prefix={self.interface_ip_prefix}, scp={self.scp}, gatherMode={self.gatherMode}, \
192
193
  extraargs={self.extraargs}, resolvedName={self.resolvedName}, i={self.i}, uuid={self.uuid}), \
193
194
  identity_file={self.identity_file}, ip={self.ip}, current_color_pair={self.current_color_pair}"
@@ -1085,10 +1086,10 @@ def __handle_reading_stream(stream,target, host):
1085
1086
  if not keepLastLine:
1086
1087
  target.pop()
1087
1088
  host.output.pop()
1088
- host.printedLines -= 1
1089
1089
  current_line_str = current_line.decode('utf-8',errors='backslashreplace')
1090
1090
  target.append(current_line_str)
1091
1091
  host.output.append(current_line_str)
1092
+ host.lineNumToReprintSet.add(len(host.output)-1)
1092
1093
  host.lastUpdateTime = time.time()
1093
1094
  current_line = bytearray()
1094
1095
  lastLineCommited = True
@@ -1355,12 +1356,13 @@ def run_command(host, sem, timeout=60,passwds=None):
1355
1356
  # remove last line if it is a countdown
1356
1357
  if host.output and timeoutLineAppended and host.output[-1].strip().endswith('] seconds!') and host.output[-1].strip().startswith('Timeout in ['):
1357
1358
  host.output.pop()
1358
- host.printedLines -= 1
1359
1359
  host.output.append(timeoutLine)
1360
+ host.lineNumToReprintSet.add(len(host.output)-1)
1360
1361
  timeoutLineAppended = True
1361
1362
  elif host.output and timeoutLineAppended and host.output[-1].strip().endswith('] seconds!') and host.output[-1].strip().startswith('Timeout in ['):
1362
1363
  host.output.pop()
1363
- host.printedLines -= 1
1364
+ host.output.append('')
1365
+ host.lineNumToReprintSet.add(len(host.output)-1)
1364
1366
  timeoutLineAppended = False
1365
1367
  if _emo:
1366
1368
  host.stderr.append('Ctrl C detected, Emergency Stop!')
@@ -1525,43 +1527,6 @@ def __approximate_color_24bit(r, g, b):
1525
1527
  best_match = color
1526
1528
  return best_match
1527
1529
 
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
1530
  def __get_curses_color_pair(fg, bg):
1566
1531
  """
1567
1532
  Use curses color int values to create a curses color pair.
@@ -1587,7 +1552,123 @@ def __get_curses_color_pair(fg, bg):
1587
1552
  __curses_current_color_pair_index += 1
1588
1553
  return curses.color_pair(__curses_global_color_pairs[(fg, bg)])
1589
1554
 
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):
1555
+ def __parse_ansi_escape_sequence_to_curses_attr(escape_code,color_pair_list = None):
1556
+ """
1557
+ Parse ANSI escape codes to extract foreground and background colors.
1558
+
1559
+ Args:
1560
+ escape_code: ANSI escape sequence for color
1561
+ color_pair_list: List of [foreground, background, color_pair] curses color pair values
1562
+
1563
+ Returns:
1564
+ Curses color pair / attribute code
1565
+ """
1566
+ if not escape_code:
1567
+ return 1
1568
+ if not color_pair_list:
1569
+ color_pair_list = [-1,-1,1]
1570
+ color_match = escape_code.lstrip("\x1b[").rstrip("m").split(";")
1571
+ color_match = [x if x else '0' for x in color_match] # Replace empty strings with '0' (reset)
1572
+ if color_match:
1573
+ processed_index = -1
1574
+ for i, param in enumerate(color_match):
1575
+ if processed_index >= i:
1576
+ # if the index has been processed, skip
1577
+ continue
1578
+ if param.isdigit():
1579
+ if int(param) == 0:
1580
+ color_pair_list[0] = -1
1581
+ color_pair_list[1] = -1
1582
+ color_pair_list[2] = 1
1583
+ elif int(param) == 38:
1584
+ if i + 1 >= len(color_match):
1585
+ # Invalid color code, skip
1586
+ continue
1587
+ if color_match[i + 1] == "5":
1588
+ # 8-bit foreground color
1589
+ if i + 2 >= len(color_match) or not color_match[i + 2].isdigit():
1590
+ # Invalid color code, skip
1591
+ processed_index = i + 1
1592
+ continue
1593
+ color_pair_list[0] = __approximate_color_8bit(int(color_match[i + 2]))
1594
+ color_pair_list[2] = __get_curses_color_pair(color_pair_list[0], color_pair_list[1])
1595
+ processed_index = i + 2
1596
+ elif color_match[i + 1] == "2":
1597
+ # 24-bit foreground color
1598
+ if i + 4 >= len(color_match) or not all(x.isdigit() for x in color_match[i + 2:i + 5]):
1599
+ # Invalid color code, skip
1600
+ processed_index = i + 1
1601
+ continue
1602
+ color_pair_list[0] = __approximate_color_24bit(int(color_match[i + 2]), int(color_match[i + 3]), int(color_match[i + 4]))
1603
+ color_pair_list[2] = __get_curses_color_pair(color_pair_list[0], color_pair_list[1])
1604
+ processed_index = i + 4
1605
+ elif int(param) == 48:
1606
+ if i + 1 >= len(color_match):
1607
+ # Invalid color code, skip
1608
+ continue
1609
+ if color_match[i + 1] == "5":
1610
+ # 8-bit background color
1611
+ if i + 2 >= len(color_match) or not color_match[i + 2].isdigit():
1612
+ # Invalid color code, skip
1613
+ processed_index = i + 1
1614
+ continue
1615
+ color_pair_list[1] = __approximate_color_8bit(int(color_match[i + 2]))
1616
+ color_pair_list[2] = __get_curses_color_pair(color_pair_list[0], color_pair_list[1])
1617
+ processed_index = i + 2
1618
+ elif color_match[i + 1] == "2":
1619
+ # 24-bit background color
1620
+ if i + 4 >= len(color_match) or not all(x.isdigit() for x in color_match[i + 2:i + 5]):
1621
+ # Invalid color code, skip
1622
+ processed_index = i + 1
1623
+ continue
1624
+ color_pair_list[1] = __approximate_color_24bit(int(color_match[i + 2]), int(color_match[i + 3]), int(color_match[i + 4]))
1625
+ color_pair_list[2] = __get_curses_color_pair(color_pair_list[0], color_pair_list[1])
1626
+ processed_index = i + 4
1627
+ elif 30 <= int(param) <= 37 or 90 <= int(param) <= 97:
1628
+ # 4-bit foreground color
1629
+ color_pair_list[0] = ANSI_TO_CURSES_COLOR.get(int(param), curses.COLOR_WHITE)
1630
+ color_pair_list[2] = __get_curses_color_pair(color_pair_list[0], color_pair_list[1])
1631
+ elif 40 <= int(param) <= 47 or 100 <= int(param) <= 107:
1632
+ # 4-bit background color
1633
+ color_pair_list[1] = ANSI_TO_CURSES_COLOR.get(int(param)-10, curses.COLOR_BLACK)
1634
+ color_pair_list[2] = __get_curses_color_pair(color_pair_list[0], color_pair_list[1])
1635
+ elif int(param) == 1:
1636
+ color_pair_list[2] = color_pair_list[2] | curses.A_BOLD
1637
+ elif int(param) == 2:
1638
+ color_pair_list[2] = color_pair_list[2] | curses.A_DIM
1639
+ elif int(param) == 4:
1640
+ color_pair_list[2] = color_pair_list[2] | curses.A_UNDERLINE
1641
+ elif int(param) == 5:
1642
+ color_pair_list[2] = color_pair_list[2] | curses.A_BLINK
1643
+ elif int(param) == 7:
1644
+ color_pair_list[2] = color_pair_list[2] | curses.A_REVERSE
1645
+ elif int(param) == 8:
1646
+ color_pair_list[2] = color_pair_list[2] | curses.A_INVIS
1647
+ elif int(param) == 21:
1648
+ color_pair_list[2] = color_pair_list[2] & ~curses.A_BOLD
1649
+ elif int(param) == 22:
1650
+ color_pair_list[2] = color_pair_list[2] & ~curses.A_DIM
1651
+ elif int(param) == 24:
1652
+ color_pair_list[2] = color_pair_list[2] & ~curses.A_UNDERLINE
1653
+ elif int(param) == 25:
1654
+ color_pair_list[2] = color_pair_list[2] & ~curses.A_BLINK
1655
+ elif int(param) == 27:
1656
+ color_pair_list[2] = color_pair_list[2] & ~curses.A_REVERSE
1657
+ elif int(param) == 28:
1658
+ color_pair_list[2] = color_pair_list[2] & ~curses.A_INVIS
1659
+ elif int(param) == 39:
1660
+ color_pair_list[0] = -1
1661
+ color_pair_list[2] = __get_curses_color_pair(color_pair_list[0], color_pair_list[1])
1662
+ elif int(param) == 49:
1663
+ color_pair_list[1] = -1
1664
+ color_pair_list[2] = __get_curses_color_pair(color_pair_list[0], color_pair_list[1])
1665
+ else:
1666
+ color_pair_list[0] = -1
1667
+ color_pair_list[1] = -1
1668
+ color_pair_list[2] = 1
1669
+ return color_pair_list[2]
1670
+
1671
+ 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):
1591
1672
  """
1592
1673
  Add a string to a curses window with / without ANSI color escape sequences translated to curses color pairs.
1593
1674
 
@@ -1599,10 +1680,12 @@ def _curses_add_string_to_window(window, line, y = 0, x = 0, number_of_char_to_w
1599
1680
  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
1681
  color_pair_list: List of [foreground, background, color_pair] curses color pair values
1601
1682
  fill_char: Character to fill the remaining space in the line
1602
- parse_ascii_colors: Parse ASCII color codes
1683
+ parse_ansi_colors: Parse ASCII color codes
1603
1684
  centered: Center the text in the window
1604
1685
  lead_str: Leading string to add to the line
1605
1686
  trail_str: Trailing string to add to the line
1687
+ box_ansi_color: ANSI color escape sequence for the box color
1688
+ keep_top_n_lines: Number of lines to keep at the top of the window
1606
1689
 
1607
1690
  Returns:
1608
1691
  None
@@ -1622,41 +1705,35 @@ def _curses_add_string_to_window(window, line, y = 0, x = 0, number_of_char_to_w
1622
1705
  if numChar < 0:
1623
1706
  return
1624
1707
  if y < 0 or y >= window.getmaxyx()[0]:
1625
- window.move(0, 0)
1708
+ if keep_top_n_lines > window.getmaxyx()[0] -1:
1709
+ keep_top_n_lines = window.getmaxyx()[0] -1
1710
+ if keep_top_n_lines < 0:
1711
+ keep_top_n_lines = 0
1712
+ window.move(keep_top_n_lines,0)
1626
1713
  window.deleteln()
1627
1714
  y = window.getmaxyx()[0] - 1
1628
- if parse_ascii_colors:
1715
+ line = line.replace('\n', ' ').replace('\r', ' ')
1716
+ if parse_ansi_colors:
1629
1717
  segments = re.split(r"(\x1b\[[\d;]*m)", line) # Split line by ANSI escape codes
1630
1718
  else:
1631
1719
  segments = [line]
1632
1720
  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)
1721
+ boxAttr = __parse_ansi_escape_sequence_to_curses_attr(box_ansi_color)
1640
1722
  # first add the lead_str
1641
- window.addnstr(y, x, lead_str, numChar, boxColorPair)
1723
+ window.addnstr(y, x, lead_str, numChar, boxAttr)
1642
1724
  charsWritten = min(len(lead_str), numChar)
1643
1725
  # process centering
1644
1726
  if centered:
1645
1727
  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)
1728
+ window.addnstr(y, x + charsWritten, fill_char * (fill_length // 2), numChar - charsWritten, boxAttr)
1647
1729
  charsWritten += min(fill_length // 2, numChar - charsWritten)
1648
1730
  # add the segments
1649
1731
  for segment in segments:
1650
- if parse_ascii_colors and segment.startswith("\x1b["):
1732
+ if not segment:
1733
+ continue
1734
+ if parse_ansi_colors and segment.startswith("\x1b["):
1651
1735
  # 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)
1736
+ newAttr = __parse_ansi_escape_sequence_to_curses_attr(segment,color_pair_list)
1660
1737
  else:
1661
1738
  # Add text with current color
1662
1739
  if charsWritten < numChar:
@@ -1666,10 +1743,10 @@ def _curses_add_string_to_window(window, line, y = 0, x = 0, number_of_char_to_w
1666
1743
  if charsWritten + len(trail_str) < numChar:
1667
1744
  fillStr = fill_char * (numChar - charsWritten - len(trail_str))
1668
1745
  #fillStr = f'{color_pair_list}'
1669
- window.addnstr(y, x + charsWritten, fillStr + trail_str, numChar - charsWritten, boxColorPair)
1746
+ window.addnstr(y, x + charsWritten, fillStr + trail_str, numChar - charsWritten, boxAttr)
1670
1747
  charsWritten += numChar - charsWritten
1671
1748
  else:
1672
- window.addnstr(y, x + charsWritten, trail_str, numChar - charsWritten, boxColorPair)
1749
+ window.addnstr(y, x + charsWritten, trail_str, numChar - charsWritten, boxAttr)
1673
1750
 
1674
1751
  def _get_hosts_to_display (hosts, max_num_hosts, hosts_to_display = None):
1675
1752
  '''
@@ -1705,6 +1782,7 @@ def _get_hosts_to_display (hosts, max_num_hosts, hosts_to_display = None):
1705
1782
 
1706
1783
  def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,single_window=DEFAULT_SINGLE_WINDOW, config_reason= 'New Configuration'):
1707
1784
  try:
1785
+ box_ansi_color = None
1708
1786
  org_dim = stdscr.getmaxyx()
1709
1787
  new_configured = True
1710
1788
  # To do this, first we need to know the size of the terminal
@@ -1757,7 +1835,6 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
1757
1835
 
1758
1836
  old_stat = ''
1759
1837
  old_bottom_stat = ''
1760
- old_cursor_position = -1
1761
1838
  # we refresh the screen every 0.1 seconds
1762
1839
  last_refresh_time = time.perf_counter()
1763
1840
  stdscr.clear()
@@ -1788,7 +1865,7 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
1788
1865
  bottom_border.leaveok(True)
1789
1866
  #bottom_border.clear()
1790
1867
  #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='-')
1868
+ _curses_add_string_to_window(window=bottom_border, y=0, line='-' * (max_x - 1),fill_char='-',box_ansi_color=box_ansi_color)
1792
1869
  bottom_border.refresh()
1793
1870
  while host_stats['running'] > 0 or host_stats['waiting'] > 0:
1794
1871
  # Check for keypress
@@ -1879,40 +1956,40 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
1879
1956
  return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Terminal resize detected')
1880
1957
  # We generate the aggregated stats if user did not input anything
1881
1958
  if not __keyPressesIn[lineToDisplay]:
1882
- stats = '┍'+ f" Total: {len(hosts)} Running: {host_stats['running']} Failed: {host_stats['failed']} Finished: {host_stats['finished']} Waiting: {host_stats['waiting']} ww: {min_char_len} wh:{min_line_len} "[:max_x - 2].center(max_x - 2, "━")
1959
+ #stats = '┍'+ f" Total: {len(hosts)} Running: {host_stats['running']} Failed: {host_stats['failed']} Finished: {host_stats['finished']} Waiting: {host_stats['waiting']} ww: {min_char_len} wh:{min_line_len} "[:max_x - 2].center(max_x - 2, "━")
1960
+ stats = f"Total: {len(hosts)} Running: {host_stats['running']} Failed: {host_stats['failed']} Finished: {host_stats['finished']} Waiting: {host_stats['waiting']} ww: {min_char_len} wh:{min_line_len} "
1883
1961
  else:
1884
1962
  # we use the stat bar to display the key presses
1885
1963
  encodedLine = ''.join(__keyPressesIn[lineToDisplay]).encode().decode().strip('\n') + ' '
1886
- # # add the flashing indicator at the curse position
1887
- # if time.perf_counter() % 1 > 0.5:
1888
- # encodedLine = encodedLine[:curserPosition] + '█' + encodedLine[curserPosition:]
1964
+ #stats = '┍'+ f"Send CMD: {encodedLine}"[:max_x - 2].center(max_x - 2, "━")
1965
+ stats = f"Send CMD: {encodedLine}"
1966
+ # format the stats line with chracter at curser position inverted using ansi escape sequence
1967
+ stats = f'{stats[:curserPosition]}\x1b[7m{stats[curserPosition]}\x1b[0m{stats[curserPosition + 1:]}'
1968
+ if stats != old_stat :
1969
+ old_stat = stats
1970
+ # calculate the real curser position in stats as we centered the stats
1971
+ # if 'Send CMD: ' in stats:
1972
+ # curserPositionStats = min(min(curserPosition,len(encodedLine) -1) + stats.find('Send CMD: ')+len('Send CMD: '), max_x -2)
1889
1973
  # else:
1890
- # encodedLine = encodedLine[:curserPosition] + ' ' + encodedLine[curserPosition:]
1891
- stats = '┍'+ f"Send CMD: {encodedLine}"[:max_x - 2].center(max_x - 2, "━")
1974
+ # curserPositionStats = max_x -2
1975
+ #stat_window.clear()
1976
+ #stat_window.addstr(0, 0, stats)
1977
+ # add the line with curser that inverses the color at the curser position
1978
+ # stat_window.addstr(0, 0, stats[:curserPositionStats])
1979
+ # stat_window.addch(0,curserPositionStats, stats[curserPositionStats], curses.A_REVERSE)
1980
+ # stat_window.addnstr(0, curserPositionStats + 1, stats[curserPositionStats + 1:], max_x - 1 - curserPositionStats)
1981
+ # stat_window.refresh()
1982
+ _curses_add_string_to_window(window=stat_window, y=0, line=stats, color_pair_list=[-1, -1, 1],centered=True,fill_char='━',lead_str='┍',box_ansi_color=box_ansi_color)
1892
1983
  if bottom_border:
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, "─")
1984
+ #target_length = max_x - 2 + len('\x1b[33m\x1b[0m\x1b[31m\x1b[0m\x1b[32m\x1b[0m')
1985
+ #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, "─")
1986
+ 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']} "
1895
1987
  if bottom_stats != old_bottom_stat:
1896
1988
  old_bottom_stat = bottom_stats
1897
1989
  #bottom_border.clear()
1898
1990
  #bottom_border.addnstr(0, 0, bottom_stats, max_x - 1)
1899
- _curses_add_string_to_window(window=bottom_border, y=0, line=bottom_stats)
1991
+ _curses_add_string_to_window(window=bottom_border, y=0, line=bottom_stats,fill_char='─',centered=True,lead_str='└',box_ansi_color=box_ansi_color)
1900
1992
  bottom_border.refresh()
1901
- if stats != old_stat or curserPosition != old_cursor_position:
1902
- old_stat = stats
1903
- old_cursor_position = curserPosition
1904
- # calculate the real curser position in stats as we centered the stats
1905
- if 'Send CMD: ' in stats:
1906
- curserPositionStats = min(min(curserPosition,len(encodedLine) -1) + stats.find('Send CMD: ')+len('Send CMD: '), max_x -2)
1907
- else:
1908
- curserPositionStats = max_x -2
1909
- #stat_window.clear()
1910
- #stat_window.addstr(0, 0, stats)
1911
- # add the line with curser that inverses the color at the curser position
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)
1915
- stat_window.refresh()
1916
1993
  # set the maximum refresh rate to 100 Hz
1917
1994
  if time.perf_counter() - last_refresh_time < 0.01:
1918
1995
  time.sleep(max(0,0.01 - time.perf_counter() + last_refresh_time))
@@ -1924,20 +2001,27 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
1924
2001
  try:
1925
2002
  #host_window.clear()
1926
2003
  # we will try to center the name of the host with ┼ at the beginning and end and ─ in between
1927
- linePrintOut = f'┼{(host.name+":["+host.command+"]")[:host_window_width - 2].center(host_window_width - 1, "─")}'.replace('\n', ' ').replace('\r', ' ').strip()
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)
2004
+ #linePrintOut = f'┼{(host.name+":["+host.command+"]")[:host_window_width - 2].center(host_window_width - 1, "─")}'.replace('\n', ' ').replace('\r', ' ').strip()
2005
+ #linePrintOut = f'┼{(host.name+":["+host.command+"]")[:host_window_width - 2].center(host_window_width - 1, "─")}'.replace('\n', ' ').replace('\r', ' ').strip()
2006
+ #host_window.addnstr(0, 0, linePrintOut, host_window_width - 1)
2007
+ linePrintOut = f'{host.name}:[{host.command}]'.replace('\n', ' ').replace('\r', ' ').strip()
2008
+ _curses_add_string_to_window(window=host_window, y=0, line=linePrintOut, color_pair_list=[-1, -1, 1],centered=True,fill_char='─',lead_str='┼',box_ansi_color=box_ansi_color)
2009
+ #_add_line_with_ansi_colors(window=host_window, y=0, x=0, line=linePrintOut, n=host_window_width - 1, color_pair_list = host.current_color_pair)
1930
2010
  # we will display the latest outputs of the host as much as we can
1931
- for i, line in enumerate(host.output[-(host_window_height - 1):]):
2011
+ #for i, line in enumerate(host.output[-(host_window_height - 1):]):
1932
2012
  # print(f"Printng a line at {i + 1} with length of {len('│'+line[:host_window_width - 1])}")
1933
2013
  # time.sleep(10)
1934
- linePrintOut = ('│'+line[:host_window_width - 2].replace('\n', ' ').replace('\r', ' ')).strip()
2014
+ #linePrintOut = ('│'+line[:host_window_width - 2].replace('\n', ' ').replace('\r', ' ')).strip()
1935
2015
  #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)
2016
+ #_curses_add_string_to_window(window=host_window, y=i + 1, line=line, color_pair_list=host.current_color_pair,lead_str='│')
1937
2017
  # we draw the rest of the available lines
2018
+ # for i in range(len(host.output), host_window_height - 1):
2019
+ # # print(f"Printng a line at {i + 1} with length of {len('│')}")
2020
+ # host_window.addnstr(i + 1, 0, '│'.ljust(host_window_width - 1, ' '), host_window_width - 1)
2021
+ for i in range(host.printedLines, len(host.output)):
2022
+ _curses_add_string_to_window(window=host_window, y=i + 1, line=host.output[i], color_pair_list=host.current_color_pair,lead_str='│',keep_top_n_lines=1,box_ansi_color=box_ansi_color)
1938
2023
  for i in range(len(host.output), host_window_height - 1):
1939
- # print(f"Printng a line at {i + 1} with length of {len('│')}")
1940
- host_window.addnstr(i + 1, 0, '│'.ljust(host_window_width - 1, ' '), host_window_width - 1)
2024
+ _curses_add_string_to_window(window=host_window, y=i + 1,lead_str='│',keep_top_n_lines=1,box_ansi_color=box_ansi_color)
1941
2025
  host.printedLines = len(host.output)
1942
2026
  host_window.refresh()
1943
2027
  except Exception as e:
@@ -1946,6 +2030,24 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
1946
2030
  # print(traceback.format_exc().strip())
1947
2031
  if org_dim != stdscr.getmaxyx():
1948
2032
  return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Terminal resize detected')
2033
+ if host.lineNumToReprintSet:
2034
+ # visible range is from host.printedLines - host_window_height + 1 to host.printedLines
2035
+ visibleLowerBound = host.printedLines - host_window_height + 1
2036
+ for lineNumToReprint in host.lineNumToReprintSet:
2037
+ # if the line is visible, we will reprint it
2038
+ if visibleLowerBound <= lineNumToReprint <= host.printedLines:
2039
+ if visibleLowerBound <= 0:
2040
+ # this means all lines are visible
2041
+ linePos = lineNumToReprint
2042
+ else:
2043
+ # calculate the position of the line to reprint
2044
+ linePos = lineNumToReprint - visibleLowerBound
2045
+ # Note: color can be incorrect if repainting an old line with new colors already initialized,
2046
+ # Thus we will not use any presistent color pair for old lines
2047
+ cpl = host.current_color_pair if lineNumToReprint == host.printedLines else [-1,-1,1]
2048
+ _curses_add_string_to_window(window=host_window, y=linePos + 1, line=host.output[lineNumToReprint], color_pair_list=cpl,lead_str='│',keep_top_n_lines=1,box_ansi_color=box_ansi_color)
2049
+ host.lineNumToReprintSet = set()
2050
+ host_window.refresh()
1949
2051
  new_configured = False
1950
2052
  last_refresh_time = time.perf_counter()
1951
2053
  except Exception as e:
@@ -2,7 +2,7 @@ from setuptools import setup
2
2
 
3
3
  setup(
4
4
  name='multiSSH3',
5
- version='5.30',
5
+ version='5.33',
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