refactor(native-command): require explicit window visibility

This commit is contained in:
HYD@OVERTON authored and Yudong Han committed 2026-09-27 23:55:26 +08:00
1 parent a895066443
commit 989fc3e2f5
18 files changed
+74 -73

No files matched your search

@@ -37,10 +37,10 @@ async function desktopFile(root: string, id: string, directory = root): Promise<
export async function linuxFileApplications(
path: string, signal: AbortSignal, run: NativeCommandRunner, env: NodeJS.ProcessEnv,
): Promise<readonly NativeFileApplication[]> {
const info = await run('gio', ['info', '-a', 'standard::content-type', path], signal)
const info = await run('gio', ['info', '-a', 'standard::content-type', path], signal, 'hidden')
const mime = /standard::content-type:\s*(\S+)/.exec(info.stdout)?.[1]
if (mime === undefined) throw new Error('GIO did not identify the file content type')
const result = await run('env', ['LC_ALL=C', 'gio', 'mime', mime], signal)
const result = await run('env', ['LC_ALL=C', 'gio', 'mime', mime], signal, 'hidden')
const preferred = /^Default application.*:\s*(.+\.desktop)\s*$/m.exec(result.stdout)?.[1]
const ids = [...new Set([...(preferred === undefined ? [] : [preferred]), ...result.stdout.split(/\r?\n/)
.filter(line => /^\s+.*\.desktop\s*$/.test(line)).map(line => line.trim())])]
@@ -268,7 +268,7 @@ ${application === null ? 'ConvertTo-Json -InputObject @([DshFileAssociations]::L
await writeFile(scriptPath, `\uFEFF${script}`, 'utf8')
const result = await run('powershell.exe', [
'-NoProfile', '-NonInteractive', '-STA', '-ExecutionPolicy', 'Bypass', '-File', scriptPath,
], signal)
], signal, 'hidden')
return result.stdout
} finally {
await rm(directory, { recursive: true, force: true })
@@ -71,7 +71,7 @@ async function queryFileApplications(
const run = internals.run ?? runNativeCommand
if (target.platform === 'linux') return linuxFileApplications(path, signal, run, internals.env ?? process.env)
if (target.platform === 'darwin') {
const { stdout } = await run('osascript', ['-l', 'JavaScript', '-e', MAC_APPLICATIONS, target.path, display ? 'icons' : 'handlers'], signal)
const { stdout } = await run('osascript', ['-l', 'JavaScript', '-e', MAC_APPLICATIONS, target.path, display ? 'icons' : 'handlers'], signal, 'hidden')
const applications = parseMacApplications(JSON.parse(stdout))
return display ? dedupeMacApplications(applications) : applications
}
@@ -179,8 +179,8 @@ export async function openNativeFileApplication(
}
const apps = await queryFileApplications(path, signal, internals, false)
if (!apps.some(app => app.id === application)) throw new Error('Application is not registered for this file')
if (target.platform === 'linux') await run('gio', ['launch', application, path], signal)
else await run('open', ['-a', application, path], signal)
if (target.platform === 'linux') await run('gio', ['launch', application, path], signal, 'hidden')
else await run('open', ['-a', application, path], signal, 'hidden')
}
/** Resolve the desktop that owns the file, including Windows applications reached from WSL. */
@@ -190,7 +190,7 @@ async function desktopTarget(
signal.throwIfAborted()
const platform = internals.platform ?? process.platform
if (platform === 'linux' && nativeFileManager(internals) === 'explorer') {
const translated = await (internals.run ?? runNativeCommand)('wslpath', ['-w', path], signal)
const translated = await (internals.run ?? runNativeCommand)('wslpath', ['-w', path], signal, 'hidden')
signal.throwIfAborted()
const windowsPath = translated.stdout.replace(/[\r\n]+$/, '')
if (windowsPath === '') throw new Error('wslpath returned no Windows path')
+10 -10
View File
@@ -58,7 +58,7 @@ async function openInBrowser(
let bundle: string | undefined
try {
const { stdout } = await run(
'defaults', ['read', 'com.apple.LaunchServices/com.apple.launchservices.secure'], signal)
'defaults', ['read', 'com.apple.LaunchServices/com.apple.launchservices.secure'], signal, 'hidden')
bundle = macBundleForHttps(stdout)
} catch {
// No LaunchServices record (a fresh account never changed a default):
@@ -66,7 +66,7 @@ async function openInBrowser(
return false
}
if (bundle === undefined) return false
await run('open', ['-b', bundle, path], signal)
await run('open', ['-b', bundle, path], signal, 'hidden')
return true
}
if (platform === 'linux') {
@@ -74,7 +74,7 @@ async function openInBrowser(
// xdg-settings needs a launcher this package has no business shipping.
const browser = env.BROWSER
if (browser === undefined || browser === '') return false
await run(browser, [path], signal)
await run(browser, [path], signal, 'hidden')
return true
}
// Windows names no browser without reading the UserChoice registry, and its
@@ -136,7 +136,7 @@ function explorerTarget(windowsPath: string): string {
async function runExplorer(args: readonly string[], signal: AbortSignal, run: PathOpenerRunner): Promise<void> {
try {
// SW_HIDE also hides Explorer's folder window, not just console windows.
await run('explorer.exe', args, signal, { windowsHide: false })
await run('explorer.exe', args, signal, 'visible')
} catch (error: unknown) {
signal.throwIfAborted()
// Explorer can exit 1 after delegating to the existing desktop process.
@@ -157,7 +157,7 @@ async function openWindowsPath(path: string, signal: AbortSignal, run: PathOpene
/** Translate a WSL path before handing it to the Windows desktop. */
async function openWslPath(path: string, signal: AbortSignal, run: PathOpenerRunner): Promise<void> {
const translated = await run('wslpath', ['-w', path], signal)
const translated = await run('wslpath', ['-w', path], signal, 'hidden')
signal.throwIfAborted()
const windowsPath = translated.stdout.replace(/[\r\n]+$/, '')
if (windowsPath === '') throw new Error('wslpath returned no Windows path')
@@ -180,7 +180,7 @@ async function openNativePathWithIntent(
&& await openInBrowser(path, signal, platform, run, env)) return
if (platform === 'darwin') {
await run('open', intent === 'text-editor' ? ['-t', path] : [path], signal)
await run('open', intent === 'text-editor' ? ['-t', path] : [path], signal, 'hidden')
return
}
@@ -194,7 +194,7 @@ async function openNativePathWithIntent(
await openWslPath(path, signal, run)
return
}
await run('xdg-open', [path], signal)
await run('xdg-open', [path], signal, 'hidden')
return
}
@@ -293,13 +293,13 @@ export async function revealNativePath(
const run = internals.run ?? runNativeCommand
const manager = nativeFileManager({ ...internals, platform })
if (manager === 'finder') {
await run('open', ['-R', path], signal)
await run('open', ['-R', path], signal, 'hidden')
return
}
if (manager === 'explorer') {
let windowsPath = path
if (platform === 'linux') {
const translated = await run('wslpath', ['-w', path], signal)
const translated = await run('wslpath', ['-w', path], signal, 'hidden')
signal.throwIfAborted()
windowsPath = translated.stdout.replace(/[\r\n]+$/, '')
if (windowsPath === '') throw new Error('wslpath returned no Windows path')
@@ -308,7 +308,7 @@ export async function revealNativePath(
return
}
if (manager === 'directory') {
await run('xdg-open', [dirname(path)], signal)
await run('xdg-open', [dirname(path)], signal, 'hidden')
return
}
throw new Error(`native file manager is unsupported on ${platform}`)
+5 -4
View File
@@ -10,7 +10,7 @@ export type NativeCommandRunner = (
command: string,
args: readonly string[],
signal: AbortSignal,
options?: { windowsHide: boolean },
window: 'hidden' | 'visible',
) => Promise<{ stdout: string; stderr: string }>
/**
@@ -18,15 +18,16 @@ export type NativeCommandRunner = (
* @param command - executable path or PATH name.
* @param args - argv (never a shell string).
* @param signal - caller/connection lifetime; abort terminates the child.
* @param options - Windows startup visibility; consoles are hidden by default, GUI launchers must pass `windowsHide: false`.
* @param window - Windows startup visibility: `hidden` for background commands,
* `visible` for GUI launchers. Ignored on other platforms.
* @returns captured stdout/stderr on exit 0.
*/
export const runNativeCommand: NativeCommandRunner = (command, args, signal, options = { windowsHide: true }) =>
export const runNativeCommand: NativeCommandRunner = (command, args, signal, window) =>
new Promise((resolve, reject) => {
execFile(
command,
[...args],
{ encoding: 'utf8', signal, windowsHide: options.windowsHide },
{ encoding: 'utf8', signal, windowsHide: window === 'hidden' },
(error, stdout, stderr) => {
if (error !== null) {
const failure = Object.assign(new Error(error.message, { cause: error }), {