-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathimp-cmd.mts
More file actions
392 lines (336 loc) · 11 KB
/
imp-cmd.mts
File metadata and controls
392 lines (336 loc) · 11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
#!/usr/bin/node
import { ImpT } from "./imp-core.mjs";
import { ImpLoader, lexerTable, TokT } from "./imp-load.mjs";
import { impShow } from "./imp-show.mjs";
import { impEval, impWords, setInputProvider, resetWords } from "./imp-eval.mjs";
import { parsePartialPath, reconstructImplishPath } from "./lib-file.mjs";
import { highlightCode } from "./imp-highlight.mjs";
import * as readline from "readline";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
// Parse command-line arguments
let quietMode = false;
let forceColor = false;
const args = process.argv.slice(2);
for (let i = 0; i < args.length; i++) {
if (args[i] === '-q' || args[i] === '--quiet') {
quietMode = true;
} else if (args[i] === '--color') {
forceColor = true;
}
}
// Detect if output is to a terminal (for syntax highlighting)
// Can be overridden with --color flag
const isTerminal = forceColor || process.stdout.isTTY;
let il = new ImpLoader();
// History file location
const historyFile = path.join(os.homedir(), '.imp-history');
// Tab completion for file paths and word names
function completer(line: string): [string[], string] {
// Find the token under the cursor (simplified: assume cursor at end)
let tokens: Array<{type: string, text: string, start: number}> = []
let pos = 0
let remaining = line
// Tokenize the line to find where we are
while (remaining.length > 0) {
let matched = false
for (let [tokType, rx, _trim] of lexerTable) {
let m = rx.exec(remaining)
if (m) {
tokens.push({type: tokType, text: m[0], start: pos})
pos += m[0].length
remaining = remaining.slice(m[0].length)
matched = true
break
}
}
if (!matched) break
}
// Get the last token (the one being completed)
if (tokens.length === 0) return [[], '']
let lastTok = tokens[tokens.length - 1]
// Handle FILE tokens (file path completion)
if (lastTok.type === TokT.FILE) {
return completeFilePath(lastTok.text)
}
// Handle RAW tokens (word/symbol completion)
if (lastTok.type === TokT.RAW) {
return completeWord(lastTok.text)
}
// No completion for other token types
return [[], '']
}
// Complete file paths (tokens starting with %)
function completeFilePath(token: string): [string[], string] {
// Strip the % prefix
let partialPath = token.slice(1)
// Special case for Windows: %/ lists drives
if (process.platform === 'win32' && partialPath === '/') {
return completeWindowsDrives()
}
// Parse the partial path using lib-path utilities
let parsed = parsePartialPath(partialPath)
// Read directory and filter matches
try {
let entries = fs.readdirSync(parsed.nativeDir, {withFileTypes: true})
let matches = entries
.filter(e => e.name.startsWith(parsed.prefix))
.map(e => reconstructImplishPath(parsed, e.name, e.isDirectory()))
return [matches, token]
} catch (e) {
// Directory doesn't exist or can't be read
return [[], token]
}
}
// List Windows drives as %/c/, %/d/, etc.
function completeWindowsDrives(): [string[], string] {
let drives = []
// Check common drive letters A-Z
for (let i = 65; i <= 90; i++) {
let letter = String.fromCharCode(i).toLowerCase()
try {
// Try to access the drive - if it exists, it won't throw
fs.accessSync(letter + ':/')
drives.push('%/' + letter + '/')
} catch (e) {
// Drive doesn't exist or isn't accessible
}
}
return [drives, '%/']
}
// Complete word names from the current scope
function completeWord(token: string): [string[], string] {
// Get all words that start with the token
let matches = Object.keys(impWords)
.filter(word => word.startsWith(token))
.sort()
return [matches, token]
}
// Quiet mode REPL: simple line-by-line reading without readline
async function quietRepl() {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false // Disable terminal features (no prompt, no special handling)
});
const lineIterator = rl[Symbol.asyncIterator]();
// Create a provider that reads from the shared iterator
class REPLInputProvider {
async readLine() {
const { value, done } = await lineIterator.next();
if (done) throw new Error('End of input');
return value;
}
}
setInputProvider(new REPLInputProvider());
// Process lines without prompting
for await (const line of lineIterator) {
const trimmed = line.trim()
if (trimmed === '/reset') {
il = new ImpLoader()
resetWords()
console.log('Ready.')
continue
}
try {
il.send(line)
let r = il.read()
if (r) {
let e = await impEval(r)
if (e[0] !== ImpT.NIL) console.log(impShow(e))
}
} catch (e) {
console.log("Error: " + e)
}
}
setInputProvider(null);
}
// Print welcome banner with version info
function printBanner() {
const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf-8'));
const version = pkg.version;
const date = new Date().toISOString().split('T')[0];
// Try to get git hash
let gitHash = '';
try {
gitHash = fs.readFileSync('.git/HEAD', 'utf-8').trim();
if (gitHash.startsWith('ref:')) {
const ref = gitHash.substring(5).trim();
gitHash = fs.readFileSync(`.git/${ref}`, 'utf-8').trim().substring(0, 7);
} else {
gitHash = gitHash.substring(0, 7);
}
} catch (e) {
gitHash = 'unknown';
}
const bannerColor = '\x1b[38;5;139m'; // Purple
const linkColor = '\x1b[38;5;114m'; // Green
const resetColor = '\x1b[0m';
if (isTerminal) {
console.log(`${bannerColor}implish${resetColor} (c) ${date} ${linkColor}http://implish.org${resetColor} (v:${gitHash}) | '?' for help\n`);
} else {
console.log(`implish (c) ${date} http://implish.org (v:${gitHash}) | '?' for help\n`);
}
}
// Interactive mode REPL: full readline with prompt, completion, history
async function interactiveRepl() {
printBanner();
// Create readline without automatic echoing if we're in a terminal
// so we can do our own syntax-highlighted rendering
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
completer: completer,
terminal: true
});
// Set prompt with color if terminal supports it (matches web version's #cc7832)
const promptColor = '\x1b[38;5;173m'; // Orange color (same as keywords)
const resetColor = '\x1b[0m';
const promptText = isTerminal ? `${promptColor}>${resetColor} ` : '> ';
rl.setPrompt(promptText);
// Add syntax highlighting to input line if we're in a terminal
if (isTerminal) {
const rlAny = rl as any;
// Override _refreshLine to add syntax highlighting
const originalRefreshLine = rlAny._refreshLine;
rlAny._refreshLine = function() {
// Save the original state
const savedLine = this.line;
const savedCursor = this.cursor;
// Don't highlight if line is empty
if (!savedLine) {
originalRefreshLine.call(this);
return;
}
// Highlight the entire line
const highlighted = highlightCode(savedLine);
// Count visible characters before cursor (excluding ANSI codes)
// We need to calculate where the cursor appears in the highlighted string
let visibleCount = 0;
let highlightedCursor = 0;
let inAnsiCode = false;
for (let i = 0; i < highlighted.length && visibleCount < savedCursor; i++) {
if (highlighted[i] === '\x1b') {
inAnsiCode = true;
}
if (inAnsiCode) {
highlightedCursor++;
if (highlighted[i] === 'm') {
inAnsiCode = false;
}
} else {
visibleCount++;
highlightedCursor++;
}
}
// Set the highlighted line and adjusted cursor position
this.line = highlighted;
this.cursor = highlightedCursor;
// Refresh with highlighted content
originalRefreshLine.call(this);
// Restore original values for the actual buffer
this.line = savedLine;
this.cursor = savedCursor;
};
// Force refresh on every input character
// This ensures highlighting updates as you type
const originalInsertString = rlAny._insertString;
rlAny._insertString = function(c: string) {
originalInsertString.call(this, c);
// Force a refresh to trigger highlighting
this._refreshLine();
};
const originalDeleteLeft = rlAny._deleteLeft;
rlAny._deleteLeft = function() {
originalDeleteLeft.call(this);
this._refreshLine();
};
const originalDeleteRight = rlAny._deleteRight;
rlAny._deleteRight = function() {
originalDeleteRight.call(this);
this._refreshLine();
};
}
// Only enable history persistence when running interactively (TTY)
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
if (isInteractive) {
// Load history from file if it exists
try {
const history = fs.readFileSync(historyFile, 'utf-8')
.split('\n')
.filter(line => line.trim().length > 0)
.reverse(); // Reverse because history is added in reverse order
for (const line of history) {
(rl as any).history.push(line);
}
} catch (e) {
// History file doesn't exist yet or can't be read - that's fine
}
// Save history on exit
function saveHistory() {
try {
const history = (rl as any).history
.slice()
.reverse()
.join('\n') + '\n';
fs.writeFileSync(historyFile, history, 'utf-8');
} catch (e) {
console.error('Failed to save history:', e);
}
}
process.on('exit', saveHistory);
process.on('SIGINT', () => {
saveHistory();
process.exit(0);
});
}
// Create an async iterator that we control
const lineIterator = rl[Symbol.asyncIterator]();
// Create a provider that reads from the shared iterator
class REPLInputProvider {
async readLine() {
const { value, done } = await lineIterator.next();
if (done) throw new Error('End of input');
return value;
}
}
setInputProvider(new REPLInputProvider());
rl.prompt();
// Manually iterate instead of using for-await to share the iterator
while (true) {
const { value: line, done } = await lineIterator.next();
if (done) break;
const trimmed = line.trim()
if (trimmed === '/reset') {
il = new ImpLoader()
resetWords()
console.log('Ready.')
rl.prompt()
continue
}
try {
il.send(line)
let r = il.read()
if (r) {
let e = await impEval(r)
if (e[0] !== ImpT.NIL) {
const output = impShow(e)
console.log(isTerminal ? highlightCode(output) : output)
}
}
} catch (e) {
console.log("Error: " + e)
}
rl.prompt();
}
// Clean up when REPL exits
console.log(); // Add final newline on exit (e.g., when Ctrl+D is pressed)
setInputProvider(null);
}
// Start the appropriate REPL mode
if (quietMode) {
await quietRepl()
} else {
await interactiveRepl()
}