cliedit 0.1.0 → 0.1.1

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/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 CodeTease
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2025 CodeTease
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,88 +1,92 @@
1
- # cliedit
2
-
3
- A lightweight, zero-dependency (other than `keypress`), raw-mode terminal editor component for Node.js.
4
-
5
- `cliedit` is designed to be imported into your own CLI application to provide a full-featured, TTY-based text editing experience. It's perfect for applications that need to ask the user for multi-line input, edit configuration files, or write commit messages.
6
-
7
- It includes line wrapping, visual navigation, undo/redo, text selection, and clipboard support.
8
-
9
- ## Features
10
-
11
- - **Raw Mode TTY:** Takes over the terminal for a full "app-like" feel.
12
- - **Visual Line Wrapping:** Text wraps to fit the terminal width.
13
- - **Visual Navigation:** `Up`/`Down` arrows move by visual rows, not logical lines.
14
- - **Undo/Redo:** `Ctrl+Z` / `Ctrl+Y` for persistent history.
15
- - **Text Selection:** `Ctrl+Arrow` keys to select text.
16
- - **Clipboard Support:** `Ctrl+C` (Copy), `Ctrl+X` (Cut), `Ctrl+V` (Paste) for system clipboard (macOS/Windows).
17
- - **File I/O:** Loads from and saves to the filesystem.
18
- - **Search:** `Ctrl+W` to find text.
19
-
20
- ## Installation
21
- ```bash
22
- npm install cliedit
23
- ```
24
-
25
- ## Usage
26
-
27
- The package exports an `async` function `openEditor` that returns a `Promise`. The promise resolves when the user quits the editor.
28
- ```javascript
29
- import { openEditor } from 'cliedit';
30
- import path from 'path';
31
-
32
- async function getCommitMessage() {
33
- const tempFile = path.resolve(process.cwd(), 'COMMIT_MSG.txt');
34
- console.log('Opening editor for commit message...');
35
-
36
- try {
37
- const result = await openEditor(tempFile);
38
-
39
- // Give the terminal a moment to restore
40
- await new Promise(res => setTimeout(res, 50));
41
-
42
- if (result.saved) {
43
- console.log('Message saved!');
44
- console.log('---------------------');
45
- console.log(result.content);
46
- console.log('---------------------');
47
- } else {
48
- console.log('Editor quit without saving.');
49
- }
50
- } catch (err) {
51
- console.error('Editor failed to start:', err);
52
- }
53
- }
54
-
55
- getCommitMessage();
56
- ```
57
-
58
- ## Public API
59
-
60
- `openEditor(filepath: string)`
61
-
62
- Opens the editor for the specified file. If the file doesn't exist, it will be created upon saving.
63
- - **Returns:** `Promise<{ saved: boolean; content: string }>`
64
- * `saved`: `true` if the user saved (Ctrl+S), `false` otherwise (Ctrl+Q).
65
- * `content`: The final content of the file as a string.
66
-
67
- `CliEditor`
68
-
69
- The main editor class. You can import this directly if you need to extend or instantiate the editor with custom logic.
70
- ```javascript
71
- import { CliEditor } from 'cliedit';
72
- ```
73
-
74
- ### Types
75
-
76
- Key types are also exported for convenience:
77
- ```javascript
78
- import type {
79
- DocumentState,
80
- VisualRow,
81
- EditorMode,
82
- NormalizedRange,
83
- } from 'cliedit';
84
- ```
85
-
86
- ## License
87
-
1
+ # cliedit
2
+
3
+ A lightweight, zero-dependency, raw-mode terminal editor component for Node.js.
4
+
5
+ `cliedit` is designed to be imported into your own CLI application to provide a full-featured, TTY-based text editing experience. It's perfect for applications that need to ask the user for multi-line input, edit configuration files, or write commit messages.
6
+
7
+ It includes line wrapping, visual navigation, undo/redo, text selection, and clipboard support.
8
+
9
+ ## Features
10
+
11
+ - **Raw Mode TTY:** Takes over the terminal for a full "app-like" feel.
12
+ - **Visual Line Wrapping:** Text wraps to fit the terminal width.
13
+ - **Visual Navigation:** `Up`/`Down` arrows move by visual rows, not logical lines.
14
+ - **Undo/Redo:** `Ctrl+Z` / `Ctrl+Y` for persistent history.
15
+ - **Text Selection:** `Ctrl+Arrow` keys to select text.
16
+ - **Clipboard Support:** `Ctrl+C` (Copy), `Ctrl+X` (Cut), `Ctrl+V` (Paste) for system clipboard (macOS/Windows).
17
+ - **File I/O:** Loads from and saves to the filesystem.
18
+ - **Search:** `Ctrl+W` to find text.
19
+
20
+ ## Installation
21
+ ```bash
22
+ npm install cliedit
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ The package exports an `async` function `openEditor` that returns a `Promise`. The promise resolves when the user quits the editor.
28
+ ```javascript
29
+ import { openEditor } from 'cliedit';
30
+ import path from 'path';
31
+
32
+ async function getCommitMessage() {
33
+ const tempFile = path.resolve(process.cwd(), 'COMMIT_MSG.txt');
34
+ console.log('Opening editor for commit message...');
35
+
36
+ try {
37
+ const result = await openEditor(tempFile);
38
+
39
+ // Give the terminal a moment to restore
40
+ await new Promise(res => setTimeout(res, 50));
41
+
42
+ if (result.saved) {
43
+ console.log('Message saved!');
44
+ console.log('---------------------');
45
+ console.log(result.content);
46
+ console.log('---------------------');
47
+ } else {
48
+ console.log('Editor quit without saving.');
49
+ }
50
+ } catch (err) {
51
+ console.error('Editor failed to start:', err);
52
+ }
53
+ }
54
+
55
+ getCommitMessage();
56
+ ```
57
+
58
+ ## Public API
59
+
60
+ `openEditor(filepath: string)`
61
+
62
+ Opens the editor for the specified file. If the file doesn't exist, it will be created upon saving.
63
+ - **Returns:** `Promise<{ saved: boolean; content: string }>`
64
+ * `saved`: `true` if the user saved (Ctrl+S), `false` otherwise (Ctrl+Q).
65
+ * `content`: The final content of the file as a string.
66
+
67
+ `CliEditor`
68
+
69
+ The main editor class. You can import this directly if you need to extend or instantiate the editor with custom logic.
70
+ ```javascript
71
+ import { CliEditor } from 'cliedit';
72
+ ```
73
+
74
+ ### Types
75
+
76
+ Key types are also exported for convenience:
77
+ ```javascript
78
+ import type {
79
+ DocumentState,
80
+ VisualRow,
81
+ EditorMode,
82
+ NormalizedRange,
83
+ } from 'cliedit';
84
+ ```
85
+
86
+ ## Acknowledgements
87
+
88
+ Please see the [ACKNOWLEDGEMENTS.md](ACKNOWLEDGEMENTS.md) file for important copyright information regarding the vendored `keypress` component.
89
+
90
+ ## License
91
+
88
92
  [MIT](LICENSE)
package/dist/editor.d.ts CHANGED
@@ -1,14 +1,5 @@
1
1
  import { HistoryManager } from './history.js';
2
2
  import { VisualRow, EditorMode } from './types.js';
3
- declare module 'keypress' {
4
- interface KeypressEvent {
5
- name?: string;
6
- ctrl: boolean;
7
- meta: boolean;
8
- shift: boolean;
9
- sequence: string;
10
- }
11
- }
12
3
  import { editingMethods } from './editor.editing.js';
13
4
  import { clipboardMethods } from './editor.clipboard.js';
14
5
  import { navigationMethods } from './editor.navigation.js';
package/dist/editor.js CHANGED
@@ -1,6 +1,8 @@
1
- import keypress from 'keypress';
1
+ // Cập nhật 1: Import từ ./vendor/keypress.js
2
+ import keypress from './vendor/keypress.js';
2
3
  import { ANSI } from './constants.js';
3
4
  import { HistoryManager } from './history.js';
5
+ // Block `declare module 'keypress'` đã bị xóa
4
6
  // Import all functional modules
5
7
  import { editingMethods } from './editor.editing.js';
6
8
  import { clipboardMethods } from './editor.clipboard.js';
@@ -1,13 +1,4 @@
1
- import type { KeypressEvent } from 'keypress';
2
- declare module 'keypress' {
3
- interface KeypressEvent {
4
- name?: string;
5
- ctrl: boolean;
6
- meta: boolean;
7
- shift: boolean;
8
- sequence: string;
9
- }
10
- }
1
+ import type { KeypressEvent } from './vendor/keypress.js';
11
2
  export type TKeyHandlingMethods = {
12
3
  handleKeypressEvent: (ch: string, key: KeypressEvent) => void;
13
4
  handleEditKeys: (key: string) => boolean;
@@ -8,19 +8,23 @@ function handleKeypressEvent(ch, key) {
8
8
  if (this.isExiting) {
9
9
  return;
10
10
  }
11
- // CRASH FIX: Handle case where 'key' is undefined (a normal character key)
11
+ let keyName = undefined;
12
+ let edited = false;
13
+ // --- 1. Xử lý trường hợp key là null/undefined (Ký tự in được) ---
12
14
  if (!key) {
13
- if (ch && ch >= ' ' && ch <= '~') {
14
- this.handleCharacterKey(ch);
15
- this.recalculateVisualRows();
15
+ if (ch && ch.length === 1 && ch >= ' ' && ch <= '~') {
16
+ edited = this.handleEditKeys(ch);
17
+ if (edited) {
18
+ this.saveState();
19
+ this.recalculateVisualRows(); // Phải tính toán lại sau khi gõ
20
+ }
16
21
  this.render();
22
+ return;
17
23
  }
18
24
  return;
19
25
  }
20
- // --- From here, 'key' object is guaranteed to exist ---
21
- let keyName = undefined;
22
- let edited = false;
23
- // 1. Map Control sequences (Ctrl+Arrow for selection)
26
+ // --- 2. Từ đây, 'key' object đảm bảo (phím đặc biệt hoặc Ctrl/Meta) ---
27
+ // 2.1. Ánh xạ Control sequences (Ctrl+Arrow cho selection)
24
28
  if (key.ctrl) {
25
29
  if (key.name === 'up')
26
30
  keyName = KEYS.CTRL_ARROW_UP;
@@ -31,10 +35,10 @@ function handleKeypressEvent(ch, key) {
31
35
  else if (key.name === 'right')
32
36
  keyName = KEYS.CTRL_ARROW_RIGHT;
33
37
  else
34
- keyName = key.sequence; // Use sequence for Ctrl+S, Ctrl+C, etc.
38
+ keyName = key.sequence;
35
39
  }
36
40
  else {
37
- // 2. (FIXED) Map standard navigation keys (Arrow, Home, End)
41
+ // 2.2. Ánh xạ phím tiêu chuẩn (Arrow, Home, End, Enter, Tab)
38
42
  if (key.name === 'up')
39
43
  keyName = KEYS.ARROW_UP;
40
44
  else if (key.name === 'down')
@@ -60,55 +64,41 @@ function handleKeypressEvent(ch, key) {
60
64
  else if (key.name === 'tab')
61
65
  keyName = KEYS.TAB;
62
66
  else
63
- keyName = key.sequence; // Fallback
67
+ keyName = key.sequence;
64
68
  }
65
- // 3. (FIXED) Handle printable characters immediately
66
- // This was the source of the "no typing" bug.
67
- // We must check for characters *before* routing to handleEditKeys.
68
- if (keyName && keyName.length === 1 && keyName >= ' ' && keyName <= '~' && !key.ctrl && !key.meta) {
69
- this.handleCharacterKey(keyName);
70
- this.recalculateVisualRows();
71
- this.render();
72
- return;
73
- }
74
- // 4. Mode Routing (If it's not a character, it's a command)
69
+ // --- 3. Định tuyến theo Mode ---
75
70
  if (this.mode === 'search') {
76
71
  this.handleSearchKeys(keyName || ch);
77
72
  }
78
73
  else {
79
- // 5. Handle Selection Keys (Ctrl+Arrow)
74
+ // 4. Xử phím lựa chọn (Ctrl+Arrow) - Navigation
80
75
  switch (keyName) {
81
76
  case KEYS.CTRL_ARROW_UP:
82
- this.startOrUpdateSelection();
83
- this.moveCursorVisually(-1);
84
- this.render();
85
- return;
86
77
  case KEYS.CTRL_ARROW_DOWN:
87
- this.startOrUpdateSelection();
88
- this.moveCursorVisually(1);
89
- this.render();
90
- return;
91
78
  case KEYS.CTRL_ARROW_LEFT:
92
- this.startOrUpdateSelection();
93
- this.moveCursorLogically(-1);
94
- this.render();
95
- return;
96
79
  case KEYS.CTRL_ARROW_RIGHT:
97
80
  this.startOrUpdateSelection();
98
- this.moveCursorLogically(1);
81
+ if (keyName === KEYS.CTRL_ARROW_UP)
82
+ this.moveCursorVisually(-1);
83
+ else if (keyName === KEYS.CTRL_ARROW_DOWN)
84
+ this.moveCursorVisually(1);
85
+ else if (keyName === KEYS.CTRL_ARROW_LEFT)
86
+ this.moveCursorLogically(-1);
87
+ else if (keyName === KEYS.CTRL_ARROW_RIGHT)
88
+ this.moveCursorLogically(1);
99
89
  this.render();
100
90
  return;
101
91
  }
102
- // 6. Handle all other command keys (Editing/Commands)
92
+ // 5. Xử tất cả các phím lệnh/chỉnh sửa khác
103
93
  edited = this.handleEditKeys(keyName || ch);
104
94
  }
105
- // 7. State Update and Render
95
+ // 6. Cập nhật Trạng thái và Render
106
96
  if (edited) {
107
- this.saveState();
108
- this.recalculateVisualRows();
97
+ this.saveState(); // <-- Chỉ gọi khi gõ phím, xóa, v.v.
98
+ this.recalculateVisualRows(); // Tính toán lại layout
109
99
  }
110
100
  if (!this.isExiting) {
111
- this.render();
101
+ this.render(); // Render cuối cùng (với visual rows đã được cập nhật nếu cần)
112
102
  }
113
103
  }
114
104
  /**
@@ -116,10 +106,6 @@ function handleKeypressEvent(ch, key) {
116
106
  * Returns true if content was modified.
117
107
  */
118
108
  function handleEditKeys(key) {
119
- // (FIXED) Removed the guard clause that was blocking typing.
120
- // if (key.length === 1 && key >= ' ' && key <= '~') {
121
- // return false;
122
- // }
123
109
  // Cancel selection on normal navigation
124
110
  const isNavigation = [
125
111
  KEYS.ARROW_UP, KEYS.ARROW_DOWN, KEYS.ARROW_LEFT, KEYS.ARROW_RIGHT,
@@ -131,7 +117,6 @@ function handleEditKeys(key) {
131
117
  this.setStatusMessage(this.DEFAULT_STATUS, 0);
132
118
  }
133
119
  }
134
- // Commands that return Promises must be wrapped in a sync call here
135
120
  switch (key) {
136
121
  // --- Exit / Save ---
137
122
  case KEYS.CTRL_Q:
@@ -143,7 +128,7 @@ function handleEditKeys(key) {
143
128
  case KEYS.CTRL_C:
144
129
  this.handleCopy();
145
130
  return false;
146
- // --- Navigation ---
131
+ // --- Navigation (Non-Selection) ---
147
132
  case KEYS.ARROW_UP:
148
133
  this.moveCursorVisually(-1);
149
134
  return false;
@@ -194,12 +179,16 @@ function handleEditKeys(key) {
194
179
  case KEYS.CTRL_G:
195
180
  this.findNext();
196
181
  return false;
182
+ // ***** SỬA LỖI VISUAL *****
183
+ // Sau khi undo/redo, chúng ta PHẢI tính toán lại visual rows
197
184
  case KEYS.CTRL_Z:
198
185
  this.undo();
199
- return true;
186
+ this.recalculateVisualRows(); // <-- THÊM DÒNG NÀY
187
+ return false;
200
188
  case KEYS.CTRL_Y:
201
189
  this.redo();
202
- return true;
190
+ this.recalculateVisualRows(); // <-- THÊM DÒNG NÀY
191
+ return false;
203
192
  // --- Clipboard ---
204
193
  case KEYS.CTRL_K: // Cut Line (Traditional)
205
194
  this.cutLine();
@@ -208,12 +197,17 @@ function handleEditKeys(key) {
208
197
  this.pasteLine();
209
198
  return true;
210
199
  case KEYS.CTRL_X: // Cut Selection
211
- this.cutSelection(); // Synchronous wrapper for cutSelectionAsync
200
+ this.cutSelection();
212
201
  return true;
213
202
  case KEYS.CTRL_V: // Paste Selection
214
203
  this.pasteSelection();
215
204
  return true;
205
+ // Xử lý Ký tự in được
216
206
  default:
207
+ if (key.length === 1 && key >= ' ' && key <= '~') {
208
+ this.handleCharacterKey(key);
209
+ return true;
210
+ }
217
211
  return false;
218
212
  }
219
213
  }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Định nghĩa giao diện cho một sự kiện keypress.
3
+ * Được điều chỉnh từ file editor.ts.
4
+ */
5
+ export interface KeypressEvent {
6
+ name?: string;
7
+ ctrl: boolean;
8
+ meta: boolean;
9
+ shift: boolean;
10
+ sequence: string;
11
+ code?: string;
12
+ }
13
+ /**
14
+ * Hàm chính, chấp nhận một Readable Stream và làm cho nó
15
+ * phát ra sự kiện "keypress".
16
+ */
17
+ export default function keypress(stream: NodeJS.ReadStream): void;
@@ -0,0 +1,425 @@
1
+ // src/vendor/keypress.ts
2
+ // Đây là phiên bản "vendored" của thư viện 'keypress' (0.2.1)
3
+ // được chuyển đổi sang TypeScript và loại bỏ hỗ trợ chuột
4
+ // để tích hợp trực tiếp vào cliedit.
5
+ import { EventEmitter } from 'events';
6
+ import { StringDecoder } from 'string_decoder';
7
+ /**
8
+ * Hàm polyfill cho `EventEmitter.listenerCount()`, để tương thích ngược.
9
+ */
10
+ let listenerCount = EventEmitter.listenerCount;
11
+ if (!listenerCount) {
12
+ listenerCount = function (emitter, event) {
13
+ return emitter.listeners(event).length;
14
+ };
15
+ }
16
+ /**
17
+ * Regexes dùng để phân tích escape code của ansi
18
+ */
19
+ const metaKeyCodeRe = /^(?:\x1b)([a-zA-Z0-9])$/;
20
+ const functionKeyCodeRe = /^(?:\x1b+)(O|N|\[|\[\[)(?:(\d+)(?:;(\d+))?([~^$])|(?:1;)?(\d+)?([a-zA-Z]))/;
21
+ /**
22
+ * Hàm chính, chấp nhận một Readable Stream và làm cho nó
23
+ * phát ra sự kiện "keypress".
24
+ */
25
+ export default function keypress(stream) {
26
+ if (isEmittingKeypress(stream))
27
+ return;
28
+ // Gắn decoder vào stream để theo dõi
29
+ stream._keypressDecoder = new StringDecoder('utf8');
30
+ function onData(b) {
31
+ if (listenerCount(stream, 'keypress') > 0) {
32
+ const r = stream._keypressDecoder.write(b);
33
+ if (r)
34
+ emitKey(stream, r);
35
+ }
36
+ else {
37
+ // Không ai đang nghe, gỡ bỏ listener
38
+ stream.removeListener('data', onData);
39
+ stream.on('newListener', onNewListener);
40
+ }
41
+ }
42
+ function onNewListener(event) {
43
+ if (event === 'keypress') {
44
+ stream.on('data', onData);
45
+ stream.removeListener('newListener', onNewListener);
46
+ }
47
+ }
48
+ if (listenerCount(stream, 'keypress') > 0) {
49
+ stream.on('data', onData);
50
+ }
51
+ else {
52
+ stream.on('newListener', onNewListener);
53
+ }
54
+ }
55
+ /**
56
+ * Kiểm tra xem stream đã phát ra sự kiện "keypress" hay chưa.
57
+ */
58
+ function isEmittingKeypress(stream) {
59
+ let rtn = !!stream._keypressDecoder;
60
+ if (!rtn) {
61
+ // XXX: Đối với các phiên bản node cũ, chúng ta muốn xóa các
62
+ // listener "data" và "newListener" hiện có vì chúng sẽ không
63
+ // bao gồm các phần mở rộng của module này (như "mousepress" đã bị loại bỏ).
64
+ stream.listeners('data').slice(0).forEach(function (l) {
65
+ if (l.name === 'onData' && /emitKey/.test(l.toString())) {
66
+ // FIX TS2769: Ép kiểu 'l' thành kiểu listener hợp lệ
67
+ stream.removeListener('data', l);
68
+ }
69
+ });
70
+ stream.listeners('newListener').slice(0).forEach(function (l) {
71
+ if (l.name === 'onNewListener' && /keypress/.test(l.toString())) {
72
+ // FIX TS2769: Ép kiểu 'l' thành kiểu listener hợp lệ
73
+ stream.removeListener('newListener', l);
74
+ }
75
+ });
76
+ }
77
+ return rtn;
78
+ }
79
+ /**
80
+ * Phần code bên dưới được lấy từ module `readline.js` của node-core
81
+ * và đã được chuyển đổi sang TypeScript.
82
+ */
83
+ function emitKey(stream, s) {
84
+ let ch;
85
+ const key = {
86
+ name: undefined,
87
+ ctrl: false,
88
+ meta: false,
89
+ shift: false,
90
+ sequence: s,
91
+ };
92
+ let parts;
93
+ // Cảnh báo: Block `Buffer.isBuffer(s)` đã bị loại bỏ.
94
+ // Lý do: `onData` luôn gọi `emitKey` với một string (kết quả từ StringDecoder).
95
+ // Block đệ quy (paste) cũng gọi với string.
96
+ // Vì vậy, `s` luôn là string.
97
+ if (s === '\r') {
98
+ // carriage return
99
+ key.name = 'return';
100
+ }
101
+ else if (s === '\n') {
102
+ // enter, đáng lẽ phải là linefeed
103
+ key.name = 'enter';
104
+ }
105
+ else if (s === '\t') {
106
+ // tab
107
+ key.name = 'tab';
108
+ }
109
+ else if (s === '\b' ||
110
+ s === '\x7f' ||
111
+ s === '\x1b\x7f' ||
112
+ s === '\x1b\b') {
113
+ // backspace hoặc ctrl+h
114
+ key.name = 'backspace';
115
+ key.meta = s.charAt(0) === '\x1b';
116
+ }
117
+ else if (s === '\x1b' || s === '\x1b\x1b') {
118
+ // escape key
119
+ key.name = 'escape';
120
+ key.meta = s.length === 2;
121
+ }
122
+ else if (s === ' ' || s === '\x1b ') {
123
+ key.name = 'space';
124
+ key.meta = s.length === 2;
125
+ }
126
+ else if (s <= '\x1a') {
127
+ // ctrl+letter
128
+ key.name = String.fromCharCode(s.charCodeAt(0) + 'a'.charCodeAt(0) - 1);
129
+ key.ctrl = true;
130
+ }
131
+ else if (s.length === 1 && s >= 'a' && s <= 'z') {
132
+ // lowercase letter
133
+ key.name = s;
134
+ }
135
+ else if (s.length === 1 && s >= 'A' && s <= 'Z') {
136
+ // shift+letter
137
+ key.name = s.toLowerCase();
138
+ key.shift = true;
139
+ }
140
+ else if ((parts = metaKeyCodeRe.exec(s))) {
141
+ // meta+character key
142
+ key.name = parts[1].toLowerCase();
143
+ key.meta = true;
144
+ key.shift = /^[A-Z]$/.test(parts[1]);
145
+ }
146
+ else if ((parts = functionKeyCodeRe.exec(s))) {
147
+ // ansi escape sequence
148
+ // Lắp ráp lại key code, bỏ qua \x1b đứng đầu,
149
+ // bitflag của phím bổ trợ và bất kỳ chuỗi "1;" vô nghĩa nào
150
+ const code = (parts[1] || '') +
151
+ (parts[2] || '') +
152
+ (parts[4] || '') +
153
+ (parts[6] || '');
154
+ // FIX TS2362: Chuyển đổi (parts[...]) sang number bằng parseInt
155
+ const modifier = parseInt(parts[3] || parts[5] || '1', 10) - 1;
156
+ // Phân tích phím bổ trợ
157
+ key.ctrl = !!(modifier & 4);
158
+ key.meta = !!(modifier & 10);
159
+ key.shift = !!(modifier & 1);
160
+ key.code = code;
161
+ // Phân tích chính phím đó
162
+ switch (code) {
163
+ /* xterm/gnome ESC O letter */
164
+ case 'OP':
165
+ key.name = 'f1';
166
+ break;
167
+ case 'OQ':
168
+ key.name = 'f2';
169
+ break;
170
+ case 'OR':
171
+ key.name = 'f3';
172
+ break;
173
+ case 'OS':
174
+ key.name = 'f4';
175
+ break;
176
+ /* xterm/rxvt ESC [ number ~ */
177
+ case '[11~':
178
+ key.name = 'f1';
179
+ break;
180
+ case '[12~':
181
+ key.name = 'f2';
182
+ break;
183
+ case '[13~':
184
+ key.name = 'f3';
185
+ break;
186
+ case '[14~':
187
+ key.name = 'f4';
188
+ break;
189
+ /* from Cygwin and used in libuv */
190
+ case '[[A':
191
+ key.name = 'f1';
192
+ break;
193
+ case '[[B':
194
+ key.name = 'f2';
195
+ break;
196
+ case '[[C':
197
+ key.name = 'f3';
198
+ break;
199
+ case '[[D':
200
+ key.name = 'f4';
201
+ break;
202
+ case '[[E':
203
+ key.name = 'f5';
204
+ break;
205
+ /* common */
206
+ case '[15~':
207
+ key.name = 'f5';
208
+ break;
209
+ case '[17~':
210
+ key.name = 'f6';
211
+ break;
212
+ case '[18~':
213
+ key.name = 'f7';
214
+ break;
215
+ case '[19~':
216
+ key.name = 'f8';
217
+ break;
218
+ case '[20~':
219
+ key.name = 'f9';
220
+ break;
221
+ case '[21~':
222
+ key.name = 'f10';
223
+ break;
224
+ case '[23~':
225
+ key.name = 'f11';
226
+ break;
227
+ case '[24~':
228
+ key.name = 'f12';
229
+ break;
230
+ /* xterm ESC [ letter */
231
+ case '[A':
232
+ key.name = 'up';
233
+ break;
234
+ case '[B':
235
+ key.name = 'down';
236
+ break;
237
+ case '[C':
238
+ key.name = 'right';
239
+ break;
240
+ case '[D':
241
+ key.name = 'left';
242
+ break;
243
+ case '[E':
244
+ key.name = 'clear';
245
+ break;
246
+ case '[F':
247
+ key.name = 'end';
248
+ break;
249
+ case '[H':
250
+ key.name = 'home';
251
+ break;
252
+ /* xterm/gnome ESC O letter */
253
+ case 'OA':
254
+ key.name = 'up';
255
+ break;
256
+ case 'OB':
257
+ key.name = 'down';
258
+ break;
259
+ case 'OC':
260
+ key.name = 'right';
261
+ break;
262
+ case 'OD':
263
+ key.name = 'left';
264
+ break;
265
+ case 'OE':
266
+ key.name = 'clear';
267
+ break;
268
+ case 'OF':
269
+ key.name = 'end';
270
+ break;
271
+ case 'OH':
272
+ key.name = 'home';
273
+ break;
274
+ /* xterm/rxvt ESC [ number ~ */
275
+ case '[1~':
276
+ key.name = 'home';
277
+ break;
278
+ case '[2~':
279
+ key.name = 'insert';
280
+ break;
281
+ case '[3~':
282
+ key.name = 'delete';
283
+ break;
284
+ case '[4~':
285
+ key.name = 'end';
286
+ break;
287
+ case '[5~':
288
+ key.name = 'pageup';
289
+ break;
290
+ case '[6~':
291
+ key.name = 'pagedown';
292
+ break;
293
+ /* putty */
294
+ case '[[5~':
295
+ key.name = 'pageup';
296
+ break;
297
+ case '[[6~':
298
+ key.name = 'pagedown';
299
+ break;
300
+ /* rxvt */
301
+ case '[7~':
302
+ key.name = 'home';
303
+ break;
304
+ case '[8~':
305
+ key.name = 'end';
306
+ break;
307
+ /* rxvt keys with modifiers */
308
+ case '[a':
309
+ key.name = 'up';
310
+ key.shift = true;
311
+ break;
312
+ case '[b':
313
+ key.name = 'down';
314
+ key.shift = true;
315
+ break;
316
+ case '[c':
317
+ key.name = 'right';
318
+ key.shift = true;
319
+ break;
320
+ case '[d':
321
+ key.name = 'left';
322
+ key.shift = true;
323
+ break;
324
+ case '[e':
325
+ key.name = 'clear';
326
+ key.shift = true;
327
+ break;
328
+ case '[2$':
329
+ key.name = 'insert';
330
+ key.shift = true;
331
+ break;
332
+ case '[3$':
333
+ key.name = 'delete';
334
+ key.shift = true;
335
+ break;
336
+ case '[5$':
337
+ key.name = 'pageup';
338
+ key.shift = true;
339
+ break;
340
+ case '[6$':
341
+ key.name = 'pagedown';
342
+ key.shift = true;
343
+ break;
344
+ case '[7$':
345
+ key.name = 'home';
346
+ key.shift = true;
347
+ break;
348
+ case '[8$':
349
+ key.name = 'end';
350
+ key.shift = true;
351
+ break;
352
+ case 'Oa':
353
+ key.name = 'up';
354
+ key.ctrl = true;
355
+ break;
356
+ case 'Ob':
357
+ key.name = 'down';
358
+ key.ctrl = true;
359
+ break;
360
+ case 'Oc':
361
+ key.name = 'right';
362
+ key.ctrl = true;
363
+ break;
364
+ case 'Od':
365
+ key.name = 'left';
366
+ key.ctrl = true;
367
+ break;
368
+ case 'Oe':
369
+ key.name = 'clear';
370
+ key.ctrl = true;
371
+ break;
372
+ case '[2^':
373
+ key.name = 'insert';
374
+ key.ctrl = true;
375
+ break;
376
+ case '[3^':
377
+ key.name = 'delete';
378
+ key.ctrl = true;
379
+ break;
380
+ case '[5^':
381
+ key.name = 'pageup';
382
+ key.ctrl = true;
383
+ break;
384
+ case '[6^':
385
+ key.name = 'pagedown';
386
+ key.ctrl = true;
387
+ break;
388
+ case '[7^':
389
+ key.name = 'home';
390
+ key.ctrl = true;
391
+ break;
392
+ case '[8^':
393
+ key.name = 'end';
394
+ key.ctrl = true;
395
+ break;
396
+ /* misc. */
397
+ case '[Z':
398
+ key.name = 'tab';
399
+ key.shift = true;
400
+ break;
401
+ default:
402
+ key.name = 'undefined';
403
+ break;
404
+ }
405
+ }
406
+ else if (s.length > 1 && s[0] !== '\x1b') {
407
+ // Nhận được một chuỗi ký tự dài hơn một.
408
+ // Có thể là paste, vì nó không phải là control sequence.
409
+ for (const c of s) {
410
+ emitKey(stream, c);
411
+ }
412
+ return;
413
+ }
414
+ // XXX: code phân tích "mouse" đã bị XÓA theo yêu cầu.
415
+ // Không phát ra key nếu không tìm thấy tên
416
+ if (key.name === undefined) {
417
+ return; // key = undefined;
418
+ }
419
+ if (s.length === 1) {
420
+ ch = s;
421
+ }
422
+ if (key || ch) {
423
+ stream.emit('keypress', ch, key);
424
+ }
425
+ }
package/package.json CHANGED
@@ -1,44 +1,43 @@
1
- {
2
- "name": "cliedit",
3
- "version": "0.1.0",
4
- "description": "A lightweight, raw-mode terminal editor utility for Node.js CLI applications, with line wrapping and undo/redo support.",
5
- "repository": "https://github.com/CodeTease/cliedit",
6
- "type": "module",
7
- "main": "dist/index.js",
8
- "types": "dist/index.d.ts",
9
- "exports": {
10
- ".": {
11
- "import": "./dist/index.js",
12
- "types": "./dist/index.d.ts"
13
- }
14
- },
15
- "files": [
16
- "dist",
17
- "LICENSE",
18
- "README.md"
19
- ],
20
- "scripts": {
21
- "build": "tsc",
22
- "prepublishOnly": "npm run build",
23
- "demo": "npm run build && cross-env NODE_ENV=development node dist/demo.js",
24
- "test": "echo \"Error: no test specified\" && exit 1"
25
- },
26
- "keywords": [
27
- "cli",
28
- "editor",
29
- "terminal",
30
- "tui",
31
- "raw-mode",
32
- "typescript"
33
- ],
34
- "author": "CodeTease",
35
- "license": "MIT",
36
- "devDependencies": {
37
- "@types/node": "^20.12.12",
38
- "cross-env": "^7.0.3",
39
- "typescript": "^5.4.5"
40
- },
41
- "dependencies": {
42
- "keypress": "^0.2.1"
43
- }
1
+ {
2
+ "name": "cliedit",
3
+ "version": "0.1.1",
4
+ "description": "A lightweight, raw-mode terminal editor utility for Node.js CLI applications, with line wrapping and undo/redo support.",
5
+ "repository": "https://github.com/CodeTease/cliedit",
6
+ "type": "module",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "LICENSE",
18
+ "README.md"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsc",
22
+ "prepublishOnly": "npm run build",
23
+ "demo": "npm run build && cross-env NODE_ENV=development node dist/demo.js",
24
+ "test": "echo \"Error: no test specified\" && exit 1"
25
+ },
26
+ "keywords": [
27
+ "cli",
28
+ "editor",
29
+ "terminal",
30
+ "tui",
31
+ "raw-mode",
32
+ "typescript"
33
+ ],
34
+ "author": "CodeTease",
35
+ "license": "MIT",
36
+ "devDependencies": {
37
+ "@types/node": "^20.12.12",
38
+ "cross-env": "^7.0.3",
39
+ "typescript": "^5.4.5"
40
+ },
41
+ "dependencies": {
42
+ }
44
43
  }