curint 0.1.2__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.
curint/__init__.py ADDED
@@ -0,0 +1,101 @@
1
+ from __future__ import annotations
2
+
3
+ from .animations import (
4
+ bounce,
5
+ bounce_frames,
6
+ glitch,
7
+ glitch_frames,
8
+ print_animated,
9
+ print_bounce,
10
+ print_wave,
11
+ shimmer,
12
+ shimmer_frames,
13
+ typewriter,
14
+ wave,
15
+ wave_frames,
16
+ )
17
+ from .colors import PALETTES, RGB, ansi_bg, ansi_fg, blend_many, parse_color, strip_ansi
18
+ from .console import SpectraConsole, debug, error, info, success, warning
19
+ from .logging import SpectraLogHandler, configure_logging
20
+ from .progress import Spinner, SpinnerRich, progress_bar, progress_bar_rich, rich_status, track_iter
21
+ from .prompt import ask, choose, confirm
22
+ from .text import (
23
+ box,
24
+ center,
25
+ color_text,
26
+ cprint,
27
+ gradient_text,
28
+ highlight,
29
+ palette_text,
30
+ print_color,
31
+ print_gradient,
32
+ print_gradient_rich,
33
+ print_rainbow,
34
+ rainbow_text,
35
+ sky_gradient,
36
+ style_text,
37
+ termcolor_text,
38
+ )
39
+ from .theme import THEMES, Theme, get_theme, register_theme, theme_names, use_theme
40
+
41
+ __all__ = [
42
+ "PALETTES",
43
+ "RGB",
44
+ "THEMES",
45
+ "SpectraConsole",
46
+ "SpectraLogHandler",
47
+ "Spinner",
48
+ "SpinnerRich",
49
+ "Theme",
50
+ "ansi_bg",
51
+ "ansi_fg",
52
+ "ask",
53
+ "blend_many",
54
+ "bounce",
55
+ "bounce_frames",
56
+ "box",
57
+ "center",
58
+ "choose",
59
+ "color_text",
60
+ "configure_logging",
61
+ "confirm",
62
+ "cprint",
63
+ "debug",
64
+ "error",
65
+ "get_theme",
66
+ "glitch",
67
+ "glitch_frames",
68
+ "gradient_text",
69
+ "highlight",
70
+ "info",
71
+ "palette_text",
72
+ "parse_color",
73
+ "print_animated",
74
+ "print_bounce",
75
+ "print_color",
76
+ "print_gradient",
77
+ "print_gradient_rich",
78
+ "print_rainbow",
79
+ "print_wave",
80
+ "progress_bar",
81
+ "progress_bar_rich",
82
+ "rainbow_text",
83
+ "register_theme",
84
+ "rich_status",
85
+ "shimmer",
86
+ "shimmer_frames",
87
+ "sky_gradient",
88
+ "strip_ansi",
89
+ "style_text",
90
+ "success",
91
+ "termcolor_text",
92
+ "theme_names",
93
+ "track_iter",
94
+ "typewriter",
95
+ "use_theme",
96
+ "warning",
97
+ "wave",
98
+ "wave_frames",
99
+ ]
100
+
101
+ __version__ = "0.1.2"
curint/_compat.py ADDED
@@ -0,0 +1,46 @@
1
+ """Compatibility shims for optional imports during local development.
2
+
3
+ The package declares its core dependencies in setup.py. The fallbacks below
4
+ let tests and docs render in constrained environments before installation.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ try: # pragma: no cover - exercised when dependency is installed
12
+ from colorama import Back, Fore, Style, just_fix_windows_console
13
+ except Exception: # pragma: no cover - fallback for source tree tests
14
+ class _Codes:
15
+ BLACK = RED = GREEN = YELLOW = BLUE = MAGENTA = CYAN = WHITE = RESET = RESET_ALL = ""
16
+ BRIGHT = DIM = NORMAL = ""
17
+
18
+ Fore = Back = Style = _Codes() # type: ignore[assignment]
19
+
20
+ def just_fix_windows_console() -> None:
21
+ return None
22
+
23
+ try: # pragma: no cover
24
+ from termcolor import colored
25
+ except Exception: # pragma: no cover
26
+ def colored(text: str, color: str | None = None, on_color: str | None = None, attrs: list[str] | None = None) -> str:
27
+ return text
28
+
29
+ try: # pragma: no cover
30
+ from rich.console import Console
31
+ from rich.markdown import Markdown
32
+ from rich.panel import Panel
33
+ from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn, TimeElapsedColumn, track
34
+ from rich.table import Table
35
+ from rich.text import Text
36
+ from rich.tree import Tree
37
+ except Exception as exc: # pragma: no cover
38
+ raise RuntimeError("curint requires rich. Install with `pip install curint`.") from exc
39
+
40
+ try: # pragma: no cover
41
+ import skytext as _skytext
42
+ except Exception: # pragma: no cover
43
+ _skytext = None
44
+
45
+
46
+ SKYTEXT: Any = _skytext
curint/animations.py ADDED
@@ -0,0 +1,109 @@
1
+ """Animated terminal text helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import random
6
+ import sys
7
+ import time
8
+ from collections.abc import Iterator, Sequence
9
+
10
+ from ._compat import Console
11
+ from .colors import strip_ansi
12
+ from .text import gradient_text, rainbow_text, style_text
13
+
14
+ console = Console()
15
+
16
+
17
+ def typewriter(text: str, *, delay: float = 0.03, color: str | Sequence[int] | None = None, end: str = "\n") -> None:
18
+ """Print text one character at a time."""
19
+
20
+ styled = style_text(text, fg=color) if color else text
21
+ for char in styled:
22
+ sys.stdout.write(char)
23
+ sys.stdout.flush()
24
+ time.sleep(delay)
25
+ sys.stdout.write(end)
26
+ sys.stdout.flush()
27
+
28
+
29
+ def wave_frames(text: str, *, height: int = 2, cycles: int = 1) -> Iterator[str]:
30
+ """Generate simple vertical wave frames for testing or playback."""
31
+
32
+ height = max(1, height)
33
+ clean = strip_ansi(text)
34
+ for offset in range(max(1, cycles * len(clean))):
35
+ lines = [[" " for _ in clean] for _ in range(height + 1)]
36
+ for index, char in enumerate(clean):
37
+ row = abs((index + offset) % (height * 2) - height)
38
+ lines[row][index] = char
39
+ yield "\n".join("".join(line).rstrip() for line in lines)
40
+
41
+
42
+ def wave(text: str, *, color: str | Sequence[int] | None = None, height: int = 2, cycles: int = 2, delay: float = 0.08) -> None:
43
+ for frame in wave_frames(text, height=height, cycles=cycles):
44
+ console.clear()
45
+ console.print(style_text(frame, fg=color) if color else frame, markup=False)
46
+ time.sleep(delay)
47
+
48
+
49
+ def bounce_frames(text: str, *, width: int = 40, cycles: int = 2) -> Iterator[str]:
50
+ clean = strip_ansi(text)
51
+ width = max(len(clean), width)
52
+ span = width - len(clean)
53
+ path = list(range(span + 1)) + list(range(max(0, span - 1), 0, -1))
54
+ for _ in range(cycles):
55
+ for pos in path or [0]:
56
+ yield " " * pos + clean
57
+
58
+
59
+ def bounce(text: str, *, color: str | Sequence[int] | None = None, width: int = 40, cycles: int = 2, delay: float = 0.05) -> None:
60
+ for frame in bounce_frames(text, width=width, cycles=cycles):
61
+ sys.stdout.write("\r" + style_text(frame, fg=color) if color else "\r" + frame)
62
+ sys.stdout.flush()
63
+ time.sleep(delay)
64
+ sys.stdout.write("\n")
65
+
66
+
67
+ def shimmer_frames(text: str, *, cycles: int = 1) -> Iterator[str]:
68
+ clean = strip_ansi(text)
69
+ for phase in range(max(1, len(clean) * cycles)):
70
+ pieces: list[str] = []
71
+ for index, char in enumerate(clean):
72
+ if char.isspace():
73
+ pieces.append(char)
74
+ elif index == phase % len(clean):
75
+ pieces.append(style_text(char, fg="#ffffff", bold=True))
76
+ else:
77
+ pieces.append(style_text(char, fg="#6272a4"))
78
+ yield "".join(pieces)
79
+
80
+
81
+ def shimmer(text: str, *, cycles: int = 2, delay: float = 0.04) -> None:
82
+ for frame in shimmer_frames(text, cycles=cycles):
83
+ sys.stdout.write("\r" + frame)
84
+ sys.stdout.flush()
85
+ time.sleep(delay)
86
+ sys.stdout.write("\n")
87
+
88
+
89
+ def glitch_frames(text: str, *, intensity: float = 0.12, frames: int = 8, charset: str = "!@#$%^&*<>?/\\|~") -> Iterator[str]:
90
+ clean = strip_ansi(text)
91
+ intensity = min(1.0, max(0.0, intensity))
92
+ for _ in range(max(1, frames)):
93
+ chars = [random.choice(charset) if char.strip() and random.random() < intensity else char for char in clean]
94
+ yield rainbow_text("".join(chars), bold=True)
95
+ yield clean
96
+
97
+
98
+ def glitch(text: str, *, intensity: float = 0.12, frames: int = 8, delay: float = 0.05) -> None:
99
+ for frame in glitch_frames(text, intensity=intensity, frames=frames):
100
+ sys.stdout.write("\r" + frame)
101
+ sys.stdout.flush()
102
+ time.sleep(delay)
103
+ sys.stdout.write("\n")
104
+
105
+
106
+ # Convenience aliases.
107
+ print_animated = typewriter
108
+ print_wave = wave
109
+ print_bounce = bounce
curint/colors.py ADDED
@@ -0,0 +1,152 @@
1
+ """Color parsing and ANSI helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ import re
7
+ from dataclasses import dataclass
8
+ from typing import Iterable, Iterator, Sequence
9
+
10
+ RGBTuple = tuple[int, int, int]
11
+
12
+ RESET = "\033[0m"
13
+ ANSI_RE = re.compile(r"\x1b\[[0-9;]*m")
14
+
15
+ CSS_COLORS: dict[str, RGBTuple] = {
16
+ "black": (0, 0, 0),
17
+ "red": (255, 85, 85),
18
+ "green": (80, 250, 123),
19
+ "yellow": (241, 250, 140),
20
+ "blue": (98, 114, 164),
21
+ "magenta": (255, 121, 198),
22
+ "cyan": (139, 233, 253),
23
+ "white": (248, 248, 242),
24
+ "orange": (255, 184, 108),
25
+ "purple": (189, 147, 249),
26
+ "pink": (255, 121, 198),
27
+ "gray": (128, 128, 128),
28
+ "grey": (128, 128, 128),
29
+ }
30
+
31
+ PALETTES: dict[str, list[RGBTuple]] = {
32
+ "rainbow": [(255, 0, 0), (255, 127, 0), (255, 255, 0), (0, 255, 0), (0, 170, 255), (75, 0, 130), (148, 0, 211)],
33
+ "sunset": [(255, 94, 98), (255, 153, 102), (255, 206, 84), (106, 90, 205)],
34
+ "ocean": [(0, 180, 219), (0, 131, 176), (72, 85, 99)],
35
+ "neon": [(57, 255, 20), (0, 255, 255), (255, 0, 255), (255, 255, 0)],
36
+ "fire": [(255, 0, 0), (255, 120, 0), (255, 255, 0), (255, 255, 255)],
37
+ }
38
+
39
+ @dataclass(frozen=True)
40
+ class RGB:
41
+ """An immutable RGB color."""
42
+
43
+ r: int
44
+ g: int
45
+ b: int
46
+
47
+ def __post_init__(self) -> None:
48
+ for channel in (self.r, self.g, self.b):
49
+ if channel < 0 or channel > 255:
50
+ raise ValueError("RGB channels must be between 0 and 255")
51
+
52
+ @property
53
+ def tuple(self) -> RGBTuple:
54
+ return (self.r, self.g, self.b)
55
+
56
+ @property
57
+ def hex(self) -> str:
58
+ return f"#{self.r:02x}{self.g:02x}{self.b:02x}"
59
+
60
+ def fg(self) -> str:
61
+ return ansi_fg(self.tuple)
62
+
63
+ def bg(self) -> str:
64
+ return ansi_bg(self.tuple)
65
+
66
+
67
+ def clamp(value: float, minimum: int = 0, maximum: int = 255) -> int:
68
+ return max(minimum, min(maximum, int(round(value))))
69
+
70
+
71
+ def parse_color(color: str | Sequence[int] | RGB) -> RGBTuple:
72
+ """Parse a color name, hex value, RGB tuple/list, or :class:`RGB`."""
73
+
74
+ if isinstance(color, RGB):
75
+ return color.tuple
76
+ if isinstance(color, str):
77
+ value = color.strip().lower()
78
+ if value in CSS_COLORS:
79
+ return CSS_COLORS[value]
80
+ if value.startswith("#"):
81
+ value = value[1:]
82
+ if len(value) == 3 and all(ch in "0123456789abcdef" for ch in value):
83
+ return tuple(int(ch * 2, 16) for ch in value) # type: ignore[return-value]
84
+ if len(value) == 6 and all(ch in "0123456789abcdef" for ch in value):
85
+ return (int(value[0:2], 16), int(value[2:4], 16), int(value[4:6], 16))
86
+ raise ValueError(f"Unknown color: {color!r}")
87
+ if len(color) != 3:
88
+ raise ValueError("RGB colors need exactly three channels")
89
+ rgb = tuple(int(channel) for channel in color)
90
+ if not all(0 <= channel <= 255 for channel in rgb):
91
+ raise ValueError("RGB channels must be between 0 and 255")
92
+ return rgb # type: ignore[return-value]
93
+
94
+
95
+ def ansi_fg(color: str | Sequence[int] | RGB) -> str:
96
+ r, g, b = parse_color(color)
97
+ return f"\033[38;2;{r};{g};{b}m"
98
+
99
+
100
+ def ansi_bg(color: str | Sequence[int] | RGB) -> str:
101
+ r, g, b = parse_color(color)
102
+ return f"\033[48;2;{r};{g};{b}m"
103
+
104
+
105
+ def strip_ansi(text: str) -> str:
106
+ return ANSI_RE.sub("", text)
107
+
108
+
109
+ def lerp(start: RGBTuple, end: RGBTuple, t: float) -> RGBTuple:
110
+ return tuple(clamp(start[i] + (end[i] - start[i]) * t) for i in range(3)) # type: ignore[return-value]
111
+
112
+
113
+ def blend_many(colors: Sequence[str | Sequence[int] | RGB], steps: int) -> list[RGBTuple]:
114
+ """Return ``steps`` colors interpolated through all stops."""
115
+
116
+ if steps <= 0:
117
+ return []
118
+ stops = [parse_color(color) for color in colors]
119
+ if len(stops) == 1:
120
+ return stops * steps
121
+ if steps == 1:
122
+ return [stops[0]]
123
+
124
+ result: list[RGBTuple] = []
125
+ segments = len(stops) - 1
126
+ for index in range(steps):
127
+ pos = index / (steps - 1)
128
+ segment = min(segments - 1, int(pos * segments))
129
+ local_start = segment / segments
130
+ local_end = (segment + 1) / segments
131
+ local_t = 0 if local_end == local_start else (pos - local_start) / (local_end - local_start)
132
+ result.append(lerp(stops[segment], stops[segment + 1], local_t))
133
+ return result
134
+
135
+
136
+ def rainbow_color(index: int, total: int, phase: float = 0.0) -> RGBTuple:
137
+ """Return a smooth rainbow RGB value for ``index`` in ``total``."""
138
+
139
+ if total <= 0:
140
+ return (255, 255, 255)
141
+ angle = 2 * math.pi * ((index / max(total, 1)) + phase)
142
+ r = clamp(math.sin(angle) * 127 + 128)
143
+ g = clamp(math.sin(angle + 2 * math.pi / 3) * 127 + 128)
144
+ b = clamp(math.sin(angle + 4 * math.pi / 3) * 127 + 128)
145
+ return (r, g, b)
146
+
147
+
148
+ def cycle_palette(name: str, length: int) -> Iterator[RGBTuple]:
149
+ palette = PALETTES.get(name, PALETTES["rainbow"])
150
+ if length <= 0:
151
+ return iter(())
152
+ return iter(blend_many(palette, length))
curint/console.py ADDED
@@ -0,0 +1,143 @@
1
+ """Rich-powered console wrappers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from collections.abc import Iterable, Mapping, Sequence
7
+ from typing import Any
8
+
9
+ from ._compat import Console, Markdown, Panel, Table, Tree
10
+ from .colors import parse_color
11
+ from .text import gradient_text, rainbow_text, style_text
12
+ from .theme import Theme, get_theme
13
+
14
+
15
+ def _rich_color(color: str | Sequence[int] | None) -> str | None:
16
+ if color is None:
17
+ return None
18
+ if isinstance(color, str) and not color.startswith("#"):
19
+ try:
20
+ r, g, b = parse_color(color)
21
+ return f"#{r:02x}{g:02x}{b:02x}"
22
+ except ValueError:
23
+ return color
24
+ r, g, b = parse_color(color)
25
+ return f"#{r:02x}{g:02x}{b:02x}"
26
+
27
+
28
+ class SpectraConsole:
29
+ """Convenience wrapper around :class:`rich.console.Console`."""
30
+
31
+ def __init__(self, *, theme: str | Theme | None = None, console: Console | None = None) -> None:
32
+ self.theme = get_theme(theme) if isinstance(theme, str) or theme is None else theme
33
+ self.console = console or Console()
34
+
35
+ def print(self, value: Any = "", *, style: str | None = None, markup: bool = True) -> None:
36
+ self.console.print(value, style=style, markup=markup)
37
+
38
+ def gradient(self, text: str, start: str | Sequence[int] | None = None, end: str | Sequence[int] | None = None, *, bold: bool = False) -> None:
39
+ self.console.print(gradient_text(text, start or self.theme.primary, end or self.theme.secondary, bold=bold), markup=False)
40
+
41
+ def rainbow(self, text: str, *, bold: bool = False) -> None:
42
+ self.console.print(rainbow_text(text, bold=bold), markup=False)
43
+
44
+ def banner(self, text: str, *, width: int | None = None, gradient: bool = True) -> str:
45
+ """Print a compact text banner without external font dependencies."""
46
+
47
+ title = f" {text.strip()} " if text.strip() else " "
48
+ line_width = max(len(title), width or len(title))
49
+ rendered = "═" * line_width + "\n" + title.center(line_width) + "\n" + "═" * line_width
50
+ output = gradient_text(rendered, self.theme.primary, self.theme.secondary, bold=True) if gradient else rendered
51
+ self.console.print(output, markup=False)
52
+ return output
53
+
54
+ def panel(self, text: str, *, title: str | None = None, border: str | Sequence[int] | None = None) -> None:
55
+ border_style = _rich_color(border or self.theme.primary)
56
+ self.console.print(Panel(text, title=title, border_style=border_style))
57
+
58
+ def rule(self, title: str = "", *, color: str | Sequence[int] | None = None) -> None:
59
+ self.console.rule(title, style=_rich_color(color or self.theme.primary))
60
+
61
+ def markdown(self, source: str) -> None:
62
+ self.console.print(Markdown(source))
63
+
64
+ def json(self, data: Any, *, indent: int = 2) -> None:
65
+ self.console.print_json(json.dumps(data, indent=indent, default=str))
66
+
67
+ def table(
68
+ self,
69
+ rows: Iterable[Mapping[str, Any] | Sequence[Any]],
70
+ *,
71
+ headers: Sequence[str] | None = None,
72
+ title: str | None = None,
73
+ ) -> Table:
74
+ rows = list(rows)
75
+ if headers is None:
76
+ first = rows[0] if rows else []
77
+ headers = list(first.keys()) if isinstance(first, Mapping) else [f"Column {i + 1}" for i in range(len(first))]
78
+ table = Table(title=title, header_style=_rich_color(self.theme.primary) or "bold")
79
+ for header in headers:
80
+ table.add_column(str(header))
81
+ for row in rows:
82
+ if isinstance(row, Mapping):
83
+ values = [row.get(header, "") for header in headers]
84
+ else:
85
+ values = list(row)
86
+ table.add_row(*(str(value) for value in values))
87
+ self.console.print(table)
88
+ return table
89
+
90
+ def tree(self, root: str, branches: Mapping[str, Any]) -> Tree:
91
+ tree = Tree(root)
92
+
93
+ def add(parent: Tree, value: Any) -> None:
94
+ if isinstance(value, Mapping):
95
+ for key, child in value.items():
96
+ node = parent.add(str(key))
97
+ add(node, child)
98
+ elif isinstance(value, (list, tuple, set)):
99
+ for item in value:
100
+ add(parent, item)
101
+ else:
102
+ parent.add(str(value))
103
+
104
+ add(tree, branches)
105
+ self.console.print(tree)
106
+ return tree
107
+
108
+ def status(self, text: str, kind: str = "info") -> None:
109
+ icon = {
110
+ "success": "✓",
111
+ "error": "✖",
112
+ "warning": "⚠",
113
+ "info": "ℹ",
114
+ "debug": "•",
115
+ }.get(kind, "•")
116
+ color = getattr(self.theme, kind, self.theme.info)
117
+ self.console.print(style_text(f"{icon} {text}", fg=color, bold=True), markup=False)
118
+
119
+ def capture_gradient(self, text: str, start: str | Sequence[int] | None = None, end: str | Sequence[int] | None = None) -> str:
120
+ return gradient_text(text, start or self.theme.primary, end or self.theme.secondary)
121
+
122
+
123
+ _default_console = SpectraConsole()
124
+
125
+
126
+ def success(text: str) -> None:
127
+ _default_console.status(text, "success")
128
+
129
+
130
+ def error(text: str) -> None:
131
+ _default_console.status(text, "error")
132
+
133
+
134
+ def warning(text: str) -> None:
135
+ _default_console.status(text, "warning")
136
+
137
+
138
+ def info(text: str) -> None:
139
+ _default_console.status(text, "info")
140
+
141
+
142
+ def debug(text: str) -> None:
143
+ _default_console.status(text, "debug")
curint/logging.py ADDED
@@ -0,0 +1,47 @@
1
+ """Logging helpers that use Rich output."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import TextIO
7
+
8
+ from ._compat import Console
9
+ from .theme import get_theme
10
+
11
+
12
+ class SpectraLogHandler(logging.Handler):
13
+ """A compact, colorful logging handler powered by Rich."""
14
+
15
+ LEVEL_STYLES = {
16
+ logging.DEBUG: "dim",
17
+ logging.INFO: "cyan",
18
+ logging.WARNING: "yellow",
19
+ logging.ERROR: "bold red",
20
+ logging.CRITICAL: "bold white on red",
21
+ }
22
+
23
+ def __init__(self, *, console: Console | None = None, level: int = logging.NOTSET) -> None:
24
+ super().__init__(level=level)
25
+ self.console = console or Console(stderr=True)
26
+ self.setFormatter(logging.Formatter("%(message)s"))
27
+
28
+ def emit(self, record: logging.LogRecord) -> None:
29
+ try:
30
+ message = self.format(record)
31
+ style = self.LEVEL_STYLES.get(record.levelno, "white")
32
+ self.console.print(f"[{record.levelname.lower()}] {message}", style=style)
33
+ except Exception: # pragma: no cover
34
+ self.handleError(record)
35
+
36
+
37
+ def configure_logging(level: int | str = logging.INFO, *, logger_name: str | None = None) -> logging.Logger:
38
+ """Attach a :class:`SpectraLogHandler` and return the configured logger."""
39
+
40
+ logger = logging.getLogger(logger_name)
41
+ numeric_level = logging.getLevelName(level) if isinstance(level, str) else level
42
+ if not isinstance(numeric_level, int):
43
+ numeric_level = logging.INFO
44
+ logger.setLevel(numeric_level)
45
+ logger.handlers[:] = [SpectraLogHandler(level=numeric_level)]
46
+ logger.propagate = False
47
+ return logger
curint/progress.py ADDED
@@ -0,0 +1,107 @@
1
+ """Progress bars, spinners, and task tracking."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import itertools
7
+ import sys
8
+ import threading
9
+ import time
10
+ from collections.abc import Iterable, Iterator
11
+ from typing import TypeVar
12
+
13
+ from ._compat import BarColumn, Console, Progress, SpinnerColumn, TextColumn, TimeElapsedColumn, track
14
+
15
+ T = TypeVar("T")
16
+
17
+ SPINNER_STYLES: dict[str, tuple[str, ...]] = {
18
+ "dots": ("⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"),
19
+ "line": ("-", "\\", "|", "/"),
20
+ "circle": ("◐", "◓", "◑", "◒"),
21
+ "moon": ("🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"),
22
+ "bounce": ("⠁", "⠂", "⠄", "⠂"),
23
+ }
24
+
25
+
26
+ def progress_bar(total: int = 100, description: str = "Working", *, delay: float = 0.02, transient: bool = False) -> None:
27
+ """Display a Rich progress bar that advances from 0 to ``total``."""
28
+
29
+ total = max(0, int(total))
30
+ with Progress(
31
+ SpinnerColumn(),
32
+ TextColumn("[progress.description]{task.description}"),
33
+ BarColumn(),
34
+ TextColumn("{task.percentage:>3.0f}%"),
35
+ TimeElapsedColumn(),
36
+ transient=transient,
37
+ ) as progress:
38
+ task = progress.add_task(description, total=total)
39
+ while not progress.finished:
40
+ progress.advance(task)
41
+ time.sleep(delay)
42
+
43
+
44
+ class Spinner:
45
+ """Threaded terminal spinner.
46
+
47
+ Use as a context manager:
48
+
49
+ >>> with Spinner("Uploading"):
50
+ ... run_upload()
51
+ """
52
+
53
+ def __init__(self, message: str = "Working", *, style: str = "dots", delay: float = 0.1, stream=None) -> None:
54
+ self.message = message
55
+ self.frames = SPINNER_STYLES.get(style, SPINNER_STYLES["dots"])
56
+ self.delay = delay
57
+ self.stream = stream or sys.stderr
58
+ self._stop = threading.Event()
59
+ self._thread: threading.Thread | None = None
60
+
61
+ def start(self) -> "Spinner":
62
+ if self._thread and self._thread.is_alive():
63
+ return self
64
+ self._stop.clear()
65
+ self._thread = threading.Thread(target=self._spin, daemon=True)
66
+ self._thread.start()
67
+ return self
68
+
69
+ def _spin(self) -> None:
70
+ for frame in itertools.cycle(self.frames):
71
+ if self._stop.is_set():
72
+ break
73
+ self.stream.write(f"\r{frame} {self.message}")
74
+ self.stream.flush()
75
+ time.sleep(self.delay)
76
+
77
+ def stop(self, final: str | None = None) -> None:
78
+ self._stop.set()
79
+ if self._thread:
80
+ self._thread.join(timeout=self.delay * 3)
81
+ clear = " " * (len(self.message) + 8)
82
+ self.stream.write(f"\r{clear}\r")
83
+ if final:
84
+ self.stream.write(final + "\n")
85
+ self.stream.flush()
86
+
87
+ def __enter__(self) -> "Spinner":
88
+ return self.start()
89
+
90
+ def __exit__(self, exc_type, exc, tb) -> None:
91
+ self.stop()
92
+
93
+
94
+ @contextlib.contextmanager
95
+ def rich_status(message: str, *, spinner: str = "dots") -> Iterator[None]:
96
+ console = Console()
97
+ with console.status(message, spinner=spinner):
98
+ yield
99
+
100
+
101
+ def track_iter(iterable: Iterable[T], description: str = "Processing") -> Iterator[T]:
102
+ yield from track(iterable, description=description)
103
+
104
+
105
+ # Convenience aliases.
106
+ progress_bar_rich = progress_bar
107
+ SpinnerRich = Spinner
curint/prompt.py ADDED
@@ -0,0 +1,38 @@
1
+ """Tiny prompt helpers styled with curint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+
7
+ from .text import style_text
8
+ from .theme import get_theme
9
+
10
+
11
+ def ask(question: str, *, default: str | None = None) -> str:
12
+ theme = get_theme()
13
+ suffix = f" [{default}]" if default is not None else ""
14
+ answer = input(style_text(f"? {question}{suffix}: ", fg=theme.primary, bold=True))
15
+ return answer or (default or "")
16
+
17
+
18
+ def confirm(question: str, *, default: bool = False) -> bool:
19
+ marker = "Y/n" if default else "y/N"
20
+ answer = ask(f"{question} ({marker})").strip().lower()
21
+ if not answer:
22
+ return default
23
+ return answer in {"y", "yes", "true", "1"}
24
+
25
+
26
+ def choose(question: str, choices: Sequence[str], *, default: str | None = None) -> str:
27
+ if not choices:
28
+ raise ValueError("choices cannot be empty")
29
+ default = default or choices[0]
30
+ print(style_text(f"? {question}", fg=get_theme().primary, bold=True))
31
+ for index, choice in enumerate(choices, start=1):
32
+ print(f" {index}. {choice}")
33
+ answer = ask("Choose a number", default=str(choices.index(default) + 1))
34
+ try:
35
+ position = int(answer) - 1
36
+ return choices[position]
37
+ except Exception:
38
+ return default
curint/text.py ADDED
@@ -0,0 +1,150 @@
1
+ """Text styling helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from typing import Any
7
+
8
+ from ._compat import Console, SKYTEXT, colored, just_fix_windows_console
9
+ from .colors import PALETTES, RESET, RGB, ansi_bg, ansi_fg, blend_many, parse_color, rainbow_color, strip_ansi
10
+
11
+ just_fix_windows_console()
12
+ console = Console()
13
+
14
+
15
+ def style_text(
16
+ text: str,
17
+ fg: str | Sequence[int] | RGB | None = None,
18
+ bg: str | Sequence[int] | RGB | None = None,
19
+ *,
20
+ bold: bool = False,
21
+ italic: bool = False,
22
+ underline: bool = False,
23
+ dim: bool = False,
24
+ ) -> str:
25
+ """Return ANSI-styled text with true-color foreground/background support."""
26
+
27
+ codes: list[str] = []
28
+ if bold:
29
+ codes.append("\033[1m")
30
+ if dim:
31
+ codes.append("\033[2m")
32
+ if italic:
33
+ codes.append("\033[3m")
34
+ if underline:
35
+ codes.append("\033[4m")
36
+ if fg is not None:
37
+ codes.append(ansi_fg(fg))
38
+ if bg is not None:
39
+ codes.append(ansi_bg(bg))
40
+ if not codes:
41
+ return text
42
+ return "".join(codes) + text + RESET
43
+
44
+
45
+ def termcolor_text(text: str, color: str | None = None, on_color: str | None = None, attrs: list[str] | None = None) -> str:
46
+ """Small wrapper around :func:`termcolor.colored` for classic ANSI names."""
47
+
48
+ return colored(text, color=color, on_color=on_color, attrs=attrs)
49
+
50
+
51
+ def gradient_text(
52
+ text: str,
53
+ start: str | Sequence[int] | RGB = "#ff5e62",
54
+ end: str | Sequence[int] | RGB = "#00d4ff",
55
+ *,
56
+ stops: Sequence[str | Sequence[int] | RGB] | None = None,
57
+ bold: bool = False,
58
+ ) -> str:
59
+ """Return text colored by a smooth RGB gradient."""
60
+
61
+ chars = list(text)
62
+ colors = blend_many(stops or [start, end], len(chars))
63
+ pieces: list[str] = []
64
+ for char, color in zip(chars, colors, strict=False):
65
+ if char.isspace():
66
+ pieces.append(char)
67
+ else:
68
+ pieces.append(style_text(char, color, bold=bold))
69
+ return "".join(pieces)
70
+
71
+
72
+ def palette_text(text: str, palette: str = "rainbow", *, bold: bool = False) -> str:
73
+ colors = blend_many(PALETTES.get(palette, PALETTES["rainbow"]), len(text))
74
+ return "".join(char if char.isspace() else style_text(char, color, bold=bold) for char, color in zip(text, colors, strict=False))
75
+
76
+
77
+ def rainbow_text(text: str, *, phase: float = 0.0, bold: bool = False) -> str:
78
+ pieces: list[str] = []
79
+ total = max(1, len(text))
80
+ for index, char in enumerate(text):
81
+ if char.isspace():
82
+ pieces.append(char)
83
+ else:
84
+ pieces.append(style_text(char, rainbow_color(index, total, phase), bold=bold))
85
+ return "".join(pieces)
86
+
87
+
88
+ def highlight(text: str, query: str, *, fg: str | Sequence[int] | RGB = "black", bg: str | Sequence[int] | RGB = "yellow") -> str:
89
+ """Highlight all exact occurrences of ``query`` in ``text``."""
90
+
91
+ if not query:
92
+ return text
93
+ return text.replace(query, style_text(query, fg, bg, bold=True))
94
+
95
+
96
+ def center(text: str, width: int = 80, fill: str = " ") -> str:
97
+ """Center text using a simple ANSI-stripped display width."""
98
+
99
+ display = max(0, len(strip_ansi(text)))
100
+ if display >= width:
101
+ return text
102
+ total = width - display
103
+ left = total // 2
104
+ right = total - left
105
+ return f"{fill * left}{text}{fill * right}"
106
+
107
+
108
+ def box(text: str, *, title: str | None = None, width: int | None = None) -> str:
109
+ """Return a simple ANSI-free box string for logs or plain terminals."""
110
+
111
+ lines = text.splitlines() or [""]
112
+ content_width = max(len(strip_ansi(line)) for line in lines)
113
+ if title:
114
+ content_width = max(content_width, len(title) + 2)
115
+ if width is not None:
116
+ content_width = max(content_width, width - 4)
117
+ top_label = f" {title} " if title else ""
118
+ top = "┌" + top_label + "─" * max(0, content_width - len(top_label)) + "┐"
119
+ bottom = "└" + "─" * content_width + "┘"
120
+ body = ["│" + line + " " * max(0, content_width - len(strip_ansi(line))) + "│" for line in lines]
121
+ return "\n".join([top, *body, bottom])
122
+
123
+
124
+ def sky_gradient(text: str, start: tuple[int, int, int] = (255, 0, 0), end: tuple[int, int, int] = (0, 255, 255)) -> str:
125
+ """Use SkyText's gradient when available, otherwise use curint's fallback."""
126
+
127
+ if SKYTEXT is not None:
128
+ fn = getattr(SKYTEXT, "gradient", None) or getattr(SKYTEXT, "print_gradient", None)
129
+ if callable(fn):
130
+ # Most SkyText helpers print directly. We return our fallback to keep this function pure.
131
+ return gradient_text(text, start, end)
132
+ return gradient_text(text, start, end)
133
+
134
+
135
+ def print_color(text: str, fg: str | Sequence[int] | RGB | None = None, bg: str | Sequence[int] | RGB | None = None, **kwargs: Any) -> None:
136
+ console.print(style_text(text, fg=fg, bg=bg, **kwargs), markup=False)
137
+
138
+
139
+ def print_gradient(text: str, start: str | Sequence[int] | RGB = "#ff5e62", end: str | Sequence[int] | RGB = "#00d4ff", **kwargs: Any) -> None:
140
+ console.print(gradient_text(text, start, end, **kwargs), markup=False)
141
+
142
+
143
+ def print_rainbow(text: str, **kwargs: Any) -> None:
144
+ console.print(rainbow_text(text, **kwargs), markup=False)
145
+
146
+
147
+ # Convenience aliases for callers who prefer print-oriented names.
148
+ color_text = style_text
149
+ cprint = print_color
150
+ print_gradient_rich = print_gradient
curint/theme.py ADDED
@@ -0,0 +1,119 @@
1
+ """Theme definitions used by console and status helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, replace
6
+ from typing import Mapping
7
+
8
+ ColorLike = str | tuple[int, int, int]
9
+
10
+ @dataclass(frozen=True)
11
+ class Theme:
12
+ name: str
13
+ primary: ColorLike = "#00d4ff"
14
+ secondary: ColorLike = "#ff5e62"
15
+ accent: ColorLike = "#ffd166"
16
+ success: ColorLike = "#50fa7b"
17
+ error: ColorLike = "#ff5555"
18
+ warning: ColorLike = "#f1fa8c"
19
+ info: ColorLike = "#8be9fd"
20
+ muted: ColorLike = "#6272a4"
21
+
22
+ def as_dict(self) -> dict[str, ColorLike]:
23
+ return {
24
+ "primary": self.primary,
25
+ "secondary": self.secondary,
26
+ "accent": self.accent,
27
+ "success": self.success,
28
+ "error": self.error,
29
+ "warning": self.warning,
30
+ "info": self.info,
31
+ "muted": self.muted,
32
+ }
33
+
34
+ def with_values(self, **values: ColorLike) -> "Theme":
35
+ return replace(self, **values)
36
+
37
+
38
+ THEMES: dict[str, Theme] = {
39
+ "default": Theme("default"),
40
+ "dracula": Theme(
41
+ "dracula",
42
+ primary="#bd93f9",
43
+ secondary="#ff79c6",
44
+ accent="#f1fa8c",
45
+ success="#50fa7b",
46
+ error="#ff5555",
47
+ warning="#ffb86c",
48
+ info="#8be9fd",
49
+ muted="#6272a4",
50
+ ),
51
+ "ocean": Theme(
52
+ "ocean",
53
+ primary="#00b4db",
54
+ secondary="#0083b0",
55
+ accent="#caf0f8",
56
+ success="#80ed99",
57
+ error="#ff6b6b",
58
+ warning="#ffd166",
59
+ info="#90e0ef",
60
+ muted="#6c757d",
61
+ ),
62
+ "sunset": Theme(
63
+ "sunset",
64
+ primary="#ff5e62",
65
+ secondary="#ff9966",
66
+ accent="#ffe66d",
67
+ success="#96f550",
68
+ error="#ff3864",
69
+ warning="#ffcc00",
70
+ info="#a0e7e5",
71
+ muted="#6d6875",
72
+ ),
73
+ "mono": Theme(
74
+ "mono",
75
+ primary="white",
76
+ secondary="gray",
77
+ accent="white",
78
+ success="white",
79
+ error="white",
80
+ warning="white",
81
+ info="white",
82
+ muted="gray",
83
+ ),
84
+ }
85
+
86
+ _current_theme = THEMES["default"]
87
+
88
+
89
+ def get_theme(name: str | None = None) -> Theme:
90
+ if name is None:
91
+ return _current_theme
92
+ try:
93
+ return THEMES[name]
94
+ except KeyError as exc:
95
+ available = ", ".join(sorted(THEMES))
96
+ raise ValueError(f"Unknown theme {name!r}. Available themes: {available}") from exc
97
+
98
+
99
+ def use_theme(name: str) -> Theme:
100
+ global _current_theme
101
+ _current_theme = get_theme(name)
102
+ return _current_theme
103
+
104
+
105
+ def register_theme(theme: Theme, *, replace_existing: bool = False) -> Theme:
106
+ if theme.name in THEMES and not replace_existing:
107
+ raise ValueError(f"Theme {theme.name!r} already exists")
108
+ THEMES[theme.name] = theme
109
+ return theme
110
+
111
+
112
+ def theme_names() -> list[str]:
113
+ return sorted(THEMES)
114
+
115
+
116
+ def theme_from_mapping(name: str, mapping: Mapping[str, ColorLike]) -> Theme:
117
+ base = Theme(name)
118
+ valid = {field: value for field, value in mapping.items() if field in base.as_dict()}
119
+ return base.with_values(**valid)
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 curint contributors
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,171 @@
1
+ Metadata-Version: 2.1
2
+ Name: curint
3
+ Version: 0.1.2
4
+ Summary: Colorful terminal output toolkit
5
+ Author: corotosh
6
+ License: MIT
7
+ Keywords: terminal,colors,rich,ansi,gradient,animation
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Classifier: Topic :: Terminals
16
+ Classifier: Topic :: Utilities
17
+ Requires-Python: >=3.8
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: colorama
21
+ Requires-Dist: termcolor
22
+ Requires-Dist: skytext
23
+ Requires-Dist: rich
24
+
25
+ curint is a Python library for colorful terminal output. It provides styled text, RGB gradients, rainbow text, themed console helpers, Rich panels and tables, status messages, prompts, logging, spinners, progress bars, and lightweight text animations.
26
+
27
+ ## Features
28
+
29
+ - True-color foreground and background styling.
30
+ - Gradient, palette, and rainbow text.
31
+ - Built-in themes such as `default`, `sunset`, `ocean`, `dracula`, and `forest`.
32
+ - Rich-backed panels, tables, trees, rules, Markdown, and JSON output.
33
+ - Status helpers: `success()`, `error()`, `warning()`, `info()`, and `debug()`.
34
+ - Simple prompts: `ask()`, `confirm()`, and `choose()`.
35
+ - Spinners, progress bars, and iterable tracking.
36
+ - Text effects: typewriter, wave, bounce, glitch, and shimmer.
37
+ - A colorful `logging.Handler` for terminal applications.
38
+
39
+ ## Quick start
40
+
41
+ ```python
42
+ from curint import SpectraConsole, Spinner, progress_bar, success
43
+
44
+ console = SpectraConsole(theme="sunset")
45
+ console.banner("curint")
46
+ console.gradient("Gradient text with a theme", bold=True)
47
+ console.rainbow("Rainbow output", bold=True)
48
+ console.panel("Useful for build scripts, demos, and terminal applications.", title="Hello")
49
+
50
+ with Spinner("Working"):
51
+ import time
52
+ time.sleep(0.5)
53
+
54
+ progress_bar(25, "Finishing", delay=0.01)
55
+ success("Done")
56
+ ```
57
+
58
+ ## Text helpers
59
+
60
+ ```python
61
+ from curint import gradient_text, highlight, palette_text, rainbow_text, style_text
62
+
63
+ print(style_text("bold cyan", fg="cyan", bold=True))
64
+ print(gradient_text("sunset gradient", "#ff5e62", "#ffd166"))
65
+ print(rainbow_text("rainbow"))
66
+ print(palette_text("neon", "neon"))
67
+ print(highlight("ship the package", "ship"))
68
+ ```
69
+
70
+ ## Console helpers
71
+
72
+ ```python
73
+ from curint import SpectraConsole
74
+
75
+ console = SpectraConsole(theme="ocean")
76
+ console.banner("release")
77
+ console.rule("checks")
78
+ console.panel("All systems ready", title="Status")
79
+ console.table(
80
+ [
81
+ {"Check": "tests", "Result": "passed"},
82
+ {"Check": "build", "Result": "passed"},
83
+ ],
84
+ title="Summary",
85
+ )
86
+ console.tree("package", {"dist": ["sdist", "wheel"], "docs": ["README"]})
87
+ console.markdown("**Markdown** rendering via Rich")
88
+ console.json({"ok": True, "version": "0.1.2"})
89
+ ```
90
+
91
+ ## Themes
92
+
93
+ ```python
94
+ from curint import Theme, register_theme, theme_names, use_theme
95
+
96
+ register_theme(
97
+ Theme(
98
+ name="brand",
99
+ primary="#7c3aed",
100
+ secondary="#06b6d4",
101
+ accent="#f59e0b",
102
+ )
103
+ )
104
+
105
+ console = use_theme("brand")
106
+ console.gradient("Custom brand theme")
107
+ print(theme_names())
108
+ ```
109
+
110
+ ## Animations
111
+
112
+ ```python
113
+ from curint import bounce, glitch, shimmer, typewriter, wave
114
+
115
+ typewriter("Typed output", delay=0.03)
116
+ wave("wave text")
117
+ bounce("bounce text")
118
+ glitch("glitch text")
119
+ shimmer("shimmer text")
120
+ ```
121
+
122
+ For tests, use frame helpers that return strings instead of printing:
123
+
124
+ ```python
125
+ from curint import glitch_frames, wave_frames
126
+
127
+ frames = wave_frames("abc", frames=3)
128
+ assert len(frames) == 3
129
+ assert glitch_frames("abc", frames=2)
130
+ ```
131
+
132
+ ## Progress and spinners
133
+
134
+ ```python
135
+ from curint import Spinner, progress_bar, rich_status, track_iter
136
+
137
+ with Spinner("Uploading"):
138
+ import time
139
+ time.sleep(0.5)
140
+
141
+ progress_bar(50, "Building", delay=0.01)
142
+
143
+ with rich_status("Indexing"):
144
+ pass
145
+
146
+ for item in track_iter(range(10), "Processing"):
147
+ pass
148
+ ```
149
+
150
+ ## Logging
151
+
152
+ ```python
153
+ from curint import configure_logging
154
+
155
+ logger = configure_logging("INFO", logger_name="curint-demo")
156
+ logger.info("Colorful logging is ready")
157
+ ```
158
+
159
+ ## Prompts
160
+
161
+ ```python
162
+ from curint import ask, choose, confirm
163
+
164
+ name = ask("Project name", default="demo")
165
+ color = choose("Theme", ["sunset", "ocean", "dracula"], default="ocean")
166
+ ready = confirm("Continue?", default=True)
167
+ ```
168
+
169
+ ## License
170
+
171
+ MIT License.
@@ -0,0 +1,15 @@
1
+ curint/__init__.py,sha256=V1nnIV9-Tiehl6rPZoRrGknsd25oNHIP9ivXHw255Kw,2027
2
+ curint/_compat.py,sha256=REIu4qTuEZ6dGoqJdySNqijCrKSTvh6BLqEP2zkQJmc,1620
3
+ curint/animations.py,sha256=Lx_7ULdWY9HuVH2PinX8m3xnwm57g4MaBlzk5mVsGqY,3859
4
+ curint/colors.py,sha256=-hwmkYt0EWRZHVr157jxXMeu74AlbhHSHHtU97J59Pc,4935
5
+ curint/console.py,sha256=3vDvw1A03kiAvdoOdWC1FB84gVGb-NcJd_11au-8iwE,5413
6
+ curint/logging.py,sha256=vAtAqZpLUh9h6qyzAwAORtA0R5jAzCIvQfUM0OLUPy4,1640
7
+ curint/progress.py,sha256=ElqEf5MjWb4AamTR8jkGn1e1hf9JvtvjjVpCA9KRM_g,3289
8
+ curint/prompt.py,sha256=0OLWL228ZOGqehCpy58hCYH79fVnN2IsB8i_ui5Zv0A,1268
9
+ curint/text.py,sha256=Osfs5ybGtnnyBf5sQ8pLiDsHUSs6K6uQO3Dyhs-PIYc,5321
10
+ curint/theme.py,sha256=4g0mvtWYo9_JqXRhJVk1dw9RSwYP9I3BMXyXceJyMOQ,3063
11
+ curint-0.1.2.dist-info/LICENSE,sha256=5Ai5gNurXlg9oUX2qebY4Uw1XBvkB13Eabp5xsBBgYg,1076
12
+ curint-0.1.2.dist-info/METADATA,sha256=dNe3JfxTXEJSBV4qFriFSnRg0qP1Shu3BuDfEvxdjnk,4600
13
+ curint-0.1.2.dist-info/WHEEL,sha256=51RkbunBAw4BWsgaQWTpPhg4Diwp3c9P5iaLk67Hdtg,92
14
+ curint-0.1.2.dist-info/top_level.txt,sha256=-rNbEsJbMaVg0mNvGg7mNd6mGsDRvOpzDEKR7XodMUM,7
15
+ curint-0.1.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.47.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ curint