mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-31 16:33:34 +08:00
fix: open Windows external targets reliably
This commit is contained in:
parent
83a96ef13d
commit
9fbfd906d0
@ -121,6 +121,38 @@ describe('openTargetService', () => {
|
|||||||
expect(result.targets[0]?.iconUrl).toBe('/api/open-targets/icons/explorer')
|
expect(result.targets[0]?.iconUrl).toBe('/api/open-targets/icons/explorer')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('does not include stale Windows command shims without the app executable', async () => {
|
||||||
|
const { service } = createService('win32', {
|
||||||
|
commandPaths: {
|
||||||
|
'code.cmd': 'C:\\Users\\nanmi\\AppData\\Local\\Programs\\Microsoft VS Code\\bin\\code.cmd',
|
||||||
|
'goland.cmd': 'C:\\Users\\nanmi\\AppData\\Local\\JetBrains\\Toolbox\\scripts\\goland.cmd',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await service.listTargets()
|
||||||
|
|
||||||
|
expect(result.targets.map((target) => target.id)).toEqual(['explorer'])
|
||||||
|
expect(result.primaryTargetId).toBe('explorer')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('detects Windows IDEs only when a real executable is resolved', async () => {
|
||||||
|
const commandPath = 'C:\\Users\\nanmi\\AppData\\Local\\Programs\\Microsoft VS Code\\bin\\code.cmd'
|
||||||
|
const executablePath = 'C:\\Users\\nanmi\\AppData\\Local\\Programs\\Microsoft VS Code\\Code.exe'
|
||||||
|
const { service } = createService('win32', {
|
||||||
|
commandPaths: {
|
||||||
|
'code.cmd': commandPath,
|
||||||
|
},
|
||||||
|
paths: {
|
||||||
|
[executablePath]: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await service.listTargets()
|
||||||
|
|
||||||
|
expect(result.targets.map((target) => target.id)).toEqual(['vscode', 'explorer'])
|
||||||
|
expect(result.primaryTargetId).toBe('vscode')
|
||||||
|
})
|
||||||
|
|
||||||
it('only includes the Linux file-manager fallback when xdg-open is available', async () => {
|
it('only includes the Linux file-manager fallback when xdg-open is available', async () => {
|
||||||
const withoutXdg = createService('linux')
|
const withoutXdg = createService('linux')
|
||||||
expect((await withoutXdg.service.listTargets()).targets).toEqual([])
|
expect((await withoutXdg.service.listTargets()).targets).toEqual([])
|
||||||
@ -213,6 +245,41 @@ describe('openTargetService', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('opens Windows command-shim targets through the resolved executable', async () => {
|
||||||
|
const dir = await makeDir()
|
||||||
|
const commandPath = 'C:\\Users\\nanmi\\AppData\\Local\\Programs\\Microsoft VS Code\\bin\\code.cmd'
|
||||||
|
const executablePath = 'C:\\Users\\nanmi\\AppData\\Local\\Programs\\Microsoft VS Code\\Code.exe'
|
||||||
|
const { service, launched } = createService('win32', {
|
||||||
|
commandPaths: {
|
||||||
|
'code.cmd': commandPath,
|
||||||
|
},
|
||||||
|
paths: {
|
||||||
|
[executablePath]: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
await service.openTarget({ targetId: 'vscode', path: dir })
|
||||||
|
|
||||||
|
expect(launched).toEqual([{ command: executablePath, args: [dir] }])
|
||||||
|
} finally {
|
||||||
|
await rm(dir, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('opens Windows Explorer through the file-manager fallback', async () => {
|
||||||
|
const dir = await makeDir()
|
||||||
|
const { service, launched } = createService('win32')
|
||||||
|
|
||||||
|
try {
|
||||||
|
await service.openTarget({ targetId: 'explorer', path: dir })
|
||||||
|
|
||||||
|
expect(launched).toEqual([{ command: 'cmd.exe', args: ['/d', '/c', 'start', '', dir] }])
|
||||||
|
} finally {
|
||||||
|
await rm(dir, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
it('reports launch failures instead of returning success', async () => {
|
it('reports launch failures instead of returning success', async () => {
|
||||||
const dir = await makeDir()
|
const dir = await makeDir()
|
||||||
const { service } = createService('darwin', {
|
const { service } = createService('darwin', {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { execFile as execFileCallback } from 'node:child_process'
|
import { execFile as execFileCallback, spawn } from 'node:child_process'
|
||||||
import { mkdtemp, readFile, readdir, rm, stat } from 'node:fs/promises'
|
import { mkdtemp, readFile, readdir, rm, stat } from 'node:fs/promises'
|
||||||
import { homedir, tmpdir } from 'node:os'
|
import { homedir, tmpdir } from 'node:os'
|
||||||
import { basename, extname, join, resolve, win32 as winPath } from 'node:path'
|
import { extname, join, posix as posixPath, resolve, win32 as winPath } from 'node:path'
|
||||||
import { promisify } from 'node:util'
|
import { promisify } from 'node:util'
|
||||||
import { ApiError } from '../middleware/errorHandler.js'
|
import { ApiError } from '../middleware/errorHandler.js'
|
||||||
|
|
||||||
@ -88,7 +88,7 @@ const TARGET_DEFINITIONS: TargetDefinition[] = [
|
|||||||
appPaths: {
|
appPaths: {
|
||||||
darwin: [
|
darwin: [
|
||||||
'/Applications/Visual Studio Code.app',
|
'/Applications/Visual Studio Code.app',
|
||||||
join(homedir(), 'Applications', 'Visual Studio Code.app'),
|
posixPath.join(homedir(), 'Applications', 'Visual Studio Code.app'),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -105,7 +105,7 @@ const TARGET_DEFINITIONS: TargetDefinition[] = [
|
|||||||
},
|
},
|
||||||
windowsExecutableNames: ['Cursor.exe'],
|
windowsExecutableNames: ['Cursor.exe'],
|
||||||
appPaths: {
|
appPaths: {
|
||||||
darwin: ['/Applications/Cursor.app', join(homedir(), 'Applications', 'Cursor.app')],
|
darwin: ['/Applications/Cursor.app', posixPath.join(homedir(), 'Applications', 'Cursor.app')],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -121,7 +121,7 @@ const TARGET_DEFINITIONS: TargetDefinition[] = [
|
|||||||
},
|
},
|
||||||
windowsExecutableNames: ['sublime_text.exe', 'subl.exe'],
|
windowsExecutableNames: ['sublime_text.exe', 'subl.exe'],
|
||||||
appPaths: {
|
appPaths: {
|
||||||
darwin: ['/Applications/Sublime Text.app', join(homedir(), 'Applications', 'Sublime Text.app')],
|
darwin: ['/Applications/Sublime Text.app', posixPath.join(homedir(), 'Applications', 'Sublime Text.app')],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -134,7 +134,7 @@ const TARGET_DEFINITIONS: TargetDefinition[] = [
|
|||||||
darwin: ['antigravity'],
|
darwin: ['antigravity'],
|
||||||
},
|
},
|
||||||
appPaths: {
|
appPaths: {
|
||||||
darwin: ['/Applications/Antigravity.app', join(homedir(), 'Applications', 'Antigravity.app')],
|
darwin: ['/Applications/Antigravity.app', posixPath.join(homedir(), 'Applications', 'Antigravity.app')],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -150,7 +150,7 @@ const TARGET_DEFINITIONS: TargetDefinition[] = [
|
|||||||
},
|
},
|
||||||
windowsExecutableNames: ['goland64.exe', 'goland.exe'],
|
windowsExecutableNames: ['goland64.exe', 'goland.exe'],
|
||||||
appPaths: {
|
appPaths: {
|
||||||
darwin: ['/Applications/GoLand.app', join(homedir(), 'Applications', 'GoLand.app')],
|
darwin: ['/Applications/GoLand.app', posixPath.join(homedir(), 'Applications', 'GoLand.app')],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -166,7 +166,7 @@ const TARGET_DEFINITIONS: TargetDefinition[] = [
|
|||||||
},
|
},
|
||||||
windowsExecutableNames: ['pycharm64.exe', 'pycharm.exe'],
|
windowsExecutableNames: ['pycharm64.exe', 'pycharm.exe'],
|
||||||
appPaths: {
|
appPaths: {
|
||||||
darwin: ['/Applications/PyCharm.app', join(homedir(), 'Applications', 'PyCharm.app')],
|
darwin: ['/Applications/PyCharm.app', posixPath.join(homedir(), 'Applications', 'PyCharm.app')],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -201,13 +201,13 @@ const TARGET_DEFINITIONS: TargetDefinition[] = [
|
|||||||
const LINUX_APPLICATION_DIRS = [
|
const LINUX_APPLICATION_DIRS = [
|
||||||
'/usr/share/applications',
|
'/usr/share/applications',
|
||||||
'/usr/local/share/applications',
|
'/usr/local/share/applications',
|
||||||
join(homedir(), '.local', 'share', 'applications'),
|
posixPath.join(homedir(), '.local', 'share', 'applications'),
|
||||||
'/var/lib/flatpak/exports/share/applications',
|
'/var/lib/flatpak/exports/share/applications',
|
||||||
join(homedir(), '.local', 'share', 'flatpak', 'exports', 'share', 'applications'),
|
posixPath.join(homedir(), '.local', 'share', 'flatpak', 'exports', 'share', 'applications'),
|
||||||
]
|
]
|
||||||
|
|
||||||
const LINUX_ICON_ROOTS = [
|
const LINUX_ICON_ROOTS = [
|
||||||
join(homedir(), '.local', 'share', 'icons'),
|
posixPath.join(homedir(), '.local', 'share', 'icons'),
|
||||||
'/usr/local/share/icons',
|
'/usr/local/share/icons',
|
||||||
'/usr/share/icons',
|
'/usr/share/icons',
|
||||||
]
|
]
|
||||||
@ -261,29 +261,98 @@ async function defaultPathExists(targetPath: string): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function defaultLaunch(command: string, args: string[]): Promise<OpenTargetLaunchResult> {
|
async function defaultLaunch(command: string, args: string[]): Promise<OpenTargetLaunchResult> {
|
||||||
try {
|
return await new Promise((resolveLaunch) => {
|
||||||
const { stdout, stderr } = await execFile(command, args, {
|
let settled = false
|
||||||
timeout: 10_000,
|
const settle = (result: OpenTargetLaunchResult) => {
|
||||||
windowsHide: true,
|
if (settled) return
|
||||||
})
|
settled = true
|
||||||
return {
|
resolveLaunch(result)
|
||||||
code: 0,
|
|
||||||
stdout: String(stdout ?? ''),
|
|
||||||
stderr: String(stderr ?? ''),
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
const err = error as {
|
try {
|
||||||
code?: unknown
|
const child = spawn(command, args, {
|
||||||
stdout?: unknown
|
detached: true,
|
||||||
stderr?: unknown
|
stdio: 'ignore',
|
||||||
message?: string
|
windowsHide: true,
|
||||||
}
|
})
|
||||||
return {
|
|
||||||
code: typeof err.code === 'number' ? err.code : 1,
|
child.once('error', (error) => {
|
||||||
stdout: String(err.stdout ?? ''),
|
settle({
|
||||||
stderr: String(err.stderr ?? err.message ?? ''),
|
code: 1,
|
||||||
|
stdout: '',
|
||||||
|
stderr: error.message,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
child.once('spawn', () => {
|
||||||
|
child.unref()
|
||||||
|
settle({
|
||||||
|
code: 0,
|
||||||
|
stdout: '',
|
||||||
|
stderr: '',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as { message?: string }
|
||||||
|
settle({
|
||||||
|
code: 1,
|
||||||
|
stdout: '',
|
||||||
|
stderr: String(err.message ?? error),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveWindowsApplicationPath(
|
||||||
|
definition: TargetDefinition,
|
||||||
|
runtime: Runtime,
|
||||||
|
): Promise<string | null> {
|
||||||
|
for (const appPath of definition.appPaths?.win32 ?? []) {
|
||||||
|
if (await runtime.pathExists(appPath)) return appPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const command of definition.commands?.win32 ?? []) {
|
||||||
|
const commandPath = await runtime.resolveCommand(command)
|
||||||
|
if (!commandPath) continue
|
||||||
|
|
||||||
|
const executablePath = await resolveWindowsExecutablePath(commandPath, definition, runtime)
|
||||||
|
if (executablePath) return executablePath
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveWindowsExecutablePath(
|
||||||
|
commandPath: string,
|
||||||
|
definition: TargetDefinition,
|
||||||
|
runtime: Runtime,
|
||||||
|
): Promise<string | null> {
|
||||||
|
const extension = winPath.extname(commandPath).toLowerCase()
|
||||||
|
if (extension === '.exe' && await runtime.pathExists(commandPath)) {
|
||||||
|
return commandPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extension !== '.cmd' && extension !== '.bat') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const executableNames = definition.windowsExecutableNames
|
||||||
|
?? definition.commands?.win32?.filter((command) => winPath.extname(command).toLowerCase() === '.exe')
|
||||||
|
?? []
|
||||||
|
|
||||||
|
let currentDir = winPath.dirname(commandPath)
|
||||||
|
for (let depth = 0; depth < 5; depth += 1) {
|
||||||
|
for (const executableName of executableNames) {
|
||||||
|
const candidate = winPath.join(currentDir, executableName)
|
||||||
|
if (await runtime.pathExists(candidate)) return candidate
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextDir = winPath.dirname(currentDir)
|
||||||
|
if (!nextDir || nextDir === currentDir) break
|
||||||
|
currentDir = nextDir
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
async function defaultReadDirNames(targetPath: string): Promise<string[]> {
|
async function defaultReadDirNames(targetPath: string): Promise<string[]> {
|
||||||
@ -466,6 +535,10 @@ async function isDetected(definition: TargetDefinition, runtime: Runtime): Promi
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (runtime.platform === 'win32') {
|
||||||
|
return (await resolveWindowsApplicationPath(definition, runtime)) !== null
|
||||||
|
}
|
||||||
|
|
||||||
for (const appPath of definition.appPaths?.[runtime.platform] ?? []) {
|
for (const appPath of definition.appPaths?.[runtime.platform] ?? []) {
|
||||||
if (await runtime.pathExists(appPath)) {
|
if (await runtime.pathExists(appPath)) {
|
||||||
return true
|
return true
|
||||||
@ -495,7 +568,7 @@ async function resolveLaunchPlan(
|
|||||||
case 'darwin':
|
case 'darwin':
|
||||||
return { command: 'open', args: [targetPath] }
|
return { command: 'open', args: [targetPath] }
|
||||||
case 'win32':
|
case 'win32':
|
||||||
return { command: 'explorer.exe', args: [targetPath] }
|
return { command: 'cmd.exe', args: ['/d', '/c', 'start', '', targetPath] }
|
||||||
case 'linux':
|
case 'linux':
|
||||||
return { command: 'xdg-open', args: [targetPath] }
|
return { command: 'xdg-open', args: [targetPath] }
|
||||||
default:
|
default:
|
||||||
@ -511,6 +584,11 @@ async function resolveLaunchPlan(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (runtime.platform === 'win32') {
|
||||||
|
const applicationPath = await resolveWindowsApplicationPath(definition, runtime)
|
||||||
|
return applicationPath ? { command: applicationPath, args: [targetPath] } : null
|
||||||
|
}
|
||||||
|
|
||||||
for (const command of definition.commands?.[runtime.platform] ?? []) {
|
for (const command of definition.commands?.[runtime.platform] ?? []) {
|
||||||
if (await runtime.commandExists(command)) {
|
if (await runtime.commandExists(command)) {
|
||||||
return { command, args: [targetPath] }
|
return { command, args: [targetPath] }
|
||||||
@ -555,8 +633,8 @@ async function findDarwinBundleIconPath(
|
|||||||
definition: TargetDefinition,
|
definition: TargetDefinition,
|
||||||
runtime: Runtime,
|
runtime: Runtime,
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
const resourcesPath = join(appPath, 'Contents', 'Resources')
|
const resourcesPath = posixPath.join(appPath, 'Contents', 'Resources')
|
||||||
const plistPath = join(appPath, 'Contents', 'Info.plist')
|
const plistPath = posixPath.join(appPath, 'Contents', 'Info.plist')
|
||||||
const plistIcon = await runtime.readPlistValue(plistPath, 'CFBundleIconFile')
|
const plistIcon = await runtime.readPlistValue(plistPath, 'CFBundleIconFile')
|
||||||
|
|
||||||
const candidates = [
|
const candidates = [
|
||||||
@ -566,7 +644,7 @@ async function findDarwinBundleIconPath(
|
|||||||
].filter((value): value is string => Boolean(value))
|
].filter((value): value is string => Boolean(value))
|
||||||
|
|
||||||
for (const fileName of candidates) {
|
for (const fileName of candidates) {
|
||||||
const iconPath = join(resourcesPath, fileName)
|
const iconPath = posixPath.join(resourcesPath, fileName)
|
||||||
if (await runtime.pathExists(iconPath)) return iconPath
|
if (await runtime.pathExists(iconPath)) return iconPath
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -576,7 +654,7 @@ async function findDarwinBundleIconPath(
|
|||||||
.find((fileName) => !/document/i.test(fileName)) ?? null
|
.find((fileName) => !/document/i.test(fileName)) ?? null
|
||||||
|
|
||||||
if (!firstIcon) return null
|
if (!firstIcon) return null
|
||||||
const fallbackPath = join(resourcesPath, firstIcon)
|
const fallbackPath = posixPath.join(resourcesPath, firstIcon)
|
||||||
return await runtime.pathExists(fallbackPath) ? fallbackPath : null
|
return await runtime.pathExists(fallbackPath) ? fallbackPath : null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -610,50 +688,8 @@ async function resolveWindowsIconPath(
|
|||||||
if (await runtime.pathExists(iconPath)) return iconPath
|
if (await runtime.pathExists(iconPath)) return iconPath
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const appPath of definition.appPaths?.win32 ?? []) {
|
const applicationPath = await resolveWindowsApplicationPath(definition, runtime)
|
||||||
if (await runtime.pathExists(appPath)) return appPath
|
if (applicationPath) return applicationPath
|
||||||
}
|
|
||||||
|
|
||||||
for (const command of definition.commands?.win32 ?? []) {
|
|
||||||
const commandPath = await runtime.resolveCommand(command)
|
|
||||||
if (!commandPath) continue
|
|
||||||
|
|
||||||
const directIconPath = await resolveWindowsExecutableIconPath(commandPath, definition, runtime)
|
|
||||||
if (directIconPath) return directIconPath
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resolveWindowsExecutableIconPath(
|
|
||||||
commandPath: string,
|
|
||||||
definition: TargetDefinition,
|
|
||||||
runtime: Runtime,
|
|
||||||
): Promise<string | null> {
|
|
||||||
const extension = winPath.extname(commandPath).toLowerCase()
|
|
||||||
if ((extension === '.exe' || extension === '.ico') && await runtime.pathExists(commandPath)) {
|
|
||||||
return commandPath
|
|
||||||
}
|
|
||||||
|
|
||||||
if (extension !== '.cmd' && extension !== '.bat') {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const executableNames = definition.windowsExecutableNames
|
|
||||||
?? definition.commands?.win32?.filter((command) => winPath.extname(command).toLowerCase() === '.exe')
|
|
||||||
?? []
|
|
||||||
|
|
||||||
let currentDir = winPath.dirname(commandPath)
|
|
||||||
for (let depth = 0; depth < 5; depth += 1) {
|
|
||||||
for (const executableName of executableNames) {
|
|
||||||
const candidate = winPath.join(currentDir, executableName)
|
|
||||||
if (await runtime.pathExists(candidate)) return candidate
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextDir = winPath.dirname(currentDir)
|
|
||||||
if (!nextDir || nextDir === currentDir) break
|
|
||||||
currentDir = nextDir
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -702,7 +738,7 @@ async function findLinuxDesktopEntries(
|
|||||||
for (const fileName of fileNames) {
|
for (const fileName of fileNames) {
|
||||||
if (!fileName.endsWith('.desktop')) continue
|
if (!fileName.endsWith('.desktop')) continue
|
||||||
|
|
||||||
const filePath = join(directory, fileName)
|
const filePath = posixPath.join(directory, fileName)
|
||||||
const text = await runtime.readTextFile(filePath)
|
const text = await runtime.readTextFile(filePath)
|
||||||
if (!text) continue
|
if (!text) continue
|
||||||
|
|
||||||
@ -757,7 +793,7 @@ function linuxDesktopEntryMatchesDefinition(
|
|||||||
definition: TargetDefinition,
|
definition: TargetDefinition,
|
||||||
): boolean {
|
): boolean {
|
||||||
const commandNames = new Set(
|
const commandNames = new Set(
|
||||||
(definition.commands?.linux ?? []).map((command) => basename(command).toLowerCase()),
|
(definition.commands?.linux ?? []).map((command) => posixPath.basename(command).toLowerCase()),
|
||||||
)
|
)
|
||||||
const normalizedNeedles = [
|
const normalizedNeedles = [
|
||||||
definition.id,
|
definition.id,
|
||||||
@ -790,7 +826,7 @@ function extractLinuxDesktopExecCommand(execValue: string): string | null {
|
|||||||
|
|
||||||
for (const token of tokens) {
|
for (const token of tokens) {
|
||||||
if (!token || token.includes('=')) continue
|
if (!token || token.includes('=')) continue
|
||||||
const command = basename(token).toLowerCase()
|
const command = posixPath.basename(token).toLowerCase()
|
||||||
if (command === 'env') continue
|
if (command === 'env') continue
|
||||||
return command
|
return command
|
||||||
}
|
}
|
||||||
@ -816,11 +852,11 @@ async function resolveLinuxIconName(
|
|||||||
const directRoots = [
|
const directRoots = [
|
||||||
'/usr/share/pixmaps',
|
'/usr/share/pixmaps',
|
||||||
'/usr/local/share/pixmaps',
|
'/usr/local/share/pixmaps',
|
||||||
join(homedir(), '.local', 'share', 'pixmaps'),
|
posixPath.join(homedir(), '.local', 'share', 'pixmaps'),
|
||||||
]
|
]
|
||||||
for (const root of directRoots) {
|
for (const root of directRoots) {
|
||||||
for (const candidateExtension of extensions) {
|
for (const candidateExtension of extensions) {
|
||||||
const candidate = join(root, `${baseName}${candidateExtension}`)
|
const candidate = posixPath.join(root, `${baseName}${candidateExtension}`)
|
||||||
if (await runtime.pathExists(candidate)) return candidate
|
if (await runtime.pathExists(candidate)) return candidate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -830,7 +866,7 @@ async function resolveLinuxIconName(
|
|||||||
for (const themeName of themeNames) {
|
for (const themeName of themeNames) {
|
||||||
for (const subdir of LINUX_ICON_THEME_SUBDIRS) {
|
for (const subdir of LINUX_ICON_THEME_SUBDIRS) {
|
||||||
for (const candidateExtension of extensions) {
|
for (const candidateExtension of extensions) {
|
||||||
const candidate = join(root, themeName, subdir, `${baseName}${candidateExtension}`)
|
const candidate = posixPath.join(root, themeName, subdir, `${baseName}${candidateExtension}`)
|
||||||
if (await runtime.pathExists(candidate)) return candidate
|
if (await runtime.pathExists(candidate)) return candidate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user