@reliverse/rempts-core 1.6.1 → 2.3.2

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.
Files changed (155) hide show
  1. package/README.md +398 -102
  2. package/dist/cli.d.ts +32 -0
  3. package/dist/cli.js +731 -0
  4. package/dist/config-loader.d.ts +42 -0
  5. package/dist/config-loader.js +20 -0
  6. package/dist/config.d.ts +99 -0
  7. package/dist/config.js +188 -0
  8. package/dist/file-loader.d.ts +43 -0
  9. package/dist/file-loader.js +199 -0
  10. package/dist/global-flags.d.ts +36 -0
  11. package/dist/global-flags.js +36 -0
  12. package/dist/mod.d.ts +13 -0
  13. package/dist/mod.js +19 -0
  14. package/dist/parser.d.ts +6 -0
  15. package/dist/parser.js +137 -0
  16. package/dist/plugin/context.d.ts +13 -0
  17. package/dist/plugin/context.js +53 -0
  18. package/dist/plugin/create.d.ts +92 -0
  19. package/dist/plugin/create.js +61 -0
  20. package/dist/plugin/loader.d.ts +12 -0
  21. package/dist/plugin/loader.js +65 -0
  22. package/dist/plugin/manager.d.ts +53 -0
  23. package/dist/plugin/manager.js +135 -0
  24. package/dist/plugin/mod.d.ts +10 -0
  25. package/dist/plugin/mod.js +27 -0
  26. package/dist/plugin/store.d.ts +45 -0
  27. package/dist/plugin/store.js +60 -0
  28. package/dist/plugin/testing.d.ts +38 -0
  29. package/dist/plugin/testing.js +175 -0
  30. package/dist/plugin/types.d.ts +146 -0
  31. package/dist/tui/registry.d.ts +8 -0
  32. package/dist/tui/registry.js +10 -0
  33. package/dist/tui/types.d.ts +58 -0
  34. package/dist/tui/types.js +10 -0
  35. package/dist/types.d.ts +178 -0
  36. package/dist/types.js +25 -0
  37. package/dist/utils/logger.d.ts +10 -0
  38. package/dist/utils/logger.js +27 -0
  39. package/dist/utils/merge.d.ts +13 -0
  40. package/dist/utils/merge.js +25 -0
  41. package/dist/utils/mod.d.ts +6 -0
  42. package/dist/utils/mod.js +2 -0
  43. package/dist/utils/type-helpers.d.ts +41 -0
  44. package/dist/utils/type-helpers.js +0 -0
  45. package/dist/validation.d.ts +30 -0
  46. package/dist/validation.js +121 -0
  47. package/package.json +47 -44
  48. package/src/cli.ts +1049 -0
  49. package/src/config-loader.ts +71 -0
  50. package/src/config.ts +270 -0
  51. package/src/file-loader.ts +346 -0
  52. package/src/global-flags.ts +50 -0
  53. package/src/mod.ts +74 -0
  54. package/src/parser.ts +212 -0
  55. package/src/plugin/context.ts +88 -0
  56. package/src/plugin/create.ts +174 -0
  57. package/src/plugin/loader.ts +111 -0
  58. package/src/plugin/manager.ts +244 -0
  59. package/src/plugin/mod.ts +51 -0
  60. package/src/plugin/store.ts +124 -0
  61. package/src/plugin/testing.ts +236 -0
  62. package/src/plugin/types.ts +206 -0
  63. package/src/tui/registry.ts +22 -0
  64. package/src/tui/types.ts +79 -0
  65. package/src/types.ts +285 -0
  66. package/src/utils/logger.ts +43 -0
  67. package/src/utils/merge.ts +54 -0
  68. package/src/utils/mod.ts +7 -0
  69. package/src/utils/type-helpers.ts +151 -0
  70. package/src/validation.ts +177 -0
  71. package/LICENSE +0 -21
  72. package/bin/core-impl/anykey/anykey-mod.d.ts +0 -12
  73. package/bin/core-impl/anykey/anykey-mod.js +0 -125
  74. package/bin/core-impl/date/date.d.ts +0 -2
  75. package/bin/core-impl/date/date.js +0 -236
  76. package/bin/core-impl/editor/editor-mod.d.ts +0 -25
  77. package/bin/core-impl/editor/editor-mod.js +0 -896
  78. package/bin/core-impl/figures/figures-mod.d.ts +0 -233
  79. package/bin/core-impl/figures/figures-mod.js +0 -286
  80. package/bin/core-impl/figures/figures.test.d.ts +0 -1
  81. package/bin/core-impl/figures/figures.test.js +0 -474
  82. package/bin/core-impl/input/confirm-prompt.d.ts +0 -5
  83. package/bin/core-impl/input/confirm-prompt.js +0 -173
  84. package/bin/core-impl/input/input-prompt.d.ts +0 -16
  85. package/bin/core-impl/input/input-prompt.js +0 -370
  86. package/bin/core-impl/launcher/_parser.d.ts +0 -2
  87. package/bin/core-impl/launcher/_parser.js +0 -122
  88. package/bin/core-impl/launcher/_utils.d.ts +0 -8
  89. package/bin/core-impl/launcher/_utils.js +0 -29
  90. package/bin/core-impl/launcher/args.d.ts +0 -3
  91. package/bin/core-impl/launcher/args.js +0 -89
  92. package/bin/core-impl/launcher/command.d.ts +0 -8
  93. package/bin/core-impl/launcher/command.js +0 -68
  94. package/bin/core-impl/launcher/launcher-mod.d.ts +0 -8
  95. package/bin/core-impl/launcher/launcher-mod.js +0 -34
  96. package/bin/core-impl/launcher/usage.d.ts +0 -3
  97. package/bin/core-impl/launcher/usage.js +0 -104
  98. package/bin/core-impl/msg-fmt/colors.d.ts +0 -30
  99. package/bin/core-impl/msg-fmt/colors.js +0 -42
  100. package/bin/core-impl/msg-fmt/logger.d.ts +0 -17
  101. package/bin/core-impl/msg-fmt/logger.js +0 -106
  102. package/bin/core-impl/msg-fmt/mapping.d.ts +0 -3
  103. package/bin/core-impl/msg-fmt/mapping.js +0 -49
  104. package/bin/core-impl/msg-fmt/messages.d.ts +0 -35
  105. package/bin/core-impl/msg-fmt/messages.js +0 -314
  106. package/bin/core-impl/msg-fmt/terminal.d.ts +0 -15
  107. package/bin/core-impl/msg-fmt/terminal.js +0 -59
  108. package/bin/core-impl/msg-fmt/variants.d.ts +0 -11
  109. package/bin/core-impl/msg-fmt/variants.js +0 -52
  110. package/bin/core-impl/next-steps/next-steps.d.ts +0 -14
  111. package/bin/core-impl/next-steps/next-steps.js +0 -24
  112. package/bin/core-impl/number/number-mod.d.ts +0 -28
  113. package/bin/core-impl/number/number-mod.js +0 -197
  114. package/bin/core-impl/results/results.d.ts +0 -7
  115. package/bin/core-impl/results/results.js +0 -27
  116. package/bin/core-impl/select/multiselect-prompt.d.ts +0 -2
  117. package/bin/core-impl/select/multiselect-prompt.js +0 -341
  118. package/bin/core-impl/select/nummultiselect-prompt.d.ts +0 -6
  119. package/bin/core-impl/select/nummultiselect-prompt.js +0 -105
  120. package/bin/core-impl/select/numselect-prompt.d.ts +0 -7
  121. package/bin/core-impl/select/numselect-prompt.js +0 -115
  122. package/bin/core-impl/select/select-prompt.d.ts +0 -33
  123. package/bin/core-impl/select/select-prompt.js +0 -302
  124. package/bin/core-impl/select/toggle-prompt.d.ts +0 -5
  125. package/bin/core-impl/select/toggle-prompt.js +0 -208
  126. package/bin/core-impl/st-end/end.d.ts +0 -2
  127. package/bin/core-impl/st-end/end.js +0 -42
  128. package/bin/core-impl/st-end/start.d.ts +0 -17
  129. package/bin/core-impl/st-end/start.js +0 -66
  130. package/bin/core-impl/task/progress.d.ts +0 -2
  131. package/bin/core-impl/task/progress.js +0 -57
  132. package/bin/core-impl/task/spinner.d.ts +0 -15
  133. package/bin/core-impl/task/spinner.js +0 -110
  134. package/bin/core-impl/utils/colorize.d.ts +0 -2
  135. package/bin/core-impl/utils/colorize.js +0 -134
  136. package/bin/core-impl/utils/errors.d.ts +0 -1
  137. package/bin/core-impl/utils/errors.js +0 -15
  138. package/bin/core-impl/utils/prevent.d.ts +0 -10
  139. package/bin/core-impl/utils/prevent.js +0 -69
  140. package/bin/core-impl/utils/prompt-end.d.ts +0 -8
  141. package/bin/core-impl/utils/prompt-end.js +0 -33
  142. package/bin/core-impl/utils/stream-text.d.ts +0 -18
  143. package/bin/core-impl/utils/stream-text.js +0 -136
  144. package/bin/core-impl/utils/system.d.ts +0 -6
  145. package/bin/core-impl/utils/system.js +0 -7
  146. package/bin/core-impl/utils/validate.d.ts +0 -22
  147. package/bin/core-impl/utils/validate.js +0 -17
  148. package/bin/core-impl/visual/animate/animate.d.ts +0 -14
  149. package/bin/core-impl/visual/animate/animate.js +0 -64
  150. package/bin/core-impl/visual/ascii-art/ascii-art.d.ts +0 -6
  151. package/bin/core-impl/visual/ascii-art/ascii-art.js +0 -12
  152. package/bin/core-types.d.ts +0 -434
  153. package/bin/main.d.ts +0 -41
  154. package/bin/main.js +0 -96
  155. /package/{bin/core-types.js → dist/plugin/types.js} +0 -0
@@ -1,896 +0,0 @@
1
- import { re } from "@reliverse/relico";
2
- import { loadConfig } from "c12";
3
- import fs from "fs-extra";
4
- import path from "pathe";
5
- import termkit from "terminal-kit";
6
- const { terminal: term } = termkit;
7
- let state = {
8
- lines: [""],
9
- // Document content as an array of strings
10
- cursorX: 0,
11
- // Horizontal cursor position (0-based index within the line string)
12
- cursorY: 0,
13
- // Vertical cursor position (0-based line index)
14
- topLine: 0,
15
- // Index of the top visible line in the viewport
16
- leftCol: 0,
17
- // Index of the leftmost visible column (for horizontal scroll - basic impl)
18
- filename: null,
19
- // Current file path
20
- originalContent: "",
21
- // Content when the file was opened/saved last
22
- modified: false,
23
- // Has the file been modified?
24
- statusMessage: "",
25
- // Message to display in the status bar
26
- lastSearchTerm: "",
27
- editorConfig: {},
28
- // Loaded configuration
29
- hooks: {},
30
- // Callbacks { onSave, onExit }
31
- options: {
32
- // Default options before resolution
33
- allowSaveAs: true,
34
- allowOpen: true,
35
- autoCloseOnSave: false,
36
- returnContentOnSave: false
37
- },
38
- clipboard: [],
39
- // Simple line-based clipboard
40
- isRunning: true,
41
- // Flag to control the main loop
42
- theme: {
43
- // Default Light Theme
44
- text: (str) => str,
45
- statusBarBg: (str) => re.bgGray(str),
46
- statusBarText: (str) => re.white(str),
47
- highlight: (str) => re.invert(str),
48
- // For search results, etc.
49
- lineNumber: (str) => re.gray(str)
50
- },
51
- syntaxHighlightToggle: false,
52
- // Toggled state for syntax highlighting
53
- exitResolver: null,
54
- exitRejecter: null
55
- };
56
- async function loadEditorConfig(cwd = process.cwd(), overrides = {}) {
57
- const { config } = await loadConfig({
58
- name: "minedit",
59
- cwd,
60
- defaults: {
61
- // Low priority defaults
62
- syntaxHighlighting: false,
63
- theme: "auto",
64
- defaultAllowSaveAs: true,
65
- defaultAllowOpen: true,
66
- defaultAutoCloseOnSave: false,
67
- defaultReturnContentOnSave: false
68
- },
69
- // user provided overrides during programmatic call have higher priority
70
- overrides
71
- });
72
- return config || {};
73
- }
74
- function setupTheme(configTheme) {
75
- let mode = configTheme;
76
- if (mode === "auto") {
77
- const termBg = process.env.COLORFGBG;
78
- mode = termBg && termBg.split(";")[1] === "0" ? "dark" : "light";
79
- if (!termBg) mode = "light";
80
- }
81
- if (mode === "dark") {
82
- state.theme = {
83
- text: (str) => re.white(str),
84
- statusBarBg: (str) => re.bgWhite(str),
85
- statusBarText: (str) => re.black(str),
86
- highlight: (str) => re.bgYellow(re.black(str)),
87
- lineNumber: (str) => re.blue(str)
88
- };
89
- term.bgColor("black").color("white");
90
- } else {
91
- state.theme = {
92
- text: (str) => re.black(str),
93
- statusBarBg: (str) => re.bgGray(str),
94
- statusBarText: (str) => re.white(str),
95
- highlight: (str) => re.bgCyan(re.black(str)),
96
- lineNumber: (str) => re.gray(str)
97
- };
98
- term.bgColor("white").color("black");
99
- }
100
- term.styleReset();
101
- }
102
- function clamp(value, min, max) {
103
- return Math.max(min, Math.min(value, max));
104
- }
105
- function getCurrentLine() {
106
- return state.lines[state.cursorY] || "";
107
- }
108
- function updateModifiedStatus() {
109
- const currentContent = state.lines.join("\n");
110
- state.modified = currentContent !== state.originalContent;
111
- }
112
- function renderStatusBar() {
113
- const { width } = term;
114
- const filename = state.filename ? path.basename(state.filename) : "[No Name]";
115
- const modifiedIndicator = state.modified ? "*" : "";
116
- const position = `L: ${state.cursorY + 1} C: ${state.cursorX + 1}`;
117
- const fileInfo = `${filename}${modifiedIndicator} - ${state.lines.length} lines`;
118
- const leftPart = ` ${fileInfo} `;
119
- const rightPart = ` ${position} `;
120
- let hints = " Ctrl+S:Save | Ctrl+A:SaveAs | Ctrl+O:Open | Ctrl+X:Save&Exit | Ctrl+C:Exit | Ctrl+F:Find ";
121
- if (!state.options.allowSaveAs) {
122
- hints = hints.replace("Ctrl+A:SaveAs | ", "");
123
- }
124
- if (!state.options.allowOpen) {
125
- hints = hints.replace("Ctrl+O:Open | ", "");
126
- }
127
- const remainingWidth = width - leftPart.length - rightPart.length;
128
- const hintPadding = Math.max(0, remainingWidth - hints.length);
129
- const middlePart = hints + " ".repeat(hintPadding);
130
- const statusBar = leftPart + middlePart + rightPart;
131
- term.moveTo(1, term.height - 1);
132
- term(
133
- state.theme.statusBarBg(state.theme.statusBarText(statusBar.padEnd(width)))
134
- );
135
- }
136
- function renderMessageBar() {
137
- term.moveTo(1, term.height);
138
- term.eraseLine();
139
- if (state.statusMessage) {
140
- term(state.theme.highlight(state.statusMessage.slice(0, term.width)));
141
- setTimeout(() => {
142
- if (state.isRunning) {
143
- state.statusMessage = "";
144
- render();
145
- }
146
- }, 3e3);
147
- }
148
- }
149
- function applySyntaxHighlighting(line) {
150
- if (!state.editorConfig.syntaxHighlighting || !state.syntaxHighlightToggle) {
151
- return state.theme.text(line);
152
- }
153
- let highlightedLine = line;
154
- highlightedLine = highlightedLine.replace(
155
- /(\/\/.*)/g,
156
- (match) => re.green(match)
157
- );
158
- highlightedLine = highlightedLine.replace(
159
- /(['"`].*?['"`])/g,
160
- (match) => re.magenta(match)
161
- );
162
- highlightedLine = highlightedLine.replace(
163
- /\b(const|let|var|function|return|if|else|for|while|import|export|from|default|async|await|new|this)\b/g,
164
- (match) => re.blue(match)
165
- );
166
- highlightedLine = highlightedLine.replace(
167
- /(\d+)/g,
168
- (match) => re.cyan(match)
169
- );
170
- return state.theme.text(highlightedLine);
171
- }
172
- function renderEditor() {
173
- const { height, width } = term;
174
- const editorHeight = height - 2;
175
- if (state.cursorY < state.topLine) {
176
- state.topLine = state.cursorY;
177
- } else if (state.cursorY >= state.topLine + editorHeight) {
178
- state.topLine = state.cursorY - editorHeight + 1;
179
- }
180
- const displayWidth = width - 4;
181
- if (state.cursorX < state.leftCol) {
182
- state.leftCol = state.cursorX;
183
- } else if (state.cursorX >= state.leftCol + displayWidth) {
184
- state.leftCol = state.cursorX - displayWidth + 1;
185
- }
186
- state.leftCol = Math.max(0, state.leftCol);
187
- for (let y = 0; y < editorHeight; y++) {
188
- const fileLineIndex = state.topLine + y;
189
- term.moveTo(1, y + 1);
190
- if (fileLineIndex < state.lines.length) {
191
- const lineNum = String(fileLineIndex + 1).padStart(3);
192
- term(state.theme.lineNumber(`${lineNum} `));
193
- const line = state.lines[fileLineIndex];
194
- const displayLine = line.substring(
195
- state.leftCol,
196
- state.leftCol + displayWidth
197
- );
198
- const highlightedDisplayLine = applySyntaxHighlighting(displayLine);
199
- term(highlightedDisplayLine);
200
- term.eraseLineAfter();
201
- } else {
202
- term.eraseLine();
203
- }
204
- }
205
- }
206
- function render() {
207
- if (!state.isRunning) return;
208
- term.hideCursor();
209
- term.clear();
210
- renderEditor();
211
- renderStatusBar();
212
- renderMessageBar();
213
- const screenX = state.cursorX - state.leftCol + 4 + 1;
214
- const screenY = state.cursorY - state.topLine + 1;
215
- term.moveTo(
216
- clamp(screenX, 5, term.width),
217
- clamp(screenY, 1, term.height - 2)
218
- );
219
- term.restoreCursor();
220
- }
221
- function insertChar(char) {
222
- const line = getCurrentLine();
223
- const newLine = line.slice(0, state.cursorX) + char + line.slice(state.cursorX);
224
- state.lines[state.cursorY] = newLine;
225
- state.cursorX++;
226
- updateModifiedStatus();
227
- }
228
- function deleteCharBackward() {
229
- if (state.cursorX > 0) {
230
- const line = getCurrentLine();
231
- const newLine = line.slice(0, state.cursorX - 1) + line.slice(state.cursorX);
232
- state.lines[state.cursorY] = newLine;
233
- state.cursorX--;
234
- updateModifiedStatus();
235
- } else if (state.cursorY > 0) {
236
- const currentLine = state.lines.splice(state.cursorY, 1)[0];
237
- state.cursorY--;
238
- const prevLine = state.lines[state.cursorY];
239
- state.cursorX = prevLine.length;
240
- state.lines[state.cursorY] = prevLine + currentLine;
241
- updateModifiedStatus();
242
- }
243
- }
244
- function deleteCharForward() {
245
- const line = getCurrentLine();
246
- if (state.cursorX < line.length) {
247
- const newLine = line.slice(0, state.cursorX) + line.slice(state.cursorX + 1);
248
- state.lines[state.cursorY] = newLine;
249
- updateModifiedStatus();
250
- } else if (state.cursorY < state.lines.length - 1) {
251
- const nextLine = state.lines.splice(state.cursorY + 1, 1)[0];
252
- state.lines[state.cursorY] = line + nextLine;
253
- updateModifiedStatus();
254
- }
255
- }
256
- function insertNewline() {
257
- const line = getCurrentLine();
258
- const beforeCursor = line.slice(0, state.cursorX);
259
- const afterCursor = line.slice(state.cursorX);
260
- state.lines[state.cursorY] = beforeCursor;
261
- state.lines.splice(state.cursorY + 1, 0, afterCursor);
262
- state.cursorY++;
263
- state.cursorX = 0;
264
- state.leftCol = 0;
265
- updateModifiedStatus();
266
- }
267
- function copyLine() {
268
- if (state.lines.length > 0) {
269
- state.clipboard = [getCurrentLine()];
270
- state.statusMessage = "Line copied";
271
- }
272
- }
273
- function cutLine() {
274
- if (state.lines.length > 0) {
275
- state.clipboard = state.lines.splice(state.cursorY, 1);
276
- if (state.lines.length === 0) {
277
- state.lines.push("");
278
- }
279
- state.cursorY = clamp(state.cursorY, 0, state.lines.length - 1);
280
- state.cursorX = 0;
281
- updateModifiedStatus();
282
- state.statusMessage = "Line cut";
283
- }
284
- }
285
- function pasteLine() {
286
- if (state.clipboard.length > 0) {
287
- state.lines.splice(state.cursorY + 1, 0, ...state.clipboard);
288
- state.cursorY += state.clipboard.length;
289
- state.cursorX = 0;
290
- updateModifiedStatus();
291
- state.statusMessage = `${state.clipboard.length} line(s) pasted`;
292
- }
293
- }
294
- function moveCursor(dx, dy) {
295
- state.cursorY = clamp(state.cursorY + dy, 0, state.lines.length - 1);
296
- const currentLineLength = getCurrentLine().length;
297
- state.cursorX = clamp(state.cursorX + dx, 0, currentLineLength);
298
- if (dy !== 0) {
299
- state.cursorX = clamp(state.cursorX, 0, getCurrentLine().length);
300
- }
301
- }
302
- function pageMove(direction) {
303
- const { height } = term;
304
- const editorHeight = height - 2;
305
- const step = direction * (editorHeight - 1);
306
- state.cursorY = clamp(state.cursorY + step, 0, state.lines.length - 1);
307
- state.topLine = clamp(
308
- state.topLine + step,
309
- 0,
310
- Math.max(0, state.lines.length - editorHeight)
311
- );
312
- if (state.cursorY < state.topLine) {
313
- state.topLine = state.cursorY;
314
- } else if (state.cursorY >= state.topLine + editorHeight) {
315
- state.topLine = state.cursorY - editorHeight + 1;
316
- }
317
- state.cursorX = clamp(state.cursorX, 0, getCurrentLine().length);
318
- }
319
- function jumpToLineEdge(pos) {
320
- if (pos === "start") {
321
- state.cursorX = 0;
322
- } else if (pos === "end") {
323
- state.cursorX = getCurrentLine().length;
324
- }
325
- state.leftCol = 0;
326
- }
327
- function jumpToDocumentEdge(pos) {
328
- if (pos === "start") {
329
- state.cursorY = 0;
330
- state.cursorX = 0;
331
- state.topLine = 0;
332
- } else if (pos === "end") {
333
- state.cursorY = state.lines.length - 1;
334
- state.cursorX = getCurrentLine().length;
335
- const { height } = term;
336
- const editorHeight = height - 2;
337
- state.topLine = Math.max(0, state.lines.length - editorHeight);
338
- }
339
- state.leftCol = 0;
340
- }
341
- async function promptForFilename(promptMessage = "File path: ") {
342
- renderStatusBar();
343
- renderMessageBar();
344
- term.moveTo(1, term.height);
345
- term.eraseLine();
346
- term(promptMessage);
347
- try {
348
- const input = await term.inputField({ echo: true }).promise;
349
- term.moveTo(1, term.height).eraseLine();
350
- return input ? input.trim() : null;
351
- } catch (_error) {
352
- term.moveTo(1, term.height).eraseLine();
353
- state.statusMessage = "Cancelled";
354
- render();
355
- return null;
356
- }
357
- }
358
- async function confirmAction(promptMessage = "Are you sure? (y/N)") {
359
- renderStatusBar();
360
- renderMessageBar();
361
- term.moveTo(1, term.height);
362
- term.eraseLine();
363
- term(`${promptMessage} `);
364
- try {
365
- const confirm = await term.yesOrNo({
366
- yes: ["y", "Y"],
367
- no: ["n", "N", "ENTER"]
368
- }).promise;
369
- term.moveTo(1, term.height).eraseLine();
370
- return confirm;
371
- } catch (_error) {
372
- term.moveTo(1, term.height).eraseLine();
373
- state.statusMessage = "Cancelled";
374
- render();
375
- return false;
376
- }
377
- }
378
- async function saveFile() {
379
- if (!state.filename) {
380
- return saveAsFile();
381
- }
382
- let contentToSave = state.lines.join("\n");
383
- let proceed = true;
384
- if (state.hooks?.onSave) {
385
- try {
386
- const hookResult = await state.hooks.onSave(
387
- contentToSave,
388
- state.filename
389
- );
390
- if (hookResult === false) {
391
- state.statusMessage = "Save prevented by hook.";
392
- proceed = false;
393
- } else if (typeof hookResult === "string") {
394
- contentToSave = hookResult;
395
- state.lines = contentToSave.split("\n");
396
- state.statusMessage = "Content modified by pre-save hook.";
397
- updateModifiedStatus();
398
- }
399
- } catch (error) {
400
- const errorMessage = error instanceof Error ? error.message : String(error);
401
- state.statusMessage = `Error in onSave hook: ${errorMessage}`;
402
- console.error("onSave Hook Error:", error);
403
- proceed = false;
404
- }
405
- }
406
- if (!proceed) {
407
- render();
408
- return false;
409
- }
410
- const returnContent = state.options.returnContentOnSave;
411
- if (returnContent) {
412
- state.originalContent = contentToSave;
413
- state.modified = false;
414
- state.statusMessage = `Content prepared. ${state.filename || ""}`;
415
- render();
416
- if (state.options.autoCloseOnSave) {
417
- await cleanupAndExit(true, contentToSave);
418
- }
419
- return true;
420
- }
421
- try {
422
- await fs.writeFile(state.filename, contentToSave);
423
- state.originalContent = contentToSave;
424
- state.modified = false;
425
- state.statusMessage = `Saved to ${state.filename}`;
426
- render();
427
- if (state.options.autoCloseOnSave) {
428
- await cleanupAndExit(true);
429
- }
430
- return true;
431
- } catch (error) {
432
- const errorMessage = error instanceof Error ? error.message : String(error);
433
- state.statusMessage = `Error saving file: ${errorMessage}`;
434
- console.error("Save Error:", error);
435
- render();
436
- return false;
437
- }
438
- }
439
- async function saveAsFile() {
440
- if (!state.options.allowSaveAs) {
441
- state.statusMessage = "Save As is disabled in this mode.";
442
- render();
443
- return false;
444
- }
445
- const newFilename = await promptForFilename("Save As: ");
446
- if (newFilename) {
447
- const cwd = state.options.cwd || process.cwd();
448
- state.filename = path.resolve(cwd, newFilename);
449
- return saveFile();
450
- }
451
- state.statusMessage = "Save As cancelled.";
452
- render();
453
- return false;
454
- }
455
- async function openFilePrompt() {
456
- if (!state.options.allowOpen) {
457
- state.statusMessage = "Opening files is disabled in this mode.";
458
- render();
459
- return;
460
- }
461
- if (state.modified) {
462
- const shouldSave = await confirmAction(
463
- `Save changes to ${state.filename || "current file"}? (Y/n)`
464
- );
465
- if (shouldSave) {
466
- const saved = await saveFile();
467
- if (!saved) {
468
- state.statusMessage = "Save failed or cancelled. Open cancelled.";
469
- render();
470
- return;
471
- }
472
- }
473
- }
474
- const fileToOpen = await promptForFilename("Open file: ");
475
- if (fileToOpen) {
476
- await loadFile(fileToOpen);
477
- } else {
478
- state.statusMessage = "Open cancelled.";
479
- render();
480
- }
481
- }
482
- async function loadFile(filePath) {
483
- try {
484
- const cwd = state.options.cwd || process.cwd();
485
- const absolutePath = path.resolve(cwd, filePath);
486
- let content = "";
487
- try {
488
- content = await fs.readFile(absolutePath, "utf-8");
489
- state.statusMessage = `Opened ${absolutePath}`;
490
- } catch (error) {
491
- if (error && error.code === "ENOENT") {
492
- content = "";
493
- state.statusMessage = `New file: ${absolutePath}`;
494
- } else {
495
- throw error;
496
- }
497
- }
498
- state.lines = content.split("\n");
499
- if (state.lines.length === 0 || state.lines.length === 1 && state.lines[0] === "" && content.length > 0) {
500
- if (state.lines.length === 0) state.lines = [""];
501
- }
502
- state.filename = absolutePath;
503
- state.originalContent = content;
504
- state.modified = false;
505
- state.cursorX = 0;
506
- state.cursorY = 0;
507
- state.topLine = 0;
508
- state.leftCol = 0;
509
- render();
510
- } catch (error) {
511
- const errorMessage = error instanceof Error ? error.message : String(error);
512
- state.statusMessage = `Error opening file: ${errorMessage}`;
513
- console.error("Open Error:", error);
514
- render();
515
- }
516
- }
517
- async function findText() {
518
- const searchTerm = await promptForFilename(
519
- `Find (Leave empty to cancel, Prev: ${state.lastSearchTerm}): `
520
- );
521
- if (searchTerm === null) {
522
- state.statusMessage = "Find cancelled.";
523
- render();
524
- return;
525
- }
526
- if (!searchTerm && !state.lastSearchTerm) {
527
- state.statusMessage = "No search term provided.";
528
- render();
529
- return;
530
- }
531
- const termToUse = searchTerm || state.lastSearchTerm;
532
- if (!termToUse) return;
533
- state.lastSearchTerm = termToUse;
534
- state.statusMessage = `Searching for: ${termToUse}`;
535
- let found = false;
536
- for (let y = state.cursorY; y < state.lines.length; y++) {
537
- const line = state.lines[y];
538
- const startIdx = y === state.cursorY ? state.cursorX + 1 : 0;
539
- const matchIndex = line.indexOf(termToUse, startIdx);
540
- if (matchIndex !== -1) {
541
- state.cursorY = y;
542
- state.cursorX = matchIndex;
543
- found = true;
544
- break;
545
- }
546
- }
547
- if (!found) {
548
- state.statusMessage = `Search wrapped: ${termToUse}`;
549
- for (let y = 0; y <= state.cursorY; y++) {
550
- const line = state.lines[y];
551
- const endIdx = y === state.cursorY ? state.cursorX + 1 : line.length;
552
- const matchIndex = line.substring(0, endIdx).indexOf(termToUse);
553
- if (matchIndex !== -1) {
554
- state.cursorY = y;
555
- state.cursorX = matchIndex;
556
- found = true;
557
- break;
558
- }
559
- }
560
- }
561
- if (found) {
562
- state.statusMessage = `Found: ${termToUse} at L:${state.cursorY + 1}, C:${state.cursorX + 1}`;
563
- } else {
564
- state.statusMessage = `Not found: ${termToUse}`;
565
- }
566
- render();
567
- }
568
- async function handleInput(key, _matches, data) {
569
- let handled = true;
570
- if (data.isCharacter) {
571
- insertChar(key);
572
- } else {
573
- switch (key) {
574
- // --- Hotkeys ---
575
- case "CTRL_S":
576
- await saveFile();
577
- break;
578
- case "CTRL_A":
579
- if (state.options.allowSaveAs) {
580
- await saveAsFile();
581
- } else {
582
- state.statusMessage = "Save As disabled.";
583
- }
584
- break;
585
- case "CTRL_O":
586
- if (state.options.allowOpen) {
587
- await openFilePrompt();
588
- } else {
589
- state.statusMessage = "Open disabled.";
590
- }
591
- break;
592
- case "CTRL_X":
593
- if (state.modified) {
594
- const saved = await saveFile();
595
- if (saved) {
596
- if (!state.options.autoCloseOnSave) {
597
- await cleanupAndExit(true);
598
- }
599
- } else if (!state.filename && !saved) {
600
- state.statusMessage = "Save failed or cancelled. Exit cancelled.";
601
- } else if (state.filename && !saved) {
602
- state.statusMessage = "Save failed. Exit cancelled.";
603
- }
604
- } else {
605
- await cleanupAndExit(false);
606
- }
607
- break;
608
- case "CTRL_C":
609
- if (state.modified) {
610
- const confirm = await confirmAction("Discard changes? (y/N)");
611
- if (confirm) {
612
- await cleanupAndExit(false);
613
- } else {
614
- state.statusMessage = "Exit cancelled.";
615
- }
616
- } else {
617
- await cleanupAndExit(false);
618
- }
619
- break;
620
- case "CTRL_F":
621
- await findText();
622
- break;
623
- case "CTRL_K":
624
- cutLine();
625
- break;
626
- case "CTRL_U":
627
- pasteLine();
628
- break;
629
- case "ALT_C":
630
- // Simple Copy Line (Alternative binding)
631
- case "CTRL_INSERT":
632
- copyLine();
633
- break;
634
- case "SHIFT_INSERT":
635
- pasteLine();
636
- break;
637
- case "CTRL_T": {
638
- state.syntaxHighlightToggle = !state.syntaxHighlightToggle;
639
- state.statusMessage = `Syntax Highlighting: ${state.syntaxHighlightToggle ? "ON" : "OFF"}`;
640
- break;
641
- }
642
- // --- Navigation ---
643
- case "UP":
644
- moveCursor(0, -1);
645
- break;
646
- case "DOWN":
647
- moveCursor(0, 1);
648
- break;
649
- case "LEFT":
650
- moveCursor(-1, 0);
651
- break;
652
- case "RIGHT":
653
- moveCursor(1, 0);
654
- break;
655
- case "PAGE_UP":
656
- pageMove(-1);
657
- break;
658
- case "PAGE_DOWN":
659
- pageMove(1);
660
- break;
661
- case "HOME":
662
- jumpToLineEdge("start");
663
- break;
664
- case "END":
665
- jumpToLineEdge("end");
666
- break;
667
- case "CTRL_HOME":
668
- jumpToDocumentEdge("start");
669
- break;
670
- case "CTRL_END":
671
- jumpToDocumentEdge("end");
672
- break;
673
- // --- Editing ---
674
- case "BACKSPACE":
675
- deleteCharBackward();
676
- break;
677
- case "DELETE":
678
- deleteCharForward();
679
- break;
680
- case "ENTER":
681
- insertNewline();
682
- break;
683
- case "TAB":
684
- {
685
- const tabWidth = 4;
686
- for (let i = 0; i < tabWidth; i++) insertChar(" ");
687
- }
688
- break;
689
- default:
690
- handled = false;
691
- break;
692
- }
693
- }
694
- if (handled && state.isRunning) {
695
- updateModifiedStatus();
696
- render();
697
- }
698
- }
699
- async function cleanupAndExit(saved = false, content = null) {
700
- if (!state.isRunning) return;
701
- state.isRunning = false;
702
- const exitContent = content ?? (saved ? state.lines.join("\n") : null);
703
- if (state.hooks?.onExit) {
704
- try {
705
- await state.hooks.onExit(exitContent, saved, state.filename);
706
- } catch (error) {
707
- console.error("onExit Hook Error:", error);
708
- }
709
- }
710
- term.off("key", handleInputWrapper);
711
- term.off("resize", handleResize);
712
- term.hideCursor(false);
713
- term.grabInput(false);
714
- term.fullscreen(false);
715
- term.styleReset();
716
- term.clear();
717
- if (state.exitResolver) {
718
- state.exitResolver({
719
- saved,
720
- content: exitContent,
721
- filename: state.filename
722
- });
723
- state.exitResolver = null;
724
- state.exitRejecter = null;
725
- } else {
726
- process.exit(0);
727
- }
728
- }
729
- function handleResize(_width, _height) {
730
- if (state.isRunning) {
731
- render();
732
- }
733
- }
734
- let isHandlingInput = false;
735
- async function handleInputWrapper(key, matches, data) {
736
- if (isHandlingInput || !state.isRunning) return;
737
- isHandlingInput = true;
738
- try {
739
- await handleInput(key, matches, data);
740
- } catch (error) {
741
- console.error("Input Handling Error:", error);
742
- const errorMessage = error instanceof Error ? error.message : String(error);
743
- state.statusMessage = `Error: ${errorMessage}`;
744
- render();
745
- } finally {
746
- isHandlingInput = false;
747
- }
748
- }
749
- async function initializeEditorState(options) {
750
- state = {
751
- // Keep defaults for theme, clipboard, potentially others?
752
- ...state,
753
- // Start with previous state defaults for theme etc.
754
- lines: [""],
755
- cursorX: 0,
756
- cursorY: 0,
757
- topLine: 0,
758
- leftCol: 0,
759
- filename: options.filename || null,
760
- originalContent: "",
761
- modified: false,
762
- statusMessage: "",
763
- lastSearchTerm: "",
764
- editorConfig: {},
765
- // Will be loaded/merged below
766
- hooks: {
767
- onSave: options.onSave,
768
- onExit: options.onExit
769
- },
770
- options: {
771
- // Will be resolved below
772
- filename: options.filename,
773
- initialContent: options.initialContent,
774
- configOverrides: options.configOverrides || {},
775
- allowSaveAs: true,
776
- // Default, will be overridden
777
- allowOpen: true,
778
- // Default, will be overridden
779
- autoCloseOnSave: false,
780
- // Default, will be overridden
781
- returnContentOnSave: false,
782
- // Default, will be overridden
783
- mode: options.mode || "normal",
784
- cwd: options.cwd || process.cwd()
785
- },
786
- clipboard: [],
787
- isRunning: true,
788
- // Reset running state
789
- exitResolver: null,
790
- // Will be set by Promise executor
791
- exitRejecter: null,
792
- // Will be set by Promise executor
793
- syntaxHighlightToggle: false
794
- // Reset toggle state
795
- };
796
- try {
797
- const loadedConfig = await loadEditorConfig(
798
- state.options.cwd,
799
- options.configOverrides
800
- );
801
- state.editorConfig = loadedConfig;
802
- state.options.allowSaveAs = options.allowSaveAs ?? state.editorConfig.defaultAllowSaveAs ?? true;
803
- state.options.allowOpen = options.allowOpen ?? state.editorConfig.defaultAllowOpen ?? true;
804
- state.options.autoCloseOnSave = options.autoCloseOnSave ?? state.editorConfig.defaultAutoCloseOnSave ?? false;
805
- state.options.returnContentOnSave = options.returnContentOnSave ?? state.editorConfig.defaultReturnContentOnSave ?? false;
806
- state.syntaxHighlightToggle = state.editorConfig.syntaxHighlighting ?? false;
807
- state.theme = {
808
- // Reset theme based on config
809
- text: (str) => str,
810
- statusBarBg: (str) => re.bgGray(str),
811
- statusBarText: (str) => re.white(str),
812
- highlight: (str) => re.invert(str),
813
- lineNumber: (str) => re.gray(str)
814
- };
815
- setupTheme(state.editorConfig.theme);
816
- } catch (error) {
817
- const message = error instanceof Error ? error.message : String(error);
818
- console.error("Error loading configuration:", message, error);
819
- term.red("Failed to load configuration. Using defaults.\n");
820
- state.options.allowSaveAs = options.allowSaveAs ?? true;
821
- state.options.allowOpen = options.allowOpen ?? true;
822
- state.options.autoCloseOnSave = options.autoCloseOnSave ?? false;
823
- state.options.returnContentOnSave = options.returnContentOnSave ?? false;
824
- state.syntaxHighlightToggle = false;
825
- setupTheme("light");
826
- }
827
- if (options.initialContent !== void 0 && options.initialContent !== null) {
828
- const content = String(options.initialContent);
829
- state.lines = content.split("\n");
830
- if (state.lines.length === 0) state.lines = [""];
831
- state.originalContent = content;
832
- state.modified = false;
833
- state.filename = options.filename || null;
834
- state.statusMessage = options.filename ? `Editing ${path.basename(options.filename)}` : "Editing new buffer";
835
- } else if (options.filename) {
836
- await loadFile(options.filename);
837
- } else {
838
- state.lines = [""];
839
- state.originalContent = "";
840
- state.modified = false;
841
- state.filename = null;
842
- state.statusMessage = "New file. Ctrl+S to save.";
843
- }
844
- }
845
- export async function startEditor(options = {}) {
846
- await initializeEditorState(options);
847
- return new Promise((resolve, reject) => {
848
- state.exitResolver = resolve;
849
- state.exitRejecter = reject;
850
- try {
851
- term.fullscreen(true);
852
- term.grabInput(true);
853
- term.on("key", handleInputWrapper);
854
- term.on("resize", handleResize);
855
- render();
856
- } catch (termError) {
857
- state.isRunning = false;
858
- reject(
859
- new Error(
860
- `Terminal initialization failed: ${termError instanceof Error ? termError.message : String(termError)}`
861
- )
862
- );
863
- try {
864
- term.grabInput(false);
865
- term.fullscreen(false);
866
- term.styleReset();
867
- term.clear();
868
- } catch (_cleanupErr) {
869
- }
870
- }
871
- });
872
- }
873
- const isDirectRun = (() => {
874
- try {
875
- const scriptPath = fs.realpathSync(process.argv[1] || "");
876
- const modulePath = fs.realpathSync(import.meta.filename);
877
- return scriptPath === modulePath;
878
- } catch (_e) {
879
- return false;
880
- }
881
- })();
882
- if (isDirectRun) {
883
- const fileArg = process.argv[2];
884
- startEditor({ filename: fileArg }).then((_result) => {
885
- }).catch((error) => {
886
- try {
887
- term.grabInput(false);
888
- term.fullscreen(false);
889
- term.styleReset();
890
- term.clear();
891
- } catch (_cleanupErr) {
892
- }
893
- console.error("\nAn unexpected error occurred:", error);
894
- process.exit(1);
895
- });
896
- }