shinyshell 0.2.2__tar.gz → 0.3.0__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,12 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinyshell
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: Python library for beautiful terminal output — colored printing, tables, progress bars, syntax highlighting, bar charts, QR codes, and more. Zero dependencies.
5
5
  Home-page: https://github.com/adnanahamed66772ndpc/shinyshell
6
6
  Author: Adnan Ahamed Himal
7
7
  Author-email: hello@adnanahamedhimal.com
8
8
  Project-URL: Source, https://github.com/adnanahamed66772ndpc/shinyshell
9
9
  Project-URL: Bug Reports, https://github.com/adnanahamed66772ndpc/shinyshell/issues
10
+ Project-URL: Discussions, https://github.com/adnanahamed66772ndpc/shinyshell/discussions
10
11
  Project-URL: Documentation, https://github.com/adnanahamed66772ndpc/shinyshell#readme
11
12
  Keywords: terminal,cli,console,pretty,beautiful,output,print,colors,ansi,progress bar,table,spinner,syntax highlighting,diff,json,bar chart,qr code,ascii art,markdown,debug,trace,logging,bash,shell,colored,formatting,rich,colorama,termcolor,python library,python package,python cli,python terminal,developer tools,devtools,zero dependency
12
13
  Classifier: Development Status :: 4 - Beta
@@ -6,7 +6,7 @@ with open("README.md", "r") as f:
6
6
 
7
7
  setup(
8
8
  name="shinyshell",
9
- version="0.2.2",
9
+ version="0.3.0",
10
10
  author="Adnan Ahamed Himal",
11
11
  author_email="hello@adnanahamedhimal.com",
12
12
  description="Python library for beautiful terminal output — colored printing, tables, progress bars, syntax highlighting, bar charts, QR codes, and more. Zero dependencies.",
@@ -16,6 +16,7 @@ setup(
16
16
  project_urls={
17
17
  "Source": "https://github.com/adnanahamed66772ndpc/shinyshell",
18
18
  "Bug Reports": "https://github.com/adnanahamed66772ndpc/shinyshell/issues",
19
+ "Discussions": "https://github.com/adnanahamed66772ndpc/shinyshell/discussions",
19
20
  "Documentation": "https://github.com/adnanahamed66772ndpc/shinyshell#readme",
20
21
  },
21
22
  packages=find_packages(),
@@ -12,7 +12,7 @@ One import. All the pretty you need.
12
12
  Pure Python stdlib. Works on Linux, macOS, Windows.
13
13
  """
14
14
 
15
- __version__ = "0.2.2"
15
+ __version__ = "0.3.0"
16
16
  __all__ = ["Shell"]
17
17
 
18
18
  import os
@@ -946,7 +946,442 @@ class Shell:
946
946
  sys.stdout.write(f"\r {self._style(_ICONS['check'], color='green')} {lines[0]} {' ' * 20}\n")
947
947
  sys.stdout.flush()
948
948
 
949
+ # ── v0.3.0 NEW Features ──────────────────────────────────────
950
+
951
+ # 21. Sparklines
952
+ def sparkline(self, values: List[Union[int, float]], title: Optional[str] = None,
953
+ width: int = 40) -> None:
954
+ """Tufte-style inline sparkline chart. sh.sparkline([1,5,2,8,3,9,4]) → ▁▃▁▆▂█▃"""
955
+ if not values:
956
+ return
957
+ chars = " ▁▂▃▄▅▆▇█"
958
+ mn, mx = min(values), max(values)
959
+ rng = max(mx - mn, 1)
960
+ scaled = [int((v - mn) / rng * (len(chars) - 1)) for v in values]
961
+ if len(scaled) > width:
962
+ step = len(scaled) / width
963
+ scaled = [scaled[int(i * step)] for i in range(width)]
964
+ spark = "".join(chars[min(i, len(chars) - 1)] for i in scaled)
965
+ print()
966
+ if title:
967
+ print(f" {self._style(title, 'bold')}")
968
+ print(f" {self._style(spark, color='cyan')} {self._style(f'{mn}—{mx}', color='bright_black')}")
969
+ print()
970
+
971
+ # 22. Styled Input
972
+ def input(self, prompt: str = "", default: str = "", validate: Optional[Callable[[str], bool]] = None) -> str:
973
+ """Styled text input with optional validation. sh.input('Name:', default='World')"""
974
+ p = f" {self._style(_ICONS['question'], color='cyan')} {prompt} "
975
+ if default:
976
+ p += self._style(f"[{default}]", color="bright_black") + " "
977
+ try:
978
+ val = input(p).strip()
979
+ except (KeyboardInterrupt, EOFError):
980
+ print()
981
+ return default
982
+ if not val:
983
+ return default
984
+ if validate and not validate(val):
985
+ self.warning(f"Invalid input: {val}")
986
+ return self.input(prompt, default, validate)
987
+ return val
988
+
989
+ # 23. Password Input
990
+ def password(self, prompt: str = "Password:", min_len: int = 4) -> str:
991
+ """Masked password input with strength meter. sh.password('Enter API key:')"""
992
+ import getpass
993
+ print()
994
+ try:
995
+ pwd = getpass.getpass(f" {self._style(_ICONS['lock'], color='yellow')} {prompt} ")
996
+ except (KeyboardInterrupt, EOFError):
997
+ print()
998
+ return ""
999
+ if not pwd:
1000
+ return ""
1001
+
1002
+ # Strength meter
1003
+ score = min(4, sum([
1004
+ len(pwd) >= 8,
1005
+ len(pwd) >= 12,
1006
+ any(c.isupper() for c in pwd),
1007
+ any(c.islower() for c in pwd),
1008
+ any(c.isdigit() for c in pwd),
1009
+ any(not c.isalnum() for c in pwd),
1010
+ ]))
1011
+ bar = ["▯" * 10, "▮▯▯▯▯▯▯▯▯▯", "▮▮▮▯▯▯▯▯▯▯", "▮▮▮▮▮▯▯▯▯▯", "▮▮▮▮▮▮▮▯▯▯", "▮▮▮▮▮▮▮▮▮▮"][min(score, 5)]
1012
+ labels = ["", "Weak", "Fair", "Good", "Strong", "Very Strong"]
1013
+ colors = ["", "red", "yellow", "cyan", "green", "green"]
1014
+ print(f" {self._style(bar, color=colors[min(score,5)])} {self._style(labels[min(score,5)], color=colors[min(score,5)])}")
1015
+ return pwd
1016
+
1017
+ # 24. Themes
1018
+ def theme(self, name: str = "default") -> None:
1019
+ """Apply a color theme: dracula, nord, solarized, monokai, default. sh.theme('dracula')"""
1020
+ themes = {
1021
+ "dracula": {"success": "green", "error": "red", "warning": "yellow", "info": "magenta", "header": "magenta", "accent": "magenta"},
1022
+ "nord": {"success": "green", "error": "red", "warning": "yellow", "info": "cyan", "header": "cyan", "accent": "cyan"},
1023
+ "solarized": {"success": "green", "error": "red", "warning": "yellow", "info": "cyan", "header": "yellow", "accent": "yellow"},
1024
+ "monokai": {"success": "green", "error": "red", "warning": "yellow", "info": "magenta", "header": "magenta", "accent": "magenta"},
1025
+ "ocean": {"success": "green", "error": "red", "warning": "yellow", "info": "blue", "header": "blue", "accent": "blue"},
1026
+ }
1027
+ self._theme = themes.get(name, themes.get("default", {}))
1028
+ if name != "default":
1029
+ self.success(f"Theme '{name}' applied!")
1030
+
1031
+ # 25. Log Viewer
1032
+ def log(self, level: str, message: str, timestamp: bool = True) -> None:
1033
+ """Pretty log line. sh.log('INFO', 'Server started', timestamp=True)"""
1034
+ import datetime
1035
+ colors = {"DEBUG": "bright_black", "INFO": "cyan", "WARN": "yellow", "ERROR": "red", "CRITICAL": "magenta"}
1036
+ color = colors.get(level.upper(), "white")
1037
+ ts = ""
1038
+ if timestamp:
1039
+ ts = self._style(datetime.datetime.now().strftime("%H:%M:%S"), color="bright_black") + " "
1040
+ lvl = self._style(f"{level.upper():8s}", "bold", color=color)
1041
+ print(f" {ts}{lvl} {message}")
1042
+
1043
+ # 26. Calendar
1044
+ def calendar(self, year: Optional[int] = None, month: Optional[int] = None) -> None:
1045
+ """Display a monthly calendar. sh.calendar(2026, 7)"""
1046
+ import calendar as cal_mod, datetime
1047
+ now = datetime.datetime.now()
1048
+ y = year or now.year
1049
+ m = month or now.month
1050
+ cal = cal_mod.TextCalendar()
1051
+ print()
1052
+ header = self._style(f" {cal_mod.month_name[m]} {y}", "bold", color="cyan")
1053
+ print(header)
1054
+ print(self._style(" Mo Tu We Th Fr Sa Su", color="bright_black"))
1055
+ for week in cal.monthdayscalendar(y, m):
1056
+ line = " "
1057
+ for d in week:
1058
+ if d == 0:
1059
+ line += " "
1060
+ elif d == now.day and m == now.month and y == now.year:
1061
+ line += self._style(f"{d:2d} ", "bold", color="green", bg="green")
1062
+ else:
1063
+ line += f"{d:2d} "
1064
+ print(f" {line}")
1065
+ print()
1066
+
1067
+ # 27. Network Utilities
1068
+ def network_ping(self, host: str, count: int = 4) -> None:
1069
+ """Simple ping with visual output. sh.network_ping('google.com')"""
1070
+ import subprocess, platform
1071
+ print()
1072
+ self.info(f"Pinging {host}...")
1073
+ cmd = ["ping", "-n" if platform.system() == "Windows" else "-c", str(count), host]
1074
+ try:
1075
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
1076
+ for line in result.stdout.split("\n"):
1077
+ if "time=" in line or "time<" in line or "ms" in line:
1078
+ print(f" {self._style('⚡', color='cyan')} {line.strip()}")
1079
+ if result.returncode == 0:
1080
+ self.success(f"{host} is reachable")
1081
+ else:
1082
+ self.error(f"{host} unreachable")
1083
+ except Exception as e:
1084
+ self.error(f"Ping failed: {e}")
1085
+
1086
+ def network_status(self, url: str) -> None:
1087
+ """Check HTTP status with visual output. sh.network_status('https://api.example.com')"""
1088
+ import urllib.request
1089
+ print()
1090
+ try:
1091
+ req = urllib.request.Request(url, method="HEAD")
1092
+ resp = urllib.request.urlopen(req, timeout=5)
1093
+ status = resp.status
1094
+ if status < 300:
1095
+ self.success(f"{url} → {status} OK ({resp.headers.get('Server', '?')})")
1096
+ elif status < 400:
1097
+ self.warning(f"{url} → {status} Redirect")
1098
+ else:
1099
+ self.error(f"{url} → {status} Error")
1100
+ except Exception as e:
1101
+ self.error(f"{url} → {str(e)[:60]}")
1102
+
1103
+ # 28. Gauge
1104
+ def gauge(self, value: float, max_val: float = 100, title: Optional[str] = None,
1105
+ width: int = 30, color: str = "green") -> None:
1106
+ """Circular-style gauge display. sh.gauge(75, 100, 'CPU Usage')"""
1107
+ pct = min(value / max(max_val, 1), 1.0)
1108
+ filled = int(pct * width)
1109
+ bar = "█" * filled + "░" * (width - filled)
1110
+ if pct > 0.9:
1111
+ color = "red"
1112
+ elif pct > 0.7:
1113
+ color = "yellow"
1114
+ print()
1115
+ if title:
1116
+ print(f" {self._style(title, 'bold')}")
1117
+ print(f" {self._style(bar, color=color)} {self._style(f'{pct*100:.0f}%', 'bold', color=color)}")
1118
+ print()
1119
+
1120
+ # 29. Clipboard
1121
+ def clipboard_copy(self, text: str) -> None:
1122
+ """Copy text to system clipboard. sh.clipboard_copy('Hello')"""
1123
+ import subprocess, platform
1124
+ sys_name = platform.system()
1125
+ try:
1126
+ if sys_name == "Darwin":
1127
+ subprocess.run(["pbcopy"], input=text.encode(), timeout=2)
1128
+ elif sys_name == "Linux":
1129
+ subprocess.run(["xclip", "-selection", "clipboard"], input=text.encode(), timeout=2)
1130
+ elif sys_name == "Windows":
1131
+ subprocess.run(["clip"], input=text.encode(), timeout=2)
1132
+ self.success("Copied to clipboard!")
1133
+ except Exception:
1134
+ # Fallback: use OSC 52 (works in many terminals)
1135
+ encoded = __import__('base64').b64encode(text.encode()).decode()
1136
+ sys.stdout.write(f"\033]52;c;{encoded}\007")
1137
+ sys.stdout.flush()
1138
+ self.info("Copied (OSC 52)")
1139
+
1140
+ # 30. Config Viewer
1141
+ def config(self, path: str) -> None:
1142
+ """View config files (TOML, YAML, INI, JSON) with syntax colors. sh.config('.env')"""
1143
+ import configparser
1144
+ print()
1145
+ print(self._style(f" 📄 {path}", "bold"))
1146
+ try:
1147
+ with open(path) as f:
1148
+ content = f.read()
1149
+ if path.endswith(('.json',)):
1150
+ self.json(_json.loads(content))
1151
+ elif path.endswith(('.ini', '.cfg', '.conf')):
1152
+ cp = configparser.ConfigParser()
1153
+ cp.read_string(content)
1154
+ for section in cp.sections():
1155
+ print(self._style(f" [{section}]", "bold", color="cyan"))
1156
+ for k, v in cp.items(section):
1157
+ print(f" {self._style(k, color='green')} = {v}")
1158
+ else:
1159
+ for line in content.strip().split("\n"):
1160
+ if line.startswith("#") or line.startswith(";"):
1161
+ print(f" {self._style(line, color='bright_black')}")
1162
+ elif "=" in line:
1163
+ k, v = line.split("=", 1)
1164
+ print(f" {self._style(k.strip(), color='green')} = {self._style(v.strip(), color='yellow')}")
1165
+ else:
1166
+ print(f" {line}")
1167
+ except Exception as e:
1168
+ self.error(f"Cannot read: {e}")
1169
+ print()
1170
+
1171
+ # 31. Pipe
1172
+ def pipe(self, data: Any):
1173
+ """Chained output: sh.pipe(data).table().metrics()"""
1174
+ return _PipeOutput(data, self)
1175
+
1176
+ # 32. Heatmap
1177
+ def heatmap(self, data: List[List[float]], title: Optional[str] = None) -> None:
1178
+ """Display 2D data as a heatmap. sh.heatmap([[1,2,3],[4,5,6],[7,8,9]])"""
1179
+ if not data:
1180
+ return
1181
+ print()
1182
+ if title:
1183
+ print(self._style(f" {title}", "bold"))
1184
+ chars = " ·░▒▓█"
1185
+ flat = [v for row in data for v in row]
1186
+ mn, mx = min(flat), max(flat)
1187
+ rng = max(mx - mn, 1)
1188
+ for row in data:
1189
+ line = " "
1190
+ for v in row:
1191
+ idx = int((v - mn) / rng * (len(chars) - 1))
1192
+ line += chars[min(idx, len(chars) - 1)] * 2
1193
+ print(line)
1194
+ print()
1195
+
1196
+ # 33. Notification
1197
+ def notify(self, title: str, message: str = "") -> None:
1198
+ """Cross-platform desktop notification. sh.notify('Build complete', 'All tests passed')"""
1199
+ import subprocess, platform
1200
+ sys_name = platform.system()
1201
+ try:
1202
+ if sys_name == "Darwin":
1203
+ subprocess.run(["osascript", "-e", f'display notification "{message}" with title "{title}"'], timeout=3)
1204
+ elif sys_name == "Linux":
1205
+ subprocess.run(["notify-send", title, message], timeout=3)
1206
+ elif sys_name == "Windows":
1207
+ from win10toast import ToastNotifier
1208
+ ToastNotifier().show_toast(title, message, duration=3)
1209
+ self.success(f"Notification sent: {title}")
1210
+ except Exception:
1211
+ print(f"\n 🔔 {title}: {message}\n")
1212
+
1213
+ # 34. File Watcher
1214
+ def filewatch(self, path: str, callback: Optional[Callable[[str], None]] = None) -> None:
1215
+ """Watch a file/directory for changes. sh.filewatch('app.py', callback=my_handler)"""
1216
+ import time as _time
1217
+ import hashlib
1218
+ print()
1219
+ self.info(f"Watching {path}... (Ctrl+C to stop)")
1220
+ try:
1221
+ if os.path.isfile(path):
1222
+ last_hash = hashlib.md5(open(path, "rb").read()).hexdigest()
1223
+ while True:
1224
+ _time.sleep(1)
1225
+ try:
1226
+ new_hash = hashlib.md5(open(path, "rb").read()).hexdigest()
1227
+ if new_hash != last_hash:
1228
+ last_hash = new_hash
1229
+ self.success(f"Changed: {path}")
1230
+ if callback:
1231
+ callback(path)
1232
+ except FileNotFoundError:
1233
+ self.warning(f"File deleted: {path}")
1234
+ break
1235
+ elif os.path.isdir(path):
1236
+ last_files = set(os.listdir(path))
1237
+ while True:
1238
+ _time.sleep(1)
1239
+ try:
1240
+ files = set(os.listdir(path))
1241
+ added = files - last_files
1242
+ removed = last_files - files
1243
+ if added:
1244
+ self.success(f"Added: {', '.join(added)}")
1245
+ if removed:
1246
+ self.error(f"Removed: {', '.join(removed)}")
1247
+ if added or removed:
1248
+ if callback:
1249
+ callback(path)
1250
+ last_files = files
1251
+ except Exception:
1252
+ pass
1253
+ except KeyboardInterrupt:
1254
+ self.info("Stopped watching.")
1255
+
1256
+ # 35. Menu (arrow-key navigable)
1257
+ def menu(self, options: List[str], title: Optional[str] = None) -> Optional[int]:
1258
+ """Arrow-key navigable menu. Returns selected index or None.
1259
+
1260
+ index = sh.menu(['Deploy', 'Rollback', 'Status', 'Exit'])
1261
+ """
1262
+ if not options:
1263
+ return None
1264
+ selected = 0
1265
+ print()
1266
+ if title:
1267
+ print(self._style(f" {title}", "bold"))
1268
+ print()
1269
+
1270
+ # Use raw terminal input for arrow keys
1271
+ import tty, termios, select as _select
1272
+
1273
+ def _get_key():
1274
+ fd = sys.stdin.fileno()
1275
+ old = termios.tcgetattr(fd)
1276
+ try:
1277
+ tty.setraw(fd)
1278
+ if _select.select([sys.stdin], [], [], 0.1)[0]:
1279
+ ch = sys.stdin.read(1)
1280
+ if ch == '\x1b':
1281
+ ch2 = sys.stdin.read(2)
1282
+ if ch2 == '[A':
1283
+ return 'up'
1284
+ elif ch2 == '[B':
1285
+ return 'down'
1286
+ elif ch2 == '[C':
1287
+ return 'right'
1288
+ elif ch2 == '[D':
1289
+ return 'left'
1290
+ elif ch in ('\r', '\n'):
1291
+ return 'enter'
1292
+ elif ch == '\x03':
1293
+ return 'ctrl_c'
1294
+ elif ch == 'q':
1295
+ return 'quit'
1296
+ return None
1297
+ finally:
1298
+ termios.tcsetattr(fd, termios.TCSADRAIN, old)
1299
+
1300
+ def _render():
1301
+ sys.stdout.write('\033[F' * (len(options) + 3))
1302
+ sys.stdout.write('\033[J')
1303
+ print()
1304
+ if title:
1305
+ print(self._style(f" {title}", "bold"))
1306
+ print()
1307
+ for i, opt in enumerate(options):
1308
+ if i == selected:
1309
+ print(f" {self._style('▸', color='cyan')} {self._style(opt, 'bold', color='cyan', bg='cyan')}")
1310
+ else:
1311
+ print(f" {opt}")
1312
+ print(f" {self._style('↑↓ navigate ↵ select q quit', color='bright_black')}")
1313
+ sys.stdout.flush()
1314
+
1315
+ self.info("Arrow keys: navigate, Enter: select, q: quit")
1316
+ try:
1317
+ _render()
1318
+ while True:
1319
+ key = _get_key()
1320
+ if key == 'up':
1321
+ selected = (selected - 1) % len(options)
1322
+ _render()
1323
+ elif key == 'down':
1324
+ selected = (selected + 1) % len(options)
1325
+ _render()
1326
+ elif key == 'enter':
1327
+ sys.stdout.write('\033[F' * (len(options) + 3))
1328
+ sys.stdout.write('\033[J')
1329
+ self.success(f"Selected: {options[selected]}")
1330
+ return selected
1331
+ elif key in ('quit', 'ctrl_c'):
1332
+ sys.stdout.write('\033[F' * (len(options) + 3))
1333
+ sys.stdout.write('\033[J')
1334
+ self.info("Cancelled")
1335
+ return None
1336
+ except Exception:
1337
+ return None
1338
+
1339
+ # 36. Simple Venn Diagram
1340
+ def venn(self, set_a: set, set_b: set, labels: tuple = ("A", "B"),
1341
+ title: Optional[str] = None) -> None:
1342
+ """ASCII Venn diagram for 2 sets. sh.venn({1,2,3}, {2,3,4}, ('Users', 'Admins'))"""
1343
+ a_only = set_a - set_b
1344
+ b_only = set_b - set_a
1345
+ both = set_a & set_b
1346
+ print()
1347
+ if title:
1348
+ print(self._style(f" {title}", "bold"))
1349
+ print()
1350
+ print(f" {self._style(labels[0], 'bold', color='cyan')}: {len(set_a)} | {self._style(labels[1], 'bold', color='magenta')}: {len(set_b)}")
1351
+ print(f" {self._style('Only ' + labels[0], color='bright_black')}: {len(a_only)} | {self._style('Only ' + labels[1], color='bright_black')}: {len(b_only)} | {self._style('Both', color='bright_black')}: {len(both)}")
1352
+ print()
1353
+
949
1354
  @property
950
1355
  def icons(self):
951
1356
  """Access icon constants."""
952
1357
  return _ICONS
1358
+
1359
+
1360
+ class _PipeOutput:
1361
+ """Enables chained output: sh.pipe(data).table()"""
1362
+ def __init__(self, data, shell):
1363
+ self.data = data
1364
+ self._sh = shell
1365
+
1366
+ def table(self, title=None, style="single"):
1367
+ self._sh.table(self.data, title=title, style=style)
1368
+ return self
1369
+
1370
+ def json(self, title=None):
1371
+ self._sh.json(self.data, title=title)
1372
+ return self
1373
+
1374
+ def metrics(self):
1375
+ if isinstance(self.data, dict):
1376
+ self._sh.metrics(self.data)
1377
+ return self
1378
+
1379
+ def bar(self, title=None):
1380
+ if isinstance(self.data, dict):
1381
+ self._sh.bar(self.data, title=title)
1382
+ return self
1383
+
1384
+ def columns(self, cols=2):
1385
+ if isinstance(self.data, list):
1386
+ self._sh.columns([str(x) for x in self.data], cols=cols)
1387
+ return self
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinyshell
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: Python library for beautiful terminal output — colored printing, tables, progress bars, syntax highlighting, bar charts, QR codes, and more. Zero dependencies.
5
5
  Home-page: https://github.com/adnanahamed66772ndpc/shinyshell
6
6
  Author: Adnan Ahamed Himal
7
7
  Author-email: hello@adnanahamedhimal.com
8
8
  Project-URL: Source, https://github.com/adnanahamed66772ndpc/shinyshell
9
9
  Project-URL: Bug Reports, https://github.com/adnanahamed66772ndpc/shinyshell/issues
10
+ Project-URL: Discussions, https://github.com/adnanahamed66772ndpc/shinyshell/discussions
10
11
  Project-URL: Documentation, https://github.com/adnanahamed66772ndpc/shinyshell#readme
11
12
  Keywords: terminal,cli,console,pretty,beautiful,output,print,colors,ansi,progress bar,table,spinner,syntax highlighting,diff,json,bar chart,qr code,ascii art,markdown,debug,trace,logging,bash,shell,colored,formatting,rich,colorama,termcolor,python library,python package,python cli,python terminal,developer tools,devtools,zero dependency
12
13
  Classifier: Development Status :: 4 - Beta
File without changes
File without changes
File without changes