@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/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"}
@@ -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": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }