cliedit 0.1.3 → 0.2.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 CHANGED
@@ -18,6 +18,9 @@ It includes line wrapping, visual navigation, smart auto-indentation, undo/redo,
18
18
  - **Search & Replace:** `Ctrl+W` to find text, `Ctrl+R` to find and replace interactively.
19
19
  - **Go to Line:** `Ctrl+L` to quickly jump to a specific line number.
20
20
  - **Smart Auto-Indentation:** Automatically preserves indentation level when pressing Enter.
21
+ - **Smart Navigation:** `Alt + Left/Right` to jump by words, `Ctrl + M` to jump between matching brackets.
22
+ - **Piping Support:** Works with standard Unix pipes (e.g. `cat file.txt | cliedit`).
23
+ - **Crash Recovery:** Automatically saves changes to a hidden swap file (e.g. `.filename.swp`) to prevent data loss.
21
24
 
22
25
  ## Installation
23
26
  ```bash
@@ -34,35 +37,61 @@ import path from 'path';
34
37
 
35
38
  async function getCommitMessage() {
36
39
  const tempFile = path.resolve(process.cwd(), 'COMMIT_MSG.txt');
37
- console.log('Opening editor for commit message...');
40
+
41
+ // Example with custom options
42
+ const options = {
43
+ tabSize: 2,
44
+ gutterWidth: 3
45
+ };
38
46
 
39
47
  try {
40
- const result = await openEditor(tempFile);
41
-
42
- // Give the terminal a moment to restore
43
- await new Promise(res => setTimeout(res, 50));
48
+ const result = await openEditor(tempFile, options);
44
49
 
45
50
  if (result.saved) {
46
- console.log('Message saved!');
47
- console.log('---------------------');
48
- console.log(result.content);
49
- console.log('---------------------');
51
+ console.log('Message saved:', result.content);
50
52
  } else {
51
53
  console.log('Editor quit without saving.');
52
54
  }
53
55
  } catch (err) {
54
- console.error('Editor failed to start:', err);
56
+ console.error('Editor failed:', err);
55
57
  }
56
58
  }
57
59
 
58
60
  getCommitMessage();
59
61
  ```
60
62
 
63
+ ### Piping Support
64
+
65
+ `cliedit` supports standard input piping. When used in a pipeline, it reads the input content, then re-opens the TTY to allow interactive editing.
66
+
67
+ ```bash
68
+ # Edit a file using cat
69
+ cat README.md | node my-app.js
70
+
71
+ # Edit the output of a command
72
+ git diff | node my-app.js
73
+ ```
74
+
61
75
  ## Public API
62
76
 
63
- `openEditor(filepath: string)`
77
+ `openEditor(filepath: string, options?: EditorOptions)`
78
+
79
+ Opens the editor for the specified file.
80
+
81
+ - **filepath**: Path to the file to edit.
82
+ - **options**: (Optional) Configuration object.
83
+ - `tabSize`: Number of spaces for a tab (default: 4).
84
+ - `gutterWidth`: Width of the line number gutter (default: 5).
85
+
86
+ - **Returns:** `Promise<{ saved: boolean; content: string }>`
87
+ * `saved`: `true` if the user saved (Ctrl+S), `false` otherwise (Ctrl+Q).
88
+ * `content`: The final content of the file as a string.
89
+
90
+ ### Crash Recovery
91
+
92
+ `cliedit` includes a built-in safety mechanism. It periodically saves the current content to a hidden swap file (e.g., `.myfile.txt.swp`) in the same directory.
64
93
 
65
- Opens the editor for the specified file. If the file doesn't exist, it will be created upon saving.
94
+ If the process crashes or is terminated abruptly, the next time you open the file, `cliedit` will detect the swap file and automatically recover the unsaved content, displaying a `RECOVERED FROM SWAP FILE` message.
66
95
 
67
96
  - **Returns:** `Promise<{ saved: boolean; content: string }>`
68
97
  * `saved`: `true` if the user saved (Ctrl+S), `false` otherwise (Ctrl+Q).
@@ -29,6 +29,7 @@ export declare const KEYS: {
29
29
  CTRL_U: string;
30
30
  CTRL_X: string;
31
31
  CTRL_V: string;
32
+ CTRL_M: string;
32
33
  CTRL_ARROW_UP: string;
33
34
  CTRL_ARROW_DOWN: string;
34
35
  CTRL_ARROW_RIGHT: string;
package/dist/constants.js CHANGED
@@ -31,6 +31,7 @@ export const KEYS = {
31
31
  CTRL_U: '\x15', // Paste/Un-kill
32
32
  CTRL_X: '\x18', // Cut Selection
33
33
  CTRL_V: '\x16', // Paste Selection
34
+ CTRL_M: '\x0d', // Match Bracket (Ctrl+M is often Enter, but we distinguish if possible or rely on context)
34
35
  // Selection Keys (Mapped to Ctrl+Arrow for reliable detection)
35
36
  CTRL_ARROW_UP: 'C-up',
36
37
  CTRL_ARROW_DOWN: 'C-down',
package/dist/editor.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { HistoryManager } from './history.js';
2
- import { VisualRow, EditorMode } from './types.js';
2
+ import { VisualRow, EditorMode, EditorOptions } from './types.js';
3
+ import { SwapManager } from './editor.swap.js';
3
4
  import { editingMethods } from './editor.editing.js';
4
5
  import { clipboardMethods } from './editor.clipboard.js';
5
6
  import { navigationMethods } from './editor.navigation.js';
@@ -35,6 +36,7 @@ export declare class CliEditor {
35
36
  screenRows: number;
36
37
  screenCols: number;
37
38
  gutterWidth: number;
39
+ tabSize: number;
38
40
  screenStartRow: number;
39
41
  visualRows: VisualRow[];
40
42
  mode: EditorMode;
@@ -52,14 +54,16 @@ export declare class CliEditor {
52
54
  }[];
53
55
  searchResultIndex: number;
54
56
  history: HistoryManager;
57
+ swapManager: SwapManager;
55
58
  isCleanedUp: boolean;
56
59
  resolvePromise: ((value: {
57
60
  saved: boolean;
58
61
  content: string;
59
62
  }) => void) | null;
60
63
  rejectPromise: ((reason?: any) => void) | null;
64
+ inputStream: any;
61
65
  isExiting: boolean;
62
- constructor(initialContent: string, filepath: string);
66
+ constructor(initialContent: string, filepath: string, options?: EditorOptions);
63
67
  run(): Promise<{
64
68
  saved: boolean;
65
69
  content: string;
@@ -13,7 +13,7 @@ declare function insertContentAtCursor(this: CliEditor, contentLines: string[]):
13
13
  */
14
14
  declare function insertCharacter(this: CliEditor, char: string): void;
15
15
  /**
16
- * Inserts a soft tab (4 spaces).
16
+ * Inserts a soft tab (using configured tabSize).
17
17
  */
18
18
  declare function insertSoftTab(this: CliEditor): void;
19
19
  /**
@@ -44,10 +44,11 @@ function insertCharacter(char) {
44
44
  this.cursorX += char.length;
45
45
  }
46
46
  /**
47
- * Inserts a soft tab (4 spaces).
47
+ * Inserts a soft tab (using configured tabSize).
48
48
  */
49
49
  function insertSoftTab() {
50
- this.insertCharacter(' ');
50
+ const spaces = ' '.repeat(this.tabSize || 4);
51
+ this.insertCharacter(spaces);
51
52
  }
52
53
  /**
53
54
  * Inserts a new line, splitting the current line at the cursor position.
package/dist/editor.io.js CHANGED
@@ -16,6 +16,7 @@ async function saveFile() {
16
16
  const content = this.lines.join('\n');
17
17
  try {
18
18
  await fs.writeFile(this.filepath, content, 'utf-8');
19
+ await this.swapManager.clear(); // Clear swap on successful save
19
20
  this.isDirty = false; // Reset dirty flag
20
21
  this.quitConfirm = false; // Reset quit confirmation
21
22
  this.setStatusMessage(`Saved: ${this.filepath}`, 2000);
package/dist/editor.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import keypress from './vendor/keypress.js';
3
3
  import { ANSI } from './constants.js';
4
4
  import { HistoryManager } from './history.js';
5
+ import { SwapManager } from './editor.swap.js';
5
6
  // Block `declare module 'keypress'` đã bị xóa
6
7
  // Import all functional modules
7
8
  import { editingMethods } from './editor.editing.js';
@@ -18,7 +19,7 @@ const DEFAULT_STATUS = 'HELP: Ctrl+S = Save | Ctrl+Q = Quit | Ctrl+W = Find | Ct
18
19
  * Main editor class managing application state, TTY interaction, and rendering.
19
20
  */
20
21
  export class CliEditor {
21
- constructor(initialContent, filepath) {
22
+ constructor(initialContent, filepath, options = {}) {
22
23
  this.isDirty = false;
23
24
  this.cursorX = 0;
24
25
  this.cursorY = 0;
@@ -27,6 +28,7 @@ export class CliEditor {
27
28
  this.screenRows = 0;
28
29
  this.screenCols = 0;
29
30
  this.gutterWidth = 5;
31
+ this.tabSize = 4;
30
32
  this.screenStartRow = 1;
31
33
  this.visualRows = [];
32
34
  this.mode = 'edit';
@@ -50,29 +52,38 @@ export class CliEditor {
50
52
  this.lines = [''];
51
53
  }
52
54
  this.filepath = filepath;
55
+ this.gutterWidth = options.gutterWidth ?? 5;
56
+ this.tabSize = options.tabSize ?? 4;
57
+ this.inputStream = options.inputStream || process.stdin;
53
58
  this.history = new HistoryManager();
54
59
  this.saveState(true);
60
+ // Initialize SwapManager
61
+ this.swapManager = new SwapManager(this.filepath, () => this.lines.join('\n'));
55
62
  }
56
63
  // --- Lifecycle Methods ---
57
64
  run() {
58
65
  this.setupTerminal();
59
66
  this.render();
67
+ this.swapManager.start();
60
68
  return new Promise((resolve, reject) => {
61
69
  const performCleanup = (callback) => {
70
+ this.swapManager.stop(); // Stop swap interval
62
71
  if (this.isCleanedUp) {
63
72
  if (callback)
64
73
  callback();
65
74
  return;
66
75
  }
67
76
  // 1. Remove listeners immediately
68
- process.stdin.removeAllListeners('keypress');
77
+ this.inputStream.removeAllListeners('keypress');
69
78
  process.stdout.removeAllListeners('resize');
70
79
  // 2. (FIX GHOST TUI) Write exit sequence and use callback to ensure it's written
71
80
  // before Node.js fully releases the TTY.
72
81
  process.stdout.write(ANSI.CLEAR_SCREEN + ANSI.MOVE_CURSOR_TOP_LEFT + ANSI.SHOW_CURSOR + ANSI.EXIT_ALTERNATE_SCREEN, () => {
73
82
  // 3. Disable TTY raw mode and pause stdin after screen is cleared
74
- process.stdin.setRawMode(false);
75
- process.stdin.pause();
83
+ if (this.inputStream.setRawMode) {
84
+ this.inputStream.setRawMode(false);
85
+ }
86
+ this.inputStream.pause();
76
87
  this.isCleanedUp = true;
77
88
  if (callback)
78
89
  callback();
@@ -87,19 +98,27 @@ export class CliEditor {
87
98
  });
88
99
  }
89
100
  setupTerminal() {
90
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
91
- throw new Error('Editor requires a TTY environment.');
101
+ // If we are using a custom inputStream (re-opened TTY), it might be a ReadStream which is TTY.
102
+ // Check if it is TTY
103
+ if (!this.inputStream.isTTY && !process.stdin.isTTY) {
104
+ // If both are not TTY, we have a problem.
105
+ // But if inputStream is our manually opened TTY, isTTY should be true.
106
+ }
107
+ if (!process.stdout.isTTY) {
108
+ throw new Error('Editor requires a TTY environment (stdout).');
92
109
  }
93
110
  this.updateScreenSize();
94
111
  this.recalculateVisualRows();
95
112
  // Enter alternate screen and hide cursor
96
113
  process.stdout.write(ANSI.ENTER_ALTERNATE_SCREEN + ANSI.HIDE_CURSOR + ANSI.CLEAR_SCREEN);
97
- process.stdin.setRawMode(true);
98
- process.stdin.resume();
99
- process.stdin.setEncoding('utf-8');
114
+ if (this.inputStream.setRawMode) {
115
+ this.inputStream.setRawMode(true);
116
+ }
117
+ this.inputStream.resume();
118
+ this.inputStream.setEncoding('utf-8');
100
119
  // Setup keypress listener
101
- keypress(process.stdin);
102
- process.stdin.on('keypress', this.handleKeypressEvent.bind(this));
120
+ keypress(this.inputStream);
121
+ this.inputStream.on('keypress', this.handleKeypressEvent.bind(this));
103
122
  process.stdout.on('resize', this.handleResize.bind(this));
104
123
  }
105
124
  handleResize() {
@@ -10,5 +10,6 @@ export type TKeyHandlingMethods = {
10
10
  handleCharacterKey: (ch: string) => void;
11
11
  cutSelection: () => Promise<void>;
12
12
  handleSave: () => Promise<void>;
13
+ handleAltArrows: (keyName: string) => void;
13
14
  };
14
15
  export declare const keyHandlingMethods: TKeyHandlingMethods;
@@ -81,6 +81,10 @@ function handleKeypressEvent(ch, key) {
81
81
  keyName = KEYS.ENTER;
82
82
  else if (key.name === 'tab')
83
83
  keyName = KEYS.TAB;
84
+ else if (key.meta && key.name === 'left')
85
+ keyName = 'ALT_LEFT';
86
+ else if (key.meta && key.name === 'right')
87
+ keyName = 'ALT_RIGHT';
84
88
  else
85
89
  keyName = key.sequence;
86
90
  }
@@ -125,6 +129,12 @@ function handleKeypressEvent(ch, key) {
125
129
  this.render(); // Render cuối cùng (với visual rows đã được cập nhật nếu cần)
126
130
  }
127
131
  }
132
+ function handleAltArrows(keyName) {
133
+ if (keyName === 'ALT_LEFT')
134
+ this.moveCursorByWord('left');
135
+ else if (keyName === 'ALT_RIGHT')
136
+ this.moveCursorByWord('right');
137
+ }
128
138
  /**
129
139
  * Handles all command keys in 'edit' mode.
130
140
  * Returns true if content was modified.
@@ -223,6 +233,20 @@ function handleEditKeys(key) {
223
233
  case KEYS.CTRL_G:
224
234
  this.findNext();
225
235
  return false;
236
+ // --- Smart Navigation ---
237
+ case 'ALT_LEFT':
238
+ this.moveCursorByWord('left');
239
+ return false;
240
+ case 'ALT_RIGHT':
241
+ this.moveCursorByWord('right');
242
+ return false;
243
+ case KEYS.CTRL_M: // Or any key for Bracket Match. Ctrl+M is technically Enter in some terms but if available...
244
+ // Let's use Ctrl+B (Bracket) if not taken? Ctrl+B is often bold, but here it's CLI.
245
+ // Or just check if key is match bracket key.
246
+ // Let's try to map a specific key or use Meta.
247
+ // For now, let's use Ctrl+B?
248
+ this.matchBracket();
249
+ return false;
226
250
  // ***** SỬA LỖI VISUAL *****
227
251
  // Sau khi undo/redo, chúng ta PHẢI tính toán lại visual rows
228
252
  case KEYS.CTRL_Z:
@@ -465,4 +489,5 @@ export const keyHandlingMethods = {
465
489
  handleCharacterKey,
466
490
  cutSelection,
467
491
  handleSave,
492
+ handleAltArrows,
468
493
  };
@@ -48,5 +48,9 @@ export declare const navigationMethods: {
48
48
  scroll: typeof scroll;
49
49
  jumpToLine: typeof jumpToLine;
50
50
  enterGoToLineMode: typeof enterGoToLineMode;
51
+ moveCursorByWord: typeof moveCursorByWord;
52
+ matchBracket: typeof matchBracket;
51
53
  };
54
+ declare function moveCursorByWord(this: CliEditor, direction: 'left' | 'right'): void;
55
+ declare function matchBracket(this: CliEditor): void;
52
56
  export {};
@@ -153,4 +153,95 @@ export const navigationMethods = {
153
153
  scroll,
154
154
  jumpToLine,
155
155
  enterGoToLineMode,
156
+ moveCursorByWord,
157
+ matchBracket,
156
158
  };
159
+ function moveCursorByWord(direction) {
160
+ const line = this.lines[this.cursorY];
161
+ if (direction === 'left') {
162
+ if (this.cursorX === 0) {
163
+ if (this.cursorY > 0) {
164
+ this.cursorY--;
165
+ this.cursorX = this.lines[this.cursorY].length;
166
+ }
167
+ }
168
+ else {
169
+ // Move left until we hit a non-word char, then until we hit a word char
170
+ // Simple logic: skip whitespace, then skip word chars
171
+ let i = this.cursorX - 1;
172
+ // 1. Skip spaces if we are currently on a space
173
+ while (i > 0 && line[i] === ' ')
174
+ i--;
175
+ // 2. Skip non-spaces
176
+ while (i > 0 && line[i - 1] !== ' ')
177
+ i--;
178
+ this.cursorX = i;
179
+ }
180
+ }
181
+ else {
182
+ if (this.cursorX >= line.length) {
183
+ if (this.cursorY < this.lines.length - 1) {
184
+ this.cursorY++;
185
+ this.cursorX = 0;
186
+ }
187
+ }
188
+ else {
189
+ let i = this.cursorX;
190
+ // 1. Skip current word chars
191
+ while (i < line.length && line[i] !== ' ')
192
+ i++;
193
+ // 2. Skip spaces
194
+ while (i < line.length && line[i] === ' ')
195
+ i++;
196
+ this.cursorX = i;
197
+ }
198
+ }
199
+ }
200
+ function matchBracket() {
201
+ const line = this.lines[this.cursorY];
202
+ const char = line[this.cursorX];
203
+ const pairs = { '(': ')', '[': ']', '{': '}' };
204
+ const revPairs = { ')': '(', ']': '[', '}': '{' };
205
+ if (pairs[char]) {
206
+ // Find closing
207
+ let depth = 1;
208
+ // Search forward
209
+ for (let y = this.cursorY; y < this.lines.length; y++) {
210
+ const l = this.lines[y];
211
+ const startX = (y === this.cursorY) ? this.cursorX + 1 : 0;
212
+ for (let x = startX; x < l.length; x++) {
213
+ if (l[x] === char)
214
+ depth++;
215
+ else if (l[x] === pairs[char])
216
+ depth--;
217
+ if (depth === 0) {
218
+ this.cursorY = y;
219
+ this.cursorX = x;
220
+ this.scroll();
221
+ return;
222
+ }
223
+ }
224
+ }
225
+ }
226
+ else if (revPairs[char]) {
227
+ // Find opening
228
+ let depth = 1;
229
+ // Search backward
230
+ for (let y = this.cursorY; y >= 0; y--) {
231
+ const l = this.lines[y];
232
+ const startX = (y === this.cursorY) ? this.cursorX - 1 : l.length - 1;
233
+ for (let x = startX; x >= 0; x--) {
234
+ if (l[x] === char)
235
+ depth++;
236
+ else if (l[x] === revPairs[char])
237
+ depth--;
238
+ if (depth === 0) {
239
+ this.cursorY = y;
240
+ this.cursorX = x;
241
+ this.scroll();
242
+ return;
243
+ }
244
+ }
245
+ }
246
+ }
247
+ }
@@ -0,0 +1,17 @@
1
+ export declare class SwapManager {
2
+ private filepath;
3
+ private swapPath;
4
+ private intervalId;
5
+ private contentGetter;
6
+ private lastSavedContent;
7
+ private intervalMs;
8
+ constructor(filepath: string, contentGetter: () => string);
9
+ start(): void;
10
+ stop(): void;
11
+ update(): Promise<void>;
12
+ private saveSwap;
13
+ clear(): Promise<void>;
14
+ static getSwapPath(filepath: string): string;
15
+ static check(filepath: string): Promise<boolean>;
16
+ static read(filepath: string): Promise<string>;
17
+ }
@@ -0,0 +1,83 @@
1
+ // src/editor.swap.ts
2
+ import { promises as fs } from 'fs';
3
+ import * as path from 'path';
4
+ export class SwapManager {
5
+ constructor(filepath, contentGetter) {
6
+ this.intervalId = null;
7
+ this.lastSavedContent = '';
8
+ this.intervalMs = 2000; // 2 seconds
9
+ this.filepath = filepath;
10
+ // If filepath is empty (e.g. untitled/piped), we can't really make a relative swap file easily.
11
+ // We will default to a temp file or current dir if filepath is empty.
12
+ // For now, let's assume if filepath is provided, we use it.
13
+ if (!filepath) {
14
+ this.swapPath = path.resolve('.untitled.swp');
15
+ }
16
+ else {
17
+ const dir = path.dirname(filepath);
18
+ const file = path.basename(filepath);
19
+ this.swapPath = path.join(dir, '.' + file + '.swp');
20
+ }
21
+ this.contentGetter = contentGetter;
22
+ }
23
+ start() {
24
+ if (this.intervalId)
25
+ return;
26
+ this.intervalId = setInterval(() => this.saveSwap(), this.intervalMs);
27
+ }
28
+ stop() {
29
+ if (this.intervalId) {
30
+ clearInterval(this.intervalId);
31
+ this.intervalId = null;
32
+ }
33
+ }
34
+ // Explicitly update swap (can be called on keypress if we want instant-ish updates)
35
+ async update() {
36
+ await this.saveSwap();
37
+ }
38
+ async saveSwap() {
39
+ const currentContent = this.contentGetter();
40
+ // Optimization: Don't write if nothing changed since last swap save
41
+ if (currentContent === this.lastSavedContent)
42
+ return;
43
+ try {
44
+ await fs.writeFile(this.swapPath, currentContent, 'utf-8');
45
+ this.lastSavedContent = currentContent;
46
+ }
47
+ catch (error) {
48
+ // Silently ignore swap errors to not disrupt user flow
49
+ }
50
+ }
51
+ async clear() {
52
+ this.stop();
53
+ try {
54
+ await fs.unlink(this.swapPath);
55
+ }
56
+ catch (err) {
57
+ if (err.code !== 'ENOENT') {
58
+ // ignore
59
+ }
60
+ }
61
+ }
62
+ static getSwapPath(filepath) {
63
+ if (!filepath)
64
+ return path.resolve('.untitled.swp');
65
+ const dir = path.dirname(filepath);
66
+ const file = path.basename(filepath);
67
+ return path.join(dir, '.' + file + '.swp');
68
+ }
69
+ static async check(filepath) {
70
+ const swapPath = SwapManager.getSwapPath(filepath);
71
+ try {
72
+ await fs.access(swapPath);
73
+ return true;
74
+ }
75
+ catch {
76
+ return false;
77
+ }
78
+ }
79
+ static async read(filepath) {
80
+ const swapPath = SwapManager.getSwapPath(filepath);
81
+ return fs.readFile(swapPath, 'utf-8');
82
+ }
83
+ }
package/dist/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
+ import { EditorOptions } from './types.js';
1
2
  /**
2
3
  * Public API function: Opens the editor.
3
4
  * Reads the file and initializes CliEditor.
4
5
  */
5
- export declare function openEditor(filepath: string): Promise<{
6
+ export declare function openEditor(filepath: string, options?: EditorOptions): Promise<{
6
7
  saved: boolean;
7
8
  content: string;
8
9
  }>;
package/dist/index.js CHANGED
@@ -1,24 +1,62 @@
1
1
  // src/index.ts
2
2
  import { promises as fs } from 'fs';
3
3
  import { CliEditor } from './editor.js';
4
+ import { SwapManager } from './editor.swap.js';
4
5
  /**
5
6
  * Public API function: Opens the editor.
6
7
  * Reads the file and initializes CliEditor.
7
8
  */
8
- export async function openEditor(filepath) {
9
- let initialContent = '';
10
- try {
11
- // 1. Read file
12
- initialContent = await fs.readFile(filepath, 'utf-8');
9
+ export async function openEditor(filepath, options) {
10
+ // 0. Handle Piping (Stdin)
11
+ let pipedContent = '';
12
+ if (!process.stdin.isTTY) {
13
+ try {
14
+ const chunks = [];
15
+ for await (const chunk of process.stdin) {
16
+ chunks.push(chunk);
17
+ }
18
+ pipedContent = Buffer.concat(chunks).toString('utf-8');
19
+ // CRITICAL: Re-open TTY for user input!
20
+ // We need to bypass the consumed stdin and open the actual terminal device.
21
+ const ttyPath = process.platform === 'win32' ? 'CONIN$' : '/dev/tty';
22
+ const ttyFd = await fs.open(ttyPath, 'r');
23
+ // Let's rely on the fact that we can construct a new ReadStream.
24
+ const { ReadStream } = await import('tty');
25
+ const ttyReadStream = new ReadStream(ttyFd.fd);
26
+ if (options) {
27
+ options.inputStream = ttyReadStream;
28
+ }
29
+ else {
30
+ options = { inputStream: ttyReadStream };
31
+ }
32
+ }
33
+ catch (e) {
34
+ console.error('Failed to read from stdin or open TTY:', e);
35
+ }
13
36
  }
14
- catch (err) {
15
- // 2. If file does not exist (ENOENT), treat it as a new file
16
- if (err.code !== 'ENOENT') {
17
- throw err; // Throw error if not 'File not found'
37
+ // Check for swap file (only if filepath provided)
38
+ if (filepath && await SwapManager.check(filepath)) {
39
+ console.log(`\x1b[33mWarning: Swap file detected for ${filepath}. Recovering content...\x1b[0m`);
40
+ await new Promise(r => setTimeout(r, 1500));
41
+ const swapContent = await SwapManager.read(filepath);
42
+ const editor = new CliEditor(swapContent, filepath, options);
43
+ editor.isDirty = true; // Mark as dirty manually to avoid potential mixin issues
44
+ editor.statusMessage = 'RECOVERED FROM SWAP FILE';
45
+ return editor.run();
46
+ }
47
+ let initialContent = pipedContent; // Default to piped content
48
+ if (filepath && !initialContent) {
49
+ try {
50
+ initialContent = await fs.readFile(filepath, 'utf-8');
51
+ }
52
+ catch (err) {
53
+ if (err.code !== 'ENOENT') {
54
+ throw err;
55
+ }
18
56
  }
19
57
  }
20
58
  // 3. Initialize and run editor
21
- const editor = new CliEditor(initialContent, filepath);
59
+ const editor = new CliEditor(initialContent, filepath, options);
22
60
  return editor.run();
23
61
  }
24
62
  // --- Public Exports ---
package/dist/types.d.ts CHANGED
@@ -16,3 +16,8 @@ export interface VisualRow {
16
16
  content: string;
17
17
  }
18
18
  export type EditorMode = 'edit' | 'search_find' | 'search_replace' | 'search_confirm' | 'goto_line';
19
+ export interface EditorOptions {
20
+ tabSize?: number;
21
+ gutterWidth?: number;
22
+ inputStream?: any;
23
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cliedit",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "A lightweight, raw-mode terminal editor utility for Node.js CLI applications, with line wrapping and undo/redo support.",
5
5
  "repository": "https://github.com/CodeTease/cliedit",
6
6
  "type": "module",