voonex 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +223 -0
- package/dist/components/Box.d.ts +33 -0
- package/dist/components/Box.js +242 -0
- package/dist/components/Button.d.ts +23 -0
- package/dist/components/Button.js +76 -0
- package/dist/components/Checkbox.d.ts +20 -0
- package/dist/components/Checkbox.js +40 -0
- package/dist/components/Input.d.ts +23 -0
- package/dist/components/Input.js +79 -0
- package/dist/components/Menu.d.ts +27 -0
- package/dist/components/Menu.js +93 -0
- package/dist/components/Popup.d.ts +7 -0
- package/dist/components/Popup.js +50 -0
- package/dist/components/ProgressBar.d.ts +28 -0
- package/dist/components/ProgressBar.js +47 -0
- package/dist/components/Radio.d.ts +22 -0
- package/dist/components/Radio.js +64 -0
- package/dist/components/Select.d.ts +24 -0
- package/dist/components/Select.js +126 -0
- package/dist/components/Tab.d.ts +24 -0
- package/dist/components/Tab.js +72 -0
- package/dist/components/Table.d.ts +25 -0
- package/dist/components/Table.js +116 -0
- package/dist/components/Textarea.d.ts +26 -0
- package/dist/components/Textarea.js +176 -0
- package/dist/components/Tree.d.ts +33 -0
- package/dist/components/Tree.js +166 -0
- package/dist/core/Component.d.ts +6 -0
- package/dist/core/Component.js +5 -0
- package/dist/core/Cursor.d.ts +7 -0
- package/dist/core/Cursor.js +59 -0
- package/dist/core/Events.d.ts +11 -0
- package/dist/core/Events.js +37 -0
- package/dist/core/Focus.d.ts +16 -0
- package/dist/core/Focus.js +69 -0
- package/dist/core/Input.d.ts +13 -0
- package/dist/core/Input.js +88 -0
- package/dist/core/Layout.d.ts +18 -0
- package/dist/core/Layout.js +65 -0
- package/dist/core/Screen.d.ts +78 -0
- package/dist/core/Screen.js +373 -0
- package/dist/core/Styler.d.ts +71 -0
- package/dist/core/Styler.js +157 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +41 -0
- package/package.json +29 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Focusable } from '../core/Focus';
|
|
2
|
+
import * as readline from 'readline';
|
|
3
|
+
export interface InputOptions {
|
|
4
|
+
id: string;
|
|
5
|
+
label?: string;
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
width?: number;
|
|
10
|
+
type?: 'text' | 'password';
|
|
11
|
+
}
|
|
12
|
+
export declare class InputField implements Focusable {
|
|
13
|
+
id: string;
|
|
14
|
+
value: string;
|
|
15
|
+
private isFocused;
|
|
16
|
+
private options;
|
|
17
|
+
constructor(options: InputOptions);
|
|
18
|
+
focus(): void;
|
|
19
|
+
blur(): void;
|
|
20
|
+
setValue(val: string): void;
|
|
21
|
+
handleKey(key: readline.Key): boolean;
|
|
22
|
+
render(): void;
|
|
23
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InputField = void 0;
|
|
4
|
+
const Styler_1 = require("../core/Styler");
|
|
5
|
+
const Screen_1 = require("../core/Screen");
|
|
6
|
+
class InputField {
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.value = "";
|
|
9
|
+
this.isFocused = false;
|
|
10
|
+
this.id = options.id;
|
|
11
|
+
this.options = { width: 30, type: 'text', ...options };
|
|
12
|
+
}
|
|
13
|
+
focus() {
|
|
14
|
+
this.isFocused = true;
|
|
15
|
+
}
|
|
16
|
+
blur() {
|
|
17
|
+
this.isFocused = false;
|
|
18
|
+
}
|
|
19
|
+
setValue(val) {
|
|
20
|
+
this.value = val;
|
|
21
|
+
}
|
|
22
|
+
handleKey(key) {
|
|
23
|
+
if (!this.isFocused)
|
|
24
|
+
return false;
|
|
25
|
+
if (key.name === 'backspace') {
|
|
26
|
+
this.value = this.value.slice(0, -1);
|
|
27
|
+
this.render();
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
else if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
|
|
31
|
+
// Basic text input filtering
|
|
32
|
+
// Only accept printable characters (rough check)
|
|
33
|
+
if (/^[\x20-\x7E]$/.test(key.sequence)) {
|
|
34
|
+
this.value += key.sequence;
|
|
35
|
+
this.render();
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Did not consume (e.g., arrow keys, tabs)
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
render() {
|
|
43
|
+
const { x, y, label, width, type } = this.options;
|
|
44
|
+
const labelText = label ? `${label}: ` : '';
|
|
45
|
+
const labelLen = labelText.length;
|
|
46
|
+
// Draw Label
|
|
47
|
+
if (label) {
|
|
48
|
+
const labelStyle = this.isFocused ? 'brightCyan' : 'dim';
|
|
49
|
+
Screen_1.Screen.write(x, y, Styler_1.Styler.style(labelText, labelStyle, 'bold'));
|
|
50
|
+
}
|
|
51
|
+
// Draw Input Box Background
|
|
52
|
+
const inputX = x + labelLen;
|
|
53
|
+
const maxWidth = (width || 30) - labelLen;
|
|
54
|
+
let displayValue = this.value;
|
|
55
|
+
if (type === 'password') {
|
|
56
|
+
displayValue = '*'.repeat(this.value.length);
|
|
57
|
+
}
|
|
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);
|
|
66
|
+
}
|
|
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);
|
|
71
|
+
const style = this.isFocused ? 'white' : 'gray';
|
|
72
|
+
Screen_1.Screen.write(inputX, y, Styler_1.Styler.style(fieldContent + padding, style));
|
|
73
|
+
// Underline
|
|
74
|
+
const underline = '─'.repeat(maxWidth);
|
|
75
|
+
const underlineColor = this.isFocused ? 'brightGreen' : 'dim';
|
|
76
|
+
Screen_1.Screen.write(inputX, y + 1, Styler_1.Styler.style(underline, underlineColor));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
exports.InputField = InputField;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Focusable } from '../core/Focus';
|
|
2
|
+
import * as readline from 'readline';
|
|
3
|
+
interface MenuOptions {
|
|
4
|
+
id?: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
items: string[];
|
|
7
|
+
onSelect: (index: number, item: string) => void;
|
|
8
|
+
x?: number;
|
|
9
|
+
y?: number;
|
|
10
|
+
}
|
|
11
|
+
export declare class Menu implements Focusable {
|
|
12
|
+
id: string;
|
|
13
|
+
private selectedIndex;
|
|
14
|
+
private options;
|
|
15
|
+
private active;
|
|
16
|
+
private isFocused;
|
|
17
|
+
constructor(options: MenuOptions);
|
|
18
|
+
focus(): void;
|
|
19
|
+
blur(): void;
|
|
20
|
+
handleKey(key: readline.Key): boolean;
|
|
21
|
+
render(): void;
|
|
22
|
+
/**
|
|
23
|
+
* Legacy Standalone Start
|
|
24
|
+
*/
|
|
25
|
+
start(): void;
|
|
26
|
+
}
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Menu = void 0;
|
|
4
|
+
const Input_1 = require("../core/Input");
|
|
5
|
+
const Screen_1 = require("../core/Screen");
|
|
6
|
+
const Styler_1 = require("../core/Styler");
|
|
7
|
+
const Cursor_1 = require("../core/Cursor");
|
|
8
|
+
class Menu {
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.selectedIndex = 0;
|
|
11
|
+
this.active = false; // "Active" in standalone mode, or used for "Focused" logic?
|
|
12
|
+
this.isFocused = false;
|
|
13
|
+
this.id = options.id || `menu-${Math.random().toString(36).substr(2, 9)}`;
|
|
14
|
+
this.options = options;
|
|
15
|
+
this.options.x = options.x || 2;
|
|
16
|
+
this.options.y = options.y || 2;
|
|
17
|
+
}
|
|
18
|
+
focus() {
|
|
19
|
+
this.isFocused = true;
|
|
20
|
+
this.render();
|
|
21
|
+
}
|
|
22
|
+
blur() {
|
|
23
|
+
this.isFocused = false;
|
|
24
|
+
this.render();
|
|
25
|
+
}
|
|
26
|
+
handleKey(key) {
|
|
27
|
+
if (!this.isFocused && !this.active)
|
|
28
|
+
return false;
|
|
29
|
+
let consumed = false;
|
|
30
|
+
switch (key.name) {
|
|
31
|
+
case 'up':
|
|
32
|
+
this.selectedIndex = (this.selectedIndex - 1 + this.options.items.length) % this.options.items.length;
|
|
33
|
+
this.render();
|
|
34
|
+
consumed = true;
|
|
35
|
+
break;
|
|
36
|
+
case 'down':
|
|
37
|
+
this.selectedIndex = (this.selectedIndex + 1) % this.options.items.length;
|
|
38
|
+
this.render();
|
|
39
|
+
consumed = true;
|
|
40
|
+
break;
|
|
41
|
+
case 'return': // Enter key
|
|
42
|
+
case 'enter':
|
|
43
|
+
//this.active = false; // Don't deactivate if managed by FocusManager?
|
|
44
|
+
// Provide visual feedback
|
|
45
|
+
// We should probably just call select
|
|
46
|
+
this.options.onSelect(this.selectedIndex, this.options.items[this.selectedIndex]);
|
|
47
|
+
consumed = true;
|
|
48
|
+
break;
|
|
49
|
+
case 'escape':
|
|
50
|
+
// Maybe defocus or exit?
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
return consumed;
|
|
54
|
+
}
|
|
55
|
+
render() {
|
|
56
|
+
const { x, y, items, title } = this.options;
|
|
57
|
+
if (title) {
|
|
58
|
+
Screen_1.Screen.write(x, y, Styler_1.Styler.style(title, 'bold', 'underline'));
|
|
59
|
+
}
|
|
60
|
+
items.forEach((item, index) => {
|
|
61
|
+
const isSelected = index === this.selectedIndex;
|
|
62
|
+
// Show different cursor if focused
|
|
63
|
+
const prefix = isSelected ? (this.isFocused || this.active ? '❯ ' : '> ') : ' ';
|
|
64
|
+
let style = ['dim'];
|
|
65
|
+
if (isSelected) {
|
|
66
|
+
style = this.isFocused || this.active ? ['cyan', 'bold'] : ['white'];
|
|
67
|
+
}
|
|
68
|
+
const label = Styler_1.Styler.style(item, ...style);
|
|
69
|
+
// +1 (or +2) for title offset
|
|
70
|
+
Screen_1.Screen.write(x, y + index + (title ? 2 : 0), `${prefix}${label}`);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Legacy Standalone Start
|
|
75
|
+
*/
|
|
76
|
+
start() {
|
|
77
|
+
this.active = true;
|
|
78
|
+
Cursor_1.Cursor.hide(); // Hide cursor for cleaner UI
|
|
79
|
+
this.render();
|
|
80
|
+
// This effectively takes over input globally, effectively "Mocking" FocusManager for this single component
|
|
81
|
+
Input_1.Input.onKey((key) => {
|
|
82
|
+
if (!this.active)
|
|
83
|
+
return;
|
|
84
|
+
// Re-use logic
|
|
85
|
+
const consumed = this.handleKey(key);
|
|
86
|
+
if (key.name === 'q' || key.name === 'escape') {
|
|
87
|
+
this.active = false;
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
exports.Menu = Menu;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Popup = void 0;
|
|
4
|
+
const Box_1 = require("./Box");
|
|
5
|
+
const Screen_1 = require("../core/Screen");
|
|
6
|
+
const Styler_1 = require("../core/Styler");
|
|
7
|
+
const Input_1 = require("../core/Input");
|
|
8
|
+
class Popup {
|
|
9
|
+
static alert(message, options = {}) {
|
|
10
|
+
// Mount a high-priority render function
|
|
11
|
+
const renderPopup = () => {
|
|
12
|
+
const { width, height } = Screen_1.Screen.size;
|
|
13
|
+
const lines = message.split('\n');
|
|
14
|
+
const maxLen = Math.max(...lines.map(l => Styler_1.Styler.len(l)));
|
|
15
|
+
const boxWidth = Math.max(maxLen + 6, 40);
|
|
16
|
+
const boxHeight = lines.length + 4;
|
|
17
|
+
const x = Math.floor((width - boxWidth) / 2);
|
|
18
|
+
const y = Math.floor((height - boxHeight) / 2);
|
|
19
|
+
// Manually clear popup area for opacity
|
|
20
|
+
for (let i = 0; i < boxHeight; i++) {
|
|
21
|
+
Screen_1.Screen.write(x, y + i, " ".repeat(boxWidth));
|
|
22
|
+
}
|
|
23
|
+
Box_1.Box.render([
|
|
24
|
+
...lines
|
|
25
|
+
], {
|
|
26
|
+
x, y,
|
|
27
|
+
width: boxWidth,
|
|
28
|
+
height: boxHeight,
|
|
29
|
+
title: options.title || "ALERT",
|
|
30
|
+
borderColor: options.color || 'red',
|
|
31
|
+
style: 'double',
|
|
32
|
+
padding: 1
|
|
33
|
+
});
|
|
34
|
+
Screen_1.Screen.write(x + 2, y + boxHeight - 1, Styler_1.Styler.style("[Press Enter]", 'dim'));
|
|
35
|
+
};
|
|
36
|
+
const POPUP_Z_INDEX = 9999;
|
|
37
|
+
Screen_1.Screen.mount(renderPopup, POPUP_Z_INDEX);
|
|
38
|
+
const handler = (key) => {
|
|
39
|
+
if (key.name === 'return' || key.name === 'enter' || key.name === 'escape') {
|
|
40
|
+
Screen_1.Screen.unmount(renderPopup);
|
|
41
|
+
Input_1.Input.offKey(handler);
|
|
42
|
+
return true; // Stop propagation
|
|
43
|
+
}
|
|
44
|
+
return true; // Consume other keys while popup is open?
|
|
45
|
+
// Usually alerts are modal, so yes, consume everything.
|
|
46
|
+
};
|
|
47
|
+
Input_1.Input.onKey(handler);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
exports.Popup = Popup;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ColorName } from '../core/Styler';
|
|
2
|
+
interface ProgressBarOptions {
|
|
3
|
+
width?: number;
|
|
4
|
+
total?: number;
|
|
5
|
+
completeChar?: string;
|
|
6
|
+
incompleteChar?: string;
|
|
7
|
+
format?: string;
|
|
8
|
+
color?: ColorName;
|
|
9
|
+
x?: number;
|
|
10
|
+
y?: number;
|
|
11
|
+
}
|
|
12
|
+
export declare class ProgressBar {
|
|
13
|
+
private current;
|
|
14
|
+
private total;
|
|
15
|
+
private width;
|
|
16
|
+
private chars;
|
|
17
|
+
private format;
|
|
18
|
+
private color;
|
|
19
|
+
private x;
|
|
20
|
+
private y;
|
|
21
|
+
constructor(options?: ProgressBarOptions);
|
|
22
|
+
update(current: number, tokens?: Record<string, string>, pos?: {
|
|
23
|
+
x: number;
|
|
24
|
+
y: number;
|
|
25
|
+
}): void;
|
|
26
|
+
finish(): void;
|
|
27
|
+
}
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ProgressBar = void 0;
|
|
4
|
+
const Styler_1 = require("../core/Styler");
|
|
5
|
+
const Screen_1 = require("../core/Screen");
|
|
6
|
+
class ProgressBar {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.current = 0;
|
|
9
|
+
this.total = options.total || 100;
|
|
10
|
+
this.width = options.width || 40;
|
|
11
|
+
this.chars = {
|
|
12
|
+
complete: options.completeChar || '█',
|
|
13
|
+
incomplete: options.incompleteChar || '░'
|
|
14
|
+
};
|
|
15
|
+
this.format = options.format || "[:bar] :percent";
|
|
16
|
+
this.color = options.color || 'green';
|
|
17
|
+
this.x = options.x || 0;
|
|
18
|
+
this.y = options.y || 0;
|
|
19
|
+
}
|
|
20
|
+
// Allow overriding position in update
|
|
21
|
+
update(current, tokens = {}, pos) {
|
|
22
|
+
this.current = current;
|
|
23
|
+
// Use provided pos or stored pos
|
|
24
|
+
const drawX = pos?.x ?? this.x;
|
|
25
|
+
const drawY = pos?.y ?? this.y;
|
|
26
|
+
const ratio = Math.min(Math.max(current / this.total, 0), 1);
|
|
27
|
+
const percent = Math.floor(ratio * 100);
|
|
28
|
+
const completeLen = Math.round(this.width * ratio);
|
|
29
|
+
const incompleteLen = this.width - completeLen;
|
|
30
|
+
const bar = Styler_1.Styler.style(this.chars.complete.repeat(completeLen), this.color) +
|
|
31
|
+
Styler_1.Styler.style(this.chars.incomplete.repeat(incompleteLen), 'dim');
|
|
32
|
+
let str = this.format
|
|
33
|
+
.replace(':bar', bar)
|
|
34
|
+
.replace(':percent', Styler_1.Styler.style(`${percent}%`.padStart(4), 'bold'));
|
|
35
|
+
// Replace custom tokens (e.g., :msg)
|
|
36
|
+
for (const [key, val] of Object.entries(tokens)) {
|
|
37
|
+
str = str.replace(`:${key}`, val);
|
|
38
|
+
}
|
|
39
|
+
// Use Screen.write instead of direct stdout
|
|
40
|
+
Screen_1.Screen.write(drawX, drawY, str);
|
|
41
|
+
}
|
|
42
|
+
finish() {
|
|
43
|
+
// No-op in buffered mode, or maybe clear?
|
|
44
|
+
// Or keep last state.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
exports.ProgressBar = ProgressBar;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Focusable } from '../core/Focus';
|
|
2
|
+
import * as readline from 'readline';
|
|
3
|
+
export interface RadioGroupOptions {
|
|
4
|
+
id: string;
|
|
5
|
+
label?: string;
|
|
6
|
+
items: string[];
|
|
7
|
+
selectedIndex?: number;
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
direction?: 'horizontal' | 'vertical';
|
|
11
|
+
}
|
|
12
|
+
export declare class RadioGroup implements Focusable {
|
|
13
|
+
id: string;
|
|
14
|
+
selectedIndex: number;
|
|
15
|
+
private options;
|
|
16
|
+
private isFocused;
|
|
17
|
+
constructor(options: RadioGroupOptions);
|
|
18
|
+
focus(): void;
|
|
19
|
+
blur(): void;
|
|
20
|
+
handleKey(key: readline.Key): boolean;
|
|
21
|
+
render(): void;
|
|
22
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RadioGroup = void 0;
|
|
4
|
+
const Styler_1 = require("../core/Styler");
|
|
5
|
+
const Screen_1 = require("../core/Screen");
|
|
6
|
+
class RadioGroup {
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.isFocused = false;
|
|
9
|
+
this.id = options.id;
|
|
10
|
+
this.options = { direction: 'vertical', ...options };
|
|
11
|
+
this.selectedIndex = options.selectedIndex || 0;
|
|
12
|
+
}
|
|
13
|
+
focus() {
|
|
14
|
+
this.isFocused = true;
|
|
15
|
+
this.render();
|
|
16
|
+
}
|
|
17
|
+
blur() {
|
|
18
|
+
this.isFocused = false;
|
|
19
|
+
this.render();
|
|
20
|
+
}
|
|
21
|
+
handleKey(key) {
|
|
22
|
+
if (!this.isFocused)
|
|
23
|
+
return false;
|
|
24
|
+
const count = this.options.items.length;
|
|
25
|
+
if (key.name === 'up' || key.name === 'left') {
|
|
26
|
+
this.selectedIndex = (this.selectedIndex - 1 + count) % count;
|
|
27
|
+
this.render();
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
else if (key.name === 'down' || key.name === 'right') {
|
|
31
|
+
this.selectedIndex = (this.selectedIndex + 1) % count;
|
|
32
|
+
this.render();
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
render() {
|
|
38
|
+
const { x, y, label, items, direction } = this.options;
|
|
39
|
+
let currentX = x;
|
|
40
|
+
let currentY = y;
|
|
41
|
+
if (label) {
|
|
42
|
+
Screen_1.Screen.write(currentX, currentY, Styler_1.Styler.style(label + ':', 'bold'));
|
|
43
|
+
if (direction === 'vertical')
|
|
44
|
+
currentY++;
|
|
45
|
+
else
|
|
46
|
+
currentX += label.length + 2;
|
|
47
|
+
}
|
|
48
|
+
items.forEach((item, index) => {
|
|
49
|
+
const isSelected = index === this.selectedIndex;
|
|
50
|
+
const symbol = isSelected ? '(*)' : '( )';
|
|
51
|
+
const symbolColor = isSelected ? 'green' : 'dim';
|
|
52
|
+
const textColor = this.isFocused && isSelected ? 'brightCyan' : (this.isFocused ? 'white' : 'gray');
|
|
53
|
+
const content = Styler_1.Styler.style(symbol, symbolColor) + ' ' + Styler_1.Styler.style(item, textColor);
|
|
54
|
+
Screen_1.Screen.write(currentX, currentY, content);
|
|
55
|
+
if (direction === 'vertical') {
|
|
56
|
+
currentY++;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
currentX += item.length + 6; // spacing
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
exports.RadioGroup = RadioGroup;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Focusable } from '../core/Focus';
|
|
2
|
+
import * as readline from 'readline';
|
|
3
|
+
export interface SelectOptions {
|
|
4
|
+
id: string;
|
|
5
|
+
label?: string;
|
|
6
|
+
items: string[];
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
width?: number;
|
|
10
|
+
onSelect?: (index: number, item: string) => void;
|
|
11
|
+
}
|
|
12
|
+
export declare class Select implements Focusable {
|
|
13
|
+
id: string;
|
|
14
|
+
private options;
|
|
15
|
+
private selectedIndex;
|
|
16
|
+
private isFocused;
|
|
17
|
+
private isOpen;
|
|
18
|
+
constructor(options: SelectOptions);
|
|
19
|
+
focus(): void;
|
|
20
|
+
blur(): void;
|
|
21
|
+
handleKey(key: readline.Key): boolean;
|
|
22
|
+
private clearDropdownArea;
|
|
23
|
+
render(): void;
|
|
24
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Select = void 0;
|
|
4
|
+
const Styler_1 = require("../core/Styler");
|
|
5
|
+
const Screen_1 = require("../core/Screen");
|
|
6
|
+
class Select {
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.selectedIndex = 0;
|
|
9
|
+
this.isFocused = false;
|
|
10
|
+
this.isOpen = false;
|
|
11
|
+
this.id = options.id;
|
|
12
|
+
this.options = { width: 20, ...options };
|
|
13
|
+
}
|
|
14
|
+
focus() {
|
|
15
|
+
this.isFocused = true;
|
|
16
|
+
this.render();
|
|
17
|
+
}
|
|
18
|
+
blur() {
|
|
19
|
+
this.isFocused = false;
|
|
20
|
+
if (this.isOpen) {
|
|
21
|
+
this.isOpen = false;
|
|
22
|
+
this.clearDropdownArea(); // Clear artifacts
|
|
23
|
+
}
|
|
24
|
+
this.render();
|
|
25
|
+
}
|
|
26
|
+
handleKey(key) {
|
|
27
|
+
if (!this.isFocused)
|
|
28
|
+
return false;
|
|
29
|
+
if (this.isOpen) {
|
|
30
|
+
// Dropdown navigation
|
|
31
|
+
if (key.name === 'up') {
|
|
32
|
+
this.selectedIndex = (this.selectedIndex - 1 + this.options.items.length) % this.options.items.length;
|
|
33
|
+
this.render();
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
else if (key.name === 'down') {
|
|
37
|
+
this.selectedIndex = (this.selectedIndex + 1) % this.options.items.length;
|
|
38
|
+
this.render();
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
else if (key.name === 'return' || key.name === 'enter' || key.name === 'escape') {
|
|
42
|
+
this.isOpen = false;
|
|
43
|
+
this.clearDropdownArea(); // Clear artifacts
|
|
44
|
+
if (key.name !== 'escape' && this.options.onSelect) {
|
|
45
|
+
this.options.onSelect(this.selectedIndex, this.options.items[this.selectedIndex]);
|
|
46
|
+
}
|
|
47
|
+
this.render();
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
// Closed state navigation
|
|
53
|
+
if (key.name === 'return' || key.name === 'enter' || key.name === 'space') {
|
|
54
|
+
this.isOpen = true;
|
|
55
|
+
this.render();
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
// Optional: Allow arrows to cycle through items without opening?
|
|
59
|
+
// Usually selects allow this.
|
|
60
|
+
if (key.name === 'down') {
|
|
61
|
+
this.selectedIndex = (this.selectedIndex + 1) % this.options.items.length;
|
|
62
|
+
if (this.options.onSelect)
|
|
63
|
+
this.options.onSelect(this.selectedIndex, this.options.items[this.selectedIndex]);
|
|
64
|
+
this.render();
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
if (key.name === 'up') {
|
|
68
|
+
this.selectedIndex = (this.selectedIndex - 1 + this.options.items.length) % this.options.items.length;
|
|
69
|
+
if (this.options.onSelect)
|
|
70
|
+
this.options.onSelect(this.selectedIndex, this.options.items[this.selectedIndex]);
|
|
71
|
+
this.render();
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
clearDropdownArea() {
|
|
78
|
+
const { x, y, width, items, label } = this.options;
|
|
79
|
+
const w = width || 20;
|
|
80
|
+
const labelText = label ? `${label}: ` : '';
|
|
81
|
+
const inputX = x + labelText.length;
|
|
82
|
+
// Clear the exact area where dropdown items were drawn
|
|
83
|
+
for (let i = 0; i < items.length; i++) {
|
|
84
|
+
const itemY = y + 1 + i;
|
|
85
|
+
// Write spaces to clear. We use ' ' * w.
|
|
86
|
+
// This might wipe underlying content, but it removes the ghost text.
|
|
87
|
+
Screen_1.Screen.write(inputX, itemY, ' '.repeat(w));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
render() {
|
|
91
|
+
const { x, y, width, label, items } = this.options;
|
|
92
|
+
const w = width || 20;
|
|
93
|
+
const labelText = label ? `${label}: ` : '';
|
|
94
|
+
const labelLen = labelText.length;
|
|
95
|
+
if (label) {
|
|
96
|
+
const labelStyle = this.isFocused ? 'brightCyan' : 'white';
|
|
97
|
+
// If not focused, make it dim using modifier
|
|
98
|
+
if (this.isFocused) {
|
|
99
|
+
Screen_1.Screen.write(x, y, Styler_1.Styler.style(labelText, 'brightCyan', 'bold'));
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
Screen_1.Screen.write(x, y, Styler_1.Styler.style(labelText, 'white', 'dim'));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const inputX = x + labelLen;
|
|
106
|
+
const selectedItem = items[this.selectedIndex] || "";
|
|
107
|
+
const arrow = this.isOpen ? '▲' : '▼';
|
|
108
|
+
// Draw the main box
|
|
109
|
+
const content = ` ${selectedItem} `.padEnd(w - 2) + arrow + ' ';
|
|
110
|
+
const style = this.isFocused ? 'bgBlue' : 'bgWhite';
|
|
111
|
+
const fg = this.isFocused ? 'white' : 'black';
|
|
112
|
+
Screen_1.Screen.write(inputX, y, Styler_1.Styler.style(content, style, fg));
|
|
113
|
+
// Draw Dropdown if open
|
|
114
|
+
if (this.isOpen) {
|
|
115
|
+
for (let i = 0; i < items.length; i++) {
|
|
116
|
+
const itemY = y + 1 + i;
|
|
117
|
+
const isSelected = i === this.selectedIndex;
|
|
118
|
+
const itemContent = ` ${items[i]} `.padEnd(w);
|
|
119
|
+
const itemStyle = isSelected ? 'bgCyan' : 'bgWhite';
|
|
120
|
+
const itemFg = isSelected ? 'black' : 'black';
|
|
121
|
+
Screen_1.Screen.write(inputX, itemY, Styler_1.Styler.style(itemContent, itemStyle, itemFg));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
exports.Select = Select;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Focusable } from '../core/Focus';
|
|
2
|
+
import * as readline from 'readline';
|
|
3
|
+
export interface TabOptions {
|
|
4
|
+
id: string;
|
|
5
|
+
titles: string[];
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
initialTab?: number;
|
|
11
|
+
onTabChange?: (index: number) => void;
|
|
12
|
+
}
|
|
13
|
+
export declare class TabManager implements Focusable {
|
|
14
|
+
id: string;
|
|
15
|
+
private options;
|
|
16
|
+
private activeIndex;
|
|
17
|
+
private isFocused;
|
|
18
|
+
constructor(options: TabOptions);
|
|
19
|
+
focus(): void;
|
|
20
|
+
blur(): void;
|
|
21
|
+
handleKey(key: readline.Key): boolean;
|
|
22
|
+
private triggerChange;
|
|
23
|
+
render(): void;
|
|
24
|
+
}
|