multiCMD 1.52__tar.gz → 1.53__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.
- {multicmd-1.52 → multicmd-1.53}/PKG-INFO +1 -1
- {multicmd-1.52 → multicmd-1.53}/multiCMD.egg-info/PKG-INFO +1 -1
- {multicmd-1.52 → multicmd-1.53}/multiCMD.py +50 -38
- {multicmd-1.52 → multicmd-1.53}/tests/test_helpers.py +19 -0
- {multicmd-1.52 → multicmd-1.53}/README.md +0 -0
- {multicmd-1.52 → multicmd-1.53}/multiCMD.egg-info/SOURCES.txt +0 -0
- {multicmd-1.52 → multicmd-1.53}/multiCMD.egg-info/dependency_links.txt +0 -0
- {multicmd-1.52 → multicmd-1.53}/multiCMD.egg-info/entry_points.txt +0 -0
- {multicmd-1.52 → multicmd-1.53}/multiCMD.egg-info/top_level.txt +0 -0
- {multicmd-1.52 → multicmd-1.53}/setup.cfg +0 -0
- {multicmd-1.52 → multicmd-1.53}/setup.py +0 -0
- {multicmd-1.52 → multicmd-1.53}/tests/test_api.py +0 -0
- {multicmd-1.52 → multicmd-1.53}/tests/test_async_executor.py +0 -0
- {multicmd-1.52 → multicmd-1.53}/tests/test_cli.py +0 -0
- {multicmd-1.52 → multicmd-1.53}/tests/test_cli_input.py +0 -0
- {multicmd-1.52 → multicmd-1.53}/tests/test_cli_summary.py +0 -0
- {multicmd-1.52 → multicmd-1.53}/tests/test_display.py +0 -0
- {multicmd-1.52 → multicmd-1.53}/tests/test_expand_ranges.py +0 -0
- {multicmd-1.52 → multicmd-1.53}/tests/test_parsing.py +0 -0
- {multicmd-1.52 → multicmd-1.53}/tests/test_ping.py +0 -0
- {multicmd-1.52 → multicmd-1.53}/tests/test_pool.py +0 -0
- {multicmd-1.52 → multicmd-1.53}/tests/test_process_groups.py +0 -0
- {multicmd-1.52 → multicmd-1.53}/tests/test_pty.py +0 -0
- {multicmd-1.52 → multicmd-1.53}/tests/test_reader.py +0 -0
- {multicmd-1.52 → multicmd-1.53}/tests/test_regressions.py +0 -0
- {multicmd-1.52 → multicmd-1.53}/tests/test_run_command.py +0 -0
- {multicmd-1.52 → multicmd-1.53}/tests/test_run_commands.py +0 -0
- {multicmd-1.52 → multicmd-1.53}/tests/test_sudo.py +0 -0
- {multicmd-1.52 → multicmd-1.53}/tests/test_supervision.py +0 -0
- {multicmd-1.52 → multicmd-1.53}/tests/test_task.py +0 -0
- {multicmd-1.52 → multicmd-1.53}/tests/test_windows.py +0 -0
|
@@ -25,9 +25,9 @@ import time
|
|
|
25
25
|
from pprint import pformat
|
|
26
26
|
|
|
27
27
|
#%% global vars
|
|
28
|
-
version = '1.
|
|
28
|
+
version = '1.53'
|
|
29
29
|
__version__ = version
|
|
30
|
-
COMMIT_DATE = '2026-10-
|
|
30
|
+
COMMIT_DATE = '2026-10-11'
|
|
31
31
|
# fire-and-forget threads (run_commands with wait_for_return=False) that join_threads() waits on
|
|
32
32
|
__running_threads = set()
|
|
33
33
|
__running_threads_lock = threading.Lock()
|
|
@@ -1954,7 +1954,8 @@ def input_with_timeout_and_countdown(timeout, prompt='Please enter your selectio
|
|
|
1954
1954
|
|
|
1955
1955
|
def pretty_format_table(data, delimiter=None, header=None, full=False, remove_empty_columns=False):
|
|
1956
1956
|
import re
|
|
1957
|
-
|
|
1957
|
+
import unicodedata
|
|
1958
|
+
version = 1.21
|
|
1958
1959
|
_ = version
|
|
1959
1960
|
if not data:
|
|
1960
1961
|
return ""
|
|
@@ -1984,19 +1985,44 @@ def pretty_format_table(data, delimiter=None, header=None, full=False, remove_em
|
|
|
1984
1985
|
header = data[0]
|
|
1985
1986
|
data = data[1:]
|
|
1986
1987
|
num_cols = len(header)
|
|
1988
|
+
# Widths count terminal columns: an East Asian wide character takes two,
|
|
1989
|
+
# a combining mark and an ANSI color code none.
|
|
1990
|
+
ansi = re.compile(r"(\x1b\[[0-?]*[ -/]*[@-~])")
|
|
1991
|
+
def char_width(ch):
|
|
1992
|
+
if ch < "\u0300": # ASCII and Latin-1
|
|
1993
|
+
return 1
|
|
1994
|
+
if unicodedata.combining(ch) or unicodedata.category(ch) in ("Mn", "Me", "Cf") or "\u1160" <= ch <= "\u11ff":
|
|
1995
|
+
return 0
|
|
1996
|
+
return 2 if unicodedata.east_asian_width(ch) in ("W", "F") else 1
|
|
1987
1997
|
def visible_len(s):
|
|
1988
|
-
|
|
1989
|
-
|
|
1998
|
+
if "\x1b" in s:
|
|
1999
|
+
s = ansi.sub("", s)
|
|
2000
|
+
if not s or max(s) < "\u0300":
|
|
2001
|
+
return len(s)
|
|
2002
|
+
return sum(char_width(ch) for ch in s)
|
|
2003
|
+
def pad(s, width):
|
|
2004
|
+
return s + " " * (width - visible_len(s))
|
|
2005
|
+
def cut(s, width):
|
|
2006
|
+
# The characters that fit in ``width`` columns, and every color code.
|
|
2007
|
+
used, out = 0, []
|
|
2008
|
+
for i, piece in enumerate(ansi.split(s)):
|
|
2009
|
+
if i % 2:
|
|
2010
|
+
out.append(piece)
|
|
2011
|
+
continue
|
|
2012
|
+
for ch in piece:
|
|
2013
|
+
w = char_width(ch)
|
|
2014
|
+
if used + w > width:
|
|
2015
|
+
width = -1 # full: no further characters
|
|
2016
|
+
break
|
|
2017
|
+
out.append(ch)
|
|
2018
|
+
used += w
|
|
2019
|
+
return "".join(out)
|
|
1990
2020
|
col_widths = [0] * num_cols
|
|
1991
|
-
data_invisible_length = []
|
|
1992
2021
|
for row in data:
|
|
1993
|
-
|
|
1994
|
-
for item, col_width, cwi in zip(row, col_widths, range(num_cols)):
|
|
2022
|
+
for cwi, item in enumerate(row[:num_cols]):
|
|
1995
2023
|
vl = visible_len(item)
|
|
1996
|
-
if vl >
|
|
2024
|
+
if vl > col_widths[cwi]:
|
|
1997
2025
|
col_widths[cwi] = vl
|
|
1998
|
-
dil.append(len(item) - vl)
|
|
1999
|
-
data_invisible_length.append(dil)
|
|
2000
2026
|
cols_to_remove = []
|
|
2001
2027
|
if remove_empty_columns and not full:
|
|
2002
2028
|
cols_to_remove = [i for i, col_width in enumerate(col_widths) if col_width == 0]
|
|
@@ -2005,24 +2031,17 @@ def pretty_format_table(data, delimiter=None, header=None, full=False, remove_em
|
|
|
2005
2031
|
num_cols = len(header)
|
|
2006
2032
|
header_widths = [visible_len(h) for h in header]
|
|
2007
2033
|
col_widths = [max(w, hw) for w, hw in zip(col_widths, header_widths)]
|
|
2008
|
-
header = [
|
|
2034
|
+
header = [pad(item, col_width) for item, col_width in zip(header, col_widths)]
|
|
2009
2035
|
normalized_padded_data = []
|
|
2010
|
-
|
|
2011
|
-
for row, invisible_lengths in zip(data, data_invisible_length):
|
|
2036
|
+
for row in data:
|
|
2012
2037
|
if cols_to_remove:
|
|
2013
2038
|
row = [item for i, item in enumerate(row) if i not in cols_to_remove]
|
|
2014
|
-
invisible_lengths = [il for i, il in enumerate(invisible_lengths) if i not in cols_to_remove]
|
|
2015
2039
|
if not any(row):
|
|
2016
2040
|
row = ['-' * col_width for col_width in col_widths]
|
|
2017
|
-
|
|
2018
|
-
row = [row[i]
|
|
2019
|
-
elif len(row) >= num_cols:
|
|
2020
|
-
#row = row[:num_cols]
|
|
2021
|
-
row = [item.ljust(col_width + invisible_length) for item, col_width, invisible_length in zip(row, col_widths, invisible_lengths)]
|
|
2041
|
+
else:
|
|
2042
|
+
row = [pad(row[i] if i < len(row) else "", col_widths[i]) for i in range(num_cols)]
|
|
2022
2043
|
normalized_padded_data.append(row)
|
|
2023
|
-
new_data_invisible_length.append(invisible_lengths)
|
|
2024
2044
|
data = normalized_padded_data
|
|
2025
|
-
data_invisible_length = new_data_invisible_length
|
|
2026
2045
|
column_separator = " | "
|
|
2027
2046
|
horizontal_separator = "-+-"
|
|
2028
2047
|
terminal_width = get_terminal_size()[0]
|
|
@@ -2053,24 +2072,17 @@ def pretty_format_table(data, delimiter=None, header=None, full=False, remove_em
|
|
|
2053
2072
|
reduce_by = min(diff, total_overflow_width)
|
|
2054
2073
|
col_widths[i] -= reduce_by
|
|
2055
2074
|
total_overflow_width -= reduce_by
|
|
2056
|
-
def truncate_to_width(string, width
|
|
2057
|
-
|
|
2058
|
-
if s_len <= width:
|
|
2075
|
+
def truncate_to_width(string, width):
|
|
2076
|
+
if visible_len(string) <= width:
|
|
2059
2077
|
return string
|
|
2060
|
-
|
|
2061
|
-
if
|
|
2062
|
-
return
|
|
2078
|
+
stripped = string.rstrip()
|
|
2079
|
+
if visible_len(stripped) <= width:
|
|
2080
|
+
return pad(stripped, width)
|
|
2063
2081
|
if width < 2:
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
return string[: width + invisible_length - 2] + ".."
|
|
2069
|
-
data = [
|
|
2070
|
-
[truncate_to_width(item, col_width, invisible_length) for item, col_width, invisible_length in zip(row, col_widths, dil)]
|
|
2071
|
-
for row, dil in zip(data, data_invisible_length)
|
|
2072
|
-
]
|
|
2073
|
-
header = [item[:col_width] for item, col_width in zip(header, col_widths)]
|
|
2082
|
+
return "."[:width]
|
|
2083
|
+
return pad(cut(string, width - 2) + "..", width)
|
|
2084
|
+
data = [[truncate_to_width(item, col_width) for item, col_width in zip(row, col_widths)] for row in data]
|
|
2085
|
+
header = [truncate_to_width(item, col_width) for item, col_width in zip(header, col_widths)]
|
|
2074
2086
|
return render(header, data, col_widths, column_separator, horizontal_separator)
|
|
2075
2087
|
|
|
2076
2088
|
def parseTable(data,sort=False,min_space=2):
|
|
@@ -2,6 +2,7 @@ import builtins
|
|
|
2
2
|
import select
|
|
3
3
|
import sys
|
|
4
4
|
|
|
5
|
+
import multiCMD
|
|
5
6
|
from multiCMD import (
|
|
6
7
|
_genrate_progress_bar,
|
|
7
8
|
format_bytes,
|
|
@@ -22,6 +23,24 @@ def test_pretty_format_table_basic_and_remove_empty():
|
|
|
22
23
|
assert out == "h1\n--\na \nb \n"
|
|
23
24
|
|
|
24
25
|
|
|
26
|
+
def test_pretty_format_table_pads_wide_characters_by_display_width():
|
|
27
|
+
# East Asian wide characters take two terminal columns, a combining mark none.
|
|
28
|
+
out = pretty_format_table([["name", "v"], ["中文", "1"], ["abcd", "2"], ["e\u0301", "3"]], full=True)
|
|
29
|
+
assert out == "name | v\n-----+--\n中文 | 1\nabcd | 2\ne\u0301 | 3\n"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_pretty_format_table_truncates_wide_characters_to_the_terminal(monkeypatch):
|
|
33
|
+
monkeypatch.setattr(multiCMD, "get_terminal_size", lambda: (12, 24))
|
|
34
|
+
out = pretty_format_table([["k", "v"], ["a", "中文中文中文"]])
|
|
35
|
+
assert out == "k|v \n-+----------\na|中文中文..\n"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_pretty_format_table_narrows_a_wide_header_by_display_width(monkeypatch):
|
|
39
|
+
monkeypatch.setattr(multiCMD, "get_terminal_size", lambda: (10, 24))
|
|
40
|
+
out = pretty_format_table([["名前", "v"], ["aaaaaaaaaa", "1"]])
|
|
41
|
+
assert out == "名前 |v\n--------+-\naaaaaa..|1\n"
|
|
42
|
+
|
|
43
|
+
|
|
25
44
|
def test_parse_table():
|
|
26
45
|
text = "col1 col2\nfoo bar\n"
|
|
27
46
|
table = parseTable(text)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|