shinyshell 0.1.0__py3-none-any.whl
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/__init__.py
ADDED
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
"""
|
|
2
|
+
shinyshell — Beautiful terminal output for Python. Zero dependencies.
|
|
3
|
+
=====================================
|
|
4
|
+
One import. All the pretty you need.
|
|
5
|
+
|
|
6
|
+
from shinyshell import Shell
|
|
7
|
+
sh = Shell()
|
|
8
|
+
sh.success("Deployed!")
|
|
9
|
+
sh.table(users)
|
|
10
|
+
sh.progress("Loading...")
|
|
11
|
+
|
|
12
|
+
Pure Python stdlib. Works on Linux, macOS, Windows.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
__version__ = "0.1.0"
|
|
16
|
+
__all__ = ["Shell"]
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
import shutil
|
|
21
|
+
import textwrap
|
|
22
|
+
import math
|
|
23
|
+
from typing import Any, List, Dict, Optional, Union, Callable
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ── ANSI / Color utilities (stdlib only) ───────────────────────
|
|
27
|
+
|
|
28
|
+
def _supports_color() -> bool:
|
|
29
|
+
"""Check if the terminal supports color output."""
|
|
30
|
+
if not hasattr(sys.stdout, "isatty"):
|
|
31
|
+
return False
|
|
32
|
+
if not sys.stdout.isatty():
|
|
33
|
+
return False
|
|
34
|
+
if sys.platform == "win32":
|
|
35
|
+
try:
|
|
36
|
+
import ctypes
|
|
37
|
+
kernel32 = ctypes.windll.kernel32
|
|
38
|
+
mode = ctypes.c_uint32()
|
|
39
|
+
handle = kernel32.GetStdHandle(-11) # STD_OUTPUT_HANDLE
|
|
40
|
+
kernel32.GetConsoleMode(handle, ctypes.byref(mode))
|
|
41
|
+
kernel32.SetConsoleMode(handle, mode.value | 0x0004) # ENABLE_VIRTUAL_TERMINAL
|
|
42
|
+
return True
|
|
43
|
+
except Exception:
|
|
44
|
+
return False
|
|
45
|
+
term = os.environ.get("TERM", "")
|
|
46
|
+
if term == "dumb":
|
|
47
|
+
return False
|
|
48
|
+
if "NO_COLOR" in os.environ:
|
|
49
|
+
return False
|
|
50
|
+
return True
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class _ANSICodes:
|
|
54
|
+
"""ANSI escape codes for colors and styles."""
|
|
55
|
+
RESET = "\033[0m"
|
|
56
|
+
BOLD = "\033[1m"
|
|
57
|
+
DIM = "\033[2m"
|
|
58
|
+
ITALIC = "\033[3m"
|
|
59
|
+
UNDERLINE = "\033[4m"
|
|
60
|
+
BLINK = "\033[5m"
|
|
61
|
+
REVERSE = "\033[7m"
|
|
62
|
+
STRIKE = "\033[9m"
|
|
63
|
+
|
|
64
|
+
COLORS = {
|
|
65
|
+
"black": 30, "red": 31, "green": 32, "yellow": 33,
|
|
66
|
+
"blue": 34, "magenta": 35, "cyan": 36, "white": 37,
|
|
67
|
+
"bright_black": 90, "bright_red": 91, "bright_green": 92,
|
|
68
|
+
"bright_yellow": 93, "bright_blue": 94, "bright_magenta": 95,
|
|
69
|
+
"bright_cyan": 96, "bright_white": 97,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
BG_COLORS = {
|
|
73
|
+
"black": 40, "red": 41, "green": 42, "yellow": 43,
|
|
74
|
+
"blue": 44, "magenta": 45, "cyan": 46, "white": 47,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def color(cls, name: str, background: bool = False) -> str:
|
|
79
|
+
if background:
|
|
80
|
+
return f"\033[{cls.BG_COLORS.get(name, 40)}m"
|
|
81
|
+
return f"\033[{cls.COLORS.get(name, 37)}m"
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def rgb(cls, r: int, g: int, b: int, background: bool = False) -> str:
|
|
85
|
+
base = 48 if background else 38
|
|
86
|
+
return f"\033[{base};2;{r};{g};{b}m"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class _Style:
|
|
90
|
+
"""Applies styles to text only if color is supported."""
|
|
91
|
+
def __init__(self, enabled: bool = True):
|
|
92
|
+
self.enabled = enabled
|
|
93
|
+
|
|
94
|
+
def __call__(self, text: str, *styles: str, color: Optional[str] = None,
|
|
95
|
+
bg: Optional[str] = None) -> str:
|
|
96
|
+
if not self.enabled:
|
|
97
|
+
return str(text)
|
|
98
|
+
codes = _ANSICodes()
|
|
99
|
+
parts = []
|
|
100
|
+
for s in styles:
|
|
101
|
+
code = getattr(codes, s.upper(), None)
|
|
102
|
+
if code:
|
|
103
|
+
parts.append(code)
|
|
104
|
+
if color:
|
|
105
|
+
parts.append(codes.color(color))
|
|
106
|
+
if bg:
|
|
107
|
+
parts.append(codes.color(bg, background=True))
|
|
108
|
+
if not parts:
|
|
109
|
+
return str(text)
|
|
110
|
+
return "".join(parts) + str(text) + codes.RESET
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ── Icons (Unicode, works everywhere) ──────────────────────────
|
|
114
|
+
_ICONS = {
|
|
115
|
+
"success": "✨",
|
|
116
|
+
"error": "💥",
|
|
117
|
+
"warning": "⚠️",
|
|
118
|
+
"info": "ℹ️",
|
|
119
|
+
"question": "❓",
|
|
120
|
+
"star": "⭐",
|
|
121
|
+
"heart": "💜",
|
|
122
|
+
"fire": "🔥",
|
|
123
|
+
"rocket": "🚀",
|
|
124
|
+
"check": "✅",
|
|
125
|
+
"cross": "❌",
|
|
126
|
+
"arrow": "→",
|
|
127
|
+
"bullet": "•",
|
|
128
|
+
"diamond": "◆",
|
|
129
|
+
"pointer": "▸",
|
|
130
|
+
"dot": "·",
|
|
131
|
+
"lightning": "⚡",
|
|
132
|
+
"clock": "🕐",
|
|
133
|
+
"lock": "🔒",
|
|
134
|
+
"unlock": "🔓",
|
|
135
|
+
"key": "🔑",
|
|
136
|
+
"gear": "⚙️",
|
|
137
|
+
"package": "📦",
|
|
138
|
+
"link": "🔗",
|
|
139
|
+
"globe": "🌍",
|
|
140
|
+
"mail": "📧",
|
|
141
|
+
"phone": "📞",
|
|
142
|
+
"pin": "📌",
|
|
143
|
+
"bookmark": "🔖",
|
|
144
|
+
"tag": "🏷️",
|
|
145
|
+
"flag": "🚩",
|
|
146
|
+
"target": "🎯",
|
|
147
|
+
"trophy": "🏆",
|
|
148
|
+
"medal": "🥇",
|
|
149
|
+
"gift": "🎁",
|
|
150
|
+
"party": "🎉",
|
|
151
|
+
"sparkles": "✨",
|
|
152
|
+
"magic": "🪄",
|
|
153
|
+
"robot": "🤖",
|
|
154
|
+
"bug": "🐛",
|
|
155
|
+
"eyes": "👀",
|
|
156
|
+
"brain": "🧠",
|
|
157
|
+
"tools": "🛠️",
|
|
158
|
+
"chart": "📊",
|
|
159
|
+
"database": "🗄️",
|
|
160
|
+
"file": "📄",
|
|
161
|
+
"folder": "📁",
|
|
162
|
+
"download": "📥",
|
|
163
|
+
"upload": "📤",
|
|
164
|
+
"save": "💾",
|
|
165
|
+
"print": "🖨️",
|
|
166
|
+
"search": "🔍",
|
|
167
|
+
"shield": "🛡️",
|
|
168
|
+
"money": "💰",
|
|
169
|
+
"credit": "💳",
|
|
170
|
+
"shopping": "🛒",
|
|
171
|
+
"home": "🏠",
|
|
172
|
+
"world": "🌐",
|
|
173
|
+
"mobile": "📱",
|
|
174
|
+
"desktop": "🖥️",
|
|
175
|
+
"server": "🖥",
|
|
176
|
+
"cloud": "☁️",
|
|
177
|
+
"terminal": "💻",
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ── Borders / Frames ───────────────────────────────────────────
|
|
182
|
+
_BORDERS = {
|
|
183
|
+
"single": "─│┌┐└┘├┤┬┴┼",
|
|
184
|
+
"double": "═║╔╗╚╝╠╣╦╩╬",
|
|
185
|
+
"round": "─│╭╮╰╯├┤┬┴┼",
|
|
186
|
+
"bold": "━┃┏┓┗┛┣┫┳┻╋",
|
|
187
|
+
"dashed": "┄┆┌┐└┘├┤┬┴┼",
|
|
188
|
+
"none": " ",
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ── Shell Class ─────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
class Shell:
|
|
195
|
+
"""Beautiful terminal output for Python scripts and CLIs."""
|
|
196
|
+
|
|
197
|
+
def __init__(self, color: bool = True, width: Optional[int] = None):
|
|
198
|
+
self._color_enabled = color and _supports_color()
|
|
199
|
+
self._style = _Style(self._color_enabled)
|
|
200
|
+
self._width = width or min(shutil.get_terminal_size().columns, 120)
|
|
201
|
+
|
|
202
|
+
# ── Message Helpers ─────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
def success(self, message: str) -> None:
|
|
205
|
+
"""Print a success message with green checkmark."""
|
|
206
|
+
self._log("success", message, "green")
|
|
207
|
+
|
|
208
|
+
def error(self, message: str) -> None:
|
|
209
|
+
"""Print an error message with red cross."""
|
|
210
|
+
self._log("error", message, "red")
|
|
211
|
+
|
|
212
|
+
def warning(self, message: str) -> None:
|
|
213
|
+
"""Print a warning with yellow triangle."""
|
|
214
|
+
self._log("warning", message, "yellow")
|
|
215
|
+
|
|
216
|
+
def info(self, message: str) -> None:
|
|
217
|
+
"""Print an info message with blue circle."""
|
|
218
|
+
self._log("info", message, "cyan")
|
|
219
|
+
|
|
220
|
+
def _log(self, level: str, message: str, color: str) -> None:
|
|
221
|
+
icon = _ICONS.get(level, "•")
|
|
222
|
+
prefix = self._style(f" {icon} ", bg=color, color="white")
|
|
223
|
+
text = self._style(f" {message}", color=color)
|
|
224
|
+
print(f"{prefix}{text}")
|
|
225
|
+
|
|
226
|
+
# ── Sections & Headers ──────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
def header(self, title: str, level: int = 1) -> None:
|
|
229
|
+
"""Print a styled header."""
|
|
230
|
+
if level == 1:
|
|
231
|
+
line = "═" * (self._width - 4)
|
|
232
|
+
text = self._style(f" {title} ", "bold")
|
|
233
|
+
print(f"\n{self._style(f'╔{line}╗', color='cyan')}")
|
|
234
|
+
print(f"{self._style('║', color='cyan')}{text.center(self._width - 2)}{self._style('║', color='cyan')}")
|
|
235
|
+
print(f"{self._style(f'╚{line}╝', color='cyan')}")
|
|
236
|
+
else:
|
|
237
|
+
text = self._style(f"── {title} ", "bold", color="cyan")
|
|
238
|
+
rest = "─" * max(0, self._width - len(title) - 10)
|
|
239
|
+
print(f"\n{text}{self._style(rest, color='cyan', dim=True)}")
|
|
240
|
+
|
|
241
|
+
def banner(self, text: str, color: str = "cyan") -> None:
|
|
242
|
+
"""Display a large ASCII banner."""
|
|
243
|
+
from shinyshell.banner import render
|
|
244
|
+
print(self._style(render(text), color=color))
|
|
245
|
+
|
|
246
|
+
# ── Progress ────────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
def spinner(self, message: str, duration: float = 3.0) -> None:
|
|
249
|
+
"""Show a brief spinning animation with message."""
|
|
250
|
+
frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
251
|
+
import time
|
|
252
|
+
start = time.time()
|
|
253
|
+
i = 0
|
|
254
|
+
print()
|
|
255
|
+
try:
|
|
256
|
+
while time.time() - start < duration:
|
|
257
|
+
frame = self._style(frames[i % len(frames)], color="cyan")
|
|
258
|
+
sys.stdout.write(f"\r {frame} {message}")
|
|
259
|
+
sys.stdout.flush()
|
|
260
|
+
time.sleep(0.08)
|
|
261
|
+
i += 1
|
|
262
|
+
sys.stdout.write(f"\r {self._style(_ICONS['check'], color='green')} {message} {self._style('Done!', color='green')}\n")
|
|
263
|
+
except KeyboardInterrupt:
|
|
264
|
+
sys.stdout.write(f"\r {self._style(_ICONS['cross'], color='red')} {message} Cancelled\n")
|
|
265
|
+
|
|
266
|
+
def progress(self, message: str = "Working") -> Callable[[int, int], None]:
|
|
267
|
+
"""Return a progress updater function. Use as context:
|
|
268
|
+
|
|
269
|
+
update = sh.progress("Downloading")
|
|
270
|
+
for i in range(100):
|
|
271
|
+
update(i+1, 100)
|
|
272
|
+
"""
|
|
273
|
+
def update(current: int, total: int):
|
|
274
|
+
percent = int((current / max(total, 1)) * 100)
|
|
275
|
+
bar_width = 30
|
|
276
|
+
filled = int(bar_width * current / max(total, 1))
|
|
277
|
+
bar = "█" * filled + "░" * (bar_width - filled)
|
|
278
|
+
pct = f"{percent:3d}%"
|
|
279
|
+
sys.stdout.write(f"\r {message} {self._style(bar, color='cyan')} {pct}")
|
|
280
|
+
sys.stdout.flush()
|
|
281
|
+
if current >= total:
|
|
282
|
+
print()
|
|
283
|
+
return update
|
|
284
|
+
|
|
285
|
+
def countdown(self, seconds: int, message: str = "Starting in") -> None:
|
|
286
|
+
"""Show an animated countdown."""
|
|
287
|
+
import time
|
|
288
|
+
for i in range(seconds, 0, -1):
|
|
289
|
+
sys.stdout.write(f"\r {self._style(_ICONS['clock'], color='yellow')} {message} {self._style(str(i), 'bold', color='yellow')}...")
|
|
290
|
+
sys.stdout.flush()
|
|
291
|
+
time.sleep(1)
|
|
292
|
+
print(f"\r {self._style(_ICONS['rocket'], color='green')} {self._style('Go!', 'bold', color='green')} ")
|
|
293
|
+
|
|
294
|
+
# ── Tables ──────────────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
def table(self, data: List[Dict[str, Any]], title: Optional[str] = None,
|
|
297
|
+
style: str = "single") -> None:
|
|
298
|
+
"""Display data as a beautiful table.
|
|
299
|
+
|
|
300
|
+
data = [
|
|
301
|
+
{"Name": "Alice", "Role": "Developer", "Stars": 42},
|
|
302
|
+
{"Name": "Bob", "Role": "Designer", "Stars": 17},
|
|
303
|
+
]
|
|
304
|
+
sh.table(data, title="Team Members")
|
|
305
|
+
"""
|
|
306
|
+
if not data:
|
|
307
|
+
return
|
|
308
|
+
|
|
309
|
+
keys = list(data[0].keys())
|
|
310
|
+
col_widths = {k: max(len(str(k)), max(len(str(row.get(k, ""))) for row in data)) + 2
|
|
311
|
+
for k in keys}
|
|
312
|
+
|
|
313
|
+
if title:
|
|
314
|
+
print()
|
|
315
|
+
print(self._style(f" {title}", "bold"))
|
|
316
|
+
|
|
317
|
+
border = _BORDERS.get(style, _BORDERS["single"])
|
|
318
|
+
h, v, tl, tr, bl, br, l, r, t, b, x = list(border)
|
|
319
|
+
|
|
320
|
+
total_width = sum(col_widths.values()) + len(keys) - 1
|
|
321
|
+
|
|
322
|
+
# Top border
|
|
323
|
+
top = tl + h * total_width + tr
|
|
324
|
+
print(self._style(f" {top}", color="bright_black"))
|
|
325
|
+
|
|
326
|
+
# Header
|
|
327
|
+
cells = [self._style(f" {str(k).ljust(col_widths[k])}", "bold")
|
|
328
|
+
for k in keys]
|
|
329
|
+
print(f" {v}{v.join(cells)}{v}")
|
|
330
|
+
|
|
331
|
+
# Separator
|
|
332
|
+
sep = l + h * total_width + r
|
|
333
|
+
print(self._style(f" {sep}", color="bright_black"))
|
|
334
|
+
|
|
335
|
+
# Rows
|
|
336
|
+
for row in data:
|
|
337
|
+
cells = [f" {str(row.get(k, '')).ljust(col_widths[k])}" for k in keys]
|
|
338
|
+
print(f" {self._style(v, color='bright_black')}{v.join(cells)}{self._style(v, color='bright_black')}")
|
|
339
|
+
|
|
340
|
+
# Bottom border
|
|
341
|
+
bottom = bl + h * total_width + br
|
|
342
|
+
print(self._style(f" {bottom}", color="bright_black"))
|
|
343
|
+
print()
|
|
344
|
+
|
|
345
|
+
# ── Code / Syntax ───────────────────────────────────────────
|
|
346
|
+
|
|
347
|
+
def code(self, source: str, language: str = "python") -> None:
|
|
348
|
+
"""Display syntax-highlighted code block."""
|
|
349
|
+
keywords = {"def", "class", "import", "from", "return", "if", "else",
|
|
350
|
+
"elif", "for", "while", "try", "except", "finally", "with",
|
|
351
|
+
"as", "in", "not", "and", "or", "True", "False", "None",
|
|
352
|
+
"async", "await", "yield", "raise", "pass", "break", "continue"}
|
|
353
|
+
strings_color = "green"
|
|
354
|
+
kw_color = "magenta"
|
|
355
|
+
comment_color = "bright_black"
|
|
356
|
+
num_color = "yellow"
|
|
357
|
+
func_color = "cyan"
|
|
358
|
+
|
|
359
|
+
lines = source.strip().split("\n")
|
|
360
|
+
max_num = len(str(len(lines)))
|
|
361
|
+
print()
|
|
362
|
+
|
|
363
|
+
for i, line in enumerate(lines):
|
|
364
|
+
num = str(i + 1).rjust(max_num)
|
|
365
|
+
prefix = self._style(f" {num} ", color="bright_black")
|
|
366
|
+
|
|
367
|
+
# Basic syntax highlighting
|
|
368
|
+
highlighted = []
|
|
369
|
+
words = line.split(" ")
|
|
370
|
+
for w in words:
|
|
371
|
+
if w in keywords:
|
|
372
|
+
highlighted.append(self._style(w, color=kw_color))
|
|
373
|
+
elif w.startswith("#"):
|
|
374
|
+
highlighted.append(self._style(w, color=comment_color))
|
|
375
|
+
elif w.startswith(('"', "'")):
|
|
376
|
+
highlighted.append(self._style(w, color=strings_color))
|
|
377
|
+
elif w.isdigit():
|
|
378
|
+
highlighted.append(self._style(w, color=num_color))
|
|
379
|
+
elif w.endswith("(") and not w.startswith(('"', "'")):
|
|
380
|
+
highlighted.append(self._style(w, color=func_color))
|
|
381
|
+
else:
|
|
382
|
+
highlighted.append(w)
|
|
383
|
+
|
|
384
|
+
print(f"{prefix}{' '.join(highlighted)}")
|
|
385
|
+
print()
|
|
386
|
+
|
|
387
|
+
# ── Diff ────────────────────────────────────────────────────
|
|
388
|
+
|
|
389
|
+
def diff(self, old: str, new: str, old_label: str = "OLD",
|
|
390
|
+
new_label: str = "NEW") -> None:
|
|
391
|
+
"""Show a colored diff between two strings."""
|
|
392
|
+
import difflib
|
|
393
|
+
old_lines = old.splitlines(keepends=True)
|
|
394
|
+
new_lines = new.splitlines(keepends=True)
|
|
395
|
+
differ = difflib.unified_diff(old_lines, new_lines,
|
|
396
|
+
fromfile=old_label, tofile=new_label)
|
|
397
|
+
print()
|
|
398
|
+
for line in differ:
|
|
399
|
+
line = line.rstrip("\n")
|
|
400
|
+
if line.startswith("+++"):
|
|
401
|
+
print(self._style(f" {line}", color="green"))
|
|
402
|
+
elif line.startswith("---"):
|
|
403
|
+
print(self._style(f" {line}", color="red"))
|
|
404
|
+
elif line.startswith("@@"):
|
|
405
|
+
print(self._style(f" {line}", color="cyan"))
|
|
406
|
+
elif line.startswith("+"):
|
|
407
|
+
print(self._style(f" {line}", color="green"))
|
|
408
|
+
elif line.startswith("-"):
|
|
409
|
+
print(self._style(f" {line}", color="red"))
|
|
410
|
+
else:
|
|
411
|
+
print(f" {self._style(line, color='bright_black')}")
|
|
412
|
+
print()
|
|
413
|
+
|
|
414
|
+
# ── Tree ────────────────────────────────────────────────────
|
|
415
|
+
|
|
416
|
+
def tree(self, path: str = ".", max_depth: int = 3,
|
|
417
|
+
exclude: Optional[List[str]] = None) -> None:
|
|
418
|
+
"""Display a directory tree."""
|
|
419
|
+
if exclude is None:
|
|
420
|
+
exclude = [".git", "__pycache__", ".DS_Store", "node_modules",
|
|
421
|
+
".venv", "venv", ".idea", ".vscode", "*.pyc"]
|
|
422
|
+
|
|
423
|
+
print()
|
|
424
|
+
print(self._style(f" {path}", "bold", color="cyan"))
|
|
425
|
+
|
|
426
|
+
def _match_exclude(name: str) -> bool:
|
|
427
|
+
for pat in exclude:
|
|
428
|
+
if pat.startswith("*"):
|
|
429
|
+
if name.endswith(pat[1:]):
|
|
430
|
+
return True
|
|
431
|
+
elif name == pat:
|
|
432
|
+
return True
|
|
433
|
+
return False
|
|
434
|
+
|
|
435
|
+
def _walk(current: str, prefix: str = "", depth: int = 0):
|
|
436
|
+
if depth >= max_depth:
|
|
437
|
+
return
|
|
438
|
+
try:
|
|
439
|
+
entries = sorted(os.listdir(current))
|
|
440
|
+
except PermissionError:
|
|
441
|
+
print(f"{prefix}{self._style('└── [denied]', color='red')}")
|
|
442
|
+
return
|
|
443
|
+
|
|
444
|
+
entries = [e for e in entries if not _match_exclude(e)]
|
|
445
|
+
for i, entry in enumerate(entries):
|
|
446
|
+
full = os.path.join(current, entry)
|
|
447
|
+
is_last = i == len(entries) - 1
|
|
448
|
+
connector = "└── " if is_last else "├── "
|
|
449
|
+
if os.path.isdir(full):
|
|
450
|
+
icon = _ICONS["folder"]
|
|
451
|
+
print(f"{prefix}{connector}{self._style(icon + ' ' + entry, color='cyan')}")
|
|
452
|
+
next_prefix = prefix + (" " if is_last else "│ ")
|
|
453
|
+
_walk(full, next_prefix, depth + 1)
|
|
454
|
+
else:
|
|
455
|
+
icon = _ICONS["file"]
|
|
456
|
+
size = ""
|
|
457
|
+
try:
|
|
458
|
+
s = os.path.getsize(full)
|
|
459
|
+
if s > 1024 * 1024:
|
|
460
|
+
size = f" ({s / 1024 / 1024:.1f}MB)"
|
|
461
|
+
elif s > 1024:
|
|
462
|
+
size = f" ({s / 1024:.1f}KB)"
|
|
463
|
+
except Exception:
|
|
464
|
+
pass
|
|
465
|
+
print(f"{prefix}{connector}{self._style(icon, color='bright_black')} {entry}{self._style(size, color='bright_black')}")
|
|
466
|
+
|
|
467
|
+
_walk(path)
|
|
468
|
+
print()
|
|
469
|
+
|
|
470
|
+
# ── Interactive ─────────────────────────────────────────────
|
|
471
|
+
|
|
472
|
+
def confirm(self, question: str, default: bool = True) -> bool:
|
|
473
|
+
"""Ask a y/n question and return True/False."""
|
|
474
|
+
hint = "[Y/n]" if default else "[y/N]"
|
|
475
|
+
prompt = f" {_ICONS['question']} {question} {self._style(hint, color='bright_black')}: "
|
|
476
|
+
try:
|
|
477
|
+
answer = input(prompt).strip().lower()
|
|
478
|
+
except (KeyboardInterrupt, EOFError):
|
|
479
|
+
print()
|
|
480
|
+
return False
|
|
481
|
+
if not answer:
|
|
482
|
+
return default
|
|
483
|
+
return answer in ("y", "yes", "yeah", "yep", "sure")
|
|
484
|
+
|
|
485
|
+
def choice(self, question: str, options: List[str]) -> Optional[str]:
|
|
486
|
+
"""Ask user to pick from a list of options."""
|
|
487
|
+
print(f"\n {self._style(_ICONS['question'], color='yellow')} {self._style(question, 'bold')}")
|
|
488
|
+
for i, opt in enumerate(options, 1):
|
|
489
|
+
print(f" {self._style(str(i), color='cyan')}. {opt}")
|
|
490
|
+
try:
|
|
491
|
+
num = input(f" {self._style('Enter number:', color='bright_black')} ")
|
|
492
|
+
idx = int(num) - 1
|
|
493
|
+
if 0 <= idx < len(options):
|
|
494
|
+
return options[idx]
|
|
495
|
+
except (ValueError, KeyboardInterrupt, EOFError):
|
|
496
|
+
pass
|
|
497
|
+
return None
|
|
498
|
+
|
|
499
|
+
# ── Boxed Content ──────────────────────────────────────────
|
|
500
|
+
|
|
501
|
+
def box(self, content: str, title: Optional[str] = None,
|
|
502
|
+
style: str = "round", color: str = "cyan") -> None:
|
|
503
|
+
"""Display content inside a styled box."""
|
|
504
|
+
lines = content.strip().split("\n")
|
|
505
|
+
max_len = max(len(l) for l in lines)
|
|
506
|
+
width = min(max_len + 4, self._width - 4)
|
|
507
|
+
|
|
508
|
+
border = _BORDERS.get(style, _BORDERS["round"])
|
|
509
|
+
h, v, tl, tr, bl, br, l, r, t, b, x = list(border)
|
|
510
|
+
|
|
511
|
+
print()
|
|
512
|
+
if title:
|
|
513
|
+
top = tl + h * 2 + f" {title} " + h * (width - len(title) - 4) + tr
|
|
514
|
+
else:
|
|
515
|
+
top = tl + h * width + tr
|
|
516
|
+
print(self._style(f" {top}", color=color))
|
|
517
|
+
|
|
518
|
+
for line in lines:
|
|
519
|
+
padded = line.ljust(width)
|
|
520
|
+
print(f" {self._style(v, color=color)} {padded} {self._style(v, color=color)}")
|
|
521
|
+
|
|
522
|
+
bottom = bl + h * width + br
|
|
523
|
+
print(self._style(f" {bottom}", color=color))
|
|
524
|
+
print()
|
|
525
|
+
|
|
526
|
+
# ── Metrics / Stats ─────────────────────────────────────────
|
|
527
|
+
|
|
528
|
+
def metrics(self, items: Dict[str, Union[str, int, float]]) -> None:
|
|
529
|
+
"""Display key-value metrics in a compact format."""
|
|
530
|
+
print()
|
|
531
|
+
max_key = max(len(k) for k in items.keys())
|
|
532
|
+
for key, value in items.items():
|
|
533
|
+
k = self._style(f" {key.ljust(max_key)}", color="bright_black")
|
|
534
|
+
if isinstance(value, (int, float)) and value > 0:
|
|
535
|
+
v = self._style(f" {value:,}", "bold", color="green")
|
|
536
|
+
elif isinstance(value, str) and value.startswith("✅"):
|
|
537
|
+
v = self._style(f" {value}", "bold", color="green")
|
|
538
|
+
elif isinstance(value, str) and value.startswith("❌"):
|
|
539
|
+
v = self._style(f" {value}", color="red")
|
|
540
|
+
else:
|
|
541
|
+
v = f" {value}"
|
|
542
|
+
print(f"{k} {self._style('→', color='cyan')}{v}")
|
|
543
|
+
print()
|
|
544
|
+
|
|
545
|
+
# ── Horizontal Rule ─────────────────────────────────────────
|
|
546
|
+
|
|
547
|
+
def hr(self, label: Optional[str] = None) -> None:
|
|
548
|
+
"""Print a horizontal rule, optionally with a label."""
|
|
549
|
+
if label:
|
|
550
|
+
left = "─" * 4
|
|
551
|
+
right = "─" * max(0, self._width - len(label) - 12)
|
|
552
|
+
print(f"\n {self._style(left, color='bright_black')} {self._style(label, 'bold', color='cyan')} {self._style(right, color='bright_black')}")
|
|
553
|
+
else:
|
|
554
|
+
print(f" {self._style('─' * (self._width - 4), color='bright_black')}")
|
|
555
|
+
|
|
556
|
+
@property
|
|
557
|
+
def icons(self):
|
|
558
|
+
"""Access icon constants."""
|
|
559
|
+
return _ICONS
|
shinyshell/banner.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
shinyshell ASCII Banner renderer.
|
|
3
|
+
==================================
|
|
4
|
+
Converts text to large ASCII art banners.
|
|
5
|
+
Uses FIGlet-style rendering with built-in font.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__all__ = ["render", "fonts"]
|
|
9
|
+
|
|
10
|
+
_FONTS = {
|
|
11
|
+
"default": {
|
|
12
|
+
"A": [" █████ ", " ██ ██ ", "█████████", "██ ██", "██ ██"],
|
|
13
|
+
"B": ["███████ ", "██ ██ ", "███████ ", "██ ██ ", "███████ "],
|
|
14
|
+
"C": [" ██████ ", "██ ██ ", "██ ", "██ ██ ", " ██████ "],
|
|
15
|
+
"D": ["███████ ", "██ ██ ", "██ ██ ", "██ ██ ", "███████ "],
|
|
16
|
+
"E": ["████████", "██ ", "██████ ", "██ ", "████████"],
|
|
17
|
+
"F": ["████████", "██ ", "██████ ", "██ ", "██ "],
|
|
18
|
+
"G": [" ██████ ", "██ ", "██ ███ ", "██ ██ ", " ██████ "],
|
|
19
|
+
"H": ["██ ██", "██ ██", "████████", "██ ██", "██ ██"],
|
|
20
|
+
"I": ["████", " ██ ", " ██ ", " ██ ", "████"],
|
|
21
|
+
"J": [" █████", " ██ ", " ██ ", "██ ██ ", " █████ "],
|
|
22
|
+
"K": ["██ ███", "██ ██ ", "█████ ", "██ ██ ", "██ ███"],
|
|
23
|
+
"L": ["██ ", "██ ", "██ ", "██ ", "█████████"],
|
|
24
|
+
"M": ["███ ███", "████ ████", "██ ████ ██", "██ ██ ██", "██ ██"],
|
|
25
|
+
"N": ["███ ██", "████ ██", "██ ██ ██", "██ ██ ██", "██ ████"],
|
|
26
|
+
"O": [" █████ ", " ██ ██ ", "██ ██", " ██ ██ ", " █████ "],
|
|
27
|
+
"P": ["███████ ", "██ ██ ", "███████ ", "██ ", "██ "],
|
|
28
|
+
"Q": [" █████ ", " ██ ██ ", "██ █ ██", " ██ ██ ", " ████ █"],
|
|
29
|
+
"R": ["███████ ", "██ ██ ", "███████ ", "██ ██ ", "██ ███"],
|
|
30
|
+
"S": [" ███████ ", "██ ", " █████ ", " ██", "███████ "],
|
|
31
|
+
"T": ["██████████", " ██ ", " ██ ", " ██ ", " ██ "],
|
|
32
|
+
"U": ["██ ██", "██ ██", "██ ██", "██ ██", " ███████ "],
|
|
33
|
+
"V": ["██ ██", " ██ ██ ", " ██ ██ ", " ███ ", " █ "],
|
|
34
|
+
"W": ["██ ██", "██ ██ ██", "██ ████ ██", " ███ ███ ", " ██ ██ "],
|
|
35
|
+
"X": ["██ ██", " ██ ██ ", " ███ ", " ██ ██ ", "██ ██"],
|
|
36
|
+
"Y": ["██ ██", " ██ ██ ", " █████ ", " ███ ", " ███ "],
|
|
37
|
+
"Z": ["████████", " ███ ", " ███ ", "███ ", "████████"],
|
|
38
|
+
"0": [" █████ ", " ██ ███ ", "██ ██ ██ ", "███ ██ ", " █████ "],
|
|
39
|
+
"1": [" ████", " ██", " ██", " ██", "██████"],
|
|
40
|
+
"2": ["███████ ", " ██", "███████ ", "██ ", "████████"],
|
|
41
|
+
"3": ["██████ ", " ██", " █████ ", " ██", "██████ "],
|
|
42
|
+
"4": ["██ ██", "██ ██", "████████", " ██", " ██"],
|
|
43
|
+
"5": ["███████", "██ ", "███████", " ██", "███████"],
|
|
44
|
+
"6": [" ██████ ", "██ ", "███████ ", "██ ██", " ██████ "],
|
|
45
|
+
"7": ["████████", " ██ ", " ██ ", " ██ ", " ██ "],
|
|
46
|
+
"8": [" █████ ", "██ ██ ", " █████ ", "██ ██ ", " █████ "],
|
|
47
|
+
"9": [" █████ ", "██ ██ ", " ██████ ", " ██ ", " █████ "],
|
|
48
|
+
" ": [" ", " ", " ", " ", " "],
|
|
49
|
+
".": [" ", " ", " ", " ", " ██ "],
|
|
50
|
+
",": [" ", " ", " ", " ██ ", "██ "],
|
|
51
|
+
"!": [" ██ ", " ██ ", " ██ ", " ", " ██ "],
|
|
52
|
+
"?": ["██████ ", " ██", " ████ ", " ", " ██ "],
|
|
53
|
+
"-": [" ", " ", "███████", " ", " "],
|
|
54
|
+
"_": [" ", " ", " ", " ", "████████"],
|
|
55
|
+
":": [" ", " ██ ", " ", " ██ ", " "],
|
|
56
|
+
";": [" ", " ██ ", " ", " ██ ", "██ "],
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def render(text: str, font: str = "default") -> str:
|
|
62
|
+
"""Render text as ASCII art banner."""
|
|
63
|
+
chars = _FONTS.get(font, _FONTS["default"])
|
|
64
|
+
lines = [""] * 5
|
|
65
|
+
for ch in text.upper():
|
|
66
|
+
glyph = chars.get(ch, chars.get(" ", [" "] * 5))
|
|
67
|
+
for i, row in enumerate(glyph):
|
|
68
|
+
lines[i] += row + " "
|
|
69
|
+
return "\n".join(lines)
|
|
70
|
+
|
|
71
|
+
fonts = list(_FONTS.keys())
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: shinyshell
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Beautiful terminal output for Python. Zero dependencies.
|
|
5
|
+
Home-page: https://github.com/adnanahamed66772ndpc/shinyshell
|
|
6
|
+
Author: Adnan Ahamed Himal
|
|
7
|
+
Keywords: terminal,shell,pretty,beautiful,output,cli,print,colors
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Classifier: Topic :: Terminals
|
|
19
|
+
Classifier: Environment :: Console
|
|
20
|
+
Requires-Python: >=3.8
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Dynamic: author
|
|
24
|
+
Dynamic: classifier
|
|
25
|
+
Dynamic: description
|
|
26
|
+
Dynamic: description-content-type
|
|
27
|
+
Dynamic: home-page
|
|
28
|
+
Dynamic: keywords
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
Dynamic: requires-python
|
|
31
|
+
Dynamic: summary
|
|
32
|
+
|
|
33
|
+
# ✨ shinyshell
|
|
34
|
+
|
|
35
|
+
**Beautiful terminal output for Python. Zero dependencies. One import.**
|
|
36
|
+
|
|
37
|
+
[](https://pypi.org/project/shinyshell/)
|
|
38
|
+
[](https://pypi.org/project/shinyshell/)
|
|
39
|
+
[](LICENSE)
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from shinyshell import Shell
|
|
43
|
+
|
|
44
|
+
sh = Shell()
|
|
45
|
+
|
|
46
|
+
sh.success("Deployed to production!")
|
|
47
|
+
sh.table(users, title="Team Members")
|
|
48
|
+
sh.progress("Installing deps...")
|
|
49
|
+
sh.header("My CLI App")
|
|
50
|
+
sh.code("print('hello')")
|
|
51
|
+
sh.countdown(5, "Launching")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 📸 Demo
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from shinyshell import Shell
|
|
60
|
+
sh = Shell()
|
|
61
|
+
|
|
62
|
+
# Headers & banners
|
|
63
|
+
sh.header("DEPLOYMENT REPORT")
|
|
64
|
+
sh.banner("SHINY SHELL")
|
|
65
|
+
|
|
66
|
+
# Success / Warning / Error / Info
|
|
67
|
+
sh.success("Database migrated successfully")
|
|
68
|
+
sh.warning("Rate limit approaching — 93% used")
|
|
69
|
+
sh.error("Connection to API failed after 3 retries")
|
|
70
|
+
sh.info("Server running on http://localhost:8000")
|
|
71
|
+
|
|
72
|
+
# Beautiful tables
|
|
73
|
+
sh.table([
|
|
74
|
+
{"Name": "Alice Chen", "Role": "Backend Dev", "Status": "✅ Active"},
|
|
75
|
+
{"Name": "Bob Kumar", "Role": "Frontend", "Status": "✅ Active"},
|
|
76
|
+
{"Name": "Charlie D.", "Role": "Design Lead", "Status": "⏳ On Leave"},
|
|
77
|
+
], title="Team Status")
|
|
78
|
+
|
|
79
|
+
# Progress with spinner
|
|
80
|
+
sh.spinner("Compiling assets...")
|
|
81
|
+
update = sh.progress("Uploading")
|
|
82
|
+
for i in range(100):
|
|
83
|
+
update(i + 1, 100)
|
|
84
|
+
|
|
85
|
+
# Countdown
|
|
86
|
+
sh.countdown(5, "Deploying to production")
|
|
87
|
+
|
|
88
|
+
# Boxed content
|
|
89
|
+
sh.box("API Key: sk-****abcd\nEndpoint: /v1/chat\nModel: gpt-4", title="Configuration")
|
|
90
|
+
|
|
91
|
+
# Directory tree
|
|
92
|
+
sh.tree("./src", max_depth=2)
|
|
93
|
+
|
|
94
|
+
# Code blocks
|
|
95
|
+
sh.code("""
|
|
96
|
+
def fibonacci(n):
|
|
97
|
+
if n <= 1:
|
|
98
|
+
return n
|
|
99
|
+
return fibonacci(n-1) + fibonacci(n-2)
|
|
100
|
+
""")
|
|
101
|
+
|
|
102
|
+
# Metrics dashboard
|
|
103
|
+
sh.metrics({
|
|
104
|
+
"Users": 12483,
|
|
105
|
+
"Active Now": 342,
|
|
106
|
+
"Uptime": "✅ 99.9%",
|
|
107
|
+
"Error Rate": "0.02%",
|
|
108
|
+
"Avg Response": "23ms",
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
# Colored diff
|
|
112
|
+
sh.diff("hello world", "hello beautiful world")
|
|
113
|
+
|
|
114
|
+
# Interactive
|
|
115
|
+
if sh.confirm("Deploy to production?"):
|
|
116
|
+
sh.success("Ship it! 🚀")
|
|
117
|
+
|
|
118
|
+
# Horizontal rule
|
|
119
|
+
sh.hr("Section 2")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## 📦 Install
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
pip install shinyshell
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
No dependencies. Uses only Python standard library. Works on **Linux**, **macOS**, and **Windows** (PowerShell, CMD, Windows Terminal).
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## 🎯 Features
|
|
135
|
+
|
|
136
|
+
| Feature | Description |
|
|
137
|
+
|---------|-------------|
|
|
138
|
+
| `success()` | ✅ Green success messages |
|
|
139
|
+
| `warning()` | ⚠️ Yellow warnings |
|
|
140
|
+
| `error()` | ❌ Red errors |
|
|
141
|
+
| `info()` | ℹ️ Cyan info |
|
|
142
|
+
| `header()` | Styled section headers |
|
|
143
|
+
| `banner()` | ASCII art banners |
|
|
144
|
+
| `table()` | Beautiful data tables |
|
|
145
|
+
| `box()` | Content in styled boxes |
|
|
146
|
+
| `code()` | Syntax-highlighted code blocks |
|
|
147
|
+
| `diff()` | Colored git diff |
|
|
148
|
+
| `tree()` | Directory trees |
|
|
149
|
+
| `metrics()` | Key-value dashboards |
|
|
150
|
+
| `spinner()` | Animated loading spinner |
|
|
151
|
+
| `progress()` | Progress bars |
|
|
152
|
+
| `countdown()` | Animated countdown |
|
|
153
|
+
| `confirm()` | Interactive y/n prompts |
|
|
154
|
+
| `choice()` | Interactive option picker |
|
|
155
|
+
| `hr()` | Horizontal rules with labels |
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## 🤔 Why?
|
|
160
|
+
|
|
161
|
+
Every Python script prints to the terminal. Most output looks boring. **shinyshell** makes it beautiful — with zero dependencies.
|
|
162
|
+
|
|
163
|
+
Before:
|
|
164
|
+
```
|
|
165
|
+
Task completed
|
|
166
|
+
Users: 1042
|
|
167
|
+
ERROR: Connection failed
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
After:
|
|
171
|
+
```
|
|
172
|
+
✨ Task completed
|
|
173
|
+
📊 Users: 1,042
|
|
174
|
+
💥 Connection failed
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## 🔧 Advanced
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
# Custom width
|
|
183
|
+
sh = Shell(width=80)
|
|
184
|
+
|
|
185
|
+
# Disable color
|
|
186
|
+
sh = Shell(color=False)
|
|
187
|
+
|
|
188
|
+
# Access icons
|
|
189
|
+
print(sh.icons["rocket"]) # 🚀
|
|
190
|
+
|
|
191
|
+
# Chaining
|
|
192
|
+
sh.success("Step 1 done").progress("Step 2...")
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## 📄 License
|
|
198
|
+
|
|
199
|
+
MIT — © 2026 Adnan Ahamed Himal
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## ⭐ Support
|
|
204
|
+
|
|
205
|
+
Found this useful? **Star this repo** and share it with your team!
|
|
206
|
+
|
|
207
|
+
[⬆ Back to top](#-shinyshell)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
shinyshell/__init__.py,sha256=IbZ-tAWmo5DIKID77wUye5j-XRKSlQAKts46CqU9ITI,21669
|
|
2
|
+
shinyshell/banner.py,sha256=JLth6P3pz9hktx1HklOipJOhIJANMfP4NlDmM7UNofM,5668
|
|
3
|
+
shinyshell-0.1.0.dist-info/licenses/LICENSE,sha256=PWu9BGjvowDdNQpJWh474xai09MaIkSN1VF8CzF7yaE,1075
|
|
4
|
+
shinyshell-0.1.0.dist-info/METADATA,sha256=i8e6J4HyKfJ-x4UIC6SDTVBV7wacehyYoUVIcyxGwI8,4922
|
|
5
|
+
shinyshell-0.1.0.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
|
|
6
|
+
shinyshell-0.1.0.dist-info/top_level.txt,sha256=aDs3y4Zb6GqRX71sj-UoaBKnALl0gzdRSgVmXyrAY7w,11
|
|
7
|
+
shinyshell-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adnan Ahamed Himal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
shinyshell
|