caroushell 0.1.20 → 0.1.23
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 +4 -6
- package/dist/ai-suggester.js +1 -1
- package/dist/app.js +61 -14
- package/dist/carousel.js +234 -54
- package/dist/keyboard.js +55 -34
- package/dist/spawner.js +83 -0
- package/dist/terminal.js +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/caroushell)
|
|
4
4
|
[](https://www.npmjs.com/package/caroushell)
|
|
5
5
|
|
|
6
|
-
Caroushell is
|
|
7
|
-
|
|
6
|
+
Caroushell is kind of like `bash` but you see history and AI suggestions as you
|
|
7
|
+
type.
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
@@ -12,9 +12,6 @@ history, and AI suggestions as you type.
|
|
|
12
12
|
- The bottom panel of the carousel shows AI-generated command suggestions.
|
|
13
13
|
- Go up and down the carousel with arrow keys.
|
|
14
14
|
- Press `Enter` to run the highlighted command.
|
|
15
|
-
- Logs activity under `~/.caroushell/logs` for easy troubleshooting.
|
|
16
|
-
- Extensible config file (`~/.caroushell/config.toml`) so you can point the CLI
|
|
17
|
-
at different AI providers.
|
|
18
15
|
|
|
19
16
|
## UI
|
|
20
17
|
|
|
@@ -42,12 +39,13 @@ It would look like this:
|
|
|
42
39
|
|
|
43
40
|

|
|
44
41
|
|
|
45
|
-
##
|
|
42
|
+
## Setup
|
|
46
43
|
|
|
47
44
|
- Node.js 18 or newer.
|
|
48
45
|
- On first launch Caroushell will prompt you for an OpenAI-compatible endpoint
|
|
49
46
|
URL, API key, and model name, then store them in `~/.caroushell/config.toml`.
|
|
50
47
|
- You can also create the file manually:
|
|
48
|
+
- Logs are at `~/.caroushell/logs` for easy troubleshooting.
|
|
51
49
|
|
|
52
50
|
```toml
|
|
53
51
|
apiUrl = "https://openrouter.ai/api/v1"
|
package/dist/ai-suggester.js
CHANGED
|
@@ -80,7 +80,7 @@ async function generateContent(prompt, options) {
|
|
|
80
80
|
async function listModels(apiUrl, apiKey) {
|
|
81
81
|
const url = apiUrl.replace("/chat/completions", "") + "/models";
|
|
82
82
|
const res = await fetch(url, { headers: headers(apiKey) });
|
|
83
|
-
const models = await res.json();
|
|
83
|
+
const models = (await res.json());
|
|
84
84
|
return models.data.map((m) => m.id);
|
|
85
85
|
}
|
|
86
86
|
function headers(apiKey) {
|
package/dist/app.js
CHANGED
|
@@ -9,6 +9,9 @@ const ai_suggester_1 = require("./ai-suggester");
|
|
|
9
9
|
const file_suggester_1 = require("./file-suggester");
|
|
10
10
|
const spawner_1 = require("./spawner");
|
|
11
11
|
const logs_1 = require("./logs");
|
|
12
|
+
function collapseLineContinuations(input) {
|
|
13
|
+
return input.replace(/\\\r?\n/g, "");
|
|
14
|
+
}
|
|
12
15
|
class App {
|
|
13
16
|
constructor(deps = {}) {
|
|
14
17
|
this.usingFileSuggestions = false;
|
|
@@ -63,17 +66,12 @@ class App {
|
|
|
63
66
|
if (this.tryAcceptHighlightedFileSuggestion()) {
|
|
64
67
|
return;
|
|
65
68
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
this.terminal.resetBlockTracking();
|
|
73
|
-
// Render the prompt, without this we'd wait for the suggestions to call render
|
|
74
|
-
// and it would appear slow
|
|
75
|
-
this.render();
|
|
76
|
-
this.queueUpdateSuggestions();
|
|
69
|
+
if (this.carousel.isPromptRowSelected()) {
|
|
70
|
+
await this.enterOnPrompt();
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
await this.enterOnSuggestion();
|
|
74
|
+
}
|
|
77
75
|
},
|
|
78
76
|
char: (evt) => {
|
|
79
77
|
this.carousel.insertAtCursor(evt.sequence);
|
|
@@ -83,10 +81,20 @@ class App {
|
|
|
83
81
|
this.queueUpdateSuggestions();
|
|
84
82
|
},
|
|
85
83
|
up: () => {
|
|
84
|
+
if (this.carousel.shouldUpMoveMultilineCursor()) {
|
|
85
|
+
this.carousel.moveMultilineCursorUp();
|
|
86
|
+
this.render();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
86
89
|
this.carousel.up();
|
|
87
90
|
this.render();
|
|
88
91
|
},
|
|
89
92
|
down: () => {
|
|
93
|
+
if (this.carousel.shouldDownMoveMultilineCursor()) {
|
|
94
|
+
this.carousel.moveMultilineCursorDown();
|
|
95
|
+
this.render();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
90
98
|
this.carousel.down();
|
|
91
99
|
this.render();
|
|
92
100
|
},
|
|
@@ -136,13 +144,21 @@ class App {
|
|
|
136
144
|
async run() {
|
|
137
145
|
await this.init();
|
|
138
146
|
this.keyboard.enableCapture();
|
|
139
|
-
this.
|
|
147
|
+
this.onKeyHandler = (evt) => {
|
|
140
148
|
void this.handleKey(evt);
|
|
141
|
-
}
|
|
149
|
+
};
|
|
150
|
+
this.keyboard.on("key", this.onKeyHandler);
|
|
142
151
|
// Initial draw
|
|
143
152
|
this.render();
|
|
144
153
|
await this.carousel.updateSuggestions();
|
|
145
154
|
}
|
|
155
|
+
end() {
|
|
156
|
+
if (this.onKeyHandler) {
|
|
157
|
+
this.keyboard.off("key", this.onKeyHandler);
|
|
158
|
+
this.onKeyHandler = undefined;
|
|
159
|
+
}
|
|
160
|
+
this.keyboard.disableCapture();
|
|
161
|
+
}
|
|
146
162
|
async handleKey(evt) {
|
|
147
163
|
const fn = this.handlers[evt.name];
|
|
148
164
|
if (fn) {
|
|
@@ -177,13 +193,44 @@ class App {
|
|
|
177
193
|
}
|
|
178
194
|
finally {
|
|
179
195
|
this.terminal.enableWrites();
|
|
196
|
+
this.terminal.reset();
|
|
180
197
|
this.keyboard.enableCapture();
|
|
181
198
|
}
|
|
182
199
|
}
|
|
200
|
+
async enterOnPrompt() {
|
|
201
|
+
// Check for '\' line continuation
|
|
202
|
+
const lineInfo = this.carousel.getInputLineInfoAtCursor();
|
|
203
|
+
if (lineInfo.lineText.endsWith("\\") &&
|
|
204
|
+
lineInfo.column === lineInfo.lineText.length) {
|
|
205
|
+
this.carousel.insertAtCursor("\n");
|
|
206
|
+
this.render();
|
|
207
|
+
this.queueUpdateSuggestions();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const rawInput = this.carousel.getInputBuffer();
|
|
211
|
+
const cmd = collapseLineContinuations(rawInput).trim();
|
|
212
|
+
await this.confirmCommandRun(cmd);
|
|
213
|
+
}
|
|
214
|
+
async enterOnSuggestion() {
|
|
215
|
+
const cmd = this.carousel.getCurrentRow().trim();
|
|
216
|
+
await this.confirmCommandRun(cmd);
|
|
217
|
+
}
|
|
218
|
+
async confirmCommandRun(cmd) {
|
|
219
|
+
this.carousel.setInputBuffer("", 0);
|
|
220
|
+
await this.runCommand(cmd);
|
|
221
|
+
// Carousel should point to the prompt
|
|
222
|
+
this.carousel.resetIndex();
|
|
223
|
+
// After arbitrary output, reset render block tracking
|
|
224
|
+
this.terminal.resetBlockTracking();
|
|
225
|
+
// Render the prompt, without this we'd wait for the suggestions to call render
|
|
226
|
+
// and it would appear slow
|
|
227
|
+
this.render();
|
|
228
|
+
this.queueUpdateSuggestions();
|
|
229
|
+
}
|
|
183
230
|
exit() {
|
|
184
231
|
// Clear terminal contents before shutting down to leave a clean screen.
|
|
185
232
|
this.terminal.renderBlock([]);
|
|
186
|
-
this.
|
|
233
|
+
this.end();
|
|
187
234
|
process.exit(0);
|
|
188
235
|
}
|
|
189
236
|
async tryAutocompleteFile() {
|
package/dist/carousel.js
CHANGED
|
@@ -1,13 +1,67 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Carousel = void 0;
|
|
4
|
+
exports.getDisplayWidth = getDisplayWidth;
|
|
4
5
|
const logs_1 = require("./logs");
|
|
5
6
|
const terminal_1 = require("./terminal");
|
|
7
|
+
const ANSI_ESCAPE_REGEX = /\x1b\[[0-9;]*m/g;
|
|
8
|
+
const COMBINING_MARK_REGEX = /^\p{Mark}+$/u;
|
|
9
|
+
const EMOJI_REGEX = /\p{Extended_Pictographic}/u;
|
|
10
|
+
const GRAPHEME_SEGMENTER = typeof Intl !== "undefined" && "Segmenter" in Intl
|
|
11
|
+
? new Intl.Segmenter("en", { granularity: "grapheme" })
|
|
12
|
+
: null;
|
|
13
|
+
function isFullWidthCodePoint(codePoint) {
|
|
14
|
+
return (codePoint >= 0x1100 &&
|
|
15
|
+
(codePoint <= 0x115f ||
|
|
16
|
+
codePoint === 0x2329 ||
|
|
17
|
+
codePoint === 0x232a ||
|
|
18
|
+
(codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) ||
|
|
19
|
+
(codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
|
|
20
|
+
(codePoint >= 0xf900 && codePoint <= 0xfaff) ||
|
|
21
|
+
(codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
|
|
22
|
+
(codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
|
|
23
|
+
(codePoint >= 0xff00 && codePoint <= 0xff60) ||
|
|
24
|
+
(codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
|
|
25
|
+
(codePoint >= 0x1f300 && codePoint <= 0x1f6ff) ||
|
|
26
|
+
(codePoint >= 0x1f900 && codePoint <= 0x1f9ff) ||
|
|
27
|
+
(codePoint >= 0x1fa70 && codePoint <= 0x1faff) ||
|
|
28
|
+
(codePoint >= 0x20000 && codePoint <= 0x3fffd)));
|
|
29
|
+
}
|
|
30
|
+
function getDisplayWidth(text) {
|
|
31
|
+
const stripped = text.replace(ANSI_ESCAPE_REGEX, "");
|
|
32
|
+
let width = 0;
|
|
33
|
+
if (GRAPHEME_SEGMENTER) {
|
|
34
|
+
for (const { segment } of GRAPHEME_SEGMENTER.segment(stripped)) {
|
|
35
|
+
if (!segment)
|
|
36
|
+
continue;
|
|
37
|
+
if (COMBINING_MARK_REGEX.test(segment))
|
|
38
|
+
continue;
|
|
39
|
+
if (EMOJI_REGEX.test(segment)) {
|
|
40
|
+
width += 2;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const codePoint = segment.codePointAt(0) ?? 0;
|
|
44
|
+
width += isFullWidthCodePoint(codePoint) ? 2 : 1;
|
|
45
|
+
}
|
|
46
|
+
return width;
|
|
47
|
+
}
|
|
48
|
+
for (const char of stripped) {
|
|
49
|
+
if (COMBINING_MARK_REGEX.test(char))
|
|
50
|
+
continue;
|
|
51
|
+
if (EMOJI_REGEX.test(char)) {
|
|
52
|
+
width += 2;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const codePoint = char.codePointAt(0) ?? 0;
|
|
56
|
+
width += isFullWidthCodePoint(codePoint) ? 2 : 1;
|
|
57
|
+
}
|
|
58
|
+
return width;
|
|
59
|
+
}
|
|
6
60
|
class Carousel {
|
|
7
61
|
constructor(opts) {
|
|
8
62
|
this.index = 0;
|
|
9
63
|
this.inputBuffer = "";
|
|
10
|
-
this.
|
|
64
|
+
this.cursorIndex = 0;
|
|
11
65
|
this.terminal = opts.terminal;
|
|
12
66
|
this.top = opts.top;
|
|
13
67
|
this.bottom = opts.bottom;
|
|
@@ -27,6 +81,7 @@ class Carousel {
|
|
|
27
81
|
if (this.index >= topLength) {
|
|
28
82
|
this.index = topLength;
|
|
29
83
|
}
|
|
84
|
+
this.clampCursorToActiveRow();
|
|
30
85
|
}
|
|
31
86
|
down() {
|
|
32
87
|
this.index -= 1;
|
|
@@ -34,6 +89,7 @@ class Carousel {
|
|
|
34
89
|
if (-this.index >= bottomLength) {
|
|
35
90
|
this.index = -bottomLength;
|
|
36
91
|
}
|
|
92
|
+
this.clampCursorToActiveRow();
|
|
37
93
|
}
|
|
38
94
|
getRow(rowIndex) {
|
|
39
95
|
const latestTop = this.top.latest();
|
|
@@ -60,22 +116,32 @@ class Carousel {
|
|
|
60
116
|
}
|
|
61
117
|
return "$> ";
|
|
62
118
|
}
|
|
63
|
-
|
|
119
|
+
getFormattedSuggestionRow(rowIndex) {
|
|
64
120
|
const rowStr = this.getRow(rowIndex);
|
|
65
|
-
let prefix = this.
|
|
66
|
-
const {
|
|
121
|
+
let prefix = this.getSuggestionPrefix(rowIndex, rowStr);
|
|
122
|
+
const { reset, dim } = terminal_1.colors;
|
|
67
123
|
let color = dim;
|
|
68
124
|
if (this.index === rowIndex) {
|
|
69
125
|
color = terminal_1.colors.purple;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
126
|
+
}
|
|
127
|
+
return `${color}${prefix}${rowStr}${reset}`;
|
|
128
|
+
}
|
|
129
|
+
getSuggestionPrefix(rowIndex, rowStr) {
|
|
130
|
+
let prefix = this.getPrefixByIndex(rowIndex);
|
|
131
|
+
if (this.index === rowIndex && rowIndex !== 0) {
|
|
132
|
+
prefix = `${prefix}> `;
|
|
73
133
|
}
|
|
74
134
|
if (rowIndex !== 0 && !rowStr) {
|
|
75
135
|
// The edge of the top or bottom panel
|
|
76
136
|
prefix = "---";
|
|
77
137
|
}
|
|
78
|
-
return
|
|
138
|
+
return prefix;
|
|
139
|
+
}
|
|
140
|
+
getFormattedPromptRow(lineIndex, lineText, promptSelected) {
|
|
141
|
+
const { reset, dim } = terminal_1.colors;
|
|
142
|
+
const color = promptSelected ? terminal_1.colors.purple : dim;
|
|
143
|
+
const prefix = this.getPromptPrefix(lineIndex);
|
|
144
|
+
return `${color}${prefix}${lineText}${reset}`;
|
|
79
145
|
}
|
|
80
146
|
getCurrentRow() {
|
|
81
147
|
return this.getRow(this.index);
|
|
@@ -89,50 +155,67 @@ class Carousel {
|
|
|
89
155
|
}
|
|
90
156
|
setInputBuffer(value, cursorPos = value.length) {
|
|
91
157
|
this.inputBuffer = value;
|
|
92
|
-
this.
|
|
158
|
+
this.cursorIndex = Math.max(0, Math.min(cursorPos, this.inputBuffer.length));
|
|
159
|
+
}
|
|
160
|
+
getInputBuffer() {
|
|
161
|
+
return this.inputBuffer;
|
|
93
162
|
}
|
|
94
163
|
resetIndex() {
|
|
95
164
|
this.index = 0;
|
|
165
|
+
this.cursorIndex = Math.min(this.cursorIndex, this.inputBuffer.length);
|
|
96
166
|
}
|
|
97
167
|
adoptSelectionIntoInput() {
|
|
168
|
+
// When you highlighted a suggestion row (history/AI) and then type
|
|
169
|
+
// or edit, we want to pull that selected row into the input buffer
|
|
98
170
|
if (this.index === 0)
|
|
99
171
|
return;
|
|
100
172
|
const current = this.getRow(this.index);
|
|
101
|
-
this.setInputBuffer(current, current.length);
|
|
173
|
+
this.setInputBuffer(current, Math.min(this.cursorIndex, current.length));
|
|
102
174
|
this.index = 0;
|
|
103
175
|
}
|
|
176
|
+
getActiveRowLength() {
|
|
177
|
+
if (this.isPromptRowSelected())
|
|
178
|
+
return this.inputBuffer.length;
|
|
179
|
+
return this.getRow(this.index).length;
|
|
180
|
+
}
|
|
181
|
+
canMoveRight() {
|
|
182
|
+
return this.cursorIndex < this.getActiveRowLength();
|
|
183
|
+
}
|
|
184
|
+
clampCursorToActiveRow() {
|
|
185
|
+
const len = this.getActiveRowLength();
|
|
186
|
+
this.cursorIndex = Math.max(0, Math.min(this.cursorIndex, len));
|
|
187
|
+
}
|
|
104
188
|
insertAtCursor(text) {
|
|
105
189
|
if (!text)
|
|
106
190
|
return;
|
|
107
191
|
this.adoptSelectionIntoInput();
|
|
108
|
-
const before = this.inputBuffer.slice(0, this.
|
|
109
|
-
const after = this.inputBuffer.slice(this.
|
|
192
|
+
const before = this.inputBuffer.slice(0, this.cursorIndex);
|
|
193
|
+
const after = this.inputBuffer.slice(this.cursorIndex);
|
|
110
194
|
this.inputBuffer = `${before}${text}${after}`;
|
|
111
|
-
this.
|
|
195
|
+
this.cursorIndex += text.length;
|
|
112
196
|
}
|
|
113
197
|
deleteBeforeCursor() {
|
|
114
198
|
this.adoptSelectionIntoInput();
|
|
115
|
-
if (this.
|
|
199
|
+
if (this.cursorIndex === 0)
|
|
116
200
|
return;
|
|
117
|
-
const before = this.inputBuffer.slice(0, this.
|
|
118
|
-
const after = this.inputBuffer.slice(this.
|
|
201
|
+
const before = this.inputBuffer.slice(0, this.cursorIndex - 1);
|
|
202
|
+
const after = this.inputBuffer.slice(this.cursorIndex);
|
|
119
203
|
this.inputBuffer = `${before}${after}`;
|
|
120
|
-
this.
|
|
204
|
+
this.cursorIndex -= 1;
|
|
121
205
|
}
|
|
122
206
|
moveCursorLeft() {
|
|
123
|
-
this.
|
|
124
|
-
if (this.inputCursor === 0)
|
|
207
|
+
if (this.cursorIndex === 0)
|
|
125
208
|
return;
|
|
126
|
-
this.
|
|
209
|
+
this.cursorIndex -= 1;
|
|
127
210
|
}
|
|
128
211
|
isWhitespace(char) {
|
|
129
212
|
return /\s/.test(char);
|
|
130
213
|
}
|
|
131
214
|
moveCursorWordLeft() {
|
|
132
215
|
this.adoptSelectionIntoInput();
|
|
133
|
-
if (this.
|
|
216
|
+
if (this.cursorIndex === 0)
|
|
134
217
|
return;
|
|
135
|
-
let pos = this.
|
|
218
|
+
let pos = this.cursorIndex;
|
|
136
219
|
// Skip any whitespace directly to the left of the cursor
|
|
137
220
|
while (pos > 0 && this.isWhitespace(this.inputBuffer[pos - 1])) {
|
|
138
221
|
pos -= 1;
|
|
@@ -141,19 +224,46 @@ class Carousel {
|
|
|
141
224
|
while (pos > 0 && !this.isWhitespace(this.inputBuffer[pos - 1])) {
|
|
142
225
|
pos -= 1;
|
|
143
226
|
}
|
|
144
|
-
this.
|
|
227
|
+
this.cursorIndex = pos;
|
|
145
228
|
}
|
|
146
229
|
moveCursorRight() {
|
|
230
|
+
if (!this.canMoveRight())
|
|
231
|
+
return;
|
|
232
|
+
this.cursorIndex += 1;
|
|
233
|
+
}
|
|
234
|
+
shouldUpMoveMultilineCursor() {
|
|
235
|
+
const info = this.getLineInfoAtPosition(this.cursorIndex);
|
|
236
|
+
return this.isPromptRowSelected() && info.lineIndex > 0;
|
|
237
|
+
}
|
|
238
|
+
shouldDownMoveMultilineCursor() {
|
|
239
|
+
const info = this.getLineInfoAtPosition(this.cursorIndex);
|
|
240
|
+
return this.isPromptRowSelected() && info.lineIndex < info.lines.length - 1;
|
|
241
|
+
}
|
|
242
|
+
moveMultilineCursorUp() {
|
|
243
|
+
this.adoptSelectionIntoInput();
|
|
244
|
+
const info = this.getLineInfoAtPosition(this.cursorIndex);
|
|
245
|
+
if (info.lineIndex === 0)
|
|
246
|
+
return;
|
|
247
|
+
const targetIndex = info.lineIndex - 1;
|
|
248
|
+
const targetStart = this.getLineStartIndex(targetIndex, info.lines);
|
|
249
|
+
const targetLen = info.lines[targetIndex].length;
|
|
250
|
+
this.cursorIndex = targetStart + Math.min(info.column, targetLen);
|
|
251
|
+
}
|
|
252
|
+
moveMultilineCursorDown() {
|
|
147
253
|
this.adoptSelectionIntoInput();
|
|
148
|
-
|
|
254
|
+
const info = this.getLineInfoAtPosition(this.cursorIndex);
|
|
255
|
+
if (info.lineIndex >= info.lines.length - 1)
|
|
149
256
|
return;
|
|
150
|
-
|
|
257
|
+
const targetIndex = info.lineIndex + 1;
|
|
258
|
+
const targetStart = this.getLineStartIndex(targetIndex, info.lines);
|
|
259
|
+
const targetLen = info.lines[targetIndex].length;
|
|
260
|
+
this.cursorIndex = targetStart + Math.min(info.column, targetLen);
|
|
151
261
|
}
|
|
152
262
|
moveCursorWordRight() {
|
|
153
263
|
this.adoptSelectionIntoInput();
|
|
154
|
-
if (this.
|
|
264
|
+
if (this.cursorIndex >= this.inputBuffer.length)
|
|
155
265
|
return;
|
|
156
|
-
let pos = this.
|
|
266
|
+
let pos = this.cursorIndex;
|
|
157
267
|
const len = this.inputBuffer.length;
|
|
158
268
|
// Skip any whitespace to the right of the cursor
|
|
159
269
|
while (pos < len && this.isWhitespace(this.inputBuffer[pos])) {
|
|
@@ -163,30 +273,35 @@ class Carousel {
|
|
|
163
273
|
while (pos < len && !this.isWhitespace(this.inputBuffer[pos])) {
|
|
164
274
|
pos += 1;
|
|
165
275
|
}
|
|
166
|
-
this.
|
|
276
|
+
this.cursorIndex = pos;
|
|
167
277
|
}
|
|
168
278
|
moveCursorHome() {
|
|
169
279
|
this.adoptSelectionIntoInput();
|
|
170
|
-
this.
|
|
280
|
+
this.cursorIndex = this.getLineInfoAtPosition(this.cursorIndex).lineStart;
|
|
171
281
|
}
|
|
172
282
|
moveCursorEnd() {
|
|
173
283
|
this.adoptSelectionIntoInput();
|
|
174
|
-
this.
|
|
284
|
+
this.cursorIndex = this.getLineInfoAtPosition(this.cursorIndex).lineEnd;
|
|
175
285
|
}
|
|
176
286
|
deleteAtCursor() {
|
|
177
287
|
this.adoptSelectionIntoInput();
|
|
178
|
-
if (this.
|
|
288
|
+
if (this.cursorIndex >= this.inputBuffer.length)
|
|
179
289
|
return;
|
|
180
|
-
const before = this.inputBuffer.slice(0, this.
|
|
181
|
-
const after = this.inputBuffer.slice(this.
|
|
290
|
+
const before = this.inputBuffer.slice(0, this.cursorIndex);
|
|
291
|
+
const after = this.inputBuffer.slice(this.cursorIndex + 1);
|
|
182
292
|
this.inputBuffer = `${before}${after}`;
|
|
183
293
|
}
|
|
184
294
|
deleteToLineStart() {
|
|
185
295
|
this.adoptSelectionIntoInput();
|
|
186
|
-
if (this.
|
|
296
|
+
if (this.cursorIndex === 0)
|
|
187
297
|
return;
|
|
188
|
-
const
|
|
189
|
-
|
|
298
|
+
const info = this.getLineInfoAtPosition(this.cursorIndex);
|
|
299
|
+
if (info.column === 0)
|
|
300
|
+
return;
|
|
301
|
+
const before = this.inputBuffer.slice(0, info.lineStart);
|
|
302
|
+
const after = this.inputBuffer.slice(this.cursorIndex);
|
|
303
|
+
this.inputBuffer = `${before}${after}`;
|
|
304
|
+
this.cursorIndex = info.lineStart;
|
|
190
305
|
}
|
|
191
306
|
clearInput() {
|
|
192
307
|
this.adoptSelectionIntoInput();
|
|
@@ -200,14 +315,14 @@ class Carousel {
|
|
|
200
315
|
return this.index === 0;
|
|
201
316
|
}
|
|
202
317
|
getInputCursor() {
|
|
203
|
-
return this.
|
|
318
|
+
return this.cursorIndex;
|
|
204
319
|
}
|
|
205
320
|
getWordInfoAtCursor() {
|
|
206
|
-
let start = this.
|
|
321
|
+
let start = this.cursorIndex;
|
|
207
322
|
while (start > 0 && !this.isWhitespace(this.inputBuffer[start - 1])) {
|
|
208
323
|
start -= 1;
|
|
209
324
|
}
|
|
210
|
-
let end = this.
|
|
325
|
+
let end = this.cursorIndex;
|
|
211
326
|
const len = this.inputBuffer.length;
|
|
212
327
|
while (end < len && !this.isWhitespace(this.inputBuffer[end])) {
|
|
213
328
|
end += 1;
|
|
@@ -215,33 +330,98 @@ class Carousel {
|
|
|
215
330
|
return {
|
|
216
331
|
start,
|
|
217
332
|
end,
|
|
218
|
-
prefix: this.inputBuffer.slice(start, this.
|
|
333
|
+
prefix: this.inputBuffer.slice(start, this.cursorIndex),
|
|
219
334
|
word: this.inputBuffer.slice(start, end),
|
|
220
335
|
};
|
|
221
336
|
}
|
|
337
|
+
getInputLineInfoAtCursor() {
|
|
338
|
+
return this.getLineInfoAtPosition(this.cursorIndex);
|
|
339
|
+
}
|
|
222
340
|
getPromptCursorColumn() {
|
|
223
|
-
const
|
|
224
|
-
|
|
341
|
+
const info = this.getLineInfoAtPosition(this.cursorIndex);
|
|
342
|
+
const prefix = this.getPromptPrefix(info.lineIndex);
|
|
343
|
+
const linePrefix = info.lineText.slice(0, info.column);
|
|
344
|
+
return getDisplayWidth(prefix) + getDisplayWidth(linePrefix);
|
|
345
|
+
}
|
|
346
|
+
getLineInfoAtPosition(pos) {
|
|
347
|
+
// Map a buffer index to its line/column and line boundaries.
|
|
348
|
+
const lines = this.getInputLines();
|
|
349
|
+
let start = 0;
|
|
350
|
+
for (let i = 0; i < lines.length; i++) {
|
|
351
|
+
const line = lines[i];
|
|
352
|
+
const end = start + line.length;
|
|
353
|
+
if (pos <= end) {
|
|
354
|
+
// Cursor is on this line or at its end.
|
|
355
|
+
return {
|
|
356
|
+
lines,
|
|
357
|
+
lineIndex: i,
|
|
358
|
+
lineText: line,
|
|
359
|
+
lineStart: start,
|
|
360
|
+
lineEnd: end,
|
|
361
|
+
column: pos - start,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
start = end + 1;
|
|
365
|
+
}
|
|
366
|
+
const lastIndex = Math.max(0, lines.length - 1);
|
|
367
|
+
const lastStart = Math.max(0, this.inputBuffer.length - lines[lastIndex].length);
|
|
368
|
+
// Fallback when pos is beyond the buffer end.
|
|
369
|
+
return {
|
|
370
|
+
lines,
|
|
371
|
+
lineIndex: lastIndex,
|
|
372
|
+
lineText: lines[lastIndex] ?? "",
|
|
373
|
+
lineStart: lastStart,
|
|
374
|
+
lineEnd: lastStart + (lines[lastIndex]?.length ?? 0),
|
|
375
|
+
column: Math.max(0, pos - lastStart),
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
getInputLines() {
|
|
379
|
+
return this.inputBuffer.split("\n");
|
|
380
|
+
}
|
|
381
|
+
getLineStartIndex(lineIndex, lines) {
|
|
382
|
+
let start = 0;
|
|
383
|
+
for (let i = 0; i < lineIndex; i++) {
|
|
384
|
+
start += lines[i].length + 1;
|
|
385
|
+
}
|
|
386
|
+
return start;
|
|
387
|
+
}
|
|
388
|
+
getPromptPrefix(lineIndex) {
|
|
389
|
+
return lineIndex === 0 ? "$> " : "> ";
|
|
225
390
|
}
|
|
226
391
|
render() {
|
|
227
392
|
(0, logs_1.logLine)("Rendering carousel");
|
|
228
|
-
// Draw all the lines
|
|
229
393
|
const width = process.stdout.columns || 80;
|
|
230
|
-
const { brightWhite, reset, dim } = terminal_1.colors;
|
|
231
394
|
const lines = [];
|
|
232
|
-
const start = this.index + this.topRowCount;
|
|
233
395
|
const rowCount = this.topRowCount + this.bottomRowCount + 1;
|
|
396
|
+
const start = this.index + this.topRowCount;
|
|
234
397
|
const end = start - rowCount;
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
398
|
+
const promptLines = this.getInputLines();
|
|
399
|
+
const promptSelected = this.index === 0;
|
|
400
|
+
const lineInfo = this.getLineInfoAtPosition(this.cursorIndex);
|
|
401
|
+
let cursorRow = 0;
|
|
402
|
+
let cursorCol = 0;
|
|
403
|
+
for (let rowIndex = start; rowIndex > end; rowIndex--) {
|
|
404
|
+
if (rowIndex === 0) {
|
|
405
|
+
for (let i = 0; i < promptLines.length; i++) {
|
|
406
|
+
if (this.index === 0 && i === lineInfo.lineIndex) {
|
|
407
|
+
cursorRow = lines.length;
|
|
408
|
+
cursorCol = this.getPromptCursorColumn();
|
|
409
|
+
}
|
|
410
|
+
lines.push(this.getFormattedPromptRow(i, promptLines[i], promptSelected));
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
if (this.index === rowIndex) {
|
|
415
|
+
const rowStr = this.getRow(rowIndex);
|
|
416
|
+
const prefix = this.getSuggestionPrefix(rowIndex, rowStr);
|
|
417
|
+
cursorRow = lines.length;
|
|
418
|
+
const cursorText = rowStr.slice(0, Math.min(this.cursorIndex, rowStr.length));
|
|
419
|
+
cursorCol = getDisplayWidth(prefix) + getDisplayWidth(cursorText);
|
|
420
|
+
}
|
|
421
|
+
lines.push(this.getFormattedSuggestionRow(rowIndex));
|
|
422
|
+
}
|
|
240
423
|
}
|
|
241
|
-
|
|
242
|
-
const cursorRow = this.topRowCount;
|
|
243
|
-
const cursorCol = this.getPromptCursorColumn();
|
|
244
|
-
this.terminal.renderBlock(lines.map((line) => line.text), cursorRow, cursorCol);
|
|
424
|
+
this.terminal.renderBlock(lines.map((line) => line.slice(0, width - 2)), cursorRow, cursorCol);
|
|
245
425
|
}
|
|
246
426
|
setTopSuggester(suggester) {
|
|
247
427
|
if (this.top === suggester)
|
package/dist/keyboard.js
CHANGED
|
@@ -1,45 +1,66 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Keyboard = void 0;
|
|
3
|
+
exports.Keyboard = exports.KEY_SEQUENCES = void 0;
|
|
4
|
+
exports.keySequence = keySequence;
|
|
4
5
|
const events_1 = require("events");
|
|
5
|
-
// Map escape/control sequences
|
|
6
|
-
const
|
|
6
|
+
// Map semantic key names to escape/control sequences
|
|
7
|
+
const KEY_DEFINITIONS = {
|
|
7
8
|
// Control keys
|
|
8
|
-
'
|
|
9
|
-
'
|
|
10
|
-
'
|
|
11
|
-
|
|
12
|
-
'\r'
|
|
13
|
-
'\
|
|
14
|
-
|
|
15
|
-
'\u0008': { name: 'backspace' }, // BS (Windows)
|
|
16
|
-
'\u001b': { name: 'escape' },
|
|
9
|
+
'ctrl-c': [{ sequence: '\u0003', ctrl: true }], // ^C
|
|
10
|
+
'ctrl-d': [{ sequence: '\u0004', ctrl: true }], // ^D
|
|
11
|
+
'ctrl-u': [{ sequence: '\u0015', ctrl: true }], // ^U
|
|
12
|
+
tab: [{ sequence: '\t' }],
|
|
13
|
+
enter: [{ sequence: '\r' }, { sequence: '\n' }],
|
|
14
|
+
backspace: [{ sequence: '\u007f' }, { sequence: '\u0008' }], // DEL, BS (Windows)
|
|
15
|
+
escape: [{ sequence: '\u001b' }],
|
|
17
16
|
// Arrows (ANSI)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
'
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
'
|
|
31
|
-
|
|
32
|
-
|
|
17
|
+
up: [{ sequence: '\u001b[A' }],
|
|
18
|
+
down: [{ sequence: '\u001b[B' }],
|
|
19
|
+
right: [{ sequence: '\u001b[C' }],
|
|
20
|
+
left: [{ sequence: '\u001b[D' }],
|
|
21
|
+
'ctrl-right': [
|
|
22
|
+
{ sequence: '\u001b[1;5C', ctrl: true },
|
|
23
|
+
{ sequence: '\u001b[5C', ctrl: true },
|
|
24
|
+
// Option/Alt-based word jumps (macOS/iTerm send meta-modified arrows or ESC+b/f)
|
|
25
|
+
{ sequence: '\u001b[1;3C', meta: true },
|
|
26
|
+
{ sequence: '\u001b[1;9C', meta: true },
|
|
27
|
+
{ sequence: '\u001bf', meta: true },
|
|
28
|
+
],
|
|
29
|
+
'ctrl-left': [
|
|
30
|
+
{ sequence: '\u001b[1;5D', ctrl: true },
|
|
31
|
+
{ sequence: '\u001b[5D', ctrl: true },
|
|
32
|
+
// Option/Alt-based word jumps (macOS/iTerm send meta-modified arrows or ESC+b/f)
|
|
33
|
+
{ sequence: '\u001b[1;3D', meta: true },
|
|
34
|
+
{ sequence: '\u001b[1;9D', meta: true },
|
|
35
|
+
{ sequence: '\u001bb', meta: true },
|
|
36
|
+
],
|
|
33
37
|
// Home/End/Delete variants
|
|
34
|
-
'\u001b[H'
|
|
35
|
-
'\u001b[F'
|
|
36
|
-
|
|
37
|
-
'\u001b[4~': { name: 'end' },
|
|
38
|
-
'\u001b[3~': { name: 'delete' },
|
|
38
|
+
home: [{ sequence: '\u001b[H' }, { sequence: '\u001b[1~' }],
|
|
39
|
+
end: [{ sequence: '\u001b[F' }, { sequence: '\u001b[4~' }],
|
|
40
|
+
delete: [{ sequence: '\u001b[3~' }],
|
|
39
41
|
// Focus in/out (sent by some terminals on focus change - swallow these)
|
|
40
|
-
'
|
|
41
|
-
'
|
|
42
|
+
'focus-in': [{ sequence: '\u001b[I' }],
|
|
43
|
+
'focus-out': [{ sequence: '\u001b[O' }],
|
|
42
44
|
};
|
|
45
|
+
exports.KEY_SEQUENCES = Object.fromEntries(Object.entries(KEY_DEFINITIONS).map(([name, defs]) => [
|
|
46
|
+
name,
|
|
47
|
+
defs.map((def) => def.sequence),
|
|
48
|
+
]));
|
|
49
|
+
function keySequence(name) {
|
|
50
|
+
return exports.KEY_SEQUENCES[name][0];
|
|
51
|
+
}
|
|
52
|
+
// Map escape/control sequences to semantic key names
|
|
53
|
+
const KEYMAP = {};
|
|
54
|
+
for (const [name, defs] of Object.entries(KEY_DEFINITIONS)) {
|
|
55
|
+
for (const def of defs) {
|
|
56
|
+
KEYMAP[def.sequence] = {
|
|
57
|
+
name,
|
|
58
|
+
ctrl: def.ctrl,
|
|
59
|
+
meta: def.meta,
|
|
60
|
+
shift: def.shift,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
43
64
|
// For efficient prefix checks
|
|
44
65
|
const KEY_PREFIXES = new Set();
|
|
45
66
|
for (const seq of Object.keys(KEYMAP)) {
|
package/dist/spawner.js
CHANGED
|
@@ -7,6 +7,18 @@ const logs_1 = require("./logs");
|
|
|
7
7
|
const isWin = process.platform === "win32";
|
|
8
8
|
const shellBinary = isWin ? "cmd.exe" : "/bin/bash";
|
|
9
9
|
const shellArgs = isWin ? ["/d", "/s", "/c"] : ["-lc"];
|
|
10
|
+
const dirStack = [];
|
|
11
|
+
// Track last-known cwd per drive so `E:` switches like cmd.exe
|
|
12
|
+
// into the folder you were in before switching drives.
|
|
13
|
+
const driveCwds = {};
|
|
14
|
+
function updateDriveCwd(cwd = process.cwd()) {
|
|
15
|
+
if (!isWin)
|
|
16
|
+
return;
|
|
17
|
+
const drive = cwd.slice(0, 2).toUpperCase();
|
|
18
|
+
if (/^[A-Z]:$/.test(drive)) {
|
|
19
|
+
driveCwds[drive] = cwd;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
10
22
|
const builtInCommands = {
|
|
11
23
|
cd: async (args) => {
|
|
12
24
|
if (args.length === 1) {
|
|
@@ -16,6 +28,7 @@ const builtInCommands = {
|
|
|
16
28
|
const dest = expandVars(args[1]);
|
|
17
29
|
try {
|
|
18
30
|
process.chdir(dest);
|
|
31
|
+
updateDriveCwd();
|
|
19
32
|
}
|
|
20
33
|
catch (err) {
|
|
21
34
|
process.stderr.write(`cd: ${err.message}\n`);
|
|
@@ -23,11 +36,66 @@ const builtInCommands = {
|
|
|
23
36
|
}
|
|
24
37
|
return true;
|
|
25
38
|
},
|
|
39
|
+
pushd: async (args) => {
|
|
40
|
+
const current = process.cwd();
|
|
41
|
+
if (args.length === 1) {
|
|
42
|
+
const next = dirStack.shift();
|
|
43
|
+
if (!next) {
|
|
44
|
+
process.stderr.write("pushd: no other directory\n");
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
dirStack.unshift(current);
|
|
48
|
+
try {
|
|
49
|
+
process.chdir(next);
|
|
50
|
+
updateDriveCwd();
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
process.stderr.write(`pushd: ${err.message}\n`);
|
|
54
|
+
dirStack.shift();
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
writeDirStack();
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
const dest = expandVars(args[1]);
|
|
61
|
+
try {
|
|
62
|
+
process.chdir(dest);
|
|
63
|
+
updateDriveCwd();
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
process.stderr.write(`pushd: ${err.message}\n`);
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
dirStack.unshift(current);
|
|
70
|
+
writeDirStack();
|
|
71
|
+
return true;
|
|
72
|
+
},
|
|
73
|
+
popd: async () => {
|
|
74
|
+
const next = dirStack.shift();
|
|
75
|
+
if (!next) {
|
|
76
|
+
process.stderr.write("popd: directory stack empty\n");
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
process.chdir(next);
|
|
81
|
+
updateDriveCwd();
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
process.stderr.write(`popd: ${err.message}\n`);
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
writeDirStack();
|
|
88
|
+
return true;
|
|
89
|
+
},
|
|
26
90
|
exit: async () => {
|
|
27
91
|
(0, process_1.exit)(0);
|
|
28
92
|
return false;
|
|
29
93
|
},
|
|
30
94
|
};
|
|
95
|
+
function writeDirStack() {
|
|
96
|
+
const parts = [process.cwd(), ...dirStack];
|
|
97
|
+
process.stdout.write(parts.join(" ") + "\n");
|
|
98
|
+
}
|
|
31
99
|
function expandVars(input) {
|
|
32
100
|
let out = input;
|
|
33
101
|
if (isWin) {
|
|
@@ -52,6 +120,20 @@ async function runUserCommand(command) {
|
|
|
52
120
|
const trimmed = command.trim();
|
|
53
121
|
if (!trimmed)
|
|
54
122
|
return false;
|
|
123
|
+
if (isWin && /^[a-zA-Z]:$/.test(trimmed)) {
|
|
124
|
+
// Windows drive switch (eg "E:") should restore that drive's last cwd.
|
|
125
|
+
const drive = trimmed.toUpperCase();
|
|
126
|
+
const target = driveCwds[drive] ?? `${drive}\\`;
|
|
127
|
+
try {
|
|
128
|
+
process.chdir(target);
|
|
129
|
+
updateDriveCwd();
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
process.stderr.write(`${trimmed}: ${err.message}\n`);
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
55
137
|
const args = command.split(/\s+/);
|
|
56
138
|
if (typeof args[0] === "string" && builtInCommands[args[0]]) {
|
|
57
139
|
return await builtInCommands[args[0]](args);
|
|
@@ -70,3 +152,4 @@ async function runUserCommand(command) {
|
|
|
70
152
|
// many times until we fix it.
|
|
71
153
|
return true;
|
|
72
154
|
}
|
|
155
|
+
updateDriveCwd();
|
package/dist/terminal.js
CHANGED
|
@@ -29,6 +29,13 @@ class Terminal {
|
|
|
29
29
|
enableWrites() {
|
|
30
30
|
this.writesDisabled = false;
|
|
31
31
|
}
|
|
32
|
+
reset() {
|
|
33
|
+
// Some apps (such as vim) change the terminal cursor mode.
|
|
34
|
+
// We need to reset it to the default. To avoid arrow keys causing this:
|
|
35
|
+
// $> OAOBOCODODODODOAOAOCOB
|
|
36
|
+
const RESET_CURSOR_MODE = "\x1b[?1l";
|
|
37
|
+
this.write(RESET_CURSOR_MODE);
|
|
38
|
+
}
|
|
32
39
|
canWrite() {
|
|
33
40
|
return !this.writesDisabled;
|
|
34
41
|
}
|