cli-menu-kit 0.1.26 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +23 -5
- package/dist/api.js +16 -4
- package/dist/component-factories.d.ts +59 -0
- package/dist/component-factories.js +141 -0
- package/dist/components/display/header-v2.d.ts +13 -0
- package/dist/components/display/header-v2.js +43 -0
- package/dist/components/display/hints-v2.d.ts +10 -0
- package/dist/components/display/hints-v2.js +34 -0
- package/dist/components/display/hints.d.ts +56 -0
- package/dist/components/display/hints.js +81 -0
- package/dist/components/display/index.d.ts +3 -0
- package/dist/components/display/index.js +15 -1
- package/dist/components/display/input-prompt.d.ts +35 -0
- package/dist/components/display/input-prompt.js +36 -0
- package/dist/components/display/list.d.ts +49 -0
- package/dist/components/display/list.js +86 -0
- package/dist/components/display/table.d.ts +42 -0
- package/dist/components/display/table.js +107 -0
- package/dist/components/menus/boolean-menu.js +2 -1
- package/dist/components/menus/checkbox-menu.d.ts +2 -1
- package/dist/components/menus/checkbox-menu.js +30 -59
- package/dist/components/menus/checkbox-table-menu.d.ts +12 -0
- package/dist/components/menus/checkbox-table-menu.js +395 -0
- package/dist/components/menus/index.d.ts +1 -0
- package/dist/components/menus/index.js +3 -1
- package/dist/components/menus/radio-menu-split.d.ts +33 -0
- package/dist/components/menus/radio-menu-split.js +248 -0
- package/dist/components/menus/radio-menu-v2.d.ts +11 -0
- package/dist/components/menus/radio-menu-v2.js +150 -0
- package/dist/components/menus/radio-menu.d.ts +2 -1
- package/dist/components/menus/radio-menu.js +60 -123
- package/dist/core/hint-manager.d.ts +29 -0
- package/dist/core/hint-manager.js +65 -0
- package/dist/core/renderer.d.ts +2 -1
- package/dist/core/renderer.js +22 -6
- package/dist/core/screen-manager.d.ts +54 -0
- package/dist/core/screen-manager.js +119 -0
- package/dist/core/state-manager.d.ts +27 -0
- package/dist/core/state-manager.js +56 -0
- package/dist/core/terminal.d.ts +4 -1
- package/dist/core/terminal.js +37 -4
- package/dist/core/virtual-scroll.d.ts +65 -0
- package/dist/core/virtual-scroll.js +120 -0
- package/dist/i18n/languages/en.js +4 -1
- package/dist/i18n/languages/zh.js +4 -1
- package/dist/i18n/registry.d.ts +4 -3
- package/dist/i18n/registry.js +12 -4
- package/dist/i18n/types.d.ts +3 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +30 -4
- package/dist/layout.d.ts +68 -0
- package/dist/layout.js +134 -0
- package/dist/page-layout.d.ts +92 -0
- package/dist/page-layout.js +156 -0
- package/dist/types/menu.types.d.ts +57 -5
- package/package.json +1 -1
- package/dist/types/layout.types.d.ts +0 -56
- package/dist/types/layout.types.js +0 -36
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hint Manager - Manages hints from multiple components with priority
|
|
3
|
+
*
|
|
4
|
+
* Allows multiple components to set hints with different priorities.
|
|
5
|
+
* The highest priority hint is displayed.
|
|
6
|
+
*/
|
|
7
|
+
import { EventEmitter } from 'events';
|
|
8
|
+
export declare class HintManager extends EventEmitter {
|
|
9
|
+
private entries;
|
|
10
|
+
/**
|
|
11
|
+
* Set a hint with a token and priority
|
|
12
|
+
* @param token - Unique identifier for this hint source
|
|
13
|
+
* @param text - Hint text to display
|
|
14
|
+
* @param priority - Higher priority hints are displayed first (default: 0)
|
|
15
|
+
*/
|
|
16
|
+
set(token: string, text: string, priority?: number): void;
|
|
17
|
+
/**
|
|
18
|
+
* Clear a hint by token
|
|
19
|
+
*/
|
|
20
|
+
clear(token: string): void;
|
|
21
|
+
/**
|
|
22
|
+
* Get the current highest priority hint
|
|
23
|
+
*/
|
|
24
|
+
current(): string;
|
|
25
|
+
/**
|
|
26
|
+
* Clear all hints
|
|
27
|
+
*/
|
|
28
|
+
clearAll(): void;
|
|
29
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Hint Manager - Manages hints from multiple components with priority
|
|
4
|
+
*
|
|
5
|
+
* Allows multiple components to set hints with different priorities.
|
|
6
|
+
* The highest priority hint is displayed.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.HintManager = void 0;
|
|
10
|
+
const events_1 = require("events");
|
|
11
|
+
class HintManager extends events_1.EventEmitter {
|
|
12
|
+
constructor() {
|
|
13
|
+
super(...arguments);
|
|
14
|
+
this.entries = new Map();
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Set a hint with a token and priority
|
|
18
|
+
* @param token - Unique identifier for this hint source
|
|
19
|
+
* @param text - Hint text to display
|
|
20
|
+
* @param priority - Higher priority hints are displayed first (default: 0)
|
|
21
|
+
*/
|
|
22
|
+
set(token, text, priority = 0) {
|
|
23
|
+
this.entries.set(token, {
|
|
24
|
+
text,
|
|
25
|
+
priority,
|
|
26
|
+
timestamp: Date.now()
|
|
27
|
+
});
|
|
28
|
+
this.emit('change', this.current());
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Clear a hint by token
|
|
32
|
+
*/
|
|
33
|
+
clear(token) {
|
|
34
|
+
if (this.entries.delete(token)) {
|
|
35
|
+
this.emit('change', this.current());
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Get the current highest priority hint
|
|
40
|
+
*/
|
|
41
|
+
current() {
|
|
42
|
+
const list = Array.from(this.entries.values());
|
|
43
|
+
if (list.length === 0) {
|
|
44
|
+
return '';
|
|
45
|
+
}
|
|
46
|
+
// Sort by priority (descending), then by timestamp (most recent first)
|
|
47
|
+
list.sort((a, b) => {
|
|
48
|
+
if (b.priority !== a.priority) {
|
|
49
|
+
return b.priority - a.priority;
|
|
50
|
+
}
|
|
51
|
+
return b.timestamp - a.timestamp;
|
|
52
|
+
});
|
|
53
|
+
return list[0].text;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Clear all hints
|
|
57
|
+
*/
|
|
58
|
+
clearAll() {
|
|
59
|
+
if (this.entries.size > 0) {
|
|
60
|
+
this.entries.clear();
|
|
61
|
+
this.emit('change', '');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
exports.HintManager = HintManager;
|
package/dist/core/renderer.d.ts
CHANGED
|
@@ -43,8 +43,9 @@ export declare function renderSeparator(char?: string, width?: number): void;
|
|
|
43
43
|
* Render a section label (menu grouping)
|
|
44
44
|
* @param label - Label text (optional)
|
|
45
45
|
* @param width - Total width of the separator (default: 30)
|
|
46
|
+
* @param align - Alignment of the label (default: 'center')
|
|
46
47
|
*/
|
|
47
|
-
export declare function renderSectionLabel(label?: string, width?: number): void;
|
|
48
|
+
export declare function renderSectionLabel(label?: string, width?: number, align?: 'left' | 'center' | 'right'): void;
|
|
48
49
|
/**
|
|
49
50
|
* Render a message with icon
|
|
50
51
|
* @param type - Message type (success, error, warning, info, question)
|
package/dist/core/renderer.js
CHANGED
|
@@ -135,17 +135,33 @@ function renderSeparator(char = '─', width) {
|
|
|
135
135
|
* Render a section label (menu grouping)
|
|
136
136
|
* @param label - Label text (optional)
|
|
137
137
|
* @param width - Total width of the separator (default: 30)
|
|
138
|
+
* @param align - Alignment of the label (default: 'center')
|
|
138
139
|
*/
|
|
139
|
-
function renderSectionLabel(label, width = 30) {
|
|
140
|
+
function renderSectionLabel(label, width = 30, align = 'center') {
|
|
140
141
|
if (label) {
|
|
141
|
-
const totalWidth = width;
|
|
142
|
-
const padding = 2; // Spaces around label
|
|
142
|
+
const totalWidth = width;
|
|
143
143
|
const labelWithPadding = ` ${label} `;
|
|
144
144
|
const labelLength = labelWithPadding.length;
|
|
145
145
|
const dashesTotal = totalWidth - labelLength;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
let dashesLeft;
|
|
147
|
+
let dashesRight;
|
|
148
|
+
switch (align) {
|
|
149
|
+
case 'left':
|
|
150
|
+
dashesLeft = 0;
|
|
151
|
+
dashesRight = dashesTotal;
|
|
152
|
+
break;
|
|
153
|
+
case 'right':
|
|
154
|
+
dashesLeft = dashesTotal;
|
|
155
|
+
dashesRight = 0;
|
|
156
|
+
break;
|
|
157
|
+
case 'center':
|
|
158
|
+
default:
|
|
159
|
+
dashesLeft = Math.floor(dashesTotal / 2);
|
|
160
|
+
dashesRight = dashesTotal - dashesLeft;
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
// Use primary color (cyan) and bold for phase labels
|
|
164
|
+
const line = ` ${colors_js_1.colors.cyan}${colors_js_1.colors.bold}${'─'.repeat(dashesLeft)}${labelWithPadding}${'─'.repeat(dashesRight)}${colors_js_1.colors.reset}`;
|
|
149
165
|
(0, terminal_js_1.writeLine)(line);
|
|
150
166
|
}
|
|
151
167
|
else {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screen Manager - Manages screen regions with fixed-height layout
|
|
3
|
+
*
|
|
4
|
+
* Uses absolute cursor positioning and per-region caching for efficient updates.
|
|
5
|
+
* Only updates regions that have changed (diff-based rendering).
|
|
6
|
+
*/
|
|
7
|
+
export interface Rect {
|
|
8
|
+
top: number;
|
|
9
|
+
left: number;
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
}
|
|
13
|
+
export declare class ScreenManager {
|
|
14
|
+
private cache;
|
|
15
|
+
private regions;
|
|
16
|
+
private isAltScreen;
|
|
17
|
+
/**
|
|
18
|
+
* Enter alternate screen buffer and hide cursor
|
|
19
|
+
*/
|
|
20
|
+
enter(): void;
|
|
21
|
+
/**
|
|
22
|
+
* Exit alternate screen buffer and show cursor
|
|
23
|
+
*/
|
|
24
|
+
exit(): void;
|
|
25
|
+
/**
|
|
26
|
+
* Move cursor to absolute position (1-based)
|
|
27
|
+
*/
|
|
28
|
+
moveTo(row: number, col: number): void;
|
|
29
|
+
/**
|
|
30
|
+
* Register a fixed-height region
|
|
31
|
+
*/
|
|
32
|
+
registerRegion(id: string, rect: Rect): void;
|
|
33
|
+
/**
|
|
34
|
+
* Render a region with diff-based updates
|
|
35
|
+
* Only updates lines that have changed
|
|
36
|
+
*/
|
|
37
|
+
renderRegion(id: string, lines: string[]): void;
|
|
38
|
+
/**
|
|
39
|
+
* Clear a region (fill with spaces)
|
|
40
|
+
*/
|
|
41
|
+
clearRegion(id: string): void;
|
|
42
|
+
/**
|
|
43
|
+
* Invalidate all cached content (forces full re-render on next update)
|
|
44
|
+
*/
|
|
45
|
+
invalidateAll(): void;
|
|
46
|
+
/**
|
|
47
|
+
* Reset manager state
|
|
48
|
+
*/
|
|
49
|
+
reset(): void;
|
|
50
|
+
/**
|
|
51
|
+
* Get region rect
|
|
52
|
+
*/
|
|
53
|
+
getRegion(id: string): Rect | undefined;
|
|
54
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Screen Manager - Manages screen regions with fixed-height layout
|
|
4
|
+
*
|
|
5
|
+
* Uses absolute cursor positioning and per-region caching for efficient updates.
|
|
6
|
+
* Only updates regions that have changed (diff-based rendering).
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.ScreenManager = void 0;
|
|
10
|
+
const CSI = '\x1b[';
|
|
11
|
+
/**
|
|
12
|
+
* Fit text to exact width by padding or truncating
|
|
13
|
+
*/
|
|
14
|
+
function fitText(text, width) {
|
|
15
|
+
// Simple implementation - can be enhanced with ANSI-aware width calculation
|
|
16
|
+
const stripped = text.replace(/\x1b\[[0-9;]*m/g, ''); // Strip ANSI codes for length calc
|
|
17
|
+
const len = stripped.length;
|
|
18
|
+
if (len === width)
|
|
19
|
+
return text;
|
|
20
|
+
if (len < width)
|
|
21
|
+
return text + ' '.repeat(width - len);
|
|
22
|
+
// Truncate - preserve ANSI codes at start if present
|
|
23
|
+
const ansiMatch = text.match(/^(\x1b\[[0-9;]*m)*/);
|
|
24
|
+
const prefix = ansiMatch ? ansiMatch[0] : '';
|
|
25
|
+
const content = text.slice(prefix.length);
|
|
26
|
+
return prefix + content.slice(0, width);
|
|
27
|
+
}
|
|
28
|
+
class ScreenManager {
|
|
29
|
+
constructor() {
|
|
30
|
+
this.cache = new Map();
|
|
31
|
+
this.regions = new Map();
|
|
32
|
+
this.isAltScreen = false;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Enter alternate screen buffer and hide cursor
|
|
36
|
+
*/
|
|
37
|
+
enter() {
|
|
38
|
+
if (!this.isAltScreen) {
|
|
39
|
+
process.stdout.write(`${CSI}?1049h${CSI}?25l`);
|
|
40
|
+
this.isAltScreen = true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Exit alternate screen buffer and show cursor
|
|
45
|
+
*/
|
|
46
|
+
exit() {
|
|
47
|
+
if (this.isAltScreen) {
|
|
48
|
+
process.stdout.write(`${CSI}?25h${CSI}?1049l`);
|
|
49
|
+
this.isAltScreen = false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Move cursor to absolute position (1-based)
|
|
54
|
+
*/
|
|
55
|
+
moveTo(row, col) {
|
|
56
|
+
process.stdout.write(`${CSI}${row};${col}H`);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Register a fixed-height region
|
|
60
|
+
*/
|
|
61
|
+
registerRegion(id, rect) {
|
|
62
|
+
this.regions.set(id, rect);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Render a region with diff-based updates
|
|
66
|
+
* Only updates lines that have changed
|
|
67
|
+
*/
|
|
68
|
+
renderRegion(id, lines) {
|
|
69
|
+
const rect = this.regions.get(id);
|
|
70
|
+
if (!rect) {
|
|
71
|
+
throw new Error(`Region ${id} not registered`);
|
|
72
|
+
}
|
|
73
|
+
const prev = this.cache.get(id) ?? [];
|
|
74
|
+
const next = Array.from({ length: rect.height }, (_, i) => fitText(lines[i] ?? '', rect.width));
|
|
75
|
+
// Update only changed lines
|
|
76
|
+
for (let i = 0; i < rect.height; i++) {
|
|
77
|
+
if (next[i] === prev[i])
|
|
78
|
+
continue;
|
|
79
|
+
this.moveTo(rect.top + i, rect.left);
|
|
80
|
+
process.stdout.write(next[i]);
|
|
81
|
+
}
|
|
82
|
+
this.cache.set(id, next);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Clear a region (fill with spaces)
|
|
86
|
+
*/
|
|
87
|
+
clearRegion(id) {
|
|
88
|
+
const rect = this.regions.get(id);
|
|
89
|
+
if (!rect) {
|
|
90
|
+
throw new Error(`Region ${id} not registered`);
|
|
91
|
+
}
|
|
92
|
+
const blank = ' '.repeat(rect.width);
|
|
93
|
+
for (let i = 0; i < rect.height; i++) {
|
|
94
|
+
this.moveTo(rect.top + i, rect.left);
|
|
95
|
+
process.stdout.write(blank);
|
|
96
|
+
}
|
|
97
|
+
this.cache.set(id, Array(rect.height).fill(blank));
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Invalidate all cached content (forces full re-render on next update)
|
|
101
|
+
*/
|
|
102
|
+
invalidateAll() {
|
|
103
|
+
this.cache.clear();
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Reset manager state
|
|
107
|
+
*/
|
|
108
|
+
reset() {
|
|
109
|
+
this.cache.clear();
|
|
110
|
+
this.regions.clear();
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Get region rect
|
|
114
|
+
*/
|
|
115
|
+
getRegion(id) {
|
|
116
|
+
return this.regions.get(id);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
exports.ScreenManager = ScreenManager;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared State Manager
|
|
3
|
+
* Allows components to share state without direct coupling
|
|
4
|
+
*/
|
|
5
|
+
type StateListener<T> = (value: T) => void;
|
|
6
|
+
export declare class StateManager {
|
|
7
|
+
private states;
|
|
8
|
+
private listeners;
|
|
9
|
+
/**
|
|
10
|
+
* Set a state value and notify listeners
|
|
11
|
+
*/
|
|
12
|
+
setState<T>(key: string, value: T): void;
|
|
13
|
+
/**
|
|
14
|
+
* Get a state value
|
|
15
|
+
*/
|
|
16
|
+
getState<T>(key: string): T | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Subscribe to state changes
|
|
19
|
+
*/
|
|
20
|
+
subscribe<T>(key: string, listener: StateListener<T>): () => void;
|
|
21
|
+
/**
|
|
22
|
+
* Clear all state
|
|
23
|
+
*/
|
|
24
|
+
clear(): void;
|
|
25
|
+
}
|
|
26
|
+
export declare const globalState: StateManager;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shared State Manager
|
|
4
|
+
* Allows components to share state without direct coupling
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.globalState = exports.StateManager = void 0;
|
|
8
|
+
class StateManager {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.states = new Map();
|
|
11
|
+
this.listeners = new Map();
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Set a state value and notify listeners
|
|
15
|
+
*/
|
|
16
|
+
setState(key, value) {
|
|
17
|
+
this.states.set(key, value);
|
|
18
|
+
// Notify all listeners
|
|
19
|
+
const listeners = this.listeners.get(key);
|
|
20
|
+
if (listeners) {
|
|
21
|
+
listeners.forEach(listener => listener(value));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Get a state value
|
|
26
|
+
*/
|
|
27
|
+
getState(key) {
|
|
28
|
+
return this.states.get(key);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Subscribe to state changes
|
|
32
|
+
*/
|
|
33
|
+
subscribe(key, listener) {
|
|
34
|
+
if (!this.listeners.has(key)) {
|
|
35
|
+
this.listeners.set(key, new Set());
|
|
36
|
+
}
|
|
37
|
+
this.listeners.get(key).add(listener);
|
|
38
|
+
// Return unsubscribe function
|
|
39
|
+
return () => {
|
|
40
|
+
const listeners = this.listeners.get(key);
|
|
41
|
+
if (listeners) {
|
|
42
|
+
listeners.delete(listener);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Clear all state
|
|
48
|
+
*/
|
|
49
|
+
clear() {
|
|
50
|
+
this.states.clear();
|
|
51
|
+
this.listeners.clear();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
exports.StateManager = StateManager;
|
|
55
|
+
// Global state manager instance
|
|
56
|
+
exports.globalState = new StateManager();
|
package/dist/core/terminal.d.ts
CHANGED
|
@@ -9,12 +9,14 @@ export interface TerminalState {
|
|
|
9
9
|
stdin: NodeJS.ReadStream;
|
|
10
10
|
renderedLines: number;
|
|
11
11
|
isRawMode: boolean;
|
|
12
|
+
useAltScreen: boolean;
|
|
12
13
|
}
|
|
13
14
|
/**
|
|
14
15
|
* Initialize terminal for interactive mode
|
|
16
|
+
* @param useAltScreen - Whether to use alternate screen buffer (prevents scroll issues)
|
|
15
17
|
* @returns Terminal state object
|
|
16
18
|
*/
|
|
17
|
-
export declare function initTerminal(): TerminalState;
|
|
19
|
+
export declare function initTerminal(useAltScreen?: boolean): TerminalState;
|
|
18
20
|
/**
|
|
19
21
|
* Restore terminal to normal mode
|
|
20
22
|
* @param state - Terminal state
|
|
@@ -22,6 +24,7 @@ export declare function initTerminal(): TerminalState;
|
|
|
22
24
|
export declare function restoreTerminal(state: TerminalState): void;
|
|
23
25
|
/**
|
|
24
26
|
* Clear the current menu display
|
|
27
|
+
* Only clears the lines that were rendered by this menu
|
|
25
28
|
* @param state - Terminal state
|
|
26
29
|
*/
|
|
27
30
|
export declare function clearMenu(state: TerminalState): void;
|
package/dist/core/terminal.js
CHANGED
|
@@ -22,20 +22,36 @@ exports.clearScreen = clearScreen;
|
|
|
22
22
|
exports.exitWithGoodbye = exitWithGoodbye;
|
|
23
23
|
/**
|
|
24
24
|
* Initialize terminal for interactive mode
|
|
25
|
+
* @param useAltScreen - Whether to use alternate screen buffer (prevents scroll issues)
|
|
25
26
|
* @returns Terminal state object
|
|
26
27
|
*/
|
|
27
|
-
function initTerminal() {
|
|
28
|
+
function initTerminal(useAltScreen = false) {
|
|
28
29
|
const stdin = process.stdin;
|
|
30
|
+
// Disable all mouse tracking modes BEFORE enabling raw mode
|
|
31
|
+
process.stdout.write('\x1b[?1000l'); // Disable normal mouse tracking
|
|
32
|
+
process.stdout.write('\x1b[?1001l'); // Disable highlight mouse tracking
|
|
33
|
+
process.stdout.write('\x1b[?1002l'); // Disable button event tracking
|
|
34
|
+
process.stdout.write('\x1b[?1003l'); // Disable any event tracking
|
|
35
|
+
process.stdout.write('\x1b[?1004l'); // Disable focus events
|
|
36
|
+
process.stdout.write('\x1b[?1005l'); // Disable UTF-8 mouse mode
|
|
37
|
+
process.stdout.write('\x1b[?1006l'); // Disable SGR extended mouse mode
|
|
38
|
+
process.stdout.write('\x1b[?1015l'); // Disable urxvt mouse mode
|
|
29
39
|
// Enable raw mode for character-by-character input
|
|
30
40
|
stdin.setRawMode(true);
|
|
31
41
|
stdin.resume();
|
|
32
42
|
stdin.setEncoding('utf8');
|
|
43
|
+
// Use alternate screen buffer if requested
|
|
44
|
+
if (useAltScreen) {
|
|
45
|
+
process.stdout.write('\x1b[?1049h'); // Enable alternate screen
|
|
46
|
+
process.stdout.write('\x1b[H'); // Move cursor to home
|
|
47
|
+
}
|
|
33
48
|
// Hide cursor
|
|
34
49
|
process.stdout.write('\x1b[?25l');
|
|
35
50
|
return {
|
|
36
51
|
stdin,
|
|
37
52
|
renderedLines: 0,
|
|
38
|
-
isRawMode: true
|
|
53
|
+
isRawMode: true,
|
|
54
|
+
useAltScreen
|
|
39
55
|
};
|
|
40
56
|
}
|
|
41
57
|
/**
|
|
@@ -47,20 +63,37 @@ function restoreTerminal(state) {
|
|
|
47
63
|
state.stdin.setRawMode(false);
|
|
48
64
|
state.isRawMode = false;
|
|
49
65
|
}
|
|
66
|
+
// Restore alternate screen if it was used
|
|
67
|
+
if (state.useAltScreen) {
|
|
68
|
+
process.stdout.write('\x1b[?1049l'); // Disable alternate screen
|
|
69
|
+
}
|
|
50
70
|
// Show cursor
|
|
51
71
|
process.stdout.write('\x1b[?25h');
|
|
52
72
|
state.stdin.pause();
|
|
53
73
|
}
|
|
54
74
|
/**
|
|
55
75
|
* Clear the current menu display
|
|
76
|
+
* Only clears the lines that were rendered by this menu
|
|
56
77
|
* @param state - Terminal state
|
|
57
78
|
*/
|
|
58
79
|
function clearMenu(state) {
|
|
59
80
|
if (state.renderedLines > 0) {
|
|
60
81
|
// Move cursor up to the start of the menu
|
|
61
82
|
process.stdout.write(`\x1b[${state.renderedLines}A`);
|
|
62
|
-
// Clear
|
|
63
|
-
|
|
83
|
+
// Clear each line individually
|
|
84
|
+
for (let i = 0; i < state.renderedLines; i++) {
|
|
85
|
+
process.stdout.write('\x1b[2K'); // Clear entire line
|
|
86
|
+
if (i < state.renderedLines - 1) {
|
|
87
|
+
process.stdout.write('\x1b[1B'); // Move down one line
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Move cursor back to start position
|
|
91
|
+
// After loop, cursor is at the last rendered line
|
|
92
|
+
// To get back to line 1, move up (renderedLines - 1)
|
|
93
|
+
// Note: \x1b[0A defaults to 1 in ANSI spec, so skip when renderedLines === 1
|
|
94
|
+
if (state.renderedLines > 1) {
|
|
95
|
+
process.stdout.write(`\x1b[${state.renderedLines - 1}A`);
|
|
96
|
+
}
|
|
64
97
|
state.renderedLines = 0;
|
|
65
98
|
}
|
|
66
99
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Virtual scrolling utilities for rendering large lists efficiently
|
|
3
|
+
* by only displaying a visible window of items.
|
|
4
|
+
*/
|
|
5
|
+
export interface VirtualScrollOptions<T> {
|
|
6
|
+
/** All items in the list */
|
|
7
|
+
items: T[];
|
|
8
|
+
/** Current cursor/focus position (index in items array) */
|
|
9
|
+
cursorIndex: number;
|
|
10
|
+
/** Target number of lines to display */
|
|
11
|
+
targetLines: number;
|
|
12
|
+
/** Function to calculate how many lines each item will occupy when rendered */
|
|
13
|
+
getItemLineCount: (item: T, index: number) => number;
|
|
14
|
+
}
|
|
15
|
+
export interface VirtualScrollResult {
|
|
16
|
+
/** Start index of visible range (inclusive) */
|
|
17
|
+
visibleStart: number;
|
|
18
|
+
/** End index of visible range (exclusive) */
|
|
19
|
+
visibleEnd: number;
|
|
20
|
+
/** Actual number of lines that will be rendered */
|
|
21
|
+
actualLines: number;
|
|
22
|
+
/** Whether virtual scrolling is active (content exceeds target) */
|
|
23
|
+
isScrolled: boolean;
|
|
24
|
+
/** Whether there are items before the visible range */
|
|
25
|
+
hasItemsBefore: boolean;
|
|
26
|
+
/** Whether there are items after the visible range */
|
|
27
|
+
hasItemsAfter: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Calculate visible range for virtual scrolling based on line count.
|
|
31
|
+
*
|
|
32
|
+
* This function maintains a stable viewport height by calculating which items
|
|
33
|
+
* to display based on their actual line count, not just item count. This prevents
|
|
34
|
+
* height jumping when items have varying heights (e.g., separators with descriptions).
|
|
35
|
+
*
|
|
36
|
+
* Algorithm:
|
|
37
|
+
* 1. Calculate total lines needed for all items
|
|
38
|
+
* 2. If total <= targetLines, show everything (no scrolling)
|
|
39
|
+
* 3. Otherwise, create a window centered on cursor:
|
|
40
|
+
* - Start from cursor position
|
|
41
|
+
* - Expand downward until reaching target or end
|
|
42
|
+
* - Expand upward to fill remaining space
|
|
43
|
+
* - Expand downward again if space remains
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* const result = calculateVirtualScroll({
|
|
48
|
+
* items: menuOptions,
|
|
49
|
+
* cursorIndex: 10,
|
|
50
|
+
* targetLines: 30,
|
|
51
|
+
* getItemLineCount: (item, index) => {
|
|
52
|
+
* if (item.type === 'separator') {
|
|
53
|
+
* return 1 + (item.description ? 1 : 0) + (index > 0 ? 1 : 0);
|
|
54
|
+
* }
|
|
55
|
+
* return 1;
|
|
56
|
+
* }
|
|
57
|
+
* });
|
|
58
|
+
*
|
|
59
|
+
* // Render only visible items
|
|
60
|
+
* for (let i = result.visibleStart; i < result.visibleEnd; i++) {
|
|
61
|
+
* renderItem(items[i]);
|
|
62
|
+
* }
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export declare function calculateVirtualScroll<T>(options: VirtualScrollOptions<T>): VirtualScrollResult;
|