tidepool 1.0.6 → 2.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/README.md CHANGED
@@ -1,71 +1,231 @@
1
- # 🌊 Tidepool
2
- A TUI library written in TypeScript.
3
-
4
- ### What tidepool offers ðŸ”Ĩ
5
- - Modular API 🔧
6
- - Lightweight ðŸŠķ
7
- - Type-safe âŒĻïļ
8
- - Easy to integrate with other technologies ðŸ’Ą
9
-
10
- API Example using readline for movement:
11
- ```ts
12
- import { TideScreen, Box, Text, Line } from "tidepool";
13
- import readline from 'readline';
14
-
15
- // Create new tidepool screen instance
16
- const tui = new TideScreen(process.stdout.columns-1, process.stdout.rows-1);
17
-
18
- // Create text to be displayed inside of boxes
19
- const text = new Text(0, 0, "Welcome to tidepool!", 'cyan', 1);
20
- const text2 = new Text(0, 1, "This is a tidepool example with readline.", 'green', 1);
21
-
22
- // search for the middle of the screen and define first box
23
- const x = Math.floor(process.stdout.columns / 2)
24
- const y = Math.floor(process.stdout.rows / 2)
25
- const box = new Box(x, y, 25, 3, 'magenta', 'Tidepool Box', 1);
26
-
27
- // Define the box that will be under the other box
28
- const underBox = new Box(x-5, y-5, 25, 10, 'red', 'Under the other one', 2);
29
-
30
- // Define a cosmetic line
31
- const line = new Line(0, 0, underBox.width-2, 'yellow', 1);
32
-
33
- // Add content and components to the screen
34
- box.addContent(text);
35
- underBox.addContent(text2);
36
- underBox.addContent(line);
37
-
38
- tui.addComponent(box);
39
- tui.addComponent(underBox);
40
-
41
- // Render first frame
42
- tui.nextFrame();
43
-
44
- readline.emitKeypressEvents(process.stdin);
45
- process.stdin.setRawMode(true);
46
-
47
- // Listen for keypresses
48
- process.stdin.on('keypress', (str, key) => {
49
- if (key.name === 'q' || (key.ctrl && key.name === 'c')) {
50
- process.exit();
51
- } else {
52
- if (key.name === 'up' && box.y > 0) {
53
- box.move(0, -1);
54
- } else if (key.name === 'down' && box.y + box.height < tui.height) {
55
- box.move(0, 1);
56
- } else if (key.name === 'left' && box.x > 0) {
57
- box.move(-1, 0);
58
- } else if (key.name === 'right' && box.x + box.width < tui.width) {
59
- box.move(1, 0);
60
- }
61
-
62
- // Render new frame after box moved.
63
- tui.nextFrame();
64
- }
65
- });
66
-
67
- ```
68
-
69
- ## Repository
70
- [GitHub Repository](https://github.com/stuncs69/tidepool)</br>
71
- [NPM](https://www.npmjs.com/package/tidepool)
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
- export { Box, Text, Line, TideScreen };
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
@@ -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;AAEtC,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,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; } });
@@ -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
+ }
@@ -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: Function;
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
@@ -1 +1 @@
1
- {"version":3,"file":"interfaces.d.ts","sourceRoot":"","sources":["../src/interfaces.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,QAAQ,CAAC;CAClB"}
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"}