scriptcast 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.
@@ -0,0 +1,265 @@
1
+ # scriptcast/generator.py
2
+ import json
3
+ import re
4
+ import shlex
5
+ from collections import deque
6
+ from pathlib import Path
7
+
8
+ from .config import ScriptcastConfig
9
+ from .directives import build_directives
10
+
11
+
12
+ def _parse_sc_header(
13
+ lines: list[str],
14
+ base: ScriptcastConfig | None = None,
15
+ ) -> ScriptcastConfig:
16
+ """Parse .sc JSONL header + pre-scene set directives into a ScriptcastConfig.
17
+
18
+ Reads the first line as a JSON header for width/height/directive-prefix.
19
+ Then walks subsequent lines applying 'set' directives until the first 'scene'.
20
+ If *base* is provided, its fields serve as defaults overridden by the header.
21
+ """
22
+ if not lines:
23
+ return base.copy() if base is not None else ScriptcastConfig()
24
+
25
+ try:
26
+ header = json.loads(lines[0])
27
+ except json.JSONDecodeError:
28
+ return base.copy() if base is not None else ScriptcastConfig()
29
+
30
+ if base is not None:
31
+ config = base.copy()
32
+ config.directive_prefix = header.get("directive-prefix", config.directive_prefix)
33
+ config.width = header.get("width", config.width)
34
+ config.height = header.get("height", config.height)
35
+ else:
36
+ config = ScriptcastConfig(
37
+ directive_prefix=header.get("directive-prefix", "SC"),
38
+ width=header.get("width", 100),
39
+ height=header.get("height", 28),
40
+ )
41
+
42
+ for raw in lines[1:]:
43
+ raw = raw.strip()
44
+ if not raw:
45
+ continue
46
+ try:
47
+ row = json.loads(raw)
48
+ except json.JSONDecodeError:
49
+ continue
50
+ if not isinstance(row, list) or len(row) < 3:
51
+ continue
52
+ _, typ, text = row[0], row[1], row[2]
53
+ if typ != "dir":
54
+ continue
55
+ parts = shlex.split(str(text))
56
+ if not parts:
57
+ continue
58
+ if parts[0] == "scene":
59
+ break
60
+ if parts[0] == "set" and len(parts) >= 3:
61
+ config.apply("set", parts[1:])
62
+
63
+ return config
64
+
65
+
66
+ def build_config_from_sc_text(
67
+ sc_text: str,
68
+ base: ScriptcastConfig | None = None,
69
+ ) -> ScriptcastConfig:
70
+ """Parse .sc JSONL header and pre-scene set directives into a ScriptcastConfig."""
71
+ return _parse_sc_header(sc_text.splitlines(), base)
72
+
73
+
74
+ def generate_from_sc(
75
+ sc_path: Path,
76
+ output_dir: Path,
77
+ output_stem: str | None = None,
78
+ *,
79
+ split_scenes: bool = False,
80
+ base: ScriptcastConfig | None = None,
81
+ ) -> list[Path]:
82
+ """Read a JSONL .sc file and write .cast file(s) to output_dir."""
83
+ return generate_from_sc_text(
84
+ sc_path.read_text(),
85
+ output_dir,
86
+ output_stem=output_stem or sc_path.stem,
87
+ split_scenes=split_scenes,
88
+ base=base,
89
+ )
90
+
91
+
92
+ def generate_from_sc_text(
93
+ sc_text: str,
94
+ output_dir: Path,
95
+ output_stem: str = "output",
96
+ *,
97
+ split_scenes: bool = False,
98
+ base: ScriptcastConfig | None = None,
99
+ ) -> list[Path]:
100
+ """Convert JSONL .sc text to .cast file(s). Returns list of written paths.
101
+
102
+ If *base* is provided, its fields serve as defaults for the generated config.
103
+ Pre-scene ``set`` directives in the .sc text still override the base, so a
104
+ theme passed as *base* acts as a template that the script can override.
105
+ """
106
+ lines = sc_text.splitlines()
107
+ if not lines:
108
+ return []
109
+
110
+ header = json.loads(lines[0])
111
+ pipeline_version = header.get("pipeline-version", 1)
112
+ if pipeline_version not in (1, 2, 3):
113
+ raise ValueError(f"Unsupported .sc pipeline-version: {pipeline_version}")
114
+
115
+ config = _parse_sc_header(lines, base)
116
+ config.split_scenes = split_scenes
117
+
118
+ events: list[tuple] = []
119
+ for line in lines[1:]:
120
+ line = line.strip()
121
+ if not line:
122
+ continue
123
+ try:
124
+ ts, typ, text = json.loads(line)
125
+ events.append((float(ts), str(typ), str(text)))
126
+ except (json.JSONDecodeError, ValueError):
127
+ continue
128
+
129
+ scenes = _split_scenes(events)
130
+
131
+ if config.split_scenes:
132
+ paths = []
133
+ for scene_name, scene_events in scenes:
134
+ content = _render_scene(scene_events, config.copy(), scene_name)
135
+ path = output_dir / f"{scene_name}.cast"
136
+ path.write_text(content)
137
+ paths.append(path)
138
+ return paths
139
+
140
+ cast_header = {
141
+ "version": 2,
142
+ "width": config.width,
143
+ "height": config.height,
144
+ "timestamp": 0,
145
+ "env": {"TERM": "xterm-256color"},
146
+ }
147
+ all_lines = [json.dumps(cast_header)]
148
+ cursor = 0.0
149
+ for scene_name, scene_events in scenes:
150
+ scene_lines, cursor = _render_scene_lines(scene_events, config.copy(), scene_name, cursor)
151
+ all_lines.extend(scene_lines)
152
+ path = output_dir / f"{output_stem}.cast"
153
+ path.write_text("\n".join(all_lines) + "\n")
154
+ return [path]
155
+
156
+
157
+ def _split_scenes(events: list[tuple]) -> list[tuple[str, list[tuple]]]:
158
+ scenes: list[tuple[str, list[tuple]]] = []
159
+ current_name: str | None = None
160
+ current_events: list[tuple] = []
161
+
162
+ for event in events:
163
+ _, typ, text = event
164
+ if typ == "dir" and text.split()[0] == "scene":
165
+ if current_name is not None:
166
+ scenes.append((current_name, current_events))
167
+ parts = text.split(maxsplit=1)
168
+ current_name = parts[1] if len(parts) > 1 else "unnamed"
169
+ current_events = []
170
+ else:
171
+ if current_name is None:
172
+ current_name = "main"
173
+ current_events.append(event)
174
+
175
+ if current_name is not None:
176
+ scenes.append((current_name, current_events))
177
+
178
+ _overhead = {"set"}
179
+
180
+ def _is_overhead_only(evts: list[tuple]) -> bool:
181
+ return all(
182
+ typ == "dir" and text.split()[0] in _overhead
183
+ for _, typ, text in evts
184
+ )
185
+
186
+ return [
187
+ (name, evts)
188
+ for name, evts in scenes
189
+ if not (name == "main" and _is_overhead_only(evts))
190
+ ]
191
+
192
+
193
+ def _render_scene(
194
+ events: list[tuple],
195
+ config: ScriptcastConfig,
196
+ scene_name: str,
197
+ ) -> str:
198
+ cast_header = {
199
+ "version": 2,
200
+ "width": config.width,
201
+ "height": config.height,
202
+ "timestamp": 0,
203
+ "env": {"TERM": "xterm-256color"},
204
+ }
205
+ lines, _ = _render_scene_lines(events, config, scene_name, 0.0)
206
+ return "\n".join([json.dumps(cast_header)] + lines) + "\n"
207
+
208
+
209
+ def _render_scene_lines(
210
+ events: list[tuple],
211
+ config: ScriptcastConfig,
212
+ scene_name: str,
213
+ initial_cursor: float = 0.0,
214
+ ) -> tuple[list[str], float]:
215
+ lines: list[str] = []
216
+ cursor = initial_cursor
217
+ active = config.copy()
218
+ # Build gen registry: directive name → directive instance
219
+ # Uses handles attribute; directives without handles are recorder-only.
220
+ all_directives = build_directives(active.directive_prefix, active.trace_prefix)
221
+ gen_registry = {d.handles: d for d in all_directives if d.handles is not None}
222
+
223
+ lines.append(json.dumps([round(cursor, 6), "o", "\x1b[2J\x1b[H"]))
224
+ cursor += active.enter_wait / 1000.0
225
+
226
+ queue: deque[tuple] = deque(events)
227
+ while queue:
228
+ event = queue.popleft()
229
+ _, typ, text = event
230
+
231
+ if typ == "dir":
232
+ parts = text.split()
233
+ name = parts[0] if parts else ""
234
+ d = gen_registry.get(name)
235
+ if d is not None:
236
+ cursor, new_lines = d.gen(event, queue, active, cursor)
237
+ lines.extend(new_lines)
238
+
239
+ elif typ == "cmd":
240
+ lines.append(json.dumps([round(cursor, 6), "o", active.prompt]))
241
+ cursor += active.cmd_wait / 1000.0
242
+ for char in text:
243
+ cursor += active.type_speed / 1000.0
244
+ lines.append(json.dumps([round(cursor, 6), "o", char]))
245
+ if char == " ":
246
+ cursor += active.effective_word_pause_s
247
+ cursor += active.type_speed / 1000.0
248
+ lines.append(json.dumps([round(cursor, 6), "o", "\r\n"]))
249
+
250
+ elif typ == "out":
251
+ if active.cr_delay > 0 and '\r' in text:
252
+ # Split before each bare \r (not \r\n) for progress-bar animation
253
+ parts = re.split(r'(?=\r(?!\n))', text)
254
+ for j, part in enumerate(parts):
255
+ if part:
256
+ lines.append(json.dumps([round(cursor, 6), "o", part]))
257
+ if j < len(parts) - 1:
258
+ cursor += active.cr_delay / 1000.0
259
+ elif text:
260
+ lines.append(json.dumps([round(cursor, 6), "o", text]))
261
+
262
+ lines.append(json.dumps([round(cursor, 6), "o", active.prompt]))
263
+ cursor += active.exit_wait / 1000.0
264
+ lines.append(json.dumps([round(cursor, 6), "o", ""]))
265
+ return lines, cursor
scriptcast/recorder.py ADDED
@@ -0,0 +1,212 @@
1
+ # scriptcast/recorder.py
2
+ import errno
3
+ import fcntl
4
+ import json
5
+ import logging
6
+ import os
7
+ import pty
8
+ import re
9
+ import struct
10
+ import subprocess
11
+ import tempfile
12
+ import termios
13
+ import time
14
+ from pathlib import Path
15
+
16
+ from .config import ScriptcastConfig
17
+ from .directives import ScEvent, build_directives
18
+ from .shell import get_adapter
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ def _parse_raw(
24
+ raw_text: str,
25
+ trace_prefix: str = "+",
26
+ directive_prefix: str = "SC",
27
+ ) -> list[ScEvent]:
28
+ """Parse raw xtrace log text into an initial list of ScEvents.
29
+
30
+ Each record has the form: "<ts> <content><term>" where <term> is either
31
+ \\n or empty (last record with no terminator). Bare \\r within content
32
+ is preserved verbatim — it is not a record separator.
33
+
34
+ Classification rules (per record):
35
+ "+ : SC <rest>" → ScEvent(ts, "dir", rest) — terminator discarded, trailing \\r stripped
36
+ "+ <cmd>" → ScEvent(ts, "cmd", cmd) — terminator discarded, trailing \\r stripped
37
+ "<text>" → ScEvent(ts, "out", text + term) — verbatim, \\r preserved
38
+
39
+ Lines with non-float timestamps are skipped.
40
+ """
41
+ sc_prefix = f"{trace_prefix} : {directive_prefix} "
42
+ trace_prefix_sp = f"{trace_prefix} "
43
+ events: list[ScEvent] = []
44
+
45
+ # Split on any line terminator, keeping each terminator as a token.
46
+ # re.split with a capturing group gives [c0, t0, c1, t1, ..., cN].
47
+ parts = re.split(r'(\n)', raw_text)
48
+
49
+ i = 0
50
+ while i < len(parts):
51
+ entry = parts[i]
52
+ term = parts[i + 1] if i + 1 < len(parts) else ''
53
+ i += 2
54
+
55
+ if not entry:
56
+ continue
57
+ ts_str, _, content = entry.partition(' ')
58
+ try:
59
+ ts = float(ts_str)
60
+ except ValueError:
61
+ continue
62
+
63
+ if content.startswith(sc_prefix):
64
+ events.append(ScEvent(ts, "dir", content[len(sc_prefix):].removesuffix('\r')))
65
+ elif content.startswith(trace_prefix_sp):
66
+ events.append(ScEvent(ts, "cmd", content[len(trace_prefix_sp):].removesuffix('\r')))
67
+ else:
68
+ events.append(ScEvent(ts, "out", content + term))
69
+
70
+ return events
71
+
72
+
73
+ def _serialise(events: list[ScEvent]) -> str:
74
+ """Convert a list of ScEvents to JSONL text (no trailing newline if empty)."""
75
+ lines = [json.dumps([e.ts, e.type, e.text]) for e in events]
76
+ return "\n".join(lines) + ("\n" if lines else "")
77
+
78
+
79
+ def _preprocess(script_text: str, directive_prefix: str = "SC") -> str:
80
+ """Rewrite script lines using directive pre-phase before shell execution."""
81
+ directives = build_directives(directive_prefix)
82
+ lines = script_text.splitlines(keepends=True)
83
+ for d in directives:
84
+ lines = d.pre(lines)
85
+ return "".join(lines)
86
+
87
+
88
+ def _postprocess(
89
+ raw_text: str,
90
+ trace_prefix: str = "+",
91
+ directive_prefix: str = "SC",
92
+ ) -> str:
93
+ """Convert raw .log text to JSONL .sc body (no header line)."""
94
+ directives = build_directives(directive_prefix, trace_prefix)
95
+ events = _parse_raw(raw_text, trace_prefix, directive_prefix)
96
+ for d in directives:
97
+ events = d.post(events)
98
+ return _serialise(events)
99
+
100
+
101
+ def record(
102
+ script_path: str | Path,
103
+ sc_path: str | Path,
104
+ config: ScriptcastConfig,
105
+ shell: str,
106
+ xtrace_log: bool = False,
107
+ ) -> int:
108
+ """Run script_path in shell with tracing, write cleaned output to sc_path.
109
+
110
+ If xtrace_log is True, the raw PTY capture (before postprocessing) is
111
+ written to a file beside sc_path with the suffix ``.xtrace``.
112
+
113
+ Returns the shell exit code. Warns (does not raise) on non-zero exit.
114
+ """
115
+ script_path = Path(script_path)
116
+ sc_path = Path(sc_path)
117
+ adapter = get_adapter(shell)
118
+
119
+ preamble = adapter.tracing_preamble(config.trace_prefix)
120
+
121
+ script_content = script_path.read_text()
122
+ if script_content.startswith("#!"):
123
+ _, _, script_content = script_content.partition("\n")
124
+
125
+ script_content = _preprocess(script_content, config.directive_prefix)
126
+ logger.debug(
127
+ "Recording %s (shell=%s, width=%d, height=%d)",
128
+ script_path.name, shell, config.width, config.height,
129
+ )
130
+
131
+ master_fd = -1
132
+ proc = None
133
+ tmp = tempfile.NamedTemporaryFile(
134
+ mode="w", suffix=".sh", delete=False, dir=tempfile.gettempdir()
135
+ )
136
+ try:
137
+ tmp.write(preamble)
138
+ tmp.write(script_content)
139
+ tmp.flush()
140
+ tmp.close()
141
+
142
+ master_fd, slave_fd = pty.openpty()
143
+ try:
144
+ fcntl.ioctl(
145
+ slave_fd, termios.TIOCSWINSZ,
146
+ struct.pack('HHHH', config.height, config.width, 0, 0),
147
+ )
148
+ stdin_fd = os.open('/dev/null', os.O_RDONLY)
149
+ try:
150
+ proc = subprocess.Popen(
151
+ [shell, tmp.name],
152
+ stdin=stdin_fd,
153
+ stdout=slave_fd,
154
+ stderr=slave_fd,
155
+ close_fds=True,
156
+ cwd=script_path.parent,
157
+ )
158
+ finally:
159
+ os.close(slave_fd)
160
+ os.close(stdin_fd)
161
+ except Exception:
162
+ os.close(master_fd)
163
+ master_fd = -1
164
+ raise
165
+
166
+ raw_lines: list[str] = []
167
+ try:
168
+ with open(master_fd, 'rb', closefd=False) as f:
169
+ for line in f:
170
+ raw_lines.append(f"{time.time():.3f} {line.decode('utf-8', errors='replace')}")
171
+ except OSError as e:
172
+ if e.errno != errno.EIO:
173
+ raise
174
+ # Note: output without a trailing newline (e.g. printf "text") is not
175
+ # captured — the BufferedReader discards partial data when EIO fires.
176
+ os.close(master_fd)
177
+ master_fd = -1 # signal to finally: already closed
178
+ proc.wait()
179
+
180
+ logger.debug("PTY capture complete: %d raw lines", len(raw_lines))
181
+
182
+ raw_text = "".join(raw_lines)
183
+ if xtrace_log:
184
+ xtrace_path = sc_path.with_suffix('.xtrace')
185
+ xtrace_path.write_text(raw_text)
186
+ logger.info("Saved: %s", xtrace_path)
187
+ clean_text = _postprocess(raw_text, config.trace_prefix, config.directive_prefix)
188
+ logger.debug("Post-processed to %d events", clean_text.count("\n"))
189
+
190
+ header = json.dumps({
191
+ "version": 1,
192
+ "shell": adapter.name,
193
+ "width": config.width,
194
+ "height": config.height,
195
+ "directive-prefix": config.directive_prefix,
196
+ "pipeline-version": 3,
197
+ })
198
+ sc_path.write_text(header + "\n" + clean_text)
199
+
200
+ if proc.returncode != 0:
201
+ logger.warning(
202
+ "Script exited with non-zero status %d; .sc file written anyway.",
203
+ proc.returncode,
204
+ )
205
+ return proc.returncode
206
+ finally:
207
+ if master_fd != -1:
208
+ os.close(master_fd)
209
+ if proc is not None and proc.poll() is None:
210
+ proc.terminate()
211
+ proc.wait()
212
+ os.unlink(tmp.name)
@@ -0,0 +1,20 @@
1
+ # scriptcast/shell/__init__.py
2
+ from .adapter import ShellAdapter
3
+ from .bash import BashAdapter
4
+ from .zsh import ZshAdapter
5
+
6
+ _ADAPTERS: dict[str, ShellAdapter] = {
7
+ "bash": BashAdapter(),
8
+ "zsh": ZshAdapter(),
9
+ }
10
+
11
+ def get_adapter(shell: str) -> ShellAdapter:
12
+ """Return adapter for shell name or full path (e.g. '/bin/bash' → BashAdapter).
13
+ Raises ValueError for unsupported shells.
14
+ """
15
+ name = shell.split("/")[-1]
16
+ if name not in _ADAPTERS:
17
+ raise ValueError(
18
+ f"Unsupported shell: {shell!r}. Supported: {list(_ADAPTERS)}"
19
+ )
20
+ return _ADAPTERS[name]
@@ -0,0 +1,13 @@
1
+ # scriptcast/shell/adapter.py
2
+ from abc import ABC, abstractmethod
3
+
4
+
5
+ class ShellAdapter(ABC):
6
+ @property
7
+ @abstractmethod
8
+ def name(self) -> str: ...
9
+
10
+ @abstractmethod
11
+ def tracing_preamble(self, trace_prefix: str) -> str:
12
+ """Return shell code to enable tracing with the given prefix."""
13
+ ...
@@ -0,0 +1,11 @@
1
+ # scriptcast/shell/bash.py
2
+ from .adapter import ShellAdapter
3
+
4
+
5
+ class BashAdapter(ShellAdapter):
6
+ @property
7
+ def name(self) -> str:
8
+ return "bash"
9
+
10
+ def tracing_preamble(self, trace_prefix: str) -> str:
11
+ return f'PS4="{trace_prefix} "\nset -x\n'
@@ -0,0 +1,11 @@
1
+ # scriptcast/shell/zsh.py
2
+ from .adapter import ShellAdapter
3
+
4
+
5
+ class ZshAdapter(ShellAdapter):
6
+ @property
7
+ def name(self) -> str:
8
+ return "zsh"
9
+
10
+ def tracing_preamble(self, trace_prefix: str) -> str:
11
+ return f'PS4="{trace_prefix} "\nsetopt xtrace\n'
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: scriptcast
3
+ Version: 0.1.0
4
+ Summary: Generate terminal demos (asciinema casts & GIFs) from simple shell-like scripts.
5
+ Author-email: Nasser Alansari <dacrystal@users.noreply.github.com>
6
+ License: MIT
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Requires-Python: >=3.10
13
+ Requires-Dist: click>=8.0
14
+ Requires-Dist: pillow>=9.0
15
+ Provides-Extra: dev
16
+ Requires-Dist: asciinema>=2.4.0; extra == "dev"
17
+ Requires-Dist: pytest>=8.0; extra == "dev"
18
+ Requires-Dist: pytest-cov; extra == "dev"
19
+ Requires-Dist: ruff>=0.4; extra == "dev"
20
+ Requires-Dist: mypy>=1.10; extra == "dev"
21
+ Requires-Dist: git-cliff>=2.0; extra == "dev"
@@ -0,0 +1,33 @@
1
+ scriptcast/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ scriptcast/__main__.py,sha256=XX2_fh8fXEDW5c67IPuYc4m9LUDHkycUuygryntMnSs,13415
3
+ scriptcast/config.py,sha256=COMFJLg5IPMlhzFXY6Sk7bi7mavV6XJAqt2_uHLjah8,7610
4
+ scriptcast/directives.py,sha256=nYXW8cFCuZYj6i5wfiTu1e9NfxPVsE4OCb8-9-3mXjA,15050
5
+ scriptcast/export.py,sha256=vPXNHvdK7kXsyxBImHwehWNfl2GlRBEjUyBNP5Lx90g,20085
6
+ scriptcast/generator.py,sha256=MxKc2zoTfzGwLC6xzg5Ni_chE1lhKkpy90UvUGSpNJ8,8661
7
+ scriptcast/recorder.py,sha256=-TLo_oFF_CE2GViDMHYCKoo6Q3tzd3a1PDHu8YbBD6Y,6974
8
+ scriptcast/assets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ scriptcast/assets/fonts/DMSans-Regular.ttf,sha256=qVoziUKqNXFFQnvnyWP_aR-f6FnDUS2cBIPhkU1Xogg,78256
10
+ scriptcast/assets/fonts/Pacifico.ttf,sha256=W2wNUzSnv3fepSuXXFoMQIh4wPcRXtW2-xUfY0t79wE,329380
11
+ scriptcast/assets/themes/aurora.sh,sha256=futWVqJ2tH2WgbQ-Pj-3ONQM-5o2zJ8M3AX57MGxWHU,666
12
+ scriptcast/assets/themes/dark.sh,sha256=-nqvnitACQ_dHgHZYCUvuED1bZ1GTUTlBLOljHUywCw,631
13
+ scriptcast/assets/themes/light.sh,sha256=bFi3WDgI_ppotocaTox76JBJAkec_xcO1jgiQfVnTI0,631
14
+ scriptcast/shell/__init__.py,sha256=iCT6AUH6s1ljS4KkC5YwxZ4mCmaU1cFuOLwpkp5_-mc,602
15
+ scriptcast/shell/adapter.py,sha256=aswPs514-jaFTyETxnze9EKXv4c8Ahh50oYrDjBDTWI,322
16
+ scriptcast/shell/bash.py,sha256=VRvVeKAEb5Og6uXpWeLp2xha9_UzQ3v8t5HCBJvlu14,268
17
+ scriptcast/shell/zsh.py,sha256=47F44D0q5QlmcANgIDUbkgIgT5EnwgRSiW7zEWoutl4,272
18
+ tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
+ tests/test_cli.py,sha256=5vtfdy5MgjqD_bS1CTizSfnQXfXip1QpfmwKWKV9l8E,11380
20
+ tests/test_config.py,sha256=RPaP0LqfOKEfxuP4NG0__ZDuAtbaiuPrRl8bOaTewZ0,13111
21
+ tests/test_directives.py,sha256=n4Tz2gp0NnWWuCua49qsKJWNuXNq_J66E-50AV5a6Gc,19032
22
+ tests/test_export.py,sha256=GlwGho6rWjrq5SKauxNKdgCRqtEGlqxUHSIP0m4T3-E,38461
23
+ tests/test_generator.py,sha256=_HFmZH3ajctEKBMz0Kw6kRw6p2SgVIIK9R0XinpFjSk,14490
24
+ tests/test_integration.py,sha256=VBFBUNHQLWiUkb7cLWWdW95R7HraHIDNt78BUN6btPk,3284
25
+ tests/test_recorder.py,sha256=ZN5MiwmYbsXpXXfZCr8wx8NmxaRU8bZgrrJx0lziTCU,15802
26
+ tests/test_registry.py,sha256=zqRMdR0M4TsUsx4EPB6x6Dgv5YuxxDZY5yKWrURHulI,1471
27
+ tests/test_shell.py,sha256=IU8FpX7RoyreabtcassC87cO6zzUpojoruAvjQ9QwzM,933
28
+ tests/test_theme.py,sha256=mR0uxvznJE9XYHwPWzXYT0L3TfabsonRewqkdXe5yV0,6898
29
+ scriptcast-0.1.0.dist-info/METADATA,sha256=1krzrP7aIfIKUZUayOMjmqRuXXFeDeJLZECjxCOk1tY,835
30
+ scriptcast-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
31
+ scriptcast-0.1.0.dist-info/entry_points.txt,sha256=-GcwrQR9RHWFrD3kgAeEOViuc_02JmHoHBCEfw6m0w0,55
32
+ scriptcast-0.1.0.dist-info/top_level.txt,sha256=q338tb0EgIDmLUyS3fcLPWbYG8C7BIvo-isL-yRsq7k,17
33
+ scriptcast-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ scriptcast = scriptcast.__main__:cli
@@ -0,0 +1,2 @@
1
+ scriptcast
2
+ tests
tests/__init__.py ADDED
File without changes