split-exec 0.0.1
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/.gitlab-ci.yml +16 -0
- package/README.md +65 -0
- package/bin/cli.js +11 -0
- package/package.json +26 -0
- package/src/index.js +150 -0
package/.gitlab-ci.yml
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# [split-exec](https://gitlab.com/GCSBOSS/split-exec)
|
|
2
|
+
|
|
3
|
+
A lightweight Node.js TUI (Text User Interface) to run multiple commands side-by-side in a split terminal view.
|
|
4
|
+
|
|
5
|
+
**Features:**
|
|
6
|
+
- 🖥️ **Split Screen:** Automatically divides terminal width for 2, 3, 4+ commands.
|
|
7
|
+
- 📜 **Smart Scrolling:** Auto-scrolls to bottom, but pauses if you scroll up to read history.
|
|
8
|
+
- 🔄 **Restart:** Press `r` to instantly kill and restart any specific command.
|
|
9
|
+
- 🖱️ **Interactive:** Supports Mouse wheel, clicking, and keyboard navigation.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install split-exec
|
|
15
|
+
# or globally
|
|
16
|
+
npm install -g split-exec
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
### 1. CLI Usage
|
|
23
|
+
|
|
24
|
+
Run commands directly from your terminal. Great for on-the-fly monitoring.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Run 3 commands side by side
|
|
28
|
+
npx split-exec "ping google.com" "ping 1.1.1.1" "ls -R /"
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 2. Programmatic Usage
|
|
33
|
+
|
|
34
|
+
Create a dashboard file (e.g., `dev-dashboard.js`) for your project.
|
|
35
|
+
|
|
36
|
+
```javascript
|
|
37
|
+
const { splitExec } = require('split-exec');
|
|
38
|
+
|
|
39
|
+
splitExec([
|
|
40
|
+
// Simple string
|
|
41
|
+
'npm run server',
|
|
42
|
+
|
|
43
|
+
// Object for more control
|
|
44
|
+
{
|
|
45
|
+
title: 'Jest Tests',
|
|
46
|
+
cmd: 'npm',
|
|
47
|
+
args: ['test', '--', '--watch']
|
|
48
|
+
}
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Controls
|
|
54
|
+
|
|
55
|
+
| Key | Action |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| **Tab / Right** | Focus next column |
|
|
58
|
+
| **Shift+Tab / Left** | Focus previous column |
|
|
59
|
+
| **Up / Down** | Scroll history |
|
|
60
|
+
| **r** | **Restart** the focused command |
|
|
61
|
+
| **q / Esc** | Quit |
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
MIT
|
package/bin/cli.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "split-exec",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Run multiple commands in parallel with a split-screen TUI dashboard.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"split-exec": "bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"cli",
|
|
14
|
+
"dashboard",
|
|
15
|
+
"parallel",
|
|
16
|
+
"terminal",
|
|
17
|
+
"split-screen",
|
|
18
|
+
"blessed"
|
|
19
|
+
],
|
|
20
|
+
"author": "Your Name",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"blessed": "^0.1.81",
|
|
24
|
+
"iconv-lite": "^0.6.3"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
const blessed = require('blessed');
|
|
2
|
+
const { spawn, execSync } = require('child_process');
|
|
3
|
+
const iconv = require('iconv-lite');
|
|
4
|
+
|
|
5
|
+
// 1. Detect System Encoding (Robust Version)
|
|
6
|
+
function getSystemEncoding() {
|
|
7
|
+
// Mac/Linux are almost always UTF-8
|
|
8
|
+
if (process.platform !== 'win32') return 'utf8';
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
// Run 'chcp' to find active code page
|
|
12
|
+
// output example: "Página de código ativa: 850"
|
|
13
|
+
const output = execSync('chcp').toString();
|
|
14
|
+
|
|
15
|
+
// Regex: Find ALL sequences of digits
|
|
16
|
+
const matches = output.match(/\d+/g);
|
|
17
|
+
|
|
18
|
+
if (matches && matches.length > 0) {
|
|
19
|
+
// The code page is invariably the last number in the string
|
|
20
|
+
const pageId = matches[matches.length - 1];
|
|
21
|
+
const encoding = 'cp' + pageId; // e.g. 'cp850'
|
|
22
|
+
|
|
23
|
+
// Ensure iconv supports it, otherwise fallback
|
|
24
|
+
if (iconv.encodingExists(encoding)) {
|
|
25
|
+
return encoding;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
} catch (e) {
|
|
29
|
+
// Ignore error
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// CRITICAL FALLBACK for Portuguese/Western Europe Windows
|
|
33
|
+
// If we couldn't detect it, assume CP850 (Standard OEM), NOT UTF-8.
|
|
34
|
+
return 'cp850';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const SYSTEM_ENCODING = getSystemEncoding();
|
|
38
|
+
|
|
39
|
+
function start(commands) {
|
|
40
|
+
const screen = blessed.screen({
|
|
41
|
+
smartCSR: true,
|
|
42
|
+
title: `split-exec`,
|
|
43
|
+
dockBorders: true,
|
|
44
|
+
mouse: true
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const configs = commands.map((item, index) => {
|
|
48
|
+
if (typeof item === 'string') {
|
|
49
|
+
return { cmd: item, args: [], title: item };
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
cmd: item.cmd,
|
|
53
|
+
args: item.args || [],
|
|
54
|
+
title: item.title || `Command ${index + 1}`
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const boxes = [];
|
|
59
|
+
|
|
60
|
+
configs.forEach((config, i) => {
|
|
61
|
+
const widthPercent = Math.floor(100 / configs.length);
|
|
62
|
+
|
|
63
|
+
const box = blessed.box({
|
|
64
|
+
top: 0,
|
|
65
|
+
left: `${i * widthPercent}%`,
|
|
66
|
+
width: `${widthPercent}%`,
|
|
67
|
+
height: '100%',
|
|
68
|
+
label: ` ${config.title} `,
|
|
69
|
+
tags: true,
|
|
70
|
+
keys: true,
|
|
71
|
+
mouse: true,
|
|
72
|
+
border: { type: 'line' },
|
|
73
|
+
style: { fg: 'white', border: { fg: '#f0f0f0' }, focus: { border: { fg: 'green' } } },
|
|
74
|
+
scrollable: true,
|
|
75
|
+
alwaysScroll: true,
|
|
76
|
+
scrollbar: { ch: ' ', bg: 'blue' }
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
boxes.push(box);
|
|
80
|
+
screen.append(box);
|
|
81
|
+
|
|
82
|
+
box.key('r', () => spawnInBox(box, config, screen));
|
|
83
|
+
spawnInBox(box, config, screen);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (boxes.length > 0) boxes[0].focus();
|
|
87
|
+
|
|
88
|
+
screen.key(['tab', 'right'], () => { screen.focusNext(); screen.render(); });
|
|
89
|
+
screen.key(['S-tab', 'left'], () => { screen.focusPrevious(); screen.render(); });
|
|
90
|
+
screen.key(['escape', 'q', 'C-c'], () => process.exit(0));
|
|
91
|
+
|
|
92
|
+
const tip = blessed.box({
|
|
93
|
+
bottom: 0, right: 0, height: 1, width: 'shrink',
|
|
94
|
+
content: ` [TAB] Nav | [r] Restart | [q] Quit | Enc: ${SYSTEM_ENCODING} `,
|
|
95
|
+
style: { bg: 'blue', fg: 'white' }
|
|
96
|
+
});
|
|
97
|
+
screen.append(tip);
|
|
98
|
+
|
|
99
|
+
screen.render();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function spawnInBox(box, config, screen) {
|
|
103
|
+
if (box.activeProcess) {
|
|
104
|
+
try { box.activeProcess.kill(); } catch (e) {}
|
|
105
|
+
box.pushLine(`{yellow-fg}--- RESTARTING ---{/}`);
|
|
106
|
+
} else {
|
|
107
|
+
box.setContent('');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
box.border.fg = 'green';
|
|
111
|
+
screen.render();
|
|
112
|
+
|
|
113
|
+
const write = (msg) => {
|
|
114
|
+
const isAtBottom = (box.childBase + box.height) >= box.getScrollHeight();
|
|
115
|
+
box.pushLine(msg);
|
|
116
|
+
if (isAtBottom) box.setScrollPerc(100);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
// Spawn with shell:true to support standard windows commands
|
|
121
|
+
const child = spawn(config.cmd, config.args, { shell: true });
|
|
122
|
+
|
|
123
|
+
box.activeProcess = child;
|
|
124
|
+
|
|
125
|
+
// IMPORTANT: Treat output as raw buffer
|
|
126
|
+
child.stdout.on('data', d => {
|
|
127
|
+
// Decode Buffer -> String using detected encoding
|
|
128
|
+
const cleanStr = iconv.decode(d, SYSTEM_ENCODING);
|
|
129
|
+
write(cleanStr.trim());
|
|
130
|
+
screen.render();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
child.stderr.on('data', d => {
|
|
134
|
+
const cleanStr = iconv.decode(d, SYSTEM_ENCODING);
|
|
135
|
+
write(`{red-fg}${cleanStr.trim()}{/}`);
|
|
136
|
+
screen.render();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
child.on('close', (code) => {
|
|
140
|
+
box.activeProcess = null;
|
|
141
|
+
box.border.fg = '#f0f0f0';
|
|
142
|
+
write(code === 0 ? `{green-fg}Done (0){/}` : `{red-fg}Exit (${code}){/}`);
|
|
143
|
+
screen.render();
|
|
144
|
+
});
|
|
145
|
+
} catch (e) {
|
|
146
|
+
write(`{red-fg}Error: ${e.message}{/}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = { splitExec: start };
|