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.
- {shinyshell-0.2.2/shinyshell.egg-info → shinyshell-0.3.0}/PKG-INFO +2 -1
- {shinyshell-0.2.2 → shinyshell-0.3.0}/setup.py +2 -1
- {shinyshell-0.2.2 → shinyshell-0.3.0}/shinyshell/__init__.py +436 -1
- {shinyshell-0.2.2 → shinyshell-0.3.0/shinyshell.egg-info}/PKG-INFO +2 -1
- {shinyshell-0.2.2 → shinyshell-0.3.0}/LICENSE +0 -0
- {shinyshell-0.2.2 → shinyshell-0.3.0}/README.md +0 -0
- {shinyshell-0.2.2 → shinyshell-0.3.0}/setup.cfg +0 -0
- {shinyshell-0.2.2 → shinyshell-0.3.0}/shinyshell/banner.py +0 -0
- {shinyshell-0.2.2 → shinyshell-0.3.0}/shinyshell.egg-info/SOURCES.txt +0 -0
- {shinyshell-0.2.2 → shinyshell-0.3.0}/shinyshell.egg-info/dependency_links.txt +0 -0
- {shinyshell-0.2.2 → shinyshell-0.3.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.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.
|
|
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.
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|