voonex 0.1.0 → 0.2.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 CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 CodeTease
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2025 CodeTease
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,223 +1,175 @@
1
- # Voonex
2
-
3
- **Voonex** is a modern, zero-dependency Terminal UI (TUI) library for Node.js, built with TypeScript. It provides a robust virtual buffer, reactive rendering system, and a rich set of widgets to build complex command-line interfaces with ease.
4
-
5
- ## Features
6
-
7
- - **Zero Dependencies**: Lightweight and easy to audit.
8
- - **Double Buffering & Diffing**: Efficient rendering that eliminates flickering and minimizes I/O.
9
- - **Component System**: Built-in widgets like `Box`, `Menu`, `ProgressBar`, `Input`, `Table`, and more.
10
- - **Reactive Rendering**: Automated screen updates via `Screen.mount()` and `Screen.scheduleRender()`.
11
- - **Focus Management**: Built-in keyboard navigation and focus delegation.
12
- - **Styling Engine**: Simple yet powerful API for ANSI colors and text modifiers.
13
- - **TypeScript Support**: Written in TypeScript with full type definitions included.
14
-
15
- ## Installation
16
-
17
- Install via npm:
18
-
19
- ```bash
20
- npm install voonex
21
- ```
22
-
23
- ## Quick Start
24
-
25
- Here is a minimal example showing how to initialize the screen and display a simple box.
26
-
27
- ```typescript
28
- import { Screen, Box, Styler, Input } from 'voonex';
29
-
30
- // 1. Enter the alternate screen buffer
31
- Screen.enter();
32
-
33
- // 2. Define your render function
34
- function render() {
35
- // Render a Box at (5, 5) with a title
36
- Box.render([
37
- "Welcome to Voonex!",
38
- "Press 'q' to exit."
39
- ], {
40
- title: "Hello World",
41
- x: 5,
42
- y: 5,
43
- padding: 1,
44
- style: 'double',
45
- borderColor: 'cyan'
46
- });
47
- }
48
-
49
- // 3. Mount the render function to the screen loop
50
- Screen.mount(render);
51
-
52
- // 4. Handle Input
53
- Input.onKey((key) => {
54
- if (key.name === 'q') {
55
- // Leave the screen buffer properly before exiting
56
- Screen.leave();
57
- process.exit(0);
58
- }
59
- });
60
- ```
61
-
62
- Run it with:
63
- ```bash
64
- npx ts-node my-app.ts
65
- ```
66
-
67
- ## Core Concepts
68
-
69
- ### The Screen
70
- The `Screen` class is the heart of Voonex. It manages the terminal buffer, handles resizing, and optimizes rendering using a diffing algorithm.
71
-
72
- - `Screen.enter()`: Switches to the alternate buffer (like `vim` or `nano`).
73
- - `Screen.leave()`: Restores the original terminal state. **Always call this before exiting.**
74
- - `Screen.mount(renderFn)`: Registers a function to be called during the render cycle. Voonex uses a "Painter's Algorithm", so functions mounted later are drawn on top.
75
- - `Screen.scheduleRender()`: Triggers a screen update. This is automatically called by most interactive components, but you can call it manually if you update state asynchronously (e.g., inside a `setInterval`).
76
-
77
- ### Input Handling
78
- Voonex provides a global input listener wrapper around Node's `process.stdin`.
79
-
80
- ```typescript
81
- import { Input } from 'voonex';
82
-
83
- Input.onKey((key) => {
84
- console.log(key.name); // 'up', 'down', 'enter', 'a', 'b', etc.
85
- console.log(key.ctrl); // true if Ctrl is pressed
86
- });
87
- ```
88
-
89
- ### Styling
90
- The `Styler` class provides utilities for coloring and formatting text.
91
-
92
- ```typescript
93
- import { Styler } from 'voonex';
94
-
95
- const text = Styler.style("Success!", 'green', 'bold', 'underline');
96
- ```
97
-
98
- ## Components
99
-
100
- Voonex comes with several built-in components. Components can be used in two ways:
101
- 1. **Static Rendering**: Using static methods like `Box.render()`.
102
- 2. **Stateful Instances**: Creating an instance (e.g., `new Menu()`) and calling its methods.
103
-
104
- ### Box
105
- A container for text with optional borders, padding, and titles.
106
-
107
- ```typescript
108
- Box.render([
109
- "Line 1",
110
- "Line 2"
111
- ], {
112
- x: 2, y: 2,
113
- width: 30,
114
- borderColor: 'green',
115
- style: 'round' // 'single', 'double', or 'round'
116
- });
117
- ```
118
-
119
- ### Menu
120
- A vertical list of selectable items.
121
-
122
- ```typescript
123
- const menu = new Menu({
124
- title: "Main Menu",
125
- x: 4, y: 4,
126
- items: ["Start", "Options", "Exit"],
127
- onSelect: (index, item) => {
128
- console.log(`Selected: ${item}`);
129
- }
130
- });
131
-
132
- // In your input handler:
133
- Input.onKey((key) => {
134
- menu.handleKey(key); // Passes key events to the menu
135
- });
136
-
137
- // In your render function:
138
- menu.render();
139
- ```
140
-
141
- ### ProgressBar
142
- Displays a progress bar.
143
-
144
- ```typescript
145
- const bar = new ProgressBar({
146
- width: 20,
147
- total: 100,
148
- x: 4, y: 10,
149
- completeChar: '█',
150
- incompleteChar: '░'
151
- });
152
-
153
- // Update progress
154
- bar.update(50); // 50%
155
- ```
156
-
157
- ### Popup
158
- A modal dialog that overlays other content.
159
-
160
- ```typescript
161
- // Shows a message and waits for user to press Enter/Esc
162
- await Popup.alert("This is an important message!", { title: "Alert" });
163
-
164
- // Asks for confirmation (returns boolean)
165
- const confirmed = await Popup.confirm("Are you sure?", { title: "Confirm" });
166
- ```
167
-
168
- ### Input Field
169
- A text input field for capturing user input.
170
-
171
- ```typescript
172
- const nameInput = new InputField({
173
- x: 2, y: 2,
174
- width: 20,
175
- placeholder: "Enter name..."
176
- });
177
-
178
- Input.onKey(key => {
179
- if (key.name === 'tab') {
180
- nameInput.focus();
181
- }
182
- nameInput.handleKey(key);
183
- });
184
-
185
- // In render loop
186
- nameInput.render();
187
- ```
188
-
189
- ## Advanced Usage
190
-
191
- ### Manual Layout
192
- Voonex relies on absolute positioning `(x, y)`. For complex layouts, you can calculate coordinates dynamically based on `Screen.size`.
193
-
194
- ```typescript
195
- const { width, height } = Screen.size;
196
- const centerX = Math.floor(width / 2);
197
- const centerY = Math.floor(height / 2);
198
- ```
199
-
200
- ### Creating Custom Components
201
- Any class can be a component. To integrate with the Voonex ecosystem, it's recommended (but not required) to implement the `Focusable` interface if the component handles input.
202
-
203
- ```typescript
204
- import { Screen, Styler, Focusable } from 'voonex';
205
-
206
- class MyWidget implements Focusable {
207
- focus() { /* handle focus */ }
208
- blur() { /* handle blur */ }
209
-
210
- handleKey(key) {
211
- // return true if key was consumed
212
- return false;
213
- }
214
-
215
- render() {
216
- Screen.write(10, 10, "My Custom Widget");
217
- }
218
- }
219
- ```
220
-
221
- ## License
222
-
223
- This project is under the **MIT License**.
1
+ # Voonex
2
+
3
+ **Voonex** is a modern, zero-dependency Terminal UI (TUI) library for Node.js, built with TypeScript. It provides a robust virtual buffer, reactive rendering system, and a rich set of widgets to build complex command-line interfaces with ease.
4
+
5
+ ## Features
6
+
7
+ - **Zero Dependencies**: Lightweight and easy to audit.
8
+ - **Double Buffering & Diffing**: Efficient rendering that eliminates flickering and minimizes I/O.
9
+ - **Auto Layout**: Flexbox-like layout engine for responsive designs.
10
+ - **Layer Management**: Z-index support for Modals, Tooltips, and Popups.
11
+ - **Component System**: Built-in widgets like `Box`, `Menu`, `ProgressBar`, `Input`, `Table`, and more.
12
+ - **Reactive Rendering**: Automated screen updates via `Screen.mount()` and `Screen.scheduleRender()`.
13
+ - **Focus Management**: Built-in keyboard navigation and focus delegation.
14
+ - **Styling Engine**: Simple yet powerful API for ANSI colors and text modifiers.
15
+ - **TypeScript Support**: Written in TypeScript with full type definitions included.
16
+
17
+ ## Installation
18
+
19
+ Install via npm:
20
+
21
+ ```bash
22
+ npm install voonex
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ Here is a minimal example showing how to initialize the screen and display a simple box.
28
+
29
+ ```typescript
30
+ import { Screen, Box, Styler, Input } from 'voonex';
31
+
32
+ // 1. Enter the alternate screen buffer
33
+ Screen.enter();
34
+
35
+ // 2. Define your render function
36
+ function render() {
37
+ // Render a Box at (5, 5) with a title
38
+ Box.render([
39
+ "Welcome to Voonex!",
40
+ "Press 'q' to exit."
41
+ ], {
42
+ title: "Hello World",
43
+ x: 5,
44
+ y: 5,
45
+ padding: 1,
46
+ style: 'double',
47
+ borderColor: 'cyan'
48
+ });
49
+ }
50
+
51
+ // 3. Mount the render function to the screen loop
52
+ Screen.mount(render);
53
+
54
+ // 4. Handle Input
55
+ Input.onKey((key) => {
56
+ if (key.name === 'q') {
57
+ // Leave the screen buffer properly before exiting
58
+ Screen.leave();
59
+ process.exit(0);
60
+ }
61
+ });
62
+ ```
63
+
64
+ Run it with:
65
+ ```bash
66
+ npx ts-node my-app.ts
67
+ ```
68
+
69
+ ## Core Concepts
70
+
71
+ ### The Screen
72
+ The `Screen` class is the heart of Voonex. It manages the terminal buffer, handles resizing, and optimizes rendering using a diffing algorithm.
73
+
74
+ - `Screen.enter()`: Switches to the alternate buffer (like `vim` or `nano`).
75
+ - `Screen.leave()`: Restores the original terminal state. **Always call this before exiting.**
76
+ - `Screen.mount(renderFn, layer?)`: Registers a function to be called during the render cycle.
77
+ - `Screen.scheduleRender()`: Triggers a screen update.
78
+
79
+ #### Layer Management
80
+ Voonex uses a "Painter's Algorithm" with Z-index layers.
81
+ ```typescript
82
+ import { Screen, Layer } from 'voonex';
83
+
84
+ Screen.mount(drawBackground, Layer.BACKGROUND); // 0
85
+ Screen.mount(drawContent, Layer.CONTENT); // 10
86
+ Screen.mount(drawPopup, Layer.MODAL); // 100
87
+ ```
88
+
89
+ ### Input Handling
90
+ Voonex provides global input listeners.
91
+
92
+ **Keyboard:**
93
+ ```typescript
94
+ Input.onKey((key) => {
95
+ console.log(key.name);
96
+ });
97
+ ```
98
+
99
+ ### Layout Engine
100
+ The `Layout` class helps calculate coordinates dynamically (Flexbox-style).
101
+
102
+ ```typescript
103
+ import { Layout, Screen } from 'voonex';
104
+
105
+ const { rects } = Layout.compute(Screen.size, {
106
+ direction: 'row',
107
+ children: [
108
+ { weight: 1 }, // Left sidebar (33%)
109
+ { weight: 2 } // Main content (66%)
110
+ ]
111
+ });
112
+
113
+ const sidebarRect = rects[0];
114
+ const contentRect = rects[1];
115
+ ```
116
+
117
+ ## Components
118
+
119
+ ### Box
120
+ A container for text with optional borders, padding, and titles.
121
+
122
+ ```typescript
123
+ Box.render([
124
+ "Line 1",
125
+ "Line 2"
126
+ ], {
127
+ x: 2, y: 2,
128
+ width: 30,
129
+ borderColor: 'green',
130
+ style: 'round' // 'single', 'double', or 'round'
131
+ });
132
+ ```
133
+
134
+ ### Button
135
+ Interactive button that supports Enter.
136
+
137
+ ```typescript
138
+ const btn = new Button({
139
+ id: 'submit',
140
+ text: "Submit",
141
+ x: 10, y: 10,
142
+ onPress: () => submitForm()
143
+ });
144
+ ```
145
+
146
+ ### Input Field
147
+ A fully featured text editor with cursor support, scrolling, and editing keys.
148
+
149
+ ```typescript
150
+ const nameInput = new InputField({
151
+ x: 2, y: 2,
152
+ width: 20,
153
+ placeholder: "Enter name..."
154
+ });
155
+
156
+ Input.onKey(key => {
157
+ // Navigate focus
158
+ if (key.name === 'tab') nameInput.focus();
159
+
160
+ // Handle typing (Home, End, Arrows, Backspace supported)
161
+ nameInput.handleKey(key);
162
+ });
163
+ ```
164
+
165
+ ### Popup
166
+ A modal dialog that overlays other content (uses `Layer.MODAL`).
167
+
168
+ ```typescript
169
+ // Shows a message and waits for user to press Enter/Esc
170
+ await Popup.alert("This is an important message!", { title: "Alert" });
171
+ ```
172
+
173
+ ## License
174
+
175
+ This project is under the **MIT License**.
@@ -18,6 +18,7 @@ export declare class Button implements Focusable {
18
18
  focus(): void;
19
19
  blur(): void;
20
20
  handleKey(key: readline.Key): boolean;
21
+ private triggerPress;
21
22
  render(): void;
22
23
  }
23
24
  export {};
@@ -23,18 +23,21 @@ class Button {
23
23
  if (!this.isFocused)
24
24
  return false;
25
25
  if (key.name === 'return' || key.name === 'enter' || key.name === 'space') {
26
- this.isPressed = true;
27
- this.render(); // Show pressed state visually
28
- // Trigger action slightly delayed to show visual feedback
29
- setTimeout(() => {
30
- this.isPressed = false;
31
- this.render();
32
- this.options.onPress();
33
- }, 150);
26
+ this.triggerPress();
34
27
  return true;
35
28
  }
36
29
  return false;
37
30
  }
31
+ triggerPress() {
32
+ this.isPressed = true;
33
+ this.render(); // Show pressed state visually
34
+ // Trigger action slightly delayed to show visual feedback
35
+ setTimeout(() => {
36
+ this.isPressed = false;
37
+ this.render();
38
+ this.options.onPress();
39
+ }, 150);
40
+ }
38
41
  render() {
39
42
  const { x, y, text, width } = this.options;
40
43
  let label = text;
@@ -14,6 +14,8 @@ export declare class InputField implements Focusable {
14
14
  value: string;
15
15
  private isFocused;
16
16
  private options;
17
+ private cursorIndex;
18
+ private scrollOffset;
17
19
  constructor(options: InputOptions);
18
20
  focus(): void;
19
21
  blur(): void;
@@ -7,35 +7,78 @@ class InputField {
7
7
  constructor(options) {
8
8
  this.value = "";
9
9
  this.isFocused = false;
10
+ // New properties for editing
11
+ this.cursorIndex = 0;
12
+ this.scrollOffset = 0;
10
13
  this.id = options.id;
11
14
  this.options = { width: 30, type: 'text', ...options };
12
15
  }
13
16
  focus() {
14
17
  this.isFocused = true;
18
+ this.render();
15
19
  }
16
20
  blur() {
17
21
  this.isFocused = false;
22
+ this.render();
18
23
  }
19
24
  setValue(val) {
20
25
  this.value = val;
26
+ this.cursorIndex = val.length;
27
+ this.render();
21
28
  }
22
29
  handleKey(key) {
23
30
  if (!this.isFocused)
24
31
  return false;
32
+ let consumed = false;
25
33
  if (key.name === 'backspace') {
26
- this.value = this.value.slice(0, -1);
27
- this.render();
28
- return true;
34
+ if (this.cursorIndex > 0) {
35
+ this.value = this.value.slice(0, this.cursorIndex - 1) + this.value.slice(this.cursorIndex);
36
+ this.cursorIndex--;
37
+ consumed = true;
38
+ }
39
+ }
40
+ else if (key.name === 'delete') { // Forward delete
41
+ if (this.cursorIndex < this.value.length) {
42
+ this.value = this.value.slice(0, this.cursorIndex) + this.value.slice(this.cursorIndex + 1);
43
+ consumed = true;
44
+ }
45
+ }
46
+ else if (key.name === 'left') {
47
+ if (this.cursorIndex > 0) {
48
+ this.cursorIndex--;
49
+ consumed = true;
50
+ }
51
+ }
52
+ else if (key.name === 'right') {
53
+ if (this.cursorIndex < this.value.length) {
54
+ this.cursorIndex++;
55
+ consumed = true;
56
+ }
57
+ }
58
+ else if (key.name === 'home') {
59
+ if (this.cursorIndex > 0) {
60
+ this.cursorIndex = 0;
61
+ consumed = true;
62
+ }
63
+ }
64
+ else if (key.name === 'end') {
65
+ if (this.cursorIndex < this.value.length) {
66
+ this.cursorIndex = this.value.length;
67
+ consumed = true;
68
+ }
29
69
  }
30
70
  else if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
31
71
  // Basic text input filtering
32
- // Only accept printable characters (rough check)
33
72
  if (/^[\x20-\x7E]$/.test(key.sequence)) {
34
- this.value += key.sequence;
35
- this.render();
36
- return true;
73
+ this.value = this.value.slice(0, this.cursorIndex) + key.sequence + this.value.slice(this.cursorIndex);
74
+ this.cursorIndex++;
75
+ consumed = true;
37
76
  }
38
77
  }
78
+ if (consumed) {
79
+ this.render();
80
+ return true;
81
+ }
39
82
  // Did not consume (e.g., arrow keys, tabs)
40
83
  return false;
41
84
  }
@@ -51,25 +94,47 @@ class InputField {
51
94
  // Draw Input Box Background
52
95
  const inputX = x + labelLen;
53
96
  const maxWidth = (width || 30) - labelLen;
54
- let displayValue = this.value;
97
+ // Adjust scrollOffset to keep cursor in view
98
+ if (this.cursorIndex < this.scrollOffset) {
99
+ this.scrollOffset = this.cursorIndex;
100
+ }
101
+ else if (this.cursorIndex >= this.scrollOffset + maxWidth) {
102
+ this.scrollOffset = this.cursorIndex - maxWidth + 1;
103
+ }
104
+ // Clamp scrollOffset
105
+ if (this.scrollOffset < 0)
106
+ this.scrollOffset = 0;
107
+ let displayValue = this.value.substring(this.scrollOffset, this.scrollOffset + maxWidth);
55
108
  if (type === 'password') {
56
- displayValue = '*'.repeat(this.value.length);
109
+ displayValue = '*'.repeat(displayValue.length);
57
110
  }
58
- // Cursor logic
59
- const showCursor = this.isFocused;
60
- const cursorChar = '█';
61
- // Display content calculation
62
- // Ensure we don't overflow width
63
- if (displayValue.length >= maxWidth - 1) {
64
- const start = displayValue.length - (maxWidth - 2);
65
- displayValue = displayValue.substring(start);
111
+ // Prepare content with cursor
112
+ let renderedContent = "";
113
+ const cursorRelPos = this.cursorIndex - this.scrollOffset;
114
+ // Iterate through visible area
115
+ for (let i = 0; i < maxWidth; i++) {
116
+ const charIndex = this.scrollOffset + i;
117
+ let char = "";
118
+ if (charIndex < this.value.length) {
119
+ char = type === 'password' ? '*' : this.value[charIndex];
120
+ }
121
+ else {
122
+ char = " ";
123
+ }
124
+ if (this.isFocused && i === cursorRelPos) {
125
+ // Draw cursor
126
+ renderedContent += Styler_1.Styler.style(char === " " ? " " : char, 'bgGreen', 'black');
127
+ }
128
+ else {
129
+ renderedContent += char;
130
+ }
66
131
  }
67
- const fieldContent = displayValue + (showCursor ? Styler_1.Styler.style(cursorChar, 'green') : ' ');
68
- // Fill remaining space to clear old chars
69
- const paddingLen = Math.max(0, maxWidth - Styler_1.Styler.len(fieldContent));
70
- const padding = ' '.repeat(paddingLen);
132
+ // If cursor is at the very end (past last char), we need to handle it
133
+ // The loop above goes up to maxWidth.
134
+ // If cursorRelPos == displayValue.length, it might be drawn?
135
+ // Actually the loop ensures we draw maxWidth chars.
71
136
  const style = this.isFocused ? 'white' : 'gray';
72
- Screen_1.Screen.write(inputX, y, Styler_1.Styler.style(fieldContent + padding, style));
137
+ Screen_1.Screen.write(inputX, y, Styler_1.Styler.style(renderedContent, style));
73
138
  // Underline
74
139
  const underline = '─'.repeat(maxWidth);
75
140
  const underlineColor = this.isFocused ? 'brightGreen' : 'dim';
@@ -33,8 +33,7 @@ class Popup {
33
33
  });
34
34
  Screen_1.Screen.write(x + 2, y + boxHeight - 1, Styler_1.Styler.style("[Press Enter]", 'dim'));
35
35
  };
36
- const POPUP_Z_INDEX = 9999;
37
- Screen_1.Screen.mount(renderPopup, POPUP_Z_INDEX);
36
+ Screen_1.Screen.mount(renderPopup, Screen_1.Layer.MODAL);
38
37
  const handler = (key) => {
39
38
  if (key.name === 'return' || key.name === 'enter' || key.name === 'escape') {
40
39
  Screen_1.Screen.unmount(renderPopup);
@@ -1,5 +1,19 @@
1
1
  import { Rect } from './Screen';
2
2
  export { Rect };
3
+ export interface LayoutOptions {
4
+ direction: 'row' | 'column';
5
+ gap?: number;
6
+ children: LayoutItem[];
7
+ }
8
+ export interface LayoutItem {
9
+ weight?: number;
10
+ fixed?: number;
11
+ id?: string;
12
+ }
13
+ export interface LayoutResult {
14
+ rects: Rect[];
15
+ map: Map<string, Rect>;
16
+ }
3
17
  export declare class Layout {
4
18
  /**
5
19
  * Splits a rectangle vertically (into columns).
@@ -15,4 +29,8 @@ export declare class Layout {
15
29
  * Creates a padded inner rectangle.
16
30
  */
17
31
  static pad(rect: Rect, padding: number): Rect;
32
+ /**
33
+ * Advanced Layout Calculator (Flexbox-like)
34
+ */
35
+ static compute(parent: Rect, options: LayoutOptions): LayoutResult;
18
36
  }
@@ -61,5 +61,58 @@ class Layout {
61
61
  height: Math.max(0, rect.height - (padding * 2))
62
62
  };
63
63
  }
64
+ /**
65
+ * Advanced Layout Calculator (Flexbox-like)
66
+ */
67
+ static compute(parent, options) {
68
+ const { direction, gap = 0, children } = options;
69
+ const count = children.length;
70
+ if (count === 0)
71
+ return { rects: [], map: new Map() };
72
+ const totalGap = gap * (count - 1);
73
+ const availableSpace = (direction === 'row' ? parent.width : parent.height) - totalGap;
74
+ // 1. Calculate Fixed Sizes
75
+ let usedSpace = 0;
76
+ let totalWeight = 0;
77
+ children.forEach(child => {
78
+ if (child.fixed !== undefined) {
79
+ usedSpace += child.fixed;
80
+ }
81
+ else {
82
+ totalWeight += (child.weight || 1);
83
+ }
84
+ });
85
+ const remainingSpace = Math.max(0, availableSpace - usedSpace);
86
+ const unitSpace = totalWeight > 0 ? remainingSpace / totalWeight : 0;
87
+ // 2. Compute Rects
88
+ const rects = [];
89
+ const map = new Map();
90
+ let currentPos = direction === 'row' ? parent.x : parent.y;
91
+ children.forEach((child, i) => {
92
+ let size = 0;
93
+ if (child.fixed !== undefined) {
94
+ size = child.fixed;
95
+ }
96
+ else {
97
+ size = Math.floor((child.weight || 1) * unitSpace);
98
+ // Last dynamic item gets rounding dust?
99
+ // We should be careful about accumulating errors.
100
+ // But for now simple floor is okay, maybe we can improve later.
101
+ }
102
+ // Adjust for last item to fill gap if we used flooring?
103
+ // Actually, let's keep it simple.
104
+ const rect = {
105
+ x: direction === 'row' ? currentPos : parent.x,
106
+ y: direction === 'column' ? currentPos : parent.y,
107
+ width: direction === 'row' ? size : parent.width,
108
+ height: direction === 'column' ? size : parent.height
109
+ };
110
+ rects.push(rect);
111
+ if (child.id)
112
+ map.set(child.id, rect);
113
+ currentPos += size + gap;
114
+ });
115
+ return { rects, map };
116
+ }
64
117
  }
65
118
  exports.Layout = Layout;
@@ -4,6 +4,13 @@ export interface Rect {
4
4
  width: number;
5
5
  height: number;
6
6
  }
7
+ export declare const Layer: {
8
+ BACKGROUND: number;
9
+ CONTENT: number;
10
+ MODAL: number;
11
+ TOOLTIP: number;
12
+ MAX: number;
13
+ };
7
14
  export declare class Screen {
8
15
  private static isAlternateBuffer;
9
16
  private static resizeTimeout;
@@ -24,6 +31,7 @@ export declare class Screen {
24
31
  * Leaves the Alternate Screen Buffer and restores cursor.
25
32
  */
26
33
  static leave(): void;
34
+ private static setupSignalHandlers;
27
35
  private static resizeBuffers;
28
36
  /**
29
37
  * Schedules a render flush in the next tick.
@@ -51,7 +59,7 @@ export declare class Screen {
51
59
  /**
52
60
  * Registers a root component for the rendering loop (Painter's Algorithm).
53
61
  * @param renderFn The function that renders the component
54
- * @param zIndex Priority (higher draws on top)
62
+ * @param zIndex Priority (higher draws on top). Use Layer.* constants.
55
63
  */
56
64
  static mount(renderFn: () => void, zIndex?: number): void;
57
65
  /**
@@ -3,8 +3,17 @@
3
3
  // CORE: SCREEN MANAGER (The "Canvas")
4
4
  // ==========================================
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.Screen = void 0;
6
+ exports.Screen = exports.Layer = void 0;
7
7
  const Cursor_1 = require("./Cursor");
8
+ const Input_1 = require("./Input");
9
+ // Z-Index Layers
10
+ exports.Layer = {
11
+ BACKGROUND: 0,
12
+ CONTENT: 10,
13
+ MODAL: 100,
14
+ TOOLTIP: 1000,
15
+ MAX: 9999
16
+ };
8
17
  class Screen {
9
18
  /**
10
19
  * Enters the Alternate Screen Buffer (like Vim or Nano).
@@ -21,6 +30,8 @@ class Screen {
21
30
  this.resizeBuffers();
22
31
  this.scheduleRender();
23
32
  });
33
+ // Setup graceful shutdown signals
34
+ this.setupSignalHandlers();
24
35
  }
25
36
  /**
26
37
  * Leaves the Alternate Screen Buffer and restores cursor.
@@ -28,11 +39,23 @@ class Screen {
28
39
  static leave() {
29
40
  if (!this.isAlternateBuffer)
30
41
  return;
31
- // Removed stopLoop()
42
+ // Cleanup Input (disable mouse, reset raw mode)
43
+ Input_1.Input.reset();
32
44
  Cursor_1.Cursor.show();
33
45
  process.stdout.write('\x1b[?1049l'); // Leave alternate buffer
34
46
  this.isAlternateBuffer = false;
35
47
  }
48
+ static setupSignalHandlers() {
49
+ // Prevent multiple handlers if called multiple times
50
+ // But for static class, it's fine.
51
+ const cleanup = () => {
52
+ Screen.leave();
53
+ process.exit(0);
54
+ };
55
+ process.on('SIGINT', cleanup);
56
+ process.on('SIGTERM', cleanup);
57
+ // We can't catch SIGKILL
58
+ }
36
59
  static resizeBuffers() {
37
60
  this.width = process.stdout.columns || 80;
38
61
  this.height = process.stdout.rows || 24;
@@ -166,9 +189,9 @@ class Screen {
166
189
  /**
167
190
  * Registers a root component for the rendering loop (Painter's Algorithm).
168
191
  * @param renderFn The function that renders the component
169
- * @param zIndex Priority (higher draws on top)
192
+ * @param zIndex Priority (higher draws on top). Use Layer.* constants.
170
193
  */
171
- static mount(renderFn, zIndex = 0) {
194
+ static mount(renderFn, zIndex = exports.Layer.CONTENT) {
172
195
  this.renderRoots.push({ render: renderFn, zIndex });
173
196
  this.renderRoots.sort((a, b) => a.zIndex - b.zIndex);
174
197
  this.scheduleRender();
@@ -188,49 +211,18 @@ class Screen {
188
211
  static flush() {
189
212
  // Painter's Algorithm Phase: Re-run all render functions if any exist
190
213
  // This clears the buffer and redraws everything from scratch (logically)
191
- // But to keep performance, we might want to just let them draw over currentBuffer?
192
- // User asked for: "1. Xóa Buffer ảo. 2. Duyệt qua danh sách...".
193
- // If we have registered roots, we should follow this.
194
214
  if (this.renderRoots.length > 0) {
195
- // We need to clear currentBuffer effectively?
196
- // Or maybe just let them overwrite. If transparency is involved, clearing is safer.
197
- // But clearing everything is expensive if we just diff later.
198
- // The Diff algorithm handles the "screen update" optimization.
199
- // The "Virtual Buffer" update needs to be correct.
200
- // If we have layers, and top layer moves, we need to redraw bottom layer to see what's behind.
201
- // So YES, we must clear currentBuffer (or reset it to background state) and redraw all layers.
202
- // Reset currentBuffer to empty/clean state WITHOUT affecting previousBuffer (screen state)
215
+ // Reset currentBuffer to empty/clean state
203
216
  for (let y = 0; y < this.height; y++) {
204
217
  for (let x = 0; x < this.width; x++) {
205
218
  this.currentBuffer[y][x] = { char: ' ', style: '' };
206
219
  }
207
- // We don't mark dirtyRows here blindly, we mark them if they CHANGE in the diff phase.
208
- // Wait, dirtyRows optimization relies on us knowing which rows MIGHT have changed.
209
- // If we clear everything, we potentially change everything.
210
- // So we should mark all as dirty?
211
- // "Dirty" means "Virtual Buffer Row differs from Previous Buffer Row".
212
- // Since we are rebuilding Virtual Buffer, we don't know yet.
213
- // We should just run the diff loop on all rows?
214
- // Or maybe we can track which rows are touched during render?
215
220
  }
216
- // Render all layers
221
+ // Render all layers in sorted order
217
222
  for (const root of this.renderRoots) {
218
223
  root.render();
219
224
  }
220
- // Mark all rows as dirty for the Diff phase to check them?
221
- // Since we rebuilt the buffer, any row could be different from previousBuffer.
222
- // Optimization: Maybe only rows that were touched?
223
- // But 'write' marks dirtyRows.
224
- // So if we clear buffer (resetting chars), we are effectively writing spaces.
225
- // If we use 'clear()' method, it marks all dirty.
226
- // Let's rely on 'write' marking dirtyRows.
227
- // But we just did manual reset loop above.
228
- // Let's check:
229
- // If we reset manual loop, we are changing 'currentBuffer'.
230
- // Previous buffer holds what is on screen.
231
- // If we don't mark dirtyRows, flush() skips the row.
232
- // If screen has text, and we cleared it to spaces, and didn't mark dirty, screen stays text. Bad.
233
- // So yes, we should mark all dirty if we do a full clear-redraw cycle.
225
+ // Mark all rows as dirty because we rebuilt the buffer from scratch
234
226
  this.dirtyRows.fill(true);
235
227
  }
236
228
  if (!this.currentBuffer.length)
@@ -261,8 +253,6 @@ class Screen {
261
253
  const diff = x - (lastX + 1);
262
254
  if (diff > 0)
263
255
  output += `\x1b[${diff}C`;
264
- // If diff is 0 (next char), we do nothing.
265
- // Actually if x = lastX + 2, diff is 1. We need \x1b[1C.
266
256
  }
267
257
  else {
268
258
  // Absolute Move
@@ -279,23 +269,6 @@ class Screen {
279
269
  while (nextX < this.width) {
280
270
  const nextCell = this.currentBuffer[y][nextX];
281
271
  const nextPrev = this.previousBuffer[y][nextX];
282
- // We only batch if the NEXT cell also NEEDS update AND has SAME style
283
- // Actually, even if next cell DOES NOT need update,
284
- // if we write over it with same content, it's fine (redundant write but saves cursor move).
285
- // BUT, if we write over it, we might overwrite something valid with something else?
286
- // No, currentBuffer is truth.
287
- // Strategy:
288
- // 1. Only batch consecutive CHANGED cells?
289
- // 2. Or batch consecutive cells regardless, as long as style matches, to avoid cursor jumps?
290
- // If we skip a cell (not changed), we have to move cursor.
291
- // Moving cursor costs bytes (e.g. \x1b[C is 3 bytes).
292
- // Writing a char is 1 byte.
293
- // So overwriting valid char is cheaper than skipping 1-2 chars.
294
- // But if we skip 10 chars, move is cheaper.
295
- // Let's stick to simple logic: Only batch if next cell ALSO needs update OR we just overwrite it anyway to jump gap?
296
- // The user asked for "Gộp chuỗi ký tự liên tiếp có cùng style".
297
- // If we strictly follow diff, we only write changed cells.
298
- // So let's look for consecutive CHANGED cells with same style.
299
272
  if (nextCell.char !== nextPrev.char || nextCell.style !== nextPrev.style) {
300
273
  if (nextCell.style === cell.style) {
301
274
  batch += nextCell.char;
@@ -308,10 +281,6 @@ class Screen {
308
281
  }
309
282
  }
310
283
  else {
311
- // Next cell is not changed.
312
- // Should we continue batching to bridge the gap?
313
- // If gap is small (1-2 chars), yes.
314
- // But that complicates logic. Let's stop batching.
315
284
  break;
316
285
  }
317
286
  }
@@ -326,8 +295,6 @@ class Screen {
326
295
  }
327
296
  }
328
297
  if (output.length > 0) {
329
- // Reset style
330
- // output += '\x1b[0m';
331
298
  process.stdout.write(output);
332
299
  }
333
300
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "voonex",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A zero-dependency Terminal UI Library for Node.js.",
5
5
  "license": "MIT",
6
6
  "author": "CodeTease",
@@ -17,9 +17,9 @@
17
17
  "prepublishOnly": "npm run build"
18
18
  },
19
19
  "devDependencies": {
20
- "typescript": "5.9.3",
20
+ "@types/node": "^20.4.2",
21
21
  "ts-node": "10.9.2",
22
- "@types/node": "20.4.2"
22
+ "typescript": "5.9.3"
23
23
  },
24
24
  "files": [
25
25
  "dist",