richer-tui 0.1.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.
- richer_tui-0.1.0/PKG-INFO +871 -0
- richer_tui-0.1.0/README.md +861 -0
- richer_tui-0.1.0/pyproject.toml +15 -0
- richer_tui-0.1.0/richer/__init__.py +71 -0
- richer_tui-0.1.0/richer/bars.py +262 -0
- richer_tui-0.1.0/richer/colors.py +212 -0
- richer_tui-0.1.0/richer/filetree.py +294 -0
- richer_tui-0.1.0/richer/graphs.py +610 -0
- richer_tui-0.1.0/richer/live.py +230 -0
- richer_tui-0.1.0/richer/loadscreen.py +337 -0
- richer_tui-0.1.0/richer/logger.py +664 -0
- richer_tui-0.1.0/richer/loghandler.py +96 -0
- richer_tui-0.1.0/richer/markdown.py +251 -0
- richer_tui-0.1.0/richer/markup.py +210 -0
- richer_tui-0.1.0/richer/prompt.py +216 -0
- richer_tui-0.1.0/richer/screen.py +227 -0
- richer_tui-0.1.0/richer/spinners.py +196 -0
- richer_tui-0.1.0/richer/styles.py +243 -0
- richer_tui-0.1.0/richer/syntax.py +389 -0
- richer_tui-0.1.0/richer/text.py +272 -0
- richer_tui-0.1.0/richer/themes.py +182 -0
- richer_tui-0.1.0/richer_tui.egg-info/PKG-INFO +871 -0
- richer_tui-0.1.0/richer_tui.egg-info/SOURCES.txt +24 -0
- richer_tui-0.1.0/richer_tui.egg-info/dependency_links.txt +1 -0
- richer_tui-0.1.0/richer_tui.egg-info/top_level.txt +1 -0
- richer_tui-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,871 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: richer-tui
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Zero-dependency Python TUI toolkit — true-color logging, progress bars, graphs, live dashboards, syntax highlighting, and more
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/uqhf/richer
|
|
7
|
+
Keywords: tui,terminal,cli,logging,ansi,color,progress
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# richer
|
|
12
|
+
|
|
13
|
+
Zero-dependency Python TUI toolkit. True-color ANSI rendering, logging, progress bars, spinners, graphs, live dashboards, syntax highlighting, markdown, markup language, prompts, file trees, themes -- all from scratch, no `rich`, no `curses`, no `pygments`.
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
pip install richer-tui
|
|
17
|
+
python -m richer.demo # live preview of everything
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Requires Python 3.10+, a terminal with true-color support (any modern terminal on Windows/macOS/Linux).
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Table of Contents
|
|
25
|
+
|
|
26
|
+
1. [Color & Gradient](#1-color--gradient)
|
|
27
|
+
2. [Style & Layout](#2-style--layout)
|
|
28
|
+
3. [LogKit -- structured logger](#3-logkit----structured-logger)
|
|
29
|
+
4. [Spinners](#4-spinners)
|
|
30
|
+
5. [Progress Bars](#5-progress-bars)
|
|
31
|
+
6. [Graphs](#6-graphs)
|
|
32
|
+
7. [Markup Language](#7-markup-language)
|
|
33
|
+
8. [Syntax Highlighting](#8-syntax-highlighting)
|
|
34
|
+
9. [Live & Dashboard](#9-live--dashboard)
|
|
35
|
+
10. [Themes](#10-themes)
|
|
36
|
+
11. [Screen Control](#11-screen-control)
|
|
37
|
+
12. [Prompt](#12-prompt)
|
|
38
|
+
13. [Logging Bridge](#13-logging-bridge)
|
|
39
|
+
14. [Text Object](#14-text-object)
|
|
40
|
+
15. [File Tree](#15-file-tree)
|
|
41
|
+
16. [Markdown Renderer](#16-markdown-renderer)
|
|
42
|
+
17. [Loading Screen](#17-loading-screen)
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 1. Color & Gradient
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from richer import Color, Gradient
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### `Color` -- ANSI escape helpers
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
# 16-color constants
|
|
56
|
+
Color.RED # "\033[31m"
|
|
57
|
+
Color.BRIGHT_WHITE # "\033[97m"
|
|
58
|
+
Color.BG_BLUE # "\033[44m"
|
|
59
|
+
Color.RESET # "\033[0m"
|
|
60
|
+
|
|
61
|
+
# True-color (24-bit)
|
|
62
|
+
Color.rgb(88, 101, 242) # foreground escape
|
|
63
|
+
Color.bg_rgb(30, 30, 40) # background escape
|
|
64
|
+
Color.from_hex("#5865F2") # hex string -> rgb escape
|
|
65
|
+
|
|
66
|
+
# 256-color
|
|
67
|
+
Color.color256(196)
|
|
68
|
+
Color.bg_color256(52)
|
|
69
|
+
|
|
70
|
+
# Gradient text
|
|
71
|
+
Color.gradient_text("hello", (255, 80, 0), (255, 220, 0))
|
|
72
|
+
|
|
73
|
+
# Multi-stop gradient -- stops are (r,g,b) tuples
|
|
74
|
+
Color.multi_gradient("loading...", (88,101,242), (0,200,255), (0,255,150))
|
|
75
|
+
|
|
76
|
+
# Full HSV rainbow rotation
|
|
77
|
+
Color.rainbow("spectral")
|
|
78
|
+
|
|
79
|
+
# Utility
|
|
80
|
+
Color.blend((255,0,0), (0,0,255), 0.5) # lerp -> (127, 0, 127)
|
|
81
|
+
Color.pulse((88,101,242), t) # sinusoidal brightness, t in [0,1]
|
|
82
|
+
Color.dim_rgb((200,200,200), factor=0.4) # darken tuple -> rgb escape
|
|
83
|
+
Color.strip("text with \033[31mcodes\033[0m") # strip all ANSI
|
|
84
|
+
|
|
85
|
+
# Convenience wrappers
|
|
86
|
+
Color.bold("text")
|
|
87
|
+
Color.italic("text")
|
|
88
|
+
Color.underline("text")
|
|
89
|
+
Color.strike("text")
|
|
90
|
+
Color.dim_text("text")
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### `Gradient` -- named stop lists
|
|
94
|
+
|
|
95
|
+
Every preset is a `list[tuple[int,int,int]]` passed to `Color.multi_gradient`.
|
|
96
|
+
|
|
97
|
+
| Name | Visual |
|
|
98
|
+
|-----------|--------|
|
|
99
|
+
| `FIRE` | red -> orange -> yellow |
|
|
100
|
+
| `OCEAN` | deep navy -> cyan |
|
|
101
|
+
| `NEON` | pink -> violet -> cyan |
|
|
102
|
+
| `MATRIX` | dark green -> bright green |
|
|
103
|
+
| `GOLD` | amber -> bright gold -> amber |
|
|
104
|
+
| `BLOOD` | dark red -> bright red |
|
|
105
|
+
| `CYBER` | teal -> blue -> violet |
|
|
106
|
+
| `DISCORD` | discord blurple loop |
|
|
107
|
+
| `CANDY` | pink -> yellow -> light blue |
|
|
108
|
+
| `TOXIC` | lime -> yellow-green |
|
|
109
|
+
| `VOID` | deep purple -> violet |
|
|
110
|
+
| `LAVA` | red -> orange -> yellow |
|
|
111
|
+
| `SUNSET` | orange -> crimson -> indigo |
|
|
112
|
+
| `ARCTIC` | ice blue -> white-blue |
|
|
113
|
+
| `RUST` | dark brown -> orange |
|
|
114
|
+
| `ACID` | lime -> bright green |
|
|
115
|
+
| `GHOST` | cool grey loop |
|
|
116
|
+
| `INFRA` | deep blue -> purple -> red -> yellow |
|
|
117
|
+
| `ROYAL` | purple -> lavender loop |
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
# Build from hex strings
|
|
121
|
+
stops = Gradient.from_hex("#ff0080", "#8000ff", "#00c8ff")
|
|
122
|
+
|
|
123
|
+
# Expand stop list to n evenly-spaced (r,g,b) tuples
|
|
124
|
+
pts = Gradient.interpolate(Gradient.FIRE, 256)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## 2. Style & Layout
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from richer import Style, BorderStyle, render_panel, render_table, render_two_col, render_header, render_badge, strip_ansi, term_width
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### `Style` -- ANSI text attributes
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
Style.BOLD, Style.DIM, Style.ITALIC, Style.UNDERLINE, Style.BLINK, Style.STRIKE, Style.RESET
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### `BorderStyle` -- box-drawing presets
|
|
142
|
+
|
|
143
|
+
`SINGLE`, `DOUBLE`, `ROUNDED`, `BOLD`, `DASHED`, `ASCII`
|
|
144
|
+
|
|
145
|
+
### Layout functions
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
# Bordered panel -- returns string
|
|
149
|
+
render_panel(
|
|
150
|
+
lines, # list[str]
|
|
151
|
+
title=None,
|
|
152
|
+
border=BorderStyle.ROUNDED,
|
|
153
|
+
border_color=Color.rgb(88,101,242),
|
|
154
|
+
title_color=Color.BRIGHT_WHITE,
|
|
155
|
+
padding=1,
|
|
156
|
+
width=None,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Table -- returns string
|
|
160
|
+
render_table(
|
|
161
|
+
headers, # list[str]
|
|
162
|
+
rows, # list[list[str]]
|
|
163
|
+
border=BorderStyle.SINGLE,
|
|
164
|
+
header_color=...,
|
|
165
|
+
border_color=...,
|
|
166
|
+
col_colors=None, # list[str] per column
|
|
167
|
+
zebra=False,
|
|
168
|
+
align=None, # list["left"|"right"|"center"]
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Two-column layout -- returns string
|
|
172
|
+
render_two_col(left_lines, right_lines, gap=4)
|
|
173
|
+
|
|
174
|
+
# Centered header bar -- returns string
|
|
175
|
+
render_header(text, gradient=None, width=None)
|
|
176
|
+
|
|
177
|
+
# Small badge pill -- returns string
|
|
178
|
+
render_badge(text, color=None)
|
|
179
|
+
|
|
180
|
+
# Strip ANSI escapes
|
|
181
|
+
strip_ansi(s)
|
|
182
|
+
|
|
183
|
+
# Terminal width (falls back to 80)
|
|
184
|
+
term_width()
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## 3. LogKit -- structured logger
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
from richer import LogKit, Level
|
|
193
|
+
|
|
194
|
+
log = LogKit(
|
|
195
|
+
name="app",
|
|
196
|
+
min_level=Level.DEBUG,
|
|
197
|
+
show_time=True,
|
|
198
|
+
show_name=True,
|
|
199
|
+
name_gradient=Gradient.DISCORD,
|
|
200
|
+
time_color=Color.BRIGHT_BLACK,
|
|
201
|
+
file=sys.stdout,
|
|
202
|
+
thread_safe=False,
|
|
203
|
+
history=False,
|
|
204
|
+
history_maxlen=500,
|
|
205
|
+
)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Log levels
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
log.debug("probe sent")
|
|
212
|
+
log.info("handshake complete")
|
|
213
|
+
log.success("payload injected")
|
|
214
|
+
log.warning("retry 3/5")
|
|
215
|
+
log.error("connection refused")
|
|
216
|
+
log.critical("watchdog timeout")
|
|
217
|
+
|
|
218
|
+
# Custom color override
|
|
219
|
+
log.info("custom", color=Color.rgb(200, 100, 255))
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`Level` constants: `DEBUG=0`, `INFO=1`, `SUCCESS=2`, `WARNING=3`, `ERROR=4`, `CRITICAL=5`
|
|
223
|
+
|
|
224
|
+
### Display methods
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
log.banner("SYSTEM COMPROMISED", gradient=Gradient.BLOOD, border=BorderStyle.DOUBLE)
|
|
228
|
+
|
|
229
|
+
log.rule("section header", gradient=Gradient.NEON)
|
|
230
|
+
log.rule()
|
|
231
|
+
log.divider()
|
|
232
|
+
|
|
233
|
+
log.section("phase 2", gradient=Gradient.CYBER)
|
|
234
|
+
|
|
235
|
+
log.panel(["line 1", "line 2"], title="status", border=BorderStyle.ROUNDED)
|
|
236
|
+
|
|
237
|
+
log.table(
|
|
238
|
+
["host", "status", "ip"],
|
|
239
|
+
[["web01", "up", "10.0.0.1"], ["db02", "down", "10.0.0.2"]],
|
|
240
|
+
zebra=True,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
log.kv([("pid", "1234"), ("arch", "x64")], sep=" -> ")
|
|
244
|
+
log.keyval("target", "192.168.1.100")
|
|
245
|
+
|
|
246
|
+
log.columns(["item1","item2","item3","item4"], ncols=2, headers=["col A","col B"])
|
|
247
|
+
|
|
248
|
+
log.callout("Heap base leaked at 0x7fff00001000", style="note")
|
|
249
|
+
# styles: "note", "tip", "warn", "danger", "info"
|
|
250
|
+
|
|
251
|
+
log.badge("STABLE", color=Color.rgb(50,220,100))
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Data inspection
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
log.tree({"a": {"b": [1, 2, 3]}, "c": "val"})
|
|
258
|
+
|
|
259
|
+
log.json({"status": "ok", "code": 200, "data": None})
|
|
260
|
+
|
|
261
|
+
log.hex_dump(bytes(range(64)), width=16, highlight=[0x00, 0xff])
|
|
262
|
+
|
|
263
|
+
log.diff(old_str, new_str, label_a="before", label_b="after")
|
|
264
|
+
|
|
265
|
+
log.inspect(obj, show_private=False, show_methods=False)
|
|
266
|
+
|
|
267
|
+
log.traceback() # current exception
|
|
268
|
+
log.traceback(exc) # specific exception object
|
|
269
|
+
log.multiline(["line1", "line2"], level=Level.INFO)
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Progress & metrics
|
|
273
|
+
|
|
274
|
+
```python
|
|
275
|
+
# Inline progress bar (overwrites current line)
|
|
276
|
+
for i in range(total + 1):
|
|
277
|
+
log.progress_bar(i, total, label="downloading", show_eta=True, start_time=t0)
|
|
278
|
+
|
|
279
|
+
# Inline counter
|
|
280
|
+
log.counter("packets", current, total, bar=True, bar_width=20)
|
|
281
|
+
|
|
282
|
+
# Status line (overwritten by next log call)
|
|
283
|
+
log.status("waiting for beacon...")
|
|
284
|
+
log.status_clear()
|
|
285
|
+
|
|
286
|
+
# Sparkline
|
|
287
|
+
log.sparkline([10, 25, 18, 40, 35, 55], label="rtt", gradient=Gradient.NEON)
|
|
288
|
+
|
|
289
|
+
# Rate and elapsed
|
|
290
|
+
log.rate("req", count=1024, elapsed=2.5, unit="/s")
|
|
291
|
+
log.elapsed(start_time, label="total time")
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Integration methods
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
log.syntax(code, lang="python", line_numbers=True, title="exploit.py")
|
|
298
|
+
log.markdown(md_text)
|
|
299
|
+
log.markup("[fire]armed[/fire] and [bold green]ready[/bold green]")
|
|
300
|
+
log.file_tree("./src", max_depth=3, show_size=True)
|
|
301
|
+
log.theme("dracula")
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### History
|
|
305
|
+
|
|
306
|
+
```python
|
|
307
|
+
log = LogKit("app", history=True)
|
|
308
|
+
log.info("boot")
|
|
309
|
+
records = log._hist.all() # [{"level": 1, "msg": "boot", "ts": "..."}]
|
|
310
|
+
log._hist.clear()
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## 4. Spinners
|
|
316
|
+
|
|
317
|
+
```python
|
|
318
|
+
from richer import Spinner, SpinnerGroup, SPINNER_FRAMES
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### `Spinner`
|
|
322
|
+
|
|
323
|
+
```python
|
|
324
|
+
s = Spinner("connecting to C2", style="dots")
|
|
325
|
+
s.start()
|
|
326
|
+
# ... work ...
|
|
327
|
+
s.stop("connected")
|
|
328
|
+
|
|
329
|
+
# Context manager
|
|
330
|
+
with Spinner("encrypting payload"):
|
|
331
|
+
encrypt(data)
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### `SpinnerGroup`
|
|
335
|
+
|
|
336
|
+
```python
|
|
337
|
+
sg = SpinnerGroup([
|
|
338
|
+
"stage 1 -- recon",
|
|
339
|
+
"stage 2 -- exploit",
|
|
340
|
+
"stage 3 -- persist",
|
|
341
|
+
])
|
|
342
|
+
sg.start()
|
|
343
|
+
time.sleep(1)
|
|
344
|
+
sg.done(0)
|
|
345
|
+
time.sleep(1)
|
|
346
|
+
sg.done(1)
|
|
347
|
+
sg.stop()
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## 5. Progress Bars
|
|
353
|
+
|
|
354
|
+
```python
|
|
355
|
+
from richer import ProgressBar, MultiBar, ThreadedBar, RateBar, BAR_STYLES
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### `ProgressBar`
|
|
359
|
+
|
|
360
|
+
```python
|
|
361
|
+
b = ProgressBar(
|
|
362
|
+
total=100,
|
|
363
|
+
width=40,
|
|
364
|
+
style="block",
|
|
365
|
+
fill_gradient=Gradient.FIRE,
|
|
366
|
+
prefix="download ",
|
|
367
|
+
show_count=True,
|
|
368
|
+
show_pct=True,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
for i in range(101):
|
|
372
|
+
b.update(i)
|
|
373
|
+
time.sleep(0.02)
|
|
374
|
+
print()
|
|
375
|
+
|
|
376
|
+
# Iterable wrapper
|
|
377
|
+
for item in b(my_list):
|
|
378
|
+
process(item)
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### `MultiBar`
|
|
382
|
+
|
|
383
|
+
```python
|
|
384
|
+
mb = MultiBar([
|
|
385
|
+
("loader", 40, Gradient.DISCORD),
|
|
386
|
+
("encoder", 60, Gradient.MATRIX),
|
|
387
|
+
("packer", 30, Gradient.FIRE),
|
|
388
|
+
], width=35)
|
|
389
|
+
|
|
390
|
+
vals = [0, 0, 0]
|
|
391
|
+
for i in range(60):
|
|
392
|
+
vals[0] = min(i + 1, 40)
|
|
393
|
+
vals[1] = i + 1
|
|
394
|
+
vals[2] = min(i + 1, 30)
|
|
395
|
+
mb.render_all(vals)
|
|
396
|
+
time.sleep(0.03)
|
|
397
|
+
print()
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### `ThreadedBar`
|
|
401
|
+
|
|
402
|
+
```python
|
|
403
|
+
tb = ThreadedBar(total=100, interval=0.05, prefix="upload ", fill_gradient=Gradient.OCEAN)
|
|
404
|
+
tb.start()
|
|
405
|
+
for chunk in chunks:
|
|
406
|
+
upload(chunk)
|
|
407
|
+
tb.inc()
|
|
408
|
+
tb.stop()
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
## 6. Graphs
|
|
414
|
+
|
|
415
|
+
```python
|
|
416
|
+
from richer import (
|
|
417
|
+
bar_chart, sparkline, heatmap_row, column_chart,
|
|
418
|
+
line_graph, scatter, gauge, timeline,
|
|
419
|
+
stacked_bar_chart, pie_chart,
|
|
420
|
+
)
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
All graph functions return strings -- `print()` them or embed in panels.
|
|
424
|
+
|
|
425
|
+
```python
|
|
426
|
+
# Horizontal bar chart
|
|
427
|
+
print(bar_chart(
|
|
428
|
+
[92, 78, 45, 61],
|
|
429
|
+
labels=["RCE", "LPE", "SQLi", "XSS"],
|
|
430
|
+
title="severity",
|
|
431
|
+
gradient=Gradient.BLOOD,
|
|
432
|
+
width=35,
|
|
433
|
+
))
|
|
434
|
+
|
|
435
|
+
# Inline sparkline
|
|
436
|
+
s = sparkline([10, 25, 18, 40, 35, 55], gradient=Gradient.NEON)
|
|
437
|
+
|
|
438
|
+
# Heatmap row
|
|
439
|
+
print(heatmap_row([0.1, 0.5, 0.9, 0.3], labels=["M","T","W","T"]))
|
|
440
|
+
|
|
441
|
+
# Vertical column chart
|
|
442
|
+
print(column_chart([30, 80, 50, 90], labels=["Q1","Q2","Q3","Q4"], height=10))
|
|
443
|
+
|
|
444
|
+
# Multi-series line graph
|
|
445
|
+
# NOTE: colors must be (r,g,b) tuples, NOT ANSI escape strings
|
|
446
|
+
print(line_graph(
|
|
447
|
+
[[10,25,18,40], [5,15,30,20]],
|
|
448
|
+
labels=["in", "out"],
|
|
449
|
+
colors=[(88,101,242), (237,66,69)],
|
|
450
|
+
height=8, width=50,
|
|
451
|
+
))
|
|
452
|
+
|
|
453
|
+
# Scatter plot
|
|
454
|
+
pts = [(x, y), ...]
|
|
455
|
+
print(scatter(pts, width=50, height=16, gradient=Gradient.NEON))
|
|
456
|
+
|
|
457
|
+
# Radial gauge
|
|
458
|
+
print(gauge(0.73, label="CPU", gradient=Gradient.LAVA))
|
|
459
|
+
|
|
460
|
+
# Timeline
|
|
461
|
+
events = [("boot", 0), ("exploit", 3), ("shell", 7), ("exfil", 12)]
|
|
462
|
+
print(timeline(events, width=60))
|
|
463
|
+
|
|
464
|
+
# Stacked bar chart
|
|
465
|
+
print(stacked_bar_chart(
|
|
466
|
+
series=[[30,50,20], [40,35,25]],
|
|
467
|
+
labels=["host1", "host2"],
|
|
468
|
+
series_labels=["user", "kernel", "idle"],
|
|
469
|
+
colors=[(88,101,242),(59,165,93),(250,166,26)],
|
|
470
|
+
title="CPU breakdown",
|
|
471
|
+
width=40,
|
|
472
|
+
))
|
|
473
|
+
|
|
474
|
+
# Pie chart
|
|
475
|
+
print(pie_chart(
|
|
476
|
+
data=[40, 30, 20, 10],
|
|
477
|
+
labels=["RCE", "LPE", "DoS", "Info"],
|
|
478
|
+
title="vuln classes",
|
|
479
|
+
show_pct=True,
|
|
480
|
+
))
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
---
|
|
484
|
+
|
|
485
|
+
## 7. Markup Language
|
|
486
|
+
|
|
487
|
+
```python
|
|
488
|
+
from richer import markup_render, markup_strip, mprint, mformat
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
### Tags
|
|
492
|
+
|
|
493
|
+
```
|
|
494
|
+
[red]text[/red] [green] [blue] [yellow] [cyan] [magenta] [white] [dim]
|
|
495
|
+
[bold]text[/bold] [italic] [underline] [strike] [blink]
|
|
496
|
+
[rgb(88,101,242)]text[/rgb]
|
|
497
|
+
[#5865f2]text[/#5865f2]
|
|
498
|
+
[bg_rgb(30,30,40)]text[/bg_rgb]
|
|
499
|
+
|
|
500
|
+
# Combined
|
|
501
|
+
[bold red]critical alert[/bold red]
|
|
502
|
+
|
|
503
|
+
# Named gradients
|
|
504
|
+
[fire]armed[/fire] [neon] [matrix] [ocean] [blood] [cyber] [discord] [candy]
|
|
505
|
+
[rainbow]full spectrum[/rainbow]
|
|
506
|
+
|
|
507
|
+
# Hyperlink
|
|
508
|
+
[link=https://example.com]click here[/link]
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
```python
|
|
512
|
+
s = markup_render("[bold red]ALERT[/bold red]: [neon]payload staged[/neon]")
|
|
513
|
+
plain = markup_strip("[bold]text[/bold]") # -> "text"
|
|
514
|
+
mprint("[fire]system compromised[/fire]")
|
|
515
|
+
mprint(mformat("[bold]{host}[/bold] responded in {ms}ms", host="10.0.0.1", ms=42))
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
---
|
|
519
|
+
|
|
520
|
+
## 8. Syntax Highlighting
|
|
521
|
+
|
|
522
|
+
```python
|
|
523
|
+
from richer import highlight, highlight_auto, detect_lang
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
Regex-based tokenizer, zero external deps.
|
|
527
|
+
|
|
528
|
+
**Supported languages:** `python`, `json`, `c`, `cpp`, `js`, `javascript`, `sh`, `bash`, `shell`, `yaml`, `sql`, `html`, `css`, `rust`, `go`
|
|
529
|
+
|
|
530
|
+
```python
|
|
531
|
+
code = "def exploit(buf): return buf[:8] + p64(0xdeadbeef)"
|
|
532
|
+
print(highlight(code, lang="python", line_numbers=True))
|
|
533
|
+
|
|
534
|
+
print(highlight_auto(code, filename="exploit.py", line_numbers=True))
|
|
535
|
+
|
|
536
|
+
lang = detect_lang(code, filename="main.rs") # -> "rust"
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
---
|
|
540
|
+
|
|
541
|
+
## 9. Live & Dashboard
|
|
542
|
+
|
|
543
|
+
```python
|
|
544
|
+
from richer import Live, Dashboard, LogPane
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
### `Live`
|
|
548
|
+
|
|
549
|
+
```python
|
|
550
|
+
data = {"count": 0}
|
|
551
|
+
|
|
552
|
+
def render():
|
|
553
|
+
return f"packets: {data['count']}"
|
|
554
|
+
|
|
555
|
+
with Live(render, refresh_rate=10, transient=False):
|
|
556
|
+
for i in range(100):
|
|
557
|
+
data["count"] = i
|
|
558
|
+
time.sleep(0.05)
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
- `content` -- string or `callable() -> str`
|
|
562
|
+
- `refresh_rate` -- redraws per second
|
|
563
|
+
- `transient=True` -- clears the block on exit
|
|
564
|
+
|
|
565
|
+
### `Dashboard`
|
|
566
|
+
|
|
567
|
+
```python
|
|
568
|
+
dash = Dashboard()
|
|
569
|
+
dash.row("header", height=3)
|
|
570
|
+
dash.cols(["left", "right"], heights=[20, 20])
|
|
571
|
+
dash.row("footer", height=1)
|
|
572
|
+
|
|
573
|
+
dash["header"] = render_panel(["STATUS BOARD"], border=BorderStyle.DOUBLE)
|
|
574
|
+
dash["left"] = "cell content left"
|
|
575
|
+
dash["right"] = "cell content right"
|
|
576
|
+
|
|
577
|
+
with Live(dash.render, refresh_rate=4):
|
|
578
|
+
for t in range(50):
|
|
579
|
+
dash["footer"] = f"tick {t}"
|
|
580
|
+
time.sleep(0.1)
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
### `LogPane`
|
|
584
|
+
|
|
585
|
+
```python
|
|
586
|
+
pane = LogPane(height=10)
|
|
587
|
+
pane.push("[OK] boot complete")
|
|
588
|
+
pane.push("[ERR] write failed")
|
|
589
|
+
|
|
590
|
+
with Live(pane.render, refresh_rate=5):
|
|
591
|
+
for event in event_stream():
|
|
592
|
+
pane.push(format_event(event))
|
|
593
|
+
time.sleep(0.05)
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
---
|
|
597
|
+
|
|
598
|
+
## 10. Themes
|
|
599
|
+
|
|
600
|
+
```python
|
|
601
|
+
from richer import theme_apply, theme_color, theme_names, theme_info
|
|
602
|
+
from richer import DISCORD, HACKER, DRACULA, NORD, MONOKAI, SOLARIZED, CYBERPUNK, BLOOD
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
**Available themes:** `discord`, `hacker`, `dracula`, `nord`, `monokai`, `solarized`, `cyberpunk`, `blood`
|
|
606
|
+
|
|
607
|
+
```python
|
|
608
|
+
theme_apply("dracula")
|
|
609
|
+
|
|
610
|
+
# Semantic color keys: "primary", "secondary", "success", "warning",
|
|
611
|
+
# "error", "info", "dim", "bright", "bg" (tuple), "gradient" (stop list)
|
|
612
|
+
c = theme_color("error") # ANSI escape string
|
|
613
|
+
bg = theme_color("bg") # (r,g,b) tuple
|
|
614
|
+
grad = theme_color("gradient") # gradient stop list
|
|
615
|
+
|
|
616
|
+
names = theme_names()
|
|
617
|
+
info = theme_info("monokai")
|
|
618
|
+
info = theme_info() # active theme
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
---
|
|
622
|
+
|
|
623
|
+
## 11. Screen Control
|
|
624
|
+
|
|
625
|
+
```python
|
|
626
|
+
from richer import Screen, ScreenWriter
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
```python
|
|
630
|
+
Screen.move(row, col)
|
|
631
|
+
Screen.clear()
|
|
632
|
+
Screen.hide_cursor()
|
|
633
|
+
Screen.show_cursor()
|
|
634
|
+
Screen.set_title("my app")
|
|
635
|
+
Screen.bell()
|
|
636
|
+
|
|
637
|
+
w, h = Screen.size()
|
|
638
|
+
|
|
639
|
+
Screen.draw_box(row, col, width, height, border=BorderStyle.SINGLE)
|
|
640
|
+
Screen.fill_rect(row, col, width, height, char=" ", color=None)
|
|
641
|
+
Screen.hyperlink(text, url)
|
|
642
|
+
Screen.notify(title, body)
|
|
643
|
+
|
|
644
|
+
# Context managers
|
|
645
|
+
with Screen.fullscreen:
|
|
646
|
+
...
|
|
647
|
+
|
|
648
|
+
with Screen.cursor_hidden:
|
|
649
|
+
...
|
|
650
|
+
|
|
651
|
+
with Screen.at(5, 10):
|
|
652
|
+
print("drawn at row 5 col 10")
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
```python
|
|
656
|
+
sw = ScreenWriter()
|
|
657
|
+
sw.at(3, 5, f"{Color.rgb(88,101,242)}hello{Color.RESET}")
|
|
658
|
+
sw.flush()
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
---
|
|
662
|
+
|
|
663
|
+
## 12. Prompt
|
|
664
|
+
|
|
665
|
+
```python
|
|
666
|
+
from richer import Prompt
|
|
667
|
+
|
|
668
|
+
name = Prompt.ask("target hostname", default="localhost", hint="e.g. 10.0.0.1")
|
|
669
|
+
confirmed = Prompt.confirm("continue?", default=True)
|
|
670
|
+
choice = Prompt.choose("select payload", choices=["revshell", "bind", "meter"])
|
|
671
|
+
selected = Prompt.choose("select modules", choices=[...], multi=True)
|
|
672
|
+
key = Prompt.secret("encryption key", confirm=True)
|
|
673
|
+
port = Prompt.integer("port", min_val=1, max_val=65535, default=4444)
|
|
674
|
+
timeout = Prompt.float_("timeout seconds", default=5.0)
|
|
675
|
+
path = Prompt.path("config file", must_exist=True)
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
---
|
|
679
|
+
|
|
680
|
+
## 13. Logging Bridge
|
|
681
|
+
|
|
682
|
+
```python
|
|
683
|
+
from richer import LogKitHandler, setup_logging, suppress_loggers
|
|
684
|
+
import logging
|
|
685
|
+
|
|
686
|
+
log = LogKit("app")
|
|
687
|
+
|
|
688
|
+
# Route all logging.* calls through log
|
|
689
|
+
setup_logging(log, level=logging.DEBUG, capture_warnings=True)
|
|
690
|
+
|
|
691
|
+
# Specific loggers only
|
|
692
|
+
setup_logging(log, loggers=["httpx", "asyncio"])
|
|
693
|
+
|
|
694
|
+
# Silence noisy loggers
|
|
695
|
+
suppress_loggers("urllib3", "PIL")
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
---
|
|
699
|
+
|
|
700
|
+
## 14. Text Object
|
|
701
|
+
|
|
702
|
+
```python
|
|
703
|
+
from richer import Text
|
|
704
|
+
|
|
705
|
+
t = Text()
|
|
706
|
+
t.append("status: ", "bold")
|
|
707
|
+
t.append("ONLINE", "rgb(50,220,100)")
|
|
708
|
+
t.append_markup("[fire]armed[/fire]")
|
|
709
|
+
|
|
710
|
+
print(t.render()) # ANSI string
|
|
711
|
+
print(t.plain) # stripped text
|
|
712
|
+
print(t.width) # visual width
|
|
713
|
+
|
|
714
|
+
t.truncate(40, overflow="...")
|
|
715
|
+
lines = t.wrap(60)
|
|
716
|
+
t.justify("center", 80)
|
|
717
|
+
t.highlight_substr("ONLINE", "bold yellow")
|
|
718
|
+
|
|
719
|
+
# Class methods
|
|
720
|
+
t = Text.assemble(("label: ", "dim"), ("value", "bold green"))
|
|
721
|
+
t = Text.from_markup("[bold red]ALERT[/bold red]")
|
|
722
|
+
t = Text.gradient("spectral", (255,0,128), (0,200,255))
|
|
723
|
+
t = Text.rule(title="section", width=60, char="-")
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
---
|
|
727
|
+
|
|
728
|
+
## 15. File Tree
|
|
729
|
+
|
|
730
|
+
```python
|
|
731
|
+
from richer import render_file_tree, print_file_tree
|
|
732
|
+
|
|
733
|
+
tree_str = render_file_tree(
|
|
734
|
+
path="./src",
|
|
735
|
+
max_depth=4,
|
|
736
|
+
show_hidden=False,
|
|
737
|
+
show_size=True,
|
|
738
|
+
dir_only=False,
|
|
739
|
+
sort_dirs_first=True,
|
|
740
|
+
summary=True,
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
print_file_tree("./src", max_depth=3, show_size=True)
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
60+ extension color mappings: `.py` (blue), `.rs` (orange), `.c`/`.cpp` (cyan), `.sh` (green), `.json`/`.yaml` (yellow), `.md` (white), `.env` (red), images (magenta), binaries (bright red), and more.
|
|
747
|
+
|
|
748
|
+
---
|
|
749
|
+
|
|
750
|
+
## 16. Markdown Renderer
|
|
751
|
+
|
|
752
|
+
```python
|
|
753
|
+
from richer import markdown_render, md_print
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
GFM renderer, zero deps.
|
|
757
|
+
|
|
758
|
+
**Supported:** headings h1-h6, bold/italic/strikethrough, inline code, links, blockquotes, unordered/ordered lists (3 nesting levels), GFM tables with alignment, fenced code blocks (syntax highlighted), horizontal rules.
|
|
759
|
+
|
|
760
|
+
```python
|
|
761
|
+
md = """
|
|
762
|
+
# Exploitation Report
|
|
763
|
+
|
|
764
|
+
Target responded to **CVE-2024-XXXX** with a *9.8 CVSS* score.
|
|
765
|
+
|
|
766
|
+
```python
|
|
767
|
+
payload = b"\\x00" * 8 + p64(win_addr)
|
|
768
|
+
sock.send(payload)
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
| Component | Status | Severity |
|
|
772
|
+
|-----------|--------|----------|
|
|
773
|
+
| web | pwned | critical |
|
|
774
|
+
"""
|
|
775
|
+
|
|
776
|
+
md_print(md)
|
|
777
|
+
s = markdown_render(md) # -> ANSI string
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
---
|
|
781
|
+
|
|
782
|
+
## 17. Loading Screen
|
|
783
|
+
|
|
784
|
+
```python
|
|
785
|
+
from richer import loading_screen, G_BOOT, G_READY, G_PULSE
|
|
786
|
+
|
|
787
|
+
loading_screen(
|
|
788
|
+
title="IMPLANT LOADER v2.0",
|
|
789
|
+
steps=[
|
|
790
|
+
"initializing crypto",
|
|
791
|
+
"probing target",
|
|
792
|
+
"establishing tunnel",
|
|
793
|
+
"loading modules",
|
|
794
|
+
],
|
|
795
|
+
gradient=G_BOOT,
|
|
796
|
+
step_delay=0.8,
|
|
797
|
+
final_msg="ready",
|
|
798
|
+
)
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
---
|
|
802
|
+
|
|
803
|
+
## Quick-start
|
|
804
|
+
|
|
805
|
+
```python
|
|
806
|
+
from richer import LogKit, Color, Gradient, Spinner, ProgressBar, bar_chart, mprint, theme_apply
|
|
807
|
+
import time
|
|
808
|
+
|
|
809
|
+
theme_apply("dracula")
|
|
810
|
+
|
|
811
|
+
log = LogKit("demo", show_time=True)
|
|
812
|
+
log.banner("RICHER DEMO", gradient=Gradient.NEON)
|
|
813
|
+
log.info("starting up")
|
|
814
|
+
log.success("connected to 10.0.0.1:4444")
|
|
815
|
+
log.warning("retry 2/5 -- timeout")
|
|
816
|
+
log.error("module load failed: access denied")
|
|
817
|
+
|
|
818
|
+
log.kv([
|
|
819
|
+
("pid", "31337"),
|
|
820
|
+
("arch", "x64"),
|
|
821
|
+
("priv", "SYSTEM"),
|
|
822
|
+
("os", "Windows 11 22H2"),
|
|
823
|
+
])
|
|
824
|
+
|
|
825
|
+
with Spinner("staging payload", style="dots"):
|
|
826
|
+
time.sleep(1.5)
|
|
827
|
+
|
|
828
|
+
b = ProgressBar(total=100, width=35, fill_gradient=Gradient.FIRE, prefix="upload ")
|
|
829
|
+
for i in range(101):
|
|
830
|
+
b.update(i)
|
|
831
|
+
time.sleep(0.01)
|
|
832
|
+
print()
|
|
833
|
+
|
|
834
|
+
print(bar_chart([92,78,45,61], labels=["RCE","LPE","SQLi","XSS"], gradient=Gradient.BLOOD))
|
|
835
|
+
mprint("[fire]armed[/fire] and [bold green]ready[/bold green]")
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
---
|
|
839
|
+
|
|
840
|
+
## Project layout
|
|
841
|
+
|
|
842
|
+
```
|
|
843
|
+
richer/
|
|
844
|
+
|-- __init__.py exports
|
|
845
|
+
|-- colors.py Color, Gradient
|
|
846
|
+
|-- styles.py Style, BorderStyle, layout helpers
|
|
847
|
+
|-- logger.py LogKit, Level
|
|
848
|
+
|-- spinners.py Spinner, SpinnerGroup
|
|
849
|
+
|-- bars.py ProgressBar, MultiBar, ThreadedBar, RateBar
|
|
850
|
+
|-- graphs.py bar_chart, line_graph, scatter, pie_chart, ...
|
|
851
|
+
|-- markup.py tag markup language
|
|
852
|
+
|-- syntax.py regex-based syntax highlighter
|
|
853
|
+
|-- live.py Live, Dashboard, LogPane
|
|
854
|
+
|-- themes.py 8 named themes
|
|
855
|
+
|-- screen.py Screen, ScreenWriter
|
|
856
|
+
|-- prompt.py Prompt.*
|
|
857
|
+
|-- loghandler.py stdlib logging bridge
|
|
858
|
+
|-- text.py Text object
|
|
859
|
+
|-- filetree.py render_file_tree
|
|
860
|
+
|-- markdown.py GFM markdown renderer
|
|
861
|
+
|-- loadscreen.py animated boot screen
|
|
862
|
+
|
|
863
|
+
examples/
|
|
864
|
+
|-- demo.py runnable feature showcase
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
---
|
|
868
|
+
|
|
869
|
+
## License
|
|
870
|
+
|
|
871
|
+
MIT
|