multiSSH3 5.72__py3-none-any.whl → 5.74__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
@@ -10,6 +10,7 @@ __curses_available = False
10
10
  __resource_lib_available = False
11
11
  try:
12
12
  import curses
13
+ import curses.panel
13
14
  __curses_available = True
14
15
  except ImportError:
15
16
  pass
@@ -54,10 +55,10 @@ except AttributeError:
54
55
  # If neither is available, use a dummy decorator
55
56
  def cache_decorator(func):
56
57
  return func
57
- version = '5.72'
58
+ version = '5.74'
58
59
  VERSION = version
59
60
  __version__ = version
60
- COMMIT_DATE = '2025-05-21'
61
+ COMMIT_DATE = '2025-06-03'
61
62
 
62
63
  CONFIG_FILE_CHAIN = ['./multiSSH3.config.json',
63
64
  '~/multiSSH3.config.json',
@@ -1824,14 +1825,14 @@ def _curses_add_string_to_window(window, line = '', y = 0, x = 0, number_of_char
1824
1825
  charsWritten += min(len(segment), numChar - charsWritten)
1825
1826
  # if we have finished printing segments but we still have space, we will fill it with fill_char
1826
1827
  trail_fill_length = numChar - charsWritten - len(trail_str)
1827
- if trail_fill_length > 0:
1828
- window.addnstr(y, x + charsWritten,fill_char * (trail_fill_length // len(fill_char) + 1), trail_fill_length, boxAttr)
1828
+ if trail_fill_length > 0 and fill_char:
1829
+ window.addnstr(y, x + charsWritten,fill_char * (trail_fill_length // len(fill_char) + 1), trail_fill_length , boxAttr)
1829
1830
  charsWritten += trail_fill_length
1830
1831
  if len(trail_str) > 0 and charsWritten < numChar:
1831
1832
  window.addnstr(y, x + charsWritten, trail_str, numChar - charsWritten, boxAttr)
1832
1833
  charsWritten += min(len(trail_str), numChar - charsWritten)
1833
1834
 
1834
- def _get_hosts_to_display (hosts, max_num_hosts, hosts_to_display = None):
1835
+ def _get_hosts_to_display (hosts, max_num_hosts, hosts_to_display = None, indexOffset = 0):
1835
1836
  '''
1836
1837
  Generate a list for the hosts to be displayed on the screen. This is used to display as much relevant information as possible.
1837
1838
 
@@ -1852,7 +1853,9 @@ def _get_hosts_to_display (hosts, max_num_hosts, hosts_to_display = None):
1852
1853
  failed_hosts = [host for host in hosts if host.returncode is not None and host.returncode != 0]
1853
1854
  finished_hosts = [host for host in hosts if host.returncode is not None and host.returncode == 0]
1854
1855
  waiting_hosts = [host for host in hosts if host.returncode is None and not host.output]
1855
- new_hosts_to_display = (running_hosts + failed_hosts + finished_hosts + waiting_hosts)[:max_num_hosts]
1856
+ new_hosts_to_display = (running_hosts + failed_hosts + finished_hosts + waiting_hosts)
1857
+ new_hosts_to_display = new_hosts_to_display[indexOffset:] + new_hosts_to_display[:indexOffset]
1858
+ new_hosts_to_display = new_hosts_to_display[:max_num_hosts]
1856
1859
  if not hosts_to_display:
1857
1860
  return new_hosts_to_display , {'running':len(running_hosts), 'failed':len(failed_hosts), 'finished':len(finished_hosts), 'waiting':len(waiting_hosts)}, set(new_hosts_to_display)
1858
1861
  # we will compare the new_hosts_to_display with the old one, if some hosts are not in their original position, we will reprint all lines
@@ -1866,6 +1869,7 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
1866
1869
  _ = config_reason
1867
1870
  try:
1868
1871
  box_ansi_color = None
1872
+ refresh_all = True
1869
1873
  org_dim = stdscr.getmaxyx()
1870
1874
  # To do this, first we need to know the size of the terminal
1871
1875
  max_y, max_x = org_dim
@@ -1949,6 +1953,34 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
1949
1953
  #bottom_border.addnstr(0, 0, '-' * (max_x - 1), max_x - 1)
1950
1954
  _curses_add_string_to_window(window=bottom_border, y=0, line='-' * (max_x - 1),fill_char='-',box_ansi_color=box_ansi_color)
1951
1955
  bottom_border.refresh()
1956
+ help_window_hight = min(14, max_y)
1957
+ help_window_width = min(31, max_x)
1958
+ # Create a centered help window
1959
+ help_window_y = (max_y - help_window_hight) // 2
1960
+ help_window_x = (max_x - help_window_width) // 2
1961
+ help_window = curses.newwin(help_window_hight, help_window_width, help_window_y, help_window_x)
1962
+ help_window.leaveok(True)
1963
+ help_window.scrollok(True)
1964
+ help_window.idlok(True)
1965
+ help_window.box()
1966
+ _curses_add_string_to_window(window=help_window,y=0,line='Help', color_pair_list=[-1,-1,1], centered=True, fill_char='─', lead_str='┌', box_ansi_color=box_ansi_color)
1967
+ _curses_add_string_to_window(window=help_window,y=1,line='? : Toggle Help Menu', color_pair_list=[-1,-1,1], lead_str='│', box_ansi_color=box_ansi_color)
1968
+ _curses_add_string_to_window(window=help_window,y=2,line='_ or + : Change window hight', color_pair_list=[-1,-1,1], lead_str='│', box_ansi_color=box_ansi_color)
1969
+ _curses_add_string_to_window(window=help_window,y=3,line='{ or } : Change window width', color_pair_list=[-1,-1,1], lead_str='│', box_ansi_color=box_ansi_color)
1970
+ _curses_add_string_to_window(window=help_window,y=4,line='< or > : Change host index', color_pair_list=[-1,-1,1], lead_str='│', box_ansi_color=box_ansi_color)
1971
+ _curses_add_string_to_window(window=help_window,y=5,line='|(pipe) : Toggle single host', color_pair_list=[-1,-1,1], lead_str='│', box_ansi_color=box_ansi_color)
1972
+ _curses_add_string_to_window(window=help_window,y=6,line='Ctrl+D : Exit', color_pair_list=[-1,-1,1], lead_str='│', box_ansi_color=box_ansi_color)
1973
+ _curses_add_string_to_window(window=help_window,y=7,line='Ctrl+R : Force refresh', color_pair_list=[-1,-1,1], lead_str='│', box_ansi_color=box_ansi_color)
1974
+ _curses_add_string_to_window(window=help_window,y=8,line='↑ or ↓ : Navigate history', color_pair_list=[-1,-1,1], lead_str='│', box_ansi_color=box_ansi_color)
1975
+ _curses_add_string_to_window(window=help_window,y=9,line='← or → : Move cursor', color_pair_list=[-1,-1,1], lead_str='│', box_ansi_color=box_ansi_color)
1976
+ _curses_add_string_to_window(window=help_window,y=10,line='PgUp/Dn : Scroll history by 5', color_pair_list=[-1,-1,1], lead_str='│', box_ansi_color=box_ansi_color)
1977
+ _curses_add_string_to_window(window=help_window,y=11,line='Home/End: Jump cursor', color_pair_list=[-1,-1,1], lead_str='│', box_ansi_color=box_ansi_color)
1978
+ _curses_add_string_to_window(window=help_window,y=12,line='Esc : Clear line', color_pair_list=[-1,-1,1], lead_str='│', box_ansi_color=box_ansi_color)
1979
+ help_panel = curses.panel.new_panel(help_window)
1980
+ help_panel.hide()
1981
+ help_shown = False
1982
+ curses.panel.update_panels()
1983
+ indexOffset = 0
1952
1984
  while host_stats['running'] > 0 or host_stats['waiting'] > 0:
1953
1985
  # Check for keypress
1954
1986
  key = stdscr.getch()
@@ -1958,7 +1990,7 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
1958
1990
  # When we encounter a newline, we add a new list to the list of lists. ( a new line of input )
1959
1991
  # with open('keylog.txt','a') as f:
1960
1992
  # f.write(str(key)+'\n')
1961
- if key == 410: # 410 is the key code for resize
1993
+ if key == 410 or key == curses.KEY_RESIZE: # 410 is the key code for resize
1962
1994
  return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Terminal resize requested')
1963
1995
  # if the user pressed ctrl + d and the last line is empty, we will exit by adding 'exit\n' to the last line
1964
1996
  elif key == 4 and not __keyPressesIn[-1]:
@@ -1981,10 +2013,17 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
1981
2013
  elif key == 125 and not __keyPressesIn[-1]: # 125 is the key code for }
1982
2014
  # if last line is empty, we will reconfigure the ww to be larger
1983
2015
  return (lineToDisplay,curserPosition , min_char_len +1, min_line_len, single_window, 'Increase character length')
2016
+ elif key == 60 and not __keyPressesIn[-1]: # 60 is the key code for <
2017
+ indexOffset = (indexOffset - 1 ) % len(hosts)
2018
+ elif key == 62 and not __keyPressesIn[-1]: # 62 is the key code for >
2019
+ indexOffset = (indexOffset +1 ) % len(hosts)
1984
2020
  # We handle positional keys
1985
2021
  # if the key is up arrow, we will move the line to display up
1986
2022
  elif key == 259: # 259 is the key code for up arrow
2023
+ # also scroll curserPosition to last if it is currently at the last line and curserPosition is at 0
1987
2024
  lineToDisplay = max(lineToDisplay - 1, -len(__keyPressesIn))
2025
+ if lineToDisplay == -2 and not __keyPressesIn[-1]:
2026
+ curserPosition = len(__keyPressesIn[lineToDisplay])
1988
2027
  # if the key is down arrow, we will move the line to display down
1989
2028
  elif key == 258: # 258 is the key code for down arrow
1990
2029
  lineToDisplay = min(lineToDisplay + 1, -1)
@@ -2006,6 +2045,23 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
2006
2045
  # if the key is end, we will move the cursor to the end of the line
2007
2046
  elif key == 360: # 360 is the key code for end
2008
2047
  curserPosition = len(__keyPressesIn[lineToDisplay])
2048
+ elif key == curses.KEY_REFRESH or key == curses.KEY_F5 or key == 18: # 18 is the key code for ctrl + R
2049
+ # if the key is refresh, we will refresh the screen
2050
+ return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Refresh requested')
2051
+ elif key == curses.KEY_EXIT or key == 27: # 27 is the key code for ESC
2052
+ # if the key is exit, we will exit the program
2053
+ return
2054
+ elif key == curses.KEY_HELP or key == 63 or key == curses.KEY_F1: # 63 is the key code for ?
2055
+ # if the key is help, we will display the help message
2056
+ if not help_shown:
2057
+ help_panel.show()
2058
+ help_shown = True
2059
+ else:
2060
+ help_panel.hide()
2061
+ help_shown = False
2062
+ refresh_all = True
2063
+ #return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Help closed')
2064
+ curses.panel.update_panels()
2009
2065
  # We are left with these are keys that mofidy the current line.
2010
2066
  else:
2011
2067
  # This means the user have done scrolling and is committing to modify the current line.
@@ -2042,7 +2098,7 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
2042
2098
  # We generate the aggregated stats if user did not input anything
2043
2099
  if not __keyPressesIn[lineToDisplay]:
2044
2100
  #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, "━")
2045
- 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} "
2101
+ 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} i:{indexOffset} "
2046
2102
  else:
2047
2103
  # we use the stat bar to display the key presses
2048
2104
  encodedLine = ''.join(__keyPressesIn[lineToDisplay]).encode().decode().strip('\n') + ' '
@@ -2051,7 +2107,7 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
2051
2107
  # displayCurserPosition is needed as the curserPosition can be larger than the length of the encodedLine. This is wanted to keep scrolling through the history less painful
2052
2108
  displayCurserPosition = min(curserPosition,len(encodedLine) -1)
2053
2109
  stats = f'Send CMD: {encodedLine[:displayCurserPosition]}\x1b[7m{encodedLine[displayCurserPosition]}\x1b[0m{encodedLine[displayCurserPosition + 1:]}'
2054
- if stats != old_stat :
2110
+ if stats != old_stat or refresh_all:
2055
2111
  old_stat = stats
2056
2112
  # calculate the real curser position in stats as we centered the stats
2057
2113
  # if 'Send CMD: ' in stats:
@@ -2071,7 +2127,7 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
2071
2127
  #target_length = max_x - 2 + len('\x1b[33m\x1b[0m\x1b[31m\x1b[0m\x1b[32m\x1b[0m')
2072
2128
  #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, "─")
2073
2129
  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']} "
2074
- if bottom_stats != old_bottom_stat:
2130
+ if bottom_stats != old_bottom_stat or refresh_all:
2075
2131
  old_bottom_stat = bottom_stats
2076
2132
  #bottom_border.clear()
2077
2133
  #bottom_border.addnstr(0, 0, bottom_stats, max_x - 1)
@@ -2080,6 +2136,9 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
2080
2136
  # set the maximum refresh rate to 100 Hz
2081
2137
  if time.perf_counter() - last_refresh_time < 0.01:
2082
2138
  time.sleep(max(0,0.01 - time.perf_counter() + last_refresh_time))
2139
+ if refresh_all:
2140
+ rearrangedHosts = set(hosts_to_display)
2141
+ refresh_all = False
2083
2142
  #stdscr.clear()
2084
2143
  for host_window, host in zip(host_windows, hosts_to_display):
2085
2144
  # we will only update the window if there is new output or the window is not fully printed
@@ -2102,15 +2161,16 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
2102
2161
  for lineNumToReprint in sorted(lineNumToPrintSet):
2103
2162
  # if the line is visible, we will reprint it
2104
2163
  if visibleLowerBound <= lineNumToReprint <= len(host.output):
2105
- _curses_add_string_to_window(window=host_window, y=lineNumToReprint + 1, line=host.output[lineNumToReprint], color_pair_list=host.current_color_pair,lead_str='│',keep_top_n_lines=1,box_ansi_color=box_ansi_color)
2164
+ _curses_add_string_to_window(window=host_window, y=lineNumToReprint + 1, line=host.output[lineNumToReprint], color_pair_list=host.current_color_pair,lead_str='│',keep_top_n_lines=1,box_ansi_color=box_ansi_color,fill_char='')
2106
2165
  except Exception as e:
2107
2166
  # import traceback
2108
2167
  # print(str(e).strip())
2109
2168
  # print(traceback.format_exc().strip())
2110
2169
  if org_dim != stdscr.getmaxyx():
2111
2170
  return (lineToDisplay,curserPosition , min_char_len, min_line_len, single_window, 'Terminal resize detected')
2112
- host_window.refresh()
2113
- hosts_to_display, host_stats,rearrangedHosts = _get_hosts_to_display(hosts, max_num_hosts,hosts_to_display)
2171
+ host_window.noutrefresh()
2172
+ hosts_to_display, host_stats,rearrangedHosts = _get_hosts_to_display(hosts, max_num_hosts,hosts_to_display, indexOffset)
2173
+ curses.doupdate()
2114
2174
  last_refresh_time = time.perf_counter()
2115
2175
  except Exception as e:
2116
2176
  import traceback
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multiSSH3
3
- Version: 5.72
3
+ Version: 5.74
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=h8FnoCjcZGOjyPypzJ7H4XLOhMIKU2I-kN-m6PGdjJY,149182
2
+ multissh3-5.74.dist-info/METADATA,sha256=ErFNVhzY6qUCJAiI2-paOpyfNiLJKJayjXCYiQRHJPg,18093
3
+ multissh3-5.74.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
4
+ multissh3-5.74.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
5
+ multissh3-5.74.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
6
+ multissh3-5.74.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.8.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,6 +0,0 @@
1
- multiSSH3.py,sha256=d4tiPSSa51_OrTZujc0XrzR9elZq10lyyRBLtRffWyk,144750
2
- multissh3-5.72.dist-info/METADATA,sha256=d4CQxszsQx7a7GWkQSYC2VGWdLQP13C3kp1RTAIHK5Y,18093
3
- multissh3-5.72.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
4
- multissh3-5.72.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
5
- multissh3-5.72.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
6
- multissh3-5.72.dist-info/RECORD,,