Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
362 changes: 282 additions & 80 deletions src/core/app-search/win.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,26 @@ const startMenu = path.join(
'Microsoft\\Windows\\Start Menu\\Programs'
);

const fileLists: any = [];
interface AppInfo {
value: string;
desc: string;
type: string;
icon: string;
iconSource?: string;
pluginType: string;
action: string;
keyWords: string[];
name: string;
names: string[];
match?: number[];
}

interface StartAppEntry {
Name?: string;
AppID?: string;
}

const fileLists: AppInfo[] = [];
const isZhRegex = /[\u4e00-\u9fa5]/;

const icondir = path.join(os.tmpdir(), 'ProcessIcon');
Expand All @@ -23,104 +42,287 @@ if (!exists) {
fs.mkdirSync(icondir);
}

const getico = (app) => {
const normalizeAppName = (name: string) => {
const base = String(name || '')
.normalize('NFC')
.replace(/[<>:"/\\|?*]/g, '_')
.trim();
return Array.from(base)
.map((char) => (char.charCodeAt(0) < 32 ? '_' : char))
.join('');
};

const getIconPathByName = (name: string) =>
path.join(icondir, `${normalizeAppName(name)}.png`);

const getico = (app: AppInfo): void => {
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const fileIcon = require('extract-file-icon');
const buffer = fileIcon(app.desc, 32);
const iconpath = path.join(icondir, `${app.name}.png`);

fs.exists(iconpath, (exists) => {
if (!exists) {
fs.writeFile(iconpath, buffer, 'base64', () => {
//
});
}
});
const iconpath = getIconPathByName(app.name);
if (!fs.existsSync(iconpath)) {
fs.writeFileSync(iconpath, buffer);
}
} catch (e) {
console.log(e, app.desc);
}
};

function fileDisplay(filePath) {
//根据文件路径读取文件,返回文件列表
fs.readdir(filePath, function (err, files) {
if (err) {
console.warn(err);
} else {
files.forEach(function (filename) {
const filedir = path.join(filePath, filename);
fs.stat(filedir, function (eror, stats) {
if (eror) {
console.warn('获取文件stats失败');
} else {
const isFile = stats.isFile(); // 是文件
const isDir = stats.isDirectory(); // 是文件夹
if (isFile) {
const appName = filename.split('.')[0];
const keyWords = [appName];
let appDetail: any = {};
try {
appDetail = shell.readShortcutLink(filedir);
} catch (e) {
//
}
if (
!appDetail.target ||
appDetail.target.toLowerCase().indexOf('unin') >= 0
)
return;
function buildAppInfo(appName: string, targetPath: string): AppInfo {
const keyWords = [appName];
keyWords.push(path.basename(targetPath, '.exe'));

if (isZhRegex.test(appName)) {
// const [, pinyinArr] = translate(appName);
// const zh_firstLatter = pinyinArr.map((py) => py[0]);
// // 拼音
// keyWords.push(pinyinArr.join(''));
// 缩写
// keyWords.push(zh_firstLatter.join(''));
} else {
const firstLatter = appName
.split(' ')
.map((name) => name[0])
.join('');
keyWords.push(firstLatter);
}

const icon = getIconPathByName(appName);

const appInfo = {
value: 'plugin',
desc: targetPath,
type: 'app',
icon,
pluginType: 'app',
action: `start "dummyclient" "${targetPath}"`,
keyWords,
name: appName,
names: JSON.parse(JSON.stringify(keyWords)),
};

// C:/program/cmd.exe => cmd
keyWords.push(path.basename(appDetail.target, '.exe'));

if (isZhRegex.test(appName)) {
// const [, pinyinArr] = translate(appName);
// const zh_firstLatter = pinyinArr.map((py) => py[0]);
// // 拼音
// keyWords.push(pinyinArr.join(''));
// 缩写
// keyWords.push(zh_firstLatter.join(''));
} else {
const firstLatter = appName
.split(' ')
.map((name) => name[0])
.join('');
keyWords.push(firstLatter);
getico(appInfo);
return appInfo;
}

function buildStartAppInfo(
appName: string,
appId: string,
iconSource?: string
): AppInfo {
const keyWords = [appName];

if (!isZhRegex.test(appName)) {
const firstLatter = appName
.split(' ')
.map((name) => name[0])
.join('');
keyWords.push(firstLatter);
}

appId
.split(/[\\/.!_-]+/)
.filter(Boolean)
.forEach((token) => keyWords.push(token));

const icon = getIconPathByName(appName);

return {
value: 'plugin',
desc: appId,
type: 'app',
icon,
iconSource,
pluginType: 'app',
action: `start "" "shell:AppsFolder\\${appId}"`,
keyWords,
name: appName,
names: JSON.parse(JSON.stringify(keyWords)),
};
}

function scanStartMenuDir(dirPath: string): Promise<AppInfo[]> {
return new Promise((resolve) => {
const results: AppInfo[] = [];

const walk = (currentPath: string, done: () => void): void => {
fs.readdir(currentPath, (err, files) => {
if (err) {
done();
return;
}

let pending = files.length;
if (!pending) {
done();
return;
}

files.forEach((filename) => {
const filedir = path.join(currentPath, filename);
fs.stat(filedir, (statErr, stats) => {
if (!statErr) {
if (stats.isDirectory()) {
walk(filedir, () => {
if (--pending === 0) done();
});
return;
}

const icon = path.join(
os.tmpdir(),
'ProcessIcon',
`${encodeURIComponent(appName)}.png`
);
if (stats.isFile() && filedir.toLowerCase().endsWith('.lnk')) {
let appDetail: { target?: string } = {};
try {
appDetail = shell.readShortcutLink(filedir);
} catch (e) {
//
}

const appInfo = {
value: 'plugin',
desc: appDetail.target,
type: 'app',
icon,
pluginType: 'app',
action: `start "dummyclient" "${appDetail.target}"`,
keyWords: keyWords,
name: appName,
names: JSON.parse(JSON.stringify(keyWords)),
};
fileLists.push(appInfo);
getico(appInfo);
}
if (isDir) {
fileDisplay(filedir); // 递归,如果是文件夹,就继续遍历该文件夹下面的文件
if (
appDetail.target &&
appDetail.target.toLowerCase().indexOf('unin') < 0
) {
const appName = filename.split('.')[0];
results.push(buildAppInfo(appName, appDetail.target));
}
}
} else {
console.warn('获取文件stats失败');
}
}

if (--pending === 0) done();
});
});
});
};

walk(dirPath, () => resolve(results));
});
}

function collectShortcutMap(dirPath: string, target: Map<string, string>) {
let files: string[] = [];
try {
files = fs.readdirSync(dirPath);
} catch {
return;
}

files.forEach((filename) => {
const filedir = path.join(dirPath, filename);
let stats: fs.Stats | null = null;
try {
stats = fs.statSync(filedir);
} catch {
return;
}

if (stats.isDirectory()) {
collectShortcutMap(filedir, target);
return;
}

if (stats.isFile() && filedir.toLowerCase().endsWith('.lnk')) {
const appName = filename.slice(0, -4).toLowerCase().trim();
if (appName && !target.has(appName)) {
target.set(appName, filedir);
}
}
});
}

export default () => {
fileDisplay(filePath);
fileDisplay(startMenu);
function getStartApps(shortcutMap: Map<string, string>): Promise<AppInfo[]> {
return new Promise((resolve) => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { exec } = require('child_process');
const ps =
'[Console]::OutputEncoding=[System.Text.Encoding]::UTF8; Get-StartApps | ConvertTo-Json -Compress';
exec(
`powershell -NoProfile -NonInteractive -Command "${ps}"`,
{ timeout: 8000 },
(err, stdout) => {
if (err || !stdout.trim()) {
resolve([]);
return;
}

try {
const raw = JSON.parse(stdout.trim()) as
| StartAppEntry
| StartAppEntry[];
const apps: StartAppEntry[] = Array.isArray(raw) ? raw : [raw];
const results = apps
.filter((item) => item && item.Name && item.AppID)
.map((item) => {
const appName = item.Name as string;
const shortcutPath = shortcutMap.get(
appName.toLowerCase().trim()
);
const appInfo = buildStartAppInfo(
appName,
item.AppID as string,
shortcutPath
);
if (shortcutPath) {
getico({
...appInfo,
desc: shortcutPath,
});
}
return appInfo;
});
resolve(results);
} catch (e) {
resolve([]);
}
}
);
});
}

export default async (): Promise<AppInfo[]> => {
const shortcutMap = new Map<string, string>();
collectShortcutMap(filePath, shortcutMap);
collectShortcutMap(startMenu, shortcutMap);

const [systemApps, userApps, startApps] = await Promise.all([
scanStartMenuDir(filePath),
scanStartMenuDir(startMenu),
getStartApps(shortcutMap),
]);

const hasReadyIcon = (item: AppInfo) => {
if (!item.icon) return false;
try {
return fs.existsSync(item.icon);
} catch {
return false;
}
};

const merged = [...startApps, ...systemApps, ...userApps];
const indexedByName = new Map<string, number>();
fileLists.length = 0;

merged.forEach((item) => {
const nameKey = `${item.name || ''}`.toLowerCase().trim();
if (!nameKey) {
fileLists.push(item);
return;
}

const existedIndex = indexedByName.get(nameKey);
if (typeof existedIndex === 'number') {
const existed = fileLists[existedIndex];
if (!hasReadyIcon(existed) && hasReadyIcon(item)) {
fileLists[existedIndex] = item;
}
return;
}

indexedByName.set(nameKey, fileLists.length);
fileLists.push(item);
});

return fileLists;
};
Loading