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,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TabManager = void 0;
|
|
4
|
+
const Screen_1 = require("../core/Screen");
|
|
5
|
+
const Styler_1 = require("../core/Styler");
|
|
6
|
+
class TabManager {
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.activeIndex = 0;
|
|
9
|
+
this.isFocused = false;
|
|
10
|
+
this.id = options.id;
|
|
11
|
+
this.options = options;
|
|
12
|
+
this.activeIndex = options.initialTab || 0;
|
|
13
|
+
}
|
|
14
|
+
focus() {
|
|
15
|
+
this.isFocused = true;
|
|
16
|
+
this.render();
|
|
17
|
+
}
|
|
18
|
+
blur() {
|
|
19
|
+
this.isFocused = false;
|
|
20
|
+
this.render();
|
|
21
|
+
}
|
|
22
|
+
handleKey(key) {
|
|
23
|
+
if (!this.isFocused)
|
|
24
|
+
return false;
|
|
25
|
+
// Left/Right arrows to switch tabs
|
|
26
|
+
if (key.name === 'left') {
|
|
27
|
+
this.activeIndex = (this.activeIndex - 1 + this.options.titles.length) % this.options.titles.length;
|
|
28
|
+
this.triggerChange();
|
|
29
|
+
this.render();
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
else if (key.name === 'right') {
|
|
33
|
+
this.activeIndex = (this.activeIndex + 1) % this.options.titles.length;
|
|
34
|
+
this.triggerChange();
|
|
35
|
+
this.render();
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
triggerChange() {
|
|
41
|
+
if (this.options.onTabChange) {
|
|
42
|
+
this.options.onTabChange(this.activeIndex);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
render() {
|
|
46
|
+
const { x, y, width, titles } = this.options;
|
|
47
|
+
// Draw Tabs Row
|
|
48
|
+
let currentX = x;
|
|
49
|
+
titles.forEach((title, index) => {
|
|
50
|
+
const isActive = index === this.activeIndex;
|
|
51
|
+
const style = isActive ? (this.isFocused ? 'brightCyan' : 'white') : 'dim';
|
|
52
|
+
const decoration = isActive ? 'bold' : 'dim';
|
|
53
|
+
const displayTitle = ` ${title} `;
|
|
54
|
+
// Draw top border for active tab or something distinct?
|
|
55
|
+
// Simple style: [ Active ] Inactive
|
|
56
|
+
let renderStr = displayTitle;
|
|
57
|
+
if (isActive) {
|
|
58
|
+
renderStr = Styler_1.Styler.style(displayTitle, 'bgBlue', 'white', 'bold');
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
renderStr = Styler_1.Styler.style(displayTitle, 'dim', 'bgBlack');
|
|
62
|
+
}
|
|
63
|
+
Screen_1.Screen.write(currentX, y, renderStr);
|
|
64
|
+
currentX += Styler_1.Styler.len(displayTitle) + 1;
|
|
65
|
+
});
|
|
66
|
+
// Draw separator line below tabs
|
|
67
|
+
Screen_1.Screen.write(x, y + 1, Styler_1.Styler.style('─'.repeat(width), 'dim'));
|
|
68
|
+
// Ideally, we might want to clear the content area below if the user handles it manually?
|
|
69
|
+
// Or we assume the user's `onTabChange` will re-render the content area.
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
exports.TabManager = TabManager;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Focusable } from '../core/Focus';
|
|
2
|
+
import * as readline from 'readline';
|
|
3
|
+
interface TableOptions {
|
|
4
|
+
id?: string;
|
|
5
|
+
x?: number;
|
|
6
|
+
y?: number;
|
|
7
|
+
height?: number;
|
|
8
|
+
width?: number;
|
|
9
|
+
scrollable?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare class Table implements Focusable {
|
|
12
|
+
id: string;
|
|
13
|
+
private headers;
|
|
14
|
+
private rows;
|
|
15
|
+
private options;
|
|
16
|
+
private scrollTop;
|
|
17
|
+
private isFocused;
|
|
18
|
+
constructor(headers: string[], options?: TableOptions);
|
|
19
|
+
addRow(row: string[]): void;
|
|
20
|
+
focus(): void;
|
|
21
|
+
blur(): void;
|
|
22
|
+
handleKey(key: readline.Key): boolean;
|
|
23
|
+
render(): void;
|
|
24
|
+
}
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Table = void 0;
|
|
4
|
+
const Styler_1 = require("../core/Styler");
|
|
5
|
+
const Screen_1 = require("../core/Screen");
|
|
6
|
+
class Table {
|
|
7
|
+
constructor(headers, options = {}) {
|
|
8
|
+
this.rows = [];
|
|
9
|
+
this.scrollTop = 0;
|
|
10
|
+
this.isFocused = false;
|
|
11
|
+
this.headers = headers;
|
|
12
|
+
this.id = options.id || `table-${Math.random().toString(36).substr(2, 9)}`;
|
|
13
|
+
this.options = { x: 1, y: 1, scrollable: true, ...options };
|
|
14
|
+
}
|
|
15
|
+
addRow(row) {
|
|
16
|
+
this.rows.push(row);
|
|
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.options.scrollable)
|
|
28
|
+
return false;
|
|
29
|
+
const maxScroll = Math.max(0, this.rows.length - (this.options.height ? this.options.height - 4 : this.rows.length));
|
|
30
|
+
// Height - header (1) - separator (1) - border/margin?
|
|
31
|
+
// Let's refine calculation in render, but basically:
|
|
32
|
+
// Visible rows = height - 2 (header + sep)
|
|
33
|
+
const visibleRows = (this.options.height || this.rows.length + 2) - 2;
|
|
34
|
+
if (key.name === 'up') {
|
|
35
|
+
if (this.scrollTop > 0) {
|
|
36
|
+
this.scrollTop--;
|
|
37
|
+
this.render();
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else if (key.name === 'down') {
|
|
42
|
+
if (this.scrollTop < maxScroll) {
|
|
43
|
+
this.scrollTop++;
|
|
44
|
+
this.render();
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else if (key.name === 'pageup') {
|
|
49
|
+
this.scrollTop = Math.max(0, this.scrollTop - visibleRows);
|
|
50
|
+
this.render();
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
else if (key.name === 'pagedown') {
|
|
54
|
+
this.scrollTop = Math.min(maxScroll, this.scrollTop + visibleRows);
|
|
55
|
+
this.render();
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
render() {
|
|
61
|
+
const startX = this.options.x || 1;
|
|
62
|
+
let currentY = this.options.y || 1;
|
|
63
|
+
const height = this.options.height;
|
|
64
|
+
// Calculate column widths
|
|
65
|
+
const colWidths = this.headers.map((h, i) => {
|
|
66
|
+
const maxRow = Math.max(...this.rows.map(r => Styler_1.Styler.len(r[i] || '')));
|
|
67
|
+
return Math.max(Styler_1.Styler.len(h), maxRow) + 2;
|
|
68
|
+
});
|
|
69
|
+
const buildRow = (items, isHeader = false) => {
|
|
70
|
+
return items.map((item, i) => {
|
|
71
|
+
const len = Styler_1.Styler.len(item);
|
|
72
|
+
const pad = ' '.repeat(colWidths[i] - len - 1);
|
|
73
|
+
const cell = ` ${item}${pad}`;
|
|
74
|
+
const style = isHeader ? 'blue' : (this.isFocused ? 'white' : 'gray');
|
|
75
|
+
return isHeader ? Styler_1.Styler.style(cell, 'bold', 'blue') : Styler_1.Styler.style(cell, style);
|
|
76
|
+
}).join('│');
|
|
77
|
+
};
|
|
78
|
+
const totalWidth = colWidths.reduce((a, b) => a + b, 0) + (colWidths.length - 1);
|
|
79
|
+
const separator = colWidths.map(w => '─'.repeat(w)).join('┼');
|
|
80
|
+
// Draw Header
|
|
81
|
+
Screen_1.Screen.write(startX, currentY++, buildRow(this.headers, true));
|
|
82
|
+
Screen_1.Screen.write(startX, currentY++, Styler_1.Styler.style(separator, 'dim'));
|
|
83
|
+
// Determine visible rows
|
|
84
|
+
let visibleRowsCount = this.rows.length;
|
|
85
|
+
if (height) {
|
|
86
|
+
visibleRowsCount = height - 2; // Subtract header and separator
|
|
87
|
+
}
|
|
88
|
+
for (let i = 0; i < visibleRowsCount; i++) {
|
|
89
|
+
const rowIndex = i + this.scrollTop;
|
|
90
|
+
if (rowIndex < this.rows.length) {
|
|
91
|
+
let rowStr = buildRow(this.rows[rowIndex]);
|
|
92
|
+
// Add Scrollbar indicator if needed
|
|
93
|
+
if (this.options.scrollable && height && this.rows.length > visibleRowsCount) {
|
|
94
|
+
const scrollbarHeight = Math.max(1, Math.floor((visibleRowsCount / this.rows.length) * visibleRowsCount));
|
|
95
|
+
const maxScroll = this.rows.length - visibleRowsCount;
|
|
96
|
+
const scrollPercent = this.scrollTop / maxScroll;
|
|
97
|
+
const scrollPos = Math.floor(scrollPercent * (visibleRowsCount - scrollbarHeight));
|
|
98
|
+
if (i >= scrollPos && i < scrollPos + scrollbarHeight) {
|
|
99
|
+
rowStr += Styler_1.Styler.style('│', 'brightWhite');
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
rowStr += Styler_1.Styler.style('│', 'dim');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
Screen_1.Screen.write(startX, currentY++, rowStr);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
// Clear empty lines if height is fixed
|
|
109
|
+
if (height) {
|
|
110
|
+
Screen_1.Screen.write(startX, currentY++, ' '.repeat(totalWidth + 1));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
exports.Table = Table;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Focusable } from '../core/Focus';
|
|
2
|
+
import * as readline from 'readline';
|
|
3
|
+
export interface TextareaOptions {
|
|
4
|
+
id: string;
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
label?: string;
|
|
10
|
+
value?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare class Textarea implements Focusable {
|
|
13
|
+
id: string;
|
|
14
|
+
value: string;
|
|
15
|
+
private options;
|
|
16
|
+
private isFocused;
|
|
17
|
+
private cursorIndex;
|
|
18
|
+
private scrollTop;
|
|
19
|
+
constructor(options: TextareaOptions);
|
|
20
|
+
focus(): void;
|
|
21
|
+
blur(): void;
|
|
22
|
+
handleKey(key: readline.Key): boolean;
|
|
23
|
+
private adjustScroll;
|
|
24
|
+
render(): void;
|
|
25
|
+
private getLineStart;
|
|
26
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Textarea = void 0;
|
|
4
|
+
const Styler_1 = require("../core/Styler");
|
|
5
|
+
const Screen_1 = require("../core/Screen");
|
|
6
|
+
class Textarea {
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.isFocused = false;
|
|
9
|
+
this.cursorIndex = 0; // Absolute index in value
|
|
10
|
+
this.scrollTop = 0; // Line offset
|
|
11
|
+
this.id = options.id;
|
|
12
|
+
this.options = options;
|
|
13
|
+
this.value = options.value || "";
|
|
14
|
+
this.cursorIndex = this.value.length;
|
|
15
|
+
}
|
|
16
|
+
focus() {
|
|
17
|
+
this.isFocused = true;
|
|
18
|
+
this.render();
|
|
19
|
+
}
|
|
20
|
+
blur() {
|
|
21
|
+
this.isFocused = false;
|
|
22
|
+
this.render();
|
|
23
|
+
}
|
|
24
|
+
handleKey(key) {
|
|
25
|
+
if (!this.isFocused)
|
|
26
|
+
return false;
|
|
27
|
+
let consumed = false;
|
|
28
|
+
if (key.name === 'backspace') {
|
|
29
|
+
if (this.cursorIndex > 0) {
|
|
30
|
+
this.value = this.value.slice(0, this.cursorIndex - 1) + this.value.slice(this.cursorIndex);
|
|
31
|
+
this.cursorIndex--;
|
|
32
|
+
consumed = true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else if (key.name === 'return' || key.name === 'enter') {
|
|
36
|
+
this.value = this.value.slice(0, this.cursorIndex) + '\n' + this.value.slice(this.cursorIndex);
|
|
37
|
+
this.cursorIndex++;
|
|
38
|
+
consumed = true;
|
|
39
|
+
}
|
|
40
|
+
else if (key.name === 'left') {
|
|
41
|
+
if (this.cursorIndex > 0) {
|
|
42
|
+
this.cursorIndex--;
|
|
43
|
+
consumed = true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else if (key.name === 'right') {
|
|
47
|
+
if (this.cursorIndex < this.value.length) {
|
|
48
|
+
this.cursorIndex++;
|
|
49
|
+
consumed = true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else if (key.name === 'up') {
|
|
53
|
+
// Move cursor up a line
|
|
54
|
+
const currentLineStart = this.value.lastIndexOf('\n', this.cursorIndex - 1) + 1;
|
|
55
|
+
const col = this.cursorIndex - currentLineStart;
|
|
56
|
+
if (currentLineStart > 0) {
|
|
57
|
+
const prevLineEnd = currentLineStart - 1;
|
|
58
|
+
const prevLineStart = this.value.lastIndexOf('\n', prevLineEnd - 1) + 1;
|
|
59
|
+
const prevLineLength = prevLineEnd - prevLineStart;
|
|
60
|
+
this.cursorIndex = prevLineStart + Math.min(col, prevLineLength);
|
|
61
|
+
consumed = true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else if (key.name === 'down') {
|
|
65
|
+
// Move cursor down a line
|
|
66
|
+
const nextLineStart = this.value.indexOf('\n', this.cursorIndex);
|
|
67
|
+
if (nextLineStart !== -1) {
|
|
68
|
+
const currentLineStart = this.value.lastIndexOf('\n', this.cursorIndex - 1) + 1;
|
|
69
|
+
const col = this.cursorIndex - currentLineStart;
|
|
70
|
+
const nextLineEnd = this.value.indexOf('\n', nextLineStart + 1);
|
|
71
|
+
const actualNextLineEnd = nextLineEnd === -1 ? this.value.length : nextLineEnd;
|
|
72
|
+
const nextLineLength = actualNextLineEnd - (nextLineStart + 1);
|
|
73
|
+
this.cursorIndex = (nextLineStart + 1) + Math.min(col, nextLineLength);
|
|
74
|
+
consumed = true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
|
|
78
|
+
if (/^[\x20-\x7E]$/.test(key.sequence)) {
|
|
79
|
+
this.value = this.value.slice(0, this.cursorIndex) + key.sequence + this.value.slice(this.cursorIndex);
|
|
80
|
+
this.cursorIndex++;
|
|
81
|
+
consumed = true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (consumed) {
|
|
85
|
+
this.adjustScroll();
|
|
86
|
+
this.render();
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
adjustScroll() {
|
|
92
|
+
// Calculate which line the cursor is on
|
|
93
|
+
const lines = this.value.split('\n');
|
|
94
|
+
let charCount = 0;
|
|
95
|
+
let cursorLine = 0;
|
|
96
|
+
for (let i = 0; i < lines.length; i++) {
|
|
97
|
+
const lineLen = lines[i].length + 1; // +1 for newline
|
|
98
|
+
if (this.cursorIndex < charCount + lineLen) {
|
|
99
|
+
cursorLine = i;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
charCount += lineLen;
|
|
103
|
+
}
|
|
104
|
+
// Special case: cursor at end of string which is a newline
|
|
105
|
+
if (this.cursorIndex === this.value.length && this.value.endsWith('\n')) {
|
|
106
|
+
cursorLine = lines.length; // Actually lines array might have empty string at end
|
|
107
|
+
// If split('\n') on "a\n" gives ["a", ""]. Cursor at end is on line 1 (index 1).
|
|
108
|
+
// Correct.
|
|
109
|
+
}
|
|
110
|
+
const visibleLines = this.options.height - 2; // Border
|
|
111
|
+
if (cursorLine < this.scrollTop) {
|
|
112
|
+
this.scrollTop = cursorLine;
|
|
113
|
+
}
|
|
114
|
+
else if (cursorLine >= this.scrollTop + visibleLines) {
|
|
115
|
+
this.scrollTop = cursorLine - visibleLines + 1;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
render() {
|
|
119
|
+
const { x, y, width, height, label } = this.options;
|
|
120
|
+
const borderColor = this.isFocused ? 'brightCyan' : 'white';
|
|
121
|
+
const innerWidth = width - 2;
|
|
122
|
+
const innerHeight = height - 2;
|
|
123
|
+
// Draw Label if provided
|
|
124
|
+
if (label) {
|
|
125
|
+
Screen_1.Screen.write(x, y - 1, Styler_1.Styler.style(label, 'bold'));
|
|
126
|
+
}
|
|
127
|
+
// Draw Box Border
|
|
128
|
+
const top = '┌' + '─'.repeat(innerWidth) + '┐';
|
|
129
|
+
const bottom = '└' + '─'.repeat(innerWidth) + '┘';
|
|
130
|
+
Screen_1.Screen.write(x, y, Styler_1.Styler.style(top, borderColor));
|
|
131
|
+
for (let i = 0; i < innerHeight; i++) {
|
|
132
|
+
Screen_1.Screen.write(x, y + 1 + i, Styler_1.Styler.style('│' + ' '.repeat(innerWidth) + '│', borderColor));
|
|
133
|
+
}
|
|
134
|
+
Screen_1.Screen.write(x, y + height - 1, Styler_1.Styler.style(bottom, borderColor));
|
|
135
|
+
// Draw Content
|
|
136
|
+
const lines = this.value.split('\n');
|
|
137
|
+
// We need to map cursorIndex to screen coordinates to draw the cursor character
|
|
138
|
+
let charCounter = 0;
|
|
139
|
+
for (let i = 0; i < innerHeight; i++) {
|
|
140
|
+
const lineIndex = i + this.scrollTop;
|
|
141
|
+
if (lineIndex < lines.length) {
|
|
142
|
+
let lineStr = lines[lineIndex];
|
|
143
|
+
// Truncate if too long (TODO: Horizontal scroll?)
|
|
144
|
+
if (lineStr.length > innerWidth) {
|
|
145
|
+
lineStr = lineStr.substring(0, innerWidth);
|
|
146
|
+
}
|
|
147
|
+
// Check if cursor is on this line
|
|
148
|
+
const lineStart = this.getLineStart(lines, lineIndex);
|
|
149
|
+
const lineEnd = lineStart + lines[lineIndex].length;
|
|
150
|
+
let displayStr = lineStr;
|
|
151
|
+
if (this.isFocused && this.cursorIndex >= lineStart && this.cursorIndex <= lineEnd) {
|
|
152
|
+
// Cursor is on this line
|
|
153
|
+
const col = this.cursorIndex - lineStart;
|
|
154
|
+
if (col < innerWidth) {
|
|
155
|
+
const charAtCursor = lineStr[col] || ' ';
|
|
156
|
+
const before = lineStr.substring(0, col);
|
|
157
|
+
const after = lineStr.substring(col + 1);
|
|
158
|
+
displayStr = before + Styler_1.Styler.style(charAtCursor, 'bgGreen', 'black') + after;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Pad to clear
|
|
162
|
+
const remaining = innerWidth - Styler_1.Styler.len(displayStr);
|
|
163
|
+
const finalStr = displayStr + ' '.repeat(Math.max(0, remaining));
|
|
164
|
+
Screen_1.Screen.write(x + 1, y + 1 + i, finalStr);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
getLineStart(lines, lineIndex) {
|
|
169
|
+
let start = 0;
|
|
170
|
+
for (let i = 0; i < lineIndex; i++) {
|
|
171
|
+
start += lines[i].length + 1;
|
|
172
|
+
}
|
|
173
|
+
return start;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
exports.Textarea = Textarea;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Focusable } from '../core/Focus';
|
|
2
|
+
import * as readline from 'readline';
|
|
3
|
+
export interface TreeNode {
|
|
4
|
+
name: string;
|
|
5
|
+
children?: TreeNode[];
|
|
6
|
+
info?: string;
|
|
7
|
+
expanded?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface TreeOptions {
|
|
10
|
+
id: string;
|
|
11
|
+
root: TreeNode;
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
height?: number;
|
|
15
|
+
width?: number;
|
|
16
|
+
}
|
|
17
|
+
export declare class Tree implements Focusable {
|
|
18
|
+
id: string;
|
|
19
|
+
private root;
|
|
20
|
+
private options;
|
|
21
|
+
private isFocused;
|
|
22
|
+
private flatList;
|
|
23
|
+
private selectedIndex;
|
|
24
|
+
private scrollTop;
|
|
25
|
+
constructor(options: TreeOptions);
|
|
26
|
+
private recalculateFlatList;
|
|
27
|
+
focus(): void;
|
|
28
|
+
blur(): void;
|
|
29
|
+
handleKey(key: readline.Key): boolean;
|
|
30
|
+
private ensureVisible;
|
|
31
|
+
render(): void;
|
|
32
|
+
static render(node: TreeNode, prefix?: string, isLast?: boolean): void;
|
|
33
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Tree = void 0;
|
|
4
|
+
const Styler_1 = require("../core/Styler");
|
|
5
|
+
const Screen_1 = require("../core/Screen");
|
|
6
|
+
class Tree {
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.isFocused = false;
|
|
9
|
+
this.flatList = [];
|
|
10
|
+
this.selectedIndex = 0;
|
|
11
|
+
this.scrollTop = 0;
|
|
12
|
+
this.id = options.id;
|
|
13
|
+
this.options = { width: 50, ...options }; // Default width to 50
|
|
14
|
+
this.root = options.root;
|
|
15
|
+
// Ensure root is expanded by default?
|
|
16
|
+
this.root.expanded = true;
|
|
17
|
+
this.recalculateFlatList();
|
|
18
|
+
}
|
|
19
|
+
recalculateFlatList() {
|
|
20
|
+
this.flatList = [];
|
|
21
|
+
const traverse = (node, depth, parent) => {
|
|
22
|
+
this.flatList.push({ node, depth, parent });
|
|
23
|
+
if (node.children && node.expanded) {
|
|
24
|
+
node.children.forEach(child => traverse(child, depth + 1, node));
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
traverse(this.root, 0);
|
|
28
|
+
}
|
|
29
|
+
focus() {
|
|
30
|
+
this.isFocused = true;
|
|
31
|
+
this.render();
|
|
32
|
+
}
|
|
33
|
+
blur() {
|
|
34
|
+
this.isFocused = false;
|
|
35
|
+
this.render();
|
|
36
|
+
}
|
|
37
|
+
handleKey(key) {
|
|
38
|
+
if (!this.isFocused)
|
|
39
|
+
return false;
|
|
40
|
+
let consumed = false;
|
|
41
|
+
if (key.name === 'up') {
|
|
42
|
+
if (this.selectedIndex > 0) {
|
|
43
|
+
this.selectedIndex--;
|
|
44
|
+
this.ensureVisible();
|
|
45
|
+
consumed = true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else if (key.name === 'down') {
|
|
49
|
+
if (this.selectedIndex < this.flatList.length - 1) {
|
|
50
|
+
this.selectedIndex++;
|
|
51
|
+
this.ensureVisible();
|
|
52
|
+
consumed = true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else if (key.name === 'right') {
|
|
56
|
+
const current = this.flatList[this.selectedIndex];
|
|
57
|
+
if (current.node.children) {
|
|
58
|
+
if (!current.node.expanded) {
|
|
59
|
+
current.node.expanded = true;
|
|
60
|
+
this.recalculateFlatList();
|
|
61
|
+
consumed = true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else if (key.name === 'left') {
|
|
66
|
+
const current = this.flatList[this.selectedIndex];
|
|
67
|
+
if (current.node.children && current.node.expanded) {
|
|
68
|
+
current.node.expanded = false;
|
|
69
|
+
this.recalculateFlatList();
|
|
70
|
+
consumed = true;
|
|
71
|
+
}
|
|
72
|
+
else if (current.parent) {
|
|
73
|
+
// Move selection to parent
|
|
74
|
+
const parentIdx = this.flatList.findIndex(x => x.node === current.parent);
|
|
75
|
+
if (parentIdx !== -1) {
|
|
76
|
+
this.selectedIndex = parentIdx;
|
|
77
|
+
this.ensureVisible();
|
|
78
|
+
consumed = true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else if (key.name === 'enter') {
|
|
83
|
+
// Toggle expansion
|
|
84
|
+
const current = this.flatList[this.selectedIndex];
|
|
85
|
+
if (current.node.children) {
|
|
86
|
+
current.node.expanded = !current.node.expanded;
|
|
87
|
+
this.recalculateFlatList();
|
|
88
|
+
consumed = true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (consumed)
|
|
92
|
+
this.render();
|
|
93
|
+
return consumed;
|
|
94
|
+
}
|
|
95
|
+
ensureVisible() {
|
|
96
|
+
if (!this.options.height)
|
|
97
|
+
return;
|
|
98
|
+
const visibleHeight = this.options.height;
|
|
99
|
+
if (this.selectedIndex < this.scrollTop) {
|
|
100
|
+
this.scrollTop = this.selectedIndex;
|
|
101
|
+
}
|
|
102
|
+
else if (this.selectedIndex >= this.scrollTop + visibleHeight) {
|
|
103
|
+
this.scrollTop = this.selectedIndex - visibleHeight + 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
render() {
|
|
107
|
+
const { x, y, height, width } = this.options;
|
|
108
|
+
const visibleHeight = height || this.flatList.length;
|
|
109
|
+
const renderWidth = width || 50;
|
|
110
|
+
// Clear area if possible? Or rely on overwrite.
|
|
111
|
+
// Assuming box drawing or clearing happens before or we write spaces.
|
|
112
|
+
for (let i = 0; i < visibleHeight; i++) {
|
|
113
|
+
const idx = i + this.scrollTop;
|
|
114
|
+
if (idx < this.flatList.length) {
|
|
115
|
+
const { node, depth } = this.flatList[idx];
|
|
116
|
+
const isSelected = idx === this.selectedIndex;
|
|
117
|
+
const indent = ' '.repeat(depth);
|
|
118
|
+
const icon = node.children ? (node.expanded ? '📂' : '📁') : '📄';
|
|
119
|
+
const prefix = isSelected ? (this.isFocused ? '❯ ' : '> ') : ' ';
|
|
120
|
+
let name = node.name;
|
|
121
|
+
if (isSelected && this.isFocused) {
|
|
122
|
+
name = Styler_1.Styler.style(name, 'cyan', 'bold');
|
|
123
|
+
}
|
|
124
|
+
else if (isSelected) {
|
|
125
|
+
name = Styler_1.Styler.style(name, 'white', 'bold');
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
name = Styler_1.Styler.style(name, 'dim');
|
|
129
|
+
}
|
|
130
|
+
// Note: Styler.style adds ansi codes. padEnd counts string length.
|
|
131
|
+
// We should pad AFTER stripping, or be careful.
|
|
132
|
+
// However, padEnd works on the full string including codes, so visual length will be shorter if we pad the colored string.
|
|
133
|
+
// We need to calculate how much padding to add based on visual length.
|
|
134
|
+
const content = `${prefix}${indent}${icon} ${name}`;
|
|
135
|
+
// This is still tricky because 'name' has codes.
|
|
136
|
+
// Simple hack: Write content, then clear rest of line manually.
|
|
137
|
+
// Or construct string without colors to measure length, then add padding.
|
|
138
|
+
const cleanName = node.name;
|
|
139
|
+
const cleanContent = `${prefix}${indent}${icon} ${cleanName}`; // Approximation
|
|
140
|
+
const visualLen = cleanContent.length; // Approximate
|
|
141
|
+
const padding = ' '.repeat(Math.max(0, renderWidth - visualLen));
|
|
142
|
+
Screen_1.Screen.write(x, y + i, content + padding);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
Screen_1.Screen.write(x, y + i, ' '.repeat(renderWidth));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Compatibility Static Render (prints to console directly, not Screen.write at xy)
|
|
150
|
+
static render(node, prefix = '', isLast = true) {
|
|
151
|
+
// This is for CLI usage (console.log)
|
|
152
|
+
const connector = isLast ? '└── ' : '├── ';
|
|
153
|
+
const childPrefix = prefix + (isLast ? ' ' : '│ ');
|
|
154
|
+
const icon = node.children ? '📁' : '📄';
|
|
155
|
+
const info = node.info ? Styler_1.Styler.style(` (${node.info})`, 'dim') : '';
|
|
156
|
+
const name = node.children ? Styler_1.Styler.style(node.name, 'brightCyan', 'bold') : node.name;
|
|
157
|
+
console.log(`${Styler_1.Styler.style(prefix + connector, 'dim')}${icon} ${name}${info}`);
|
|
158
|
+
if (node.children) {
|
|
159
|
+
const children = node.children;
|
|
160
|
+
children.forEach((child, index) => {
|
|
161
|
+
Tree.render(child, childPrefix, index === children.length - 1);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
exports.Tree = Tree;
|