my_wins 1.4.0 → 1.5.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/docs/conemu.md +40 -0
- package/lib/index.js +244 -15
- package/package.json +36 -36
- package/readme.md +13 -2
- package/republish.bat +1 -1
- package/test/run-test.js +79 -15
package/docs/conemu.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# ConEmu integration
|
|
2
|
+
|
|
3
|
+
`my_wins` can open console commands in `ConEmu` tabs instead of separate system `cmd.exe` windows.
|
|
4
|
+
|
|
5
|
+
## How it works
|
|
6
|
+
|
|
7
|
+
- Top-level `conemu` in `my_wins.json` points to `ConEmu.exe` or `ConEmu64.exe`.
|
|
8
|
+
- When `conemu` is configured, console wins use `ConEmu` by default.
|
|
9
|
+
- Per-win `conemu: true|false` overrides the default.
|
|
10
|
+
- New tabs are created through `ConEmu` shell/macros.
|
|
11
|
+
- After tab creation, `my_wins` explicitly renames the visible ConEmu tab caption to the win `title`.
|
|
12
|
+
- The internal console title may still change while commands run, but the ConEmu tab caption should stay stable.
|
|
13
|
+
|
|
14
|
+
## Example
|
|
15
|
+
|
|
16
|
+
```json5
|
|
17
|
+
{
|
|
18
|
+
conemu: "D:\\ProgsReady\\ConEmu\\ConEmu64.exe",
|
|
19
|
+
wins: {
|
|
20
|
+
api: {
|
|
21
|
+
title: "API",
|
|
22
|
+
cmd: "npm run dev"
|
|
23
|
+
},
|
|
24
|
+
worker: {
|
|
25
|
+
title: "Worker",
|
|
26
|
+
cmd: "npm run worker"
|
|
27
|
+
},
|
|
28
|
+
plain_cmd: {
|
|
29
|
+
conemu: false,
|
|
30
|
+
cmd: "echo this one uses a normal cmd window"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Current limitations
|
|
37
|
+
|
|
38
|
+
- `ConEmu` support is Windows-only.
|
|
39
|
+
- Background color for `ConEmu` tabs is applied after the tab is activated.
|
|
40
|
+
- Automated integration testing for `ConEmu` is skipped if `ConEmu` is not installed at the expected test path.
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { spawn, execFile, exec } = require("child_process");
|
|
2
2
|
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
3
4
|
const JSON5 = require("json5");
|
|
4
5
|
const inquirer = require("inquirer");
|
|
5
6
|
const keysender = require("@sumbat/keysender");
|
|
@@ -42,6 +43,211 @@ function awaitDelay(delay) {
|
|
|
42
43
|
});
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
function execFileAsync(file, args, options) {
|
|
47
|
+
return new Promise((resolve, reject)=>{
|
|
48
|
+
execFile(file, args, options || {}, (error, stdout, stderr)=>{
|
|
49
|
+
if (error) {
|
|
50
|
+
error.stdout = stdout;
|
|
51
|
+
error.stderr = stderr;
|
|
52
|
+
reject(error);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
resolve({ stdout, stderr });
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function toPowerShellSingleQuotedString(value) {
|
|
61
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildConEmuCreateTabCommand(win) {
|
|
65
|
+
const titleCommand = `title ${win.matchTitle}`;
|
|
66
|
+
return {
|
|
67
|
+
titleCommand,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getConEmuInfo(settings) {
|
|
72
|
+
if (!settings.conemu) return null;
|
|
73
|
+
|
|
74
|
+
const guiPath = path.resolve(settings.conemu);
|
|
75
|
+
const baseDir = path.dirname(guiPath);
|
|
76
|
+
const helperName = /64\.exe$/i.test(guiPath) ? "ConEmuC64.exe" : "ConEmuC.exe";
|
|
77
|
+
const helperCandidates = [
|
|
78
|
+
path.join(baseDir, helperName),
|
|
79
|
+
path.join(baseDir, "ConEmu", helperName),
|
|
80
|
+
];
|
|
81
|
+
const macroPath = helperCandidates.find((candidate)=>fs.existsSync(candidate));
|
|
82
|
+
|
|
83
|
+
if (!fs.existsSync(guiPath)) {
|
|
84
|
+
throw new Error(`ConEmu executable not found: ${guiPath}`);
|
|
85
|
+
}
|
|
86
|
+
if (!macroPath) {
|
|
87
|
+
throw new Error(`ConEmu GuiMacro executable not found near: ${guiPath}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
guiPath,
|
|
92
|
+
macroPath,
|
|
93
|
+
guiPid: null,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function shouldUseConEmuForWin(settings, win, conemuInfo) {
|
|
98
|
+
if (!conemuInfo) return false;
|
|
99
|
+
if (!win || win.app) return false;
|
|
100
|
+
if (typeof win.conemu === "boolean") return win.conemu;
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function execConEmuMacro(conemu, macro, tabIndex) {
|
|
105
|
+
if (!conemu || !conemu.guiPid) throw new Error("ConEmu GUI PID is not initialized");
|
|
106
|
+
const guiMacroTarget = tabIndex ? `-GuiMacro:${conemu.guiPid}:T${tabIndex}` : `-GuiMacro:${conemu.guiPid}`;
|
|
107
|
+
const command = [
|
|
108
|
+
"&",
|
|
109
|
+
toPowerShellSingleQuotedString(conemu.macroPath),
|
|
110
|
+
toPowerShellSingleQuotedString(guiMacroTarget),
|
|
111
|
+
toPowerShellSingleQuotedString(macro),
|
|
112
|
+
].join(" ");
|
|
113
|
+
const result = await execFileAsync("powershell", ["-NoProfile", "-Command", command]);
|
|
114
|
+
return (result.stdout || "").trim();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function execConEmuCommand(conemu, command, commandArgs, tabIndex) {
|
|
118
|
+
const serializedArgs = (commandArgs || []).map((arg)=>{
|
|
119
|
+
if (typeof arg === "number") return String(arg);
|
|
120
|
+
return `"${String(arg).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
121
|
+
});
|
|
122
|
+
return execConEmuMacro(conemu, `${command}(${serializedArgs.join(",")})`, tabIndex);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function findConEmuGuiPidByActiveTitle(title) {
|
|
126
|
+
if (!title) return null;
|
|
127
|
+
const titleMask = `*${title}*`;
|
|
128
|
+
const command = [
|
|
129
|
+
"(",
|
|
130
|
+
"Get-Process ConEmu64,ConEmu -ErrorAction SilentlyContinue",
|
|
131
|
+
`| Where-Object { $_.MainWindowHandle -ne 0 -and $_.MainWindowTitle -like ${toPowerShellSingleQuotedString(titleMask)} }`,
|
|
132
|
+
"| Sort-Object Id -Descending",
|
|
133
|
+
"| Select-Object -First 1 -ExpandProperty Id",
|
|
134
|
+
")",
|
|
135
|
+
].join(" ");
|
|
136
|
+
const result = await execFileAsync("powershell", ["-NoProfile", "-Command", command]);
|
|
137
|
+
const pid = parseInt((result.stdout || "").trim(), 10);
|
|
138
|
+
return Number.isFinite(pid) ? pid : null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function listConEmuGuiPids() {
|
|
142
|
+
const command = [
|
|
143
|
+
"(",
|
|
144
|
+
"Get-Process ConEmu64,ConEmu -ErrorAction SilentlyContinue",
|
|
145
|
+
"| Where-Object { $_.MainWindowHandle -ne 0 -and $_.MainWindowTitle -notlike 'About ConEmu*' }",
|
|
146
|
+
"| Sort-Object Id",
|
|
147
|
+
"| Select-Object -ExpandProperty Id",
|
|
148
|
+
")",
|
|
149
|
+
].join(" ");
|
|
150
|
+
const result = await execFileAsync("powershell", ["-NoProfile", "-Command", command]);
|
|
151
|
+
return (result.stdout || "")
|
|
152
|
+
.split(/\r?\n/)
|
|
153
|
+
.map((line)=>parseInt(line.trim(), 10))
|
|
154
|
+
.filter((pid)=>Number.isFinite(pid));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function listConEmuTabsByPid(guiPid, macroPath) {
|
|
158
|
+
const command = [
|
|
159
|
+
"&",
|
|
160
|
+
toPowerShellSingleQuotedString(macroPath),
|
|
161
|
+
toPowerShellSingleQuotedString(`-GuiMacro:${guiPid}`),
|
|
162
|
+
toPowerShellSingleQuotedString("Tab(12)"),
|
|
163
|
+
].join(" ");
|
|
164
|
+
const result = await execFileAsync("powershell", ["-NoProfile", "-Command", command]);
|
|
165
|
+
return String(result.stdout || "")
|
|
166
|
+
.split(/\r?\n/)
|
|
167
|
+
.map((line)=>line.trim())
|
|
168
|
+
.filter(Boolean);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function waitForConEmuGuiPid(title, timeoutMs, beforePids) {
|
|
172
|
+
const started = Date.now();
|
|
173
|
+
const knownBefore = new Set(beforePids || []);
|
|
174
|
+
while (Date.now() - started < timeoutMs) {
|
|
175
|
+
const pid = await findConEmuGuiPidByActiveTitle(title);
|
|
176
|
+
if (pid) return pid;
|
|
177
|
+
|
|
178
|
+
const currentPids = await listConEmuGuiPids();
|
|
179
|
+
const newPid = currentPids.find((currentPid)=>!knownBefore.has(currentPid));
|
|
180
|
+
if (newPid) return newPid;
|
|
181
|
+
if (currentPids.length === 1) return currentPids[0];
|
|
182
|
+
if (currentPids.length > 1) return currentPids[currentPids.length - 1];
|
|
183
|
+
await awaitDelay(200);
|
|
184
|
+
}
|
|
185
|
+
throw new Error(`Couldn't find ConEmu GUI window for title '${title}'`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function launchConEmuTab(conemu, win, isFirstTab) {
|
|
189
|
+
const { titleCommand } = buildConEmuCreateTabCommand(win);
|
|
190
|
+
if (isFirstTab) {
|
|
191
|
+
const beforePids = await listConEmuGuiPids();
|
|
192
|
+
const beforePid = beforePids.length ? beforePids[beforePids.length - 1] : null;
|
|
193
|
+
const beforeTabs = beforePid ? await listConEmuTabsByPid(beforePid, conemu.macroPath) : [];
|
|
194
|
+
const childArgs = ["-run", "cmd.exe", "/k", titleCommand];
|
|
195
|
+
const child = spawn(conemu.guiPath, childArgs, {
|
|
196
|
+
detached: true,
|
|
197
|
+
stdio: "ignore",
|
|
198
|
+
});
|
|
199
|
+
child.unref();
|
|
200
|
+
conemu.guiPid = await waitForConEmuGuiPid(null, 10000, beforePids);
|
|
201
|
+
const afterTabs = await listConEmuTabsByPid(conemu.guiPid, conemu.macroPath);
|
|
202
|
+
const tabIndex = afterTabs.length > beforeTabs.length ? afterTabs.length : afterTabs.length;
|
|
203
|
+
await renameConEmuTab(conemu, tabIndex, win.tabTitle);
|
|
204
|
+
return tabIndex;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const beforeTabs = await listConEmuTabsByPid(conemu.guiPid, conemu.macroPath);
|
|
208
|
+
await execConEmuCommand(conemu, "Shell", ["new_console", "cmd.exe", `/k ${titleCommand}`]);
|
|
209
|
+
await awaitDelay(300);
|
|
210
|
+
const afterTabs = await listConEmuTabsByPid(conemu.guiPid, conemu.macroPath);
|
|
211
|
+
const tabIndex = afterTabs.length > beforeTabs.length ? afterTabs.length : afterTabs.length;
|
|
212
|
+
await renameConEmuTab(conemu, tabIndex, win.tabTitle);
|
|
213
|
+
return tabIndex;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function activateConEmuTab(conemu, tabIndex) {
|
|
217
|
+
await execConEmuCommand(conemu, "Tab", [7, tabIndex]);
|
|
218
|
+
await awaitDelay(150);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function renameConEmuTab(conemu, tabIndex, tabTitle) {
|
|
222
|
+
if (!tabTitle) return;
|
|
223
|
+
await execConEmuCommand(conemu, "Rename", [1, tabTitle], tabIndex);
|
|
224
|
+
await awaitDelay(150);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function stabilizeConEmuTabTitle(conemu, win) {
|
|
228
|
+
if (!conemu || !win || !win.conemuTabIndex || !win.tabTitle) return;
|
|
229
|
+
await awaitDelay(500);
|
|
230
|
+
await renameConEmuTab(conemu, win.conemuTabIndex, win.tabTitle);
|
|
231
|
+
await awaitDelay(1000);
|
|
232
|
+
await renameConEmuTab(conemu, win.conemuTabIndex, win.tabTitle);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function setConEmuBackgroundColor(conemu, tabIndex, backgroundcolor) {
|
|
236
|
+
if (!backgroundcolor) return;
|
|
237
|
+
await execConEmuCommand(conemu, "SetOption", ["bgImage", backgroundcolor], tabIndex);
|
|
238
|
+
await awaitDelay(150);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function printInConEmuTab(conemu, tabIndex, cmd) {
|
|
242
|
+
await execConEmuCommand(conemu, "Print", [cmd], tabIndex);
|
|
243
|
+
await awaitDelay(200);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function pressEnterInConEmuTab(conemu, tabIndex) {
|
|
247
|
+
await execConEmuCommand(conemu, "Keys", ["Enter"], tabIndex);
|
|
248
|
+
await awaitDelay(200);
|
|
249
|
+
}
|
|
250
|
+
|
|
45
251
|
function parseArgs(argv) {
|
|
46
252
|
const raw = argv.slice(2);
|
|
47
253
|
let menu = false;
|
|
@@ -150,7 +356,8 @@ function buildRunSettings(baseSettings, filterNames) {
|
|
|
150
356
|
for(let k in settings.wins) {
|
|
151
357
|
const w = settings.wins[k];
|
|
152
358
|
w.name = k;
|
|
153
|
-
w.
|
|
359
|
+
w.tabTitle = w.title || w.name;
|
|
360
|
+
w.matchTitle = `my_wins_${w.name.replace(/[^\w.-]+/g, "_")}${randSuffix()}`;
|
|
154
361
|
}
|
|
155
362
|
|
|
156
363
|
return settings;
|
|
@@ -159,17 +366,23 @@ function buildRunSettings(baseSettings, filterNames) {
|
|
|
159
366
|
async function runOnce(baseSettings, filterNames) {
|
|
160
367
|
const settings = buildRunSettings(baseSettings, filterNames);
|
|
161
368
|
if (!Object.keys(settings.wins).length) return;
|
|
369
|
+
const conemu = getConEmuInfo(settings);
|
|
162
370
|
|
|
163
371
|
let wins = keysender.getAllWindows();
|
|
372
|
+
let createdConsoleCount = 0;
|
|
164
373
|
|
|
165
374
|
for(let k in settings.wins) {
|
|
166
375
|
const w = settings.wins[k];
|
|
167
|
-
const {name,
|
|
376
|
+
const {name, app, no_run, cmd} = w;
|
|
377
|
+
const useConEmu = shouldUseConEmuForWin(settings, w, conemu);
|
|
168
378
|
if(app && !no_run) {
|
|
169
379
|
exec(cmd);
|
|
170
380
|
delete settings.wins[k];
|
|
381
|
+
} else if (useConEmu) {
|
|
382
|
+
w.conemuTabIndex = await launchConEmuTab(conemu, w, createdConsoleCount === 0);
|
|
383
|
+
createdConsoleCount++;
|
|
171
384
|
} else
|
|
172
|
-
exec(`start cmd.exe @cmd /k "title ${
|
|
385
|
+
exec(`start cmd.exe @cmd /k "title ${w.matchTitle}"`);
|
|
173
386
|
}
|
|
174
387
|
|
|
175
388
|
await awaitDelay(settings.startTimeout || 1500);
|
|
@@ -181,14 +394,34 @@ async function runOnce(baseSettings, filterNames) {
|
|
|
181
394
|
let winIndex = 0;
|
|
182
395
|
for(let k in settings.wins) {
|
|
183
396
|
const w = settings.wins[k];
|
|
184
|
-
const {name,
|
|
397
|
+
const {name, app, cmd, no_run, delay} = w;
|
|
398
|
+
const useConEmu = shouldUseConEmuForWin(settings, w, conemu);
|
|
185
399
|
|
|
186
|
-
let win_info_candidates = wins.filter(c => c.title.includes(
|
|
400
|
+
let win_info_candidates = wins.filter(c => c.title.includes(w.matchTitle));
|
|
187
401
|
const win_info = win_info_candidates[0];
|
|
188
402
|
|
|
189
|
-
if (
|
|
403
|
+
if (useConEmu) {
|
|
404
|
+
w.winIndex = winIndex;
|
|
405
|
+
w.view = winsArray[winIndex];
|
|
406
|
+
await setConEmuBackgroundColor(conemu, w.conemuTabIndex, w.backgroundcolor);
|
|
407
|
+
|
|
408
|
+
w.activate = async ()=>{
|
|
409
|
+
await activateConEmuTab(conemu, w.conemuTabIndex);
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
w.print = async (nextCmd) => {
|
|
413
|
+
await printInConEmuTab(conemu, w.conemuTabIndex, nextCmd);
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
w.pressEnter = async () => {
|
|
417
|
+
await pressEnterInConEmuTab(conemu, w.conemuTabIndex);
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
await w.print(cmd);
|
|
421
|
+
}
|
|
422
|
+
else if (!win_info) console.warn(`CODE00000000 Couldn't find window ${w.matchTitle}`);
|
|
190
423
|
else if (win_info_candidates.length > 1)
|
|
191
|
-
console.warn(`CODE00000000 There are ${win_info_candidates.length} windows with title '${
|
|
424
|
+
console.warn(`CODE00000000 There are ${win_info_candidates.length} windows with title '${w.matchTitle}'. Only one window expected!`);
|
|
192
425
|
else {
|
|
193
426
|
const win = new keysender.Hardware(win_info.handle);
|
|
194
427
|
w.win = win;
|
|
@@ -214,14 +447,6 @@ async function runOnce(baseSettings, filterNames) {
|
|
|
214
447
|
}
|
|
215
448
|
|
|
216
449
|
await w.print(cmd);
|
|
217
|
-
// await awaitDelay(200);
|
|
218
|
-
// if(!no_run) {
|
|
219
|
-
// if(delay) {
|
|
220
|
-
// await awaitDelay(delay);
|
|
221
|
-
// }
|
|
222
|
-
// w.pressEnter();
|
|
223
|
-
// }
|
|
224
|
-
// await awaitDelay(300);
|
|
225
450
|
}
|
|
226
451
|
winIndex++;
|
|
227
452
|
}
|
|
@@ -234,6 +459,9 @@ async function runOnce(baseSettings, filterNames) {
|
|
|
234
459
|
await awaitDelay(delay);
|
|
235
460
|
}
|
|
236
461
|
await w.pressEnter();
|
|
462
|
+
if (shouldUseConEmuForWin(settings, w, conemu)) {
|
|
463
|
+
await stabilizeConEmuTabTitle(conemu, w);
|
|
464
|
+
}
|
|
237
465
|
}
|
|
238
466
|
}
|
|
239
467
|
|
|
@@ -262,6 +490,7 @@ async function promptMenu(winNames, exitChecked) {
|
|
|
262
490
|
}
|
|
263
491
|
|
|
264
492
|
module.exports.defaultSettings = defaultSettings;
|
|
493
|
+
module.exports.shouldUseConEmuForWin = shouldUseConEmuForWin;
|
|
265
494
|
module.exports.run = async ()=>{
|
|
266
495
|
const args = parseArgs(process.argv);
|
|
267
496
|
if (args.version) {
|
package/package.json
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
2
|
+
"name": "my_wins",
|
|
3
|
+
"version": "1.5.1",
|
|
4
|
+
"description": "Automates starting and laying out your console windows for tsc, babel, tests, verdaccio, etc...",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"automation",
|
|
7
|
+
"cmd",
|
|
8
|
+
"keysender",
|
|
9
|
+
"windows",
|
|
10
|
+
"autostart",
|
|
11
|
+
"autorun"
|
|
12
|
+
],
|
|
13
|
+
"main": "./index.js",
|
|
14
|
+
"bin": {
|
|
15
|
+
"my_wins": "bin/index.js"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"start": "node index.js",
|
|
19
|
+
"startm": "node index.js -m",
|
|
20
|
+
"test": "node test/run-test.js"
|
|
21
|
+
},
|
|
22
|
+
"author": "Yuri Yaryshev",
|
|
23
|
+
"license": "Unlicense",
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@sumbat/keysender": "^2.3.0",
|
|
26
|
+
"inquirer": "^13.2.1",
|
|
27
|
+
"json5": "^2.2.3"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"prettier": "^3.8.1"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/yuyaryshev/my_wins",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/yuyaryshev/my_wins.git"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/readme.md
CHANGED
|
@@ -4,6 +4,9 @@ Automates starting your console windows for tsc, babel, tests, verdaccio, etc...
|
|
|
4
4
|
|
|
5
5
|
my_wins opens **cmd** promts for you, position them nicely and types-in your commands into them.
|
|
6
6
|
|
|
7
|
+
If you set `conemu`, my_wins will open tabs in that ConEmu instance instead of separate system `cmd` windows.
|
|
8
|
+
Project docs are gradually being collected in [`docs`](./docs).
|
|
9
|
+
|
|
7
10
|
## Why?
|
|
8
11
|
|
|
9
12
|
Why this is better than just running cmd/bat files?
|
|
@@ -35,9 +38,11 @@ Create "my_wins.json" in your project with contents:
|
|
|
35
38
|
|
|
36
39
|
```json
|
|
37
40
|
{
|
|
41
|
+
"conemu": "D:\\ProgsReady\\ConEmu\\ConEmu64.exe",
|
|
38
42
|
"wins": {
|
|
39
43
|
"cmd1": "echo cm1",
|
|
40
|
-
"cmd2": "echo cmd2"
|
|
44
|
+
"cmd2": { "cmd": "echo cmd2", "conemu": false },
|
|
45
|
+
"cmd3": { "cmd": "echo cmd3", "conemu": true }
|
|
41
46
|
}
|
|
42
47
|
}
|
|
43
48
|
```
|
|
@@ -104,13 +109,15 @@ It's recommended to gitignore "my_wins_personal.json".
|
|
|
104
109
|
```json
|
|
105
110
|
{
|
|
106
111
|
"startTimeout": 700,
|
|
112
|
+
"conemu": "D:\\ProgsReady\\ConEmu\\ConEmu64.exe",
|
|
107
113
|
"x": 0,
|
|
108
114
|
"y": 0,
|
|
109
115
|
"height": 120,
|
|
110
116
|
"width": 500,
|
|
111
117
|
"wins": {
|
|
112
118
|
"foo":"my command line 1",
|
|
113
|
-
"baz":{"no_run":true, "cmd":"my command line 2"},
|
|
119
|
+
"baz":{"no_run":true, "cmd":"my command line 2", "conemu":false},
|
|
120
|
+
"qux":{"cmd":"my command line 3", "conemu":true},
|
|
114
121
|
"webstorm": {"app":true, "cmd":"\"C:\\Program Files\\JetBrains\\WebStorm 2019.2\\bin\\webstorm64.exe\""} // notice escaped quotes !
|
|
115
122
|
// "commented": "Comments and trailing commas are supported!",
|
|
116
123
|
},
|
|
@@ -118,12 +125,16 @@ It's recommended to gitignore "my_wins_personal.json".
|
|
|
118
125
|
```
|
|
119
126
|
**x,y,height,width** *number* - is first cmd window's position and size
|
|
120
127
|
|
|
128
|
+
**conemu** *string* - optional full path to `ConEmu.exe` or `ConEmu64.exe`. When present, console commands use ConEmu by default and are sent through ConEmu GuiMacro instead of the window key sender.
|
|
129
|
+
ConEmu tabs keep their visible title from the win `title` field even if commands later change the internal console title.
|
|
130
|
+
|
|
121
131
|
**y** gets incremented by **height** for each next window
|
|
122
132
|
|
|
123
133
|
**wins** *object*- your windows
|
|
124
134
|
|
|
125
135
|
- if you enter a string it resolves to `{"cmd":"string"}`
|
|
126
136
|
- **cmd** *string* - your command
|
|
137
|
+
- **conemu** *boolean* - optional per-win override. `true` forces this win into ConEmu, `false` forces it into a normal system `cmd` window.
|
|
127
138
|
- **no_run** *boolean* - types in the command, but won't hit "Enter".
|
|
128
139
|
- **app** *boolean* - runs yours command as Windows application, not as console. (Incompartible with "no_run").
|
|
129
140
|
|
package/republish.bat
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
npm publish && npm uninstall -g my_wins && npm install -g my_wins && my_wins
|
|
1
|
+
version-select && npm publish && npm uninstall -g my_wins && npm install -g my_wins && my_wins
|
package/test/run-test.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
const { spawn } = require("child_process");
|
|
1
|
+
const { spawn, execFileSync } = require("child_process");
|
|
2
2
|
const fs = require("fs");
|
|
3
3
|
const path = require("path");
|
|
4
|
+
const { shouldUseConEmuForWin } = require("../lib");
|
|
4
5
|
|
|
5
6
|
const repoRoot = path.resolve(__dirname, "..");
|
|
6
7
|
const tempDir = path.join(repoRoot, "temp");
|
|
7
|
-
const
|
|
8
|
+
const conemuTestFile = path.join(tempDir, "my_wins_conemu_test.txt");
|
|
9
|
+
const conemuPath = "D:\\ProgsReady\\ConEmu\\ConEmu64.exe";
|
|
10
|
+
const conemuTabTitle = "My Wins Visible Title";
|
|
11
|
+
const conemuRuntimeTitle = "Changed By Command";
|
|
12
|
+
const conemuBackgroundColor = "#123456";
|
|
8
13
|
|
|
9
14
|
function sleep(ms) {
|
|
10
15
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -20,29 +25,88 @@ async function waitForFile(filePath, timeoutMs) {
|
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
async function run() {
|
|
28
|
+
const fakeConEmu = { guiPath: conemuPath };
|
|
29
|
+
if (shouldUseConEmuForWin({ conemu: conemuPath }, { cmd: "echo default" }, fakeConEmu) !== true) {
|
|
30
|
+
throw new Error("Expected wins to use ConEmu by default when top-level conemu path is set");
|
|
31
|
+
}
|
|
32
|
+
if (shouldUseConEmuForWin({ conemu: conemuPath }, { cmd: "echo plain", conemu: false }, fakeConEmu) !== false) {
|
|
33
|
+
throw new Error("Expected win-level conemu:false to force a normal cmd window");
|
|
34
|
+
}
|
|
35
|
+
if (shouldUseConEmuForWin({ conemu: conemuPath }, { cmd: "echo tab", conemu: true }, fakeConEmu) !== true) {
|
|
36
|
+
throw new Error("Expected win-level conemu:true to force ConEmu");
|
|
37
|
+
}
|
|
38
|
+
if (shouldUseConEmuForWin({}, { cmd: "echo plain", conemu: true }, null) !== false) {
|
|
39
|
+
throw new Error("Expected win-level conemu:true to be ignored when no top-level ConEmu path is configured");
|
|
40
|
+
}
|
|
23
41
|
fs.mkdirSync(tempDir, { recursive: true });
|
|
24
|
-
if (fs.existsSync(
|
|
25
|
-
if (fs.existsSync(
|
|
26
|
-
throw new Error(`Expected test file to be deleted: ${
|
|
42
|
+
if (fs.existsSync(conemuTestFile)) fs.unlinkSync(conemuTestFile);
|
|
43
|
+
if (fs.existsSync(conemuTestFile)) {
|
|
44
|
+
throw new Error(`Expected test file to be deleted: ${conemuTestFile}`);
|
|
27
45
|
}
|
|
28
46
|
|
|
29
|
-
|
|
30
|
-
|
|
47
|
+
if (!fs.existsSync(conemuPath)) {
|
|
48
|
+
console.log(`Skipping ConEmu integration test because ConEmu was not found at ${conemuPath}`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const conemuProjectDir = path.join(tempDir, "conemu-project");
|
|
53
|
+
fs.mkdirSync(conemuProjectDir, { recursive: true });
|
|
54
|
+
fs.writeFileSync(
|
|
55
|
+
path.join(conemuProjectDir, "my_wins.json"),
|
|
56
|
+
JSON.stringify(
|
|
57
|
+
{
|
|
58
|
+
conemu: conemuPath,
|
|
59
|
+
startTimeout: 2200,
|
|
60
|
+
wins: {
|
|
61
|
+
test_conemu: {
|
|
62
|
+
title: conemuTabTitle,
|
|
63
|
+
backgroundcolor: conemuBackgroundColor,
|
|
64
|
+
cmd: `title ${conemuRuntimeTitle} && echo my_wins conemu test> ${conemuTestFile} && ping -n 12 127.0.0.1 > nul`,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
null,
|
|
69
|
+
2,
|
|
70
|
+
),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const conemuChild = spawn("node", [path.join(repoRoot, "index.js"), "--run", "test_conemu"], {
|
|
74
|
+
cwd: conemuProjectDir,
|
|
31
75
|
stdio: "inherit",
|
|
32
76
|
});
|
|
33
77
|
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
78
|
+
const conemuExitCode = await new Promise((resolve, reject) => {
|
|
79
|
+
conemuChild.on("error", reject);
|
|
80
|
+
conemuChild.on("close", resolve);
|
|
37
81
|
});
|
|
38
82
|
|
|
39
|
-
if (
|
|
40
|
-
throw new Error(`my_wins exited with code ${
|
|
83
|
+
if (conemuExitCode !== 0) {
|
|
84
|
+
throw new Error(`my_wins ConEmu run exited with code ${conemuExitCode}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const conemuCreated = await waitForFile(conemuTestFile, 10000);
|
|
88
|
+
if (!conemuCreated) {
|
|
89
|
+
throw new Error(`Expected ConEmu test file to exist: ${conemuTestFile}`);
|
|
41
90
|
}
|
|
42
91
|
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
92
|
+
const macroPath = path.join(path.dirname(conemuPath), "ConEmu", "ConEmuC64.exe");
|
|
93
|
+
const guiPidsRaw = execFileSync("powershell", [
|
|
94
|
+
"-NoProfile",
|
|
95
|
+
"-Command",
|
|
96
|
+
"(Get-Process ConEmu64,ConEmu -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne 0 -and $_.MainWindowTitle -notlike 'About ConEmu*' } | Sort-Object Id -Descending | Select-Object -ExpandProperty Id)",
|
|
97
|
+
], { encoding: "utf8" }).trim();
|
|
98
|
+
const guiPids = guiPidsRaw
|
|
99
|
+
.split(/\r?\n/)
|
|
100
|
+
.map((line) => line.trim())
|
|
101
|
+
.filter(Boolean);
|
|
102
|
+
const tabsByPid = guiPids.map((guiPid) => ({
|
|
103
|
+
guiPid,
|
|
104
|
+
tabs: execFileSync(macroPath, [`-GuiMacro:${guiPid}`, "Tab(12)"], { encoding: "utf8" }),
|
|
105
|
+
}));
|
|
106
|
+
const matchingWindow = tabsByPid.find((entry) => entry.tabs.includes(conemuTabTitle));
|
|
107
|
+
if (!matchingWindow) {
|
|
108
|
+
const allTabs = tabsByPid.map((entry) => `[${entry.guiPid}]\n${entry.tabs}`).join("\n---\n");
|
|
109
|
+
throw new Error(`Expected some ConEmu tabs list to include visible title '${conemuTabTitle}', got: ${allTabs}`);
|
|
46
110
|
}
|
|
47
111
|
}
|
|
48
112
|
|