screenshot-beautify 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.
package/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # screenshot-beautify
2
+
3
+ A CLI tool to beautify screenshots with macOS-style window frames, shadows, and gradient backgrounds.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g screenshot-beautify
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Single File
14
+
15
+ ```bash
16
+ screenshot-beautify image.png
17
+ screenshot-beautify image.png --preset sunset
18
+ screenshot-beautify image.png --background ~/Pictures/wallpaper.jpg
19
+ screenshot-beautify image.png -o output.png --padding 100
20
+ ```
21
+
22
+ ### Watch Mode
23
+
24
+ Automatically beautify screenshots as they're added to a directory:
25
+
26
+ ```bash
27
+ screenshot-beautify watch ~/Desktop ~/Desktop/beautified --preset ocean
28
+ ```
29
+
30
+ ### System Tray Mode (macOS)
31
+
32
+ Run in the background with a menu bar icon:
33
+
34
+ ```bash
35
+ screenshot-beautify tray ~/Desktop ~/Desktop/beautified --preset sunset
36
+ ```
37
+
38
+ The tray icon lets you:
39
+ - See status and processed count
40
+ - Start/Stop watching
41
+ - Open output folder
42
+ - Quit the app
43
+
44
+ ## Options
45
+
46
+ | Option | Description |
47
+ |--------|-------------|
48
+ | `--preset <name>` | Use a built-in gradient preset |
49
+ | `--background <path>` | Use a custom image as background |
50
+ | `--padding <number>` | Padding around screenshot (default: 80) |
51
+ | `-o, --output <path>` | Output file path |
52
+ | `--delete-original` | Delete original after beautifying |
53
+ | `--foreground` | Run tray in foreground (for debugging) |
54
+
55
+ ## Presets
56
+
57
+ List available presets:
58
+
59
+ ```bash
60
+ screenshot-beautify presets
61
+ ```
62
+
63
+ **Available presets:**
64
+
65
+ | Category | Presets |
66
+ |----------|---------|
67
+ | Warm | `sunset`, `sunrise`, `peach` |
68
+ | Cool | `ocean`, `sky`, `northern` |
69
+ | Dark | `charcoal`, `midnight`, `space` |
70
+ | Vibrant | `neon`, `fire`, `aurora` |
71
+ | Soft | `lavender`, `mint`, `rose` |
72
+
73
+ ## Examples
74
+
75
+ ```bash
76
+ # Basic usage
77
+ screenshot-beautify screenshot.png
78
+
79
+ # With sunset gradient
80
+ screenshot-beautify screenshot.png --preset sunset
81
+
82
+ # Watch Desktop and save to a subfolder
83
+ screenshot-beautify watch ~/Desktop ~/Desktop/beautified --preset ocean
84
+
85
+ # Run in system tray with custom wallpaper background
86
+ screenshot-beautify tray ~/Desktop ~/Desktop/beautified --background ~/wallpaper.jpg
87
+
88
+ # Custom padding
89
+ screenshot-beautify screenshot.png --padding 120 --preset midnight
90
+ ```
91
+
92
+ ## Output
93
+
94
+ The tool adds:
95
+ - macOS-style window frame with traffic light buttons
96
+ - Rounded corners
97
+ - Drop shadow
98
+ - Gradient or custom image background
99
+
100
+ ## License
101
+
102
+ MIT
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,647 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/beautify.ts
7
+ import sharp5 from "sharp";
8
+
9
+ // src/background.ts
10
+ import sharp2 from "sharp";
11
+
12
+ // src/presets.ts
13
+ import sharp from "sharp";
14
+ var GRADIENT_PRESETS = {
15
+ // Warm tones
16
+ sunset: {
17
+ name: "Sunset",
18
+ colors: ["#ff9a9e", "#fecfef", "#fecfef", "#fad0c4"],
19
+ angle: 135
20
+ },
21
+ sunrise: {
22
+ name: "Sunrise",
23
+ colors: ["#f093fb", "#f5576c"],
24
+ angle: 135
25
+ },
26
+ peach: {
27
+ name: "Peach",
28
+ colors: ["#ffecd2", "#fcb69f"],
29
+ angle: 135
30
+ },
31
+ // Cool tones
32
+ ocean: {
33
+ name: "Ocean",
34
+ colors: ["#667eea", "#764ba2"],
35
+ angle: 135
36
+ },
37
+ sky: {
38
+ name: "Sky",
39
+ colors: ["#a1c4fd", "#c2e9fb"],
40
+ angle: 135
41
+ },
42
+ northern: {
43
+ name: "Northern Lights",
44
+ colors: ["#43e97b", "#38f9d7"],
45
+ angle: 135
46
+ },
47
+ // Dark tones
48
+ charcoal: {
49
+ name: "Charcoal",
50
+ colors: ["#2d3436", "#636e72"],
51
+ angle: 135
52
+ },
53
+ midnight: {
54
+ name: "Midnight",
55
+ colors: ["#232526", "#414345"],
56
+ angle: 135
57
+ },
58
+ space: {
59
+ name: "Space",
60
+ colors: ["#0f0c29", "#302b63", "#24243e"],
61
+ angle: 135
62
+ },
63
+ // Vibrant
64
+ neon: {
65
+ name: "Neon",
66
+ colors: ["#fc00ff", "#00dbde"],
67
+ angle: 135
68
+ },
69
+ fire: {
70
+ name: "Fire",
71
+ colors: ["#f12711", "#f5af19"],
72
+ angle: 135
73
+ },
74
+ aurora: {
75
+ name: "Aurora",
76
+ colors: ["#00c6fb", "#005bea"],
77
+ angle: 135
78
+ },
79
+ // Soft/Muted
80
+ lavender: {
81
+ name: "Lavender",
82
+ colors: ["#e0c3fc", "#8ec5fc"],
83
+ angle: 135
84
+ },
85
+ mint: {
86
+ name: "Mint",
87
+ colors: ["#d4fc79", "#96e6a1"],
88
+ angle: 135
89
+ },
90
+ rose: {
91
+ name: "Rose",
92
+ colors: ["#eecda3", "#ef629f"],
93
+ angle: 135
94
+ }
95
+ };
96
+ function listPresets() {
97
+ return Object.keys(GRADIENT_PRESETS);
98
+ }
99
+ async function createPresetBackground(presetName, width, height) {
100
+ const preset = GRADIENT_PRESETS[presetName];
101
+ if (!preset) {
102
+ throw new Error(`Unknown preset: ${presetName}. Available: ${listPresets().join(", ")}`);
103
+ }
104
+ const { colors, angle = 135 } = preset;
105
+ const angleRad = angle * Math.PI / 180;
106
+ const x1 = Math.round(50 - Math.cos(angleRad) * 50);
107
+ const y1 = Math.round(50 - Math.sin(angleRad) * 50);
108
+ const x2 = Math.round(50 + Math.cos(angleRad) * 50);
109
+ const y2 = Math.round(50 + Math.sin(angleRad) * 50);
110
+ const stops = colors.map((color, i) => {
111
+ const offset = i / (colors.length - 1) * 100;
112
+ return `<stop offset="${offset}%" style="stop-color:${color};stop-opacity:1" />`;
113
+ }).join("\n ");
114
+ const svg = `
115
+ <svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
116
+ <defs>
117
+ <linearGradient id="grad" x1="${x1}%" y1="${y1}%" x2="${x2}%" y2="${y2}%">
118
+ ${stops}
119
+ </linearGradient>
120
+ </defs>
121
+ <rect width="100%" height="100%" fill="url(#grad)" />
122
+ </svg>
123
+ `;
124
+ return sharp(Buffer.from(svg)).png().toBuffer();
125
+ }
126
+
127
+ // src/background.ts
128
+ async function createBackground(options) {
129
+ const {
130
+ width,
131
+ height,
132
+ imagePath,
133
+ preset,
134
+ // Dark charcoal gradient as default
135
+ gradientStart = "#2d3436",
136
+ gradientEnd = "#636e72"
137
+ } = options;
138
+ if (imagePath) {
139
+ return sharp2(imagePath).resize(width, height, {
140
+ fit: "cover",
141
+ position: "center"
142
+ }).png().toBuffer();
143
+ }
144
+ if (preset && GRADIENT_PRESETS[preset]) {
145
+ return createPresetBackground(preset, width, height);
146
+ }
147
+ const svg = `
148
+ <svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
149
+ <defs>
150
+ <linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
151
+ <stop offset="0%" style="stop-color:${gradientStart};stop-opacity:1" />
152
+ <stop offset="100%" style="stop-color:${gradientEnd};stop-opacity:1" />
153
+ </linearGradient>
154
+ </defs>
155
+ <rect width="100%" height="100%" fill="url(#grad)" />
156
+ </svg>
157
+ `;
158
+ return sharp2(Buffer.from(svg)).png().toBuffer();
159
+ }
160
+
161
+ // src/frame.ts
162
+ import sharp3 from "sharp";
163
+ async function createFrame(options) {
164
+ const {
165
+ width,
166
+ height,
167
+ titleBarHeight = 32,
168
+ cornerRadius = 10,
169
+ buttonSize = 12
170
+ } = options;
171
+ const buttonY = titleBarHeight / 2;
172
+ const buttonSpacing = 20;
173
+ const firstButtonX = 16;
174
+ const closeColor = "#FF5F56";
175
+ const minimizeColor = "#FFBD2E";
176
+ const maximizeColor = "#27C93F";
177
+ const svg = `
178
+ <svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
179
+ <defs>
180
+ <clipPath id="roundedCorners">
181
+ <rect x="0" y="0" width="${width}" height="${height}" rx="${cornerRadius}" ry="${cornerRadius}" />
182
+ </clipPath>
183
+ </defs>
184
+
185
+ <!-- Window background with rounded corners -->
186
+ <rect x="0" y="0" width="${width}" height="${height}" rx="${cornerRadius}" ry="${cornerRadius}" fill="#FFFFFF" />
187
+
188
+ <!-- Title bar -->
189
+ <rect x="0" y="0" width="${width}" height="${titleBarHeight}" rx="${cornerRadius}" ry="${cornerRadius}" fill="#E8E8E8" />
190
+ <!-- Fill bottom corners of title bar -->
191
+ <rect x="0" y="${cornerRadius}" width="${width}" height="${titleBarHeight - cornerRadius}" fill="#E8E8E8" />
192
+
193
+ <!-- Window buttons -->
194
+ <circle cx="${firstButtonX}" cy="${buttonY}" r="${buttonSize / 2}" fill="${closeColor}" />
195
+ <circle cx="${firstButtonX + buttonSpacing}" cy="${buttonY}" r="${buttonSize / 2}" fill="${minimizeColor}" />
196
+ <circle cx="${firstButtonX + buttonSpacing * 2}" cy="${buttonY}" r="${buttonSize / 2}" fill="${maximizeColor}" />
197
+ </svg>
198
+ `;
199
+ return sharp3(Buffer.from(svg)).png().toBuffer();
200
+ }
201
+
202
+ // src/shadow.ts
203
+ import sharp4 from "sharp";
204
+ async function createShadow(options) {
205
+ const {
206
+ width,
207
+ height,
208
+ blur = 30,
209
+ opacity = 0.5,
210
+ cornerRadius = 10
211
+ } = options;
212
+ const shadowWidth = width + blur;
213
+ const shadowHeight = height + blur;
214
+ const offsetForSize = blur / 2;
215
+ const svg = `
216
+ <svg width="${shadowWidth}" height="${shadowHeight}" xmlns="http://www.w3.org/2000/svg">
217
+ <rect
218
+ x="${offsetForSize}"
219
+ y="${offsetForSize}"
220
+ width="${width}"
221
+ height="${height}"
222
+ rx="${cornerRadius}"
223
+ ry="${cornerRadius}"
224
+ fill="rgba(0, 0, 0, ${opacity})"
225
+ />
226
+ </svg>
227
+ `;
228
+ return sharp4(Buffer.from(svg)).blur(blur).png().toBuffer();
229
+ }
230
+
231
+ // src/beautify.ts
232
+ async function beautify(inputPath, outputPath, options = {}) {
233
+ const {
234
+ padding = 80,
235
+ cornerRadius = 10,
236
+ titleBarHeight = 32,
237
+ shadowBlur = 30,
238
+ shadowOffsetX = 0,
239
+ shadowOffsetY = 20,
240
+ backgroundImage,
241
+ backgroundPreset,
242
+ gradientStart = "#2d3436",
243
+ gradientEnd = "#636e72"
244
+ } = options;
245
+ const inputImage = sharp5(inputPath);
246
+ const metadata = await inputImage.metadata();
247
+ if (!metadata.width || !metadata.height) {
248
+ throw new Error("Could not determine image dimensions");
249
+ }
250
+ const imgWidth = metadata.width;
251
+ const imgHeight = metadata.height;
252
+ const framedWidth = imgWidth;
253
+ const framedHeight = imgHeight + titleBarHeight;
254
+ const totalWidth = framedWidth + padding * 2;
255
+ const totalHeight = framedHeight + padding * 2;
256
+ const shadowPadding = shadowBlur * 2 + Math.abs(shadowOffsetY);
257
+ const canvasWidth = totalWidth + shadowPadding;
258
+ const canvasHeight = totalHeight + shadowPadding;
259
+ const background = await createBackground({
260
+ width: canvasWidth,
261
+ height: canvasHeight,
262
+ imagePath: backgroundImage,
263
+ preset: backgroundPreset,
264
+ gradientStart,
265
+ gradientEnd
266
+ });
267
+ const frame = await createFrame({
268
+ width: framedWidth,
269
+ height: framedHeight,
270
+ titleBarHeight,
271
+ cornerRadius
272
+ });
273
+ const shadow = await createShadow({
274
+ width: framedWidth,
275
+ height: framedHeight,
276
+ blur: shadowBlur,
277
+ cornerRadius
278
+ });
279
+ const frameX = padding + shadowPadding / 2;
280
+ const frameY = padding + shadowPadding / 2;
281
+ const shadowX = frameX + shadowOffsetX - shadowBlur / 2;
282
+ const shadowY = frameY + shadowOffsetY - shadowBlur / 2;
283
+ const screenshotX = frameX;
284
+ const screenshotY = frameY + titleBarHeight;
285
+ const cornerMask = Buffer.from(`
286
+ <svg width="${imgWidth}" height="${imgHeight}" xmlns="http://www.w3.org/2000/svg">
287
+ <rect
288
+ x="0"
289
+ y="0"
290
+ width="${imgWidth}"
291
+ height="${imgHeight}"
292
+ rx="${cornerRadius}"
293
+ ry="${cornerRadius}"
294
+ fill="white"
295
+ />
296
+ <!-- Fill top corners to make them square (only bottom should be rounded) -->
297
+ <rect x="0" y="0" width="${cornerRadius}" height="${cornerRadius}" fill="white"/>
298
+ <rect x="${imgWidth - cornerRadius}" y="0" width="${cornerRadius}" height="${cornerRadius}" fill="white"/>
299
+ </svg>
300
+ `);
301
+ const roundedScreenshot = await sharp5(inputPath).ensureAlpha().composite([
302
+ {
303
+ input: await sharp5(cornerMask).png().toBuffer(),
304
+ blend: "dest-in"
305
+ }
306
+ ]).png().toBuffer();
307
+ await sharp5(background).composite([
308
+ // Shadow layer
309
+ {
310
+ input: shadow,
311
+ left: Math.round(shadowX),
312
+ top: Math.round(shadowY)
313
+ },
314
+ // Window frame
315
+ {
316
+ input: frame,
317
+ left: Math.round(frameX),
318
+ top: Math.round(frameY)
319
+ },
320
+ // Screenshot
321
+ {
322
+ input: roundedScreenshot,
323
+ left: Math.round(screenshotX),
324
+ top: Math.round(screenshotY)
325
+ }
326
+ ]).png().toFile(outputPath);
327
+ }
328
+
329
+ // src/tray.ts
330
+ import { createRequire } from "module";
331
+ import chokidar from "chokidar";
332
+ import { resolve, basename, extname, join } from "path";
333
+ import { existsSync, mkdirSync } from "fs";
334
+ import { exec } from "child_process";
335
+ var require2 = createRequire(import.meta.url);
336
+ var SysTray = require2("systray2").default;
337
+ var IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".webp", ".gif"];
338
+ function isImageFile(filePath) {
339
+ const ext = extname(filePath).toLowerCase();
340
+ return IMAGE_EXTENSIONS.includes(ext);
341
+ }
342
+ function isAlreadyBeautified(filePath) {
343
+ return basename(filePath).includes("_beautified");
344
+ }
345
+ var ICON_BASE64 = `iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAACXBIWXMAAAsTAAALEwEAmpwYAAAA6UlEQVR4nO2TywmDQBCG7UkF7SAhJ7UD12JUxEcP7lW0hpAifJySnLQG/YMDOYQVkTWEQPLBD8ssfMwMu4ryZw0AF7xyVt4BFvg+MYATgDvkuQE4Lolv2M910/hPmqZBnufgnKOua6yxSTxNE9I0ha7rUFWVomkafN+nO2lxURQki6IIwzBQwjCkWlmW8mLLsuB5njAFYwy2bcuLDcNAkiRCPY5jmKa5r2PGmNCx67pwHEdezDmnfc577fueEgQB1aqqkheP4yi8ivmcZdmidLP4Sdu21P2cruuwxkd/3nGn/ArgIIiVn+cBsJOTNPMb6GYAAAAASUVORK5CYII=`;
346
+ var watcher = null;
347
+ var processedCount = 0;
348
+ var systray = null;
349
+ var trayReady = false;
350
+ var config;
351
+ async function processFile(filePath) {
352
+ if (!isImageFile(filePath) || isAlreadyBeautified(filePath)) {
353
+ return;
354
+ }
355
+ const fileName = basename(filePath, extname(filePath));
356
+ const beautifiedPath = join(config.outputPath, `${fileName}_beautified.png`);
357
+ try {
358
+ await beautify(filePath, beautifiedPath, {
359
+ padding: config.padding || 80,
360
+ backgroundImage: config.backgroundImage,
361
+ backgroundPreset: config.backgroundPreset
362
+ });
363
+ processedCount++;
364
+ updateMenu();
365
+ if (config.deleteOriginal) {
366
+ const { unlink } = await import("fs/promises");
367
+ await unlink(filePath);
368
+ }
369
+ } catch {
370
+ }
371
+ }
372
+ function startWatching() {
373
+ if (watcher) {
374
+ return;
375
+ }
376
+ watcher = chokidar.watch(config.sourcePath, {
377
+ ignored: /(^|[\/\\])\../,
378
+ persistent: true,
379
+ ignoreInitial: true,
380
+ awaitWriteFinish: {
381
+ stabilityThreshold: 500,
382
+ pollInterval: 100
383
+ }
384
+ });
385
+ watcher.on("ready", () => {
386
+ updateMenu();
387
+ });
388
+ watcher.on("add", processFile);
389
+ watcher.on("error", () => {
390
+ });
391
+ }
392
+ function stopWatching() {
393
+ if (watcher) {
394
+ watcher.close();
395
+ watcher = null;
396
+ updateMenu();
397
+ }
398
+ }
399
+ function openOutputFolder() {
400
+ const cmd = process.platform === "darwin" ? `open "${config.outputPath}"` : process.platform === "win32" ? `explorer "${config.outputPath}"` : `xdg-open "${config.outputPath}"`;
401
+ exec(cmd);
402
+ }
403
+ function updateMenu() {
404
+ if (!systray || !trayReady) return;
405
+ const isWatching = watcher !== null;
406
+ try {
407
+ systray.sendAction({
408
+ type: "update-item",
409
+ item: {
410
+ title: isWatching ? `\u2713 Watching (${processedCount} processed)` : "\u25CB Not watching",
411
+ enabled: false
412
+ },
413
+ seq_id: 0
414
+ });
415
+ systray.sendAction({
416
+ type: "update-item",
417
+ item: {
418
+ title: isWatching ? "Stop Watching" : "Start Watching",
419
+ enabled: true
420
+ },
421
+ seq_id: 1
422
+ });
423
+ } catch {
424
+ }
425
+ }
426
+ async function createTray() {
427
+ systray = new SysTray({
428
+ menu: {
429
+ icon: ICON_BASE64,
430
+ title: "",
431
+ tooltip: "Screenshot Beautify",
432
+ items: [
433
+ {
434
+ title: "\u25CB Not watching",
435
+ enabled: false
436
+ },
437
+ {
438
+ title: "Start Watching",
439
+ enabled: true
440
+ },
441
+ {
442
+ title: "Open Output Folder",
443
+ enabled: true
444
+ },
445
+ SysTray.separator,
446
+ {
447
+ title: "Quit",
448
+ enabled: true
449
+ }
450
+ ]
451
+ },
452
+ debug: false,
453
+ copyDir: true
454
+ });
455
+ await systray.ready();
456
+ trayReady = true;
457
+ systray.onClick((action) => {
458
+ switch (action.seq_id) {
459
+ case 1:
460
+ if (watcher) {
461
+ stopWatching();
462
+ } else {
463
+ startWatching();
464
+ }
465
+ break;
466
+ case 2:
467
+ openOutputFolder();
468
+ break;
469
+ case 4:
470
+ stopWatching();
471
+ systray?.kill(false);
472
+ process.exit(0);
473
+ break;
474
+ }
475
+ });
476
+ startWatching();
477
+ }
478
+ async function startTray(options) {
479
+ config = {
480
+ sourcePath: resolve(options.sourcePath),
481
+ outputPath: resolve(options.outputPath),
482
+ padding: options.padding || 80,
483
+ backgroundImage: options.backgroundImage,
484
+ backgroundPreset: options.backgroundPreset,
485
+ deleteOriginal: options.deleteOriginal || false
486
+ };
487
+ if (!existsSync(config.sourcePath)) {
488
+ console.error(`Source directory does not exist: ${config.sourcePath}`);
489
+ process.exit(1);
490
+ }
491
+ if (!existsSync(config.outputPath)) {
492
+ mkdirSync(config.outputPath, { recursive: true });
493
+ }
494
+ await createTray();
495
+ }
496
+
497
+ // src/index.ts
498
+ import { resolve as resolve2, basename as basename2, dirname, extname as extname2, join as join2 } from "path";
499
+ import chokidar2 from "chokidar";
500
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
501
+ var IMAGE_EXTENSIONS2 = [".png", ".jpg", ".jpeg", ".webp", ".gif"];
502
+ function isImageFile2(filePath) {
503
+ const ext = extname2(filePath).toLowerCase();
504
+ return IMAGE_EXTENSIONS2.includes(ext);
505
+ }
506
+ function isAlreadyBeautified2(filePath) {
507
+ return basename2(filePath).includes("_beautified");
508
+ }
509
+ var program = new Command();
510
+ program.name("screenshot-beautify").description("Beautify screenshots with window frames, backgrounds, and shadows").version("1.0.0");
511
+ program.command("presets").description("List available background presets").action(() => {
512
+ console.log("Available background presets:\n");
513
+ const presets = listPresets();
514
+ presets.forEach((name) => {
515
+ console.log(` - ${name}`);
516
+ });
517
+ console.log("\nUsage: screenshot-beautify <input> --preset sunset");
518
+ });
519
+ program.command("watch <source> <output>").description("Watch a directory and auto-beautify new screenshots").option("--padding <number>", "Padding around the screenshot", "80").option("--background <path>", "Background image path").option("--preset <name>", "Background preset (run 'presets' to see options)").option("--delete-original", "Delete original file after beautifying", false).action(async (source, output, opts) => {
520
+ try {
521
+ const sourcePath = resolve2(source);
522
+ const outputPath = resolve2(output);
523
+ const padding = parseInt(opts.padding, 10);
524
+ const backgroundImage = opts.background ? resolve2(opts.background) : void 0;
525
+ const backgroundPreset = opts.preset;
526
+ if (!existsSync2(sourcePath)) {
527
+ console.error(`Source directory does not exist: ${sourcePath}`);
528
+ process.exit(1);
529
+ }
530
+ if (!existsSync2(outputPath)) {
531
+ mkdirSync2(outputPath, { recursive: true });
532
+ console.log(`Created output directory: ${outputPath}`);
533
+ }
534
+ console.log(`
535
+ \u{1F50D} Watching for screenshots in: ${sourcePath}`);
536
+ console.log(`\u{1F4C1} Beautified screenshots will be saved to: ${outputPath}`);
537
+ console.log(`\u2699\uFE0F Options: padding=${padding}${backgroundPreset ? `, preset=${backgroundPreset}` : ""}${backgroundImage ? `, background=${backgroundImage}` : ""}${opts.deleteOriginal ? ", delete-original=true" : ""}`);
538
+ console.log(`
539
+ \u2705 File watcher is now running...`);
540
+ console.log(` Press Ctrl+C to stop
541
+ `);
542
+ const watcher2 = chokidar2.watch(sourcePath, {
543
+ ignored: /(^|[\/\\])\../,
544
+ persistent: true,
545
+ ignoreInitial: true,
546
+ awaitWriteFinish: {
547
+ stabilityThreshold: 500,
548
+ pollInterval: 100
549
+ }
550
+ });
551
+ const timestamp = () => (/* @__PURE__ */ new Date()).toLocaleTimeString();
552
+ const processFile2 = async (filePath) => {
553
+ if (!isImageFile2(filePath) || isAlreadyBeautified2(filePath)) {
554
+ return;
555
+ }
556
+ const fileName = basename2(filePath, extname2(filePath));
557
+ const beautifiedPath = join2(outputPath, `${fileName}_beautified.png`);
558
+ try {
559
+ console.log(`[${timestamp()}] \u{1F4F8} New screenshot detected: ${basename2(filePath)}`);
560
+ await beautify(filePath, beautifiedPath, { padding, backgroundImage, backgroundPreset });
561
+ console.log(`[${timestamp()}] \u2728 Beautified: ${basename2(beautifiedPath)}`);
562
+ if (opts.deleteOriginal) {
563
+ const { unlink } = await import("fs/promises");
564
+ await unlink(filePath);
565
+ console.log(`[${timestamp()}] \u{1F5D1}\uFE0F Deleted original: ${basename2(filePath)}`);
566
+ }
567
+ } catch (err) {
568
+ console.error(`[${timestamp()}] \u274C Failed to beautify ${basename2(filePath)}:`, err instanceof Error ? err.message : err);
569
+ }
570
+ };
571
+ watcher2.on("ready", () => {
572
+ console.log(`[${timestamp()}] \u{1F440} Watching for new files...
573
+ `);
574
+ });
575
+ watcher2.on("add", processFile2);
576
+ watcher2.on("error", (error) => {
577
+ console.error(`[${timestamp()}] \u274C Watcher error:`, error);
578
+ });
579
+ process.on("SIGINT", () => {
580
+ console.log("\n\u{1F6D1} Stopping watcher...");
581
+ watcher2.close();
582
+ process.exit(0);
583
+ });
584
+ } catch (error) {
585
+ console.error("Error:", error instanceof Error ? error.message : error);
586
+ process.exit(1);
587
+ }
588
+ });
589
+ program.command("tray <source> <output>").description("Run as a system tray app with auto-beautify").option("--padding <number>", "Padding around the screenshot", "80").option("--background <path>", "Background image path").option("--preset <name>", "Background preset (run 'presets' to see options)").option("--delete-original", "Delete original file after beautifying", false).option("--foreground", "Run in foreground (don't detach)", false).option("--_daemon", "Internal: running as daemon", false).action(async (source, output, opts) => {
590
+ const padding = parseInt(opts.padding, 10);
591
+ const backgroundImage = opts.background ? resolve2(opts.background) : void 0;
592
+ const backgroundPreset = opts.preset;
593
+ if (!opts._daemon && !opts.foreground) {
594
+ const { spawn } = await import("child_process");
595
+ const args = [
596
+ process.argv[1],
597
+ "tray",
598
+ source,
599
+ output,
600
+ "--padding",
601
+ opts.padding,
602
+ "--_daemon"
603
+ ];
604
+ if (backgroundImage) args.push("--background", backgroundImage);
605
+ if (backgroundPreset) args.push("--preset", backgroundPreset);
606
+ if (opts.deleteOriginal) args.push("--delete-original");
607
+ const child = spawn(process.execPath, args, {
608
+ detached: true,
609
+ stdio: "ignore",
610
+ env: process.env
611
+ });
612
+ child.unref();
613
+ console.log("\u{1F4F7} Screenshot Beautify started in system tray");
614
+ console.log(` Watching: ${resolve2(source)}`);
615
+ console.log(` Output: ${resolve2(output)}`);
616
+ console.log("\nThe app is now running in the background.");
617
+ console.log("Click the tray icon to control it, or use 'Quit' to stop.");
618
+ process.exit(0);
619
+ }
620
+ await startTray({
621
+ sourcePath: source,
622
+ outputPath: output,
623
+ padding,
624
+ backgroundImage,
625
+ backgroundPreset,
626
+ deleteOriginal: opts.deleteOriginal
627
+ });
628
+ });
629
+ program.command("file <input>", { isDefault: true }).description("Beautify a single screenshot").option("-o, --output <path>", "Output file path").option("--padding <number>", "Padding around the screenshot", "80").option("--background <path>", "Background image path").option("--preset <name>", "Background preset (run 'presets' to see options)").action(async (input, opts) => {
630
+ try {
631
+ const inputPath = resolve2(input);
632
+ const padding = parseInt(opts.padding || "80", 10);
633
+ const backgroundImage = opts.background ? resolve2(opts.background) : void 0;
634
+ const backgroundPreset = opts.preset;
635
+ const outputPath = opts.output ? resolve2(opts.output) : join2(
636
+ dirname(inputPath),
637
+ `${basename2(inputPath, extname2(inputPath))}_beautified.png`
638
+ );
639
+ console.log(`Beautifying: ${inputPath}`);
640
+ await beautify(inputPath, outputPath, { padding, backgroundImage, backgroundPreset });
641
+ console.log(`Saved to: ${outputPath}`);
642
+ } catch (error) {
643
+ console.error("Error:", error instanceof Error ? error.message : error);
644
+ process.exit(1);
645
+ }
646
+ });
647
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "screenshot-beautify",
3
+ "version": "1.0.0",
4
+ "description": "CLI tool to beautify screenshots with window frames, backgrounds, and shadows",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "screenshot-beautify": "dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsup src/index.ts --format esm --dts --clean",
12
+ "dev": "tsx src/index.ts",
13
+ "prepublishOnly": "npm run build"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "keywords": [
19
+ "screenshot",
20
+ "beautify",
21
+ "cli",
22
+ "image",
23
+ "frame",
24
+ "shadow"
25
+ ],
26
+ "author": "",
27
+ "license": "MIT",
28
+ "dependencies": {
29
+ "chokidar": "^5.0.0",
30
+ "commander": "^12.1.0",
31
+ "sharp": "^0.33.5",
32
+ "systray2": "^2.1.4"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^22.10.0",
36
+ "tsup": "^8.3.5",
37
+ "tsx": "^4.19.2",
38
+ "typescript": "^5.7.2"
39
+ }
40
+ }