refactor(native-command): require explicit window visibility
This commit is contained in:
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')
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
@@ -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 }), {
|
||||
|
||||
Reference in new issue
Block a user