buffalo-tui 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/README.md +196 -0
- package/USAGE.md +151 -0
- package/classes/colorManager.js +44 -0
- package/classes/element.js +31 -0
- package/classes/inputManager.js +155 -0
- package/classes/screen.js +206 -0
- package/elements/button.js +31 -0
- package/elements/frame.js +70 -0
- package/elements/input.js +43 -0
- package/elements/label.js +25 -0
- package/examples/demo.js +413 -0
- package/index.js +8 -0
- package/package.json +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Buffalo TUI Framework
|
|
2
|
+
A beginner friendly TUI framework available on various package managers for Node.js projects.
|
|
3
|
+
|
|
4
|
+
## Features
|
|
5
|
+
1. **Floating Windows** are movable, overlapping windows with ZIndex support.
|
|
6
|
+
2. **RGB Color Support** allows for full 24-bit RGB coloring with ANSI escape sequences.
|
|
7
|
+
3. **Input Handling** gives native keyboard event handling.
|
|
8
|
+
4. **Interactive Elements** such as input fields, buttons, and labels with support for keyboard navigation.
|
|
9
|
+
5. **Control Bar** allows you to write a useful status bar with nice keyboard shortcuts.
|
|
10
|
+
6. **Efficient Rendering** uses a buffer-based rendering system for seamless render updates.
|
|
11
|
+
|
|
12
|
+
## Install Buffalo
|
|
13
|
+
Buffalo is available for anyone on the [NPM]() marketplace.
|
|
14
|
+
|
|
15
|
+
> [!NOTE]
|
|
16
|
+
> As of **Tuesday, February 10th, 2026**, Buffalo does not support TypeScript type declarations!
|
|
17
|
+
|
|
18
|
+
Install Buffalo using the Node Package Manager:
|
|
19
|
+
```bash
|
|
20
|
+
npm install buffalo
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Getting Started
|
|
24
|
+
Writing your first TUI with Buffalo is extremely easy and can be done using the code below:
|
|
25
|
+
```javascript
|
|
26
|
+
import { Screen, Frame, ColorManager, InputManager } from "buffalo";
|
|
27
|
+
|
|
28
|
+
const screen = new Screen();
|
|
29
|
+
const inputManager = new InputManager();
|
|
30
|
+
|
|
31
|
+
screen.enterFullscreen();
|
|
32
|
+
|
|
33
|
+
// (sizeX, sizeY, posX, posY, windowTitle, color)
|
|
34
|
+
const frame = new Frame(
|
|
35
|
+
50, 20,
|
|
36
|
+
5, 2,
|
|
37
|
+
"My Buffalo Program",
|
|
38
|
+
new ColorManager().setColor(100, 150, 255)
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
frame.setBackgroundColor(15, 15, 25);
|
|
42
|
+
frame.addContent(2, 2, "Hello, Buffalo!", new ColorManager().setColor(255, 255, 100));
|
|
43
|
+
|
|
44
|
+
screen.addElement(frame);
|
|
45
|
+
screen.setControlBar("Press Q to quit");
|
|
46
|
+
|
|
47
|
+
inputManager.onAny((keyName) => {
|
|
48
|
+
if (keyName === "q" || keyName === "Q") {
|
|
49
|
+
inputManager.stop();
|
|
50
|
+
screen.exitFullscreen();
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
inputManager.start();
|
|
56
|
+
screen.render();
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Learn Buffalo through API Documentation
|
|
60
|
+
|
|
61
|
+
### Screen
|
|
62
|
+
Main class for managing the terminal display.
|
|
63
|
+
```javascript
|
|
64
|
+
const screen = new Screen();
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Properties:**
|
|
68
|
+
- `width` - Terminal width in characters
|
|
69
|
+
- `height` - Terminal height in characters
|
|
70
|
+
- `elements` - Array of elements to render
|
|
71
|
+
|
|
72
|
+
**Methods:**
|
|
73
|
+
- `enterFullscreen()` - Enter fullscreen mode
|
|
74
|
+
- `exitFullscreen()` - Exit fullscreen mode
|
|
75
|
+
- `render()` - Render all elements to terminal
|
|
76
|
+
- `addElement(element)` - Add an element to the screen
|
|
77
|
+
- `removeElement(element)` - Remove an element
|
|
78
|
+
- `setControlBar(text)` - Set control bar text
|
|
79
|
+
- `writeAt(x, y, char, color)` - Write a single character
|
|
80
|
+
- `writeString(x, y, str, color)` - Write a string
|
|
81
|
+
- `drawRect(x, y, width, height, filled, char, color)` - Draw a rectangle
|
|
82
|
+
- `drawLine(x1, y1, x2, y2, char, color)` - Draw a line
|
|
83
|
+
- `getUsableHeight()` - Get height minus control bar
|
|
84
|
+
|
|
85
|
+
### Frame
|
|
86
|
+
|
|
87
|
+
Floating window element with border, title, and content.
|
|
88
|
+
|
|
89
|
+
**Constructor:**
|
|
90
|
+
```javascript
|
|
91
|
+
new Frame(sizeX, sizeY, posX, posY, windowTitle, borderColor)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Parameters:**
|
|
95
|
+
- `sizeX` - Width of the frame
|
|
96
|
+
- `sizeY` - Height of the frame
|
|
97
|
+
- `posX` - X position on screen
|
|
98
|
+
- `posY` - Y position on screen
|
|
99
|
+
- `windowTitle` - Title displayed on top border
|
|
100
|
+
- `borderColor` - ColorManager instance for border
|
|
101
|
+
|
|
102
|
+
**Methods:**
|
|
103
|
+
- `setWindowTitle(title)` - Update window title
|
|
104
|
+
- `setBorderColor(R, G, B)` - Set border color (0-255 RGB)
|
|
105
|
+
- `setBackgroundColor(R, G, B)` - Set background color
|
|
106
|
+
- `addContent(x, y, text, color)` - Add text content at position
|
|
107
|
+
- `clearContent()` - Remove all content
|
|
108
|
+
- `show()` - Make frame visible
|
|
109
|
+
- `hide()` - Make frame invisible
|
|
110
|
+
- `setZIndex(z)` - Set layering order (higher = front)
|
|
111
|
+
|
|
112
|
+
### Input
|
|
113
|
+
|
|
114
|
+
Text input field element.
|
|
115
|
+
|
|
116
|
+
**Constructor:**
|
|
117
|
+
```javascript
|
|
118
|
+
new Input(posX, posY, width, label, color)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Parameters:**
|
|
122
|
+
- `posX` - X position on screen
|
|
123
|
+
- `posY` - Y position on screen
|
|
124
|
+
- `width` - Width of input field
|
|
125
|
+
- `label` - Label text above input
|
|
126
|
+
- `color` - ColorManager instance (optional)
|
|
127
|
+
|
|
128
|
+
**Methods:**
|
|
129
|
+
- `setValue(value)` - Set input value
|
|
130
|
+
- `getValue()` - Get current value
|
|
131
|
+
- `setFocus(focused)` - Set focus state
|
|
132
|
+
- `show()` / `hide()` - Visibility control
|
|
133
|
+
|
|
134
|
+
### Button
|
|
135
|
+
|
|
136
|
+
Clickable button element.
|
|
137
|
+
|
|
138
|
+
**Constructor:**
|
|
139
|
+
```javascript
|
|
140
|
+
new Button(posX, posY, text, color)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Parameters:**
|
|
144
|
+
- `posX` - X position on screen
|
|
145
|
+
- `posY` - Y position on screen
|
|
146
|
+
- `text` - Button text
|
|
147
|
+
- `color` - ColorManager instance (optional)
|
|
148
|
+
|
|
149
|
+
**Methods:**
|
|
150
|
+
- `setText(text)` - Update button text
|
|
151
|
+
- `setHovered(hovered)` - Set hover state
|
|
152
|
+
- `show()` / `hide()` - Visibility control
|
|
153
|
+
|
|
154
|
+
### Label
|
|
155
|
+
|
|
156
|
+
Static text label element.
|
|
157
|
+
|
|
158
|
+
**Constructor:**
|
|
159
|
+
```javascript
|
|
160
|
+
new Label(posX, posY, text, color)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Parameters:**
|
|
164
|
+
- `posX` - X position on screen
|
|
165
|
+
- `posY` - Y position on screen
|
|
166
|
+
- `text` - Label text
|
|
167
|
+
- `color` - ColorManager instance (optional)
|
|
168
|
+
|
|
169
|
+
**Methods:**
|
|
170
|
+
- `setText(text)` - Update label text
|
|
171
|
+
- `setColor(R, G, B)` - Set text color
|
|
172
|
+
- `show()` / `hide()` - Visibility control
|
|
173
|
+
|
|
174
|
+
### ColorManager
|
|
175
|
+
|
|
176
|
+
RGB color management with ANSI conversion.
|
|
177
|
+
|
|
178
|
+
**Methods:**
|
|
179
|
+
- `setColor(R, G, B)` - Set RGB color (0-255 each)
|
|
180
|
+
- `getColor()` - Returns `{R, G, B}` object
|
|
181
|
+
- `toANSIForeground()` - Convert to ANSI foreground code
|
|
182
|
+
- `toANSIBackground()` - Convert to ANSI background code
|
|
183
|
+
- `static reset()` - Get ANSI reset code
|
|
184
|
+
|
|
185
|
+
### InputManager
|
|
186
|
+
|
|
187
|
+
Keyboard input handling.
|
|
188
|
+
|
|
189
|
+
**Methods:**
|
|
190
|
+
- `start()` - Start capturing keyboard input
|
|
191
|
+
- `stop()` - Stop capturing input
|
|
192
|
+
- `onAny(callback)` - Register callback for any key: `(keyName, rawKey) => {}`
|
|
193
|
+
|
|
194
|
+
**Key Events:**
|
|
195
|
+
- `keyName` - Human-readable key name
|
|
196
|
+
- `rawKey` - Raw escape sequence
|
package/USAGE.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Buffalo - Usage Examples
|
|
2
|
+
|
|
3
|
+
## Basic Setup
|
|
4
|
+
|
|
5
|
+
```javascript
|
|
6
|
+
import { Screen, Frame, ColorManager } from 'buffalo';
|
|
7
|
+
|
|
8
|
+
const screen = new Screen();
|
|
9
|
+
screen.enterFullscreen();
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Creating a Simple Window
|
|
13
|
+
|
|
14
|
+
```javascript
|
|
15
|
+
const myFrame = new Frame(
|
|
16
|
+
50, // width
|
|
17
|
+
15, // height
|
|
18
|
+
10, // x position
|
|
19
|
+
5, // y position
|
|
20
|
+
'My Window', // title
|
|
21
|
+
new ColorManager().setColor(100, 150, 255) // border color (RGB)
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
// Add the frame to the screen
|
|
25
|
+
screen.addElement(myFrame);
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Adding Content to Frames
|
|
29
|
+
|
|
30
|
+
```javascript
|
|
31
|
+
// Add text at position (x, y) relative to frame interior
|
|
32
|
+
myFrame.addContent(1, 1, 'Hello, World!', new ColorManager().setColor(255, 255, 0));
|
|
33
|
+
myFrame.addContent(1, 3, 'Another line', new ColorManager().setColor(200, 200, 200));
|
|
34
|
+
|
|
35
|
+
// Set background color
|
|
36
|
+
myFrame.setBackgroundColor(20, 20, 40);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Control Bar
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
// Set text in the bottom control bar
|
|
43
|
+
screen.setControlBar('Press Q to quit | Press H for help');
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Rendering
|
|
47
|
+
|
|
48
|
+
```javascript
|
|
49
|
+
// Render once
|
|
50
|
+
screen.render();
|
|
51
|
+
|
|
52
|
+
// Or set up an animation loop
|
|
53
|
+
setInterval(() => {
|
|
54
|
+
// Update content
|
|
55
|
+
myFrame.clearContent();
|
|
56
|
+
myFrame.addContent(1, 1, `Time: ${Date.now()}`);
|
|
57
|
+
|
|
58
|
+
// Render
|
|
59
|
+
screen.render();
|
|
60
|
+
}, 100);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Drawing Primitives
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
// Draw a rectangle (can be filled or outlined)
|
|
67
|
+
screen.drawRect(10, 10, 20, 10, false, '█', new ColorManager().setColor(255, 0, 0));
|
|
68
|
+
|
|
69
|
+
// Draw a line
|
|
70
|
+
screen.drawLine(5, 5, 30, 5, '─', new ColorManager().setColor(0, 255, 0));
|
|
71
|
+
|
|
72
|
+
// Write text directly to buffer
|
|
73
|
+
screen.writeString(15, 15, 'Direct text!', new ColorManager().setColor(255, 255, 255));
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Color Management
|
|
77
|
+
|
|
78
|
+
```javascript
|
|
79
|
+
// Create colors
|
|
80
|
+
const red = new ColorManager().setColor(255, 0, 0);
|
|
81
|
+
const green = new ColorManager().setColor(0, 255, 0);
|
|
82
|
+
const blue = new ColorManager().setColor(0, 0, 255);
|
|
83
|
+
|
|
84
|
+
// Get RGB values
|
|
85
|
+
const rgb = red.getColor(); // { R: 255, G: 0, B: 0 }
|
|
86
|
+
|
|
87
|
+
// Colors are automatically converted to ANSI codes for terminal display
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Keyboard Input
|
|
91
|
+
|
|
92
|
+
```javascript
|
|
93
|
+
import * as readline from 'readline';
|
|
94
|
+
|
|
95
|
+
readline.emitKeypressEvents(process.stdin);
|
|
96
|
+
if (process.stdin.isTTY) {
|
|
97
|
+
process.stdin.setRawMode(true);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
process.stdin.on('keypress', (str, key) => {
|
|
101
|
+
if (key.name === 'q') {
|
|
102
|
+
screen.cleanup();
|
|
103
|
+
process.exit(0);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (key.name === 'up') {
|
|
107
|
+
// Handle up arrow
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Multiple Overlapping Frames
|
|
113
|
+
|
|
114
|
+
```javascript
|
|
115
|
+
const frame1 = new Frame(40, 10, 5, 5, 'Frame 1', new ColorManager().setColor(255, 100, 100));
|
|
116
|
+
frame1.zIndex = 0;
|
|
117
|
+
|
|
118
|
+
const frame2 = new Frame(40, 10, 15, 8, 'Frame 2', new ColorManager().setColor(100, 255, 100));
|
|
119
|
+
frame2.zIndex = 1; // Will render on top
|
|
120
|
+
|
|
121
|
+
screen.addElement(frame1);
|
|
122
|
+
screen.addElement(frame2);
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Cleanup
|
|
126
|
+
|
|
127
|
+
```javascript
|
|
128
|
+
function cleanup() {
|
|
129
|
+
screen.cleanup(); // Restores terminal to normal mode
|
|
130
|
+
process.stdin.setRawMode(false);
|
|
131
|
+
process.exit(0);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Handle process termination
|
|
135
|
+
process.on('SIGINT', cleanup);
|
|
136
|
+
process.on('SIGTERM', cleanup);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Complete Example
|
|
140
|
+
|
|
141
|
+
See `example.js` for a simple working example.
|
|
142
|
+
See `demo.js` for a comprehensive demo with multiple features.
|
|
143
|
+
|
|
144
|
+
## Tips
|
|
145
|
+
|
|
146
|
+
- Always call `screen.enterFullscreen()` before rendering
|
|
147
|
+
- Always call `screen.cleanup()` before exiting
|
|
148
|
+
- Use `setInterval()` for animations and live updates
|
|
149
|
+
- The control bar takes up the bottom 3 lines
|
|
150
|
+
- Use `getUsableHeight()` to get drawing area excluding control bar
|
|
151
|
+
- Colors are clamped to 0-255 range automatically
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// only supports RGB values because it's the only kind of
|
|
2
|
+
// color codes I have memorized
|
|
3
|
+
export class ColorManager {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.R = 255;
|
|
6
|
+
this.G = 255;
|
|
7
|
+
this.B = 255;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
setColor(R, G, B) {
|
|
11
|
+
this.R = Math.max(0, Math.min(255, R));
|
|
12
|
+
this.G = Math.max(0, Math.min(255, G));
|
|
13
|
+
this.B = Math.max(0, Math.min(255, B));
|
|
14
|
+
return this;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
getColor() {
|
|
18
|
+
return { R: this.R, G: this.G, B: this.B };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
toANSIForeground() {
|
|
22
|
+
return `\x1b[38;2;${this.R};${this.G};${this.B}m`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
toANSIBackground() {
|
|
26
|
+
return `\x1b[48;2;${this.R};${this.G};${this.B}m`;
|
|
27
|
+
}
|
|
28
|
+
toANSI256() {
|
|
29
|
+
if (this.R === this.G && this.G === this.B) {
|
|
30
|
+
if (this.R < 8) return 16;
|
|
31
|
+
if (this.R > 248) return 231;
|
|
32
|
+
return Math.round(((this.R - 8) / 247) * 24) + 232;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const r = Math.round((this.R / 255) * 5);
|
|
36
|
+
const g = Math.round((this.G / 255) * 5);
|
|
37
|
+
const b = Math.round((this.B / 255) * 5);
|
|
38
|
+
return 16 + 36 * r + 6 * g + b;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static reset() {
|
|
42
|
+
return "\x1b[0m";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const properties = {
|
|
2
|
+
// hardcoded element list with what properties each has because I was too lazy
|
|
3
|
+
// to automize it lol
|
|
4
|
+
frame: {
|
|
5
|
+
sizeX: 'number',
|
|
6
|
+
sizeY: 'number',
|
|
7
|
+
posX: 'number',
|
|
8
|
+
posY: 'number',
|
|
9
|
+
windowTitle: 'string',
|
|
10
|
+
borderColor: 'ColorManager',
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class Element {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.visible = true;
|
|
17
|
+
this.zIndex = 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
show() {
|
|
21
|
+
this.visible = true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
hide() {
|
|
25
|
+
this.visible = false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
setZIndex(z) {
|
|
29
|
+
this.zIndex = z;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
export class InputManager {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.listeners = {};
|
|
4
|
+
this.focusedElement = null;
|
|
5
|
+
this.isListening = false;
|
|
6
|
+
this.keyBuffer = '';
|
|
7
|
+
this.inputHandlers = [];
|
|
8
|
+
this.specialKeys = {
|
|
9
|
+
'up': '\u001b[A',
|
|
10
|
+
'down': '\u001b[B',
|
|
11
|
+
'right': '\u001b[C',
|
|
12
|
+
'left': '\u001b[D',
|
|
13
|
+
'enter': '\r',
|
|
14
|
+
'escape': '\u001b',
|
|
15
|
+
'backspace': '\x7f',
|
|
16
|
+
'tab': '\t'
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
start() {
|
|
21
|
+
if (this.isListening) return;
|
|
22
|
+
|
|
23
|
+
this.isListening = true;
|
|
24
|
+
this.setupStdin();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
stop() {
|
|
28
|
+
this.isListening = false;
|
|
29
|
+
if (process.stdin.isTTY) {
|
|
30
|
+
process.stdin.setRawMode(false);
|
|
31
|
+
}
|
|
32
|
+
process.stdin.removeAllListeners('data');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
setupStdin() {
|
|
36
|
+
if (!process.stdin.isTTY) {
|
|
37
|
+
console.warn('Warning: InputManager requires TTY mode');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
process.stdin.setRawMode(true);
|
|
42
|
+
process.stdin.resume();
|
|
43
|
+
process.stdin.setEncoding('utf8');
|
|
44
|
+
|
|
45
|
+
process.stdin.on('data', (key) => {
|
|
46
|
+
this.handleKeyPress(key);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
handleKeyPress(key) {
|
|
51
|
+
if (key === '\u0003') {
|
|
52
|
+
this.stop();
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this.keyBuffer = key;
|
|
57
|
+
const keyName = this.identifyKey(key);
|
|
58
|
+
|
|
59
|
+
if (this.listeners['any']) {
|
|
60
|
+
this.listeners['any'].forEach(callback => callback(keyName, key));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (this.listeners[keyName]) {
|
|
64
|
+
this.listeners[keyName].forEach(callback => callback(key));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (this.focusedElement && this.focusedElement.handleInput) {
|
|
68
|
+
this.focusedElement.handleInput(keyName, key);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
this.inputHandlers.forEach(handler => {
|
|
72
|
+
if (handler.predicate ? handler.predicate(keyName) : true) {
|
|
73
|
+
handler.callback(keyName, key);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
identifyKey(key) {
|
|
79
|
+
for (let name in this.specialKeys) {
|
|
80
|
+
if (this.specialKeys[name] === key) {
|
|
81
|
+
return name;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (key.length === 1) {
|
|
86
|
+
return key;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return `unknown_${key.charCodeAt(0)}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
on(keyName, callback) {
|
|
93
|
+
if (!this.listeners[keyName]) {
|
|
94
|
+
this.listeners[keyName] = [];
|
|
95
|
+
}
|
|
96
|
+
this.listeners[keyName].push(callback);
|
|
97
|
+
return this;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
onAny(callback) {
|
|
101
|
+
return this.on('any', callback);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
off(keyName, callback) {
|
|
105
|
+
if (this.listeners[keyName]) {
|
|
106
|
+
this.listeners[keyName] = this.listeners[keyName].filter(cb => cb !== callback);
|
|
107
|
+
}
|
|
108
|
+
return this;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
addHandler(callback, predicate = null) {
|
|
112
|
+
this.inputHandlers.push({ callback, predicate });
|
|
113
|
+
return this;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
removeHandler(callback) {
|
|
117
|
+
this.inputHandlers = this.inputHandlers.filter(h => h.callback !== callback);
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
setFocus(element) {
|
|
122
|
+
if (this.focusedElement && this.focusedElement.onBlur) {
|
|
123
|
+
this.focusedElement.onBlur();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this.focusedElement = element;
|
|
127
|
+
|
|
128
|
+
if (element && element.onFocus) {
|
|
129
|
+
element.onFocus();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return this;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
getFocus() {
|
|
136
|
+
return this.focusedElement;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
clearListeners(keyName = 'all') {
|
|
140
|
+
if (keyName === 'all') {
|
|
141
|
+
this.listeners = {};
|
|
142
|
+
} else {
|
|
143
|
+
delete this.listeners[keyName];
|
|
144
|
+
}
|
|
145
|
+
return this;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
getLastKey() {
|
|
149
|
+
return this.keyBuffer;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
isActive() {
|
|
153
|
+
return this.isListening;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { ColorManager } from './colorManager.js';
|
|
2
|
+
|
|
3
|
+
export class Screen {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.width = process.stdout.columns || 80;
|
|
6
|
+
this.height = process.stdout.rows || 24;
|
|
7
|
+
this.buffer = [];
|
|
8
|
+
this.colorBuffer = [];
|
|
9
|
+
this.elements = [];
|
|
10
|
+
this.controlBarHeight = 3;
|
|
11
|
+
this.controlBarText = '';
|
|
12
|
+
|
|
13
|
+
this.clearBuffer();
|
|
14
|
+
|
|
15
|
+
process.stdout.on('resize', () => {
|
|
16
|
+
this.width = process.stdout.columns;
|
|
17
|
+
this.height = process.stdout.rows;
|
|
18
|
+
this.clearBuffer();
|
|
19
|
+
this.render();
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
clearBuffer() {
|
|
24
|
+
this.buffer = [];
|
|
25
|
+
this.colorBuffer = [];
|
|
26
|
+
|
|
27
|
+
for (let y = 0; y < this.height; y++) {
|
|
28
|
+
this.buffer[y] = [];
|
|
29
|
+
this.colorBuffer[y] = [];
|
|
30
|
+
for (let x = 0; x < this.width; x++) {
|
|
31
|
+
this.buffer[y][x] = ' ';
|
|
32
|
+
this.colorBuffer[y][x] = null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
clear() {
|
|
38
|
+
this.clearBuffer();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
writeAt(x, y, char, color = null) {
|
|
42
|
+
if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
|
|
43
|
+
this.buffer[y][x] = char || ' ';
|
|
44
|
+
this.colorBuffer[y][x] = color;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
writeString(x, y, str, color = null) {
|
|
49
|
+
for (let i = 0; i < str.length; i++) {
|
|
50
|
+
this.writeAt(x + i, y, str[i], color);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
drawRect(x, y, width, height, filled = false, char = '█', borderColor = null) {
|
|
55
|
+
if (filled) {
|
|
56
|
+
for (let dy = 0; dy < height; dy++) {
|
|
57
|
+
for (let dx = 0; dx < width; dx++) {
|
|
58
|
+
this.writeAt(x + dx, y + dy, char, borderColor);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
for (let dx = 0; dx < width; dx++) {
|
|
63
|
+
this.writeAt(x + dx, y, '─', borderColor);
|
|
64
|
+
this.writeAt(x + dx, y + height - 1, '─', borderColor);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (let dy = 0; dy < height; dy++) {
|
|
68
|
+
this.writeAt(x, y + dy, '│', borderColor);
|
|
69
|
+
this.writeAt(x + width - 1, y + dy, '│', borderColor);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
this.writeAt(x, y, '┌', borderColor);
|
|
73
|
+
this.writeAt(x + width - 1, y, '┐', borderColor);
|
|
74
|
+
this.writeAt(x, y + height - 1, '└', borderColor);
|
|
75
|
+
this.writeAt(x + width - 1, y + height - 1, '┘', borderColor);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
drawLine(x1, y1, x2, y2, char = '─', color = null) {
|
|
80
|
+
const dx = Math.abs(x2 - x1);
|
|
81
|
+
const dy = Math.abs(y2 - y1);
|
|
82
|
+
const sx = x1 < x2 ? 1 : -1;
|
|
83
|
+
const sy = y1 < y2 ? 1 : -1;
|
|
84
|
+
let err = dx - dy;
|
|
85
|
+
|
|
86
|
+
let x = x1;
|
|
87
|
+
let y = y1;
|
|
88
|
+
|
|
89
|
+
while (true) {
|
|
90
|
+
this.writeAt(x, y, char, color);
|
|
91
|
+
|
|
92
|
+
if (x === x2 && y === y2) break;
|
|
93
|
+
|
|
94
|
+
const e2 = 2 * err;
|
|
95
|
+
if (e2 > -dy) {
|
|
96
|
+
err -= dy;
|
|
97
|
+
x += sx;
|
|
98
|
+
}
|
|
99
|
+
if (e2 < dx) {
|
|
100
|
+
err += dx;
|
|
101
|
+
y += sy;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
addElement(element) {
|
|
107
|
+
this.elements.push(element);
|
|
108
|
+
this.elements.sort((a, b) => (a.zIndex || 0) - (b.zIndex || 0));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
removeElement(element) {
|
|
112
|
+
const index = this.elements.indexOf(element);
|
|
113
|
+
if (index > -1) {
|
|
114
|
+
this.elements.splice(index, 1);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
setControlBar(text) {
|
|
119
|
+
this.controlBarText = text;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
drawControlBar() {
|
|
123
|
+
const barY = this.height - this.controlBarHeight;
|
|
124
|
+
const barColor = new ColorManager().setColor(200, 200, 200);
|
|
125
|
+
const bgColor = new ColorManager().setColor(40, 40, 40);
|
|
126
|
+
|
|
127
|
+
for (let x = 0; x < this.width; x++) {
|
|
128
|
+
this.writeAt(x, barY, '═', barColor);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (let y = barY + 1; y < this.height; y++) {
|
|
132
|
+
for (let x = 0; x < this.width; x++) {
|
|
133
|
+
this.writeAt(x, y, ' ', bgColor);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (this.controlBarText) {
|
|
138
|
+
this.writeString(2, barY + 1, this.controlBarText, new ColorManager().setColor(255, 255, 255));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
renderElements() {
|
|
143
|
+
for (const element of this.elements) {
|
|
144
|
+
if (element.visible !== false && typeof element.drawToBuffer === 'function') {
|
|
145
|
+
element.drawToBuffer(this);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
render() {
|
|
151
|
+
this.clearBuffer();
|
|
152
|
+
this.renderElements();
|
|
153
|
+
this.drawControlBar();
|
|
154
|
+
|
|
155
|
+
process.stdout.write('\x1b[H\x1b[?25l');
|
|
156
|
+
|
|
157
|
+
let output = '';
|
|
158
|
+
let lastColor = null;
|
|
159
|
+
|
|
160
|
+
for (let y = 0; y < this.height; y++) {
|
|
161
|
+
for (let x = 0; x < this.width; x++) {
|
|
162
|
+
const char = this.buffer[y][x];
|
|
163
|
+
const color = this.colorBuffer[y][x];
|
|
164
|
+
|
|
165
|
+
if (color && color !== lastColor) {
|
|
166
|
+
output += color.toANSIForeground();
|
|
167
|
+
lastColor = color;
|
|
168
|
+
} else if (!color && lastColor) {
|
|
169
|
+
output += ColorManager.reset();
|
|
170
|
+
lastColor = null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
output += char;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (y < this.height - 1) {
|
|
177
|
+
output += '\n';
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (lastColor) {
|
|
182
|
+
output += ColorManager.reset();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
process.stdout.write(output);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
enterFullscreen() {
|
|
189
|
+
process.stdout.write('\x1b[?1049h');
|
|
190
|
+
process.stdout.write('\x1b[2J');
|
|
191
|
+
process.stdout.write('\x1b[?25l');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
exitFullscreen() {
|
|
195
|
+
process.stdout.write('\x1b[?25h');
|
|
196
|
+
process.stdout.write('\x1b[?1049l');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
getUsableHeight() {
|
|
200
|
+
return this.height - this.controlBarHeight;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
cleanup() {
|
|
204
|
+
this.exitFullscreen();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Element } from "../classes/element.js";
|
|
2
|
+
import { ColorManager } from "../classes/colorManager.js";
|
|
3
|
+
|
|
4
|
+
export class Button extends Element {
|
|
5
|
+
constructor(posX, posY, text, color = null) {
|
|
6
|
+
super();
|
|
7
|
+
this.posX = posX;
|
|
8
|
+
this.posY = posY;
|
|
9
|
+
this.text = text;
|
|
10
|
+
this.color = color || new ColorManager().setColor(255, 255, 255);
|
|
11
|
+
this.hovered = false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
setText(text) {
|
|
15
|
+
this.text = text;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
setHovered(hovered) {
|
|
19
|
+
this.hovered = hovered;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
drawToBuffer(screen) {
|
|
23
|
+
if (!this.visible) return;
|
|
24
|
+
|
|
25
|
+
const buttonColor = this.hovered
|
|
26
|
+
? new ColorManager().setColor(100, 255, 100)
|
|
27
|
+
: this.color;
|
|
28
|
+
|
|
29
|
+
screen.writeString(this.posX, this.posY, `[ ${this.text} ]`, buttonColor);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { properties, Element } from "../classes/element.js";
|
|
2
|
+
import { ColorManager } from "../classes/colorManager.js";
|
|
3
|
+
|
|
4
|
+
export class Frame extends Element {
|
|
5
|
+
sizeX = 0;
|
|
6
|
+
sizeY = 0;
|
|
7
|
+
posX = 0;
|
|
8
|
+
posY = 0;
|
|
9
|
+
windowTitle = "";
|
|
10
|
+
borderColor = new ColorManager().setColor(255, 255, 255);
|
|
11
|
+
backgroundColor = null;
|
|
12
|
+
content = [];
|
|
13
|
+
visible = true;
|
|
14
|
+
zIndex = 0;
|
|
15
|
+
|
|
16
|
+
constructor(sizeX, sizeY, posX, posY, windowTitle, borderColor) {
|
|
17
|
+
super();
|
|
18
|
+
this.sizeX = sizeX;
|
|
19
|
+
this.sizeY = sizeY;
|
|
20
|
+
this.posX = posX;
|
|
21
|
+
this.posY = posY;
|
|
22
|
+
this.windowTitle = windowTitle || "";
|
|
23
|
+
this.borderColor = borderColor || new ColorManager().setColor(255, 255, 255);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
setWindowTitle(title) {
|
|
27
|
+
this.windowTitle = title;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
setBorderColor(R, G, B) {
|
|
31
|
+
this.borderColor = new ColorManager().setColor(R, G, B);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
setBackgroundColor(R, G, B) {
|
|
35
|
+
this.backgroundColor = new ColorManager().setColor(R, G, B);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
addContent(x, y, text, color = null) {
|
|
39
|
+
this.content.push({ x, y, text, color });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
clearContent() {
|
|
43
|
+
this.content = [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
drawToBuffer(screen) {
|
|
47
|
+
if (!this.visible) return;
|
|
48
|
+
if (this.backgroundColor) {
|
|
49
|
+
for (let y = 1; y < this.sizeY - 1; y++) {
|
|
50
|
+
for (let x = 1; x < this.sizeX - 1; x++) {
|
|
51
|
+
screen.writeAt(this.posX + x, this.posY + y, ' ', this.backgroundColor);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
screen.drawRect(this.posX, this.posY, this.sizeX, this.sizeY, false, '█', this.borderColor);
|
|
57
|
+
|
|
58
|
+
if (this.windowTitle && this.sizeX > 4) {
|
|
59
|
+
const titleText = ` ${this.windowTitle} `;
|
|
60
|
+
const titleX = this.posX + Math.floor((this.sizeX - titleText.length) / 2);
|
|
61
|
+
screen.writeString(titleX, this.posY, titleText, this.borderColor);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const item of this.content) {
|
|
65
|
+
const contentX = this.posX + item.x + 1;
|
|
66
|
+
const contentY = this.posY + item.y + 1;
|
|
67
|
+
screen.writeString(contentX, contentY, item.text, item.color);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Element } from "../classes/element.js";
|
|
2
|
+
import { ColorManager } from "../classes/colorManager.js";
|
|
3
|
+
|
|
4
|
+
export class Input extends Element {
|
|
5
|
+
constructor(posX, posY, width, label, color = null) {
|
|
6
|
+
super();
|
|
7
|
+
this.posX = posX;
|
|
8
|
+
this.posY = posY;
|
|
9
|
+
this.width = width;
|
|
10
|
+
this.label = label;
|
|
11
|
+
this.value = "";
|
|
12
|
+
this.color = color || new ColorManager().setColor(255, 255, 255);
|
|
13
|
+
this.focused = false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
setValue(value) {
|
|
17
|
+
this.value = value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
getValue() {
|
|
21
|
+
return this.value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
setFocus(focused) {
|
|
25
|
+
this.focused = focused;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
drawToBuffer(screen) {
|
|
29
|
+
if (!this.visible) return;
|
|
30
|
+
|
|
31
|
+
if (this.label) {
|
|
32
|
+
screen.writeString(this.posX, this.posY, this.label, this.color);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const inputY = this.label ? this.posY + 1 : this.posY;
|
|
36
|
+
const display = this.value.padEnd(this.width, ' ').substring(0, this.width);
|
|
37
|
+
const inputColor = this.focused
|
|
38
|
+
? new ColorManager().setColor(100, 200, 255)
|
|
39
|
+
: new ColorManager().setColor(150, 150, 150);
|
|
40
|
+
|
|
41
|
+
screen.writeString(this.posX, inputY, `[${display}]`, inputColor);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Element } from "../classes/element.js";
|
|
2
|
+
import { ColorManager } from "../classes/colorManager.js";
|
|
3
|
+
|
|
4
|
+
export class Label extends Element {
|
|
5
|
+
constructor(posX, posY, text, color = null) {
|
|
6
|
+
super();
|
|
7
|
+
this.posX = posX;
|
|
8
|
+
this.posY = posY;
|
|
9
|
+
this.text = text;
|
|
10
|
+
this.color = color || new ColorManager().setColor(255, 255, 255);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
setText(text) {
|
|
14
|
+
this.text = text;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
setColor(R, G, B) {
|
|
18
|
+
this.color = new ColorManager().setColor(R, G, B);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
drawToBuffer(screen) {
|
|
22
|
+
if (!this.visible) return;
|
|
23
|
+
screen.writeString(this.posX, this.posY, this.text, this.color);
|
|
24
|
+
}
|
|
25
|
+
}
|
package/examples/demo.js
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Screen,
|
|
3
|
+
Frame,
|
|
4
|
+
ColorManager,
|
|
5
|
+
InputManager,
|
|
6
|
+
} from "../index.js";
|
|
7
|
+
import os from "os";
|
|
8
|
+
|
|
9
|
+
const screen = new Screen();
|
|
10
|
+
const inputManager = new InputManager();
|
|
11
|
+
|
|
12
|
+
screen.enterFullscreen();
|
|
13
|
+
|
|
14
|
+
let counter = 0;
|
|
15
|
+
let selectedElement = 0;
|
|
16
|
+
let inputValue = "";
|
|
17
|
+
let buttonPressed = false;
|
|
18
|
+
let hideOtherWindows = false;
|
|
19
|
+
|
|
20
|
+
const sampleElements = [
|
|
21
|
+
{ type: "input", name: "Text Input" },
|
|
22
|
+
{ type: "button", name: "Submit Button" },
|
|
23
|
+
{ type: "button", name: "Increment Counter" },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const windows = [
|
|
27
|
+
{
|
|
28
|
+
frame: new Frame(
|
|
29
|
+
50,
|
|
30
|
+
20,
|
|
31
|
+
5,
|
|
32
|
+
2,
|
|
33
|
+
"System Performance",
|
|
34
|
+
new ColorManager().setColor(100, 200, 255),
|
|
35
|
+
),
|
|
36
|
+
update: updateSystemPerformance,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
frame: new Frame(
|
|
40
|
+
55,
|
|
41
|
+
25,
|
|
42
|
+
15,
|
|
43
|
+
5,
|
|
44
|
+
"Sample Features",
|
|
45
|
+
new ColorManager().setColor(100, 255, 150),
|
|
46
|
+
),
|
|
47
|
+
update: updateSampleFeatures,
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
windows.forEach((win) => {
|
|
52
|
+
win.frame.setBackgroundColor(15, 15, 25);
|
|
53
|
+
screen.addElement(win.frame);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
let activeWindowIndex = 0;
|
|
57
|
+
|
|
58
|
+
function getCPUUsage() {
|
|
59
|
+
const cpus = os.cpus();
|
|
60
|
+
let totalIdle = 0;
|
|
61
|
+
let totalTick = 0;
|
|
62
|
+
|
|
63
|
+
cpus.forEach((cpu) => {
|
|
64
|
+
for (let type in cpu.times) {
|
|
65
|
+
totalTick += cpu.times[type];
|
|
66
|
+
}
|
|
67
|
+
totalIdle += cpu.times.idle;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const idle = totalIdle / cpus.length;
|
|
71
|
+
const total = totalTick / cpus.length;
|
|
72
|
+
const usage = 100 - ~~((100 * idle) / total);
|
|
73
|
+
return usage;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function updateSystemPerformance(frame) {
|
|
77
|
+
frame.clearContent();
|
|
78
|
+
|
|
79
|
+
const totalMem = os.totalmem();
|
|
80
|
+
const freeMem = os.freemem();
|
|
81
|
+
const usedMem = totalMem - freeMem;
|
|
82
|
+
const memPercent = ((usedMem / totalMem) * 100).toFixed(1);
|
|
83
|
+
|
|
84
|
+
const cpuUsage = getCPUUsage();
|
|
85
|
+
const uptime = os.uptime();
|
|
86
|
+
const days = Math.floor(uptime / 86400);
|
|
87
|
+
const hours = Math.floor((uptime % 86400) / 3600);
|
|
88
|
+
const minutes = Math.floor((uptime % 3600) / 60);
|
|
89
|
+
|
|
90
|
+
const loadAvg = os.loadavg();
|
|
91
|
+
|
|
92
|
+
frame.addContent(
|
|
93
|
+
2,
|
|
94
|
+
2,
|
|
95
|
+
`CPU Usage: ${cpuUsage}%`,
|
|
96
|
+
new ColorManager().setColor(100, 255, 100),
|
|
97
|
+
);
|
|
98
|
+
frame.addContent(
|
|
99
|
+
2,
|
|
100
|
+
3,
|
|
101
|
+
`Memory: ${memPercent}% (${(usedMem / 1024 / 1024 / 1024).toFixed(2)} GB / ${(totalMem / 1024 / 1024 / 1024).toFixed(2)} GB)`,
|
|
102
|
+
new ColorManager().setColor(150, 200, 255),
|
|
103
|
+
);
|
|
104
|
+
frame.addContent(
|
|
105
|
+
2,
|
|
106
|
+
5,
|
|
107
|
+
`Platform: ${os.platform()} ${os.arch()}`,
|
|
108
|
+
new ColorManager().setColor(200, 200, 200),
|
|
109
|
+
);
|
|
110
|
+
frame.addContent(
|
|
111
|
+
2,
|
|
112
|
+
6,
|
|
113
|
+
`Hostname: ${os.hostname()}`,
|
|
114
|
+
new ColorManager().setColor(200, 200, 200),
|
|
115
|
+
);
|
|
116
|
+
frame.addContent(
|
|
117
|
+
2,
|
|
118
|
+
7,
|
|
119
|
+
`Uptime: ${days}d ${hours}h ${minutes}m`,
|
|
120
|
+
new ColorManager().setColor(200, 200, 200),
|
|
121
|
+
);
|
|
122
|
+
frame.addContent(
|
|
123
|
+
2,
|
|
124
|
+
9,
|
|
125
|
+
`Load Average:`,
|
|
126
|
+
new ColorManager().setColor(180, 180, 180),
|
|
127
|
+
);
|
|
128
|
+
frame.addContent(
|
|
129
|
+
2,
|
|
130
|
+
10,
|
|
131
|
+
` 1m: ${loadAvg[0].toFixed(2)}`,
|
|
132
|
+
new ColorManager().setColor(160, 160, 160),
|
|
133
|
+
);
|
|
134
|
+
frame.addContent(
|
|
135
|
+
2,
|
|
136
|
+
11,
|
|
137
|
+
` 5m: ${loadAvg[1].toFixed(2)}`,
|
|
138
|
+
new ColorManager().setColor(160, 160, 160),
|
|
139
|
+
);
|
|
140
|
+
frame.addContent(
|
|
141
|
+
2,
|
|
142
|
+
12,
|
|
143
|
+
` 15m: ${loadAvg[2].toFixed(2)}`,
|
|
144
|
+
new ColorManager().setColor(160, 160, 160),
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
frame.addContent(
|
|
148
|
+
2,
|
|
149
|
+
14,
|
|
150
|
+
`CPUs: ${os.cpus().length} cores`,
|
|
151
|
+
new ColorManager().setColor(200, 200, 200),
|
|
152
|
+
);
|
|
153
|
+
frame.addContent(
|
|
154
|
+
2,
|
|
155
|
+
15,
|
|
156
|
+
`Arch: ${os.cpus()[0].model.substring(0, 35)}`,
|
|
157
|
+
new ColorManager().setColor(180, 180, 180),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function updateSampleFeatures(frame) {
|
|
162
|
+
frame.clearContent();
|
|
163
|
+
|
|
164
|
+
frame.addContent(
|
|
165
|
+
2,
|
|
166
|
+
2,
|
|
167
|
+
"Interactive Elements Demo:",
|
|
168
|
+
new ColorManager().setColor(255, 255, 100),
|
|
169
|
+
);
|
|
170
|
+
frame.addContent(
|
|
171
|
+
2,
|
|
172
|
+
3,
|
|
173
|
+
"Use ↑↓ arrows to navigate, Enter to interact",
|
|
174
|
+
new ColorManager().setColor(100, 100, 100),
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
let yPos = 5;
|
|
178
|
+
|
|
179
|
+
frame.addContent(
|
|
180
|
+
2,
|
|
181
|
+
yPos,
|
|
182
|
+
"Label Element:",
|
|
183
|
+
new ColorManager().setColor(150, 150, 150),
|
|
184
|
+
);
|
|
185
|
+
frame.addContent(
|
|
186
|
+
2,
|
|
187
|
+
yPos + 1,
|
|
188
|
+
" This is a label that cannot be changed!",
|
|
189
|
+
new ColorManager().setColor(200, 200, 200),
|
|
190
|
+
);
|
|
191
|
+
yPos += 3;
|
|
192
|
+
|
|
193
|
+
const inputSelected = selectedElement === 0;
|
|
194
|
+
const inputColor = inputSelected
|
|
195
|
+
? new ColorManager().setColor(100, 255, 100)
|
|
196
|
+
: new ColorManager().setColor(100, 200, 255);
|
|
197
|
+
const inputPrefix = inputSelected ? "► " : " ";
|
|
198
|
+
frame.addContent(
|
|
199
|
+
2,
|
|
200
|
+
yPos,
|
|
201
|
+
"Input Field:",
|
|
202
|
+
new ColorManager().setColor(150, 150, 150),
|
|
203
|
+
);
|
|
204
|
+
const displayValue = inputValue.padEnd(30, " ").substring(0, 30);
|
|
205
|
+
frame.addContent(2, yPos + 1, `${inputPrefix}[${displayValue}]`, inputColor);
|
|
206
|
+
if (inputSelected) {
|
|
207
|
+
frame.addContent(
|
|
208
|
+
2,
|
|
209
|
+
yPos + 2,
|
|
210
|
+
" Type a silly message :3",
|
|
211
|
+
new ColorManager().setColor(100, 100, 100),
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
yPos += inputSelected ? 4 : 3;
|
|
215
|
+
|
|
216
|
+
const button1Selected = selectedElement === 1;
|
|
217
|
+
const button1Color = button1Selected
|
|
218
|
+
? new ColorManager().setColor(255, 255, 100)
|
|
219
|
+
: new ColorManager().setColor(100, 255, 100);
|
|
220
|
+
const button1Prefix = button1Selected ? "► " : " ";
|
|
221
|
+
frame.addContent(
|
|
222
|
+
2,
|
|
223
|
+
yPos,
|
|
224
|
+
"Submit Button:",
|
|
225
|
+
new ColorManager().setColor(150, 150, 150),
|
|
226
|
+
);
|
|
227
|
+
frame.addContent(
|
|
228
|
+
2,
|
|
229
|
+
yPos + 1,
|
|
230
|
+
`${button1Prefix}[ Press me!! ]`,
|
|
231
|
+
button1Color,
|
|
232
|
+
);
|
|
233
|
+
if (buttonPressed && button1Selected) {
|
|
234
|
+
frame.addContent(
|
|
235
|
+
2,
|
|
236
|
+
yPos + 2,
|
|
237
|
+
" Meeoww :3",
|
|
238
|
+
new ColorManager().setColor(100, 255, 100),
|
|
239
|
+
);
|
|
240
|
+
yPos += 3;
|
|
241
|
+
} else {
|
|
242
|
+
yPos += 2;
|
|
243
|
+
}
|
|
244
|
+
yPos += 1;
|
|
245
|
+
|
|
246
|
+
const button2Selected = selectedElement === 2;
|
|
247
|
+
const button2Color = button2Selected
|
|
248
|
+
? new ColorManager().setColor(255, 255, 100)
|
|
249
|
+
: new ColorManager().setColor(255, 150, 100);
|
|
250
|
+
const button2Prefix = button2Selected ? "► " : " ";
|
|
251
|
+
frame.addContent(
|
|
252
|
+
2,
|
|
253
|
+
yPos,
|
|
254
|
+
"Counter Button:",
|
|
255
|
+
new ColorManager().setColor(150, 150, 150),
|
|
256
|
+
);
|
|
257
|
+
frame.addContent(
|
|
258
|
+
2,
|
|
259
|
+
yPos + 1,
|
|
260
|
+
`${button2Prefix}[ Increment Counter ]`,
|
|
261
|
+
button2Color,
|
|
262
|
+
);
|
|
263
|
+
frame.addContent(
|
|
264
|
+
2,
|
|
265
|
+
yPos + 2,
|
|
266
|
+
` Count: ${counter}`,
|
|
267
|
+
new ColorManager().setColor(255, 200, 100),
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function updateAllWindows() {
|
|
272
|
+
windows.forEach((win, idx) => {
|
|
273
|
+
win.update(win.frame);
|
|
274
|
+
if (idx === activeWindowIndex) {
|
|
275
|
+
win.frame.setBorderColor(255, 255, 100);
|
|
276
|
+
win.frame.show();
|
|
277
|
+
win.frame.setZIndex(100);
|
|
278
|
+
} else {
|
|
279
|
+
const colors = [
|
|
280
|
+
[100, 200, 255],
|
|
281
|
+
[100, 255, 150],
|
|
282
|
+
[255, 150, 200],
|
|
283
|
+
];
|
|
284
|
+
win.frame.setBorderColor(...colors[idx]);
|
|
285
|
+
win.frame.setZIndex(0);
|
|
286
|
+
if (hideOtherWindows) {
|
|
287
|
+
win.frame.hide();
|
|
288
|
+
} else {
|
|
289
|
+
win.frame.show();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
screen.elements.sort((a, b) => (a.zIndex || 0) - (b.zIndex || 0));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function render() {
|
|
297
|
+
updateAllWindows();
|
|
298
|
+
let controlText = `Tab: Switch | Shift+Arrows: Move | Alt+H: Focus Mode`;
|
|
299
|
+
if (activeWindowIndex === 1) {
|
|
300
|
+
controlText = `↑↓: Navigate | Enter: Select | Tab: Next Window | Shift+Arrows: Move | Alt+H: Focus Mode`;
|
|
301
|
+
}
|
|
302
|
+
if (hideOtherWindows) {
|
|
303
|
+
controlText = `[FOCUS MODE] | Alt+H: Show All | Tab: Switch | Shift+Arrows: Move`;
|
|
304
|
+
}
|
|
305
|
+
screen.setControlBar(controlText);
|
|
306
|
+
screen.render();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
inputManager.onAny((keyName, rawKey) => {
|
|
310
|
+
if (activeWindowIndex === 1 && selectedElement === 0) {
|
|
311
|
+
if (rawKey === "\r") {
|
|
312
|
+
render();
|
|
313
|
+
return;
|
|
314
|
+
} else if (rawKey === "\x7f") {
|
|
315
|
+
inputValue = inputValue.slice(0, -1);
|
|
316
|
+
render();
|
|
317
|
+
return;
|
|
318
|
+
} else if (
|
|
319
|
+
keyName &&
|
|
320
|
+
keyName.length === 1 &&
|
|
321
|
+
!rawKey.startsWith("\x1b") &&
|
|
322
|
+
keyName.charCodeAt(0) >= 32
|
|
323
|
+
) {
|
|
324
|
+
if (inputValue.length < 30) {
|
|
325
|
+
inputValue += keyName;
|
|
326
|
+
render();
|
|
327
|
+
}
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (keyName === "q" || keyName === "Q") {
|
|
333
|
+
inputManager.stop();
|
|
334
|
+
screen.exitFullscreen();
|
|
335
|
+
process.exit(0);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (rawKey === "\t") {
|
|
339
|
+
activeWindowIndex = (activeWindowIndex + 1) % windows.length;
|
|
340
|
+
selectedElement = 0;
|
|
341
|
+
render();
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (rawKey === '\x1bh' || rawKey === '\x1bH') {
|
|
346
|
+
hideOtherWindows = !hideOtherWindows;
|
|
347
|
+
render();
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const activeWin = windows[activeWindowIndex].frame;
|
|
352
|
+
|
|
353
|
+
if (rawKey === "\x1b[1;2A") {
|
|
354
|
+
activeWin.posY = Math.max(1, activeWin.posY - 2);
|
|
355
|
+
render();
|
|
356
|
+
return;
|
|
357
|
+
} else if (rawKey === "\x1b[1;2B") {
|
|
358
|
+
activeWin.posY = Math.min(
|
|
359
|
+
screen.height - activeWin.sizeY - 1,
|
|
360
|
+
activeWin.posY + 2,
|
|
361
|
+
);
|
|
362
|
+
render();
|
|
363
|
+
return;
|
|
364
|
+
} else if (rawKey === "\x1b[1;2C") {
|
|
365
|
+
activeWin.posX = Math.min(
|
|
366
|
+
screen.width - activeWin.sizeX - 1,
|
|
367
|
+
activeWin.posX + 2,
|
|
368
|
+
);
|
|
369
|
+
render();
|
|
370
|
+
return;
|
|
371
|
+
} else if (rawKey === "\x1b[1;2D") {
|
|
372
|
+
activeWin.posX = Math.max(1, activeWin.posX - 2);
|
|
373
|
+
render();
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (activeWindowIndex === 1) {
|
|
378
|
+
if (rawKey === "\x1b[A") {
|
|
379
|
+
selectedElement =
|
|
380
|
+
(selectedElement - 1 + sampleElements.length) % sampleElements.length;
|
|
381
|
+
buttonPressed = false;
|
|
382
|
+
render();
|
|
383
|
+
return;
|
|
384
|
+
} else if (rawKey === "\x1b[B") {
|
|
385
|
+
selectedElement = (selectedElement + 1) % sampleElements.length;
|
|
386
|
+
buttonPressed = false;
|
|
387
|
+
render();
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (rawKey === "\r") {
|
|
392
|
+
if (selectedElement === 1) {
|
|
393
|
+
buttonPressed = true;
|
|
394
|
+
render();
|
|
395
|
+
setTimeout(() => {
|
|
396
|
+
buttonPressed = false;
|
|
397
|
+
render();
|
|
398
|
+
}, 500);
|
|
399
|
+
} else if (selectedElement === 2) {
|
|
400
|
+
counter++;
|
|
401
|
+
render();
|
|
402
|
+
}
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
inputManager.start();
|
|
409
|
+
render();
|
|
410
|
+
|
|
411
|
+
setInterval(() => {
|
|
412
|
+
render();
|
|
413
|
+
}, 2000);
|
package/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { Screen } from './classes/screen.js';
|
|
2
|
+
export { ColorManager } from './classes/colorManager.js';
|
|
3
|
+
export { Element, properties } from './classes/element.js';
|
|
4
|
+
export { Frame } from './elements/frame.js';
|
|
5
|
+
export { Input } from './elements/input.js';
|
|
6
|
+
export { Button } from './elements/button.js';
|
|
7
|
+
export { Label } from './elements/label.js';
|
|
8
|
+
export { InputManager } from './classes/inputManager.js';
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "buffalo-tui",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A beginner friendly TUI framework available on various package managers for Node.js projects",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"demo": "node ./examples/demo.js"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/transicle/buffalo.git"
|
|
13
|
+
},
|
|
14
|
+
"author": "lily.transgirls.win <ilovesuno@proton.me>",
|
|
15
|
+
"license": "MIT"
|
|
16
|
+
}
|