@oakoliver/bubblezone 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +185 -2
- package/dist/global.d.ts +33 -0
- package/dist/global.d.ts.map +1 -0
- package/dist/index.cjs +436 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +381 -0
- package/dist/manager.d.ts +64 -0
- package/dist/manager.d.ts.map +1 -0
- package/dist/messages.d.ts +19 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/scanner.d.ts +19 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/width.d.ts +17 -0
- package/dist/width.d.ts.map +1 -0
- package/dist/zoneinfo.d.ts +51 -0
- package/dist/zoneinfo.d.ts.map +1 -0
- package/package.json +68 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
// src/manager.ts
|
|
2
|
+
import { Batch } from "@oakoliver/bubbletea";
|
|
3
|
+
|
|
4
|
+
// src/messages.ts
|
|
5
|
+
var MsgZoneInBounds = class {
|
|
6
|
+
constructor(zone, event) {
|
|
7
|
+
this.zone = zone;
|
|
8
|
+
this.event = event;
|
|
9
|
+
}
|
|
10
|
+
zone;
|
|
11
|
+
event;
|
|
12
|
+
_tag = "MsgZoneInBounds";
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// src/zoneinfo.ts
|
|
16
|
+
function position(msg) {
|
|
17
|
+
const m = msg;
|
|
18
|
+
if (typeof m.mouse === "function") return m.mouse();
|
|
19
|
+
return { x: m.x, y: m.y };
|
|
20
|
+
}
|
|
21
|
+
var ZoneInfo = class {
|
|
22
|
+
/** The ID passed to `mark()`; empty for a zone that is not known yet. */
|
|
23
|
+
id;
|
|
24
|
+
/** @internal The generated marker sequence that stands for `id` in a view. */
|
|
25
|
+
rid;
|
|
26
|
+
/** @internal The scan this zone came from, used to drop stale zones. */
|
|
27
|
+
iteration;
|
|
28
|
+
/** X of the top-left cell of the zone. */
|
|
29
|
+
startX;
|
|
30
|
+
/** Y of the top-left cell of the zone. */
|
|
31
|
+
startY;
|
|
32
|
+
/** X of the bottom-right cell of the zone. */
|
|
33
|
+
endX;
|
|
34
|
+
/** Y of the bottom-right cell of the zone. */
|
|
35
|
+
endY;
|
|
36
|
+
constructor(init = {}) {
|
|
37
|
+
this.id = init.id ?? "";
|
|
38
|
+
this.rid = init.rid ?? "";
|
|
39
|
+
this.iteration = init.iteration ?? 0;
|
|
40
|
+
this.startX = init.startX ?? 0;
|
|
41
|
+
this.startY = init.startY ?? 0;
|
|
42
|
+
this.endX = init.endX ?? 0;
|
|
43
|
+
this.endY = init.endY ?? 0;
|
|
44
|
+
}
|
|
45
|
+
/** Reports whether the zone is not known (yet). `Manager.get()` returns such a zone for unknown IDs. */
|
|
46
|
+
isZero() {
|
|
47
|
+
return this.id === "";
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Reports whether the mouse event is inside the box between the start and end
|
|
51
|
+
* cells. Unknown zones are never in bounds. A zone whose end lies before its
|
|
52
|
+
* start (for example text that wrapped onto the next line) is never in bounds
|
|
53
|
+
* either; for shapes a box cannot describe, do the check yourself.
|
|
54
|
+
*/
|
|
55
|
+
inBounds(msg) {
|
|
56
|
+
if (this.isZero()) return false;
|
|
57
|
+
if (this.startX > this.endX || this.startY > this.endY) return false;
|
|
58
|
+
const { x, y } = position(msg);
|
|
59
|
+
if (x < this.startX || y < this.startY) return false;
|
|
60
|
+
if (x > this.endX || y > this.endY) return false;
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Returns the event position relative to the zone, with (0, 0) at its top-left
|
|
65
|
+
* cell, or `[-1, -1]` if the zone is unknown or the event is outside it.
|
|
66
|
+
*/
|
|
67
|
+
pos(msg) {
|
|
68
|
+
if (this.isZero() || !this.inBounds(msg)) return [-1, -1];
|
|
69
|
+
const { x, y } = position(msg);
|
|
70
|
+
return [x - this.startX, y - this.startY];
|
|
71
|
+
}
|
|
72
|
+
/** Width in cells (0 for an unknown or inverted zone). */
|
|
73
|
+
get width() {
|
|
74
|
+
return this.isZero() || this.endX < this.startX ? 0 : this.endX - this.startX + 1;
|
|
75
|
+
}
|
|
76
|
+
/** Height in rows (0 for an unknown or inverted zone). */
|
|
77
|
+
get height() {
|
|
78
|
+
return this.isZero() || this.endY < this.startY ? 0 : this.endY - this.startY + 1;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// src/width.ts
|
|
83
|
+
var ESC = 27;
|
|
84
|
+
var BEL = 7;
|
|
85
|
+
var wideEmoji = /^\p{Emoji_Presentation}$/u;
|
|
86
|
+
var zeroWidth = /^[\p{Mn}\p{Me}\p{Cf}]$/u;
|
|
87
|
+
function isWide(cp) {
|
|
88
|
+
return cp >= 4352 && (cp <= 4447 || cp === 9001 || cp === 9002 || cp >= 11904 && cp <= 12350 || cp >= 12353 && cp <= 13311 || cp >= 13312 && cp <= 19903 || cp >= 19968 && cp <= 42191 || cp >= 43360 && cp <= 43391 || cp >= 44032 && cp <= 55203 || cp >= 63744 && cp <= 64255 || cp >= 65040 && cp <= 65049 || cp >= 65072 && cp <= 65135 || cp >= 65280 && cp <= 65376 || cp >= 65504 && cp <= 65510 || cp >= 94176 && cp <= 94180 || cp >= 94208 && cp <= 101631 || cp >= 110592 && cp <= 111359 || cp >= 131072 && cp <= 262141);
|
|
89
|
+
}
|
|
90
|
+
function runeWidth(cp) {
|
|
91
|
+
if (cp < 32 || cp >= 127 && cp < 160) return 0;
|
|
92
|
+
if (cp < 768) return 1;
|
|
93
|
+
if (cp === 8203 || cp === 8205 || cp >= 65024 && cp <= 65039) return 0;
|
|
94
|
+
if (isWide(cp)) return 2;
|
|
95
|
+
if (zeroWidth.test(String.fromCodePoint(cp))) return 0;
|
|
96
|
+
if (cp >= 8986 && wideEmoji.test(String.fromCodePoint(cp))) return 2;
|
|
97
|
+
return 1;
|
|
98
|
+
}
|
|
99
|
+
var plain = /^[\x20-\x7e]*$/;
|
|
100
|
+
function printableWidth(s) {
|
|
101
|
+
if (plain.test(s)) return s.length;
|
|
102
|
+
let n = 0;
|
|
103
|
+
for (let i = 0; i < s.length; ) {
|
|
104
|
+
const cp = s.codePointAt(i);
|
|
105
|
+
const len = cp > 65535 ? 2 : 1;
|
|
106
|
+
if (cp !== ESC) {
|
|
107
|
+
n += runeWidth(cp);
|
|
108
|
+
i += len;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const next = s.charCodeAt(i + 1);
|
|
112
|
+
if (Number.isNaN(next)) break;
|
|
113
|
+
if (next === 91) {
|
|
114
|
+
i += 2;
|
|
115
|
+
while (i < s.length) {
|
|
116
|
+
const c = s.charCodeAt(i++);
|
|
117
|
+
if (c >= 64 && c <= 126) break;
|
|
118
|
+
}
|
|
119
|
+
} else if (next === 93 || next === 80 || next === 95 || next === 94 || next === 88) {
|
|
120
|
+
i += 2;
|
|
121
|
+
while (i < s.length) {
|
|
122
|
+
const c = s.charCodeAt(i);
|
|
123
|
+
if (c === BEL) {
|
|
124
|
+
i++;
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
if (c === ESC && s.charCodeAt(i + 1) === 92) {
|
|
128
|
+
i += 2;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
i++;
|
|
132
|
+
}
|
|
133
|
+
} else if (next >= 32 && next <= 47) {
|
|
134
|
+
i += 2;
|
|
135
|
+
while (i < s.length && s.charCodeAt(i) >= 32 && s.charCodeAt(i) <= 47) i++;
|
|
136
|
+
i++;
|
|
137
|
+
} else {
|
|
138
|
+
i += 2;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return n;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/scanner.ts
|
|
145
|
+
var tokens = /\x1b\[(\d+)z|\n/g;
|
|
146
|
+
function scanView(host, input, iteration) {
|
|
147
|
+
if (input.indexOf("\x1B[") === -1 && input.indexOf("\n") === -1) return input;
|
|
148
|
+
let out = "";
|
|
149
|
+
let line = "";
|
|
150
|
+
let rows = 0;
|
|
151
|
+
let last = 0;
|
|
152
|
+
const tracked = /* @__PURE__ */ new Map();
|
|
153
|
+
tokens.lastIndex = 0;
|
|
154
|
+
for (let m = tokens.exec(input); m !== null; m = tokens.exec(input)) {
|
|
155
|
+
line += input.slice(last, m.index);
|
|
156
|
+
last = m.index + m[0].length;
|
|
157
|
+
if (m[0] === "\n") {
|
|
158
|
+
out += line + "\n";
|
|
159
|
+
line = "";
|
|
160
|
+
rows++;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (!host.enabled) continue;
|
|
164
|
+
const rid = m[0];
|
|
165
|
+
const id = host.resolve(rid);
|
|
166
|
+
if (id === void 0) continue;
|
|
167
|
+
const open = tracked.get(rid);
|
|
168
|
+
if (open) {
|
|
169
|
+
open.endX = printableWidth(line) - 1;
|
|
170
|
+
open.endY = rows;
|
|
171
|
+
host.emit(open);
|
|
172
|
+
tracked.delete(rid);
|
|
173
|
+
} else {
|
|
174
|
+
tracked.set(rid, new ZoneInfo({ id, rid, iteration, startX: printableWidth(line), startY: rows }));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return out + line + input.slice(last);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/manager.ts
|
|
181
|
+
var IDENT_START = "\x1B";
|
|
182
|
+
var IDENT_BRACKET = "[";
|
|
183
|
+
var IDENT_END = "z";
|
|
184
|
+
var markerCounter = 1e3;
|
|
185
|
+
var prefixCounter = 0;
|
|
186
|
+
var scanCounter = 0;
|
|
187
|
+
var Manager = class {
|
|
188
|
+
#enabled = true;
|
|
189
|
+
#closed = false;
|
|
190
|
+
#zones = /* @__PURE__ */ new Map();
|
|
191
|
+
#ids = /* @__PURE__ */ new Map();
|
|
192
|
+
// user ID -> marker sequence
|
|
193
|
+
#rids = /* @__PURE__ */ new Map();
|
|
194
|
+
// marker sequence -> user ID
|
|
195
|
+
/** Stops the manager. Later scans still strip markers but no longer store zones. */
|
|
196
|
+
close() {
|
|
197
|
+
this.#closed = true;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Enables or disables the manager. When disabled, `mark()` returns its input
|
|
201
|
+
* unchanged and `scan()` only strips markers (from views cached earlier).
|
|
202
|
+
* Disabling also forgets every stored zone.
|
|
203
|
+
*/
|
|
204
|
+
setEnabled(enabled) {
|
|
205
|
+
this.#enabled = enabled;
|
|
206
|
+
if (!enabled) this.#zones.clear();
|
|
207
|
+
}
|
|
208
|
+
/** Reports whether the manager is enabled. */
|
|
209
|
+
enabled() {
|
|
210
|
+
return this.#enabled;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Returns a prefix that is unique across calls, for components that build zone
|
|
214
|
+
* IDs of their own and must not collide with a sibling instance:
|
|
215
|
+
* `zone.mark(this.id + 'item-3', row)`.
|
|
216
|
+
*/
|
|
217
|
+
newPrefix() {
|
|
218
|
+
return "zone_" + String(++prefixCounter) + "__";
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Wraps `v` in a start and end marker so `scan()` can find where it ended up
|
|
222
|
+
* on screen. Returns `v` unchanged when the manager is disabled or when `id`
|
|
223
|
+
* or `v` is empty.
|
|
224
|
+
*/
|
|
225
|
+
mark(id, v) {
|
|
226
|
+
if (!this.#enabled) return v;
|
|
227
|
+
if (id === "" || v === "") return v;
|
|
228
|
+
let gid = this.#ids.get(id);
|
|
229
|
+
if (gid === void 0) {
|
|
230
|
+
gid = IDENT_START + IDENT_BRACKET + String(++markerCounter) + IDENT_END;
|
|
231
|
+
this.#ids.set(id, gid);
|
|
232
|
+
this.#rids.set(gid, id);
|
|
233
|
+
}
|
|
234
|
+
return gid + v + gid;
|
|
235
|
+
}
|
|
236
|
+
/** Forgets the stored zone for `id`. It is recorded again by the next scan that sees it. */
|
|
237
|
+
clear(id) {
|
|
238
|
+
this.#zones.delete(id);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Returns the zone stored for `id`. For an ID that is not known (yet) it
|
|
242
|
+
* returns a zero zone whose `isZero()` is true and whose `inBounds()` is
|
|
243
|
+
* false, so `get('x').inBounds(msg)` is always safe to call.
|
|
244
|
+
*/
|
|
245
|
+
get(id) {
|
|
246
|
+
return this.#zones.get(id) ?? new ZoneInfo();
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Strips the markers out of a view and records where each zone is. Call it
|
|
250
|
+
* once, on the outermost view, not inside child components.
|
|
251
|
+
*
|
|
252
|
+
* Unlike upstream, which hands the zones to a background worker, the zones are
|
|
253
|
+
* stored before `scan()` returns, so `get()` right afterwards sees them.
|
|
254
|
+
*/
|
|
255
|
+
scan(v) {
|
|
256
|
+
const iteration = ++scanCounter;
|
|
257
|
+
const found = [];
|
|
258
|
+
const out = scanView(
|
|
259
|
+
{
|
|
260
|
+
enabled: this.#enabled,
|
|
261
|
+
resolve: (rid) => this.#rids.get(rid),
|
|
262
|
+
emit: (zone) => found.push(zone)
|
|
263
|
+
},
|
|
264
|
+
v,
|
|
265
|
+
iteration
|
|
266
|
+
);
|
|
267
|
+
if (this.#closed) return out;
|
|
268
|
+
for (const zone of found) this.#zones.set(zone.id, zone);
|
|
269
|
+
for (const [id, zone] of this.#zones) {
|
|
270
|
+
if (zone.iteration !== iteration) this.#zones.delete(id);
|
|
271
|
+
}
|
|
272
|
+
return out;
|
|
273
|
+
}
|
|
274
|
+
#findInBounds(mouse) {
|
|
275
|
+
const ids = [...this.#zones.keys()].sort();
|
|
276
|
+
const hit = [];
|
|
277
|
+
for (const id of ids) {
|
|
278
|
+
const zone = this.#zones.get(id);
|
|
279
|
+
if (zone.inBounds(mouse)) hit.push(zone);
|
|
280
|
+
}
|
|
281
|
+
return hit;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Sends a `MsgZoneInBounds` to `model.update()` for every zone the mouse event
|
|
285
|
+
* is inside, in alphabetical order of their IDs. The results of `update()` are
|
|
286
|
+
* discarded.
|
|
287
|
+
*/
|
|
288
|
+
anyInBounds(model, mouse) {
|
|
289
|
+
for (const zone of this.#findInBounds(mouse)) {
|
|
290
|
+
model.update(new MsgZoneInBounds(zone, mouse));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Like `anyInBounds()`, but the model returned by each `update()` is passed to
|
|
295
|
+
* the next one and returned at the end; the commands are combined with `Batch`.
|
|
296
|
+
*/
|
|
297
|
+
anyInBoundsAndUpdate(model, mouse) {
|
|
298
|
+
const cmds = [];
|
|
299
|
+
for (const zone of this.#findInBounds(mouse)) {
|
|
300
|
+
let cmd;
|
|
301
|
+
[model, cmd] = model.update(new MsgZoneInBounds(zone, mouse));
|
|
302
|
+
cmds.push(cmd);
|
|
303
|
+
}
|
|
304
|
+
return [model, Batch(...cmds)];
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
function New() {
|
|
308
|
+
return new Manager();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// src/global.ts
|
|
312
|
+
var DefaultManager;
|
|
313
|
+
function manager() {
|
|
314
|
+
if (!DefaultManager) throw new Error("manager not initialized");
|
|
315
|
+
return DefaultManager;
|
|
316
|
+
}
|
|
317
|
+
function NewGlobal() {
|
|
318
|
+
if (DefaultManager) return;
|
|
319
|
+
DefaultManager = New();
|
|
320
|
+
}
|
|
321
|
+
function Close() {
|
|
322
|
+
manager().close();
|
|
323
|
+
}
|
|
324
|
+
function SetEnabled(v) {
|
|
325
|
+
manager().setEnabled(v);
|
|
326
|
+
}
|
|
327
|
+
function Enabled() {
|
|
328
|
+
return manager().enabled();
|
|
329
|
+
}
|
|
330
|
+
function NewPrefix() {
|
|
331
|
+
return manager().newPrefix();
|
|
332
|
+
}
|
|
333
|
+
function Mark(id, v) {
|
|
334
|
+
return manager().mark(id, v);
|
|
335
|
+
}
|
|
336
|
+
function Clear(id) {
|
|
337
|
+
manager().clear(id);
|
|
338
|
+
}
|
|
339
|
+
function Get(id) {
|
|
340
|
+
return manager().get(id);
|
|
341
|
+
}
|
|
342
|
+
function Scan(v) {
|
|
343
|
+
return manager().scan(v);
|
|
344
|
+
}
|
|
345
|
+
function AnyInBounds(model, mouse) {
|
|
346
|
+
manager().anyInBounds(model, mouse);
|
|
347
|
+
}
|
|
348
|
+
function AnyInBoundsAndUpdate(model, mouse) {
|
|
349
|
+
return manager().anyInBoundsAndUpdate(model, mouse);
|
|
350
|
+
}
|
|
351
|
+
export {
|
|
352
|
+
AnyInBounds,
|
|
353
|
+
AnyInBoundsAndUpdate,
|
|
354
|
+
Clear,
|
|
355
|
+
Close,
|
|
356
|
+
DefaultManager,
|
|
357
|
+
Enabled,
|
|
358
|
+
Get,
|
|
359
|
+
Manager,
|
|
360
|
+
Mark,
|
|
361
|
+
MsgZoneInBounds,
|
|
362
|
+
New,
|
|
363
|
+
NewGlobal,
|
|
364
|
+
NewPrefix,
|
|
365
|
+
Scan,
|
|
366
|
+
SetEnabled,
|
|
367
|
+
ZoneInfo,
|
|
368
|
+
AnyInBounds as anyInBounds,
|
|
369
|
+
AnyInBoundsAndUpdate as anyInBoundsAndUpdate,
|
|
370
|
+
Clear as clear,
|
|
371
|
+
Close as close,
|
|
372
|
+
Enabled as enabled,
|
|
373
|
+
Get as get,
|
|
374
|
+
Mark as mark,
|
|
375
|
+
NewGlobal as newGlobal,
|
|
376
|
+
NewPrefix as newPrefix,
|
|
377
|
+
printableWidth,
|
|
378
|
+
runeWidth,
|
|
379
|
+
Scan as scan,
|
|
380
|
+
SetEnabled as setEnabled
|
|
381
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Cmd, Model, MouseMsg } from '@oakoliver/bubbletea';
|
|
2
|
+
import { ZoneInfo } from './zoneinfo.js';
|
|
3
|
+
/**
|
|
4
|
+
* Parses zone information from the output of your components and stores it for
|
|
5
|
+
* later bounds checks. Enabled by default.
|
|
6
|
+
*/
|
|
7
|
+
export declare class Manager {
|
|
8
|
+
#private;
|
|
9
|
+
/** Stops the manager. Later scans still strip markers but no longer store zones. */
|
|
10
|
+
close(): void;
|
|
11
|
+
/**
|
|
12
|
+
* Enables or disables the manager. When disabled, `mark()` returns its input
|
|
13
|
+
* unchanged and `scan()` only strips markers (from views cached earlier).
|
|
14
|
+
* Disabling also forgets every stored zone.
|
|
15
|
+
*/
|
|
16
|
+
setEnabled(enabled: boolean): void;
|
|
17
|
+
/** Reports whether the manager is enabled. */
|
|
18
|
+
enabled(): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Returns a prefix that is unique across calls, for components that build zone
|
|
21
|
+
* IDs of their own and must not collide with a sibling instance:
|
|
22
|
+
* `zone.mark(this.id + 'item-3', row)`.
|
|
23
|
+
*/
|
|
24
|
+
newPrefix(): string;
|
|
25
|
+
/**
|
|
26
|
+
* Wraps `v` in a start and end marker so `scan()` can find where it ended up
|
|
27
|
+
* on screen. Returns `v` unchanged when the manager is disabled or when `id`
|
|
28
|
+
* or `v` is empty.
|
|
29
|
+
*/
|
|
30
|
+
mark(id: string, v: string): string;
|
|
31
|
+
/** Forgets the stored zone for `id`. It is recorded again by the next scan that sees it. */
|
|
32
|
+
clear(id: string): void;
|
|
33
|
+
/**
|
|
34
|
+
* Returns the zone stored for `id`. For an ID that is not known (yet) it
|
|
35
|
+
* returns a zero zone whose `isZero()` is true and whose `inBounds()` is
|
|
36
|
+
* false, so `get('x').inBounds(msg)` is always safe to call.
|
|
37
|
+
*/
|
|
38
|
+
get(id: string): ZoneInfo;
|
|
39
|
+
/**
|
|
40
|
+
* Strips the markers out of a view and records where each zone is. Call it
|
|
41
|
+
* once, on the outermost view, not inside child components.
|
|
42
|
+
*
|
|
43
|
+
* Unlike upstream, which hands the zones to a background worker, the zones are
|
|
44
|
+
* stored before `scan()` returns, so `get()` right afterwards sees them.
|
|
45
|
+
*/
|
|
46
|
+
scan(v: string): string;
|
|
47
|
+
/**
|
|
48
|
+
* Sends a `MsgZoneInBounds` to `model.update()` for every zone the mouse event
|
|
49
|
+
* is inside, in alphabetical order of their IDs. The results of `update()` are
|
|
50
|
+
* discarded.
|
|
51
|
+
*/
|
|
52
|
+
anyInBounds(model: Model, mouse: MouseMsg): void;
|
|
53
|
+
/**
|
|
54
|
+
* Like `anyInBounds()`, but the model returned by each `update()` is passed to
|
|
55
|
+
* the next one and returned at the end; the commands are combined with `Batch`.
|
|
56
|
+
*/
|
|
57
|
+
anyInBoundsAndUpdate(model: Model, mouse: MouseMsg): [Model, Cmd];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Creates a manager that is not global. Give it to the components that mark
|
|
61
|
+
* zones and to the root model that scans.
|
|
62
|
+
*/
|
|
63
|
+
export declare function New(): Manager;
|
|
64
|
+
//# sourceMappingURL=manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../src/manager.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAGjE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAezC;;;GAGG;AACH,qBAAa,OAAO;;IAOlB,oFAAoF;IACpF,KAAK,IAAI,IAAI;IAIb;;;;OAIG;IACH,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAKlC,8CAA8C;IAC9C,OAAO,IAAI,OAAO;IAIlB;;;;OAIG;IACH,SAAS,IAAI,MAAM;IAInB;;;;OAIG;IACH,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM;IAanC,4FAA4F;IAC5F,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAIvB;;;;OAIG;IACH,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,QAAQ;IAIzB;;;;;;OAMG;IACH,IAAI,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM;IAkCvB;;;;OAIG;IACH,WAAW,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,GAAG,IAAI;IAMhD;;;OAGG;IACH,oBAAoB,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC;CASlE;AAED;;;GAGG;AACH,wBAAgB,GAAG,IAAI,OAAO,CAE7B"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { MouseMsg } from '@oakoliver/bubbletea';
|
|
2
|
+
import type { ZoneInfo } from './zoneinfo.js';
|
|
3
|
+
/**
|
|
4
|
+
* Sent to a model by `anyInBounds()` for each zone a mouse event landed in.
|
|
5
|
+
* Recognised by `_tag`, like the Bubble Tea messages.
|
|
6
|
+
*/
|
|
7
|
+
export declare class MsgZoneInBounds {
|
|
8
|
+
/** The zone that is in bounds. */
|
|
9
|
+
readonly zone: ZoneInfo;
|
|
10
|
+
/** The mouse event that caused it. */
|
|
11
|
+
readonly event: MouseMsg;
|
|
12
|
+
readonly _tag: "MsgZoneInBounds";
|
|
13
|
+
constructor(
|
|
14
|
+
/** The zone that is in bounds. */
|
|
15
|
+
zone: ZoneInfo,
|
|
16
|
+
/** The mouse event that caused it. */
|
|
17
|
+
event: MouseMsg);
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=messages.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C;;;GAGG;AACH,qBAAa,eAAe;IAIxB,kCAAkC;aAClB,IAAI,EAAE,QAAQ;IAC9B,sCAAsC;aACtB,KAAK,EAAE,QAAQ;IANjC,QAAQ,CAAC,IAAI,EAAG,iBAAiB,CAAU;;IAGzC,kCAAkC;IAClB,IAAI,EAAE,QAAQ;IAC9B,sCAAsC;IACtB,KAAK,EAAE,QAAQ;CAElC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ZoneInfo } from './zoneinfo.js';
|
|
2
|
+
/** Callbacks the scanner uses to talk to its manager. */
|
|
3
|
+
export interface ScanHost {
|
|
4
|
+
/** Reports whether markers should be recorded (otherwise they are only stripped). */
|
|
5
|
+
readonly enabled: boolean;
|
|
6
|
+
/** Maps a marker sequence back to the ID given to `mark()`, or undefined if this manager did not create it. */
|
|
7
|
+
resolve(rid: string): string | undefined;
|
|
8
|
+
/** Called for each completed zone. */
|
|
9
|
+
emit(zone: ZoneInfo): void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Strips every marker out of `input` and reports each zone whose start and end
|
|
13
|
+
* markers were both found. Returns the cleaned view.
|
|
14
|
+
*
|
|
15
|
+
* Positions are measured on the cleaned text: columns are the printable width
|
|
16
|
+
* of everything since the last newline, rows count newlines.
|
|
17
|
+
*/
|
|
18
|
+
export declare function scanView(host: ScanHost, input: string, iteration: number): string;
|
|
19
|
+
//# sourceMappingURL=scanner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../src/scanner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAOzC,yDAAyD;AACzD,MAAM,WAAW,QAAQ;IACvB,qFAAqF;IACrF,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,+GAA+G;IAC/G,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IACzC,sCAAsC;IACtC,IAAI,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,CAAC;CAC5B;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAqCjF"}
|
package/dist/width.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cell-width measurement for the scanner.
|
|
3
|
+
*
|
|
4
|
+
* Upstream uses `runewidth.RuneWidth` on every code point of the text in front
|
|
5
|
+
* of a marker and skips ANSI escape sequences by hand. This is the same idea
|
|
6
|
+
* with two differences: OSC/DCS/APC/PM/SOS strings (hyperlinks, titles) are
|
|
7
|
+
* skipped up to their BEL or ST terminator instead of the first letter, and the
|
|
8
|
+
* width table is self-contained (no dependency).
|
|
9
|
+
*/
|
|
10
|
+
/** Printable width of one code point: 0 for controls and combining marks, 2 for wide, else 1. */
|
|
11
|
+
export declare function runeWidth(cp: number): number;
|
|
12
|
+
/**
|
|
13
|
+
* Returns the number of terminal cells `s` occupies when printed, skipping
|
|
14
|
+
* ANSI escape sequences.
|
|
15
|
+
*/
|
|
16
|
+
export declare function printableWidth(s: string): number;
|
|
17
|
+
//# sourceMappingURL=width.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"width.d.ts","sourceRoot":"","sources":["../src/width.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAiCH,iGAAiG;AACjG,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAQ5C;AAKD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CA+ChD"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Mouse, MouseMsg } from '@oakoliver/bubbletea';
|
|
2
|
+
/** Something that carries a mouse position: a Bubble Tea `MouseMsg` or a plain `{ x, y }`. */
|
|
3
|
+
export type MouseLike = MouseMsg | Pick<Mouse, 'x' | 'y'>;
|
|
4
|
+
/**
|
|
5
|
+
* Holds the start and end cells of a zone. All coordinates are zero-based and
|
|
6
|
+
* inclusive: `(endX, endY)` is the bottom-right cell that belongs to the zone.
|
|
7
|
+
*/
|
|
8
|
+
export declare class ZoneInfo {
|
|
9
|
+
/** The ID passed to `mark()`; empty for a zone that is not known yet. */
|
|
10
|
+
readonly id: string;
|
|
11
|
+
/** @internal The generated marker sequence that stands for `id` in a view. */
|
|
12
|
+
readonly rid: string;
|
|
13
|
+
/** @internal The scan this zone came from, used to drop stale zones. */
|
|
14
|
+
readonly iteration: number;
|
|
15
|
+
/** X of the top-left cell of the zone. */
|
|
16
|
+
startX: number;
|
|
17
|
+
/** Y of the top-left cell of the zone. */
|
|
18
|
+
startY: number;
|
|
19
|
+
/** X of the bottom-right cell of the zone. */
|
|
20
|
+
endX: number;
|
|
21
|
+
/** Y of the bottom-right cell of the zone. */
|
|
22
|
+
endY: number;
|
|
23
|
+
constructor(init?: {
|
|
24
|
+
id?: string;
|
|
25
|
+
rid?: string;
|
|
26
|
+
iteration?: number;
|
|
27
|
+
startX?: number;
|
|
28
|
+
startY?: number;
|
|
29
|
+
endX?: number;
|
|
30
|
+
endY?: number;
|
|
31
|
+
});
|
|
32
|
+
/** Reports whether the zone is not known (yet). `Manager.get()` returns such a zone for unknown IDs. */
|
|
33
|
+
isZero(): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Reports whether the mouse event is inside the box between the start and end
|
|
36
|
+
* cells. Unknown zones are never in bounds. A zone whose end lies before its
|
|
37
|
+
* start (for example text that wrapped onto the next line) is never in bounds
|
|
38
|
+
* either; for shapes a box cannot describe, do the check yourself.
|
|
39
|
+
*/
|
|
40
|
+
inBounds(msg: MouseLike): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Returns the event position relative to the zone, with (0, 0) at its top-left
|
|
43
|
+
* cell, or `[-1, -1]` if the zone is unknown or the event is outside it.
|
|
44
|
+
*/
|
|
45
|
+
pos(msg: MouseLike): [x: number, y: number];
|
|
46
|
+
/** Width in cells (0 for an unknown or inverted zone). */
|
|
47
|
+
get width(): number;
|
|
48
|
+
/** Height in rows (0 for an unknown or inverted zone). */
|
|
49
|
+
get height(): number;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=zoneinfo.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zoneinfo.d.ts","sourceRoot":"","sources":["../src/zoneinfo.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAE5D,8FAA8F;AAC9F,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,IAAI,CAAC,KAAK,EAAE,GAAG,GAAG,GAAG,CAAC,CAAC;AAQ1D;;;GAGG;AACH,qBAAa,QAAQ;IACnB,yEAAyE;IACzE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,8EAA8E;IAC9E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAE3B,0CAA0C;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;gBAED,IAAI,GAAE;QAChB,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,MAAM,CAAC;KACV;IAUN,wGAAwG;IACxG,MAAM,IAAI,OAAO;IAIjB;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO;IASjC;;;OAGG;IACH,GAAG,CAAC,GAAG,EAAE,SAAS,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,CAAC;IAM3C,0DAA0D;IAC1D,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,0DAA0D;IAC1D,IAAI,MAAM,IAAI,MAAM,CAEnB;CACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,70 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oakoliver/bubblezone",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Mouse hit-zones for Bubble Tea in TypeScript: mark a region of your view, scan once, ask whether a click landed inside it. Port of lrstanley/bubblezone",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build:esm": "esbuild src/index.ts --bundle --format=esm --platform=node --outfile=dist/index.js --target=esnext --external:@oakoliver/bubbletea",
|
|
23
|
+
"build:cjs": "esbuild src/index.ts --bundle --format=cjs --platform=node --outfile=dist/index.cjs --target=esnext --external:@oakoliver/bubbletea",
|
|
24
|
+
"build:types": "tsc --emitDeclarationOnly",
|
|
25
|
+
"build": "npm run build:esm && npm run build:cjs && npm run build:types",
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"typecheck:examples": "tsc -p examples/tsconfig.json",
|
|
28
|
+
"test": "bun test",
|
|
29
|
+
"prepublishOnly": "npm run build"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"tui",
|
|
33
|
+
"terminal",
|
|
34
|
+
"mouse",
|
|
35
|
+
"hit-testing",
|
|
36
|
+
"click",
|
|
37
|
+
"zones",
|
|
38
|
+
"bubbletea",
|
|
39
|
+
"bubblezone",
|
|
40
|
+
"charmbracelet",
|
|
41
|
+
"typescript"
|
|
42
|
+
],
|
|
43
|
+
"author": "Antonio Oliveira <antonio@oakoliver.com> (https://oakoliver.com)",
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "https://github.com/oakoliver/bubblezone"
|
|
48
|
+
},
|
|
49
|
+
"homepage": "https://github.com/oakoliver/bubblezone#readme",
|
|
50
|
+
"bugs": {
|
|
51
|
+
"url": "https://github.com/oakoliver/bubblezone/issues"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"@oakoliver/bubbletea": "^1.2.1",
|
|
55
|
+
"@oakoliver/lipgloss": "^1.1.3"
|
|
56
|
+
},
|
|
57
|
+
"peerDependenciesMeta": {
|
|
58
|
+
"@oakoliver/lipgloss": {
|
|
59
|
+
"optional": true
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@oakoliver/bubbles": "^1.2.2",
|
|
64
|
+
"@oakoliver/bubbletea": "^1.2.1",
|
|
65
|
+
"@oakoliver/lipgloss": "^1.1.3",
|
|
66
|
+
"@types/node": "^20",
|
|
67
|
+
"esbuild": "^0.28.2",
|
|
68
|
+
"typescript": "^5"
|
|
69
|
+
}
|
|
70
|
+
}
|