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.
- {shinyshell-0.2.2/shinyshell.egg-info → shinyshell-0.4.0}/PKG-INFO +2 -1
- {shinyshell-0.2.2 → shinyshell-0.4.0}/setup.py +2 -1
- {shinyshell-0.2.2 → shinyshell-0.4.0}/shinyshell/__init__.py +834 -1
- {shinyshell-0.2.2 → shinyshell-0.4.0/shinyshell.egg-info}/PKG-INFO +2 -1
- {shinyshell-0.2.2 → shinyshell-0.4.0}/LICENSE +0 -0
- {shinyshell-0.2.2 → shinyshell-0.4.0}/README.md +0 -0
- {shinyshell-0.2.2 → shinyshell-0.4.0}/setup.cfg +0 -0
- {shinyshell-0.2.2 → shinyshell-0.4.0}/shinyshell/banner.py +0 -0
- {shinyshell-0.2.2 → shinyshell-0.4.0}/shinyshell.egg-info/SOURCES.txt +0 -0
- {shinyshell-0.2.2 → shinyshell-0.4.0}/shinyshell.egg-info/dependency_links.txt +0 -0
- {shinyshell-0.2.2 → shinyshell-0.4.0}/shinyshell.egg-info/top_level.txt +0 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shinyshell
|
|
3
|
-
Version: 0.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|