Last active
May 14, 2024 16:24
-
-
Save subtleGradient/4dae70da87128229210afcf47f4341ff to your computer and use it in GitHub Desktop.
Bun.sh script to add a new LSItemContentType to ~/Library/QuickLook/QLStephen.qlgenerator/Contents/Info.plist for any file path
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bun | |
import { $, BunFile, file } from "bun" | |
type CFBundleDocumentType = { | |
CFBundleTypeName: "Content" | |
CFBundleTypeRole: "QLGenerator" | |
LSItemContentTypes: ["public.data", "public.content", ...string[]] | |
LSTypeIsPackage: false | |
NSPersistentStoreTypeKey: "XML" | |
} | |
type InfoPlist = { | |
CFBundleExecutable: "QLStephen" | |
CFBundleDocumentTypes: [CFBundleDocumentType] | |
} | |
type mdlsOutput = { | |
_kMDItemDisplayNameWithExtensions: unknown | |
kMDItemContentCreationDate: unknown | |
kMDItemContentCreationDate_Ranking: unknown | |
kMDItemContentModificationDate: unknown | |
kMDItemContentType: string | |
kMDItemContentTypeTree: string[] | |
kMDItemDateAdded: unknown | |
kMDItemDisplayName: unknown | |
kMDItemDocumentIdentifier: unknown | |
kMDItemFSContentChangeDate: unknown | |
kMDItemFSCreationDate: unknown | |
kMDItemFSCreatorCode: unknown | |
kMDItemFSFinderFlags: unknown | |
kMDItemFSInvisible: unknown | |
kMDItemFSIsExtensionHidden: unknown | |
kMDItemFSLabel: unknown | |
kMDItemFSName: unknown | |
kMDItemFSOwnerGroupID: unknown | |
kMDItemFSOwnerUserID: unknown | |
kMDItemFSSize: unknown | |
kMDItemFSTypeCode: unknown | |
kMDItemInterestingDate_Ranking: unknown | |
kMDItemKind: unknown | |
kMDItemLogicalSize: unknown | |
kMDItemPhysicalSize: unknown | |
} | |
const PlistAsJSON = { | |
read: async (plistPath: string | BunFile): Promise<InfoPlist> => | |
await $`cat < ${plistPath} | plutil -convert json -o - -`.json(), | |
write: async (plistPath: string | BunFile, info: InfoPlist) => | |
$`echo ${JSON.stringify(info)} | plutil -convert xml1 -o ${plistPath} -`, | |
} | |
const mdls = async <N extends keyof mdlsOutput>(theFile: BunFile, name: N): Promise<Pick<mdlsOutput, N>> => | |
await $`mdls -plist - -name ${name} ${theFile} | plutil -convert json -o - -`.json() | |
const refresh_ql_cache = async () => await $`qlmanage -r; qlmanage -r cache; killall Finder; killall -9 QuickLookSatellite` | |
const QLStephen$Info_plist = { | |
file: file(`${process.env.HOME!}/Library/QuickLook/QLStephen.qlgenerator/Contents/Info.plist`), | |
__cache__: null as null | InfoPlist, | |
async json() { | |
return (QLStephen$Info_plist.__cache__ ??= await PlistAsJSON.read(QLStephen$Info_plist.file)) | |
}, | |
async write() { | |
const info = await QLStephen$Info_plist.json() | |
QLStephen$Info_plist.__cache__ = null | |
return PlistAsJSON.write(QLStephen$Info_plist.file, info) | |
}, | |
LSItemContentTypes: { | |
read: async () => (await QLStephen$Info_plist.json()).CFBundleDocumentTypes[0].LSItemContentTypes, | |
push: async (newLSItemContentType: string) => { | |
const info = await QLStephen$Info_plist.json() | |
// abort if already added | |
if (info.CFBundleDocumentTypes[0].LSItemContentTypes.includes(newLSItemContentType)) | |
throw new Error(`Already added: ${newLSItemContentType}`) | |
info.CFBundleDocumentTypes[0].LSItemContentTypes.push(newLSItemContentType) | |
return QLStephen$Info_plist.write() | |
}, | |
}, | |
} | |
type PluginInfo = { | |
path: string | |
version?: string | |
loaded?: boolean | |
} | |
type QuickLookData = { | |
serverInfo: string | |
memoryUsage: string | |
descriptors: string | |
lastBurst: string | |
plugins: Record<string, PluginInfo> | |
} | |
function parseQuickLookOutput(output: string): QuickLookData { | |
const lines = output.split("\n") | |
const plugins: Record<string, PluginInfo> = {} | |
let serverInfo = "" | |
let memoryUsage = "" | |
let descriptors = "" | |
let lastBurst = "" | |
lines.forEach(line => { | |
if (line.startsWith("server:")) { | |
serverInfo = line | |
} else if (line.startsWith("memory used:")) { | |
memoryUsage = line | |
} else if (line.includes("used descriptors:")) { | |
descriptors = line | |
} else if (line.startsWith("last burst:")) { | |
lastBurst = line | |
} else if (line.startsWith(" ")) { | |
// Plugin lines are indented | |
const match = line.trim().match(/^([^ ]+) -> (.+?)( \(([^)]+)\))?$/) | |
if (match) { | |
const [_, type, path, __, versionDetail] = match | |
const loaded = versionDetail?.includes("loaded") | |
const version = versionDetail?.replace(" - loaded", "") || undefined | |
plugins[type] = { path, version, loaded: loaded || undefined } | |
} | |
} | |
}) | |
return { | |
serverInfo, | |
memoryUsage, | |
descriptors, | |
lastBurst, | |
plugins, | |
} | |
} | |
type GroupedPlugins = Record<string, Array<{ type: string; version?: string; loaded?: boolean }>> | |
function groupPluginsByPath(data: QuickLookData): GroupedPlugins { | |
const grouped: GroupedPlugins = {} | |
for (const [type, { path, version, loaded }] of Object.entries(data.plugins)) { | |
if (!grouped[path]) { | |
grouped[path] = [] | |
} | |
grouped[path].push({ type, version, loaded }) | |
} | |
return grouped | |
} | |
function getPluginsByType(data: QuickLookData, types: string[]): Record<string, PluginInfo> { | |
const filteredPlugins: Record<string, PluginInfo> = {} | |
types.forEach(type => { | |
if (data.plugins[type]) { | |
filteredPlugins[type] = data.plugins[type] | |
} | |
}) | |
return filteredPlugins | |
} | |
// const skdjfhsdkhj = groupPluginsByPath(parseQuickLookOutput(await $`qlmanage -m`.text())) | |
// console.log(skdjfhsdkhj) | |
// const skdjfhsdkjh = getPluginsByType( | |
// parseQuickLookOutput(await $`qlmanage -m`.text()), | |
// (await mdls(file(__filename), "kMDItemContentTypeTree")).kMDItemContentTypeTree, | |
// ) | |
// console.log(skdjfhsdkjh) | |
// if (skdjfhsdkhj) process.exit(1) // DEBUG | |
// Main function to add QuickLook support for a file type | |
async function addQuickLookSupport(filePath: string) { | |
const theFile = file(filePath) | |
if (!(await theFile.exists())) throw new Error(`File not found: ${filePath}`) | |
const { kMDItemContentType } = await mdls(theFile, "kMDItemContentType") | |
console.log(`Content type of ${filePath}: ${kMDItemContentType}`) | |
if (!kMDItemContentType) throw new Error(`Could not determine the content type of ${filePath}`) | |
try { | |
await QLStephen$Info_plist.LSItemContentTypes.push(kMDItemContentType) | |
} catch (error) { | |
if (error.message.startsWith("Already added:")) { | |
console.log(`QuickLook support already added for ${filePath} ${kMDItemContentType}`) | |
return process.exit(0) | |
} | |
throw error | |
} | |
if ((await QLStephen$Info_plist.LSItemContentTypes.read()).includes(kMDItemContentType)) await refresh_ql_cache() | |
else throw new Error(`Failed to add QuickLook support for ${filePath} ${kMDItemContentType}`) | |
console.log(`Added QuickLook support for ${filePath} ${kMDItemContentType}`) | |
} | |
// Get the file path from the command line arguments and run the main function | |
const filePath = process.argv[2] | |
await addQuickLookSupport(filePath) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment