my_wins 1.5.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
@@ -61,6 +61,13 @@ function toPowerShellSingleQuotedString(value) {
61
61
  return `'${String(value).replace(/'/g, "''")}'`;
62
62
  }
63
63
 
64
+ function buildConEmuCreateTabCommand(win) {
65
+ const titleCommand = `title ${win.matchTitle}`;
66
+ return {
67
+ titleCommand,
68
+ };
69
+ }
70
+
64
71
  function getConEmuInfo(settings) {
65
72
  if (!settings.conemu) return null;
66
73
 
@@ -94,24 +101,25 @@ function shouldUseConEmuForWin(settings, win, conemuInfo) {
94
101
  return true;
95
102
  }
96
103
 
97
- async function execConEmuMacro(conemu, macro) {
104
+ async function execConEmuMacro(conemu, macro, tabIndex) {
98
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}`;
99
107
  const command = [
100
108
  "&",
101
109
  toPowerShellSingleQuotedString(conemu.macroPath),
102
- toPowerShellSingleQuotedString(`-GuiMacro:${conemu.guiPid}`),
110
+ toPowerShellSingleQuotedString(guiMacroTarget),
103
111
  toPowerShellSingleQuotedString(macro),
104
112
  ].join(" ");
105
113
  const result = await execFileAsync("powershell", ["-NoProfile", "-Command", command]);
106
114
  return (result.stdout || "").trim();
107
115
  }
108
116
 
109
- async function execConEmuCommand(conemu, command, commandArgs) {
117
+ async function execConEmuCommand(conemu, command, commandArgs, tabIndex) {
110
118
  const serializedArgs = (commandArgs || []).map((arg)=>{
111
119
  if (typeof arg === "number") return String(arg);
112
120
  return `"${String(arg).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
113
121
  });
114
- return execConEmuMacro(conemu, `${command}(${serializedArgs.join(",")})`);
122
+ return execConEmuMacro(conemu, `${command}(${serializedArgs.join(",")})`, tabIndex);
115
123
  }
116
124
 
117
125
  async function findConEmuGuiPidByActiveTitle(title) {
@@ -178,7 +186,7 @@ async function waitForConEmuGuiPid(title, timeoutMs, beforePids) {
178
186
  }
179
187
 
180
188
  async function launchConEmuTab(conemu, win, isFirstTab) {
181
- const titleCommand = `title ${win.tabTitle}`;
189
+ const { titleCommand } = buildConEmuCreateTabCommand(win);
182
190
  if (isFirstTab) {
183
191
  const beforePids = await listConEmuGuiPids();
184
192
  const beforePid = beforePids.length ? beforePids[beforePids.length - 1] : null;
@@ -191,14 +199,18 @@ async function launchConEmuTab(conemu, win, isFirstTab) {
191
199
  child.unref();
192
200
  conemu.guiPid = await waitForConEmuGuiPid(null, 10000, beforePids);
193
201
  const afterTabs = await listConEmuTabsByPid(conemu.guiPid, conemu.macroPath);
194
- return afterTabs.length > beforeTabs.length ? afterTabs.length : afterTabs.length;
202
+ const tabIndex = afterTabs.length > beforeTabs.length ? afterTabs.length : afterTabs.length;
203
+ await renameConEmuTab(conemu, tabIndex, win.tabTitle);
204
+ return tabIndex;
195
205
  }
196
206
 
197
207
  const beforeTabs = await listConEmuTabsByPid(conemu.guiPid, conemu.macroPath);
198
208
  await execConEmuCommand(conemu, "Shell", ["new_console", "cmd.exe", `/k ${titleCommand}`]);
199
209
  await awaitDelay(300);
200
210
  const afterTabs = await listConEmuTabsByPid(conemu.guiPid, conemu.macroPath);
201
- return afterTabs.length > beforeTabs.length ? afterTabs.length : afterTabs.length;
211
+ const tabIndex = afterTabs.length > beforeTabs.length ? afterTabs.length : afterTabs.length;
212
+ await renameConEmuTab(conemu, tabIndex, win.tabTitle);
213
+ return tabIndex;
202
214
  }
203
215
 
204
216
  async function activateConEmuTab(conemu, tabIndex) {
@@ -206,22 +218,33 @@ async function activateConEmuTab(conemu, tabIndex) {
206
218
  await awaitDelay(150);
207
219
  }
208
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
+
209
235
  async function setConEmuBackgroundColor(conemu, tabIndex, backgroundcolor) {
210
236
  if (!backgroundcolor) return;
211
- await activateConEmuTab(conemu, tabIndex);
212
- await execConEmuCommand(conemu, "SetOption", ["bgImage", backgroundcolor]);
237
+ await execConEmuCommand(conemu, "SetOption", ["bgImage", backgroundcolor], tabIndex);
213
238
  await awaitDelay(150);
214
239
  }
215
240
 
216
241
  async function printInConEmuTab(conemu, tabIndex, cmd) {
217
- await activateConEmuTab(conemu, tabIndex);
218
- await execConEmuCommand(conemu, "Print", [cmd]);
242
+ await execConEmuCommand(conemu, "Print", [cmd], tabIndex);
219
243
  await awaitDelay(200);
220
244
  }
221
245
 
222
246
  async function pressEnterInConEmuTab(conemu, tabIndex) {
223
- await activateConEmuTab(conemu, tabIndex);
224
- await execConEmuCommand(conemu, "Keys", ["Enter"]);
247
+ await execConEmuCommand(conemu, "Keys", ["Enter"], tabIndex);
225
248
  await awaitDelay(200);
226
249
  }
227
250
 
@@ -436,6 +459,9 @@ async function runOnce(baseSettings, filterNames) {
436
459
  await awaitDelay(delay);
437
460
  }
438
461
  await w.pressEnter();
462
+ if (shouldUseConEmuForWin(settings, w, conemu)) {
463
+ await stabilizeConEmuTabTitle(conemu, w);
464
+ }
439
465
  }
440
466
  }
441
467
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "my_wins",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "Automates starting and laying out your console windows for tsc, babel, tests, verdaccio, etc...",
5
5
  "keywords": [
6
6
  "automation",
package/readme.md CHANGED
@@ -5,6 +5,7 @@ Automates starting your console windows for tsc, babel, tests, verdaccio, etc...
5
5
  my_wins opens **cmd** promts for you, position them nicely and types-in your commands into them.
6
6
 
7
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).
8
9
 
9
10
  ## Why?
10
11
 
@@ -125,6 +126,7 @@ It's recommended to gitignore "my_wins_personal.json".
125
126
  **x,y,height,width** *number* - is first cmd window's position and size
126
127
 
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.
128
130
 
129
131
  **y** gets incremented by **height** for each next window
130
132
 
package/test/run-test.js CHANGED
@@ -8,6 +8,7 @@ const tempDir = path.join(repoRoot, "temp");
8
8
  const conemuTestFile = path.join(tempDir, "my_wins_conemu_test.txt");
9
9
  const conemuPath = "D:\\ProgsReady\\ConEmu\\ConEmu64.exe";
10
10
  const conemuTabTitle = "My Wins Visible Title";
11
+ const conemuRuntimeTitle = "Changed By Command";
11
12
  const conemuBackgroundColor = "#123456";
12
13
 
13
14
  function sleep(ms) {
@@ -60,7 +61,7 @@ async function run() {
60
61
  test_conemu: {
61
62
  title: conemuTabTitle,
62
63
  backgroundcolor: conemuBackgroundColor,
63
- cmd: `echo my_wins conemu test> ${conemuTestFile} && ping -n 6 127.0.0.1 > nul && exit`,
64
+ cmd: `title ${conemuRuntimeTitle} && echo my_wins conemu test> ${conemuTestFile} && ping -n 12 127.0.0.1 > nul`,
64
65
  },
65
66
  },
66
67
  },
@@ -89,14 +90,23 @@ async function run() {
89
90
  }
90
91
 
91
92
  const macroPath = path.join(path.dirname(conemuPath), "ConEmu", "ConEmuC64.exe");
92
- const guiPid = execFileSync("powershell", [
93
+ const guiPidsRaw = execFileSync("powershell", [
93
94
  "-NoProfile",
94
95
  "-Command",
95
- "(Get-Process ConEmu64,ConEmu -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne 0 -and $_.MainWindowTitle -notlike 'About ConEmu*' } | Sort-Object Id -Descending | Select-Object -First 1 -ExpandProperty Id)",
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)",
96
97
  ], { encoding: "utf8" }).trim();
97
- const tabs = execFileSync(macroPath, [`-GuiMacro:${guiPid}`, "Tab(12)"], { encoding: "utf8" });
98
- if (!tabs.includes(conemuTabTitle)) {
99
- throw new Error(`Expected ConEmu tabs list to include visible title '${conemuTabTitle}', got: ${tabs}`);
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}`);
100
110
  }
101
111
  }
102
112