@jals1212/pi-spaces-cli 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/dist/cli.js +2 -2
- package/dist/picker.js +102 -18
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,27 @@ commands and options and a patch version fixes.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.7.1
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- A bare `pi-space` with `--setup`: a space of another profile now starts with
|
|
14
|
+
that `--setup` (as `PI_SPACES_SETUP`), as the spaces of the current profile
|
|
15
|
+
already did.
|
|
16
|
+
- A failure starting a space of another profile (a command with an unclosed
|
|
17
|
+
quote, a program that cannot start) is reported with the profile's command
|
|
18
|
+
and exits 2 or 1, with a test for each.
|
|
19
|
+
- The picker puts the terminal back (raw mode off, cursor shown, picker
|
|
20
|
+
erased) on SIGTERM, SIGHUP and SIGINT, then ends the process with that
|
|
21
|
+
signal, instead of leaving the terminal raw.
|
|
22
|
+
- An escape sequence a terminal splits after its start (`ESC [`, `ESC O`, or
|
|
23
|
+
`ESC [` with parameters) is joined with its rest instead of being read as Esc
|
|
24
|
+
and text. If the rest never comes, a lone Esc still cancels and any other
|
|
25
|
+
half sequence is ignored.
|
|
26
|
+
- Lines with wide characters (Chinese, Japanese, Korean, fullwidth, emoji)
|
|
27
|
+
are cut by the columns they take, so they no longer wrap and break the
|
|
28
|
+
redraw; combining marks take none.
|
|
29
|
+
|
|
9
30
|
## 0.7.0
|
|
10
31
|
|
|
11
32
|
### Changed
|
package/dist/cli.js
CHANGED
|
@@ -1077,7 +1077,7 @@ function pickerChoices(call , settings , callerIo )
|
|
|
1077
1077
|
|
|
1078
1078
|
/**
|
|
1079
1079
|
* A bare `pi-space` on a terminal: the picker, then Pi in the chosen space. A space of another profile starts
|
|
1080
|
-
* with that profile's settings (and the --config of this call). Esc or the end of input exit 0; Ctrl-C exits 130.
|
|
1080
|
+
* with that profile's settings (and the --config and --setup of this call). Esc or the end of input exit 0; Ctrl-C exits 130.
|
|
1081
1081
|
*/
|
|
1082
1082
|
async function pick(call , settings , callerIo ) {
|
|
1083
1083
|
const { dir, commandSource } = settings;
|
|
@@ -1099,7 +1099,7 @@ async function pick(call , settings , callerIo )
|
|
|
1099
1099
|
if (choice.profile === undefined) return launch(choice.name, [], dir, io, commandSource);
|
|
1100
1100
|
let chosen ;
|
|
1101
1101
|
try {
|
|
1102
|
-
chosen = resolveSettings({ ...(call.config !== undefined ? { config: call.config } : {}), profile: choice.profile }, callerIo);
|
|
1102
|
+
chosen = resolveSettings({ ...(call.config !== undefined ? { config: call.config } : {}), ...(call.setup !== undefined ? { setup: call.setup } : {}), profile: choice.profile }, callerIo);
|
|
1103
1103
|
} catch (error) {
|
|
1104
1104
|
if (!(error instanceof ConfigError)) throw error;
|
|
1105
1105
|
io.stderr.write(`pi-space: ${error.message}\n`);
|
package/dist/picker.js
CHANGED
|
@@ -71,6 +71,19 @@ export function parseKeys(chunk ) {
|
|
|
71
71
|
return keys;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* A chunk split into what is complete and the incomplete escape sequence at its end (empty when none): a lone
|
|
76
|
+
* ESC, ESC O, or ESC [ with parameter and intermediate bytes but no final byte yet. A terminal may send the
|
|
77
|
+
* rest of such a sequence in the next chunk.
|
|
78
|
+
*/
|
|
79
|
+
export function splitIncomplete(chunk ) {
|
|
80
|
+
const start = chunk.lastIndexOf("\x1b");
|
|
81
|
+
if (start === -1) return [chunk, ""];
|
|
82
|
+
const tail = chunk.slice(start);
|
|
83
|
+
const incomplete = tail === "\x1b" || tail === "\x1bO" || (tail.startsWith("\x1b[") && /^[\x20-\x3f]*$/.test(tail.slice(2)));
|
|
84
|
+
return incomplete ? [chunk.slice(0, start), tail] : [chunk, ""];
|
|
85
|
+
}
|
|
86
|
+
|
|
74
87
|
/** The items whose name or group holds the query, case-insensitive, in order. */
|
|
75
88
|
export function visibleItems(state ) {
|
|
76
89
|
const query = state.query.toLowerCase();
|
|
@@ -112,11 +125,44 @@ export function step(state , key ) {
|
|
|
112
125
|
|
|
113
126
|
const HINT = "Pick a space: ↑/↓ move, Enter starts Pi, Esc cancels, type to filter";
|
|
114
127
|
|
|
115
|
-
/** The
|
|
128
|
+
/** The terminal columns of one character: 0 for combining marks and joiners, 2 for East Asian wide ones and emoji, else 1. */
|
|
129
|
+
function charWidth(char ) {
|
|
130
|
+
const code = char.codePointAt(0) ;
|
|
131
|
+
if ((code >= 0x200b && code <= 0x200f) || (code >= 0xfe00 && code <= 0xfe0f) || /^[\p{Mn}\p{Me}]$/u.test(char)) return 0;
|
|
132
|
+
const wide =
|
|
133
|
+
(code >= 0x1100 && code <= 0x115f) ||
|
|
134
|
+
(code >= 0x2e80 && code <= 0xa4cf && code !== 0x303f) ||
|
|
135
|
+
(code >= 0xac00 && code <= 0xd7a3) ||
|
|
136
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
137
|
+
(code >= 0xfe30 && code <= 0xfe4f) ||
|
|
138
|
+
(code >= 0xff00 && code <= 0xff60) ||
|
|
139
|
+
(code >= 0xffe0 && code <= 0xffe6) ||
|
|
140
|
+
(code >= 0x1f300 && code <= 0x1f64f) ||
|
|
141
|
+
(code >= 0x1f900 && code <= 0x1f9ff) ||
|
|
142
|
+
(code >= 0x20000 && code <= 0x3fffd);
|
|
143
|
+
return wide ? 2 : 1;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The columns a text takes on a terminal. */
|
|
147
|
+
export function displayWidth(text ) {
|
|
148
|
+
let width = 0;
|
|
149
|
+
for (const char of text) width += charWidth(char);
|
|
150
|
+
return width;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The line cut to `columns` terminal columns, ending with "…" when it was longer, so it never wraps. */
|
|
116
154
|
function fit(line , columns ) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
155
|
+
if (displayWidth(line) <= columns) return line;
|
|
156
|
+
// Keep one column for the "…" (none when there is room for just one); a wide character is never cut in half.
|
|
157
|
+
const room = columns <= 1 ? Math.max(0, columns) : columns - 1;
|
|
158
|
+
let kept = "";
|
|
159
|
+
let width = 0;
|
|
160
|
+
for (const char of line) {
|
|
161
|
+
if (width + charWidth(char) > room) break;
|
|
162
|
+
kept += char;
|
|
163
|
+
width += charWidth(char);
|
|
164
|
+
}
|
|
165
|
+
return columns <= 1 ? kept : `${kept}…`;
|
|
120
166
|
}
|
|
121
167
|
|
|
122
168
|
/**
|
|
@@ -158,6 +204,18 @@ function frame(state , rows ) {
|
|
|
158
204
|
|
|
159
205
|
|
|
160
206
|
|
|
207
|
+
/** What runPicker needs of the process: its listeners and the way to raise a signal; the real process by default. */
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
/** The signals that end the picker's process: it restores the terminal, then dies of the same signal. */
|
|
217
|
+
const SIGNALS = ["SIGTERM", "SIGHUP", "SIGINT"] ;
|
|
218
|
+
|
|
161
219
|
/** The chosen item, none when cancelled (Esc, Ctrl-D, the end of input), and whether it was Ctrl-C. */
|
|
162
220
|
|
|
163
221
|
|
|
@@ -170,10 +228,12 @@ const ESCAPE_WAIT_MS = 50;
|
|
|
170
228
|
/**
|
|
171
229
|
* Runs the picker on a terminal: raw mode while it runs, each frame drawn over the last, and everything put
|
|
172
230
|
* back before it returns (raw mode off, the picker erased, the cursor shown, stdin paused) so Pi can start clean.
|
|
173
|
-
* That also holds when drawing throws (the promise rejects) or the process exits while it runs
|
|
174
|
-
*
|
|
231
|
+
* That also holds when drawing throws (the promise rejects) or the process exits while it runs; on SIGTERM,
|
|
232
|
+
* SIGHUP or SIGINT the terminal is restored and the signal raised again, so the process ends as it would have.
|
|
233
|
+
* A chunk ending in an incomplete escape sequence is held for `escapeWaitMs` to join the next chunk; if none
|
|
234
|
+
* comes, a held lone ESC is the Esc key and any other held sequence is dropped.
|
|
175
235
|
*/
|
|
176
|
-
export function runPicker(items , terminal , escapeWaitMs = ESCAPE_WAIT_MS) {
|
|
236
|
+
export function runPicker(items , terminal , escapeWaitMs = ESCAPE_WAIT_MS, host = process) {
|
|
177
237
|
const { stdin, stdout } = terminal;
|
|
178
238
|
let state = initialState(items);
|
|
179
239
|
let drawn = 0;
|
|
@@ -186,17 +246,26 @@ export function runPicker(items , terminal , escapeW
|
|
|
186
246
|
};
|
|
187
247
|
return new Promise ((resolve, reject) => {
|
|
188
248
|
let finished = false;
|
|
189
|
-
//
|
|
249
|
+
// An incomplete escape sequence waiting for the rest of it, and the timer that ends the wait.
|
|
190
250
|
let pending = "";
|
|
191
251
|
let held ;
|
|
192
252
|
const finish = (outcome ) => {
|
|
193
253
|
if (finished) return;
|
|
194
254
|
finished = true;
|
|
195
255
|
clearTimeout(held);
|
|
196
|
-
|
|
256
|
+
stopListening();
|
|
257
|
+
restore();
|
|
258
|
+
if ("error" in outcome) reject(outcome.error);
|
|
259
|
+
else resolve(outcome.result);
|
|
260
|
+
};
|
|
261
|
+
const stopListening = () => {
|
|
262
|
+
host.off("exit", onExit);
|
|
263
|
+
for (const signal of SIGNALS) host.off(signal, onSignal[signal]);
|
|
197
264
|
stdin.off("data", onData);
|
|
198
265
|
stdin.off("end", onEnd);
|
|
199
|
-
|
|
266
|
+
};
|
|
267
|
+
// Restoring must not stop halfway: a write that fails here must not keep the terminal raw.
|
|
268
|
+
const restore = () => {
|
|
200
269
|
try {
|
|
201
270
|
stdin.setRawMode?.(false);
|
|
202
271
|
stdin.pause();
|
|
@@ -207,9 +276,21 @@ export function runPicker(items , terminal , escapeW
|
|
|
207
276
|
try {
|
|
208
277
|
stdout.write("\x1b[?25h");
|
|
209
278
|
} catch {}
|
|
210
|
-
if ("error" in outcome) reject(outcome.error);
|
|
211
|
-
else resolve(outcome.result);
|
|
212
279
|
};
|
|
280
|
+
// A signal: restore, stop listening so its default action applies, then raise it again. The promise never settles.
|
|
281
|
+
const onSignal = Object.fromEntries(
|
|
282
|
+
SIGNALS.map((signal) => [
|
|
283
|
+
signal,
|
|
284
|
+
() => {
|
|
285
|
+
if (finished) return;
|
|
286
|
+
finished = true;
|
|
287
|
+
clearTimeout(held);
|
|
288
|
+
stopListening();
|
|
289
|
+
restore();
|
|
290
|
+
host.kill(host.pid, signal);
|
|
291
|
+
},
|
|
292
|
+
]),
|
|
293
|
+
) ;
|
|
213
294
|
// The process is going away without finish (exit, a throw elsewhere): leave the terminal usable.
|
|
214
295
|
const onExit = () => {
|
|
215
296
|
try {
|
|
@@ -242,20 +323,23 @@ export function runPicker(items , terminal , escapeW
|
|
|
242
323
|
clearTimeout(held);
|
|
243
324
|
const text = pending + data.toString();
|
|
244
325
|
pending = "";
|
|
245
|
-
|
|
326
|
+
const [complete, tail] = splitIncomplete(text);
|
|
327
|
+
if (tail === "") {
|
|
246
328
|
handle(text);
|
|
247
329
|
return;
|
|
248
330
|
}
|
|
249
|
-
//
|
|
250
|
-
if (
|
|
251
|
-
pending =
|
|
331
|
+
// An incomplete sequence at the end may be one the terminal split: hold it for the next chunk.
|
|
332
|
+
if (complete !== "" && handle(complete)) return;
|
|
333
|
+
pending = tail;
|
|
252
334
|
held = setTimeout(() => guarded(() => {
|
|
253
335
|
pending = "";
|
|
254
|
-
|
|
336
|
+
// A lone ESC is the Esc key; any other half sequence is dropped.
|
|
337
|
+
if (tail === "\x1b") handle("\x1b");
|
|
255
338
|
}), escapeWaitMs);
|
|
256
339
|
});
|
|
257
340
|
const onEnd = () => finish({ result: { item: undefined, interrupted: false } });
|
|
258
|
-
|
|
341
|
+
host.on("exit", onExit);
|
|
342
|
+
for (const signal of SIGNALS) host.once(signal, onSignal[signal]);
|
|
259
343
|
guarded(() => {
|
|
260
344
|
stdin.setRawMode?.(true);
|
|
261
345
|
stdout.write("\x1b[?25l");
|
package/package.json
CHANGED