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 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
+ }
@@ -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
+ }