mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
fix(desktop): open project folders in Windows Explorer on WSL (#1581)
Route Continue in CLI directory opens through wslpath + explorer.exe on WSL Linux instead of shell.openPath, which often launches Chrome at a file:// listing via xdg-open.
This commit is contained in:
parent
a6a56099ca
commit
f9d852749f
3 changed files with 118 additions and 1 deletions
54
apps/desktop/src/main/open-project-directory.ts
Normal file
54
apps/desktop/src/main/open-project-directory.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { execFile, type ExecFileOptionsWithStringEncoding } from "node:child_process";
|
||||
import os from "node:os";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { shell } from "electron";
|
||||
|
||||
const execFileAsync = promisify(execFile) as (
|
||||
file: string,
|
||||
args: readonly string[],
|
||||
options: ExecFileOptionsWithStringEncoding,
|
||||
) => Promise<{ stdout: string; stderr: string }>;
|
||||
|
||||
export function isWslLinux(
|
||||
release = os.release(),
|
||||
platform = process.platform,
|
||||
): boolean {
|
||||
return platform === "linux" && release.toLowerCase().includes("microsoft");
|
||||
}
|
||||
|
||||
export type OpenProjectDirectoryDeps = {
|
||||
execFile?: typeof execFileAsync;
|
||||
isWsl?: () => boolean;
|
||||
openPath?: (path: string) => Promise<string>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Open a validated project directory in the host file manager. On WSL Linux,
|
||||
* `shell.openPath` often delegates to Chrome via xdg-open; route through the
|
||||
* Windows host Explorer instead (issue #1581).
|
||||
*/
|
||||
export async function openProjectDirectoryInFileManager(
|
||||
resolvedDir: string,
|
||||
deps: OpenProjectDirectoryDeps = {},
|
||||
): Promise<string> {
|
||||
const exec = deps.execFile ?? execFileAsync;
|
||||
const openPath = deps.openPath ?? ((path: string) => shell.openPath(path));
|
||||
const isWsl = deps.isWsl ?? (() => isWslLinux());
|
||||
|
||||
if (!isWsl()) {
|
||||
return openPath(resolvedDir);
|
||||
}
|
||||
|
||||
try {
|
||||
const { stdout } = await exec("wslpath", ["-w", resolvedDir], { timeout: 5000 });
|
||||
const winPath = stdout.trim();
|
||||
if (!winPath) {
|
||||
return "wslpath returned an empty Windows path";
|
||||
}
|
||||
await exec("explorer.exe", [winPath], { timeout: 5000 });
|
||||
return "";
|
||||
} catch (err) {
|
||||
return err instanceof Error ? err.message : String(err);
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ import {
|
|||
import type { OpenDesignHostActionResult, OpenDesignHostUpdaterActionOptions } from "@open-design/host";
|
||||
|
||||
import { createElectronPdfTarget, exportPdfFromHtml, savePrintReadyDocumentAsPdf } from "./pdf-export.js";
|
||||
import { openProjectDirectoryInFileManager } from "./open-project-directory.js";
|
||||
import type { PrintReadyPdfOptions } from "./pdf-export.js";
|
||||
import type { DesktopUpdater } from "./updater.js";
|
||||
|
||||
|
|
@ -1198,7 +1199,7 @@ export async function createDesktopRuntime(options: DesktopRuntimeOptions): Prom
|
|||
const validated = await validateExistingDirectory(resolved.context.resolvedDir);
|
||||
if (!validated.ok) return `open-path: ${validated.reason}`;
|
||||
try {
|
||||
return await shell.openPath(validated.resolved);
|
||||
return await openProjectDirectoryInFileManager(validated.resolved);
|
||||
} catch (err) {
|
||||
return err instanceof Error ? err.message : String(err);
|
||||
}
|
||||
|
|
|
|||
62
apps/desktop/tests/main/open-project-directory.test.ts
Normal file
62
apps/desktop/tests/main/open-project-directory.test.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import {
|
||||
isWslLinux,
|
||||
openProjectDirectoryInFileManager,
|
||||
} from "../../src/main/open-project-directory.js";
|
||||
|
||||
describe("isWslLinux", () => {
|
||||
it("detects WSL kernels via microsoft in the release string", () => {
|
||||
expect(isWslLinux("5.15.133.1-microsoft-standard-WSL2", "linux")).toBe(true);
|
||||
expect(isWslLinux("6.8.0-45-generic", "linux")).toBe(false);
|
||||
expect(isWslLinux("5.15.133.1-microsoft-standard-WSL2", "darwin")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("openProjectDirectoryInFileManager", () => {
|
||||
it("uses shell.openPath on native Linux", async () => {
|
||||
const openPath = vi.fn(async () => "");
|
||||
const execFile = vi.fn();
|
||||
|
||||
const result = await openProjectDirectoryInFileManager("/tmp/project", {
|
||||
isWsl: () => false,
|
||||
openPath,
|
||||
execFile,
|
||||
});
|
||||
|
||||
expect(result).toBe("");
|
||||
expect(openPath).toHaveBeenCalledWith("/tmp/project");
|
||||
expect(execFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("routes through wslpath and explorer.exe on WSL (#1581)", async () => {
|
||||
const execFile = vi.fn(async (cmd: string, args: readonly string[]) => {
|
||||
if (cmd === "wslpath") {
|
||||
return { stdout: "C:\\Users\\me\\project\r\n", stderr: "" };
|
||||
}
|
||||
return { stdout: "", stderr: "" };
|
||||
});
|
||||
const openPath = vi.fn();
|
||||
|
||||
const result = await openProjectDirectoryInFileManager("/home/me/project", {
|
||||
isWsl: () => true,
|
||||
execFile,
|
||||
openPath,
|
||||
});
|
||||
|
||||
expect(result).toBe("");
|
||||
expect(execFile).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"wslpath",
|
||||
["-w", "/home/me/project"],
|
||||
{ timeout: 5000 },
|
||||
);
|
||||
expect(execFile).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"explorer.exe",
|
||||
["C:\\Users\\me\\project"],
|
||||
{ timeout: 5000 },
|
||||
);
|
||||
expect(openPath).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue