@cs7player/chat 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/.vscode/settings.json +6 -0
- package/Read.md +1 -0
- package/app.js +2 -0
- package/package.json +34 -0
- package/src/assets/gumball.png +0 -0
- package/src/assets/pic001.png +0 -0
- package/src/assets/prime.png +0 -0
- package/src/network/discovery.js +94 -0
- package/src/network/peers.js +8 -0
- package/src/network/websocket.js +108 -0
- package/src/start.js +14 -0
- package/src/state/chatState.js +51 -0
- package/src/tui/alert.js +107 -0
- package/src/tui/content.js +61 -0
- package/src/tui/header.js +60 -0
- package/src/tui/layout.js +12 -0
- package/src/tui/msg-bar.js +112 -0
- package/src/tui/side-bar.js +127 -0
- package/src/tui/welcome.js +104 -0
- package/src/utils/contants.js +17 -0
- package/src/utils/dialog.js +105 -0
- package/src/utils/screen.js +79 -0
package/Read.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
if u find any issue send mail to s.chandrasekhar.h@gmail.com
|
package/app.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cs7player/chat",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "to communicate in terminal",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"chat"
|
|
7
|
+
],
|
|
8
|
+
"bin": {
|
|
9
|
+
"lan-chat": "./app.js"
|
|
10
|
+
},
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"author": {
|
|
13
|
+
"name": "s.h.chandra sekhar",
|
|
14
|
+
"email": "s.chandrasekhar.h@gmail.com"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "app.js",
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/CS7player/lan-chat#readme",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/CS7player/lan-chat.git"
|
|
25
|
+
},
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/CS7player/lan-chat/issues"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"blessed": "^0.1.81",
|
|
31
|
+
"chalk": "^5.6.2",
|
|
32
|
+
"ws": "^8.20.1"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import dgram from "dgram";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
|
|
4
|
+
const PORT = 41234;
|
|
5
|
+
const socket = dgram.createSocket("udp4");
|
|
6
|
+
|
|
7
|
+
const ipToInt = (ip) => {
|
|
8
|
+
return ip.split(".").reduce((acc, p) => (acc << 8) + Number(p), 0) >>> 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const intToIp = (int) => {
|
|
12
|
+
return [
|
|
13
|
+
(int >>> 24) & 255,
|
|
14
|
+
(int >>> 16) & 255,
|
|
15
|
+
(int >>> 8) & 255,
|
|
16
|
+
int & 255,
|
|
17
|
+
].join(".");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const getBroadcastIp = (netObj) => {
|
|
21
|
+
if (!netObj?.address || !netObj?.netmask) return null;
|
|
22
|
+
const ipInt = ipToInt(netObj.address);
|
|
23
|
+
const maskInt = ipToInt(netObj.netmask);
|
|
24
|
+
const network = ipInt & maskInt;
|
|
25
|
+
const broadcast = network | (~maskInt >>> 0);
|
|
26
|
+
return intToIp(broadcast);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const getRealLANIP = () => {
|
|
30
|
+
const nets = os.networkInterfaces();
|
|
31
|
+
let fallback = null;
|
|
32
|
+
for (const name of Object.keys(nets)) {
|
|
33
|
+
for (const net of nets[name]) {
|
|
34
|
+
if (net.family !== "IPv4" || net.internal) continue;
|
|
35
|
+
const ip = net.address;
|
|
36
|
+
// skip virtual adapters
|
|
37
|
+
if (ip.startsWith("192.168.56.")) continue;
|
|
38
|
+
if (ip.startsWith("169.254.")) continue;
|
|
39
|
+
const isPreferred =
|
|
40
|
+
ip.startsWith("192.168.") ||
|
|
41
|
+
ip.startsWith("10.");
|
|
42
|
+
if (isPreferred) return net;
|
|
43
|
+
fallback = net;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return fallback || null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const startDiscovery = (username, onUserFound) => {
|
|
50
|
+
socket.bind(PORT, () => {
|
|
51
|
+
socket.setBroadcast(true);
|
|
52
|
+
const interval = setInterval(() => {
|
|
53
|
+
const net = getRealLANIP();
|
|
54
|
+
if (!net) {
|
|
55
|
+
console.log("No LAN interface found");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const broadcastIP = getBroadcastIp(net);
|
|
59
|
+
if (!broadcastIP) {
|
|
60
|
+
console.log("Could not compute broadcast IP");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const message = JSON.stringify({
|
|
64
|
+
type: "DISCOVER",
|
|
65
|
+
username,
|
|
66
|
+
ip: net.address,
|
|
67
|
+
port: 8080,
|
|
68
|
+
});
|
|
69
|
+
socket.send(
|
|
70
|
+
message,
|
|
71
|
+
0,
|
|
72
|
+
message.length,
|
|
73
|
+
PORT,
|
|
74
|
+
broadcastIP
|
|
75
|
+
);
|
|
76
|
+
}, 3000);
|
|
77
|
+
socket.interval = interval;
|
|
78
|
+
});
|
|
79
|
+
socket.on("message", (msg, rinfo) => {
|
|
80
|
+
try {
|
|
81
|
+
const data = JSON.parse(msg.toString());
|
|
82
|
+
if (data.type === "DISCOVER") {
|
|
83
|
+
onUserFound({
|
|
84
|
+
ip: data.ip || rinfo.address,
|
|
85
|
+
username: data.username,
|
|
86
|
+
port: data.port,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
} catch (err) {
|
|
90
|
+
// ignore invalid packets
|
|
91
|
+
console.log(err);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const peers = new Map();
|
|
2
|
+
|
|
3
|
+
export function addPeer(ip, data) { peers.set(ip, data); }
|
|
4
|
+
export function removePeer(ip) { peers.delete(ip); }
|
|
5
|
+
export function hasPeer(ip) { return peers.has(ip); }
|
|
6
|
+
export function getPeer(ip) { return peers.get(ip); }
|
|
7
|
+
export function getPeers() { return peers; }
|
|
8
|
+
export function getUsers() { return [...peers.values()]; }
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import WebSocket, { WebSocketServer } from "ws";
|
|
2
|
+
import { addMessage, chatState, removeUser, addUser } from "../state/chatState.js";
|
|
3
|
+
import { addPeer, removePeer, hasPeer, getPeers } from "./peers.js";
|
|
4
|
+
import { createAlertBox } from "../tui/alert.js";
|
|
5
|
+
import { clearFocus, screenExit } from "../utils/screen.js";
|
|
6
|
+
import { startUI } from "../tui/welcome.js";
|
|
7
|
+
|
|
8
|
+
export const startWSServer = (username) => {
|
|
9
|
+
try {
|
|
10
|
+
const wss = new WebSocketServer({ port: 8080 });
|
|
11
|
+
wss.on("listening", () => {
|
|
12
|
+
console.log("WS LISTENING on 8080");
|
|
13
|
+
startUI();
|
|
14
|
+
});
|
|
15
|
+
wss.on("error", (err) => {
|
|
16
|
+
console.error("WS failed:", err);
|
|
17
|
+
});
|
|
18
|
+
wss.on("connection", (ws, req) => {
|
|
19
|
+
const ip = req.socket.remoteAddress;
|
|
20
|
+
ws.on("message", (raw) => {
|
|
21
|
+
try {
|
|
22
|
+
const data = JSON.parse(raw.toString());
|
|
23
|
+
if (data.type === "INTRO") {
|
|
24
|
+
addPeer(ip, {
|
|
25
|
+
ws,
|
|
26
|
+
username: data.username,
|
|
27
|
+
});
|
|
28
|
+
addUser({ username: data.username, ip });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (data.type === "CHAT") {
|
|
32
|
+
for (const [, peer] of getPeers()) {
|
|
33
|
+
peer.ws.send(JSON.stringify(data));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (data.type === "PRIVATE_CHAT") {
|
|
37
|
+
|
|
38
|
+
}
|
|
39
|
+
} catch (e) {
|
|
40
|
+
console.error("❌ MESSAGE ERROR:", e);
|
|
41
|
+
screenExit();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
ws.on("close", () => {
|
|
46
|
+
const peer = getPeers().get(ip);
|
|
47
|
+
removeUser(peer?.username)
|
|
48
|
+
removePeer(ip);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.error("❌ START ERROR:", err);
|
|
54
|
+
screenExit();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const connectToPeer = (ip, username) => {
|
|
59
|
+
if (hasPeer(ip)) return;
|
|
60
|
+
const ws = new WebSocket(`ws://${ip}:8080`);
|
|
61
|
+
ws.on("open", () => {
|
|
62
|
+
addPeer(ip, { ws, username: "unknown" });
|
|
63
|
+
ws.send(JSON.stringify({
|
|
64
|
+
type: "INTRO",
|
|
65
|
+
username
|
|
66
|
+
}));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
ws.on("message", (raw) => {
|
|
70
|
+
try {
|
|
71
|
+
const data = JSON.parse(raw.toString());
|
|
72
|
+
const userObj = { username: data["from"] }
|
|
73
|
+
addMessage(userObj, data)
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.log(err);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
ws.on("close", () => { removePeer(ip); });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const sendMessage = (username, message) => {
|
|
82
|
+
const payload = JSON.stringify({
|
|
83
|
+
type: "CHAT",
|
|
84
|
+
from: username,
|
|
85
|
+
message,
|
|
86
|
+
timestamp: Date.now(),
|
|
87
|
+
});
|
|
88
|
+
for (const [, peer] of getPeers()) {
|
|
89
|
+
peer.ws.send(payload);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// send to specific peer
|
|
94
|
+
export const sendPrivateMessage = (toIp, username, message) => {
|
|
95
|
+
const peer = getPeers().get(toIp);
|
|
96
|
+
if (!peer) {
|
|
97
|
+
clearFocus();
|
|
98
|
+
createAlertBox("Selected User in Left.")
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const payload = JSON.stringify({
|
|
102
|
+
type: "PRIVATE_CHAT",
|
|
103
|
+
from: chatState.whoami,
|
|
104
|
+
message,
|
|
105
|
+
timestamp: Date.now(),
|
|
106
|
+
});
|
|
107
|
+
peer.ws.send(payload);
|
|
108
|
+
}
|
package/src/start.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as os from "os";
|
|
2
|
+
import { startWSServer, connectToPeer } from "./network/websocket.js";
|
|
3
|
+
import { startDiscovery, getRealLANIP } from "./network/discovery.js";
|
|
4
|
+
|
|
5
|
+
const username = os.userInfo().username;
|
|
6
|
+
|
|
7
|
+
startWSServer(username);
|
|
8
|
+
|
|
9
|
+
startDiscovery(username, (user) => {
|
|
10
|
+
if (user.username === username) return;
|
|
11
|
+
connectToPeer(user.ip, username);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as os from "node:os";
|
|
2
|
+
import { loadUsers } from "../tui/side-bar.js";
|
|
3
|
+
import { renderMessage, renderContent } from "../tui/content.js";
|
|
4
|
+
import { sendPrivateMessage } from "../network/websocket.js";
|
|
5
|
+
|
|
6
|
+
export const chatState = {
|
|
7
|
+
whoami: os.userInfo().username,
|
|
8
|
+
users: [],
|
|
9
|
+
messages: {},
|
|
10
|
+
selectedUser: null,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function addUser(user) {
|
|
14
|
+
chatState.users.push(user);
|
|
15
|
+
loadUsers()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function removeUser(username) {
|
|
19
|
+
chatState.users = chatState.users.filter(user => user.username !== username);
|
|
20
|
+
renderContent("no user Selected!!");
|
|
21
|
+
loadUsers()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function setSelectedUser(user) {
|
|
25
|
+
chatState.selectedUser = user;
|
|
26
|
+
if (!chatState.messages[user.username]) {
|
|
27
|
+
chatState.messages[user.username] = [];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function addMessage(user, msg) {
|
|
32
|
+
const key = user.username || "global";
|
|
33
|
+
if (!chatState.messages[key]) {
|
|
34
|
+
chatState.messages[key] = [];
|
|
35
|
+
}
|
|
36
|
+
chatState.messages[key].push(msg);
|
|
37
|
+
if (chatState.selectedUser.username == msg.from) {
|
|
38
|
+
renderMessage(msg);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function sendMessage(user, msg) {
|
|
43
|
+
sendPrivateMessage(user.ip, user.username, msg)
|
|
44
|
+
const msgObj = {
|
|
45
|
+
from: "You",
|
|
46
|
+
to: user.username,
|
|
47
|
+
message: msg,
|
|
48
|
+
time: Date.now(),
|
|
49
|
+
};
|
|
50
|
+
addMessage(user, msgObj);
|
|
51
|
+
}
|
package/src/tui/alert.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import blessed from "blessed";
|
|
2
|
+
import { screen, screenRefresh, addFocusBtn, removeFocusBtn } from "../utils/screen.js";
|
|
3
|
+
import { color, tabsfocus } from "../utils/contants.js";
|
|
4
|
+
/**
|
|
5
|
+
* Creates a simple alert modal with OK button
|
|
6
|
+
* @param {string} message
|
|
7
|
+
* @param {Function} onClose
|
|
8
|
+
*/
|
|
9
|
+
export const createAlertBox = (
|
|
10
|
+
message = 'Something happened!',
|
|
11
|
+
onClose = null
|
|
12
|
+
) => {
|
|
13
|
+
const overlay = blessed.box({
|
|
14
|
+
parent: screen,
|
|
15
|
+
top: 0,
|
|
16
|
+
left: 0,
|
|
17
|
+
width: '100%',
|
|
18
|
+
height: '100%',
|
|
19
|
+
style: {
|
|
20
|
+
bg: color.black,
|
|
21
|
+
transparent: false
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// alert container
|
|
26
|
+
const alertBox = blessed.box({
|
|
27
|
+
parent: overlay,
|
|
28
|
+
top: 'center',
|
|
29
|
+
left: 'center',
|
|
30
|
+
width: '50%',
|
|
31
|
+
height: 9,
|
|
32
|
+
border: {
|
|
33
|
+
type: 'line'
|
|
34
|
+
},
|
|
35
|
+
tags: true,
|
|
36
|
+
label: ' Alert ',
|
|
37
|
+
style: {
|
|
38
|
+
fg: color.white,
|
|
39
|
+
bg: color.black,
|
|
40
|
+
border: {
|
|
41
|
+
fg: color.purple
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// message text
|
|
47
|
+
blessed.text({
|
|
48
|
+
parent: alertBox,
|
|
49
|
+
top: 2,
|
|
50
|
+
left: 2,
|
|
51
|
+
width: '90%',
|
|
52
|
+
align: 'center',
|
|
53
|
+
content: message,
|
|
54
|
+
style: {
|
|
55
|
+
fg: color.white
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// OK button
|
|
60
|
+
const okBtn = blessed.button({
|
|
61
|
+
parent: alertBox,
|
|
62
|
+
bottom: 1,
|
|
63
|
+
left: 'center',
|
|
64
|
+
width: 10,
|
|
65
|
+
height: 1,
|
|
66
|
+
mouse: true,
|
|
67
|
+
keys: true,
|
|
68
|
+
shrink: true,
|
|
69
|
+
name: 'ok-button',
|
|
70
|
+
content: ' OK ',
|
|
71
|
+
style: {
|
|
72
|
+
fg: color.black,
|
|
73
|
+
bg: color.red,
|
|
74
|
+
focus: {
|
|
75
|
+
fg: color.white,
|
|
76
|
+
bg: color.blue
|
|
77
|
+
},
|
|
78
|
+
hover: {
|
|
79
|
+
fg: color.white,
|
|
80
|
+
bg: color.blue
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// add to focus system
|
|
86
|
+
okBtn.focus();
|
|
87
|
+
addFocusBtn({ id: 99, btn: okBtn });
|
|
88
|
+
tabsfocus.btnIndex = tabsfocus.btns.findIndex(b => b.id === 99);
|
|
89
|
+
tabsfocus.istoggle = false;
|
|
90
|
+
|
|
91
|
+
const closeAlert = () => {
|
|
92
|
+
tabsfocus.istoggle = true;
|
|
93
|
+
overlay.destroy();
|
|
94
|
+
if (onClose && typeof onClose === 'function') { onClose(); }
|
|
95
|
+
removeFocusBtn(99);
|
|
96
|
+
screenRefresh();
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
okBtn.on('press', closeAlert);
|
|
100
|
+
// ESC closes alert too
|
|
101
|
+
alertBox.key(['escape'], closeAlert);
|
|
102
|
+
screenRefresh();
|
|
103
|
+
return {
|
|
104
|
+
alertBox,
|
|
105
|
+
closeAlert
|
|
106
|
+
};
|
|
107
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import blessed from "blessed";
|
|
2
|
+
import { screen } from "../utils/screen.js";
|
|
3
|
+
import { color } from '../utils/contants.js';
|
|
4
|
+
import { chatState, addMessage } from "../state/chatState.js";
|
|
5
|
+
import { renderMsgBar } from "./msg-bar.js";
|
|
6
|
+
|
|
7
|
+
const contentBox = blessed.box({
|
|
8
|
+
top: 1,
|
|
9
|
+
left: "20%",
|
|
10
|
+
width: "80%",
|
|
11
|
+
height: "100%-6",
|
|
12
|
+
label: " Chat ",
|
|
13
|
+
tags: true,
|
|
14
|
+
border: { type: "line" },
|
|
15
|
+
scrollable: true,
|
|
16
|
+
alwaysScroll: true,
|
|
17
|
+
scrollbar: {
|
|
18
|
+
ch: "│",
|
|
19
|
+
style: { fg: color.yellow },
|
|
20
|
+
},
|
|
21
|
+
style: {
|
|
22
|
+
fg: color.white,
|
|
23
|
+
bg: color.black,
|
|
24
|
+
border: { fg: color.yellow },
|
|
25
|
+
},
|
|
26
|
+
content: " NO one is Selected",
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/* -RENDER CHAT CONTAINER-*/
|
|
30
|
+
export const renderContent = (text = "") => {
|
|
31
|
+
contentBox.setLabel(text);
|
|
32
|
+
screen.append(contentBox);
|
|
33
|
+
renderMsgBar();
|
|
34
|
+
screen.render();
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/* -CLEAR CHAT-*/
|
|
38
|
+
export const clearChat = () => {
|
|
39
|
+
contentBox.setContent("");
|
|
40
|
+
screen.render();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/* -MESSAGE RENDERER (LEFT / RIGHT)-*/
|
|
44
|
+
export const renderMessage = (msg) => {
|
|
45
|
+
const isMe = msg.from === "You";
|
|
46
|
+
const formatted = isMe
|
|
47
|
+
? `{right}${msg.from}: ${msg.message}{/right}`
|
|
48
|
+
: `{left}${msg.from}: ${msg.message}{/left}`;
|
|
49
|
+
const current = contentBox.getContent();
|
|
50
|
+
contentBox.setContent(current + "\n" + formatted);
|
|
51
|
+
contentBox.setScrollPerc(100);
|
|
52
|
+
screen.render();
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/* -LOAD CHAT HISTORY (OPTIONAL)-*/
|
|
56
|
+
export const loadChatHistory = (user) => {
|
|
57
|
+
const msgs = chatState.messages?.[user.username] || [];
|
|
58
|
+
contentBox.setContent("");
|
|
59
|
+
msgs.forEach(renderMessage);
|
|
60
|
+
screen.render();
|
|
61
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import blessed from 'blessed';
|
|
2
|
+
import { screen, addFocusBtn } from '../utils/screen.js';
|
|
3
|
+
import { showDialogBox } from '../utils/dialog.js';
|
|
4
|
+
import { color } from '../utils/contants.js';
|
|
5
|
+
import { clearFocus } from "../utils/screen.js";
|
|
6
|
+
|
|
7
|
+
// HEADER
|
|
8
|
+
const header = blessed.box({
|
|
9
|
+
top: 0,
|
|
10
|
+
left: 0,
|
|
11
|
+
width: '100%',
|
|
12
|
+
height: 1,
|
|
13
|
+
style: {
|
|
14
|
+
bg: color.green,
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const title = blessed.text({
|
|
19
|
+
parent: header,
|
|
20
|
+
top: 0,
|
|
21
|
+
left: 'center',
|
|
22
|
+
width: 'shrink',
|
|
23
|
+
height: 1,
|
|
24
|
+
align: 'center',
|
|
25
|
+
content: 'cHat TUI',
|
|
26
|
+
style: {
|
|
27
|
+
fg: color.yellow,
|
|
28
|
+
bg: color.green,
|
|
29
|
+
bold: true,
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Exit button
|
|
34
|
+
const exitBtn = blessed.button({
|
|
35
|
+
parent: header,
|
|
36
|
+
content: ' X ',
|
|
37
|
+
top: 0,
|
|
38
|
+
right: 2,
|
|
39
|
+
height: 1,
|
|
40
|
+
shrink: true,
|
|
41
|
+
mouse: true,
|
|
42
|
+
keys: true,
|
|
43
|
+
style: {
|
|
44
|
+
fg: color.white, bg: color.red,
|
|
45
|
+
focus: { fg: color.white, bg: color.purple },
|
|
46
|
+
hover: { fg: color.white, bg: color.purple },
|
|
47
|
+
active: { fg: color.white, bg: color.red }
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
exitBtn.on('press', () => {
|
|
52
|
+
clearFocus();
|
|
53
|
+
showDialogBox();
|
|
54
|
+
screen.render();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export const renderHeader = () => {
|
|
58
|
+
addFocusBtn({ id: 4, btn: exitBtn })
|
|
59
|
+
screen.append(header);
|
|
60
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { renderHeader } from './header.js';
|
|
2
|
+
import { renderSideBar } from './side-bar.js';
|
|
3
|
+
import { renderContent } from './content.js';
|
|
4
|
+
import { screenRefresh } from '../utils/screen.js';
|
|
5
|
+
// Append all
|
|
6
|
+
export const displayDashboard = () => {
|
|
7
|
+
renderHeader();
|
|
8
|
+
renderSideBar();
|
|
9
|
+
renderContent(" Chat ");
|
|
10
|
+
screenRefresh();
|
|
11
|
+
}
|
|
12
|
+
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import blessed from "blessed";
|
|
2
|
+
import { addFocusBtn, screen, focusButton, screenRefresh, clearFocus } from "../utils/screen.js";
|
|
3
|
+
import { color, tabsfocus } from '../utils/contants.js';
|
|
4
|
+
import { chatState, sendMessage } from "../state/chatState.js";
|
|
5
|
+
import { renderMessage } from "./content.js";
|
|
6
|
+
import { createAlertBox } from "./alert.js";
|
|
7
|
+
|
|
8
|
+
/* -INPUT BAR CONTAINER-*/
|
|
9
|
+
const inputBar = blessed.box({
|
|
10
|
+
bottom: 0,
|
|
11
|
+
left: "20%",
|
|
12
|
+
width: "80%",
|
|
13
|
+
height: 5,
|
|
14
|
+
label: "{yellow-fg} Use TAB to Navigate {/yellow-fg}",
|
|
15
|
+
tags: true,
|
|
16
|
+
border: {
|
|
17
|
+
type: "line"
|
|
18
|
+
},
|
|
19
|
+
style: {
|
|
20
|
+
fg: color.white,
|
|
21
|
+
bg: color.black,
|
|
22
|
+
border: {
|
|
23
|
+
fg: color.green
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/* -INPUT BOX-*/
|
|
29
|
+
export const inputBox = blessed.textbox({
|
|
30
|
+
parent: inputBar,
|
|
31
|
+
left: 0,
|
|
32
|
+
top: 0,
|
|
33
|
+
height: 3,
|
|
34
|
+
width: "85%",
|
|
35
|
+
keys: true,
|
|
36
|
+
mouse: true,
|
|
37
|
+
border: { type: "line" },
|
|
38
|
+
style: {
|
|
39
|
+
fg: color.white,
|
|
40
|
+
bg: color.black,
|
|
41
|
+
border: { fg: color.green },
|
|
42
|
+
focus: { border: { fg: color.purple } },
|
|
43
|
+
hover: { border: { fg: color.purple } }
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/* -SEND BUTTON-*/
|
|
48
|
+
const sendButton = blessed.button({
|
|
49
|
+
parent: inputBar,
|
|
50
|
+
right: 0,
|
|
51
|
+
top: 0,
|
|
52
|
+
height: 3,
|
|
53
|
+
width: 10,
|
|
54
|
+
content: " Send ",
|
|
55
|
+
mouse: true,
|
|
56
|
+
keys: true,
|
|
57
|
+
align: "center",
|
|
58
|
+
shrink: true,
|
|
59
|
+
border: { type: "line", },
|
|
60
|
+
style: {
|
|
61
|
+
fg: color.black,
|
|
62
|
+
bg: color.green,
|
|
63
|
+
border: { fg: color.white, },
|
|
64
|
+
focus: { border: { fg: color.purple }, bg: color.yellow, fg: color.green },
|
|
65
|
+
hover: { border: { fg: color.purple }, bg: color.yellow, fg: color.green }
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/* -SEND MESSAGE HANDLER-*/
|
|
70
|
+
const handleSend = () => {
|
|
71
|
+
try {
|
|
72
|
+
// inputBox.cancel();
|
|
73
|
+
const text = inputBox.getValue().trim();
|
|
74
|
+
if (!text) {
|
|
75
|
+
clearFocus();
|
|
76
|
+
createAlertBox("NO Text is Entered!");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const user = chatState.selectedUser;
|
|
80
|
+
if (!user) {
|
|
81
|
+
clearFocus();
|
|
82
|
+
createAlertBox("NO User is Selected!");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
sendMessage(user, text);
|
|
86
|
+
inputBox.clearValue();
|
|
87
|
+
} catch (err) {
|
|
88
|
+
console.log(err)
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/* -EVENTS-*/
|
|
93
|
+
sendButton.on("press", handleSend);
|
|
94
|
+
|
|
95
|
+
inputBox.on('click', () => {
|
|
96
|
+
inputBox.focus();
|
|
97
|
+
screen.render();
|
|
98
|
+
});
|
|
99
|
+
inputBox.on('submit', handleSend);
|
|
100
|
+
inputBox.key('tab', () => {
|
|
101
|
+
clearFocus();
|
|
102
|
+
return false;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
/* -RENDER-*/
|
|
106
|
+
export const renderMsgBar = () => {
|
|
107
|
+
if (!screen.children.includes(inputBar)) {
|
|
108
|
+
addFocusBtn({ id: 8, btn: inputBox });
|
|
109
|
+
addFocusBtn({ id: 9, btn: sendButton });
|
|
110
|
+
screen.append(inputBar);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import blessed from 'blessed';
|
|
2
|
+
import { addFocusBtn, screen, screenRefresh } from '../utils/screen.js';
|
|
3
|
+
import { color } from '../utils/contants.js';
|
|
4
|
+
import { renderContent, clearChat, loadChatHistory } from './content.js';
|
|
5
|
+
import { chatState } from '../state/chatState.js';
|
|
6
|
+
import { setSelectedUser } from '../state/chatState.js';
|
|
7
|
+
|
|
8
|
+
// SIDEBAR
|
|
9
|
+
const sidebar = blessed.box({
|
|
10
|
+
top: 1,
|
|
11
|
+
left: 0,
|
|
12
|
+
width: '20%',
|
|
13
|
+
height: '100%-1',
|
|
14
|
+
label: ' Users (↑/↓ nav) ',
|
|
15
|
+
border: { type: 'line' },
|
|
16
|
+
style: {
|
|
17
|
+
fg: color.white,
|
|
18
|
+
bg: color.black,
|
|
19
|
+
border: { fg: color.green },
|
|
20
|
+
focus: { border: { fg: color.purple } },
|
|
21
|
+
hover: { border: { fg: color.purple } }
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const spinnerFrames = ['|', '/', '-', '\\'];
|
|
26
|
+
let i = 0;
|
|
27
|
+
const loader = blessed.text({
|
|
28
|
+
parent: sidebar,
|
|
29
|
+
top: 1,
|
|
30
|
+
left: 'center',
|
|
31
|
+
content: 'Loading users |',
|
|
32
|
+
style: { fg: color.yellow }
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const loaderInterval = setInterval(() => {
|
|
36
|
+
i = (i + 1) % spinnerFrames.length;
|
|
37
|
+
loader.setContent(`Loading users ${spinnerFrames[i]}`);
|
|
38
|
+
sidebar.screen.render();
|
|
39
|
+
}, 120);
|
|
40
|
+
|
|
41
|
+
function stopLoader() {
|
|
42
|
+
clearInterval(loaderInterval);
|
|
43
|
+
loader.hide();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const userRows = [];
|
|
47
|
+
let selectedIndex = 0;
|
|
48
|
+
|
|
49
|
+
const renderUsers = (users, callBack) => {
|
|
50
|
+
loader.hide();
|
|
51
|
+
userRows.forEach(row => row.destroy());
|
|
52
|
+
userRows.length = 0;
|
|
53
|
+
users.forEach((user, index) => {
|
|
54
|
+
const row = blessed.box({
|
|
55
|
+
parent: sidebar,
|
|
56
|
+
top: index * 3 + 1,
|
|
57
|
+
width: '94%',
|
|
58
|
+
height: 3,
|
|
59
|
+
keys: true,
|
|
60
|
+
mouse: true,
|
|
61
|
+
clickable: true,
|
|
62
|
+
focusable: true,
|
|
63
|
+
content: user.username,
|
|
64
|
+
border: { type: 'line' },
|
|
65
|
+
style: {
|
|
66
|
+
fg: color.white,
|
|
67
|
+
bg: color.black,
|
|
68
|
+
border: { fg: color.green },
|
|
69
|
+
focus: { border: { fg: color.purple }, bg: color.green, fg: color.black },
|
|
70
|
+
hover: { border: { fg: color.purple }, bg: color.green, fg: color.black }
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
row.on('click', () => callBack(user, row));
|
|
74
|
+
row.key('enter', () => callBack(user, row));
|
|
75
|
+
userRows.push(row);
|
|
76
|
+
});
|
|
77
|
+
sidebar.screen.render();
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const handleUserClick = (user, box) => {
|
|
81
|
+
box.focus();
|
|
82
|
+
setSelectedUser(user);
|
|
83
|
+
clearChat();
|
|
84
|
+
renderContent(`Chat with ${user.username}`);
|
|
85
|
+
loadChatHistory(user);
|
|
86
|
+
screenRefresh();
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const loadUsers = () => {
|
|
90
|
+
loader.show();
|
|
91
|
+
stopLoader()
|
|
92
|
+
renderUsers(chatState.users, handleUserClick);
|
|
93
|
+
screen.render();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const renderSideBar = () => {
|
|
97
|
+
screen.append(sidebar);
|
|
98
|
+
addFocusBtn({ id: 7, btn: sidebar })
|
|
99
|
+
loadUsers();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const isSidebarFocused = () => {
|
|
103
|
+
let node = screen.focused;
|
|
104
|
+
while (node) {
|
|
105
|
+
if (node === sidebar) {
|
|
106
|
+
if (chatState.users.length == 0)
|
|
107
|
+
return false;
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
node = node.parent;
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
screen.key(['up'], () => {
|
|
116
|
+
if (!isSidebarFocused()) return;
|
|
117
|
+
selectedIndex = (selectedIndex - 1 + userRows.length) % userRows.length;
|
|
118
|
+
userRows[selectedIndex].focus();
|
|
119
|
+
screen.render();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
screen.key(['down'], () => {
|
|
123
|
+
if (!isSidebarFocused()) return;
|
|
124
|
+
selectedIndex = (selectedIndex + 1) % userRows.length;
|
|
125
|
+
userRows[selectedIndex].focus();
|
|
126
|
+
screen.render();
|
|
127
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import blessed from 'blessed';
|
|
2
|
+
import { screen, screenClear, screenExit, screenRefresh, addFocusBtn, removeFocusBtn } from "../utils/screen.js";
|
|
3
|
+
import { displayDashboard } from "./layout.js"
|
|
4
|
+
import { color } from '../utils/contants.js';
|
|
5
|
+
|
|
6
|
+
const welcome_box = blessed.box({
|
|
7
|
+
top: 'center',
|
|
8
|
+
left: 'center',
|
|
9
|
+
width: 50,
|
|
10
|
+
height: 10,
|
|
11
|
+
border: {
|
|
12
|
+
type: 'line'
|
|
13
|
+
},
|
|
14
|
+
align: 'center',
|
|
15
|
+
content: `
|
|
16
|
+
|
|
17
|
+
Welcome to Chat TUI`,
|
|
18
|
+
style: {
|
|
19
|
+
fg: color.white,
|
|
20
|
+
bg: color.black,
|
|
21
|
+
border: {
|
|
22
|
+
fg: color.green
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// YES button
|
|
28
|
+
const yesBtn = blessed.button({
|
|
29
|
+
parent: welcome_box,
|
|
30
|
+
mouse: true,
|
|
31
|
+
keys: true,
|
|
32
|
+
shrink: true,
|
|
33
|
+
padding: {
|
|
34
|
+
left: 2,
|
|
35
|
+
right: 2
|
|
36
|
+
},
|
|
37
|
+
left: 10,
|
|
38
|
+
bottom: 2,
|
|
39
|
+
name: 'yes',
|
|
40
|
+
content: 'YES',
|
|
41
|
+
style: {
|
|
42
|
+
bg: color.green,
|
|
43
|
+
fg: color.black,
|
|
44
|
+
focus: {
|
|
45
|
+
bg: color.purple,
|
|
46
|
+
fg: color.black,
|
|
47
|
+
bold: true
|
|
48
|
+
},
|
|
49
|
+
hover: {
|
|
50
|
+
bg: color.purple
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// NO button
|
|
56
|
+
const noBtn = blessed.button({
|
|
57
|
+
parent: welcome_box,
|
|
58
|
+
mouse: true,
|
|
59
|
+
keys: true,
|
|
60
|
+
shrink: true,
|
|
61
|
+
padding: {
|
|
62
|
+
left: 2,
|
|
63
|
+
right: 2
|
|
64
|
+
},
|
|
65
|
+
right: 10,
|
|
66
|
+
bottom: 2,
|
|
67
|
+
name: 'no',
|
|
68
|
+
content: 'NO',
|
|
69
|
+
style: {
|
|
70
|
+
bg: color.red,
|
|
71
|
+
fg: color.black,
|
|
72
|
+
focus: {
|
|
73
|
+
bg: color.purple,
|
|
74
|
+
fg: color.black,
|
|
75
|
+
bold: true
|
|
76
|
+
},
|
|
77
|
+
hover: {
|
|
78
|
+
bg: color.purple
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// YES event
|
|
84
|
+
yesBtn.on('press', () => {
|
|
85
|
+
screenClear()
|
|
86
|
+
removeFocusBtn(1)
|
|
87
|
+
removeFocusBtn(2)
|
|
88
|
+
displayDashboard()
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// NO event
|
|
92
|
+
noBtn.on('press', () => {
|
|
93
|
+
screenExit();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
export const startUI = () => {
|
|
97
|
+
screen.append(welcome_box);
|
|
98
|
+
yesBtn.focus();
|
|
99
|
+
addFocusBtn({ id: 1, btn: yesBtn })
|
|
100
|
+
addFocusBtn({ id: 2, btn: noBtn })
|
|
101
|
+
screenRefresh();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const color = {
|
|
2
|
+
white: "#ffffff",
|
|
3
|
+
black: "#000000",
|
|
4
|
+
primary: "#67e55e",
|
|
5
|
+
red: "#ff0000",
|
|
6
|
+
green: "#00ff00",
|
|
7
|
+
purple: "#9966ff",
|
|
8
|
+
blue: "#0066ff",
|
|
9
|
+
sky: "#00ccff",
|
|
10
|
+
yellow: "#ffff00"
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const tabsfocus = {
|
|
14
|
+
btns: [],
|
|
15
|
+
btnIndex: 0,
|
|
16
|
+
istoggle: true
|
|
17
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import blessed from 'blessed';
|
|
2
|
+
import { screen, screenRefresh, addFocusBtn, removeFocusBtn, screenExit } from './screen.js';
|
|
3
|
+
import { color } from './contants.js';
|
|
4
|
+
|
|
5
|
+
const dialogBox = blessed.box({
|
|
6
|
+
top: 'center',
|
|
7
|
+
left: 'center',
|
|
8
|
+
width: 40,
|
|
9
|
+
height: 7,
|
|
10
|
+
border: {
|
|
11
|
+
type: 'line'
|
|
12
|
+
},
|
|
13
|
+
style: {
|
|
14
|
+
fg: color.white,
|
|
15
|
+
bg: color.black,
|
|
16
|
+
border: { fg: color.yellow }
|
|
17
|
+
},
|
|
18
|
+
hidden: true
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const dialogText = blessed.text({
|
|
22
|
+
parent: dialogBox,
|
|
23
|
+
top: 1,
|
|
24
|
+
left: 'center',
|
|
25
|
+
content: 'Are you sure you want to exit?',
|
|
26
|
+
style: {
|
|
27
|
+
fg: color.white,
|
|
28
|
+
bg: color.black
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const okBtn = blessed.button({
|
|
33
|
+
parent: dialogBox,
|
|
34
|
+
content: ' ok ',
|
|
35
|
+
bottom: 1,
|
|
36
|
+
left: 8,
|
|
37
|
+
shrink: true,
|
|
38
|
+
mouse: true,
|
|
39
|
+
style: {
|
|
40
|
+
fg: color.white,
|
|
41
|
+
bg: color.red,
|
|
42
|
+
focus: { bg: color.purple },
|
|
43
|
+
hover: { bg: color.purple }
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const noBtn = blessed.button({
|
|
48
|
+
parent: dialogBox,
|
|
49
|
+
content: ' No ',
|
|
50
|
+
bottom: 1,
|
|
51
|
+
right: 8,
|
|
52
|
+
shrink: true,
|
|
53
|
+
mouse: true,
|
|
54
|
+
style: {
|
|
55
|
+
fg: color.black,
|
|
56
|
+
bg: color.green,
|
|
57
|
+
focus: { bg: color.purple },
|
|
58
|
+
hover: { bg: color.purple }
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
okBtn.on('press', () => {
|
|
63
|
+
dialogBox.hide();
|
|
64
|
+
removeFocusBtn(okBtn);
|
|
65
|
+
removeFocusBtn(noBtn);
|
|
66
|
+
screen.remove(dialogBox);
|
|
67
|
+
screenExit()
|
|
68
|
+
disableModalMode();
|
|
69
|
+
screenRefresh();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
noBtn.on('press', () => {
|
|
73
|
+
dialogBox.hide();
|
|
74
|
+
removeFocusBtn(5);
|
|
75
|
+
removeFocusBtn(6);
|
|
76
|
+
screen.remove(dialogBox);
|
|
77
|
+
disableModalMode();
|
|
78
|
+
screenRefresh();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export function showDialogBox() {
|
|
82
|
+
screen.append(dialogBox)
|
|
83
|
+
addFocusBtn({ id: 5, btn: okBtn })
|
|
84
|
+
addFocusBtn({ id: 6, btn: noBtn })
|
|
85
|
+
okBtn.focus();
|
|
86
|
+
dialogBox.show();
|
|
87
|
+
enableModalMode();
|
|
88
|
+
dialogBox.focus();
|
|
89
|
+
dialogBox.setFront();
|
|
90
|
+
screenRefresh();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function enableModalMode() {
|
|
94
|
+
screen.children.forEach(child => {
|
|
95
|
+
if (child !== dialogBox) {
|
|
96
|
+
child.hidden = true;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function disableModalMode() {
|
|
102
|
+
screen.children.forEach(child => {
|
|
103
|
+
child.hidden = false;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import blessed from 'blessed';
|
|
2
|
+
import { tabsfocus } from '../utils/contants.js';
|
|
3
|
+
|
|
4
|
+
export const screen = blessed.screen({
|
|
5
|
+
smartCSR: true,
|
|
6
|
+
mouse: true,
|
|
7
|
+
title: "Chat TUI"
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
screen.key(['escape', 'C-c'], () => {
|
|
11
|
+
screenExit();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const screenRefresh = () => { screen.render(); }
|
|
15
|
+
export const screenExit = () => {
|
|
16
|
+
try {
|
|
17
|
+
screen.destroy();
|
|
18
|
+
} catch (e) { }
|
|
19
|
+
process.stdout.write("\x1b[?25h"); // show cursor
|
|
20
|
+
process.stdout.write("\x1b[0m"); // reset styles
|
|
21
|
+
process.exit(0);
|
|
22
|
+
};
|
|
23
|
+
export const screenClear = () => {
|
|
24
|
+
[...screen.children].forEach(child => child.destroy());
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const addFocusBtn = (btn) => {
|
|
28
|
+
const exists = tabsfocus.btns.some(b => b.id == btn.id);
|
|
29
|
+
if (!exists)
|
|
30
|
+
tabsfocus.btns.push(btn);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const removeFocusBtn = (index) => {
|
|
34
|
+
const eleIndex = tabsfocus.btns.findIndex(b => b.id == index);
|
|
35
|
+
if (eleIndex != -1) {
|
|
36
|
+
tabsfocus.btns.splice(eleIndex, 1);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Helper to focus button
|
|
41
|
+
export const focusButton = (index) => {
|
|
42
|
+
if (!tabsfocus.btns.length) return;
|
|
43
|
+
const safeIndex = index % tabsfocus.btns.length;
|
|
44
|
+
tabsfocus.btnIndex = safeIndex;
|
|
45
|
+
const item = tabsfocus.btns[safeIndex];
|
|
46
|
+
if (!item || !item.btn) return;
|
|
47
|
+
item.btn.focus();
|
|
48
|
+
if (item.btn.readInput) {
|
|
49
|
+
item.btn.readInput();
|
|
50
|
+
}
|
|
51
|
+
screenRefresh();
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// TAB key
|
|
55
|
+
screen.key(['tab'], () => {
|
|
56
|
+
if (!tabsfocus.istoggle) return;
|
|
57
|
+
if (!tabsfocus.btns.length) return;
|
|
58
|
+
tabsfocus.btnIndex = (tabsfocus.btnIndex + 1) % tabsfocus.btns.length;
|
|
59
|
+
focusButton(tabsfocus.btnIndex);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ENTER key
|
|
63
|
+
screen.key(['enter'], () => {
|
|
64
|
+
const item = tabsfocus.btns[tabsfocus.btnIndex];
|
|
65
|
+
if (!item || !item.btn) return;
|
|
66
|
+
item.btn.emit('press');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
export const clearFocus = () => {
|
|
70
|
+
if (screen.focused?.cancel) {
|
|
71
|
+
screen.focused.cancel();
|
|
72
|
+
}
|
|
73
|
+
screen.focused = null;
|
|
74
|
+
screen.render();
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|