xw-devtool-cli 1.0.43 → 1.0.44

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
@@ -4,7 +4,7 @@
4
4
 
5
5
  一个基于 Node.js 的开发者命令行工具箱,旨在提供开箱即用的常用开发工具,帮助开发者快速处理日常任务。
6
6
 
7
- 主要功能包括:Base64 编解码、图片格式转换、图片与 Base64 互转、Mock 数据生成、时间戳/日期格式化、时间计算、URL 编解码、UUID 生成、汉字转拼音、颜色转换、变量格式转换、哈希计算、二维码生成、二维码识别、特殊符号大全、Markdown 语法工具、VS Code 代码段生成、当前目录树生成等。大部分结果可一键复制到剪贴板,极大提升开发效率。
7
+ 主要功能包括:Base64 编解码、图片格式转换、图片与 Base64 互转、Mock 数据生成、时间戳/日期格式化、时间计算、控制台时钟、倒计时工具、计时工具、URL 编解码、UUID 生成、汉字转拼音、颜色转换、变量格式转换、哈希计算、二维码生成、二维码识别、特殊符号大全、Markdown 语法工具、VS Code 代码段生成、当前目录树生成等。大部分结果可一键复制到剪贴板,极大提升开发效率。
8
8
 
9
9
  ## ✨ 功能特性
10
10
 
@@ -35,6 +35,9 @@
35
35
  - **时间格式化**:时间戳/日期字符串 -> `YYYY-MM-DD HH:mm:ss`。
36
36
  - **获取时间戳**:快速获取当前毫秒级时间戳。
37
37
  - **时间计算**:计算日期差值或日期偏移 (Add/Subtract)。
38
+ - **控制台时钟**:在终端实时显示当前时间,按 `Enter` 或 `Q` 返回上一步。
39
+ - **倒计时工具**:输入倒计时分钟数后启动终端倒计时;倒计时结束后继续计时并显示负号(如 `-00:00:05`)。
40
+ - **计时工具**:启动后从 `00:00:00` 开始正向计时,终端每秒刷新显示。
38
41
  - **开发辅助工具**:
39
42
  - **URL 编解码**:Encode/Decode URL。
40
43
  - **Unicode 编解码**:文本与 Unicode 转义序列 (\uXXXX) 互转。
@@ -46,7 +49,7 @@
46
49
  - **变量格式转换**:支持 CamelCase, PascalCase, SnakeCase, KebabCase, ConstantCase 互转。
47
50
  - **哈希计算**:支持 MD5, SHA1, SHA256, SHA512, SM3 算法。
48
51
  - **二维码生成**:终端直接显示二维码,支持保存为 PNG 图片(带时间戳文件名)。
49
- - **二维码识别**:支持选择二维码图片进行识别,识别结果自动复制到剪贴板。
52
+ - **二维码识别**:支持选择二维码图片进行识别,识别结果自动复制到剪贴板。
50
53
  - **特殊符号大全**:包含常用符号、箭头、数学符号、货币、希腊字母等 170+ 个符号,支持一键复制。
51
54
  - **Emoji 输入**:支持分类查看和选择常用 Emoji,一键复制到剪贴板。
52
55
  - **HTML 实体工具**:支持 HTML 实体编码与解码 (如 `&` <-> `&amp;`)。
@@ -209,13 +212,13 @@ x. 设置 / 语言 (Settings)
209
212
  - 输入文本或 URL(支持直接回车读取剪贴板)。
210
213
  - 终端直接显示二维码预览。
211
214
  - 可选保存为 PNG 图片,默认文件名包含时间戳。
212
-
213
- ### 二维码识别
214
- - 选择 `qrcodeDecode`(菜单对应数字/字母)进入。
215
- - 支持两种图片输入方式:
216
- - **选择文件(对话框)**
217
- - **手动输入文件路径**
218
- - 自动识别二维码内容并复制到剪贴板。
215
+
216
+ ### 二维码识别
217
+ - 选择 `qrcodeDecode`(菜单对应数字/字母)进入。
218
+ - 支持两种图片输入方式:
219
+ - **选择文件(对话框)**
220
+ - **手动输入文件路径**
221
+ - 自动识别二维码内容并复制到剪贴板。
219
222
 
220
223
  ### 5. URL 编解码
221
224
  - 选择 `5` 进入。
@@ -291,6 +294,22 @@ x. 设置 / 语言 (Settings)
291
294
  5. 查看计算后的新日期。
292
295
  - 示例:计算 "当前时间 3 天后" 或 "2025-01-01 5 小时前"。
293
296
 
297
+ ### 控制台时钟
298
+ - 在菜单中选择 `控制台时钟` 进入。
299
+ - 终端会每秒刷新显示当前时间。
300
+ - 按 `Enter` 或 `Q` 返回上一步。
301
+
302
+ ### 倒计时工具
303
+ - 在菜单中选择 `倒计时工具` 进入。
304
+ - 输入倒计时分钟数后,终端会每秒刷新倒计时。
305
+ - 倒计时结束后会继续计时并显示负号(如 `-00:00:01`、`-00:00:02`)。
306
+ - 按 `Enter` 或 `Q` 返回上一步。
307
+
308
+ ### 计时工具
309
+ - 在菜单中选择 `计时工具` 进入。
310
+ - 启动后从 `00:00:00` 开始,终端每秒刷新已计时时间。
311
+ - 按 `Enter` 或 `Q` 返回上一步。
312
+
294
313
  ### 14. 颜色转换 (Hex <-> RGB)
295
314
  - 选择 `e` 进入。
296
315
  - 输入 Hex 颜色值 (如 #333) 或 RGB 值 (如 rgb(51,51,51))。
@@ -354,7 +373,7 @@ x. 设置 / 语言 (Settings)
354
373
 
355
374
  A Node.js-based developer command-line toolbox designed to provide out-of-the-box common development tools to help developers handle daily tasks quickly.
356
375
 
357
- Key features include: Base64 encoding/decoding, image format conversion, image <-> Base64, Mock data generation, timestamp/date formatting, time calculation, URL encoding/decoding, UUID generation, Chinese pinyin conversion, color conversion, variable format conversion, hash calculation, QR code generation, QR code recognition, special symbols, Markdown snippets, VS Code snippet generation, current directory tree generation, etc. Most results can be copied to the clipboard in one step, greatly improving development efficiency.
376
+ Key features include: Base64 encoding/decoding, image format conversion, image <-> Base64, Mock data generation, timestamp/date formatting, time calculation, console clock, countdown timer, stopwatch, URL encoding/decoding, UUID generation, Chinese pinyin conversion, color conversion, variable format conversion, hash calculation, QR code generation, QR code recognition, special symbols, Markdown snippets, VS Code snippet generation, current directory tree generation, etc. Most results can be copied to the clipboard in one step, greatly improving development efficiency.
358
377
 
359
378
  ### ✨ Features
360
379
 
@@ -380,6 +399,9 @@ Key features include: Base64 encoding/decoding, image format conversion, image <
380
399
  - **Formatting**: Timestamp/Date string -> `YYYY-MM-DD HH:mm:ss`.
381
400
  - **Timestamp**: Quickly get current millisecond timestamp.
382
401
  - **Calculation**: Calculate date differences or offsets (Add/Subtract).
402
+ - **Console Clock**: Show real-time current time in terminal, press `Enter` or `Q` to go back.
403
+ - **Countdown Timer**: Enter countdown minutes to start timer; once it reaches zero, it keeps counting with a negative sign (such as `-00:00:05`).
404
+ - **Stopwatch**: Start from `00:00:00` and count up every second in terminal.
383
405
  - **Dev Tools**:
384
406
  - **URL Encode/Decode**
385
407
  - **Unicode Encode/Decode**: Text <-> Unicode escape sequences (\uXXXX).
@@ -389,7 +411,7 @@ Key features include: Base64 encoding/decoding, image format conversion, image <
389
411
  - **Variable Format**: CamelCase, PascalCase, SnakeCase, KebabCase, ConstantCase.
390
412
  - **Hash Calculator**: MD5, SHA1, SHA256, SHA512, SM3.
391
413
  - **QR Code**: Display QR codes in terminal, save as PNG.
392
- - **QR Code Reader**: Select an image with QR code and decode content, then copy the result to clipboard automatically.
414
+ - **QR Code Reader**: Select an image with QR code and decode content, then copy the result to clipboard automatically.
393
415
  - **Special Symbols**: 170+ symbols, arrows, math symbols, etc.
394
416
  - **Emoji Picker**: Browse and copy emojis.
395
417
  - **HTML Entity**: Encode/Decode HTML entities.
@@ -503,13 +525,13 @@ You can enter a feature key directly, or type a keyword (e.g. `json`, `image`, `
503
525
  - Input text or URL.
504
526
  - Preview in terminal.
505
527
  - Option to save as PNG.
506
-
507
- #### QR Code Reader
508
- - Select `qrcodeDecode` (menu key).
509
- - Image input methods:
510
- - **Select file (dialog)**
511
- - **Enter file path manually**
512
- - Decode content and copy it to clipboard automatically.
528
+
529
+ #### QR Code Reader
530
+ - Select `qrcodeDecode` (menu key).
531
+ - Image input methods:
532
+ - **Select file (dialog)**
533
+ - **Enter file path manually**
534
+ - Decode content and copy it to clipboard automatically.
513
535
 
514
536
  #### 5. URL Encode/Decode
515
537
  - Select `5`.
@@ -563,6 +585,22 @@ You can enter a feature key directly, or type a keyword (e.g. `json`, `image`, `
563
585
  - **Diff**: Calculate difference between two dates.
564
586
  - **Offset**: Calculate date after add/subtract time units.
565
587
 
588
+ #### Console Clock
589
+ - Select `Console Clock` in the menu.
590
+ - Current time refreshes every second in terminal.
591
+ - Press `Enter` or `Q` to go back.
592
+
593
+ #### Countdown Timer
594
+ - Select `Countdown Timer` in the menu.
595
+ - Enter minutes to start the countdown.
596
+ - After reaching zero, the timer continues with a negative sign (for example `-00:00:01`).
597
+ - Press `Enter` or `Q` to go back.
598
+
599
+ #### Stopwatch
600
+ - Select `Stopwatch` in the menu.
601
+ - It starts from `00:00:00` and updates every second.
602
+ - Press `Enter` or `Q` to go back.
603
+
566
604
  #### 14. Color Converter
567
605
  - Select `e`.
568
606
  - Hex <-> RGB.
package/README_EN.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  A Node.js-based developer command-line toolbox designed to provide out-of-the-box common development tools to help developers handle daily tasks quickly.
6
6
 
7
- Key features include: Base64 encoding/decoding, image format conversion, image <-> Base64, Mock data generation, timestamp/date formatting, time calculation, URL encoding/decoding, UUID generation, Chinese pinyin conversion, color conversion, variable format conversion, hash calculation, QR code generation, QR code recognition, special symbols, Markdown snippets, VS Code snippet generation, current directory tree generation, etc. Most results can be copied to the clipboard in one step, greatly improving development efficiency.
7
+ Key features include: Base64 encoding/decoding, image format conversion, image <-> Base64, Mock data generation, timestamp/date formatting, time calculation, console clock, countdown timer, stopwatch, URL encoding/decoding, UUID generation, Chinese pinyin conversion, color conversion, variable format conversion, hash calculation, QR code generation, QR code recognition, special symbols, Markdown snippets, VS Code snippet generation, current directory tree generation, etc. Most results can be copied to the clipboard in one step, greatly improving development efficiency.
8
8
 
9
9
  ## ✨ Features
10
10
 
@@ -29,6 +29,9 @@ Key features include: Base64 encoding/decoding, image format conversion, image <
29
29
  - **Formatting**: Timestamp/Date string -> `YYYY-MM-DD HH:mm:ss`.
30
30
  - **Timestamp**: Quickly get current millisecond timestamp.
31
31
  - **Calculation**: Calculate date differences or offsets (Add/Subtract).
32
+ - **Console Clock**: Show real-time current time in terminal, press `Enter` or `Q` to go back.
33
+ - **Countdown Timer**: Enter countdown minutes to start timer; once it reaches zero, it keeps counting with a negative sign (such as `-00:00:05`).
34
+ - **Stopwatch**: Start from `00:00:00` and count up every second in terminal.
32
35
  - **Dev Tools**:
33
36
  - **URL Encode/Decode**
34
37
  - **Unicode Encode/Decode**: Text <-> Unicode escape sequences (\uXXXX).
@@ -224,6 +227,22 @@ s. Settings (Language)
224
227
  - **Diff**: Calculate difference between two dates.
225
228
  - **Offset**: Calculate date after add/subtract time units.
226
229
 
230
+ ### Console Clock
231
+ - Select `Console Clock` in the menu.
232
+ - Current time refreshes every second in terminal.
233
+ - Press `Enter` or `Q` to go back.
234
+
235
+ ### Countdown Timer
236
+ - Select `Countdown Timer` in the menu.
237
+ - Enter minutes to start the countdown.
238
+ - After reaching zero, the timer continues with a negative sign (for example `-00:00:01`).
239
+ - Press `Enter` or `Q` to go back.
240
+
241
+ ### Stopwatch
242
+ - Select `Stopwatch` in the menu.
243
+ - It starts from `00:00:00` and updates every second.
244
+ - Press `Enter` or `Q` to go back.
245
+
227
246
  ### 14. Color Converter
228
247
  - Select `e`.
229
248
  - Hex <-> RGB.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xw-devtool-cli",
3
- "version": "1.0.43",
3
+ "version": "1.0.44",
4
4
  "type": "module",
5
5
  "description": "基于node的开发者助手cli",
6
6
  "main": "index.js",
@@ -34,6 +34,12 @@
34
34
  "mock-data",
35
35
  "timestamp",
36
36
  "time",
37
+ "clock",
38
+ "console-clock",
39
+ "countdown",
40
+ "countdown-timer",
41
+ "stopwatch",
42
+ "stopwatch-timer",
37
43
  "date-calculation",
38
44
  "image",
39
45
  "image-conversion",
@@ -0,0 +1,135 @@
1
+ import readline from 'readline';
2
+ import i18next from '../i18n.js';
3
+
4
+ function getLocale() {
5
+ return i18next.language && i18next.language.toLowerCase().startsWith('zh') ? 'zh-CN' : 'en-US';
6
+ }
7
+
8
+ function getNowDisplay(locale) {
9
+ const now = new Date();
10
+ const timeText = new Intl.DateTimeFormat(locale, {
11
+ hour12: false,
12
+ hour: '2-digit',
13
+ minute: '2-digit',
14
+ second: '2-digit'
15
+ }).format(now);
16
+ const dateText = new Intl.DateTimeFormat(locale, {
17
+ year: 'numeric',
18
+ month: '2-digit',
19
+ day: '2-digit',
20
+ weekday: 'long'
21
+ }).format(now);
22
+ return { timeText, dateText };
23
+ }
24
+
25
+ function getCharDisplayWidth(char) {
26
+ if (/[\u0000-\u001f\u007f]/.test(char)) {
27
+ return 0;
28
+ }
29
+ if (
30
+ /[\u1100-\u115f\u2329\u232a\u2e80-\u303e\u3040-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe10-\ufe19\ufe30-\ufe6f\uff00-\uff60\uffe0-\uffe6]/.test(char)
31
+ ) {
32
+ return 2;
33
+ }
34
+ return 1;
35
+ }
36
+
37
+ function getDisplayWidth(text) {
38
+ let width = 0;
39
+ for (const char of text) {
40
+ width += getCharDisplayWidth(char);
41
+ }
42
+ return width;
43
+ }
44
+
45
+ function fitText(text, width) {
46
+ let result = '';
47
+ let currentWidth = 0;
48
+ for (const char of text) {
49
+ const charWidth = getCharDisplayWidth(char);
50
+ if (currentWidth + charWidth > width) {
51
+ break;
52
+ }
53
+ result += char;
54
+ currentWidth += charWidth;
55
+ }
56
+ const remaining = width - getDisplayWidth(result);
57
+ if (remaining > 0) {
58
+ return `${result}${' '.repeat(remaining)}`;
59
+ }
60
+ return result;
61
+ }
62
+
63
+ function renderClockBlock() {
64
+ const { timeText, dateText } = getNowDisplay(getLocale());
65
+ const innerWidth = 34;
66
+ const top = `┌${'─'.repeat(innerWidth + 2)}┐`;
67
+ const bottom = `└${'─'.repeat(innerWidth + 2)}┘`;
68
+ const line = (text) => `│ ${fitText(text, innerWidth)} │`;
69
+ return [
70
+ top,
71
+ line(i18next.t('clock.title')),
72
+ line(''),
73
+ line(`${i18next.t('clock.now')}${timeText}`),
74
+ line(dateText),
75
+ line(i18next.t('clock.exitTip')),
76
+ bottom
77
+ ];
78
+ }
79
+
80
+ export async function clockHandler() {
81
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
82
+ const { timeText, dateText } = getNowDisplay(getLocale());
83
+ console.log(`${i18next.t('clock.now')}${timeText} (${dateText})`);
84
+ return;
85
+ }
86
+
87
+ await new Promise((resolve) => {
88
+ let renderedLineCount = 0;
89
+ let shouldExitApp = false;
90
+
91
+ const render = () => {
92
+ const lines = renderClockBlock();
93
+ if (renderedLineCount > 0) {
94
+ readline.moveCursor(process.stdout, 0, -renderedLineCount);
95
+ readline.cursorTo(process.stdout, 0);
96
+ readline.clearScreenDown(process.stdout);
97
+ }
98
+ process.stdout.write(`${lines.join('\n')}\n`);
99
+ renderedLineCount = lines.length;
100
+ };
101
+
102
+ const cleanup = () => {
103
+ clearInterval(timer);
104
+ process.stdin.off('data', onData);
105
+ process.stdin.setRawMode(false);
106
+ process.stdin.pause();
107
+ readline.cursorTo(process.stdout, 0);
108
+ readline.clearScreenDown(process.stdout);
109
+ process.stdout.write('\n');
110
+ if (shouldExitApp) {
111
+ console.log(i18next.t('menu.bye'));
112
+ process.exit(0);
113
+ }
114
+ resolve();
115
+ };
116
+
117
+ const onData = (chunk) => {
118
+ const key = chunk.toString().toLowerCase();
119
+ if (key === '0') {
120
+ shouldExitApp = true;
121
+ cleanup();
122
+ return;
123
+ }
124
+ if (key === '\r' || key === 'q' || key === '\u0003') {
125
+ cleanup();
126
+ }
127
+ };
128
+
129
+ const timer = setInterval(render, 1000);
130
+ process.stdin.setRawMode(true);
131
+ process.stdin.resume();
132
+ process.stdin.on('data', onData);
133
+ render();
134
+ });
135
+ }
@@ -0,0 +1,159 @@
1
+ import inquirer from 'inquirer';
2
+ import readline from 'readline';
3
+ import i18next from '../i18n.js';
4
+
5
+ function getCharDisplayWidth(char) {
6
+ if (/[\u0000-\u001f\u007f]/.test(char)) {
7
+ return 0;
8
+ }
9
+ if (
10
+ /[\u1100-\u115f\u2329\u232a\u2e80-\u303e\u3040-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe10-\ufe19\ufe30-\ufe6f\uff00-\uff60\uffe0-\uffe6]/.test(char)
11
+ ) {
12
+ return 2;
13
+ }
14
+ return 1;
15
+ }
16
+
17
+ function getDisplayWidth(text) {
18
+ let width = 0;
19
+ for (const char of text) {
20
+ width += getCharDisplayWidth(char);
21
+ }
22
+ return width;
23
+ }
24
+
25
+ function fitText(text, width) {
26
+ let result = '';
27
+ let currentWidth = 0;
28
+ for (const char of text) {
29
+ const charWidth = getCharDisplayWidth(char);
30
+ if (currentWidth + charWidth > width) {
31
+ break;
32
+ }
33
+ result += char;
34
+ currentWidth += charWidth;
35
+ }
36
+ const remaining = width - getDisplayWidth(result);
37
+ if (remaining > 0) {
38
+ return `${result}${' '.repeat(remaining)}`;
39
+ }
40
+ return result;
41
+ }
42
+
43
+ function formatSeconds(seconds) {
44
+ const isNegative = seconds < 0;
45
+ const absSeconds = Math.abs(seconds);
46
+ const hours = Math.floor(absSeconds / 3600);
47
+ const minutes = Math.floor((absSeconds % 3600) / 60);
48
+ const secs = absSeconds % 60;
49
+ const sign = isNegative ? '-' : '';
50
+ return `${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
51
+ }
52
+
53
+ function parseMinutes(input) {
54
+ const value = Number(input);
55
+ if (!Number.isFinite(value) || value < 0) {
56
+ return null;
57
+ }
58
+ return value;
59
+ }
60
+
61
+ function renderCountdownBlock(totalSeconds, elapsedSeconds) {
62
+ const remainSeconds = totalSeconds - elapsedSeconds;
63
+ const innerWidth = 34;
64
+ const top = `┌${'─'.repeat(innerWidth + 2)}┐`;
65
+ const bottom = `└${'─'.repeat(innerWidth + 2)}┘`;
66
+ const line = (text) => `│ ${fitText(text, innerWidth)} │`;
67
+ const status = remainSeconds < 0 ? i18next.t('countdown.statusOvertime') : i18next.t('countdown.statusCounting');
68
+
69
+ return [
70
+ top,
71
+ line(i18next.t('countdown.title')),
72
+ line(''),
73
+ line(`${i18next.t('countdown.remaining')}${formatSeconds(remainSeconds)}`),
74
+ line(`${i18next.t('countdown.total')}${formatSeconds(totalSeconds)}`),
75
+ line(`${i18next.t('countdown.status')}${status}`),
76
+ line(i18next.t('countdown.exitTip')),
77
+ bottom
78
+ ];
79
+ }
80
+
81
+ export async function countdownHandler() {
82
+ const { minutesInput } = await inquirer.prompt([
83
+ {
84
+ type: 'input',
85
+ name: 'minutesInput',
86
+ message: i18next.t('countdown.inputPrompt'),
87
+ default: '1',
88
+ validate: (input) => {
89
+ const value = parseMinutes(input);
90
+ if (value === null) {
91
+ return i18next.t('countdown.invalidMinutes');
92
+ }
93
+ return true;
94
+ }
95
+ }
96
+ ]);
97
+
98
+ const minutes = parseMinutes(minutesInput);
99
+ const totalSeconds = Math.round(minutes * 60);
100
+
101
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
102
+ console.log(i18next.t('countdown.nonTtyTip', { value: formatSeconds(totalSeconds) }));
103
+ return;
104
+ }
105
+
106
+ await new Promise((resolve) => {
107
+ let renderedLineCount = 0;
108
+ let elapsedSeconds = 0;
109
+ let shouldExitApp = false;
110
+
111
+ const render = () => {
112
+ const lines = renderCountdownBlock(totalSeconds, elapsedSeconds);
113
+ if (renderedLineCount > 0) {
114
+ readline.moveCursor(process.stdout, 0, -renderedLineCount);
115
+ readline.cursorTo(process.stdout, 0);
116
+ readline.clearScreenDown(process.stdout);
117
+ }
118
+ process.stdout.write(`${lines.join('\n')}\n`);
119
+ renderedLineCount = lines.length;
120
+ };
121
+
122
+ const cleanup = () => {
123
+ clearInterval(timer);
124
+ process.stdin.off('data', onData);
125
+ process.stdin.setRawMode(false);
126
+ process.stdin.pause();
127
+ readline.cursorTo(process.stdout, 0);
128
+ readline.clearScreenDown(process.stdout);
129
+ process.stdout.write('\n');
130
+ if (shouldExitApp) {
131
+ console.log(i18next.t('menu.bye'));
132
+ process.exit(0);
133
+ }
134
+ resolve();
135
+ };
136
+
137
+ const onData = (chunk) => {
138
+ const key = chunk.toString().toLowerCase();
139
+ if (key === '0') {
140
+ shouldExitApp = true;
141
+ cleanup();
142
+ return;
143
+ }
144
+ if (key === '\r' || key === 'q' || key === '\u0003') {
145
+ cleanup();
146
+ }
147
+ };
148
+
149
+ const timer = setInterval(() => {
150
+ elapsedSeconds += 1;
151
+ render();
152
+ }, 1000);
153
+
154
+ process.stdin.setRawMode(true);
155
+ process.stdin.resume();
156
+ process.stdin.on('data', onData);
157
+ render();
158
+ });
159
+ }
@@ -0,0 +1,125 @@
1
+ import readline from 'readline';
2
+ import i18next from '../i18n.js';
3
+
4
+ function getCharDisplayWidth(char) {
5
+ if (/[\u0000-\u001f\u007f]/.test(char)) {
6
+ return 0;
7
+ }
8
+ if (
9
+ /[\u1100-\u115f\u2329\u232a\u2e80-\u303e\u3040-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe10-\ufe19\ufe30-\ufe6f\uff00-\uff60\uffe0-\uffe6]/.test(char)
10
+ ) {
11
+ return 2;
12
+ }
13
+ return 1;
14
+ }
15
+
16
+ function getDisplayWidth(text) {
17
+ let width = 0;
18
+ for (const char of text) {
19
+ width += getCharDisplayWidth(char);
20
+ }
21
+ return width;
22
+ }
23
+
24
+ function fitText(text, width) {
25
+ let result = '';
26
+ let currentWidth = 0;
27
+ for (const char of text) {
28
+ const charWidth = getCharDisplayWidth(char);
29
+ if (currentWidth + charWidth > width) {
30
+ break;
31
+ }
32
+ result += char;
33
+ currentWidth += charWidth;
34
+ }
35
+ const remaining = width - getDisplayWidth(result);
36
+ if (remaining > 0) {
37
+ return `${result}${' '.repeat(remaining)}`;
38
+ }
39
+ return result;
40
+ }
41
+
42
+ function formatSeconds(seconds) {
43
+ const hours = Math.floor(seconds / 3600);
44
+ const minutes = Math.floor((seconds % 3600) / 60);
45
+ const secs = seconds % 60;
46
+ return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
47
+ }
48
+
49
+ function renderStopwatchBlock(elapsedSeconds) {
50
+ const innerWidth = 34;
51
+ const top = `┌${'─'.repeat(innerWidth + 2)}┐`;
52
+ const bottom = `└${'─'.repeat(innerWidth + 2)}┘`;
53
+ const line = (text) => `│ ${fitText(text, innerWidth)} │`;
54
+
55
+ return [
56
+ top,
57
+ line(i18next.t('stopwatch.title')),
58
+ line(''),
59
+ line(`${i18next.t('stopwatch.elapsed')}${formatSeconds(elapsedSeconds)}`),
60
+ line(`${i18next.t('stopwatch.status')}${i18next.t('stopwatch.statusRunning')}`),
61
+ line(i18next.t('stopwatch.exitTip')),
62
+ bottom
63
+ ];
64
+ }
65
+
66
+ export async function stopwatchHandler() {
67
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
68
+ console.log(i18next.t('stopwatch.nonTtyTip'));
69
+ return;
70
+ }
71
+
72
+ await new Promise((resolve) => {
73
+ let renderedLineCount = 0;
74
+ let elapsedSeconds = 0;
75
+ let shouldExitApp = false;
76
+
77
+ const render = () => {
78
+ const lines = renderStopwatchBlock(elapsedSeconds);
79
+ if (renderedLineCount > 0) {
80
+ readline.moveCursor(process.stdout, 0, -renderedLineCount);
81
+ readline.cursorTo(process.stdout, 0);
82
+ readline.clearScreenDown(process.stdout);
83
+ }
84
+ process.stdout.write(`${lines.join('\n')}\n`);
85
+ renderedLineCount = lines.length;
86
+ };
87
+
88
+ const cleanup = () => {
89
+ clearInterval(timer);
90
+ process.stdin.off('data', onData);
91
+ process.stdin.setRawMode(false);
92
+ process.stdin.pause();
93
+ readline.cursorTo(process.stdout, 0);
94
+ readline.clearScreenDown(process.stdout);
95
+ process.stdout.write('\n');
96
+ if (shouldExitApp) {
97
+ console.log(i18next.t('menu.bye'));
98
+ process.exit(0);
99
+ }
100
+ resolve();
101
+ };
102
+
103
+ const onData = (chunk) => {
104
+ const key = chunk.toString().toLowerCase();
105
+ if (key === '0') {
106
+ shouldExitApp = true;
107
+ cleanup();
108
+ return;
109
+ }
110
+ if (key === '\r' || key === 'q' || key === '\u0003') {
111
+ cleanup();
112
+ }
113
+ };
114
+
115
+ const timer = setInterval(() => {
116
+ elapsedSeconds += 1;
117
+ render();
118
+ }, 1000);
119
+
120
+ process.stdin.setRawMode(true);
121
+ process.stdin.resume();
122
+ process.stdin.on('data', onData);
123
+ render();
124
+ });
125
+ }
@@ -7,7 +7,10 @@ export async function urlHandler() {
7
7
  const mode = await selectFromMenu(i18next.t('url.title'), [
8
8
  { name: i18next.t('url.encode'), value: 'encode' },
9
9
  { name: i18next.t('url.decode'), value: 'decode' }
10
- ]);
10
+ ], true, i18next.t('common.back'));
11
+ if (mode === '__BACK__') {
12
+ return;
13
+ }
11
14
 
12
15
  const { input } = await inquirer.prompt([
13
16
  {
package/src/index.js CHANGED
@@ -14,6 +14,9 @@ import { imgSplitHandler } from './commands/imgSplit.js';
14
14
  import { imgMergeHandler } from './commands/imgMerge.js';
15
15
  import { timeFormatHandler } from './commands/timeFormat.js';
16
16
  import { timeCalcHandler } from './commands/timeCalc.js';
17
+ import { clockHandler } from './commands/clock.js';
18
+ import { countdownHandler } from './commands/countdown.js';
19
+ import { stopwatchHandler } from './commands/stopwatch.js';
17
20
  import { mockHandler } from './commands/mock.js';
18
21
  import { uuidHandler } from './commands/uuid.js';
19
22
  import { pinyinHandler } from './commands/pinyin.js';
@@ -84,6 +87,9 @@ function getFeatures() {
84
87
  // Utils (Time, Color, UUID, Hash)
85
88
  { name: i18next.t('menu.features.timeFormat'), value: 'timeFormat' },
86
89
  { name: i18next.t('menu.features.timeCalc'), value: 'timeCalc' },
90
+ { name: i18next.t('menu.features.clock'), value: 'clock' },
91
+ { name: i18next.t('menu.features.countdown'), value: 'countdown' },
92
+ { name: i18next.t('menu.features.stopwatch'), value: 'stopwatch' },
87
93
  { name: i18next.t('menu.features.color'), value: 'color' },
88
94
  { name: i18next.t('menu.features.colorPreview'), value: 'colorPreview' },
89
95
  { name: i18next.t('menu.features.uuid'), value: 'uuid' },
@@ -310,6 +316,15 @@ async function handleAction(action) {
310
316
  case 'timeCalc':
311
317
  await timeCalcHandler();
312
318
  break;
319
+ case 'clock':
320
+ await clockHandler();
321
+ break;
322
+ case 'countdown':
323
+ await countdownHandler();
324
+ break;
325
+ case 'stopwatch':
326
+ await stopwatchHandler();
327
+ break;
313
328
  case 'mock':
314
329
  await mockHandler();
315
330
  break;
package/src/locales/en.js CHANGED
@@ -61,6 +61,9 @@ export default {
61
61
  pinyin: 'Chinese to Pinyin',
62
62
  timeFormat: 'Time Format / Timestamp',
63
63
  timeCalc: 'Time Calculation (Diff/Offset)',
64
+ clock: 'Console Clock',
65
+ countdown: 'Countdown Timer',
66
+ stopwatch: 'Stopwatch',
64
67
  color: 'Color Converter (Hex <-> RGB)',
65
68
  colorPreview: 'Color Preview',
66
69
  uuid: 'Get UUID',
@@ -131,6 +134,32 @@ export default {
131
134
  result: 'Result:',
132
135
  error: 'Error processing URL:'
133
136
  },
137
+ clock: {
138
+ title: 'Console Clock',
139
+ start: 'Console clock started. Press Enter / Q to go back.',
140
+ now: 'Current time: ',
141
+ exitTip: 'Press Enter / Q to go back, 0 to exit app'
142
+ },
143
+ countdown: {
144
+ title: 'Countdown Timer',
145
+ inputPrompt: 'Enter countdown minutes:',
146
+ invalidMinutes: 'Please enter a number greater than or equal to 0',
147
+ remaining: 'Remaining: ',
148
+ total: 'Total: ',
149
+ status: 'Status: ',
150
+ statusCounting: 'Counting down',
151
+ statusOvertime: 'Overtime',
152
+ exitTip: 'Press Enter / Q to go back, 0 to exit app',
153
+ nonTtyTip: 'Current terminal does not support dynamic countdown. Target duration: {{value}}'
154
+ },
155
+ stopwatch: {
156
+ title: 'Stopwatch',
157
+ elapsed: 'Elapsed: ',
158
+ status: 'Status: ',
159
+ statusRunning: 'Running',
160
+ exitTip: 'Press Enter / Q to go back, 0 to exit app',
161
+ nonTtyTip: 'Current terminal does not support dynamic stopwatch.'
162
+ },
134
163
  baseConvert: {
135
164
  inputPrompt: 'Enter number:',
136
165
  invalidInput: 'Please enter a valid number',
package/src/locales/zh.js CHANGED
@@ -61,6 +61,9 @@ export default {
61
61
  pinyin: '汉字转拼音',
62
62
  timeFormat: '时间格式化 / 时间戳',
63
63
  timeCalc: '时间计算 (差值/偏移)',
64
+ clock: '控制台时钟',
65
+ countdown: '倒计时工具',
66
+ stopwatch: '计时工具',
64
67
  color: '颜色转换 (Hex <-> RGB)',
65
68
  colorPreview: '颜色预览',
66
69
  uuid: '生成 UUID',
@@ -131,6 +134,32 @@ export default {
131
134
  result: '结果:',
132
135
  error: '处理 URL 时出错:'
133
136
  },
137
+ clock: {
138
+ title: '控制台时钟',
139
+ start: '控制台时钟已启动,按 Enter / Q 返回上一步。',
140
+ now: '当前时间: ',
141
+ exitTip: '按 Enter / Q 返回上一步,按 0 直接退出'
142
+ },
143
+ countdown: {
144
+ title: '倒计时工具',
145
+ inputPrompt: '请输入倒计时分钟数:',
146
+ invalidMinutes: '请输入大于等于 0 的数字分钟数',
147
+ remaining: '剩余时间: ',
148
+ total: '倒计总时: ',
149
+ status: '当前状态: ',
150
+ statusCounting: '倒计时中',
151
+ statusOvertime: '已超时',
152
+ exitTip: '按 Enter / Q 返回上一步,按 0 直接退出',
153
+ nonTtyTip: '当前终端不支持动态倒计时。目标时长: {{value}}'
154
+ },
155
+ stopwatch: {
156
+ title: '计时工具',
157
+ elapsed: '已计时: ',
158
+ status: '当前状态: ',
159
+ statusRunning: '计时中',
160
+ exitTip: '按 Enter / Q 返回上一步,按 0 直接退出',
161
+ nonTtyTip: '当前终端不支持动态计时。'
162
+ },
134
163
  baseConvert: {
135
164
  inputPrompt: '请输入数字:',
136
165
  invalidInput: '请输入有效的数字',