xw-devtool-cli 1.0.23 → 1.0.25

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 CHANGED
@@ -14,6 +14,8 @@
14
14
  - **图片 ↔ Base64**:支持图片转 Base64 字符串,以及 Base64 还原为图片文件。
15
15
  - **图片分割**:支持网格等分(自定义行/列数)或自定义分割线(像素/百分比),自动生成分割后的图片文件。
16
16
  - **图片主色识别**:提取图片主色调(Hex/RGB/HSL/HSV),默认复制 Hex 到剪贴板,支持保存详细信息到文件。
17
+ - **颜色吸取**:从图片中吸取单像素颜色或区域平均颜色,支持 px/% 坐标输入,结果显示颜色条并自动复制 Hex/Hex8。
18
+ - **屏幕取色**:无需选择图片,直接在屏幕上将鼠标移动到目标位置按回车即可取色,支持透明度显示和预览。
17
19
  - **占位图生成**:快速生成指定尺寸、颜色、文字的占位图片 (Placeholder Image)。
18
20
  - **Mock 数据生成**:
19
21
  - 支持生成:英文段落 (Lorem Ipsum)、中文字符、中国居民身份证号、电子邮箱、URL、订单号、手机号、座机号。
@@ -27,7 +29,8 @@
27
29
  - **Unicode 编解码**:文本与 Unicode 转义序列 (\uXXXX) 互转。
28
30
  - **UUID**:生成 UUID v4。
29
31
  - **中文转拼音**:将汉字转换为不带声调的拼音。
30
- - **颜色转换**:Hex <-> RGB 互转。
32
+ - **颜色转换**:Hex <-> RGB 互转,并在结果中显示颜色条预览(支持透明度棋盘背景),自动复制 Hex。
33
+ - **颜色预览**:输入颜色并在终端显示颜色条,便于快速视觉确认,同时自动复制 Hex 到剪贴板。
31
34
  - **变量格式转换**:支持 CamelCase, PascalCase, SnakeCase, KebabCase, ConstantCase 互转。
32
35
  - **哈希计算**:支持 MD5, SHA1, SHA256, SHA512, SM3 算法。
33
36
  - **二维码生成**:终端直接显示二维码,支持保存为 PNG 图片(带时间戳文件名)。
package/README_EN.md CHANGED
@@ -14,6 +14,8 @@ Key features include: Base64 encoding/decoding, image format conversion, image <
14
14
  - **Image ↔ Base64**: Convert images to Base64 strings and vice versa.
15
15
  - **Image Splitter**: Split images into grid (rows/cols) or custom split lines (pixels/percentage).
16
16
  - **Placeholder Image**: Quickly generate placeholder images with custom size, color, and text.
17
+ - **Color Picker**: Pick a single pixel color or area average from an image; supports px/% coordinates; shows preview bar and auto-copies Hex/Hex8.
18
+ - **Screen Picker**: Pick color directly from the screen at the mouse position; move the cursor and press Enter to sample; supports alpha preview.
17
19
  - **Mock Data**:
18
20
  - Generate: Lorem Ipsum, Chinese characters, ID cards, Emails, URLs, Order IDs, Phone numbers.
19
21
  - Supports batch generation.
@@ -26,7 +28,8 @@ Key features include: Base64 encoding/decoding, image format conversion, image <
26
28
  - **Unicode Encode/Decode**: Text <-> Unicode escape sequences (\uXXXX).
27
29
  - **UUID**: Generate UUID v4.
28
30
  - **Pinyin**: Convert Chinese characters to Pinyin (without tone).
29
- - **Color Converter**: Hex <-> RGB.
31
+ - **Color Converter**: Hex <-> RGB, and shows a color bar preview in results (with checkerboard alpha simulation); Hex auto-copied.
32
+ - **Color Preview**: Enter a color and display a color bar in terminal for quick visual confirmation; Hex is auto-copied to clipboard.
30
33
  - **Variable Format**: CamelCase, PascalCase, SnakeCase, KebabCase, ConstantCase.
31
34
  - **Hash Calculator**: MD5, SHA1, SHA256, SHA512, SM3.
32
35
  - **QR Code**: Display QR codes in terminal, save as PNG.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xw-devtool-cli",
3
- "version": "1.0.23",
3
+ "version": "1.0.25",
4
4
  "type": "module",
5
5
  "description": "基于node的开发者助手cli",
6
6
  "main": "index.js",
@@ -2,6 +2,38 @@ import inquirer from 'inquirer';
2
2
  import tinycolor from 'tinycolor2';
3
3
  import { copy } from '../utils/clipboard.js';
4
4
  import { selectFromMenu } from '../utils/menu.js';
5
+ import i18next from '../i18n.js';
6
+
7
+ function blend(fg, bg, alpha) {
8
+ const a = Math.max(0, Math.min(1, alpha ?? 1));
9
+ return {
10
+ r: Math.round(a * fg.r + (1 - a) * bg.r),
11
+ g: Math.round(a * fg.g + (1 - a) * bg.g),
12
+ b: Math.round(a * fg.b + (1 - a) * bg.b),
13
+ };
14
+ }
15
+
16
+ function printColorBar(rgb, width = 50, height = 2, label = '', alpha = 1) {
17
+ const reset = '\x1b[0m';
18
+ const block = ' ';
19
+ if (label) {
20
+ console.log(label);
21
+ }
22
+ const useChecker = alpha < 1;
23
+ const bg1 = { r: 255, g: 255, b: 255 };
24
+ const bg2 = { r: 204, g: 204, b: 204 };
25
+ for (let row = 0; row < height; row++) {
26
+ let line = '';
27
+ for (let col = 0; col < width; col++) {
28
+ const checker = ((Math.floor(row / 1) + Math.floor(col / 2)) % 2 === 0) ? bg1 : bg2;
29
+ const blended = useChecker ? blend(rgb, checker, alpha) : rgb;
30
+ const bg = `\x1b[48;2;${blended.r};${blended.g};${blended.b}m`;
31
+ line += `${bg}${block}`;
32
+ }
33
+ line += reset;
34
+ console.log(line);
35
+ }
36
+ }
5
37
 
6
38
  export async function colorHandler() {
7
39
  const { input } = await inquirer.prompt([
@@ -17,6 +49,11 @@ export async function colorHandler() {
17
49
  ]);
18
50
 
19
51
  const color = tinycolor(input);
52
+ const rgbBar = color.toRgb();
53
+ const alpha = typeof rgbBar.a === 'number' ? rgbBar.a : 1;
54
+ const hexBar = alpha < 1 ? color.toHex8String().toUpperCase() : color.toHexString().toUpperCase();
55
+ const label = `${i18next.t('colorPreview.preview')} ${hexBar} ${color.toRgbString()} ${color.toHslString()}`;
56
+ printColorBar(rgbBar, 50, 2, label, alpha);
20
57
 
21
58
  const results = [];
22
59
  results.push(`Hex: ${color.toHexString().toUpperCase()}`);
@@ -0,0 +1,359 @@
1
+ import inquirer from 'inquirer';
2
+ import { execSync, spawn } from 'child_process';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import sharp from 'sharp';
6
+ import tinycolor from 'tinycolor2';
7
+ import i18next from '../i18n.js';
8
+ import { selectFromMenu } from '../utils/menu.js';
9
+ import { selectFile } from '../utils/fileDialog.js';
10
+ import { copy } from '../utils/clipboard.js';
11
+
12
+ function parseUnit(input, total) {
13
+ const s = String(input).trim();
14
+ if (s.endsWith('%')) {
15
+ const p = parseFloat(s.slice(0, -1));
16
+ return Math.round((p / 100) * total);
17
+ }
18
+ if (s.endsWith('px')) {
19
+ return Math.round(parseFloat(s.slice(0, -2)));
20
+ }
21
+ return Math.round(parseFloat(s));
22
+ }
23
+
24
+ function clamp(n, min, max) {
25
+ return Math.max(min, Math.min(max, n));
26
+ }
27
+
28
+ function blend(fg, bg, alpha) {
29
+ const a = Math.max(0, Math.min(1, alpha ?? 1));
30
+ return {
31
+ r: Math.round(a * fg.r + (1 - a) * bg.r),
32
+ g: Math.round(a * fg.g + (1 - a) * bg.g),
33
+ b: Math.round(a * fg.b + (1 - a) * bg.b),
34
+ };
35
+ }
36
+
37
+ function printColorBar(rgb, width = 50, height = 2, label = '', alpha = 1) {
38
+ const reset = '\x1b[0m';
39
+ const block = ' ';
40
+ if (label) console.log(label);
41
+ const useChecker = alpha < 1;
42
+ const bg1 = { r: 255, g: 255, b: 255 };
43
+ const bg2 = { r: 204, g: 204, b: 204 };
44
+ for (let row = 0; row < height; row++) {
45
+ let line = '';
46
+ for (let col = 0; col < width; col++) {
47
+ const checker = ((Math.floor(row / 1) + Math.floor(col / 2)) % 2 === 0) ? bg1 : bg2;
48
+ const blended = useChecker ? blend(rgb, checker, alpha) : rgb;
49
+ const bg = `\x1b[48;2;${blended.r};${blended.g};${blended.b}m`;
50
+ line += `${bg}${block}`;
51
+ }
52
+ line += reset;
53
+ console.log(line);
54
+ }
55
+ }
56
+
57
+ async function pickInputFile() {
58
+ const method = await selectFromMenu(
59
+ i18next.t('colorPick.inputMethod'),
60
+ [
61
+ { name: i18next.t('colorPick.inputDialog'), value: 'dialog' },
62
+ { name: i18next.t('colorPick.inputManual'), value: 'manual' }
63
+ ],
64
+ true,
65
+ i18next.t('colorPick.backPrev')
66
+ );
67
+ if (method === '__BACK__') return '__BACK__';
68
+ let filePath = '';
69
+ if (method === 'dialog') {
70
+ filePath = selectFile('Image Files|*.png;*.jpg;*.jpeg;*.webp|All Files|*.*') || '';
71
+ }
72
+ if (!filePath) {
73
+ const { filePath: fp } = await inquirer.prompt([
74
+ {
75
+ type: 'input',
76
+ name: 'filePath',
77
+ message: i18next.t('colorPick.enterPath'),
78
+ validate: (p) => (fs.existsSync(p) ? true : i18next.t('colorPick.notExist'))
79
+ }
80
+ ]);
81
+ filePath = fp;
82
+ }
83
+ return filePath;
84
+ }
85
+
86
+ async function pickPixel(image, width, height) {
87
+ const answers = await inquirer.prompt([
88
+ { type: 'input', name: 'x', message: i18next.t('colorPick.xPrompt') },
89
+ { type: 'input', name: 'y', message: i18next.t('colorPick.yPrompt') }
90
+ ]);
91
+ const x = clamp(parseUnit(answers.x, width), 0, width - 1);
92
+ const y = clamp(parseUnit(answers.y, height), 0, height - 1);
93
+ const buf = await image.ensureAlpha().extract({ left: x, top: y, width: 1, height: 1 }).raw().toBuffer();
94
+ const r = buf[0], g = buf[1], b = buf[2], a = buf[3] / 255;
95
+ return { r, g, b, a, region: { left: x, top: y, width: 1, height: 1 } };
96
+ }
97
+
98
+ async function pickAreaAverage(image, width, height) {
99
+ const answers = await inquirer.prompt([
100
+ { type: 'input', name: 'x1', message: i18next.t('colorPick.x1Prompt') },
101
+ { type: 'input', name: 'y1', message: i18next.t('colorPick.y1Prompt') },
102
+ { type: 'input', name: 'x2', message: i18next.t('colorPick.x2Prompt') },
103
+ { type: 'input', name: 'y2', message: i18next.t('colorPick.y2Prompt') }
104
+ ]);
105
+ let x1 = clamp(parseUnit(answers.x1, width), 0, width - 1);
106
+ let y1 = clamp(parseUnit(answers.y1, height), 0, height - 1);
107
+ let x2 = clamp(parseUnit(answers.x2, width), 0, width);
108
+ let y2 = clamp(parseUnit(answers.y2, height), 0, height);
109
+ if (x2 < x1) [x1, x2] = [x2, x1];
110
+ if (y2 < y1) [y1, y2] = [y2, y1];
111
+ const w = Math.max(1, x2 - x1);
112
+ const h = Math.max(1, y2 - y1);
113
+ const region = { left: x1, top: y1, width: w, height: h };
114
+ const buf = await image.ensureAlpha().extract(region).raw().toBuffer();
115
+ let sr = 0, sg = 0, sb = 0, sa = 0;
116
+ for (let i = 0; i < buf.length; i += 4) {
117
+ sr += buf[i];
118
+ sg += buf[i + 1];
119
+ sb += buf[i + 2];
120
+ sa += buf[i + 3];
121
+ }
122
+ const n = buf.length / 4;
123
+ const r = Math.round(sr / n);
124
+ const g = Math.round(sg / n);
125
+ const b = Math.round(sb / n);
126
+ const a = Math.round(sa / n) / 255;
127
+ return { r, g, b, a, region };
128
+ }
129
+
130
+ export async function colorPickHandler() {
131
+ const source = await selectFromMenu(
132
+ i18next.t('colorPick.source'),
133
+ [
134
+ { name: i18next.t('colorPick.sourceScreen'), value: 'screen' },
135
+ { name: i18next.t('colorPick.sourceImage'), value: 'image' }
136
+ ],
137
+ true,
138
+ i18next.t('colorPick.backPrev')
139
+ );
140
+ if (source === '__BACK__') return;
141
+
142
+ let picked;
143
+ if (source === 'screen') {
144
+ if (process.platform !== 'win32') {
145
+ console.log(i18next.t('colorPick.notSupported'));
146
+ return;
147
+ }
148
+ let overlay = null;
149
+ let sampler = null;
150
+ const readOneLine = (timeoutMs = 800) => new Promise((resolve) => {
151
+ let settled = false;
152
+ const onData = (buf) => {
153
+ if (settled) return;
154
+ settled = true;
155
+ resolve(buf.toString('utf8').trim());
156
+ };
157
+ sampler.stdout.once('data', onData);
158
+ if (timeoutMs > 0) {
159
+ setTimeout(() => {
160
+ if (settled) return;
161
+ settled = true;
162
+ try { sampler.stdout.removeListener('data', onData); } catch {}
163
+ resolve(null);
164
+ }, timeoutMs);
165
+ }
166
+ });
167
+ try {
168
+ const overlayScript = `
169
+ Add-Type -AssemblyName System.Windows.Forms; Add-Type -AssemblyName System.Drawing;
170
+ Add-Type -TypeDefinition @'
171
+ using System;
172
+ using System.Drawing;
173
+ using System.Windows.Forms;
174
+ using System.Runtime.InteropServices;
175
+ public class OverlayForm : Form {
176
+ const int WS_EX_TRANSPARENT=0x20;
177
+ const int WS_EX_LAYERED=0x80000;
178
+ const int GWL_EXSTYLE=-20;
179
+ [DllImport("user32.dll")] static extern int GetWindowLong(IntPtr hWnd, int nIndex);
180
+ [DllImport("user32.dll")] static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
181
+ Timer t;
182
+ public OverlayForm(){
183
+ this.FormBorderStyle=FormBorderStyle.None;
184
+ this.TopMost=true;
185
+ this.ShowInTaskbar=false;
186
+ this.WindowState=FormWindowState.Maximized;
187
+ this.BackColor=Color.Fuchsia;
188
+ this.TransparencyKey=this.BackColor;
189
+ this.DoubleBuffered=true;
190
+ }
191
+ protected override void OnShown(EventArgs e){
192
+ base.OnShown(e);
193
+ int ex=GetWindowLong(this.Handle,GWL_EXSTYLE);
194
+ SetWindowLong(this.Handle,GWL_EXSTYLE,ex|WS_EX_LAYERED|WS_EX_TRANSPARENT);
195
+ t=new Timer();
196
+ t.Interval=33;
197
+ t.Tick+=(s,ev)=>{ this.Invalidate(); };
198
+ t.Start();
199
+ }
200
+ protected override void OnPaint(PaintEventArgs e){
201
+ var p=Cursor.Position;
202
+ var g=e.Graphics;
203
+ int gap=16;
204
+ using(var pen=new Pen(Color.Red,1)){
205
+ g.DrawLine(pen,0,p.Y,Math.Max(0,p.X-gap),p.Y);
206
+ g.DrawLine(pen,Math.Min(this.Width,p.X+gap),p.Y,this.Width,p.Y);
207
+ g.DrawLine(pen,p.X,0,p.X,Math.Max(0,p.Y-gap));
208
+ g.DrawLine(pen,p.X,Math.Min(this.Height,p.Y+gap),p.X,this.Height);
209
+ }
210
+ }
211
+ }
212
+ public static class Entry {
213
+ public static void Main(){ Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new OverlayForm()); }
214
+ }
215
+ '@ -ReferencedAssemblies System.Windows.Forms,System.Drawing;
216
+ [Entry]::Main()
217
+ `;
218
+ overlay = spawn('powershell', ['-NoProfile', '-Command', overlayScript], { windowsHide: true });
219
+ const samplerScript = `
220
+ Add-Type -TypeDefinition @'
221
+ using System;
222
+ using System.Runtime.InteropServices;
223
+ public static class ColorSampler {
224
+ [StructLayout(LayoutKind.Sequential)]
225
+ public struct POINT { public int X; public int Y; }
226
+ [DllImport("user32.dll")] static extern bool GetCursorPos(out POINT lpPoint);
227
+ [DllImport("user32.dll")] static extern IntPtr GetDC(IntPtr hWnd);
228
+ [DllImport("user32.dll")] static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
229
+ [DllImport("gdi32.dll")] static extern int GetPixel(IntPtr hdc, int x, int y);
230
+ public static string Sample() {
231
+ POINT p;
232
+ if(!GetCursorPos(out p)) return "";
233
+ IntPtr hdc = GetDC(IntPtr.Zero);
234
+ if (hdc == IntPtr.Zero) return "";
235
+ int color = GetPixel(hdc, p.X, p.Y);
236
+ ReleaseDC(IntPtr.Zero, hdc);
237
+ if (color == -1) return "";
238
+ int r = color & 0xFF;
239
+ int g = (color >> 8) & 0xFF;
240
+ int b = (color >> 16) & 0xFF;
241
+ return $"{r},{g},{b},255";
242
+ }
243
+ }
244
+ '@
245
+ function Sample {
246
+ $s=[ColorSampler]::Sample();
247
+ if([string]::IsNullOrEmpty($s)){ [Console]::Out.WriteLine(""); } else { [Console]::Out.WriteLine($s) }
248
+ }
249
+ while($true){
250
+ $line = [Console]::In.ReadLine();
251
+ if($line -eq $null){ break }
252
+ if($line -eq 'SAMPLE'){ Sample }
253
+ elseif($line -eq 'EXIT'){ break }
254
+ }
255
+ `;
256
+ sampler = spawn('powershell', ['-NoProfile', '-Command', samplerScript], { windowsHide: true, stdio: ['pipe', 'pipe', 'ignore'] });
257
+ try { sampler.stdout.setEncoding('utf8'); } catch {}
258
+ } catch {}
259
+ console.log(i18next.t('colorPick.moveMousePrompt'));
260
+ let last = null;
261
+ const sampleOnce = async () => {
262
+ try {
263
+ let out = null;
264
+ if (sampler && sampler.stdin && !sampler.killed) {
265
+ sampler.stdin.write('SAMPLE\n');
266
+ out = await readOneLine(800);
267
+ }
268
+ if (!out) {
269
+ const cmd = `powershell -NoProfile -Command "Add-Type -AssemblyName System.Windows.Forms; Add-Type -AssemblyName System.Drawing; $p=[System.Windows.Forms.Cursor]::Position; $bmp=New-Object System.Drawing.Bitmap 1,1; $g=[System.Drawing.Graphics]::FromImage($bmp); $g.CopyFromScreen($p.X,$p.Y,0,0,[System.Drawing.Size]::new(1,1)); $c=$bmp.GetPixel(0,0); Write-Output (\\"$($c.R),$($c.G),$($c.B),255\\"); $g.Dispose(); $bmp.Dispose();"`;
270
+ out = execSync(cmd, { stdio: ['pipe', 'pipe', 'ignore'] }).toString('utf8').trim();
271
+ }
272
+ const parts = out.split(',').map(n => parseInt(n, 10));
273
+ const r = parts[0], g = parts[1], b = parts[2];
274
+ last = { r, g, b, a: 1 };
275
+ const tc = tinycolor({ r: last.r, g: last.g, b: last.b, a: last.a });
276
+ const hex = last.a < 1 ? tc.toHex8String().toUpperCase() : tc.toHexString().toUpperCase();
277
+ const rgb = tc.toRgbString();
278
+ const hsl = tc.toHslString();
279
+ console.log('\n=== ' + i18next.t('colorPick.result') + ' ===');
280
+ console.log(`Hex: ${hex}`);
281
+ console.log(`RGB: ${rgb}`);
282
+ console.log(`HSL: ${hsl}`);
283
+ console.log('==========================\n');
284
+ const label = `${i18next.t('colorPreview.preview')} ${hex} ${rgb} ${hsl}`;
285
+ printColorBar({ r: last.r, g: last.g, b: last.b }, 50, 2, label, last.a);
286
+ copy(hex);
287
+ } catch {}
288
+ };
289
+ await new Promise((resolve) => {
290
+ const stdin = process.stdin;
291
+ try { stdin.setRawMode(true); } catch {}
292
+ stdin.resume();
293
+ const onData = (data) => {
294
+ const s = data.toString('utf8');
295
+ if (s === ' ') {
296
+ sampleOnce();
297
+ } else if (s === '\r' || s === '\n') {
298
+ stdin.removeListener('data', onData);
299
+ try { stdin.setRawMode(false); } catch {}
300
+ stdin.pause();
301
+ resolve();
302
+ }
303
+ };
304
+ stdin.on('data', onData);
305
+ });
306
+ try { if (sampler) { sampler.stdin.write('EXIT\n'); sampler.kill(); } } catch {}
307
+ try { if (overlay) { overlay.kill(); } } catch {}
308
+ if (!last) {
309
+ console.log(i18next.t('colorPick.pickFail'));
310
+ return;
311
+ }
312
+ picked = { r: last.r, g: last.g, b: last.b, a: last.a, region: null };
313
+ } else {
314
+ const inputPath = await pickInputFile();
315
+ if (inputPath === '__BACK__') {
316
+ return await colorPickHandler();
317
+ }
318
+ const image = sharp(inputPath);
319
+ const meta = await image.metadata();
320
+ const { width, height } = meta;
321
+ console.log(`${i18next.t('colorPick.imageInfo')} ${inputPath} (${width}x${height})`);
322
+
323
+ const mode = await selectFromMenu(
324
+ i18next.t('colorPick.mode'),
325
+ [
326
+ { name: i18next.t('colorPick.modePixel'), value: 'pixel' },
327
+ { name: i18next.t('colorPick.modeArea'), value: 'area' },
328
+ ],
329
+ true,
330
+ i18next.t('colorPick.backPrev')
331
+ );
332
+ if (mode === '__BACK__') {
333
+ return await colorPickHandler();
334
+ }
335
+
336
+ if (mode === 'pixel') {
337
+ picked = await pickPixel(image, width, height);
338
+ } else {
339
+ picked = await pickAreaAverage(image, width, height);
340
+ }
341
+ }
342
+
343
+ const tc = tinycolor({ r: picked.r, g: picked.g, b: picked.b, a: picked.a });
344
+ const hex = picked.a < 1 ? tc.toHex8String().toUpperCase() : tc.toHexString().toUpperCase();
345
+ const rgb = tc.toRgbString();
346
+ const hsl = tc.toHslString();
347
+
348
+ console.log('\n=== ' + i18next.t('colorPick.result') + ' ===');
349
+ console.log(`Hex: ${hex}`);
350
+ console.log(`RGB: ${rgb}`);
351
+ console.log(`HSL: ${hsl}`);
352
+ console.log('==========================\n');
353
+
354
+ const label = `${i18next.t('colorPreview.preview')} ${hex} ${rgb} ${hsl}`;
355
+ printColorBar({ r: picked.r, g: picked.g, b: picked.b }, 50, 2, label, picked.a);
356
+
357
+ await copy(hex);
358
+ console.log(i18next.t('colorPreview.copied', { value: hex }));
359
+ }
@@ -0,0 +1,72 @@
1
+ import inquirer from 'inquirer';
2
+ import tinycolor from 'tinycolor2';
3
+ import { copy } from '../utils/clipboard.js';
4
+ import i18next from '../i18n.js';
5
+ // No width/height selection per user request; fixed size preview
6
+
7
+ function blend(fg, bg, alpha) {
8
+ const a = Math.max(0, Math.min(1, alpha ?? 1));
9
+ return {
10
+ r: Math.round(a * fg.r + (1 - a) * bg.r),
11
+ g: Math.round(a * fg.g + (1 - a) * bg.g),
12
+ b: Math.round(a * fg.b + (1 - a) * bg.b),
13
+ };
14
+ }
15
+
16
+ function printColorBar(rgb, width = 50, height = 2, label = '', alpha = 1) {
17
+ const reset = '\x1b[0m';
18
+ const block = ' ';
19
+ if (label) {
20
+ console.log(label);
21
+ }
22
+ const useChecker = alpha < 1;
23
+ const bg1 = { r: 255, g: 255, b: 255 };
24
+ const bg2 = { r: 204, g: 204, b: 204 };
25
+ for (let row = 0; row < height; row++) {
26
+ let line = '';
27
+ for (let col = 0; col < width; col++) {
28
+ const checker = ((Math.floor(row / 1) + Math.floor(col / 2)) % 2 === 0) ? bg1 : bg2;
29
+ const blended = useChecker ? blend(rgb, checker, alpha) : rgb;
30
+ const bg = `\x1b[48;2;${blended.r};${blended.g};${blended.b}m`;
31
+ line += `${bg}${block}`;
32
+ }
33
+ line += reset;
34
+ console.log(line);
35
+ }
36
+ }
37
+
38
+ function isAllowedColorFormat(val) {
39
+ if (!val || typeof val !== 'string') return false;
40
+ const s = val.trim();
41
+ const hex = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
42
+ const rgb = /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/i;
43
+ const rgba = /^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(?:0|1|0?\.\d+)\s*\)$/i;
44
+ const hsl = /^hsl\(\s*(\d{1,3})\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%\s*\)$/i;
45
+ const hsla = /^hsla\(\s*(\d{1,3})\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%\s*,\s*(?:0|1|0?\.\d+)\s*\)$/i;
46
+ return hex.test(s) || rgb.test(s) || rgba.test(s) || hsl.test(s) || hsla.test(s);
47
+ }
48
+
49
+ export async function colorPreviewHandler() {
50
+ const { input } = await inquirer.prompt([
51
+ {
52
+ type: 'input',
53
+ name: 'input',
54
+ message: i18next.t('colorPreview.input'),
55
+ validate: (val) => isAllowedColorFormat(val) || i18next.t('colorPreview.invalid')
56
+ }
57
+ ]);
58
+
59
+ const color = tinycolor(input);
60
+ const rgb = color.toRgb();
61
+ const alpha = typeof rgb.a === 'number' ? rgb.a : 1;
62
+ const hex = alpha < 1 ? color.toHex8String().toUpperCase() : color.toHexString().toUpperCase();
63
+
64
+ const width = 50;
65
+ const height = 2;
66
+
67
+ const label = `${i18next.t('colorPreview.preview')} ${hex} ${color.toRgbString()} ${color.toHslString()}`;
68
+ printColorBar(rgb, width, height, label, alpha);
69
+
70
+ await copy(hex);
71
+ console.log(i18next.t('colorPreview.copied', { value: hex }));
72
+ }
package/src/index.js CHANGED
@@ -15,6 +15,7 @@ import { mockHandler } from './commands/mock.js';
15
15
  import { uuidHandler } from './commands/uuid.js';
16
16
  import { pinyinHandler } from './commands/pinyin.js';
17
17
  import { colorHandler } from './commands/color.js';
18
+ import { colorPreviewHandler } from './commands/colorPreview.js';
18
19
  import { variableFormatHandler } from './commands/variableFormat.js';
19
20
  import { jsonFormatHandler } from './commands/jsonFormat.js';
20
21
  import { hashingHandler } from './commands/hashing.js';
@@ -26,6 +27,7 @@ import { placeholderImgHandler } from './commands/placeholderImg.js';
26
27
  import { markdownHandler } from './commands/markdown.js';
27
28
  import { vscodeSnippetHandler } from './commands/vscodeSnippet.js';
28
29
  import { dominantColorHandler } from './commands/dominantColor.js';
30
+ import { colorPickHandler } from './commands/colorPick.js';
29
31
 
30
32
  process.on('SIGINT', () => {
31
33
  console.log(`\n${i18next.t('menu.bye')}`);
@@ -49,6 +51,7 @@ function getFeatures() {
49
51
  { name: i18next.t('menu.features.imgConvert'), value: 'imgConvert' },
50
52
  { name: i18next.t('menu.features.imgSplit'), value: 'imgSplit' },
51
53
  { name: i18next.t('menu.features.dominantColor'), value: 'dominantColor' },
54
+ { name: i18next.t('menu.features.colorPick'), value: 'colorPick' },
52
55
  { name: i18next.t('menu.features.placeholderImg'), value: 'placeholderImg' },
53
56
  { name: i18next.t('menu.features.qrcode'), value: 'qrcode' },
54
57
 
@@ -65,6 +68,7 @@ function getFeatures() {
65
68
  { name: i18next.t('menu.features.timeFormat'), value: 'timeFormat' },
66
69
  { name: i18next.t('menu.features.timeCalc'), value: 'timeCalc' },
67
70
  { name: i18next.t('menu.features.color'), value: 'color' },
71
+ { name: i18next.t('menu.features.colorPreview'), value: 'colorPreview' },
68
72
  { name: i18next.t('menu.features.uuid'), value: 'uuid' },
69
73
  { name: i18next.t('menu.features.hashing'), value: 'hashing' },
70
74
 
@@ -182,6 +186,9 @@ async function handleAction(action) {
182
186
  case 'dominantColor':
183
187
  await dominantColorHandler();
184
188
  break;
189
+ case 'colorPick':
190
+ await colorPickHandler();
191
+ break;
185
192
  case 'timeFormat':
186
193
  await timeFormatHandler();
187
194
  break;
@@ -200,6 +207,9 @@ async function handleAction(action) {
200
207
  case 'color':
201
208
  await colorHandler();
202
209
  break;
210
+ case 'colorPreview':
211
+ await colorPreviewHandler();
212
+ break;
203
213
  case 'variableFormat':
204
214
  await variableFormatHandler();
205
215
  break;
package/src/locales/en.js CHANGED
@@ -10,6 +10,7 @@ export default {
10
10
  imgConvert: 'Image Format Convert',
11
11
  imgSplit: 'Image Splitter',
12
12
  dominantColor: 'Image Dominant Color',
13
+ colorPick: 'Color Picker',
13
14
  placeholderImg: 'Placeholder Image Generator',
14
15
  qrcode: 'QR Code Generator',
15
16
  url: 'URL Encode/Decode',
@@ -22,6 +23,7 @@ export default {
22
23
  timeFormat: 'Time Format / Timestamp',
23
24
  timeCalc: 'Time Calculation (Diff/Offset)',
24
25
  color: 'Color Converter (Hex <-> RGB)',
26
+ colorPreview: 'Color Preview',
25
27
  uuid: 'Get UUID',
26
28
  hashing: 'Hash Calculator (MD5/SHA/SM3)',
27
29
  mock: 'Mock Text',
@@ -37,6 +39,37 @@ export default {
37
39
  saved: 'Language saved. Please restart the tool or continue using the menu.',
38
40
  backToMenu: 'Back to Menu'
39
41
  },
42
+ colorPreview: {
43
+ input: 'Enter color (Hex/RGB/HSL):',
44
+ invalid: 'Invalid color format',
45
+ preview: 'Preview:',
46
+ copied: 'Copied to clipboard: {{value}}'
47
+ },
48
+ colorPick: {
49
+ backPrev: 'Back to previous step',
50
+ source: 'Select source',
51
+ sourceScreen: 'Screen (mouse position)',
52
+ sourceImage: 'Image file',
53
+ inputMethod: 'Image input method',
54
+ inputDialog: 'Select file (dialog)',
55
+ inputManual: 'Enter file path manually',
56
+ enterPath: 'Enter image file path:',
57
+ notExist: 'File does not exist.',
58
+ notSupported: 'Screen picking not supported on this OS.',
59
+ moveMousePrompt: 'Move mouse; press Space to sample, Enter to finish...',
60
+ pickFail: 'Failed to pick color from screen.',
61
+ imageInfo: 'Image:',
62
+ mode: 'Pick mode',
63
+ modePixel: 'Pick single pixel',
64
+ modeArea: 'Pick area average',
65
+ xPrompt: 'Enter X (px or %):',
66
+ yPrompt: 'Enter Y (px or %):',
67
+ x1Prompt: 'Enter X1 (px or %):',
68
+ y1Prompt: 'Enter Y1 (px or %):',
69
+ x2Prompt: 'Enter X2 (px or %):',
70
+ y2Prompt: 'Enter Y2 (px or %):',
71
+ result: 'Picked Color'
72
+ },
40
73
  url: {
41
74
  title: 'URL Encode/Decode',
42
75
  encode: 'Encode',
package/src/locales/zh.js CHANGED
@@ -10,6 +10,7 @@ export default {
10
10
  imgConvert: '图片格式转换',
11
11
  imgSplit: '图片分割工具',
12
12
  dominantColor: '图片主色识别',
13
+ colorPick: '颜色吸取',
13
14
  placeholderImg: '占位图生成器',
14
15
  qrcode: '二维码生成器',
15
16
  url: 'URL 编码/解码',
@@ -22,6 +23,7 @@ export default {
22
23
  timeFormat: '时间格式化 / 时间戳',
23
24
  timeCalc: '时间计算 (差值/偏移)',
24
25
  color: '颜色转换 (Hex <-> RGB)',
26
+ colorPreview: '颜色预览',
25
27
  uuid: '生成 UUID',
26
28
  hashing: '哈希计算 (MD5/SHA/SM3)',
27
29
  mock: 'Mock 文本生成',
@@ -37,6 +39,37 @@ export default {
37
39
  saved: '语言已保存。请继续使用。',
38
40
  backToMenu: '返回主菜单'
39
41
  },
42
+ colorPreview: {
43
+ input: '输入颜色 (Hex/RGB/HSL):',
44
+ invalid: '颜色格式无效',
45
+ preview: '预览:',
46
+ copied: '已复制到剪贴板: {{value}}'
47
+ },
48
+ colorPick: {
49
+ backPrev: '返回上一步',
50
+ source: '选择来源',
51
+ sourceScreen: '屏幕取色(鼠标位置)',
52
+ sourceImage: '图片取色',
53
+ inputMethod: '图片输入方式',
54
+ inputDialog: '选择文件(对话框)',
55
+ inputManual: '手动输入文件路径',
56
+ enterPath: '请输入图片路径:',
57
+ notExist: '文件不存在。',
58
+ notSupported: '当前系统不支持屏幕取色。',
59
+ moveMousePrompt: '移动鼠标;按空格采样,回车结束...',
60
+ pickFail: '屏幕取色失败。',
61
+ imageInfo: '图片:',
62
+ mode: '选择吸取模式',
63
+ modePixel: '吸取单像素颜色',
64
+ modeArea: '区域平均颜色',
65
+ xPrompt: '输入 X(px 或 %):',
66
+ yPrompt: '输入 Y(px 或 %):',
67
+ x1Prompt: '输入 X1(px 或 %):',
68
+ y1Prompt: '输入 Y1(px 或 %):',
69
+ x2Prompt: '输入 X2(px 或 %):',
70
+ y2Prompt: '输入 Y2(px 或 %):',
71
+ result: '吸取结果'
72
+ },
40
73
  url: {
41
74
  title: 'URL 编码/解码',
42
75
  encode: '编码',
package/src/utils/menu.js CHANGED
@@ -4,29 +4,38 @@ import inquirer from 'inquirer';
4
4
  * Display a numbered menu and get user selection.
5
5
  * @param {string} title - The title of the menu.
6
6
  * @param {Array<{name: string, value: any}>} options - List of options.
7
+ * @param {boolean} [allowBack=false] - Whether to show a back option (0).
8
+ * @param {string} [backText='Back'] - Text for back option.
7
9
  * @returns {Promise<any>} - The selected value.
8
10
  */
9
- export async function selectFromMenu(title, options) {
11
+ export async function selectFromMenu(title, options, allowBack = false, backText = 'Back') {
10
12
  console.log(`\n=== ${title} ===`);
11
13
  options.forEach((opt, index) => {
12
14
  console.log(`${index + 1}. ${opt.name}`);
13
15
  });
16
+ if (allowBack) {
17
+ console.log(`0. ${backText}`);
18
+ }
14
19
  console.log('=================\n');
15
20
 
16
21
  const { choice } = await inquirer.prompt([
17
22
  {
18
23
  type: 'input',
19
24
  name: 'choice',
20
- message: `Select option (1-${options.length}):`,
25
+ message: `Select option (${allowBack ? `0-${options.length}` : `1-${options.length}`}):`,
21
26
  validate: (input) => {
27
+ if (allowBack && input === '0') return true;
22
28
  const num = parseInt(input);
23
29
  if (isNaN(num) || num < 1 || num > options.length) {
24
- return `Please enter a number between 1 and ${options.length}`;
30
+ return `Please enter a number between ${allowBack ? 0 : 1} and ${options.length}`;
25
31
  }
26
32
  return true;
27
33
  }
28
34
  }
29
35
  ]);
30
36
 
37
+ if (allowBack && choice === '0') {
38
+ return '__BACK__';
39
+ }
31
40
  return options[parseInt(choice) - 1].value;
32
41
  }