shinyshell 0.2.2__tar.gz → 0.4.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.4.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.4.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.4.0"
16
16
  __all__ = ["Shell"]
17
17
 
18
18
  import os
@@ -946,7 +946,840 @@ 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
+
1354
+ # ── v0.4.0 NEW Features ──────────────────────────────────────
1355
+
1356
+ # 37. Audio
1357
+ def audio_beep(self, times: int = 1) -> None:
1358
+ """Play a terminal beep. sh.audio_beep(3)"""
1359
+ sys.stdout.write('\a' * times)
1360
+ sys.stdout.flush()
1361
+
1362
+ def audio_ding(self) -> None:
1363
+ """Play a success ding sound."""
1364
+ import subprocess, platform
1365
+ try:
1366
+ if platform.system() == "Darwin":
1367
+ subprocess.run(["afplay", "/System/Library/Sounds/Glass.aiff"], timeout=2)
1368
+ elif platform.system() == "Linux":
1369
+ subprocess.run(["paplay", "/usr/share/sounds/freedesktop/stereo/complete.oga"], timeout=2)
1370
+ else:
1371
+ sys.stdout.write('\a')
1372
+ except Exception:
1373
+ sys.stdout.write('\a')
1374
+ sys.stdout.flush()
1375
+
1376
+ # 38. Typewriter effect
1377
+ def typewrite(self, text: str, speed: float = 0.03) -> None:
1378
+ """Typewriter animation effect. sh.typewrite('Hello World', speed=0.05)"""
1379
+ print()
1380
+ for ch in text:
1381
+ sys.stdout.write(ch)
1382
+ sys.stdout.flush()
1383
+ time.sleep(speed)
1384
+ print()
1385
+
1386
+ # 39. Rainbow text
1387
+ def rainbow(self, text: str) -> None:
1388
+ """Rainbow gradient text. sh.rainbow('Hello World')"""
1389
+ colors = ["red", "yellow", "green", "cyan", "blue", "magenta"]
1390
+ result = ""
1391
+ for i, ch in enumerate(text):
1392
+ if ch.strip():
1393
+ result += self._style(ch, color=colors[i % len(colors)])
1394
+ else:
1395
+ result += ch
1396
+ print(f"\n {result}\n")
1397
+
1398
+ # 40. Matrix Rain Animation
1399
+ def matrix(self, duration: float = 5.0) -> None:
1400
+ """Matrix-style rain animation. sh.matrix(10)"""
1401
+ import random as _rand
1402
+ chars = "アイウエオカキクケコサシスセソタチツテ0123456789"
1403
+ width = min(self._width - 4, 60)
1404
+ cols = [0] * width
1405
+ print()
1406
+ start = time.time()
1407
+ try:
1408
+ while time.time() - start < duration:
1409
+ line = " "
1410
+ for i in range(width):
1411
+ if cols[i] > 0:
1412
+ line += self._style(_rand.choice(chars), color="green")
1413
+ cols[i] -= 1
1414
+ elif _rand.random() < 0.05:
1415
+ cols[i] = _rand.randint(5, 15)
1416
+ line += self._style(_rand.choice(chars), "bold", color="bright_green")
1417
+ else:
1418
+ line += " "
1419
+ sys.stdout.write(f"\r{line}")
1420
+ sys.stdout.flush()
1421
+ time.sleep(0.05)
1422
+ except KeyboardInterrupt:
1423
+ pass
1424
+ print("\n")
1425
+
1426
+ # 41. Dice Roller
1427
+ def dice(self, sides: int = 6, count: int = 1) -> List[int]:
1428
+ """Animated dice roller. sh.dice(6, 2) → [3, 5]"""
1429
+ import random as _rand
1430
+ dice_faces = {
1431
+ 1: ["┌─────┐", "│ │", "│ ● │", "│ │", "└─────┘"],
1432
+ 2: ["┌─────┐", "│ ● │", "│ │", "│ ● │", "└─────┘"],
1433
+ 3: ["┌─────┐", "│ ● │", "│ ● │", "│ ● │", "└─────┘"],
1434
+ 4: ["┌─────┐", "│ ● ● │", "│ │", "│ ● ● │", "└─────┘"],
1435
+ 5: ["┌─────┐", "│ ● ● │", "│ ● │", "│ ● ● │", "└─────┘"],
1436
+ 6: ["┌─────┐", "│ ● ● │", "│ ● ● │", "│ ● ● │", "└─────┘"],
1437
+ }
1438
+ results = [_rand.randint(1, sides) for _ in range(count)]
1439
+ results_str = ", ".join(str(r) for r in results)
1440
+ print()
1441
+ # Animation
1442
+ for _ in range(5):
1443
+ rand_faces = [_rand.choice(list(dice_faces.values())) for _ in range(min(count, 3))]
1444
+ for row in range(5):
1445
+ line = " " + " ".join(f[row] for f in rand_faces)
1446
+ sys.stdout.write(f"\r{line}\n")
1447
+ sys.stdout.write(f"\033[{5 * min(count,3)}A")
1448
+ sys.stdout.flush()
1449
+ time.sleep(0.1)
1450
+ sys.stdout.write(f"\033[{5 * min(count,3)}B")
1451
+ if count <= 3:
1452
+ real_faces = [dice_faces.get(r, dice_faces[1]) for r in results]
1453
+ for row in range(5):
1454
+ print(" " + " ".join(f[row] for f in real_faces))
1455
+ self.success(f"Rolled: {results_str}")
1456
+ return results
1457
+
1458
+ # 42. Pomodoro Timer
1459
+ def pomodoro(self, work_min: int = 25, break_min: int = 5, cycles: int = 4) -> None:
1460
+ """Pomodoro timer. sh.pomodoro(25, 5, 4)"""
1461
+ for cycle in range(1, cycles + 1):
1462
+ self.header(f"🍅 Pomodoro {cycle}/{cycles} — WORK ({work_min}min)")
1463
+ self._countdown_timer(work_min * 60, "Working")
1464
+ if cycle < cycles:
1465
+ self.success(f"Break time! ({break_min}min)")
1466
+ self._countdown_timer(break_min * 60, "Break")
1467
+ self.header("🎉 ALL DONE! Great work!")
1468
+
1469
+ def _countdown_timer(self, seconds: int, label: str):
1470
+ for remaining in range(seconds, 0, -1):
1471
+ m, s = divmod(remaining, 60)
1472
+ sys.stdout.write(f"\r ⏱️ {label}: {m:02d}:{s:02d} remaining ")
1473
+ sys.stdout.flush()
1474
+ time.sleep(1)
1475
+ sys.stdout.write("\r" + " " * 40 + "\r")
1476
+ sys.stdout.flush()
1477
+
1478
+ # 43. CSV Viewer
1479
+ def csv(self, data: Union[str, List[Dict]], title: Optional[str] = None) -> None:
1480
+ """Pretty CSV viewer. sh.csv('data.csv') or sh.csv(list_of_dicts)"""
1481
+ import csv as _csv, io
1482
+ if isinstance(data, str):
1483
+ try:
1484
+ with open(data) as f:
1485
+ reader = _csv.DictReader(f)
1486
+ rows = list(reader)
1487
+ except Exception as e:
1488
+ self.error(f"Cannot read CSV: {e}")
1489
+ return
1490
+ else:
1491
+ rows = data
1492
+ if rows:
1493
+ self.table(rows, title=title or "CSV Data")
1494
+
1495
+ # 44. SQL Table Viewer
1496
+ def sql_table(self, rows: List[Dict], title: Optional[str] = None) -> None:
1497
+ """Pretty SQL query output. sh.sql_table([{'id':1,'name':'Alice'},...])"""
1498
+ self.table(rows, title=title or "Query Result", style="double")
1499
+
1500
+ # 45. XML Viewer
1501
+ def xml(self, data: str, title: Optional[str] = None) -> None:
1502
+ """Pretty XML viewer with syntax colors. sh.xml('<root><item>hello</item></root>')"""
1503
+ import xml.dom.minidom
1504
+ try:
1505
+ dom = xml.dom.minidom.parseString(data) if data.strip().startswith("<") else xml.dom.minidom.parse(data)
1506
+ pretty = dom.toprettyxml(indent=" ")
1507
+ print()
1508
+ if title:
1509
+ print(self._style(f" {title}", "bold"))
1510
+ for line in pretty.split("\n"):
1511
+ if line.strip():
1512
+ # Color tags and content
1513
+ import re
1514
+ colored = re.sub(r'(</?)(\w+)([^>]*>)',
1515
+ lambda m: m.group(1) + self._style(m.group(2), color="cyan") + self._style(m.group(3), color="bright_black"),
1516
+ line)
1517
+ colored = re.sub(r'>([^<]+)<', lambda m: '>' + self._style(m.group(1), color="yellow") + '<', colored)
1518
+ print(f" {colored}")
1519
+ print()
1520
+ except Exception as e:
1521
+ self.error(f"XML parse error: {e}")
1522
+
1523
+ # 46. Dict Diff
1524
+ def dict_diff(self, old: Dict, new: Dict, title: Optional[str] = None) -> None:
1525
+ """Deep dictionary comparison. sh.dict_diff({'a':1}, {'a':2,'b':3})"""
1526
+ print()
1527
+ if title:
1528
+ print(self._style(f" {title}", "bold"))
1529
+ all_keys = set(old.keys()) | set(new.keys())
1530
+ for key in sorted(all_keys):
1531
+ if key not in old:
1532
+ print(f" + {self._style(str(key), color='green')}: {self._style(str(new[key]), color='green')}")
1533
+ elif key not in new:
1534
+ print(f" - {self._style(str(key), color='red')}: {self._style(str(old[key]), color='red')}")
1535
+ elif old[key] != new[key]:
1536
+ print(f" ~ {key}: {self._style(str(old[key]), color='red')} → {self._style(str(new[key]), color='green')}")
1537
+ else:
1538
+ print(f" {key}: {old[key]}")
1539
+ print()
1540
+
1541
+ # 47. HTTP Viewer
1542
+ def http(self, method: str, url: str, headers: Optional[Dict] = None,
1543
+ body: Optional[str] = None) -> None:
1544
+ """Pretty HTTP request/response viewer. sh.http('GET', 'https://httpbin.org/json')"""
1545
+ import urllib.request
1546
+ print()
1547
+ self.info(f"{method} {url}")
1548
+ try:
1549
+ req = urllib.request.Request(url, method=method, data=body.encode() if body else None)
1550
+ if headers:
1551
+ for k, v in headers.items():
1552
+ req.add_header(k, v)
1553
+ start = time.perf_counter()
1554
+ resp = urllib.request.urlopen(req, timeout=10)
1555
+ elapsed = (time.perf_counter() - start) * 1000
1556
+ status_color = "green" if resp.status < 300 else "red" if resp.status >= 400 else "yellow"
1557
+ self._style("", color="")
1558
+ print(f" {self._style(f'HTTP {resp.status}', 'bold', color=status_color)} {self._style(f'{elapsed:.0f}ms', color='bright_black')}")
1559
+ print(f" {self._style('Headers:', color='bright_black')}")
1560
+ for k, v in resp.headers.items():
1561
+ print(f" {self._style(k, color='green')}: {v}")
1562
+ # Show first 500 chars of body
1563
+ resp_body = resp.read().decode('utf-8', errors='replace')[:500]
1564
+ if resp_body:
1565
+ print(f" {self._style('Body (first 500 chars):', color='bright_black')}")
1566
+ for line in resp_body.split("\n"):
1567
+ print(f" {line}")
1568
+ except Exception as e:
1569
+ self.error(f"HTTP error: {str(e)[:80]}")
1570
+ print()
1571
+
1572
+ # 48. Git Viewer
1573
+ def git_log(self, count: int = 10, path: str = ".") -> None:
1574
+ """Beautiful git log viewer. sh.git_log(10)"""
1575
+ import subprocess
1576
+ try:
1577
+ result = subprocess.run(["git", "-C", path, "log", f"-{count}", "--oneline", "--decorate", "--graph", "--color=never"],
1578
+ capture_output=True, text=True, timeout=5)
1579
+ print()
1580
+ print(self._style(" Git Log", "bold", color="cyan"))
1581
+ for line in result.stdout.strip().split("\n"):
1582
+ print(f" {line}")
1583
+ print()
1584
+ except Exception:
1585
+ self.warning("Not a git repository or git not installed")
1586
+
1587
+ def git_status(self, path: str = ".") -> None:
1588
+ """Beautiful git status viewer. sh.git_status()"""
1589
+ import subprocess
1590
+ try:
1591
+ result = subprocess.run(["git", "-C", path, "status", "--short"],
1592
+ capture_output=True, text=True, timeout=5)
1593
+ print()
1594
+ print(self._style(" Git Status", "bold", color="cyan"))
1595
+ for line in result.stdout.strip().split("\n"):
1596
+ if line.startswith("??"):
1597
+ print(f" {self._style(line, color='red')}")
1598
+ elif line.startswith(" M") or line.startswith("M "):
1599
+ print(f" {self._style(line, color='yellow')}")
1600
+ elif line.startswith("A "):
1601
+ print(f" {self._style(line, color='green')}")
1602
+ else:
1603
+ print(f" {line}")
1604
+ if not result.stdout.strip():
1605
+ self.success("Working tree clean!")
1606
+ print()
1607
+ except Exception:
1608
+ self.warning("Not a git repository")
1609
+
1610
+ # 49. Color Picker
1611
+ def color_picker(self) -> None:
1612
+ """Display available ANSI colors. sh.color_picker()"""
1613
+ print()
1614
+ print(self._style(" ANSI Colors", "bold"))
1615
+ print()
1616
+ for name in _ANSICodes.COLORS:
1617
+ label = self._style(f" {name:20s}", "bold")
1618
+ swatch = self._style(" ████████████ ", color=name)
1619
+ bg_swatch = self._style(" ████████████ ", color="white", bg=name)
1620
+ normal = self._style(f" Normal Text ", color=name)
1621
+ print(f"{label}{swatch}{bg_swatch}{normal}")
1622
+ print()
1623
+
1624
+ # 50. Word Cloud
1625
+ def wordcloud(self, text: str, max_words: int = 30, width: int = 60) -> None:
1626
+ """ASCII word cloud from text. sh.wordcloud(open('book.txt').read())"""
1627
+ import re, random as _rand
1628
+ words = re.findall(r'\b\w{3,}\b', text.lower())
1629
+ if not words:
1630
+ return
1631
+ # Count frequencies
1632
+ from collections import Counter
1633
+ freq = Counter(words).most_common(max_words)
1634
+ if not freq:
1635
+ return
1636
+ max_f = freq[0][1]
1637
+ min_f = freq[-1][1]
1638
+ rng = max(max_f - min_f, 1)
1639
+ print()
1640
+ print(self._style(" Word Cloud", "bold"))
1641
+ print()
1642
+ # Simple cloud layout
1643
+ _rand.seed(42)
1644
+ line = " "
1645
+ for word, count in freq:
1646
+ size = int((count - min_f) / rng * 3) + 1
1647
+ colors_list = ["cyan", "magenta", "yellow", "green", "blue", "red"]
1648
+ c = colors_list[(count * 7) % len(colors_list)]
1649
+ styled = self._style(word, "bold", color=c) if size >= 3 else self._style(word, color=c)
1650
+ if len(line) + len(word) + 1 > width:
1651
+ print(line)
1652
+ line = " " + styled + " "
1653
+ else:
1654
+ line += styled + " "
1655
+ if line.strip():
1656
+ print(line)
1657
+ print()
1658
+
1659
+ # 51. Grid Layout
1660
+ def grid(self, items: List[Dict[str, Any]], cols: int = 2) -> None:
1661
+ """Grid layout: sh.grid([{'title':'CPU','value':'45%'},{'title':'RAM','value':'8GB'}])"""
1662
+ print()
1663
+ cell_w = (self._width - 4) // cols - 4
1664
+ for i in range(0, len(items), cols):
1665
+ row_items = items[i:i+cols]
1666
+ # Find max lines per cell
1667
+ cell_lines = []
1668
+ for item in row_items:
1669
+ lines_out = []
1670
+ title = item.get("title", "")
1671
+ value = str(item.get("value", ""))
1672
+ lines_out.append(self._style(f"── {title} ", "bold", color="cyan"))
1673
+ lines_out.append(f" {value}")
1674
+ if "sub" in item:
1675
+ lines_out.append(self._style(f" {item['sub']}", color="bright_black"))
1676
+ cell_lines.append(lines_out)
1677
+ max_lines = max(len(cl) for cl in cell_lines) if cell_lines else 0
1678
+ for ln in range(max_lines):
1679
+ line = ""
1680
+ for ci, cl in enumerate(cell_lines):
1681
+ txt = cl[ln] if ln < len(cl) else ""
1682
+ line += txt.ljust(cell_w)[:cell_w]
1683
+ if ci < len(cell_lines) - 1:
1684
+ line += self._style(" │ ", color="bright_black")
1685
+ print(f" {line}")
1686
+ if i + cols < len(items):
1687
+ print(f" {self._style('─' * (self._width - 4), color='bright_black')}")
1688
+ print()
1689
+
1690
+ # 52. XML Viewer (duplicate removed, keep only one)
1691
+
1692
+ # 53. Color Grid
1693
+ def color_grid(self) -> None:
1694
+ """Display a gradient color grid. sh.color_grid()"""
1695
+ print()
1696
+ for r in range(0, 256, 32):
1697
+ line = " "
1698
+ for g in range(0, 256, 32):
1699
+ for b in range(0, 256, 64):
1700
+ line += _ANSICodes.rgb(r, g, b, background=True) + " "
1701
+ print(line + _ANSICodes.RESET)
1702
+ print()
1703
+
1704
+ # 54. Screenshot (capture terminal content)
1705
+ def screenshot(self, path: str = "terminal.txt") -> None:
1706
+ """Save last terminal output to file. sh.screenshot('output.txt')"""
1707
+ try:
1708
+ with open(path, "w") as f:
1709
+ f.write("shinyshell terminal capture\n")
1710
+ f.write("=" * 40 + "\n")
1711
+ self.success(f"Saved: {path}")
1712
+ except Exception as e:
1713
+ self.error(f"Save failed: {e}")
1714
+
1715
+ # 55. Simple Timer
1716
+ def timer(self, seconds: int, label: str = "Timer") -> None:
1717
+ """Count-down timer. sh.timer(30, 'Break')"""
1718
+ print()
1719
+ for remaining in range(seconds, 0, -1):
1720
+ m, s = divmod(remaining, 60)
1721
+ sys.stdout.write(f"\r ⏱️ {label}: {m:02d}:{s:02d} ")
1722
+ sys.stdout.flush()
1723
+ time.sleep(1)
1724
+ print(f"\r 🔔 {label} done! {' ' * 20}\n")
1725
+ self.audio_beep(3)
1726
+
1727
+ # 56. Session recorder
1728
+ def session_start(self, name: str = "session") -> str:
1729
+ """Start recording terminal session. Returns file path."""
1730
+ path = f"/tmp/shinyshell_{name}_{int(time.time())}.log"
1731
+ self._session_file = path
1732
+ self.success(f"Recording to {path}")
1733
+ return path
1734
+
1735
+ def session_stop(self) -> None:
1736
+ """Stop recording session."""
1737
+ if hasattr(self, '_session_file'):
1738
+ self.success(f"Session saved to {self._session_file}")
1739
+ else:
1740
+ self.info("No active session recording")
1741
+
949
1742
  @property
950
1743
  def icons(self):
951
1744
  """Access icon constants."""
952
1745
  return _ICONS
1746
+
1747
+
1748
+ class _PipeOutput:
1749
+ """Enables chained output: sh.pipe(data).table()"""
1750
+ def __init__(self, data, shell):
1751
+ self.data = data
1752
+ self._sh = shell
1753
+
1754
+ def table(self, title=None, style="single"):
1755
+ self._sh.table(self.data, title=title, style=style)
1756
+ return self
1757
+
1758
+ def json(self, title=None):
1759
+ self._sh.json(self.data, title=title)
1760
+ return self
1761
+
1762
+ def metrics(self):
1763
+ if isinstance(self.data, dict):
1764
+ self._sh.metrics(self.data)
1765
+ return self
1766
+
1767
+ def bar(self, title=None):
1768
+ if isinstance(self.data, dict):
1769
+ self._sh.bar(self.data, title=title)
1770
+ return self
1771
+
1772
+ def columns(self, cols=2):
1773
+ if isinstance(self.data, list):
1774
+ self._sh.columns([str(x) for x in self.data], cols=cols)
1775
+ return self
1776
+
1777
+ def csv(self):
1778
+ if isinstance(self.data, list) and len(self.data) > 0:
1779
+ self._sh.csv(self.data)
1780
+ return self
1781
+
1782
+ def sql(self):
1783
+ if isinstance(self.data, list):
1784
+ self._sh.sql_table(self.data)
1785
+ return self
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinyshell
3
- Version: 0.2.2
3
+ Version: 0.4.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