@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.
@@ -0,0 +1,6 @@
1
+ {
2
+ "editor.tabSize": 1,
3
+ "editor.insertSpaces": true,
4
+ "editor.detectIndentation": false,
5
+ "css.lint.unknownAtRules": "ignore"
6
+ }
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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "./src/start.js";
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
+ }
@@ -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
+