tidepool 1.0.6 â 2.1.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/README.md +231 -71
- package/dist/effects/shadow.d.ts +9 -0
- package/dist/effects/shadow.d.ts.map +1 -0
- package/dist/effects/shadow.js +33 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/input.d.ts +33 -0
- package/dist/input.d.ts.map +1 -0
- package/dist/input.js +191 -0
- package/dist/interfaces.d.ts +40 -1
- package/dist/interfaces.d.ts.map +1 -1
- package/dist/objects/box.d.ts +9 -5
- package/dist/objects/box.d.ts.map +1 -1
- package/dist/objects/box.js +70 -32
- package/dist/objects/button.d.ts +29 -0
- package/dist/objects/button.d.ts.map +1 -0
- package/dist/objects/button.js +80 -0
- package/dist/objects/input.d.ts +38 -0
- package/dist/objects/input.d.ts.map +1 -0
- package/dist/objects/input.js +141 -0
- package/dist/objects/line.d.ts +2 -2
- package/dist/objects/line.d.ts.map +1 -1
- package/dist/objects/line.js +5 -7
- package/dist/objects/progress.d.ts +1 -0
- package/dist/objects/progress.d.ts.map +1 -0
- package/dist/objects/progress.js +1 -0
- package/dist/objects/text.d.ts +4 -2
- package/dist/objects/text.d.ts.map +1 -1
- package/dist/objects/text.js +16 -9
- package/dist/screen.d.ts +17 -7
- package/dist/screen.d.ts.map +1 -1
- package/dist/screen.js +118 -34
- package/dist/util.d.ts +3 -0
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +41 -11
- package/package.json +23 -5
- package/.github/workflows/eslint.yml +0 -50
- package/eslint.config.mjs +0 -38
package/README.md
CHANGED
|
@@ -1,71 +1,231 @@
|
|
|
1
|
-
# ð Tidepool
|
|
2
|
-
A TUI library written in TypeScript.
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
1
|
+
# ð Tidepool
|
|
2
|
+
A TUI library written in TypeScript.
|
|
3
|
+
|
|
4
|
+
Tidepool renders to a terminal using a buffered, diffed frame loop and a small set
|
|
5
|
+
of composable primitives (boxes, text, lines, buttons, input fields). It is
|
|
6
|
+
intended to be lightweight, hackable, and friendly to custom components.
|
|
7
|
+
|
|
8
|
+
## Contents
|
|
9
|
+
- Installation
|
|
10
|
+
- Quick start
|
|
11
|
+
- Core concepts
|
|
12
|
+
- Screen lifecycle
|
|
13
|
+
- Input handling
|
|
14
|
+
- Components
|
|
15
|
+
- Effects
|
|
16
|
+
- Custom components
|
|
17
|
+
- Demos
|
|
18
|
+
- Performance notes
|
|
19
|
+
- Troubleshooting
|
|
20
|
+
- Migration notes
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
```bash
|
|
24
|
+
npm install tidepool
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
```ts
|
|
29
|
+
import {
|
|
30
|
+
Box,
|
|
31
|
+
Line,
|
|
32
|
+
Text,
|
|
33
|
+
TideScreen,
|
|
34
|
+
setupInput,
|
|
35
|
+
} from "tidepool";
|
|
36
|
+
|
|
37
|
+
const screen = new TideScreen(
|
|
38
|
+
process.stdout.columns - 1,
|
|
39
|
+
process.stdout.rows - 1
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
screen.enableAutoResize();
|
|
43
|
+
|
|
44
|
+
const box = new Box(2, 2, 28, 6, "reset", "magenta", "reset", "Tidepool", 1);
|
|
45
|
+
const line = new Line(0, 1, 24, "yellow", 1);
|
|
46
|
+
const text = new Text(0, 2, "Use arrow keys to move, q to quit.", "green", 1);
|
|
47
|
+
|
|
48
|
+
box.addContent(line);
|
|
49
|
+
box.addContent(text);
|
|
50
|
+
screen.addComponent(box);
|
|
51
|
+
screen.nextFrame();
|
|
52
|
+
|
|
53
|
+
const cleanup = setupInput((_, key) => {
|
|
54
|
+
if (key.name === "q" || (key.ctrl && key.name === "c")) {
|
|
55
|
+
cleanup();
|
|
56
|
+
process.stdout.write("\x1b[?25h");
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
if (key.name === "up") box.move(0, -1);
|
|
60
|
+
if (key.name === "down") box.move(0, 1);
|
|
61
|
+
if (key.name === "left") box.move(-1, 0);
|
|
62
|
+
if (key.name === "right") box.move(1, 0);
|
|
63
|
+
screen.nextFrame();
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Core concepts
|
|
68
|
+
- Coordinates are 0-based. All draws are clipped to the screen bounds.
|
|
69
|
+
- Components added to a `Box` are positioned relative to the box interior.
|
|
70
|
+
- Higher `zIndex` renders on top. Use `TideScreen.invalidateSort()` if you change
|
|
71
|
+
zIndex after adding.
|
|
72
|
+
- Rendering is double-buffered: the back buffer is composed and diffs are written
|
|
73
|
+
to stdout.
|
|
74
|
+
|
|
75
|
+
## Screen lifecycle
|
|
76
|
+
### `TideScreen`
|
|
77
|
+
```ts
|
|
78
|
+
const screen = new TideScreen(width, height, backgroundColor?);
|
|
79
|
+
```
|
|
80
|
+
- `nextFrame()`: render one frame.
|
|
81
|
+
- `renderCycle(beforeRender?, afterRender?)`: fixed-rate loop (requires `setFPS`).
|
|
82
|
+
- `setFPS(fps: number)`: set loop speed.
|
|
83
|
+
- `setUpdateFunction(fn)`: inject per-frame updates.
|
|
84
|
+
- `resize(width, height)`: rebuild buffers to new size.
|
|
85
|
+
- `enableAutoResize(getSize?)`: remeasure on `SIGWINCH`.
|
|
86
|
+
- `disableAutoResize()`: stop listening for resize.
|
|
87
|
+
- `invalidateSort()`: re-sort components if zIndex changed.
|
|
88
|
+
|
|
89
|
+
### Backgrounds
|
|
90
|
+
`backgroundColor` supports standard color names plus `transparent` and `random`.
|
|
91
|
+
If set to `transparent`, Tidepool will skip background fill for faster rendering.
|
|
92
|
+
|
|
93
|
+
## Input handling
|
|
94
|
+
### `setupInput`
|
|
95
|
+
```ts
|
|
96
|
+
const cleanup = setupInput({
|
|
97
|
+
onKey: (str, key) => {},
|
|
98
|
+
onMouse: (event) => {},
|
|
99
|
+
enableMouse: true,
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
- Returns a cleanup function that restores terminal state.
|
|
103
|
+
- Mouse handling uses xterm SGR mouse mode; not all terminals support it.
|
|
104
|
+
|
|
105
|
+
### `InputManager`
|
|
106
|
+
```ts
|
|
107
|
+
const manager = new InputManager();
|
|
108
|
+
manager.register(inputField);
|
|
109
|
+
manager.register(button);
|
|
110
|
+
|
|
111
|
+
const cleanup = setupInputManager(manager, process.stdin, process.stdout, () => {
|
|
112
|
+
screen.nextFrame();
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
- `Tab` / `Shift+Tab` cycles focus.
|
|
116
|
+
- Clicking a focusable also focuses it.
|
|
117
|
+
|
|
118
|
+
## Components
|
|
119
|
+
### `Box`
|
|
120
|
+
```ts
|
|
121
|
+
new Box(x, y, width, height, color?, borderColor?, backgroundColor?, title?, zIndex?);
|
|
122
|
+
```
|
|
123
|
+
- Renders a bordered container and manages child content.
|
|
124
|
+
- Use `addContent()` to render `Text`, `Line`, or custom components inside.
|
|
125
|
+
|
|
126
|
+
### `Text`
|
|
127
|
+
```ts
|
|
128
|
+
new Text(x, y, text, color?, zIndex?);
|
|
129
|
+
```
|
|
130
|
+
- Word-wraps to the container width by default.
|
|
131
|
+
|
|
132
|
+
### `Line`
|
|
133
|
+
```ts
|
|
134
|
+
new Line(x, y, length, color?, zIndex?);
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### `Button`
|
|
138
|
+
```ts
|
|
139
|
+
new Button(zIndex, x, y, width, height, text, textColor?, backgroundColor?, options?);
|
|
140
|
+
```
|
|
141
|
+
Options:
|
|
142
|
+
- `onPress?: () => void`
|
|
143
|
+
- `focusedBackgroundColor?: string`
|
|
144
|
+
- `focusedTextColor?: string`
|
|
145
|
+
|
|
146
|
+
### `InputField`
|
|
147
|
+
```ts
|
|
148
|
+
new InputField(x, y, width, height?, initialValue?, options?);
|
|
149
|
+
```
|
|
150
|
+
Options:
|
|
151
|
+
- `zIndex?: number`
|
|
152
|
+
- `textColor?: string`
|
|
153
|
+
- `backgroundColor?: string`
|
|
154
|
+
- `borderColor?: string`
|
|
155
|
+
- `placeholder?: string`
|
|
156
|
+
- `placeholderColor?: string`
|
|
157
|
+
- `maxLength?: number`
|
|
158
|
+
- `onChange?: (value: string) => void`
|
|
159
|
+
- `onSubmit?: (value: string) => void`
|
|
160
|
+
|
|
161
|
+
## Effects
|
|
162
|
+
### `Shadow`
|
|
163
|
+
```ts
|
|
164
|
+
new Shadow(offsetX?, offsetY?, color?);
|
|
165
|
+
```
|
|
166
|
+
Apply to a `Box` or other `TideEffect` target:
|
|
167
|
+
```ts
|
|
168
|
+
box.addEffect(new Shadow(1, 1, "gray"));
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Custom components
|
|
172
|
+
Implement `TideObject` and draw via `RenderContext`:
|
|
173
|
+
```ts
|
|
174
|
+
import type { RenderContext, TideObject } from "tidepool";
|
|
175
|
+
import { setCell, getColorCode } from "tidepool/dist/util";
|
|
176
|
+
|
|
177
|
+
class Badge implements TideObject {
|
|
178
|
+
zIndex = 10;
|
|
179
|
+
constructor(private x: number, private y: number, private text: string) {}
|
|
180
|
+
draw(ctx: RenderContext) {
|
|
181
|
+
const color = getColorCode("cyan");
|
|
182
|
+
const reset = "\x1b[0m";
|
|
183
|
+
for (let i = 0; i < this.text.length; i++) {
|
|
184
|
+
setCell(
|
|
185
|
+
ctx.buffer,
|
|
186
|
+
ctx.origin.x + this.x + i,
|
|
187
|
+
ctx.origin.y + this.y,
|
|
188
|
+
`${color}${this.text[i]}${reset}`,
|
|
189
|
+
ctx.clip,
|
|
190
|
+
ctx.dirtyRows
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Demos
|
|
198
|
+
```bash
|
|
199
|
+
npm run demo
|
|
200
|
+
npm run demo:stack
|
|
201
|
+
npm run demo:form
|
|
202
|
+
npm run demo:chat
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Run with Bun:
|
|
206
|
+
```bash
|
|
207
|
+
bun demo/demo.ts
|
|
208
|
+
bun demo/stacking.ts
|
|
209
|
+
bun demo/form.ts
|
|
210
|
+
bun demo/chat.ts
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Performance notes
|
|
214
|
+
- Use `transparent` background for faster renders.
|
|
215
|
+
- Avoid per-frame resorting by setting `zIndex` before adding components.
|
|
216
|
+
- For heavy scenes, render only on state changes rather than a fixed FPS loop.
|
|
217
|
+
|
|
218
|
+
## Troubleshooting
|
|
219
|
+
- If input feels unresponsive, ensure `setupInput` is called and you are calling
|
|
220
|
+
`nextFrame()` after handling keys.
|
|
221
|
+
- If mouse clicks do not work, your terminal may not support SGR mouse mode.
|
|
222
|
+
- If the cursor disappears, make sure you clean up on exit and restore it with
|
|
223
|
+
`\x1b[?25h`.
|
|
224
|
+
|
|
225
|
+
## Migration notes (v1.0.6 -> current)
|
|
226
|
+
- `draw(...)` now receives a `RenderContext` instead of buffer + offsets.
|
|
227
|
+
- Component draws are clipped by a render context; negative coordinates are safe.
|
|
228
|
+
|
|
229
|
+
## Repository
|
|
230
|
+
[GitHub Repository](https://github.com/stuncs69/tidepool)
|
|
231
|
+
[NPM](https://www.npmjs.com/package/tidepool)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RenderContext, TideEffect, TideObject } from "../interfaces";
|
|
2
|
+
export declare class Shadow implements TideEffect {
|
|
3
|
+
private offset;
|
|
4
|
+
private color;
|
|
5
|
+
constructor(offsetX?: number, offsetY?: number, color?: string);
|
|
6
|
+
apply(obj: TideObject, ctx: RenderContext): void;
|
|
7
|
+
private isBoxLike;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=shadow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shadow.d.ts","sourceRoot":"","sources":["../../src/effects/shadow.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAG1E,qBAAa,MAAO,YAAW,UAAU;IACvC,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,KAAK,CAAQ;gBAET,OAAO,SAAI,EAAE,OAAO,SAAI,EAAE,KAAK,SAAS;IAKpD,KAAK,CAAC,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,aAAa;IA+BzC,OAAO,CAAC,SAAS;CAUlB"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Shadow = void 0;
|
|
4
|
+
const util_1 = require("../util");
|
|
5
|
+
class Shadow {
|
|
6
|
+
constructor(offsetX = 1, offsetY = 1, color = "gray") {
|
|
7
|
+
this.offset = { x: offsetX, y: offsetY };
|
|
8
|
+
this.color = color;
|
|
9
|
+
}
|
|
10
|
+
apply(obj, ctx) {
|
|
11
|
+
if (this.isBoxLike(obj)) {
|
|
12
|
+
const colorCode = (0, util_1.getColorCode)(this.color);
|
|
13
|
+
const originX = ctx.origin.x + obj.x;
|
|
14
|
+
const originY = ctx.origin.y + obj.y;
|
|
15
|
+
const maxY = Math.min(originY + obj.height + this.offset.y, ctx.buffer.length);
|
|
16
|
+
const maxX = Math.min(originX + obj.width + this.offset.x, ctx.buffer[0].length);
|
|
17
|
+
for (let i = originY + this.offset.y; i < maxY; i++) {
|
|
18
|
+
for (let j = originX + this.offset.x; j < maxX; j++) {
|
|
19
|
+
if (ctx.buffer[i][j] === " ") {
|
|
20
|
+
(0, util_1.setCell)(ctx.buffer, j, i, `${colorCode}â\x1b[0m`, ctx.clip, ctx.dirtyRows);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
isBoxLike(obj) {
|
|
27
|
+
return (typeof obj.x === "number" &&
|
|
28
|
+
typeof obj.y === "number" &&
|
|
29
|
+
typeof obj.width === "number" &&
|
|
30
|
+
typeof obj.height === "number");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
exports.Shadow = Shadow;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,5 +2,9 @@ import { Box } from "./objects/box";
|
|
|
2
2
|
import { Line } from "./objects/line";
|
|
3
3
|
import { Text } from "./objects/text";
|
|
4
4
|
import { TideScreen } from "./screen";
|
|
5
|
-
|
|
5
|
+
import { Shadow } from "./effects/shadow";
|
|
6
|
+
import { Button } from "./objects/button";
|
|
7
|
+
import { InputField } from "./objects/input";
|
|
8
|
+
import { InputManager, setupInput, setupInputManager } from "./input";
|
|
9
|
+
export { Box, Text, Line, TideScreen, Shadow, Button, InputField, InputManager, setupInput, setupInputManager, };
|
|
6
10
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AACpC,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AACtC,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AACpC,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AACtC,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAEtE,OAAO,EACL,GAAG,EACH,IAAI,EACJ,IAAI,EACJ,UAAU,EACV,MAAM,EACN,MAAM,EACN,UAAU,EACV,YAAY,EACZ,UAAU,EACV,iBAAiB,GAClB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.TideScreen = exports.Line = exports.Text = exports.Box = void 0;
|
|
3
|
+
exports.setupInputManager = exports.setupInput = exports.InputManager = exports.InputField = exports.Button = exports.Shadow = exports.TideScreen = exports.Line = exports.Text = exports.Box = void 0;
|
|
4
4
|
const box_1 = require("./objects/box");
|
|
5
5
|
Object.defineProperty(exports, "Box", { enumerable: true, get: function () { return box_1.Box; } });
|
|
6
6
|
const line_1 = require("./objects/line");
|
|
@@ -9,3 +9,13 @@ const text_1 = require("./objects/text");
|
|
|
9
9
|
Object.defineProperty(exports, "Text", { enumerable: true, get: function () { return text_1.Text; } });
|
|
10
10
|
const screen_1 = require("./screen");
|
|
11
11
|
Object.defineProperty(exports, "TideScreen", { enumerable: true, get: function () { return screen_1.TideScreen; } });
|
|
12
|
+
const shadow_1 = require("./effects/shadow");
|
|
13
|
+
Object.defineProperty(exports, "Shadow", { enumerable: true, get: function () { return shadow_1.Shadow; } });
|
|
14
|
+
const button_1 = require("./objects/button");
|
|
15
|
+
Object.defineProperty(exports, "Button", { enumerable: true, get: function () { return button_1.Button; } });
|
|
16
|
+
const input_1 = require("./objects/input");
|
|
17
|
+
Object.defineProperty(exports, "InputField", { enumerable: true, get: function () { return input_1.InputField; } });
|
|
18
|
+
const input_2 = require("./input");
|
|
19
|
+
Object.defineProperty(exports, "InputManager", { enumerable: true, get: function () { return input_2.InputManager; } });
|
|
20
|
+
Object.defineProperty(exports, "setupInput", { enumerable: true, get: function () { return input_2.setupInput; } });
|
|
21
|
+
Object.defineProperty(exports, "setupInputManager", { enumerable: true, get: function () { return input_2.setupInputManager; } });
|
package/dist/input.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Focusable, KeyInfo } from "./interfaces";
|
|
2
|
+
export type KeypressHandler = (str: string, key: KeyInfo) => void;
|
|
3
|
+
export type MouseHandler = (event: MouseEvent) => void;
|
|
4
|
+
export interface MouseEvent {
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
button: number;
|
|
8
|
+
type: "down" | "up";
|
|
9
|
+
ctrl: boolean;
|
|
10
|
+
meta: boolean;
|
|
11
|
+
shift: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface InputHandlers {
|
|
14
|
+
onKey?: KeypressHandler;
|
|
15
|
+
onMouse?: MouseHandler;
|
|
16
|
+
enableMouse?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare function setupInput(handlerOrOptions: KeypressHandler | InputHandlers, input?: NodeJS.ReadStream, output?: NodeJS.WriteStream): () => void;
|
|
19
|
+
export declare class InputManager {
|
|
20
|
+
private focusables;
|
|
21
|
+
private focusedIndex;
|
|
22
|
+
register(focusable: Focusable): void;
|
|
23
|
+
unregister(focusable: Focusable): void;
|
|
24
|
+
focusNext(): void;
|
|
25
|
+
focusPrevious(): void;
|
|
26
|
+
handleKey(input: string, key: KeyInfo): boolean;
|
|
27
|
+
handleMouse(event: MouseEvent): boolean;
|
|
28
|
+
private focus;
|
|
29
|
+
private sortFocusables;
|
|
30
|
+
private findTopmostAt;
|
|
31
|
+
}
|
|
32
|
+
export declare function setupInputManager(manager: InputManager, input?: NodeJS.ReadStream, output?: NodeJS.WriteStream, afterInput?: () => void): () => void;
|
|
33
|
+
//# sourceMappingURL=input.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../src/input.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvD,MAAM,MAAM,eAAe,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;AAClE,MAAM,MAAM,YAAY,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;AAEvD,MAAM,WAAW,UAAU;IACzB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAoCD,wBAAgB,UAAU,CACxB,gBAAgB,EAAE,eAAe,GAAG,aAAa,EACjD,KAAK,GAAE,MAAM,CAAC,UAA0B,EACxC,MAAM,GAAE,MAAM,CAAC,WAA4B,cA+C5C;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,UAAU,CAAmB;IACrC,OAAO,CAAC,YAAY,CAAM;IAE1B,QAAQ,CAAC,SAAS,EAAE,SAAS;IAQ7B,UAAU,CAAC,SAAS,EAAE,SAAS;IAe/B,SAAS;IAQT,aAAa;IAUb,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO;IAerC,WAAW,CAAC,KAAK,EAAE,UAAU;IAe7B,OAAO,CAAC,KAAK;IAWb,OAAO,CAAC,cAAc;IAItB,OAAO,CAAC,aAAa;CAQtB;AAED,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,YAAY,EACrB,KAAK,GAAE,MAAM,CAAC,UAA0B,EACxC,MAAM,GAAE,MAAM,CAAC,WAA4B,EAC3C,UAAU,CAAC,EAAE,MAAM,IAAI,cAoBxB"}
|
package/dist/input.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.InputManager = void 0;
|
|
7
|
+
exports.setupInput = setupInput;
|
|
8
|
+
exports.setupInputManager = setupInputManager;
|
|
9
|
+
const readline_1 = __importDefault(require("readline"));
|
|
10
|
+
function enableMouseReporting(output) {
|
|
11
|
+
output.write("\x1b[?1000h");
|
|
12
|
+
output.write("\x1b[?1006h");
|
|
13
|
+
}
|
|
14
|
+
function disableMouseReporting(output) {
|
|
15
|
+
output.write("\x1b[?1000l");
|
|
16
|
+
output.write("\x1b[?1006l");
|
|
17
|
+
}
|
|
18
|
+
function parseMouseEvent(data) {
|
|
19
|
+
if (!data.startsWith("\x1b[<")) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const match = data.match(/^\x1b\[<(\d+);(\d+);(\d+)([mM])$/);
|
|
23
|
+
if (!match) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const code = Number(match[1]);
|
|
27
|
+
const x = Number(match[2]);
|
|
28
|
+
const y = Number(match[3]);
|
|
29
|
+
const isDown = match[4] === "M";
|
|
30
|
+
const button = code & 3;
|
|
31
|
+
return {
|
|
32
|
+
x: x - 1,
|
|
33
|
+
y: y - 1,
|
|
34
|
+
button,
|
|
35
|
+
type: isDown ? "down" : "up",
|
|
36
|
+
shift: Boolean(code & 4),
|
|
37
|
+
meta: Boolean(code & 8),
|
|
38
|
+
ctrl: Boolean(code & 16),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function setupInput(handlerOrOptions, input = process.stdin, output = process.stdout) {
|
|
42
|
+
var _a;
|
|
43
|
+
const options = typeof handlerOrOptions === "function"
|
|
44
|
+
? { onKey: handlerOrOptions }
|
|
45
|
+
: handlerOrOptions;
|
|
46
|
+
const onKey = options.onKey;
|
|
47
|
+
const onMouse = options.onMouse;
|
|
48
|
+
const enableMouse = (_a = options.enableMouse) !== null && _a !== void 0 ? _a : Boolean(onMouse);
|
|
49
|
+
readline_1.default.emitKeypressEvents(input);
|
|
50
|
+
if (input.isTTY) {
|
|
51
|
+
input.setRawMode(true);
|
|
52
|
+
}
|
|
53
|
+
input.resume();
|
|
54
|
+
const keyHandler = (str, key) => {
|
|
55
|
+
if (onKey) {
|
|
56
|
+
onKey(str !== null && str !== void 0 ? str : "", key);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
input.on("keypress", keyHandler);
|
|
60
|
+
let dataHandler = null;
|
|
61
|
+
if (enableMouse && onMouse) {
|
|
62
|
+
enableMouseReporting(output);
|
|
63
|
+
dataHandler = (data) => {
|
|
64
|
+
const ev = parseMouseEvent(data.toString("utf8"));
|
|
65
|
+
if (ev) {
|
|
66
|
+
onMouse(ev);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
input.on("data", dataHandler);
|
|
70
|
+
}
|
|
71
|
+
return () => {
|
|
72
|
+
input.off("keypress", keyHandler);
|
|
73
|
+
if (dataHandler) {
|
|
74
|
+
input.off("data", dataHandler);
|
|
75
|
+
disableMouseReporting(output);
|
|
76
|
+
}
|
|
77
|
+
if (input.isTTY) {
|
|
78
|
+
input.setRawMode(false);
|
|
79
|
+
}
|
|
80
|
+
input.pause();
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
class InputManager {
|
|
84
|
+
constructor() {
|
|
85
|
+
this.focusables = [];
|
|
86
|
+
this.focusedIndex = -1;
|
|
87
|
+
}
|
|
88
|
+
register(focusable) {
|
|
89
|
+
this.focusables.push(focusable);
|
|
90
|
+
this.sortFocusables();
|
|
91
|
+
if (this.focusedIndex === -1) {
|
|
92
|
+
this.focus(0);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
unregister(focusable) {
|
|
96
|
+
const index = this.focusables.indexOf(focusable);
|
|
97
|
+
if (index === -1) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (this.focusedIndex === index) {
|
|
101
|
+
this.focusables[index].blur();
|
|
102
|
+
this.focusedIndex = -1;
|
|
103
|
+
}
|
|
104
|
+
this.focusables.splice(index, 1);
|
|
105
|
+
if (this.focusables.length > 0 && this.focusedIndex === -1) {
|
|
106
|
+
this.focus(0);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
focusNext() {
|
|
110
|
+
if (this.focusables.length === 0) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const next = (this.focusedIndex + 1) % this.focusables.length;
|
|
114
|
+
this.focus(next);
|
|
115
|
+
}
|
|
116
|
+
focusPrevious() {
|
|
117
|
+
if (this.focusables.length === 0) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const prev = (this.focusedIndex - 1 + this.focusables.length) %
|
|
121
|
+
this.focusables.length;
|
|
122
|
+
this.focus(prev);
|
|
123
|
+
}
|
|
124
|
+
handleKey(input, key) {
|
|
125
|
+
if (key.name === "tab") {
|
|
126
|
+
if (key.shift) {
|
|
127
|
+
this.focusPrevious();
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
this.focusNext();
|
|
131
|
+
}
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
if (this.focusedIndex === -1) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
return this.focusables[this.focusedIndex].handleKey(input, key);
|
|
138
|
+
}
|
|
139
|
+
handleMouse(event) {
|
|
140
|
+
if (event.type !== "down" || event.button !== 0) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
const target = this.findTopmostAt(event.x, event.y);
|
|
144
|
+
if (!target) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
const index = this.focusables.indexOf(target);
|
|
148
|
+
if (index !== -1) {
|
|
149
|
+
this.focus(index);
|
|
150
|
+
}
|
|
151
|
+
return target.handleClick(event.x, event.y);
|
|
152
|
+
}
|
|
153
|
+
focus(index) {
|
|
154
|
+
if (index < 0 || index >= this.focusables.length) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (this.focusedIndex !== -1) {
|
|
158
|
+
this.focusables[this.focusedIndex].blur();
|
|
159
|
+
}
|
|
160
|
+
this.focusedIndex = index;
|
|
161
|
+
this.focusables[this.focusedIndex].focus();
|
|
162
|
+
}
|
|
163
|
+
sortFocusables() {
|
|
164
|
+
this.focusables.sort((a, b) => b.zIndex - a.zIndex);
|
|
165
|
+
}
|
|
166
|
+
findTopmostAt(x, y) {
|
|
167
|
+
for (const focusable of this.focusables) {
|
|
168
|
+
if (focusable.contains(x, y)) {
|
|
169
|
+
return focusable;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
exports.InputManager = InputManager;
|
|
176
|
+
function setupInputManager(manager, input = process.stdin, output = process.stdout, afterInput) {
|
|
177
|
+
return setupInput({
|
|
178
|
+
onKey: (str, key) => {
|
|
179
|
+
manager.handleKey(str, key);
|
|
180
|
+
if (afterInput) {
|
|
181
|
+
afterInput();
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
onMouse: (event) => {
|
|
185
|
+
manager.handleMouse(event);
|
|
186
|
+
if (afterInput) {
|
|
187
|
+
afterInput();
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
}, input, output);
|
|
191
|
+
}
|
package/dist/interfaces.d.ts
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
|
+
export type ColorName = "reset" | "black" | "red" | "green" | "yellow" | "blue" | "magenta" | "cyan" | "white" | "gray" | "transparent" | "random";
|
|
2
|
+
export interface Point {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
}
|
|
6
|
+
export interface Rect {
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
}
|
|
12
|
+
export interface RenderContext {
|
|
13
|
+
buffer: string[][];
|
|
14
|
+
origin: Point;
|
|
15
|
+
clip: Rect;
|
|
16
|
+
screenSize: {
|
|
17
|
+
width: number;
|
|
18
|
+
height: number;
|
|
19
|
+
};
|
|
20
|
+
dirtyRows?: Set<number>;
|
|
21
|
+
}
|
|
22
|
+
export interface KeyInfo {
|
|
23
|
+
name?: string;
|
|
24
|
+
sequence?: string;
|
|
25
|
+
ctrl?: boolean;
|
|
26
|
+
meta?: boolean;
|
|
27
|
+
shift?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface Focusable {
|
|
30
|
+
zIndex: number;
|
|
31
|
+
contains(x: number, y: number): boolean;
|
|
32
|
+
focus(): void;
|
|
33
|
+
blur(): void;
|
|
34
|
+
handleKey(input: string, key: KeyInfo): boolean;
|
|
35
|
+
handleClick(x: number, y: number): boolean;
|
|
36
|
+
}
|
|
1
37
|
export interface TideObject {
|
|
2
38
|
zIndex: number;
|
|
3
|
-
draw:
|
|
39
|
+
draw(ctx: RenderContext): void;
|
|
40
|
+
}
|
|
41
|
+
export interface TideEffect {
|
|
42
|
+
apply(target: TideObject, ctx: RenderContext): void;
|
|
4
43
|
}
|
|
5
44
|
//# sourceMappingURL=interfaces.d.ts.map
|
package/dist/interfaces.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"interfaces.d.ts","sourceRoot":"","sources":["../src/interfaces.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;
|
|
1
|
+
{"version":3,"file":"interfaces.d.ts","sourceRoot":"","sources":["../src/interfaces.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,GACjB,OAAO,GACP,OAAO,GACP,KAAK,GACL,OAAO,GACP,QAAQ,GACR,MAAM,GACN,SAAS,GACT,MAAM,GACN,OAAO,GACP,MAAM,GACN,aAAa,GACb,QAAQ,CAAC;AAEb,MAAM,WAAW,KAAK;IACpB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED,MAAM,WAAW,IAAI;IACnB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,EAAE,EAAE,CAAC;IACnB,MAAM,EAAE,KAAK,CAAC;IACd,IAAI,EAAE,IAAI,CAAC;IACX,UAAU,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9C,SAAS,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACzB;AAED,MAAM,WAAW,OAAO;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACxC,KAAK,IAAI,IAAI,CAAC;IACd,IAAI,IAAI,IAAI,CAAC;IACb,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC;IAChD,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CAC5C;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAAC;CAChC;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,aAAa,GAAG,IAAI,CAAC;CACrD"}
|