Skip to content

Commit

Permalink
Merge pull request #29 from wakatime/misc/refactor-installed-apps
Browse files Browse the repository at this point in the history
Refactor getting apps for mac and win
  • Loading branch information
alanhamlett authored Sep 19, 2024
2 parents 239b0a1 + 269a758 commit 7967abe
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 206 deletions.
110 changes: 4 additions & 106 deletions electron/helpers/installed-apps/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,9 @@
import path from "node:path";

import { AppData } from "../../utils/validators";
import { allApps } from "../../watchers/apps";
import { getAppIconMac, getInstalledApps as getInstalledAppsMac } from "./mac";
import {
getFilePathWindows,
getIconFromWindows,
getInstalledApps as getInstalledAppsWindows,
} from "./windows";
import { getInstalledApps as getInstalledAppsMac } from "./mac";
import { getInstalledApps as getInstalledAppsWindows } from "./windows";

export async function getInstalledApps(): Promise<Record<string, string>[]> {
let apps: Record<string, string>[] = [];
export async function getApps(): Promise<AppData[]> {
let apps: AppData[] = [];

if (process.platform === "win32") {
apps = await getInstalledAppsWindows();
Expand All @@ -20,98 +13,3 @@ export async function getInstalledApps(): Promise<Record<string, string>[]> {

return apps;
}

export async function getApps(): Promise<AppData[]> {
const installedApps = await getInstalledApps();
const apps = (
await Promise.all(
allApps.map(async (app) => {
if (process.platform === "win32" && app.windows?.DisplayName) {
const record = installedApps.find(
(ia) =>
ia["DisplayName"] &&
app.windows?.DisplayName &&
ia["DisplayName"].startsWith(app.windows.DisplayName),
);

if (!record) {
return null;
}

let filePath: string | null = null;
try {
filePath = getFilePathWindows(record, app.windows.exePath);
} catch (_error) {
/* empty */
}
if (!filePath) {
return null;
}

let icon: string | null = null;
try {
icon = await getIconFromWindows(filePath);
} catch (_error) {
/* empty */
}

const name = record["DisplayName"];
if (!name) {
return null;
}
const version = record["DisplayVersion"];

return {
id: app.id,
icon,
name,
version,
path: filePath,
bundleId: app.mac?.bundleId ?? null,
isBrowser: app.isBrowser ?? false,
isDefaultEnabled: app.isDefaultEnabled ?? false,
isElectronApp: app.isElectronApp ?? false,
execName: path.parse(filePath).base,
} satisfies AppData;
}

if (process.platform === "darwin" && app.mac?.bundleId) {
const record = installedApps.find(
(ia) =>
ia["kMDItemCFBundleIdentifier"] &&
app.mac?.bundleId &&
ia["kMDItemCFBundleIdentifier"] === app.mac.bundleId,
);

if (!record) {
return null;
}
const execName = record["kMDItemDisplayName"];
const name = execName?.replace(".app", "");
const filePath = record["_FILE_PATH"];
if (!filePath || !name) {
return;
}
const icon = await getAppIconMac(filePath);
const version = record["kMDItemVersion"] || null;

return {
path: filePath,
icon,
name,
bundleId: app.mac.bundleId,
id: app.id,
isBrowser: app.isBrowser ?? false,
isDefaultEnabled: app.isDefaultEnabled ?? false,
isElectronApp: app.isElectronApp ?? false,
version,
execName: path.parse(filePath).base,
} satisfies AppData;
}

return null;
}),
)
).filter((app) => app !== null) as AppData[];
return apps;
}
106 changes: 65 additions & 41 deletions electron/helpers/installed-apps/mac.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import { exec, spawnSync } from "child_process";
import fs from "node:fs";
import path from "node:path";
import { AppData } from "electron/utils/validators";
import { allApps } from "electron/watchers/apps";
import iconutil from "iconutil";
import plist from "plist";

export async function getInstalledApps(directory = "/Applications") {
export async function getInstalledApps(
directory = "/Applications",
): Promise<AppData[]> {
const directoryContents = await getDirectoryContents(directory);
const appsFileInfo = await getAppsFileInfo(directoryContents);
return appsFileInfo.map((appFileInfo) => getAppData(appFileInfo));
const appsFileInfo = getAppsFileInfo(directoryContents);
return (
await Promise.all(
appsFileInfo.map((appFileInfo) => getAppData(appFileInfo)),
)
).filter(Boolean) as AppData[];
}

export function getDirectoryContents(directory: string) {
function getDirectoryContents(directory: string) {
return new Promise<string[]>((resolve, reject) => {
exec(`ls ${directory}`, (error, stdout) => {
if (error) {
Expand All @@ -26,10 +34,7 @@ export function getDirectoryContents(directory: string) {
});
}

export function getAppsSubDirectory(
stdout: string,
directory: string,
): string[] {
function getAppsSubDirectory(stdout: string, directory: string): string[] {
let stdoutArr = stdout.split(/[(\r\n)\r\n]+/);
stdoutArr = stdoutArr
.filter((o) => o.endsWith(".app"))
Expand All @@ -39,7 +44,7 @@ export function getAppsSubDirectory(
return stdoutArr;
}

export function getAppsFileInfo(appsFile: readonly string[]) {
function getAppsFileInfo(appsFile: readonly string[]) {
const runMdlsShell = spawnSync("mdls", appsFile, {
encoding: "utf8",
});
Expand All @@ -61,7 +66,7 @@ export function getAppsFileInfo(appsFile: readonly string[]) {
return allAppsFileInfoList;
}

export function getAppData(singleAppFileInfo: string[]) {
async function getAppData(singleAppFileInfo: string[]) {
const getKeyVal = (lineData: string) => {
const lineDataArr = lineData.split("=");
return {
Expand All @@ -70,39 +75,58 @@ export function getAppData(singleAppFileInfo: string[]) {
};
};

const getAppInfoData = (appArr: string[]) => {
const appData: Record<string, string> = {};

appArr.filter(Boolean).forEach((o) => {
const appKeyVal = getKeyVal(o);
if (appKeyVal.value) {
appData[appKeyVal.key] = appKeyVal.value;
const record = singleAppFileInfo.filter(Boolean).reduce(
(acc, curr) => {
const val = getKeyVal(curr);
if (val.value) {
acc[val.key] = val.value;
}
// if (appKeyVal.key === "kMDItemDisplayName") {
// appData.appName = appKeyVal.value.replace(".app", "");
// }
// if (appKeyVal.key === "kMDItemVersion") {
// appData.appVersion = appKeyVal.value;
// }
// if (appKeyVal.key === "kMDItemDateAdded") {
// appData.appInstallDate = appKeyVal.value;
// }
// if (appKeyVal.key === "kMDItemCFBundleIdentifier") {
// appData.appIdentifier = appKeyVal.value;
// }
// if (appKeyVal.key === "kMDItemAppStoreCategory") {
// appData.appCategory = appKeyVal.value;
// }
// if (appKeyVal.key === "kMDItemAppStoreCategoryType") {
// appData.appCategoryType = appKeyVal.value;
// }
});
return appData;
};
return getAppInfoData(singleAppFileInfo);
return acc;
},
{} as Record<string, string>,
);

const app = allApps.find((app) => {
return (
app.mac?.bundleId &&
record["kMDItemCFBundleIdentifier"] === app.mac.bundleId
);
});

const bundleId = app?.mac?.bundleId ?? record["kMDItemCFBundleIdentifier"];
if (!bundleId) {
return;
}

const execName = record["kMDItemDisplayName"];
const name = execName?.replace(".app", "");
if (!name) {
return;
}

const filePath = record["_FILE_PATH"];
if (!filePath) {
return;
}

const icon = await getAppIcon(filePath);
const version = record["kMDItemVersion"] || null;

return {
path: filePath,
icon,
name,
bundleId,
id: app?.id ?? bundleId,
isBrowser: app?.isBrowser ?? false,
isDefaultEnabled: app?.isDefaultEnabled ?? false,
isElectronApp: app?.isElectronApp ?? false,
version,
execName: path.parse(filePath).base,
} satisfies AppData;
}

export async function getAppIconMac(appPath: string) {
async function getAppIcon(appPath: string) {
let icnsPath: string | null = null;

const getFirstIcnsFileFromResourcesDir = () => {
Expand Down Expand Up @@ -165,7 +189,7 @@ export async function getAppIconMac(appPath: string) {
return parseIconFromIcnsFile(icnsPath);
}

export async function parseIconFromIcnsFile(path: string) {
async function parseIconFromIcnsFile(path: string) {
function getSize(filename: string) {
const match = filename.match(/(\d+)x(\d+)/);
if (match) {
Expand Down
Loading

0 comments on commit 7967abe

Please sign in to comment.