Skip to content

Commit

Permalink
feat: find more exec and its icon on linux (#35)
Browse files Browse the repository at this point in the history
  • Loading branch information
jerry4718 authored Jul 11, 2024
1 parent 00c53d9 commit ce742a1
Show file tree
Hide file tree
Showing 3 changed files with 236 additions and 17 deletions.
69 changes: 52 additions & 17 deletions src/main/platforms/linux.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import type { AppReader } from "./utils";
import os from 'os'
import fs from "fs";
import ini from "ini";
import path from "path";
import { Result } from "ts-results";
import { findIconPath } from './linux/find-icon'
import { findExecPath } from './linux/find-exec'

const desktopFilesDir = "/usr/share/applications";
const userAppsDir = path.join(os.homedir(), ".local/share/applications")
const sysAppsDir = "/usr/share/applications";

const readAppInfo = (desktopFile: string) =>
Result.wrapAsync(async () => {
Expand All @@ -29,22 +33,47 @@ const readAppInfo = (desktopFile: string) =>
exePath = entry.Exec.split(/\s+/)[0] ?? "";
}

if (!exePath.startsWith("/")) {
// try to find real exec file
if (!path.isAbsolute(exePath)) {
exePath = findExecPath(exePath) || exePath
}

if (!path.isAbsolute(exePath)) {
throw new Error("Exec path invalid");
}
if (!fs.existsSync(path.join(exePath, "../resources/electron.asar"))) {

if (
!(
fs.existsSync(path.join(exePath, "../resources/electron.asar"))
||
fs.existsSync(path.join(exePath, "../LICENSE.electron.txt"))
||
fs.existsSync(path.join(exePath, "../chrome-sandbox"))
)
) {
throw new Error("resources/electron.asar not exists");
}

let icon = "";
let iconPath = ""; // todo: set default icon
if (entry.Icon) {
try {
const iconBuffer = await fs.promises.readFile(
`/usr/share/icons/hicolor/1024x1024/apps/${entry.Icon}.png`,
);
icon = "data:image/png;base64," + iconBuffer.toString("base64");
} catch (err) {
console.error(err);
if (path.isAbsolute(entry.Icon)) {
iconPath = entry.Icon
} else {
iconPath = findIconPath(entry.Icon) || iconPath
}

if (fs.existsSync(iconPath)) {
const base64 = await fs.promises.readFile(iconPath, 'base64')

const ext = path.extname(iconPath)
if (ext === '.svg') {
icon = 'data:image/svg+xml;base64,' + base64
}
if (ext === '.png') {
icon = 'data:image/png;base64,' + base64
}
// todo: if (ext === 'xpm') {}
}
}

Expand All @@ -56,16 +85,22 @@ const readAppInfo = (desktopFile: string) =>
};
});

async function readAppDir(dir: string) {
const files = await fs.promises.readdir(dir)
const apps = await Promise.all(
files
.filter((f) => f.endsWith(".desktop"))
.map((file) => readAppInfo(path.join(dir, file))),
);
return apps.flatMap((app) => (app.ok ? [app.unwrap()] : []))
}

export const adapter: AppReader = {
readAll: () =>
Result.wrapAsync(async () => {
const files = await fs.promises.readdir(desktopFilesDir);
const apps = await Promise.all(
files
.filter((f) => f.endsWith(".desktop"))
.map((file) => readAppInfo(path.join(desktopFilesDir, file))),
);
return apps.flatMap((app) => (app.ok ? [app.unwrap()] : []));
const userApps = await readAppDir(userAppsDir)
const sysApps = await readAppDir(sysAppsDir)
return [...userApps, ...sysApps];
}),
readByPath: (p: string) =>
Result.wrapAsync(async () => {
Expand Down
28 changes: 28 additions & 0 deletions src/main/platforms/linux/find-exec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import path from 'path'
import fs from 'fs'

const pathDirs = process.env.PATH
?.split(path.delimiter)
?.filter(dir => fs.existsSync(dir))
|| []

export function findExecPath(command: string) {
for (const pathDir of pathDirs) {
const execPath = path.join(pathDir, command)
if (!fs.existsSync(execPath)) {
continue
}
if (!fs.statSync(execPath).isFile()) {
continue
}
const lstat = fs.lstatSync(execPath)
if (!lstat.isSymbolicLink()) {
return execPath
}
const realpath = fs.realpathSync(execPath)
if (!fs.existsSync(realpath)) {
continue
}
return realpath
}
}
156 changes: 156 additions & 0 deletions src/main/platforms/linux/find-icon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import os from 'os'
import path from 'path'
import fs from 'fs'

const sizeReg = /^\d+(x\d+)?(@\d+x?)?$/
const pixelIgnoreReg = /x\d+$/
const scalaIgnoreReg = /x$/

const MAX_SIZE = 9999

const DEFAULT_XDG_DATA_DIRS = [
path.join(os.homedir(), '.local/share'),
'/usr/local/share',
'/usr/share'
]

const themeIconBases = (process.env.XDG_DATA_DIRS?.split(path.delimiter) || DEFAULT_XDG_DATA_DIRS)
// If dir is not named with 'icons', append 'icons'
.map(dir => path.basename(dir) === 'icons' ? dir : path.join(dir, 'icons'))

const backwards_userIconBase = path.join(os.homedir(), '.icons')
// for backwards compatibility
if (!themeIconBases.includes(backwards_userIconBase) && !themeIconBases.includes(backwards_userIconBase + '/')) {
themeIconBases.unshift(backwards_userIconBase)
}

const themeLeveledBase = [
/* 'xxx', */ // todo: get current theme
'hicolor',
'Papirus',
'default',
'locolor'
]

function initThemeLeveledNames() {
function findThemeNames(base: string): string[] {
if (!fs.existsSync(base)) return []

const files = fs.readdirSync(base)
return files.filter(name => fs.statSync(path.join(base, name)).isDirectory())
}

const themeNames = themeIconBases.map(findThemeNames).flat()

return [
...themeLeveledBase.filter(theme => themeNames.includes(theme)),
...themeNames.filter(theme => !themeLeveledBase.includes(theme))
]
}

type SizeDir = { size: number, path: string }

// theme name sorted by leveled
const themeLeveledNames = initThemeLeveledNames()
const themeSortedDirs = new Map<string, SizeDir[]>(themeLeveledNames.map(theme => [
theme,
themeIconBases.map(base => iconDirSortBySize(findAllIconDirs(base, path.join(base, theme)))).flat()
]))

function parseSize(name: string): number {
if (name === 'scalable') return MAX_SIZE
if (name === 'symbolic') return 0.8
if (!sizeReg.test(name)) return -1
let [ pixel, scala ] = name.split('@')
let size = Number(pixel.replace(pixelIgnoreReg, ''))
let wight = Number(scala?.replace(scalaIgnoreReg, ''))
return (isNaN(size) ? 0 : size) + (isNaN(wight) ? 0 : wight) / 100
}

function findAllIconDirs(base: string, parent = base): SizeDir[] {
if (!fs.existsSync(parent)) return []

const files = fs.readdirSync(parent)
const dirs = files
.map(name => path.join(parent, name))
.filter(path => fs.statSync(path).isDirectory())
if (dirs.length) {
return dirs.map(findAllIconDirs.bind(void 0, base)).flat()
}
// resolve while cannot find child
const relative = path.relative(base, parent)
const pathNames = relative.split(path.sep)
// ignore dir that do not contain 'apps' in the path
if (!pathNames.includes('apps')) return []
// remove that path with 'apps' compatible theme/size/apps and theme/apps/size
const [ _theme, size ] = relative.split('/').filter(name => name !== 'apps')
return [ { path: parent, size: parseSize(size) } ]
}

function findThemeIconDirs(theme: string) {
// return themeIconBases.map(base => findAllIconDirs(base, path.join(base, theme))).flat()
return themeSortedDirs.get(theme) || []
}

function iconDirSortBySize(dirs: SizeDir[]): SizeDir[] {
return dirs.sort((a, b) => b.size - a.size)
}

function accessSync(path: string): boolean {
try {
fs.accessSync(path)
return true
} catch (e) {
return false
}
}

function tryFile(parent: string, name: string, ext?: string): string | undefined {
const fileName = ext ? `${name}.${ext}` : name
const filePath = path.join(parent, fileName)
if (accessSync(filePath)) return filePath
}

const supportIconExt = [ 'png', 'xpm', 'svg' ]

function findIconPathFromSizeDir(name: string, dir: SizeDir): string | undefined {
for (const ext of supportIconExt) {
const tried = tryFile(dir.path, name, ext)
if (tried) return tried
}
}

function findIconPathByTheme(finder: (dir: SizeDir) => string | void, theme: string): string | undefined {
for (const dir of findThemeIconDirs(theme)) {
const found = finder(dir)
if (found) return found
}
}

// pixmaps
const pixmapsBase = '/usr/share/pixmaps'

function findIconFromPixmaps(name: string): string | undefined {
if (!fs.existsSync(pixmapsBase)) return

for (const ext of supportIconExt) {
const tried = tryFile(pixmapsBase, name, ext)
if (tried) return tried
}
}

export function findIconPath(name: string, theme?: string): string | undefined {
const _findIconPathFromSizeDir = findIconPathFromSizeDir.bind(void 0, name)
const _findIconPathByTheme = findIconPathByTheme.bind(void 0, _findIconPathFromSizeDir)
if (theme) {
const found = _findIconPathByTheme(theme)
if (found) return found
}
for (const themeLeveled of themeLeveledNames) {
if (themeLeveled === theme) continue
const found = _findIconPathByTheme(themeLeveled)
if (found) return found
}
const found = findIconFromPixmaps(name)
if (found) return found
}

0 comments on commit ce742a1

Please sign in to comment.