shellfie 0.1.0 → 1.0.0

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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -0
  3. data/README.md +95 -236
  4. data/docs/.nojekyll +0 -0
  5. data/docs/index.html +205 -0
  6. data/docs/scripts.js +85 -0
  7. data/docs/styles.css +507 -0
  8. data/examples/simple.yml +3 -3
  9. data/lib/shellfie/animation_frame_builder.rb +178 -0
  10. data/lib/shellfie/animation_scroll_easing.rb +77 -0
  11. data/lib/shellfie/animation_timeline.rb +27 -0
  12. data/lib/shellfie/ansi_colors.rb +94 -0
  13. data/lib/shellfie/ansi_line_buffer.rb +87 -0
  14. data/lib/shellfie/ansi_normalizer.rb +51 -0
  15. data/lib/shellfie/ansi_parser.rb +50 -84
  16. data/lib/shellfie/cli.rb +22 -173
  17. data/lib/shellfie/cli_generate.rb +197 -0
  18. data/lib/shellfie/cli_info.rb +139 -0
  19. data/lib/shellfie/config.rb +108 -25
  20. data/lib/shellfie/config_defaults.rb +64 -0
  21. data/lib/shellfie/config_validation.rb +200 -0
  22. data/lib/shellfie/dependency_checker.rb +76 -0
  23. data/lib/shellfie/errors.rb +11 -1
  24. data/lib/shellfie/font_resolver.rb +58 -0
  25. data/lib/shellfie/format_resolver.rb +15 -0
  26. data/lib/shellfie/gif_generator.rb +83 -87
  27. data/lib/shellfie/gif_palette.rb +101 -0
  28. data/lib/shellfie/headless_theme_registry.rb +42 -0
  29. data/lib/shellfie/image_magick_command_builder.rb +75 -0
  30. data/lib/shellfie/line_layout.rb +137 -0
  31. data/lib/shellfie/output_writer.rb +41 -0
  32. data/lib/shellfie/parser.rb +113 -23
  33. data/lib/shellfie/parser_validation.rb +145 -0
  34. data/lib/shellfie/raster_painter.rb +157 -0
  35. data/lib/shellfie/render_chrome_cache.rb +40 -0
  36. data/lib/shellfie/render_geometry.rb +114 -0
  37. data/lib/shellfie/render_segment.rb +59 -0
  38. data/lib/shellfie/renderer.rb +79 -149
  39. data/lib/shellfie/rendering/shape_helpers.rb +42 -0
  40. data/lib/shellfie/rendering/text_painter.rb +187 -0
  41. data/lib/shellfie/rendering/window_chrome.rb +196 -0
  42. data/lib/shellfie/svg_raster_wrapper.rb +35 -0
  43. data/lib/shellfie/text_metrics.rb +96 -0
  44. data/lib/shellfie/theme_data.rb +80 -0
  45. data/lib/shellfie/theme_registry.rb +131 -0
  46. data/lib/shellfie/themes/base.rb +10 -1
  47. data/lib/shellfie/themes/configured.rb +61 -0
  48. data/lib/shellfie/themes/macos.rb +3 -1
  49. data/lib/shellfie/themes/ubuntu.rb +2 -1
  50. data/lib/shellfie/themes/windows_terminal.rb +7 -1
  51. data/lib/shellfie/version.rb +1 -1
  52. data/lib/shellfie.rb +37 -3
  53. metadata +37 -2
data/docs/scripts.js ADDED
@@ -0,0 +1,85 @@
1
+ const observer = new IntersectionObserver(
2
+ (entries) => {
3
+ entries.forEach((entry) => {
4
+ if (entry.isIntersecting) {
5
+ entry.target.classList.add("is-visible");
6
+ observer.unobserve(entry.target);
7
+ }
8
+ });
9
+ },
10
+ { threshold: 0.2 }
11
+ );
12
+
13
+ document.querySelectorAll(".section, .card, .code-block, .terminal-card").forEach((el) => {
14
+ el.classList.add("fade-in");
15
+ observer.observe(el);
16
+ });
17
+
18
+ const typewriter = document.querySelector(".typewriter");
19
+ if (typewriter) {
20
+ const rawLines = typewriter.dataset.lines || "";
21
+ const lines = rawLines.split("||");
22
+ const typingDelay = 45;
23
+ const lineDelay = 650;
24
+ const loopDelay = 5000;
25
+ let lineIndex = 0;
26
+ let charIndex = 0;
27
+
28
+ const cursor = document.createElement("span");
29
+ cursor.className = "typewriter__cursor";
30
+
31
+ const renderFrame = () => {
32
+ typewriter.innerHTML = "";
33
+ lines.forEach((line, index) => {
34
+ if (index > lineIndex) return;
35
+ const lineEl = document.createElement("div");
36
+ const isPrompt = line.trim().startsWith("$");
37
+ lineEl.className = `typewriter__line ${
38
+ isPrompt ? "typewriter__line--prompt" : "typewriter__line--output"
39
+ }`;
40
+ const text = index === lineIndex ? line.slice(0, charIndex) : line;
41
+ lineEl.textContent = text;
42
+ if (index === lineIndex) {
43
+ lineEl.appendChild(cursor);
44
+ }
45
+ typewriter.appendChild(lineEl);
46
+ });
47
+ };
48
+
49
+ const isInstantLine = (line) =>
50
+ line.startsWith("Generated:") || line.startsWith("Let the glow begin");
51
+
52
+ const typeNext = () => {
53
+ if (lineIndex >= lines.length) {
54
+ setTimeout(() => {
55
+ lineIndex = 0;
56
+ charIndex = 0;
57
+ typewriter.innerHTML = "";
58
+ typeNext();
59
+ }, loopDelay);
60
+ return;
61
+ }
62
+
63
+ if (isInstantLine(lines[lineIndex])) {
64
+ charIndex = lines[lineIndex].length;
65
+ renderFrame();
66
+ charIndex = 0;
67
+ lineIndex += 1;
68
+ setTimeout(typeNext, lineDelay);
69
+ return;
70
+ }
71
+
72
+ if (charIndex <= lines[lineIndex].length) {
73
+ renderFrame();
74
+ charIndex += 1;
75
+ setTimeout(typeNext, typingDelay);
76
+ return;
77
+ }
78
+
79
+ charIndex = 0;
80
+ lineIndex += 1;
81
+ setTimeout(typeNext, lineDelay);
82
+ };
83
+
84
+ typeNext();
85
+ }
data/docs/styles.css ADDED
@@ -0,0 +1,507 @@
1
+ @import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=Share+Tech+Mono&display=swap");
2
+
3
+ :root {
4
+ color-scheme: dark;
5
+ --bg: #0b0617;
6
+ --bg-alt: #140826;
7
+ --neon-pink: #ff5fd2;
8
+ --neon-blue: #52f2ff;
9
+ --neon-purple: #a07bff;
10
+ --neon-green: #34f5b4;
11
+ --neon-orange: #ff9f5f;
12
+ --neon-sun: #ff4fd8;
13
+ --text: #f3eaff;
14
+ --muted: #c2b0d8;
15
+ --card: rgba(18, 10, 34, 0.85);
16
+ --border: rgba(255, 255, 255, 0.08);
17
+ --glow: 0 0 24px rgba(255, 95, 210, 0.35);
18
+ }
19
+
20
+ *,
21
+ *::before,
22
+ *::after {
23
+ box-sizing: border-box;
24
+ }
25
+
26
+ body {
27
+ margin: 0;
28
+ min-height: 100vh;
29
+ font-family: "Space Grotesk", "Segoe UI", sans-serif;
30
+ color: var(--text);
31
+ background:
32
+ radial-gradient(circle at 10% 20%, rgba(255, 95, 210, 0.15), transparent 40%),
33
+ radial-gradient(circle at 90% 10%, rgba(82, 242, 255, 0.2), transparent 45%),
34
+ linear-gradient(180deg, #1a0a2f 0%, #0b0617 55%, #05020c 100%);
35
+ overflow-x: hidden;
36
+ }
37
+
38
+ a {
39
+ color: inherit;
40
+ text-decoration: none;
41
+ }
42
+
43
+ .ambient-glow {
44
+ position: fixed;
45
+ inset: -30% auto auto -10%;
46
+ width: 60vw;
47
+ height: 60vw;
48
+ background: radial-gradient(circle, rgba(255, 95, 210, 0.25), rgba(10, 4, 16, 0));
49
+ filter: blur(14px);
50
+ z-index: 0;
51
+ pointer-events: none;
52
+ }
53
+
54
+ .sun-core {
55
+ position: fixed;
56
+ top: 8vh;
57
+ left: 70%;
58
+ width: 280px;
59
+ height: 280px;
60
+ background: radial-gradient(circle at 50% 35%, var(--neon-sun), #ff8ad2 40%, rgba(255, 79, 216, 0.2) 60%, transparent 70%);
61
+ border-radius: 50%;
62
+ opacity: 0.55;
63
+ filter: blur(1px) saturate(120%);
64
+ z-index: 0;
65
+ pointer-events: none;
66
+ box-shadow: 0 0 28px rgba(255, 79, 216, 0.45);
67
+ animation: sunfloat 6s ease-in-out infinite;
68
+ }
69
+
70
+ .scanlines {
71
+ position: fixed;
72
+ inset: 0;
73
+ background: repeating-linear-gradient(
74
+ 180deg,
75
+ rgba(255, 255, 255, 0.03),
76
+ rgba(255, 255, 255, 0.03) 1px,
77
+ transparent 1px,
78
+ transparent 3px
79
+ );
80
+ mix-blend-mode: soft-light;
81
+ opacity: 0.18;
82
+ pointer-events: none;
83
+ z-index: 2;
84
+ }
85
+
86
+ .stars {
87
+ position: fixed;
88
+ inset: 0;
89
+ background-image:
90
+ radial-gradient(circle at 12% 24%, rgba(255, 255, 255, 0.6) 0 1px, transparent 2px),
91
+ radial-gradient(circle at 38% 12%, rgba(255, 255, 255, 0.4) 0 1px, transparent 2px),
92
+ radial-gradient(circle at 72% 18%, rgba(255, 255, 255, 0.5) 0 1px, transparent 2px),
93
+ radial-gradient(circle at 86% 32%, rgba(255, 255, 255, 0.5) 0 1px, transparent 2px),
94
+ radial-gradient(circle at 18% 78%, rgba(255, 255, 255, 0.3) 0 1px, transparent 2px),
95
+ radial-gradient(circle at 64% 62%, rgba(255, 255, 255, 0.4) 0 1px, transparent 2px);
96
+ opacity: 0.2;
97
+ pointer-events: none;
98
+ z-index: 0;
99
+ }
100
+
101
+ .grid-floor {
102
+ position: fixed;
103
+ inset: auto 0 0 0;
104
+ height: 45vh;
105
+ background-image:
106
+ linear-gradient(transparent 60%, rgba(82, 242, 255, 0.12)),
107
+ repeating-linear-gradient(90deg, rgba(82, 242, 255, 0.1) 0 1px, transparent 1px 60px),
108
+ repeating-linear-gradient(0deg, rgba(82, 242, 255, 0.1) 0 1px, transparent 1px 60px);
109
+ transform: perspective(600px) rotateX(65deg);
110
+ transform-origin: bottom;
111
+ opacity: 0.2;
112
+ z-index: 0;
113
+ pointer-events: none;
114
+ }
115
+
116
+ .topbar {
117
+ position: sticky;
118
+ top: 0;
119
+ z-index: 3;
120
+ display: flex;
121
+ align-items: center;
122
+ justify-content: space-between;
123
+ padding: 24px 6vw;
124
+ backdrop-filter: blur(12px);
125
+ background: rgba(7, 4, 15, 0.65);
126
+ border-bottom: 1px solid var(--border);
127
+ }
128
+
129
+ .brand {
130
+ display: flex;
131
+ align-items: center;
132
+ gap: 12px;
133
+ }
134
+
135
+ .brand__glyph {
136
+ font-size: 22px;
137
+ color: var(--neon-blue);
138
+ text-shadow: 0 0 12px rgba(82, 242, 255, 0.8);
139
+ }
140
+
141
+ .brand__title {
142
+ font-weight: 700;
143
+ letter-spacing: 0.08em;
144
+ text-transform: uppercase;
145
+ }
146
+
147
+ .brand__subtitle {
148
+ display: block;
149
+ font-size: 12px;
150
+ color: var(--muted);
151
+ }
152
+
153
+ .topnav {
154
+ display: flex;
155
+ gap: 18px;
156
+ font-size: 14px;
157
+ color: var(--muted);
158
+ }
159
+
160
+ .topnav a {
161
+ position: relative;
162
+ padding-bottom: 4px;
163
+ }
164
+
165
+ .topnav a::after {
166
+ content: "";
167
+ position: absolute;
168
+ left: 0;
169
+ bottom: 0;
170
+ width: 0;
171
+ height: 2px;
172
+ background: var(--neon-pink);
173
+ transition: width 0.3s ease;
174
+ }
175
+
176
+ .topnav a:hover::after {
177
+ width: 100%;
178
+ }
179
+
180
+ .cta {
181
+ padding: 10px 18px;
182
+ border-radius: 999px;
183
+ border: 1px solid var(--border);
184
+ font-weight: 600;
185
+ background: linear-gradient(120deg, rgba(160, 123, 255, 0.2), rgba(255, 95, 210, 0.2));
186
+ box-shadow: var(--glow);
187
+ transition: transform 0.2s ease, box-shadow 0.2s ease, filter 0.2s ease;
188
+ }
189
+
190
+ .cta--primary {
191
+ background: linear-gradient(120deg, var(--neon-pink), var(--neon-purple));
192
+ color: #0b0617;
193
+ }
194
+
195
+ .cta--ghost {
196
+ background: transparent;
197
+ border: 1px solid rgba(255, 95, 210, 0.6);
198
+ }
199
+
200
+ .cta:hover {
201
+ transform: translateY(-2px) scale(1.02);
202
+ box-shadow: 0 0 28px rgba(255, 95, 210, 0.45);
203
+ filter: brightness(1.08);
204
+ }
205
+
206
+ .cta:active {
207
+ transform: translateY(0) scale(0.99);
208
+ box-shadow: 0 0 18px rgba(255, 95, 210, 0.35);
209
+ }
210
+
211
+ main {
212
+ position: relative;
213
+ z-index: 1;
214
+ padding: 0 6vw 80px;
215
+ }
216
+
217
+ .hero {
218
+ display: grid;
219
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
220
+ gap: 48px;
221
+ padding: 80px 0 40px;
222
+ align-items: center;
223
+ }
224
+
225
+ .hero h1 {
226
+ font-size: clamp(32px, 5vw, 56px);
227
+ line-height: 1.05;
228
+ margin: 0 0 20px;
229
+ background: linear-gradient(90deg, #ffffff, #ffb6f5 35%, #7cf6ff 65%, #ffffff);
230
+ -webkit-background-clip: text;
231
+ background-clip: text;
232
+ color: transparent;
233
+ }
234
+
235
+ .hero__lead {
236
+ color: var(--muted);
237
+ font-size: 18px;
238
+ line-height: 1.6;
239
+ }
240
+
241
+ .hero__actions {
242
+ display: flex;
243
+ gap: 16px;
244
+ margin: 24px 0;
245
+ }
246
+
247
+ .hero__meta {
248
+ display: flex;
249
+ gap: 16px;
250
+ flex-wrap: wrap;
251
+ font-size: 13px;
252
+ color: var(--muted);
253
+ }
254
+
255
+ .eyebrow {
256
+ font-family: "Share Tech Mono", "Courier New", monospace;
257
+ letter-spacing: 0.18em;
258
+ text-transform: uppercase;
259
+ font-size: 12px;
260
+ color: var(--neon-green);
261
+ margin-bottom: 12px;
262
+ }
263
+
264
+ .hero__preview {
265
+ position: relative;
266
+ display: grid;
267
+ place-items: center;
268
+ }
269
+
270
+ .terminal-card {
271
+ background: var(--card);
272
+ border-radius: 18px;
273
+ border: 1px solid rgba(255, 255, 255, 0.12);
274
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.45);
275
+ width: min(100%, 420px);
276
+ overflow: hidden;
277
+ }
278
+
279
+ .terminal-card__bar {
280
+ display: flex;
281
+ gap: 8px;
282
+ padding: 12px 16px;
283
+ background: rgba(9, 5, 19, 0.8);
284
+ }
285
+
286
+ .terminal-card__bar span {
287
+ width: 12px;
288
+ height: 12px;
289
+ border-radius: 50%;
290
+ background: rgba(255, 95, 210, 0.6);
291
+ box-shadow: 0 0 8px rgba(255, 95, 210, 0.6);
292
+ }
293
+
294
+ .terminal-card__bar span:nth-child(2) {
295
+ background: rgba(82, 242, 255, 0.7);
296
+ }
297
+
298
+ .terminal-card__bar span:nth-child(3) {
299
+ background: rgba(52, 245, 180, 0.7);
300
+ }
301
+
302
+ .terminal-card__body {
303
+ font-family: "Share Tech Mono", "Courier New", monospace;
304
+ padding: 20px;
305
+ font-size: 14px;
306
+ color: #e9d7ff;
307
+ background: linear-gradient(180deg, rgba(17, 9, 29, 0.9), rgba(9, 5, 19, 0.95));
308
+ }
309
+
310
+ .typewriter {
311
+ min-height: 110px;
312
+ line-height: 1.6;
313
+ white-space: pre-wrap;
314
+ }
315
+
316
+ .typewriter__line {
317
+ color: #e9d7ff;
318
+ }
319
+
320
+ .typewriter__line--prompt {
321
+ color: var(--neon-blue);
322
+ }
323
+
324
+ .typewriter__line--output {
325
+ color: #f7b6ff;
326
+ }
327
+
328
+ .typewriter__cursor {
329
+ display: inline-block;
330
+ width: 10px;
331
+ height: 16px;
332
+ margin-left: 4px;
333
+ background: var(--neon-green);
334
+ animation: caret 0.9s steps(1) infinite;
335
+ vertical-align: text-bottom;
336
+ }
337
+
338
+ @keyframes caret {
339
+ 0%,
340
+ 49% {
341
+ opacity: 1;
342
+ }
343
+ 50%,
344
+ 100% {
345
+ opacity: 0;
346
+ }
347
+ }
348
+
349
+ .prompt {
350
+ color: var(--neon-blue);
351
+ }
352
+
353
+ .output {
354
+ color: #f7b6ff;
355
+ }
356
+
357
+ .section {
358
+ padding: 80px 0;
359
+ }
360
+
361
+ .section--alt {
362
+ background: rgba(20, 8, 38, 0.6);
363
+ border: 1px solid var(--border);
364
+ border-radius: 24px;
365
+ padding: 80px 6vw;
366
+ margin: 0 -6vw;
367
+ }
368
+
369
+ .section__header h2 {
370
+ margin: 0 0 12px;
371
+ font-size: clamp(26px, 4vw, 40px);
372
+ }
373
+
374
+ .section__lead {
375
+ color: var(--muted);
376
+ font-size: 16px;
377
+ max-width: 640px;
378
+ }
379
+
380
+ .grid {
381
+ display: grid;
382
+ gap: 24px;
383
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
384
+ margin-top: 36px;
385
+ }
386
+
387
+ .card {
388
+ padding: 24px;
389
+ background: var(--card);
390
+ border: 1px solid var(--border);
391
+ border-radius: 16px;
392
+ box-shadow: 0 12px 24px rgba(0, 0, 0, 0.3);
393
+ backdrop-filter: blur(4px);
394
+ }
395
+
396
+ .split {
397
+ display: grid;
398
+ gap: 28px;
399
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
400
+ margin-top: 36px;
401
+ }
402
+
403
+ .code-block {
404
+ background: #0a0618;
405
+ border-radius: 16px;
406
+ border: 1px solid rgba(82, 242, 255, 0.2);
407
+ padding: 18px 20px;
408
+ box-shadow: inset 0 0 24px rgba(82, 242, 255, 0.08);
409
+ }
410
+
411
+ .code-block h3 {
412
+ margin-top: 0;
413
+ }
414
+
415
+ .code-block pre {
416
+ margin: 12px 0 0;
417
+ font-family: "Share Tech Mono", "Courier New", monospace;
418
+ color: #bfefff;
419
+ white-space: pre-wrap;
420
+ }
421
+
422
+ .code-block--wide {
423
+ max-width: 860px;
424
+ }
425
+
426
+ .theme-list {
427
+ display: flex;
428
+ flex-wrap: wrap;
429
+ gap: 12px;
430
+ margin: 24px 0;
431
+ }
432
+
433
+ .theme-pill {
434
+ padding: 8px 16px;
435
+ border-radius: 999px;
436
+ border: 1px solid rgba(255, 95, 210, 0.4);
437
+ font-size: 14px;
438
+ background: rgba(255, 95, 210, 0.08);
439
+ text-transform: uppercase;
440
+ letter-spacing: 0.08em;
441
+ }
442
+
443
+ .footer {
444
+ padding: 40px 6vw 60px;
445
+ text-align: center;
446
+ color: var(--muted);
447
+ border-top: 1px solid var(--border);
448
+ }
449
+
450
+ .fade-in {
451
+ opacity: 0;
452
+ transform: translateY(24px);
453
+ transition: opacity 0.8s ease, transform 0.8s ease;
454
+ }
455
+
456
+ .fade-in.is-visible {
457
+ opacity: 1;
458
+ transform: translateY(0);
459
+ }
460
+
461
+ @keyframes sunfloat {
462
+ 0% {
463
+ transform: translateY(0);
464
+ }
465
+ 50% {
466
+ transform: translateY(-10px);
467
+ }
468
+ 100% {
469
+ transform: translateY(0);
470
+ }
471
+ }
472
+
473
+ @media (max-width: 900px) {
474
+ .topbar {
475
+ flex-wrap: wrap;
476
+ gap: 12px;
477
+ }
478
+
479
+ .topnav {
480
+ flex-wrap: wrap;
481
+ justify-content: center;
482
+ }
483
+
484
+ .hero {
485
+ padding-top: 50px;
486
+ }
487
+
488
+ .section--alt {
489
+ padding: 60px 6vw;
490
+ }
491
+ }
492
+
493
+ @media (max-width: 600px) {
494
+ .hero__actions {
495
+ flex-direction: column;
496
+ align-items: flex-start;
497
+ }
498
+
499
+ .topbar {
500
+ padding: 18px 6vw;
501
+ }
502
+
503
+ .topnav {
504
+ gap: 12px;
505
+ font-size: 13px;
506
+ }
507
+ }
data/examples/simple.yml CHANGED
@@ -11,11 +11,11 @@ lines:
11
11
  command: "gem install shellfie"
12
12
 
13
13
  - output: |
14
- Fetching shellfie-0.1.0.gem
15
- Successfully installed shellfie-0.1.0
14
+ Fetching shellfie-1.0.0.gem
15
+ Successfully installed shellfie-1.0.0
16
16
  1 gem installed
17
17
 
18
18
  - prompt: "$ "
19
19
  command: "shellfie --version"
20
20
 
21
- - output: "shellfie 0.1.0"
21
+ - output: "shellfie 1.0.0"