From e73624b1e202fd2deefffe2f9a9b8611375c953a Mon Sep 17 00:00:00 2001 From: Will Stone <654103+will-stone@users.noreply.github.com> Date: Sat, 10 Dec 2022 08:26:11 +0000 Subject: [PATCH] refactor: use app names for detection (#593) This should be much faster but currently relies on people not renaming apps. Will possibly look into users providing their own config... --- __mocks__/electron.js | 12 +- package-lock.json | 30 +-- package.json | 5 +- src/config/apps.test.ts | 10 +- src/config/apps.ts | 254 +++++------------- src/main/database.ts | 11 +- src/main/state/actions.ts | 6 +- src/main/state/middleware.action-hub.ts | 14 +- src/main/utils/get-app-icons.ts | 29 +- src/main/utils/get-installed-app-ids.ts | 44 --- src/main/utils/get-installed-app-names.ts | 44 +++ src/main/utils/open-app.ts | 10 +- src/main/windows.ts | 4 +- src/renderers/picker/components/layout.tsx | 7 +- .../picker/components/organisms/apps.test.tsx | 73 +++-- src/renderers/picker/state/actions.ts | 4 +- .../prefs/components/organisms/pane-apps.tsx | 15 +- src/renderers/prefs/state/actions.ts | 6 +- src/renderers/shared/state/hooks.ts | 9 +- src/shared/state/reducer.data.ts | 4 +- src/shared/state/reducer.storage.ts | 20 +- 21 files changed, 245 insertions(+), 366 deletions(-) delete mode 100644 src/main/utils/get-installed-app-ids.ts create mode 100644 src/main/utils/get-installed-app-names.ts diff --git a/__mocks__/electron.js b/__mocks__/electron.js index 17c77d20..54e37526 100644 --- a/__mocks__/electron.js +++ b/__mocks__/electron.js @@ -6,6 +6,7 @@ const eventEmitter = new EventTarget() let clipboard module.exports = { + app: jest.fn(), BrowserWindow: function () { return { webContents: { @@ -21,12 +22,6 @@ module.exports = { }, } }, - Notification: function () { - return { - show: jest.fn, - } - }, - app: jest.fn(), clipboard: { readText: () => clipboard, writeText: (string) => (clipboard = string), @@ -47,6 +42,11 @@ module.exports = { send: jest.fn(), }, match: jest.fn(), + Notification: function () { + return { + show: jest.fn, + } + }, remote: { getCurrentWindow() { return { diff --git a/package-lock.json b/package-lock.json index 973e8ae4..8d6c6da2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "19.3.3", "license": "GPL-3.0-only", "dependencies": { - "file-icon": "^5.1.0" + "file-icon": "^5.1.1" }, "devDependencies": { "@commitlint/cli": "^17.1.2", @@ -32,7 +32,7 @@ "@types/react-dom": "^18.0.6", "@types/react-redux": "^7.1.24", "@vercel/webpack-asset-relocator-loader": "^1.7.3", - "@will-stone/eslint-config-base": "^4.0.0", + "@will-stone/eslint-config-base": "^5.0.0", "@will-stone/eslint-config-jest": "^2.2.2", "@will-stone/eslint-config-node": "^2.0.1", "@will-stone/eslint-config-prettier": "^2.0.1", @@ -51,7 +51,7 @@ "eslint": "^8.24.0", "eslint-plugin-tailwindcss": "^3.6.2", "fast-deep-equal": "^3.1.3", - "file-icon": "^5.1.0", + "file-icon": "^5.1.1", "fork-ts-checker-webpack-plugin": "^7.2.13", "husky": "^8.0.1", "immer": "^9.0.15", @@ -3274,9 +3274,9 @@ } }, "node_modules/@will-stone/eslint-config-base": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@will-stone/eslint-config-base/-/eslint-config-base-4.0.0.tgz", - "integrity": "sha512-eUu5aeCVHbJSKRnLWFfMLG51O+slEl8Eid1JCnmlUAaWEX+yMl7qscmfvqaAigfWBJhEPqh12QQQjqC84d60vA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@will-stone/eslint-config-base/-/eslint-config-base-5.0.0.tgz", + "integrity": "sha512-EwSo830rqFglt357PRlB/mFOzOByIoCQ3j/bHqw+1V0lgmQfPEQWuRwYEPOx4LRfMyhaMEYZYXs0B7TBlT3Ksg==", "dev": true, "dependencies": { "@rushstack/eslint-patch": "^1.2.0", @@ -7798,9 +7798,9 @@ } }, "node_modules/file-icon": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/file-icon/-/file-icon-5.1.0.tgz", - "integrity": "sha512-Yhx00f5jz/y70Ep5Yvd+KcRLaBLNYSqQADEOqM95sKvYlNQRN0nYST3bMja8+cFTJolCH7OlSyp+C1M3Hnzhng==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/file-icon/-/file-icon-5.1.1.tgz", + "integrity": "sha512-rEhTPCpqPjESUIgwR1qNCWmvY0ie6cQjEvWUyXACxiYcshaPumURlr9ZpWFBTqdonGlUMJHu95l6bQKs1eL40w==", "dev": true, "dependencies": { "p-map": "^5.3.0" @@ -20553,9 +20553,9 @@ } }, "@will-stone/eslint-config-base": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@will-stone/eslint-config-base/-/eslint-config-base-4.0.0.tgz", - "integrity": "sha512-eUu5aeCVHbJSKRnLWFfMLG51O+slEl8Eid1JCnmlUAaWEX+yMl7qscmfvqaAigfWBJhEPqh12QQQjqC84d60vA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@will-stone/eslint-config-base/-/eslint-config-base-5.0.0.tgz", + "integrity": "sha512-EwSo830rqFglt357PRlB/mFOzOByIoCQ3j/bHqw+1V0lgmQfPEQWuRwYEPOx4LRfMyhaMEYZYXs0B7TBlT3Ksg==", "dev": true, "requires": { "@rushstack/eslint-patch": "^1.2.0", @@ -23953,9 +23953,9 @@ } }, "file-icon": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/file-icon/-/file-icon-5.1.0.tgz", - "integrity": "sha512-Yhx00f5jz/y70Ep5Yvd+KcRLaBLNYSqQADEOqM95sKvYlNQRN0nYST3bMja8+cFTJolCH7OlSyp+C1M3Hnzhng==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/file-icon/-/file-icon-5.1.1.tgz", + "integrity": "sha512-rEhTPCpqPjESUIgwR1qNCWmvY0ie6cQjEvWUyXACxiYcshaPumURlr9ZpWFBTqdonGlUMJHu95l6bQKs1eL40w==", "dev": true, "requires": { "p-map": "^5.3.0" diff --git a/package.json b/package.json index a17e2e3c..59acaf5a 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "website-css:watch": "npx tailwindcss -i node_modules/tailwindcss/tailwind.css -o ./docs/style.css --content docs/index.html -m --watch" }, "dependencies": { - "file-icon": "^5.1.0" + "file-icon": "^5.1.1" }, "devDependencies": { "@commitlint/cli": "^17.1.2", @@ -59,7 +59,7 @@ "@types/react-dom": "^18.0.6", "@types/react-redux": "^7.1.24", "@vercel/webpack-asset-relocator-loader": "^1.7.3", - "@will-stone/eslint-config-base": "^4.0.0", + "@will-stone/eslint-config-base": "^5.0.0", "@will-stone/eslint-config-jest": "^2.2.2", "@will-stone/eslint-config-node": "^2.0.1", "@will-stone/eslint-config-prettier": "^2.0.1", @@ -78,7 +78,6 @@ "eslint": "^8.24.0", "eslint-plugin-tailwindcss": "^3.6.2", "fast-deep-equal": "^3.1.3", - "file-icon": "^5.1.0", "fork-ts-checker-webpack-plugin": "^7.2.13", "husky": "^8.0.1", "immer": "^9.0.15", diff --git a/src/config/apps.test.ts b/src/config/apps.test.ts index 1aaa548b..11003253 100644 --- a/src/config/apps.test.ts +++ b/src/config/apps.test.ts @@ -1,11 +1,6 @@ import { getKeys } from '../shared/utils/get-keys' import { apps } from './apps' -test.each(getKeys(apps))('%s should have name', (input) => { - expect(apps[input]).toHaveProperty('name') - expect(typeof apps[input].name).toBe('string') -}) - test.each(getKeys(apps))( '%s should not include anything but allowed keys', (input) => { @@ -20,10 +15,7 @@ test.each(getKeys(apps))( ) test('should have apps in alphabetical order by name', () => { - const appNames = Object.values(apps).map((appDetails) => - appDetails.name.toLowerCase(), - ) - + const appNames = Object.keys(apps).map((appName) => appName.toLowerCase()) const sortedAppNames = [...appNames].sort() expect(appNames).toStrictEqual(sortedAppNames) }) diff --git a/src/config/apps.ts b/src/config/apps.ts index 290502f4..a59aed9f 100644 --- a/src/config/apps.ts +++ b/src/config/apps.ts @@ -1,7 +1,4 @@ -/* eslint-disable sort-keys -- apps are sorted by name */ - interface App { - name: string privateArg?: string convertUrl?: (url: string) => string } @@ -9,236 +6,129 @@ interface App { const typeApps = >(apps: T) => apps const apps = typeApps({ - 'company.thebrowser.Browser': { - name: 'Arc', - }, - 'org.blisk.Blisk': { - name: 'Blisk', - }, - 'com.brave.Browser': { - name: 'Brave', - privateArg: '--incognito', - }, - 'com.brave.Browser.beta': { - name: 'Brave Beta', - privateArg: '--incognito', - }, - 'com.brave.Browser.dev': { - name: 'Brave Dev', + 'Arc': {}, + 'Blisk': {}, + 'Brave Beta': { privateArg: '--incognito', }, - 'com.brave.Browser.nightly': { - name: 'Brave Nightly', + 'Brave Browser': { privateArg: '--incognito', }, - 'com.google.Chrome': { - name: 'Chrome', + 'Brave Dev': { privateArg: '--incognito', }, - 'com.google.Chrome.beta': { - name: 'Chrome Beta', + 'Brave Nightly': { privateArg: '--incognito', }, - 'com.google.Chrome.canary': { - name: 'Chrome Canary', + 'Chromium': { privateArg: '--incognito', }, - 'com.google.Chrome.dev': { - name: 'Chrome Dev', - privateArg: '--incognito', - }, - 'org.chromium.Chromium': { - name: 'Chromium', - privateArg: '--incognito', - }, - 'com.hnc.Discord': { - name: 'Discord', + 'Discord': { convertUrl: (url) => url.replace( /^https?:\/\/(?:(?:ptb|canary)\.)?discord\.com\//u, 'discord://-/', ), }, - 'com.hnc.DiscordCanary': { - name: 'Discord Canary', + 'Discord Canary': { convertUrl: (url) => url.replace( /^https?:\/\/(?:(?:ptb|canary)\.)?discord\.com\//u, 'discord://-/', ), }, - 'com.hnc.DiscordPTB': { - name: 'Discord PTB', + 'Discord PTB': { convertUrl: (url) => url.replace( /^https?:\/\/(?:(?:ptb|canary)\.)?discord\.com\//u, 'discord://-/', ), }, - 'com.gab.Dissenter': { - name: 'Dissenter', - }, - 'com.duckduckgo.macos.browser': { - name: 'DuckDuckGo', - }, - 'com.microsoft.edgemac': { - name: 'Edge', - }, - 'com.microsoft.edgemac.Beta': { - name: 'Edge Beta', - }, - 'com.microsoft.edgemac.Canary': { - name: 'Edge Canary', - }, - 'com.microsoft.edgemac.Dev': { - name: 'Edge Dev', - }, - 'com.figma.Desktop': { - name: 'Figma', - }, - 'net.kassett.finicky': { - name: 'Finicky', - }, - 'org.mozilla.firefox': { - name: 'Firefox', + 'Dissenter': {}, + 'DuckDuckGo': {}, + 'Figma': {}, + 'Finicky': {}, + 'Firefox': { privateArg: '--private-window', }, - 'org.mozilla.firefoxdeveloperedition': { - name: 'Firefox Dev', + 'Firefox Dev': { privateArg: '--private-window', }, - 'org.mozilla.nightly': { - name: 'Firefox Nightly', + 'Firefox Nightly': { privateArg: '--private-window', }, - 'io.freetubeapp.freetube': { - name: 'FreeTube', + 'FreeTube': {}, + 'Google Chrome': { + privateArg: '--incognito', }, - 'org.mozilla.icecat': { - name: 'IceCat', - privateArg: '--private-window', + 'Google Chrome Beta': { + privateArg: '--incognito', }, - 'de.iridiumbrowser': { - name: 'Iridium', + 'Google Chrome Canary': { + privateArg: '--incognito', }, - 'org.mozilla.librewolf': { - name: 'LibreWolf', - privateArg: '--private-window', + 'Google Chrome Dev': { + privateArg: '--incognito', }, - 'com.linear': { - name: 'Linear', + 'IceCat': { + privateArg: '--private-window', }, - 'com.maxthon.mac.Maxthon': { - name: 'Maxthon', + 'Iridium': {}, + 'LibreWolf': { + privateArg: '--private-window', }, - 'com.microsoft.teams': { - name: 'Microsoft Teams', + 'Linear': {}, + 'Maxthon': {}, + 'Microsoft Edge': {}, + 'Microsoft Edge Beta': {}, + 'Microsoft Edge Canary': {}, + 'Microsoft Edge Dev': {}, + 'Microsoft Teams': { convertUrl: (url) => url.replace('https://teams.microsoft.com/', 'msteams:/'), }, - 'com.electron.min': { - name: 'Min', - }, - 'com.electron.realtimeboard': { - name: 'Miro', - }, - 'com.naver.Whale': { - name: 'NAVER Whale', - }, - 'notion.id': { - name: 'Notion', - }, - 'com.operasoftware.Opera': { - name: 'Opera', - }, - 'com.operasoftware.OperaNext': { - name: 'Opera Beta', - }, - 'com.operasoftware.OperaCryptoDeveloper': { - name: 'Opera CD', - }, - 'com.operasoftware.OperaCrypto': { - name: 'Opera Crypto', - }, - 'com.operasoftware.OperaDeveloper': { - name: 'Opera Dev', - }, - 'com.operasoftware.OperaGX': { - name: 'Opera GX', - }, - 'com.opera.Neon': { - name: 'Opera Neon', - }, - 'com.kagi.kagimacOS': { - name: 'Orion', - }, - 'com.readitlater.PocketMac': { - name: 'Pocket', + 'Min': {}, + 'Miro': {}, + 'NAVER Whale': {}, + 'Notion': {}, + 'Opera': {}, + 'Opera Beta': {}, + 'Opera CD': {}, + 'Opera Crypto': {}, + 'Opera Dev': {}, + 'Opera GX': {}, + 'Opera Neon': {}, + 'Orion': {}, + 'Pocket': { convertUrl: (url) => `pocket://add?url=${url}`, }, - 'com.firstversionist.polypane': { - name: 'Polypane', - }, - 'org.qt-project.Qt.QtWebEngineCore': { - name: 'qutebrowser', - }, - 'com.apple.Safari': { - name: 'Safari', - }, - 'com.apple.SafariTechnologyPreview': { - name: 'Safari TP', - }, - 'com.pushplaylabs.sidekick': { - name: 'Sidekick', + 'Polypane': {}, + 'qutebrowser': {}, + 'Safari': {}, + 'Safari TP': {}, + 'Sidekick': { privateArg: '--incognito', }, - 'com.sigmaos.sigmaos.macos': { - name: 'SigmaOS', - }, - 'com.kitze.sizzy': { - name: 'Sizzy', - }, - 'com.tinyspeck.slackmacgap': { - name: 'Slack', - }, - 'com.spotify.client': { - name: 'Spotify', - }, - 'org.torproject.torbrowser': { - name: 'Tor', - }, - 'maccatalyst.com.atebits.Tweetie2': { - name: 'Twitter', - }, - 'com.vivaldi.Vivaldi': { - name: 'Vivaldi', - }, - 'com.vivaldi.Vivaldi.snapshot': { - name: 'Vivaldi Snapshot', - }, - 'net.waterfox.waterfox': { - name: 'Waterfox', - }, - 'com.bookry.wavebox': { - name: 'Wavebox', + 'SigmaOS': {}, + 'Sizzy': {}, + 'Slack': {}, + 'Spotify': {}, + 'Tor': {}, + 'Twitter': {}, + 'Vivaldi': {}, + 'Vivaldi Snapshot': {}, + 'Waterfox': {}, + 'Wavebox': { privateArg: '--incognito', }, - 'com.whisttechnologies.whist': { - name: 'Whist', - }, - 'ru.yandex.desktop.yandex-browser': { - name: 'Yandex', - }, - 'stream.yattee.app': { - name: 'Yattee', - }, - 'us.zoom.xos': { - name: 'Zoom', - }, + 'Whist': {}, + 'Yandex': {}, + 'Yattee': {}, + 'zoom.us': {}, }) type Apps = typeof apps -type AppId = keyof typeof apps +type AppName = keyof typeof apps -export { AppId, Apps, apps } +export { AppName, Apps, apps } diff --git a/src/main/database.ts b/src/main/database.ts index 8079c688..d98fa8f3 100644 --- a/src/main/database.ts +++ b/src/main/database.ts @@ -15,10 +15,6 @@ lowdb.read() lowdb.data ||= defaultStorage lowdb.write() -/** - * Keyboard shortcuts - */ - export const database = { get: (key: Key): Storage[Key] => { return database.getAll()[key] @@ -49,6 +45,13 @@ export const database = { } } + // Remove old, id-based apps + if (Array.isArray(lowdb.data.apps)) { + lowdb.data.apps = lowdb.data.apps.filter((storedApp) => + Boolean(storedApp.name), + ) + } + return { ...defaultStorage, ...lowdb.data, diff --git a/src/main/state/actions.ts b/src/main/state/actions.ts index a2381f0d..dde254c1 100644 --- a/src/main/state/actions.ts +++ b/src/main/state/actions.ts @@ -1,7 +1,7 @@ import type { Rectangle } from 'electron/main' import type { CombinedState } from 'redux' -import type { AppId } from '../../config/apps' +import type { AppName } from '../../config/apps' import type { Data } from '../../shared/state/reducer.data' import type { Storage } from '../../shared/state/reducer.storage' import { actionNamespacer } from '../../shared/utils/action-namespacer' @@ -18,14 +18,14 @@ const changedPickerWindowBounds = main( const startedScanning = main('installed-apps/scanning') -const retrievedInstalledApps = main('installed-apps/retrieved') +const retrievedInstalledApps = main('installed-apps/retrieved') const receivedRendererStartupSignal = main>('sync-reducers') const gotDefaultBrowserStatus = main('default-browser-status/got') -const gotAppIcons = main>>('app-icons/got') +const gotAppIcons = main>>('app-icons/got') const availableUpdate = main('update/available') const downloadingUpdate = main('update/downloading') diff --git a/src/main/state/middleware.action-hub.ts b/src/main/state/middleware.action-hub.ts index 47e7dd89..441c474e 100644 --- a/src/main/state/middleware.action-hub.ts +++ b/src/main/state/middleware.action-hub.ts @@ -27,7 +27,7 @@ import { database } from '../database' import { createTray } from '../tray' import copyUrlToClipboard from '../utils/copy-url-to-clipboard' import { getAppIcons } from '../utils/get-app-icons' -import { getInstalledAppIds } from '../utils/get-installed-app-ids' +import { getInstalledAppNames } from '../utils/get-installed-app-names' import { initUpdateChecker } from '../utils/init-update-checker' import { openApp } from '../utils/open-app' import { removeWindowsFromMemory } from '../utils/remove-windows-from-memory' @@ -94,7 +94,7 @@ export const actionHubMiddleware = createWindows() createTray() initUpdateChecker() - getInstalledAppIds() + getInstalledAppNames() } // When a renderer starts, send down all the locally stored data @@ -129,16 +129,16 @@ export const actionHubMiddleware = // Rescan for browsers else if (clickedRescanApps.match(action)) { - getInstalledAppIds() + getInstalledAppNames() } // Clicked app else if (clickedApp.match(action)) { - const { appId, isAlt, isShift } = action.payload + const { appName, isAlt, isShift } = action.payload // Ignore if app's bundle id is missing - if (appId) { - openApp(appId, nextState.data.url, isAlt, isShift) + if (appName) { + openApp(appName, nextState.data.url, isAlt, isShift) pickerWindow?.hide() } } @@ -163,7 +163,7 @@ export const actionHubMiddleware = if (!action.payload.metaKey && foundApp) { openApp( - foundApp.id, + foundApp.name, nextState.data.url, action.payload.altKey, action.payload.shiftKey, diff --git a/src/main/utils/get-app-icons.ts b/src/main/utils/get-app-icons.ts index ee54d068..b74b75a4 100644 --- a/src/main/utils/get-app-icons.ts +++ b/src/main/utils/get-app-icons.ts @@ -4,7 +4,7 @@ import { promisify } from 'node:util' import log from 'electron-log' -import type { AppId } from '../../config/apps' +import type { AppName } from '../../config/apps' import type { Storage } from '../../shared/state/reducer.storage' import { gotAppIcons } from '../state/actions' import { dispatch } from '../state/store' @@ -23,23 +23,32 @@ const binary = path.join( const HUNDRED_MEGABYTES = 1024 * 1024 * 100 async function getIconDataURI(file: string, size: number): Promise { - const { stdout: buffer } = await execFileP( - binary, - [JSON.stringify([{ appOrPID: file, size }])], - { encoding: null, maxBuffer: HUNDRED_MEGABYTES }, - ) + try { + const { stdout: buffer } = await execFileP( + binary, + [JSON.stringify([{ appOrPID: file, size }])], + { encoding: null, maxBuffer: HUNDRED_MEGABYTES }, + ) - return `data:image/png;base64,${buffer.toString('base64')}` + return `data:image/png;base64,${buffer.toString('base64')}` + } catch (error: unknown) { + if (error instanceof Error) { + // eslint-disable-next-line no-console + console.log(`Error reading ${file}`) + } + + throw error + } } export async function getAppIcons(apps: Storage['apps']): Promise { try { - const icons: Partial> = {} + const icons: Partial> = {} for await (const app of apps) { try { - const dataURI = await getIconDataURI(app.id, 64) - icons[app.id] = dataURI + const dataURI = await getIconDataURI(app.name, 64) + icons[app.name] = dataURI } catch (error: unknown) { log.warn(error) } diff --git a/src/main/utils/get-installed-app-ids.ts b/src/main/utils/get-installed-app-ids.ts deleted file mode 100644 index 667d9ed2..00000000 --- a/src/main/utils/get-installed-app-ids.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { execSync } from 'node:child_process' - -import { sleep } from 'tings' - -import type { AppId } from '../../config/apps' -import { apps } from '../../config/apps' -import { retrievedInstalledApps, startedScanning } from '../state/actions' -import { dispatch } from '../state/store' - -function getAllInstalledBundleIds(): string[] { - const bundleIds = execSync( - 'mdfind -onlyin /Applications kMDItemKind == \'*\' -attr kMDItemCFBundleIdentifier | sed -e "s/^.*kMDItemCFBundleIdentifier = //" -e "/(null)/d"', - ) - .toString() - .trim() - .split('\n') - - return bundleIds -} - -async function getInstalledAppIds(): Promise { - dispatch(startedScanning()) - - const allInstalledBundleIds = getAllInstalledBundleIds() - const installedApps: AppId[] = [] - - for (const installedBundleId of allInstalledBundleIds) { - if (installedBundleId in apps) { - installedApps.push(installedBundleId as AppId) - } - } - - // It appears that sometimes the installed app IDs are not fetched, maybe a - // race with Spotlight index? So if none found, keep retrying. - // https://github.com/will-stone/browserosaurus/issues/425 - if (installedApps.length === 0) { - await sleep(500) - getInstalledAppIds() - } else { - dispatch(retrievedInstalledApps(installedApps)) - } -} - -export { getInstalledAppIds } diff --git a/src/main/utils/get-installed-app-names.ts b/src/main/utils/get-installed-app-names.ts new file mode 100644 index 00000000..87a51e33 --- /dev/null +++ b/src/main/utils/get-installed-app-names.ts @@ -0,0 +1,44 @@ +import { execSync } from 'node:child_process' +import path from 'node:path' + +import { sleep } from 'tings' + +import type { AppName } from '../../config/apps' +import { apps } from '../../config/apps' +import { retrievedInstalledApps, startedScanning } from '../state/actions' +import { dispatch } from '../state/store' + +function getAllInstalledAppNames(): string[] { + const appNames = execSync( + 'find /Applications -iname "*.app" -prune -not -path "*/.*"', + ) + .toString() + .trim() + .split('\n') + .map((appPath) => path.parse(appPath).name) + + return appNames +} + +async function getInstalledAppNames(): Promise { + dispatch(startedScanning()) + + const allInstalledAppNames = getAllInstalledAppNames() + + const installedApps = Object.keys(apps).filter((appName) => + allInstalledAppNames.includes(appName), + ) as AppName[] + + // It appears that sometimes the installed app IDs are not fetched, maybe a + // race with Spotlight index? So if none found, keep retrying. + // TODO is this needed any more, now using we're `find` method? + // https://github.com/will-stone/browserosaurus/issues/425 + if (installedApps.length === 0) { + await sleep(500) + getInstalledAppNames() + } else { + dispatch(retrievedInstalledApps(installedApps)) + } +} + +export { getInstalledAppNames } diff --git a/src/main/utils/open-app.ts b/src/main/utils/open-app.ts index 9886453e..cdff079f 100644 --- a/src/main/utils/open-app.ts +++ b/src/main/utils/open-app.ts @@ -1,22 +1,22 @@ import { execFile } from 'child_process' -import type { AppId } from '../../config/apps' +import type { AppName } from '../../config/apps' import { apps } from '../../config/apps' export function openApp( - appId: AppId, + appName: AppName, url: string, isAlt: boolean, isShift: boolean, ): void { - const selectedApp = apps[appId] + const selectedApp = apps[appName] const convertedUrl = 'convertUrl' in selectedApp ? selectedApp.convertUrl(url) : url const openArguments: string[] = [ - '-b', - appId, + '-a', + appName, isAlt ? '--background' : [], isShift && 'privateArg' in selectedApp ? ['--new', '--args', selectedApp.privateArg] diff --git a/src/main/windows.ts b/src/main/windows.ts index 1b210a4e..3bfe407b 100644 --- a/src/main/windows.ts +++ b/src/main/windows.ts @@ -74,11 +74,11 @@ async function createWindows(): Promise { hasShadow: true, height, icon: path.join(__dirname, '/static/icon/icon.png'), - maxWidth: 250, maximizable: false, + maxWidth: 250, minHeight: 112, - minWidth: 250, minimizable: false, + minWidth: 250, movable: false, resizable: true, show: false, diff --git a/src/renderers/picker/components/layout.tsx b/src/renderers/picker/components/layout.tsx index c5bae578..99079349 100644 --- a/src/renderers/picker/components/layout.tsx +++ b/src/renderers/picker/components/layout.tsx @@ -64,9 +64,8 @@ const App: React.FC = () => { className="relative w-full grow overflow-y-auto px-2 pb-2" > {apps.map((app, index) => { - const key = app.id + index return ( -
+
diff --git a/src/renderers/picker/components/organisms/apps.test.tsx b/src/renderers/picker/components/organisms/apps.test.tsx index a5926f09..d6ba69c6 100644 --- a/src/renderers/picker/components/organisms/apps.test.tsx +++ b/src/renderers/picker/components/organisms/apps.test.tsx @@ -40,20 +40,16 @@ test('kitchen sink', () => { const win = new electron.BrowserWindow() win.webContents.send( Channel.MAIN, - retrievedInstalledApps([ - 'org.mozilla.firefox', - 'com.apple.Safari', - 'com.brave.Browser.nightly', - ]), + retrievedInstalledApps(['Firefox', 'Safari', 'Brave Browser']), ) // Check apps and app logos shown expect(screen.getByTestId('Firefox')).toBeVisible() expect(screen.getByRole('button', { name: 'Firefox App' })).toBeVisible() expect(screen.getByTestId('Safari')).toBeVisible() expect(screen.getByRole('button', { name: 'Safari App' })).toBeVisible() - expect(screen.getByTestId('Brave Nightly')).toBeVisible() + expect(screen.getByTestId('Brave Browser')).toBeVisible() expect( - screen.getByRole('button', { name: 'Brave Nightly App' }), + screen.getByRole('button', { name: 'Brave Browser App' }), ).toBeVisible() expect(screen.getAllByRole('button', { name: /[A-z]+ App/u })).toHaveLength(3) @@ -68,23 +64,23 @@ test('kitchen sink', () => { apps: [ { hotCode: null, - id: 'org.mozilla.firefox', isInstalled: true, + name: 'Firefox', }, { hotCode: null, - id: 'com.apple.Safari', isInstalled: true, + name: 'Safari', }, { hotCode: null, - id: 'com.operasoftware.Opera', isInstalled: false, + name: 'Opera', }, { hotCode: null, - id: 'com.brave.Browser.nightly', isInstalled: true, + name: 'Brave Browser', }, ], height: 200, @@ -104,7 +100,7 @@ test('kitchen sink', () => { Channel.PICKER, addChannelToAction( clickedApp({ - appId: 'org.mozilla.firefox', + appName: 'Firefox', isAlt: false, isShift: false, }), @@ -115,14 +111,14 @@ test('kitchen sink', () => { // Correct info sent to main when app clicked const url = 'http://example.com' win.webContents.send(Channel.MAIN, openedUrl(url)) - fireEvent.click(screen.getByRole('button', { name: 'Brave Nightly App' }), { + fireEvent.click(screen.getByRole('button', { name: 'Brave Browser App' }), { altKey: true, }) expect(electron.ipcRenderer.send).toHaveBeenCalledWith( Channel.PICKER, addChannelToAction( clickedApp({ - appId: 'com.brave.Browser.nightly', + appName: 'Brave Browser', isAlt: true, isShift: false, }), @@ -142,8 +138,8 @@ test('should show spinner when no installed apps are found', () => { apps: [ { hotCode: 'KeyS', - id: 'com.apple.Safari', isInstalled: false, + name: 'Safari', }, ], height: 200, @@ -158,10 +154,7 @@ test('should show spinner when no installed apps are found', () => { test('should use hotkey', () => { render() const win = new electron.BrowserWindow() - win.webContents.send( - Channel.MAIN, - retrievedInstalledApps(['com.apple.Safari']), - ) + win.webContents.send(Channel.MAIN, retrievedInstalledApps(['Safari'])) win.webContents.send( Channel.MAIN, receivedRendererStartupSignal({ @@ -170,8 +163,8 @@ test('should use hotkey', () => { apps: [ { hotCode: 'KeyS', - id: 'com.apple.Safari', isInstalled: true, + name: 'Safari', }, ], height: 200, @@ -202,10 +195,7 @@ test('should use hotkey', () => { test('should use hotkey with alt', () => { render() const win = new electron.BrowserWindow() - win.webContents.send( - Channel.MAIN, - retrievedInstalledApps(['com.apple.Safari']), - ) + win.webContents.send(Channel.MAIN, retrievedInstalledApps(['Safari'])) win.webContents.send( Channel.MAIN, @@ -215,8 +205,8 @@ test('should use hotkey with alt', () => { apps: [ { hotCode: 'KeyS', - id: 'com.apple.Safari', isInstalled: true, + name: 'Safari', }, ], height: 200, @@ -252,10 +242,7 @@ test('should use hotkey with alt', () => { test('should hold shift', () => { render() const win = new electron.BrowserWindow() - win.webContents.send( - Channel.MAIN, - retrievedInstalledApps(['org.mozilla.firefox']), - ) + win.webContents.send(Channel.MAIN, retrievedInstalledApps(['Firefox'])) win.webContents.send(Channel.MAIN, openedUrl('http://example.com')) fireEvent.click(screen.getByRole('button', { name: 'Firefox App' }), { shiftKey: true, @@ -264,7 +251,7 @@ test('should hold shift', () => { Channel.PICKER, addChannelToAction( clickedApp({ - appId: 'org.mozilla.firefox', + appName: 'Firefox', isAlt: false, isShift: true, }), @@ -294,11 +281,11 @@ test('should order tiles', () => { win.webContents.send( Channel.MAIN, retrievedInstalledApps([ - 'org.mozilla.firefox', - 'com.apple.Safari', - 'com.operasoftware.Opera', - 'com.microsoft.edgemac', - 'com.brave.Browser', + 'Firefox', + 'Safari', + 'Opera', + 'Microsoft Edge', + 'Brave Browser', ]), ) // Check tiles and tile logos shown @@ -309,22 +296,22 @@ test('should order tiles', () => { win.webContents.send( Channel.MAIN, reorderedApp({ - destinationId: 'org.mozilla.firefox', - sourceId: 'com.apple.Safari', + destinationName: 'Firefox', + sourceName: 'Safari', }), ) win.webContents.send( Channel.MAIN, reorderedApp({ - destinationId: 'org.mozilla.firefox', - sourceId: 'com.operasoftware.Opera', + destinationName: 'Firefox', + sourceName: 'Opera', }), ) win.webContents.send( Channel.MAIN, reorderedApp({ - destinationId: 'org.mozilla.firefox', - sourceId: 'com.brave.Browser', + destinationName: 'Firefox', + sourceName: 'Brave Browser', }), ) @@ -332,7 +319,7 @@ test('should order tiles', () => { expect(updatedApps[0]).toHaveAttribute('aria-label', 'Safari App') expect(updatedApps[1]).toHaveAttribute('aria-label', 'Opera App') - expect(updatedApps[2]).toHaveAttribute('aria-label', 'Brave App') + expect(updatedApps[2]).toHaveAttribute('aria-label', 'Brave Browser App') expect(updatedApps[3]).toHaveAttribute('aria-label', 'Firefox App') - expect(updatedApps[4]).toHaveAttribute('aria-label', 'Edge App') + expect(updatedApps[4]).toHaveAttribute('aria-label', 'Microsoft Edge App') }) diff --git a/src/renderers/picker/state/actions.ts b/src/renderers/picker/state/actions.ts index dc5babe4..3176b498 100644 --- a/src/renderers/picker/state/actions.ts +++ b/src/renderers/picker/state/actions.ts @@ -1,10 +1,10 @@ -import type { AppId } from '../../../config/apps' +import type { AppName } from '../../../config/apps' import { actionNamespacer } from '../../../shared/utils/action-namespacer' const picker = actionNamespacer('picker') interface OpenAppArguments { - appId: AppId | undefined + appName: AppName | undefined isAlt: boolean isShift: boolean } diff --git a/src/renderers/prefs/components/organisms/pane-apps.tsx b/src/renderers/prefs/components/organisms/pane-apps.tsx index 6f2188a4..97a13c2e 100644 --- a/src/renderers/prefs/components/organisms/pane-apps.tsx +++ b/src/renderers/prefs/components/organisms/pane-apps.tsx @@ -17,7 +17,7 @@ import { CSS } from '@dnd-kit/utilities' import clsx from 'clsx' import { useDispatch } from 'react-redux' -import type { AppId } from '../../../../config/apps' +import type { AppName } from '../../../../config/apps' import Input from '../../../shared/components/atoms/input' import { Spinner } from '../../../shared/components/atoms/spinner' import type { InstalledApp } from '../../../shared/state/hooks' @@ -30,7 +30,7 @@ import { reorderedApp, updatedHotCode } from '../../state/actions' import { Pane } from '../molecules/pane' interface SortableItemProps { - id: InstalledApp['id'] + id: InstalledApp['name'] name: InstalledApp['name'] index: number icon?: string @@ -100,7 +100,7 @@ const SortableItem = ({ onKeyPress={(event) => { dispatch( updatedHotCode({ - appId: id, + appName: id, value: event.code, }), ) @@ -117,7 +117,10 @@ const SortableItem = ({ export function AppsPane(): JSX.Element { const dispatch = useDispatch() - const installedApps = useInstalledApps() + const installedApps = useInstalledApps().map((installedApp) => ({ + ...installedApp, + id: installedApp.name, + })) const sensors = useSensors( useSensor(PointerSensor), @@ -130,8 +133,8 @@ export function AppsPane(): JSX.Element { if (active.id !== over?.id) { dispatch( reorderedApp({ - destinationId: over?.id as AppId, - sourceId: active.id as AppId, + destinationName: over?.id as AppName, + sourceName: active.id as AppName, }), ) } diff --git a/src/renderers/prefs/state/actions.ts b/src/renderers/prefs/state/actions.ts index 24b42e7c..46a1dd16 100644 --- a/src/renderers/prefs/state/actions.ts +++ b/src/renderers/prefs/state/actions.ts @@ -1,4 +1,4 @@ -import type { AppId } from '../../../config/apps' +import type { AppName } from '../../../config/apps' import type { PrefsTab } from '../../../shared/state/reducer.data' import { actionNamespacer } from '../../../shared/utils/action-namespacer' @@ -17,11 +17,11 @@ const clickedUpdateButton = prefs('update-button/clicked') const clickedUpdateRestartButton = prefs('update-restart-button/clicked') const confirmedReset = prefs('reset/confirmed') -const updatedHotCode = prefs<{ appId: AppId; value: string }>( +const updatedHotCode = prefs<{ appName: AppName; value: string }>( 'hot-code/updated', ) -const reorderedApp = prefs<{ sourceId: AppId; destinationId: AppId }>( +const reorderedApp = prefs<{ sourceName: AppName; destinationName: AppName }>( 'app/reordered', ) diff --git a/src/renderers/shared/state/hooks.ts b/src/renderers/shared/state/hooks.ts index 3e5a4fb0..aa9052c2 100644 --- a/src/renderers/shared/state/hooks.ts +++ b/src/renderers/shared/state/hooks.ts @@ -2,8 +2,7 @@ import deepEqual from 'fast-deep-equal' import type { TypedUseSelectorHook } from 'react-redux' import { shallowEqual, useSelector as useReduxSelector } from 'react-redux' -import type { AppId, Apps } from '../../../config/apps' -import { apps as allApps } from '../../../config/apps' +import type { AppName } from '../../../config/apps' import type { RootState } from '../../../shared/state/reducer.root' const useSelector: TypedUseSelectorHook = useReduxSelector @@ -15,8 +14,7 @@ const useDeepEqualSelector: TypedUseSelectorHook = (selector) => useSelector(selector, deepEqual) interface InstalledApp { - id: AppId - name: Apps[AppId]['name'] + name: AppName hotCode: string | null } @@ -26,8 +24,7 @@ const useInstalledApps = (): InstalledApp[] => { .filter((storedApp) => storedApp.isInstalled) .map((storedApp) => ({ hotCode: storedApp.hotCode, - id: storedApp.id, - name: allApps[storedApp.id].name, + name: storedApp.name, })) } diff --git a/src/shared/state/reducer.data.ts b/src/shared/state/reducer.data.ts index 9bca217a..61f49a7f 100644 --- a/src/shared/state/reducer.data.ts +++ b/src/shared/state/reducer.data.ts @@ -1,6 +1,6 @@ import { createReducer } from '@reduxjs/toolkit' -import type { AppId } from '../../config/apps' +import type { AppName } from '../../config/apps' import { CARROT_URL } from '../../config/CONSTANTS' import { availableUpdate, @@ -37,7 +37,7 @@ interface Data { prefsTab: PrefsTab keyCodeMap: Record scanStatus: 'init' | 'scanned' | 'scanning' - icons: Partial> + icons: Partial> activeAppIndex: number } diff --git a/src/shared/state/reducer.storage.ts b/src/shared/state/reducer.storage.ts index d1291eed..74403030 100644 --- a/src/shared/state/reducer.storage.ts +++ b/src/shared/state/reducer.storage.ts @@ -1,6 +1,6 @@ import { createReducer } from '@reduxjs/toolkit' -import type { AppId } from '../../config/apps' +import type { AppName } from '../../config/apps' import { changedPickerWindowBounds, readiedApp, @@ -19,7 +19,7 @@ import { interface Storage { apps: { - id: AppId + name: AppName hotCode: string | null isInstalled: boolean }[] @@ -49,22 +49,22 @@ const storage = createReducer(defaultStorage, (builder) => ) .addCase(retrievedInstalledApps, (state, action) => { - const installedAppIds = action.payload + const installedAppNames = action.payload for (const storedApp of state.apps) { - storedApp.isInstalled = installedAppIds.includes(storedApp.id) + storedApp.isInstalled = installedAppNames.includes(storedApp.name) } - for (const installedAppId of installedAppIds) { + for (const installedAppName of installedAppNames) { const installedAppInStorage = state.apps.some( - ({ id }) => id === installedAppId, + ({ name }) => name === installedAppName, ) if (!installedAppInStorage) { state.apps.push({ hotCode: null, - id: installedAppId, isInstalled: true, + name: installedAppName, }) } } @@ -82,7 +82,7 @@ const storage = createReducer(defaultStorage, (builder) => } const appIndex = state.apps.findIndex( - (app) => app.id === action.payload.appId, + (app) => app.name === action.payload.appName, ) state.apps[appIndex].hotCode = hotCode @@ -102,11 +102,11 @@ const storage = createReducer(defaultStorage, (builder) => .addCase(reorderedApp, (state, action) => { const sourceIndex = state.apps.findIndex( - (app) => app.id === action.payload.sourceId, + (app) => app.name === action.payload.sourceName, ) const destinationIndex = state.apps.findIndex( - (app) => app.id === action.payload.destinationId, + (app) => app.name === action.payload.destinationName, ) const [removed] = state.apps.splice(sourceIndex, 1)