multiCMD 1.34__tar.gz → 1.35__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multiCMD
3
- Version: 1.34
3
+ Version: 1.35
4
4
  Summary: Run commands simultaneously
5
5
  Home-page: https://github.com/yufei-pan/multiCMD
6
6
  Author: Yufei Pan
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multiCMD
3
- Version: 1.34
3
+ Version: 1.35
4
4
  Summary: Run commands simultaneously
5
5
  Home-page: https://github.com/yufei-pan/multiCMD
6
6
  Author: Yufei Pan
@@ -20,9 +20,9 @@ import itertools
20
20
  import signal
21
21
 
22
22
  #%% global vars
23
- version = '1.34'
23
+ version = '1.35'
24
24
  __version__ = version
25
- COMMIT_DATE = '2025-08-25'
25
+ COMMIT_DATE = '2025-09-10'
26
26
  __running_threads = set()
27
27
  __variables = {}
28
28
 
@@ -679,73 +679,119 @@ def input_with_timeout_and_countdown(timeout, prompt='Please enter your selectio
679
679
  # If there is no input, return None
680
680
  return None
681
681
 
682
- def pretty_format_table(data, delimiter = '\t',header = None):
683
- version = 1.11
682
+ def pretty_format_table(data, delimiter="\t", header=None, full=False):
683
+ import re
684
+ version = 1.12
684
685
  _ = version
686
+ def visible_len(s):
687
+ return len(re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", s))
688
+ def table_width(col_widths, sep_len):
689
+ # total width = sum of column widths + separators between columns
690
+ return sum(col_widths) + sep_len * (len(col_widths) - 1)
691
+ def truncate_to_width(s, width):
692
+ # If fits, leave as is. If too long and width >= 1, keep width-1 chars + "."
693
+ # If width == 0, nothing fits; return empty string.
694
+ if visible_len(s) <= width:
695
+ return s
696
+ if width <= 0:
697
+ return ""
698
+ # Build a truncated plain string based on visible chars (no ANSI awareness for slicing)
699
+ # For simplicity, slice the raw string. This may cut ANSI; best to avoid ANSI in data if truncation occurs.
700
+ return s[: max(width - 2, 0)] + ".."
685
701
  if not data:
686
- return ''
702
+ return ""
703
+ # Normalize input data structure
687
704
  if isinstance(data, str):
688
- data = data.strip('\n').split('\n')
705
+ data = data.strip("\n").split("\n")
689
706
  data = [line.split(delimiter) for line in data]
690
707
  elif isinstance(data, dict):
691
- # flatten the 2D dict to a list of lists
692
708
  if isinstance(next(iter(data.values())), dict):
693
- tempData = [['key'] + list(next(iter(data.values())).keys())]
694
- tempData.extend( [[key] + list(value.values()) for key, value in data.items()])
709
+ tempData = [["key"] + list(next(iter(data.values())).keys())]
710
+ tempData.extend([[key] + list(value.values()) for key, value in data.items()])
695
711
  data = tempData
696
712
  else:
697
- # it is a dict of lists
698
713
  data = [[key] + list(value) for key, value in data.items()]
699
714
  elif not isinstance(data, list):
700
715
  data = list(data)
701
- # format the list into 2d list of list of strings
702
716
  if isinstance(data[0], dict):
703
- tempData = [data[0].keys()]
717
+ tempData = [list(data[0].keys())]
704
718
  tempData.extend([list(item.values()) for item in data])
705
719
  data = tempData
706
720
  data = [[str(item) for item in row] for row in data]
707
721
  num_cols = len(data[0])
708
- col_widths = [0] * num_cols
709
- # Calculate the maximum width of each column
710
- for c in range(num_cols):
711
- #col_widths[c] = max(len(row[c]) for row in data)
712
- # handle ansii escape sequences
713
- col_widths[c] = max(len(re.sub(r'\x1b\[[0-?]*[ -/]*[@-~]','',row[c])) for row in data)
714
- if header:
715
- header_widths = [len(re.sub(r'\x1b\[[0-?]*[ -/]*[@-~]', '', col)) for col in header]
716
- col_widths = [max(col_widths[i], header_widths[i]) for i in range(num_cols)]
717
- # Build the row format string
718
- row_format = ' | '.join('{{:<{}}}'.format(width) for width in col_widths)
719
- # Print the header
720
- if not header:
722
+ # Resolve header and rows
723
+ using_provided_header = header is not None
724
+ if not using_provided_header:
721
725
  header = data[0]
722
- outTable = []
723
- outTable.append(row_format.format(*header))
724
- outTable.append('-+-'.join('-' * width for width in col_widths))
725
- for row in data[1:]:
726
- # if the row is empty, print an divider
727
- if not any(row):
728
- outTable.append('-+-'.join('-' * width for width in col_widths))
729
- else:
730
- outTable.append(row_format.format(*row))
726
+ rows = data[1:]
731
727
  else:
732
- # pad / truncate header to appropriate length
733
- if isinstance(header,str):
728
+ if isinstance(header, str):
734
729
  header = header.split(delimiter)
730
+ # Pad/trim header to match num_cols
735
731
  if len(header) < num_cols:
736
- header += ['']*(num_cols-len(header))
732
+ header = header + [""] * (num_cols - len(header))
737
733
  elif len(header) > num_cols:
738
734
  header = header[:num_cols]
739
- outTable = []
740
- outTable.append(row_format.format(*header))
741
- outTable.append('-+-'.join('-' * width for width in col_widths))
742
- for row in data:
743
- # if the row is empty, print an divider
735
+ rows = data
736
+ # Compute initial column widths based on data and header
737
+ def compute_col_widths(hdr, rows_):
738
+ col_w = [0] * len(hdr)
739
+ for i in range(len(hdr)):
740
+ col_w[i] = max(visible_len(hdr[i]), *(visible_len(r[i]) for r in rows_ if i < len(r)))
741
+ return col_w
742
+ # Ensure all rows have the same number of columns
743
+ normalized_rows = []
744
+ for r in rows:
745
+ if len(r) < num_cols:
746
+ r = r + [""] * (num_cols - len(r))
747
+ elif len(r) > num_cols:
748
+ r = r[:num_cols]
749
+ normalized_rows.append(r)
750
+ rows = normalized_rows
751
+ col_widths = compute_col_widths(header, rows)
752
+ # If full=True, keep existing formatting
753
+ # Else try to fit within the terminal width by:
754
+ # 1) Switching to compressed separators if needed
755
+ # 2) Recursively compressing columns (truncating with ".")
756
+ sep = " | "
757
+ hsep = "-+-"
758
+ cols = get_terminal_size()[0]
759
+ def render(hdr, rows, col_w, sep_str, hsep_str):
760
+ row_fmt = sep_str.join("{{:<{}}}".format(w) for w in col_w)
761
+ out = []
762
+ out.append(row_fmt.format(*hdr))
763
+ out.append(hsep_str.join("-" * w for w in col_w))
764
+ for row in rows:
744
765
  if not any(row):
745
- outTable.append('-+-'.join('-' * width for width in col_widths))
766
+ out.append(hsep_str.join("-" * w for w in col_w))
746
767
  else:
747
- outTable.append(row_format.format(*row))
748
- return '\n'.join(outTable) + '\n'
768
+ row = [truncate_to_width(row[i], col_w[i]) for i in range(len(row))]
769
+ out.append(row_fmt.format(*row))
770
+ return "\n".join(out) + "\n"
771
+ if full:
772
+ return render(header, rows, col_widths, sep, hsep)
773
+ # Try default separators first
774
+ if table_width(col_widths, len(sep)) <= cols:
775
+ return render(header, rows, col_widths, sep, hsep)
776
+ # Use compressed separators (no spaces)
777
+ sep = "|"
778
+ hsep = "+"
779
+ if table_width(col_widths, len(sep)) <= cols:
780
+ return render(header, rows, col_widths, sep, hsep)
781
+ # Begin column compression
782
+ # Track which columns have been compressed already to header width
783
+ header_widths = [visible_len(h) for h in header]
784
+ width_diff = [max(col_widths[i] - header_widths[i],0) for i in range(num_cols)]
785
+ total_overflow_width = table_width(col_widths, len(sep)) - cols
786
+ for i, diff in sorted(enumerate(width_diff), key=lambda x: -x[1]):
787
+ if total_overflow_width <= 0:
788
+ break
789
+ if diff <= 0:
790
+ continue
791
+ reduce_by = min(diff, total_overflow_width)
792
+ col_widths[i] -= reduce_by
793
+ total_overflow_width -= reduce_by
794
+ return render(header, rows, col_widths, sep, hsep)
749
795
 
750
796
  def parseTable(data,sort=False):
751
797
  if isinstance(data, str):
@@ -903,3 +949,109 @@ def print_progress_bar(iteration, total, prefix='', suffix=''):
903
949
  except:
904
950
  if iteration % 5 == 0:
905
951
  print(_genrate_progress_bar(iteration, total, prefix, suffix))
952
+
953
+ def format_bytes(size, use_1024_bytes=None, to_int=False, to_str=False, str_format=".2f"):
954
+ if to_int or isinstance(size, str):
955
+ if isinstance(size, int):
956
+ return size
957
+ elif isinstance(size, str):
958
+ match = re.match(r"(\d+(\.\d+)?)\s*([a-zA-Z]*)", size)
959
+ if not match:
960
+ if to_str:
961
+ return size
962
+ print(
963
+ "Invalid size format. Expected format: 'number [unit]', "
964
+ "e.g., '1.5 GiB' or '1.5GiB'"
965
+ )
966
+ print(f"Got: {size}")
967
+ return 0
968
+ number, _, unit = match.groups()
969
+ number = float(number)
970
+ unit = unit.strip().lower().rstrip("b")
971
+ if unit.endswith("i"):
972
+ use_1024_bytes = True
973
+ elif use_1024_bytes is None:
974
+ use_1024_bytes = False
975
+ unit = unit.rstrip("i")
976
+ if use_1024_bytes:
977
+ power = 2**10
978
+ else:
979
+ power = 10**3
980
+ unit_labels = {
981
+ "": 0,
982
+ "k": 1,
983
+ "m": 2,
984
+ "g": 3,
985
+ "t": 4,
986
+ "p": 5,
987
+ "e": 6,
988
+ "z": 7,
989
+ "y": 8,
990
+ }
991
+ if unit not in unit_labels:
992
+ if to_str:
993
+ return size
994
+ else:
995
+ if to_str:
996
+ return format_bytes(
997
+ size=int(number * (power**unit_labels[unit])),
998
+ use_1024_bytes=use_1024_bytes,
999
+ to_str=True,
1000
+ str_format=str_format,
1001
+ )
1002
+ return int(number * (power**unit_labels[unit]))
1003
+ else:
1004
+ try:
1005
+ return int(size)
1006
+ except Exception:
1007
+ return 0
1008
+ elif to_str or isinstance(size, int) or isinstance(size, float):
1009
+ if isinstance(size, str):
1010
+ try:
1011
+ size = size.rstrip("B").rstrip("b")
1012
+ size = float(size.lower().strip())
1013
+ except Exception:
1014
+ return size
1015
+ if use_1024_bytes or use_1024_bytes is None:
1016
+ power = 2**10
1017
+ n = 0
1018
+ power_labels = {
1019
+ 0: "",
1020
+ 1: "Ki",
1021
+ 2: "Mi",
1022
+ 3: "Gi",
1023
+ 4: "Ti",
1024
+ 5: "Pi",
1025
+ 6: "Ei",
1026
+ 7: "Zi",
1027
+ 8: "Yi",
1028
+ }
1029
+ while size > power:
1030
+ size /= power
1031
+ n += 1
1032
+ return f"{size:{str_format}} {' '}{power_labels[n]}".replace(" ", " ")
1033
+ else:
1034
+ power = 10**3
1035
+ n = 0
1036
+ power_labels = {
1037
+ 0: "",
1038
+ 1: "K",
1039
+ 2: "M",
1040
+ 3: "G",
1041
+ 4: "T",
1042
+ 5: "P",
1043
+ 6: "E",
1044
+ 7: "Z",
1045
+ 8: "Y",
1046
+ }
1047
+ while size > power:
1048
+ size /= power
1049
+ n += 1
1050
+ return f"{size:{str_format}} {' '}{power_labels[n]}".replace(" ", " ")
1051
+ else:
1052
+ try:
1053
+ return format_bytes(float(size), use_1024_bytes)
1054
+ except Exception:
1055
+ pass
1056
+ return 0
1057
+
File without changes
File without changes
File without changes