@neuraiproject/neurai-depin-terminal 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/LICENSE +21 -0
- package/README.md +195 -0
- package/config.example.json +10 -0
- package/dist/index.cjs +218 -0
- package/package.json +61 -0
- package/src/config/ConfigManager.js +363 -0
- package/src/constants.js +208 -0
- package/src/errors.js +149 -0
- package/src/index.js +528 -0
- package/src/lib/depinMsgLoader.js +73 -0
- package/src/lib/empty.js +2 -0
- package/src/messaging/MessagePoller.js +300 -0
- package/src/messaging/MessageSender.js +194 -0
- package/src/messaging/MessageStore.js +99 -0
- package/src/services/RpcService.js +200 -0
- package/src/ui/TerminalUI.js +272 -0
- package/src/ui/components/ErrorOverlay.js +99 -0
- package/src/ui/components/InputBox.js +63 -0
- package/src/ui/components/MessageBox.js +51 -0
- package/src/ui/components/StatusBar.js +32 -0
- package/src/ui/components/TopBar.js +63 -0
- package/src/utils.js +309 -0
- package/src/wallet/WalletManager.js +94 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RPC Service for Neurai DePIN Terminal
|
|
3
|
+
* Handles RPC connection, initialization, and method calls
|
|
4
|
+
* @module RpcService
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import neuraiJsWallet from '@neuraiproject/neurai-jswallet';
|
|
8
|
+
import {
|
|
9
|
+
RPC,
|
|
10
|
+
RPC_METHODS,
|
|
11
|
+
WARNING_MESSAGES,
|
|
12
|
+
SUCCESS_MESSAGES,
|
|
13
|
+
ERROR_MESSAGES
|
|
14
|
+
} from '../constants.js';
|
|
15
|
+
import { RpcError } from '../errors.js';
|
|
16
|
+
import { formatRpcUrl } from '../utils.js';
|
|
17
|
+
|
|
18
|
+
const { Wallet } = neuraiJsWallet;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Manages RPC connectivity and method calls
|
|
22
|
+
*/
|
|
23
|
+
export class RpcService {
|
|
24
|
+
/**
|
|
25
|
+
* Create a new RpcService instance
|
|
26
|
+
* @param {Object} config - Configuration object
|
|
27
|
+
* @param {string} config.network - Network name (xna)
|
|
28
|
+
* @param {string} config.rpc_url - RPC server URL
|
|
29
|
+
* @param {string} [config.rpc_username] - Optional RPC username
|
|
30
|
+
* @param {string} [config.rpc_password] - Optional RPC password
|
|
31
|
+
*/
|
|
32
|
+
constructor(config) {
|
|
33
|
+
this.config = config;
|
|
34
|
+
this.wallet = null;
|
|
35
|
+
this.connected = false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Initialize RPC wallet client
|
|
40
|
+
* Creates a wallet instance for RPC access
|
|
41
|
+
* @returns {Promise<void>}
|
|
42
|
+
*/
|
|
43
|
+
async initialize() {
|
|
44
|
+
const rpcUrl = formatRpcUrl(this.config.rpc_url, RPC.ENDPOINT_SUFFIX);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
this.wallet = new Wallet();
|
|
48
|
+
await this.wallet.init({
|
|
49
|
+
mnemonic: RPC.DUMMY_MNEMONIC,
|
|
50
|
+
network: this.config.network,
|
|
51
|
+
rpc_url: rpcUrl,
|
|
52
|
+
rpc_username: this.config.rpc_username || undefined,
|
|
53
|
+
rpc_password: this.config.rpc_password || undefined,
|
|
54
|
+
offlineMode: false,
|
|
55
|
+
minAmountOfAddresses: 1
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Test connection
|
|
59
|
+
await this.testConnection();
|
|
60
|
+
} catch (error) {
|
|
61
|
+
this.connected = false;
|
|
62
|
+
this.wallet = null;
|
|
63
|
+
console.warn(WARNING_MESSAGES.RPC_INIT_FAILED);
|
|
64
|
+
console.warn(` Error: ${error.message || 'Unknown error'}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Test RPC connection by calling getblockchaininfo
|
|
70
|
+
* @param {boolean} [silent=false] - If true, suppress console output
|
|
71
|
+
* @returns {Promise<boolean>} True if connected, false otherwise
|
|
72
|
+
*/
|
|
73
|
+
async testConnection(silent = false) {
|
|
74
|
+
try {
|
|
75
|
+
await this.wallet.rpc(RPC_METHODS.GET_BLOCKCHAIN_INFO, []);
|
|
76
|
+
this.connected = true;
|
|
77
|
+
if (!silent) console.log(SUCCESS_MESSAGES.RPC_CONNECTED);
|
|
78
|
+
return true;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
this.connected = false;
|
|
81
|
+
if (!silent) console.warn(WARNING_MESSAGES.RPC_CONNECTION_FAILED);
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Execute an RPC method
|
|
88
|
+
* @param {string} method - RPC method name
|
|
89
|
+
* @param {Array} [params=[]] - RPC parameters
|
|
90
|
+
* @returns {Promise<any>} RPC result
|
|
91
|
+
* @throws {RpcError} If RPC call fails or not initialized
|
|
92
|
+
*/
|
|
93
|
+
async call(method, params = []) {
|
|
94
|
+
if (!this.wallet) {
|
|
95
|
+
throw new RpcError(ERROR_MESSAGES.RPC_NOT_INITIALIZED);
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const result = await this.wallet.rpc(method, params);
|
|
99
|
+
this.connected = true;
|
|
100
|
+
return result;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
this.connected = false;
|
|
103
|
+
throw new RpcError(error.message);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Verify if an address holds a specific token
|
|
109
|
+
* @param {string} address - Address to check
|
|
110
|
+
* @param {string} token - Token name
|
|
111
|
+
* @returns {Promise<boolean>} True if address holds the token
|
|
112
|
+
*/
|
|
113
|
+
async verifyTokenOwnership(address, token) {
|
|
114
|
+
try {
|
|
115
|
+
const result = await this.call(RPC_METHODS.LIST_ADDRESSES_BY_ASSET, [token]);
|
|
116
|
+
// Result is an object where keys are addresses and values are balances
|
|
117
|
+
return Object.prototype.hasOwnProperty.call(result, address) && result[address] > 0;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.warn(`Failed to verify token ownership: ${error.message}`);
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Check if the public key for an address is revealed on the blockchain
|
|
126
|
+
* @param {string} address - Address to check
|
|
127
|
+
* @returns {Promise<boolean>} True if public key is revealed
|
|
128
|
+
*/
|
|
129
|
+
async checkPubKeyRevealed(address) {
|
|
130
|
+
try {
|
|
131
|
+
const result = await this.call(RPC_METHODS.GET_PUBKEY, [address]);
|
|
132
|
+
return result && (result.revealed === 1 || result.revealed === true);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.warn(`Failed to check pubkey: ${error.message}`);
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Check if currently connected to RPC server
|
|
141
|
+
* @returns {boolean} Connection status
|
|
142
|
+
*/
|
|
143
|
+
isConnected() {
|
|
144
|
+
return this.connected;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Attempt to reconnect to RPC server
|
|
149
|
+
* Tries to reinitialize the wallet if not connected
|
|
150
|
+
* @param {boolean} [silent=true] - If true, suppress console output
|
|
151
|
+
* @returns {Promise<boolean>} True if reconnection successful, false otherwise
|
|
152
|
+
*/
|
|
153
|
+
async attemptReconnect(silent = true) {
|
|
154
|
+
// If already connected, no need to reconnect
|
|
155
|
+
if (this.connected && this.wallet) {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// If wallet exists, just test the connection
|
|
160
|
+
if (this.wallet) {
|
|
161
|
+
try {
|
|
162
|
+
await this.wallet.rpc(RPC_METHODS.GET_BLOCKCHAIN_INFO, []);
|
|
163
|
+
this.connected = true;
|
|
164
|
+
return true;
|
|
165
|
+
} catch (error) {
|
|
166
|
+
this.connected = false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Wallet doesn't exist or check failed, need to reinitialize RPC client
|
|
171
|
+
try {
|
|
172
|
+
const rpcUrl = formatRpcUrl(this.config.rpc_url, RPC.ENDPOINT_SUFFIX);
|
|
173
|
+
|
|
174
|
+
this.wallet = new Wallet();
|
|
175
|
+
await this.wallet.init({
|
|
176
|
+
mnemonic: RPC.DUMMY_MNEMONIC,
|
|
177
|
+
network: this.config.network,
|
|
178
|
+
rpc_url: rpcUrl,
|
|
179
|
+
rpc_username: this.config.rpc_username || undefined,
|
|
180
|
+
rpc_password: this.config.rpc_password || undefined,
|
|
181
|
+
offlineMode: false,
|
|
182
|
+
minAmountOfAddresses: 1
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Test connection
|
|
186
|
+
await this.wallet.rpc(RPC_METHODS.GET_BLOCKCHAIN_INFO, []);
|
|
187
|
+
this.connected = true;
|
|
188
|
+
|
|
189
|
+
if (!silent) {
|
|
190
|
+
console.log('✓ Reconnected to RPC server');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return true;
|
|
194
|
+
} catch (error) {
|
|
195
|
+
this.connected = false;
|
|
196
|
+
this.wallet = null;
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal UI for Neurai DePIN Terminal
|
|
3
|
+
* Full-screen blessed-based interface with top bar, message area, input, and status bar
|
|
4
|
+
* @module TerminalUI
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import blessed from 'blessed';
|
|
8
|
+
import {
|
|
9
|
+
COLORS,
|
|
10
|
+
ICONS,
|
|
11
|
+
BLESSED_KEYS,
|
|
12
|
+
ADDRESS,
|
|
13
|
+
PRIVACY,
|
|
14
|
+
TIME
|
|
15
|
+
} from '../constants.js';
|
|
16
|
+
import { formatTimestamp, resetTerminal } from '../utils.js';
|
|
17
|
+
import { TopBar } from './components/TopBar.js';
|
|
18
|
+
import { MessageBox } from './components/MessageBox.js';
|
|
19
|
+
import { InputBox } from './components/InputBox.js';
|
|
20
|
+
import { StatusBar } from './components/StatusBar.js';
|
|
21
|
+
import { ErrorOverlay } from './components/ErrorOverlay.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Terminal UI manager using blessed library
|
|
25
|
+
*/
|
|
26
|
+
export class TerminalUI {
|
|
27
|
+
/**
|
|
28
|
+
* Create a new TerminalUI instance
|
|
29
|
+
* @param {Object} config - Configuration object
|
|
30
|
+
* @param {string} config.rpc_url - RPC server URL
|
|
31
|
+
* @param {string} config.token - DePIN token name
|
|
32
|
+
* @param {WalletManager} walletManager - Wallet manager instance
|
|
33
|
+
* @param {RpcService} rpcService - RPC service instance
|
|
34
|
+
*/
|
|
35
|
+
constructor(config, walletManager, rpcService) {
|
|
36
|
+
this.config = config;
|
|
37
|
+
this.walletManager = walletManager;
|
|
38
|
+
this.rpcService = rpcService;
|
|
39
|
+
this.myAddress = walletManager.getAddress();
|
|
40
|
+
this.sendCallback = null;
|
|
41
|
+
this.displayedMessages = [];
|
|
42
|
+
this.hasPrivacy = false;
|
|
43
|
+
this.encryptionType = PRIVACY.DEFAULT_ENCRYPTION;
|
|
44
|
+
this.totalMessages = 0;
|
|
45
|
+
this.lastConnectionStatus = false;
|
|
46
|
+
this.lastPollTime = null;
|
|
47
|
+
|
|
48
|
+
this.initializeScreen();
|
|
49
|
+
this.createComponents();
|
|
50
|
+
this.setupKeybindings();
|
|
51
|
+
this.inputBox.focus();
|
|
52
|
+
this.screen.render();
|
|
53
|
+
this.updateTopBar({ connected: false, lastPoll: null });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Initialize blessed screen
|
|
58
|
+
*/
|
|
59
|
+
initializeScreen() {
|
|
60
|
+
this.screen = blessed.screen({
|
|
61
|
+
smartCSR: true,
|
|
62
|
+
title: 'Neurai DePIN Terminal'
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Create UI components (top bar, message box, input box, status bar)
|
|
68
|
+
*/
|
|
69
|
+
createComponents() {
|
|
70
|
+
this.topBar = new TopBar(this.screen, this.config, this.myAddress);
|
|
71
|
+
this.messageBox = new MessageBox(this.screen);
|
|
72
|
+
this.inputBox = new InputBox(this.screen, (msg) => this.handleSend(msg));
|
|
73
|
+
this.statusBar = new StatusBar(this.screen);
|
|
74
|
+
this.errorOverlay = new ErrorOverlay(this.screen);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Show blocking error overlay
|
|
79
|
+
* @param {string[]} errors - List of error messages
|
|
80
|
+
*/
|
|
81
|
+
showBlockingErrors(errors) {
|
|
82
|
+
this.inputBox.disable();
|
|
83
|
+
this.errorOverlay.show(errors);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Clear blocking error overlay
|
|
88
|
+
*/
|
|
89
|
+
clearBlockingErrors() {
|
|
90
|
+
if (this.errorOverlay.isVisible()) {
|
|
91
|
+
this.errorOverlay.hide();
|
|
92
|
+
this.inputBox.enable();
|
|
93
|
+
this.inputBox.focus();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Setup keyboard bindings
|
|
99
|
+
*/
|
|
100
|
+
setupKeybindings() {
|
|
101
|
+
// Exit on Ctrl+C or Escape
|
|
102
|
+
this.screen.key(BLESSED_KEYS.QUIT, () => {
|
|
103
|
+
this.cleanup();
|
|
104
|
+
process.exit(0);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Update top bar with connection status and info
|
|
110
|
+
* @param {Object} status - Status object
|
|
111
|
+
* @param {boolean} status.connected - Connection status
|
|
112
|
+
* @param {Date|null} status.lastPoll - Last poll time
|
|
113
|
+
*/
|
|
114
|
+
updateTopBar(status) {
|
|
115
|
+
this.lastConnectionStatus = status.connected;
|
|
116
|
+
|
|
117
|
+
if (status.lastPoll) {
|
|
118
|
+
this.lastPollTime = status.lastPoll;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
this.topBar.update({
|
|
122
|
+
connected: status.connected,
|
|
123
|
+
lastPoll: status.lastPoll
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Format message line for display
|
|
129
|
+
* @param {Object} msg - Message object
|
|
130
|
+
* @returns {string} Formatted message line with blessed tags
|
|
131
|
+
*/
|
|
132
|
+
formatMessageLine(msg) {
|
|
133
|
+
const time = formatTimestamp(msg.timestamp, this.config.timezone);
|
|
134
|
+
const isMe = msg.sender === this.myAddress;
|
|
135
|
+
const senderLabel = isMe ? 'YOU' : msg.sender.slice(0, ADDRESS.TRUNCATE_LENGTH);
|
|
136
|
+
const color = isMe ? COLORS.MY_MESSAGE : COLORS.OTHER_MESSAGE;
|
|
137
|
+
|
|
138
|
+
return `{${color}}[${time}] ${senderLabel}: ${msg.message}{/}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Add a new message to the display
|
|
143
|
+
* Maintains chronological order (oldest to newest)
|
|
144
|
+
* @param {Object} msg - Message object
|
|
145
|
+
* @param {string} msg.sender - Sender address
|
|
146
|
+
* @param {string} msg.message - Message content
|
|
147
|
+
* @param {number} msg.timestamp - Unix timestamp in seconds
|
|
148
|
+
* @param {string} msg.hash - Message hash
|
|
149
|
+
*/
|
|
150
|
+
addMessage(msg) {
|
|
151
|
+
this.displayedMessages.push(msg);
|
|
152
|
+
|
|
153
|
+
// Sort by timestamp (oldest to newest)
|
|
154
|
+
this.displayedMessages.sort((a, b) => a.timestamp - b.timestamp);
|
|
155
|
+
|
|
156
|
+
// Redraw all messages
|
|
157
|
+
this.redrawMessages();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Redraw all messages in the message box
|
|
162
|
+
*/
|
|
163
|
+
redrawMessages() {
|
|
164
|
+
const formattedMessages = this.displayedMessages.map(msg => this.formatMessageLine(msg));
|
|
165
|
+
// Clear and rebuild message box content
|
|
166
|
+
this.messageBox.component.setContent(formattedMessages.join('\n'));
|
|
167
|
+
this.messageBox.component.setScrollPerc(100);
|
|
168
|
+
this.screen.render();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Show error message in message box
|
|
173
|
+
* @param {string} errorMsg - Error message
|
|
174
|
+
*/
|
|
175
|
+
showError(errorMsg) {
|
|
176
|
+
const line = `{${COLORS.ERROR}}[ERROR] ${errorMsg}{/}`;
|
|
177
|
+
this.messageBox.addMessage(line);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Show info message in message box
|
|
182
|
+
* @param {string} infoMsg - Info message
|
|
183
|
+
*/
|
|
184
|
+
showInfo(infoMsg) {
|
|
185
|
+
const line = `{${COLORS.INFO}}[INFO] ${infoMsg}{/}`;
|
|
186
|
+
this.messageBox.addMessage(line);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Show success message in message box
|
|
191
|
+
* @param {string} successMsg - Success message
|
|
192
|
+
*/
|
|
193
|
+
showSuccess(successMsg) {
|
|
194
|
+
const line = `{${COLORS.SUCCESS}}[${ICONS.SUCCESS}] ${successMsg}{/}`;
|
|
195
|
+
this.messageBox.addMessage(line);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Update status bar with send status
|
|
200
|
+
* @param {string} message - Status message
|
|
201
|
+
* @param {string} [type='info'] - Message type: 'success', 'error', or 'info'
|
|
202
|
+
*/
|
|
203
|
+
updateSendStatus(message, type = 'info') {
|
|
204
|
+
this.statusBar.update(message, type);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Clear status bar
|
|
209
|
+
*/
|
|
210
|
+
clearSendStatus() {
|
|
211
|
+
this.statusBar.update('');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Handle send message action
|
|
216
|
+
* @param {string} message - Message to send
|
|
217
|
+
*/
|
|
218
|
+
handleSend(message) {
|
|
219
|
+
if (!message) return;
|
|
220
|
+
|
|
221
|
+
if (this.sendCallback) {
|
|
222
|
+
this.sendCallback(message);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Register callback for send action
|
|
228
|
+
* @param {Function} callback - Callback function(message)
|
|
229
|
+
*/
|
|
230
|
+
onSend(callback) {
|
|
231
|
+
this.sendCallback = callback;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Update pool information from depingetmsginfo RPC call
|
|
236
|
+
* @param {Object} poolInfo - Pool information
|
|
237
|
+
* @param {number} [poolInfo.messages] - Total messages in pool
|
|
238
|
+
* @param {string} [poolInfo.cipher] - Encryption cipher name
|
|
239
|
+
* @param {string} [poolInfo.depinpoolpkey] - Server privacy public key
|
|
240
|
+
*/
|
|
241
|
+
updatePoolInfo(poolInfo) {
|
|
242
|
+
if (poolInfo) {
|
|
243
|
+
this.totalMessages = poolInfo.messages || 0;
|
|
244
|
+
this.encryptionType = poolInfo.cipher || PRIVACY.DEFAULT_ENCRYPTION;
|
|
245
|
+
this.hasPrivacy = poolInfo.depinpoolpkey && poolInfo.depinpoolpkey !== PRIVACY.NO_KEY_VALUE;
|
|
246
|
+
|
|
247
|
+
this.topBar.setTotalMessages(this.totalMessages);
|
|
248
|
+
this.topBar.setEncryptionType(this.encryptionType);
|
|
249
|
+
|
|
250
|
+
// Refresh top bar
|
|
251
|
+
this.updateTopBar({
|
|
252
|
+
connected: this.lastConnectionStatus,
|
|
253
|
+
lastPoll: this.lastPollTime
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Cleanup terminal state
|
|
260
|
+
* Removes listeners, destroys screen, resets terminal
|
|
261
|
+
*/
|
|
262
|
+
cleanup() {
|
|
263
|
+
if (this.screen) {
|
|
264
|
+
try {
|
|
265
|
+
this.screen.destroy();
|
|
266
|
+
} catch (err) {
|
|
267
|
+
// Ignore cleanup errors
|
|
268
|
+
}
|
|
269
|
+
resetTerminal();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import blessed from 'blessed';
|
|
2
|
+
import { COLORS } from '../../constants.js';
|
|
3
|
+
|
|
4
|
+
export class ErrorOverlay {
|
|
5
|
+
constructor(screen) {
|
|
6
|
+
this.screen = screen;
|
|
7
|
+
this.timer = null;
|
|
8
|
+
this.timeLeft = 30;
|
|
9
|
+
this.currentErrors = [];
|
|
10
|
+
|
|
11
|
+
this.component = blessed.box({
|
|
12
|
+
top: 'center',
|
|
13
|
+
left: 'center',
|
|
14
|
+
width: '50%',
|
|
15
|
+
height: 'shrink',
|
|
16
|
+
content: '',
|
|
17
|
+
tags: true,
|
|
18
|
+
border: {
|
|
19
|
+
type: 'line'
|
|
20
|
+
},
|
|
21
|
+
style: {
|
|
22
|
+
fg: COLORS.FG_WHITE,
|
|
23
|
+
bg: COLORS.ERROR,
|
|
24
|
+
border: {
|
|
25
|
+
fg: COLORS.FG_WHITE
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
hidden: true
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
this.screen.append(this.component);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Show blocking error overlay
|
|
36
|
+
* @param {string[]} errors - List of error messages
|
|
37
|
+
*/
|
|
38
|
+
show(errors) {
|
|
39
|
+
if (!errors || errors.length === 0) {
|
|
40
|
+
this.hide();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.currentErrors = errors;
|
|
45
|
+
this.timeLeft = 30;
|
|
46
|
+
|
|
47
|
+
// Clear existing timer if any
|
|
48
|
+
if (this.timer) {
|
|
49
|
+
clearInterval(this.timer);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.updateContent();
|
|
53
|
+
this.component.show();
|
|
54
|
+
this.component.setFront();
|
|
55
|
+
this.screen.render();
|
|
56
|
+
|
|
57
|
+
// Start countdown
|
|
58
|
+
this.timer = setInterval(() => {
|
|
59
|
+
this.timeLeft--;
|
|
60
|
+
if (this.timeLeft < 0) this.timeLeft = 0;
|
|
61
|
+
this.updateContent();
|
|
62
|
+
}, 1000);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Update overlay content with current timer
|
|
67
|
+
*/
|
|
68
|
+
updateContent() {
|
|
69
|
+
const content = `\n{bold}CRITICAL ERRORS:{/bold}\n\n` +
|
|
70
|
+
this.currentErrors.map(e => `• ${e}`).join('\n\n') +
|
|
71
|
+
`\n\n{center}Retrying in ${this.timeLeft}s...{/center}\n`;
|
|
72
|
+
|
|
73
|
+
this.component.setContent(content);
|
|
74
|
+
this.screen.render();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Hide blocking error overlay
|
|
79
|
+
*/
|
|
80
|
+
hide() {
|
|
81
|
+
if (this.timer) {
|
|
82
|
+
clearInterval(this.timer);
|
|
83
|
+
this.timer = null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!this.component.hidden) {
|
|
87
|
+
this.component.hide();
|
|
88
|
+
this.screen.render();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Check if overlay is visible
|
|
94
|
+
* @returns {boolean} True if visible
|
|
95
|
+
*/
|
|
96
|
+
isVisible() {
|
|
97
|
+
return !this.component.hidden;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import blessed from 'blessed';
|
|
2
|
+
import { UI, COLORS, KEY_CODES } from '../../constants.js';
|
|
3
|
+
|
|
4
|
+
export class InputBox {
|
|
5
|
+
constructor(screen, onSend) {
|
|
6
|
+
this.screen = screen;
|
|
7
|
+
this.onSend = onSend;
|
|
8
|
+
|
|
9
|
+
this.component = blessed.textarea({
|
|
10
|
+
bottom: UI.STATUS_BAR_HEIGHT,
|
|
11
|
+
left: 0,
|
|
12
|
+
width: '100%',
|
|
13
|
+
height: UI.INPUT_BOX_HEIGHT,
|
|
14
|
+
inputOnFocus: true,
|
|
15
|
+
keys: true,
|
|
16
|
+
style: {
|
|
17
|
+
fg: COLORS.FG_WHITE,
|
|
18
|
+
bg: COLORS.BG_BLACK,
|
|
19
|
+
border: {
|
|
20
|
+
fg: COLORS.BORDER
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
border: {
|
|
24
|
+
type: 'line'
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
this.setupEvents();
|
|
29
|
+
this.screen.append(this.component);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
setupEvents() {
|
|
33
|
+
this.component.key('enter', () => {
|
|
34
|
+
const message = this.component.getValue().trim();
|
|
35
|
+
if (message) {
|
|
36
|
+
this.onSend(message);
|
|
37
|
+
this.component.clearValue();
|
|
38
|
+
this.screen.render();
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
focus() {
|
|
44
|
+
if (!this.disabled) {
|
|
45
|
+
this.component.focus();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
disable() {
|
|
50
|
+
this.disabled = true;
|
|
51
|
+
this.component.inputOnFocus = false;
|
|
52
|
+
// Optional: Change style to indicate disabled state
|
|
53
|
+
this.component.style.border.fg = 'gray';
|
|
54
|
+
this.screen.render();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
enable() {
|
|
58
|
+
this.disabled = false;
|
|
59
|
+
this.component.inputOnFocus = true;
|
|
60
|
+
this.component.style.border.fg = COLORS.BORDER;
|
|
61
|
+
this.screen.render();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import blessed from 'blessed';
|
|
2
|
+
import { UI, COLORS } from '../../constants.js';
|
|
3
|
+
|
|
4
|
+
export class MessageBox {
|
|
5
|
+
constructor(screen) {
|
|
6
|
+
this.screen = screen;
|
|
7
|
+
|
|
8
|
+
this.component = blessed.box({
|
|
9
|
+
top: UI.TOP_BAR_HEIGHT,
|
|
10
|
+
left: 0,
|
|
11
|
+
width: '100%',
|
|
12
|
+
height: `100%-${UI.MESSAGE_BOX_OFFSET}`,
|
|
13
|
+
scrollable: true,
|
|
14
|
+
alwaysScroll: true,
|
|
15
|
+
keys: true,
|
|
16
|
+
vi: true,
|
|
17
|
+
mouse: true,
|
|
18
|
+
tags: true,
|
|
19
|
+
scrollbar: {
|
|
20
|
+
ch: UI.SCROLLBAR_CHAR,
|
|
21
|
+
style: {
|
|
22
|
+
bg: COLORS.BG_BLUE
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
style: {
|
|
26
|
+
fg: COLORS.FG_WHITE,
|
|
27
|
+
bg: COLORS.BG_BLACK
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
this.screen.append(this.component);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
addMessage(formattedLine) {
|
|
35
|
+
const current = this.component.getContent();
|
|
36
|
+
const next = current && current.length > 0 ? `${current}\n${formattedLine}` : formattedLine;
|
|
37
|
+
this.component.setContent(next);
|
|
38
|
+
this.component.setScrollPerc(100);
|
|
39
|
+
this.screen.render();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
scrollUp() {
|
|
43
|
+
this.component.scroll(-1);
|
|
44
|
+
this.screen.render();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
scrollDown() {
|
|
48
|
+
this.component.scroll(1);
|
|
49
|
+
this.screen.render();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import blessed from 'blessed';
|
|
2
|
+
import { UI, COLORS } from '../../constants.js';
|
|
3
|
+
|
|
4
|
+
export class StatusBar {
|
|
5
|
+
constructor(screen) {
|
|
6
|
+
this.screen = screen;
|
|
7
|
+
|
|
8
|
+
this.component = blessed.box({
|
|
9
|
+
bottom: 0,
|
|
10
|
+
left: 0,
|
|
11
|
+
width: '100%',
|
|
12
|
+
height: UI.STATUS_BAR_HEIGHT,
|
|
13
|
+
content: ' Ready',
|
|
14
|
+
tags: true,
|
|
15
|
+
style: {
|
|
16
|
+
fg: COLORS.FG_WHITE,
|
|
17
|
+
bg: COLORS.BG_BLUE
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
this.screen.append(this.component);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
update(message, type = 'info') {
|
|
25
|
+
let color = COLORS.INFO;
|
|
26
|
+
if (type === 'error') color = COLORS.ERROR;
|
|
27
|
+
if (type === 'success') color = COLORS.SUCCESS;
|
|
28
|
+
|
|
29
|
+
this.component.setContent(` {${color}}${message}{/${color}}`);
|
|
30
|
+
this.screen.render();
|
|
31
|
+
}
|
|
32
|
+
}
|