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 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.title = w.name+randSuffix()
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, title, app, no_run, cmd} = w;
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 ${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, title, app, cmd, no_run, delay} = w;
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(title));
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 (!win_info) console.warn(`CODE00000000 Couldn't find window ${title}`);
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 '${title}'. Only one window expected!`);
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
- "name": "my_wins",
3
- "version": "1.4.0",
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
- }
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 testFile = path.join(tempDir, "my_wins_test.txt");
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(testFile)) fs.unlinkSync(testFile);
25
- if (fs.existsSync(testFile)) {
26
- throw new Error(`Expected test file to be deleted: ${testFile}`);
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
- const child = spawn("node", ["index.js", "--run", "test_file"], {
30
- cwd: repoRoot,
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 exitCode = await new Promise((resolve, reject) => {
35
- child.on("error", reject);
36
- child.on("close", resolve);
78
+ const conemuExitCode = await new Promise((resolve, reject) => {
79
+ conemuChild.on("error", reject);
80
+ conemuChild.on("close", resolve);
37
81
  });
38
82
 
39
- if (exitCode !== 0) {
40
- throw new Error(`my_wins exited with code ${exitCode}`);
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 created = await waitForFile(testFile, 8000);
44
- if (!created) {
45
- throw new Error(`Expected test file to exist: ${testFile}`);
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