qlstephen: Add support for dynamic and unknown UTI types

I get a lot of plain text files with various extensions such as .1 or .inc and currently they only show the icon in QuickLook. Looking at one such file with mdls I see it has a dynamic type. But the content type tree includes public.data. Would it be possible to extend support to ALL filetypes?

$ mdls wget-log.2 
...
kMDItemContentType                 = "dyn.ah62d4rv4ge8xe"
kMDItemContentTypeTree             = (
    "dyn.ah62d4rv4ge8xe",
    "public.data",
    "public.item"
)
...

$ mdls function.inc 
...
kMDItemContentType                 = "dyn.ah62d4rv4ge80w5xd"
kMDItemContentTypeTree             = (
    "dyn.ah62d4rv4ge80w5xd",
    "public.item",
    "dyn.ah62d4rv4ge80w5xd",
    "public.data"
)
...

About this issue

  • Original URL
  • State: open
  • Created 5 years ago
  • Comments: 37 (10 by maintainers)

Commits related to this issue

Most upvoted comments

As another solution, I wrote a Fish script that automatically detect the UTI type of a given file and append the dyn.* type to QLStephen:

function ql
	set type (mdls -name kMDItemContentType $argv[1] | sed -n 's/^kMDItemContentType = \"\(.*\)\"$/\1/p')
	echo $type
	plutil -insert CFBundleDocumentTypes.0.LSItemContentTypes.0 -string $type ~/Library/QuickLook/QLStephen.qlgenerator/Contents/Info.plist
	qlmanage -r
end

Usage for Fish shell:

$ ql <drag a file to the terminal window>

So, I’ve come up with what I think might be a workable solution, though I’m looking for thoughts and feedback on it.

Rather than having to worry about generating dyn.* UTIs, etc., all we really need to do is simply have a way to tell the system that an unknown filename extension (e.g. .map) conforms to public.plain-text. By doing so, we then allow the system’s built-in text quicklook plugin to be used to preview the contents.

The solution I came up with is based on the assumption that only the end-user is going to know what filename extensions they come across and whether those files are text-based or not. It uses 2 different apps:

  1. QLStephen.app: the primary app you interact with. It holds a list of the filename extensions that you’ve mapped to being treated as text. To map a particular filename extension to be treated as text, we declare that it conforms to “public.plain-text”. By doing so, we then allow the system’s built-in text quicklook plugin to be used to preview the contents. To implement these file mappings on the system, QLStephen.app uses a second application, “QLStephen Dummy.app”, in which the mappings are declared in the UTExportedTypeDeclarations of its Info.plist file. QLStephen.app also adds a Service so that you can right-click/control-click on any file in the Finder, and choose “Quick Look with QLStephen ✅” (yes, this is a bit misleading, I guess). This automatically opens it in QLStephen.app, and if it’s found to be text-based, is added to the list of file extension mappings. Then, after updating the dummy app, you can deselect and then reselect the file in the Finder to preview its contents as text.

  2. QLStephen Dummy.app: This app is created in the ~/Applications/ folder. As mentioned previously, this is a dummy app that declares the file extension mappings in its ‘UTExportedTypeDeclarations` key in its Info.plist file. After modifying the Info.plist of the installed app, it is codesigned to run locally.

To be clear, these 2 apps exist separately from the QuickLook plugin which is still installed into ~/Library/QuickLook.

Some notes/known issues: • Despite having a “Kind” column for you to fill out a file extension’s description, it appears the Finder will always refer to the kind as “Plain Text Document”. (I think declaring the documents in the CFBundleDocumentTypes as well might convince Launch Services to use their descriptions). • There are some filename extensions that still won’t preview even though they are text-based: .css files and .strings files are 2 examples I’ve encountered.

Screen Shot 2021-02-04 at 2 53 56 PM

Screen Shot 2021-02-04 at 3 03 58 PM

I have a qlstephen.app branch where I pushed this to in my fork: https://github.com/NSGod/qlstephen/tree/qlstephen.app.

A link to a compiled and signed binary: https://markdouma.com/developer/QLStephen.zip I welcome any feedback.

To summarize the current workaround, below are instructions to quick look a given file or file type (*.Rmd in the below example).

This was tested on macOS Big Sur 11.3 Terminal (zsh).

  1. Run the following command on an existing file of interest to find a string that begins with dyn.a...:

    $ mdls -name kMDItemContentType ~/path/file.Rmd
    kMDItemContentType = "dyn.a..."
    
  2. Open Info.plist to edit it, at ~/Library/QuickLook/QLStephen.qlgenerator/Contents.

    To navigate there, you can right click QLstephen.qlgenerator and “Show Package Contents” or use a code editor to get there

  3. Add the relevant dyn.a... to Info.plist like in the following:

    ...
    <key>LSItemContentTypes</key>
    	<array>
    		<string>public.data</string>
    		<string>public.content</string>
    		<string>public.unix-executable</string>
    		<string>dyn.a...</string>
    	</array>
    ...
    
  4. Reset quick look with a) qlmanage -r and b) Relaunch Finder: Apple menu - Force Quit ... - Finder - Relaunch.

Ok, so according to the source I references above, I would do the following:

  1. Generate the dyn content, in this case I guess its ?0=6:1=sql.
    Though I am not sure if the 6 is correct or if it should be 7. Where numbers are substituted as follows:
0: UTTypeConformsTo
1: public.filename-extension
2: com.apple.ostype
3: public.mime-type
4: com.apple.nspboard-type
5: public.url-scheme
6: public.data
7: public.text
8: public.plain-text
9: public.utf16-plain-text
A: com.apple.traditional-mac-plain-text
B: public.image
C: public.video
D: public.audio
E: public.directory
F: public.folder
  1. Next you put this string into a custom base32 converter. E.g. this website

    Input: ?0=6:1=sql
    Variant: Custom
    Alphabet: abcdefghkmnpqrstuvwxyz0123456789
    Padding: – Delete if there is any –

  2. The output should be h62d4rv4ge81g6pq. If you have any trailing = delete it, thats the padding.

  3. Prepend dyn.a and that is your final string.

  4. What you should insert in the Info.plist is dyn.ah62d4rv4ge81g6pq

<key>LSItemContentTypes</key>
<array>
	<string>public.data</string>
	<string>public.content</string>
	<string>public.unix-executable</string>
	<string>dyn.ah62d4rv4ge81g6pq</string>
</array>

PS. You dont need to remove the extension, you can also right-click and ‘Show Package Contents’

After several evenings I’ve stumbled upon a fix and would appreciate a confirmation.

First to showcase the problem: https://github.com/whomwah/qlstephen/compare/master...toy:extensions-problem-showcase?expand=1

  1. Make QLStephen also handle UTI toy.explicit
  2. Add application extensions.app with the goal to define UTIs:
    • toy.explicit (for .toy-explicit extension) conforming to public.plain-text
    • toy.a0 (for .toy-a0 extension) conforming to public.plain-text
      • toy.a1 (for .toy-a1 extension) conforming to toy.a0
        • toy.a2 (for .toy-a2 extension) conforming to toy.a1
    • toy.l0 (for .toy-l0 extension) conforming to public.data
      • toy.l1 (for .toy-l1 extension) conforming to toy.l0
        • toy.l2 (for .toy-l2 extension) conforming to toy.l1
  3. Add files for each UTI + an unknown one with extension .toy-unknown

To test checkout extensions-problem-showcase and run make && make install, in this state only .toy-a0, .toy-a1, .toy-a2 and .toy-explicit should have preview/thumbnail generated (first 3 by Text.qlgenerator, last by QLStephen because it is explicitly in its list of UTIs).

Fix: https://github.com/toy/qlstephen/compare/extensions-problem-showcase...toy:extensions-showcase-fix?expand=1 Just change the bundle id so that it starts with com.apple. and suddenly all test files get the preview/tumbnail. To test checkout extensions-showcase-fix and run make && make install (wait for qlmanage -m to show the list, sometimes repeating make && make install is needed).

If confirmed #135 is the fix unless someone has a better idea

Fun with UTIs Found this to be very helpful in understanding what is happening.

Right now the plugin registers itself at the highest point that makes sense in both the physical and functional hierarchies. public.data and public.content

There is a good case to be made for changing public.content to public.text but that’s not a discussion we need to have here. Let’s figure out how we can get this plugin working for more files.

Execute mdls -name kMDItemContentType ~/path/to/file.ext to get the dyn value, add it to the plist

For anyone wanting the bash/zsh version of the fish function posted by @xupefei

ql() {
  set type (mdls -name kMDItemContentType $argv[1] | sed -n 's/^kMDItemContentType = \"\(.*\)\"$/\1/p')
  echo $type
  plutil -insert CFBundleDocumentTypes.0.LSItemContentTypes.0 -string $type ~/Library/QuickLook/QLStephen.qlgenerator/Contents/Info.plist
  qlmanage -r
}

Add it to your ~/.bashrc or ~/.zshrc to have it available when a new shell is opened.

To summarize the current workaround, below are instructions to quick look a given file or file type (*.Rmd in the below example).

This was tested on macOS Big Sur 11.3 Terminal (zsh).

  1. Run the following command on an existing file of interest to find a string that begins with dyn.a...:
    $ mdls -name kMDItemContentType ~/path/file.Rmd
    kMDItemContentType = "dyn.a..."
    
  2. Open Info.plist to edit it, at ~/Library/QuickLook/QLStephen.qlgenerator/Contents. To navigate there, you can right click QLstephen.qlgenerator and “Show Package Contents” or use a code editor to get there
  3. Add the relevant dyn.a... to Info.plist like in the following:
    ...
    <key>LSItemContentTypes</key>
    	<array>
    		<string>public.data</string>
    		<string>public.content</string>
    		<string>public.unix-executable</string>
    		<string>dyn.a...</string>
    	</array>
    ...
    
  4. Reset quick look with a) qlmanage -r and b) Relaunch Finder: Apple menu - Force Quit ... - Finder - Relaunch.

Works fine, thanks!

Note: command killall Finder equal to Relaunch Finder.

My current understanding of the issue is that:

  1. QuickLook does not fallback to other UTI in kMDItemContentTypeTree
  2. Instead the kMDItemContentType is used to assign the corresponding QL plugin.
  3. Every file extension has its own dyn.* UTI

Which means, we would need a list of extensions that should be supported by QLStephen. That implies that QLStephen can potentially override another QL plugin that does a better job in representing a known extension. E.g., there are specialised markdown plugins and I would be pissed if QLStephen would break them for me. Not sure how QuickLook handles plugin precedence.

Some plugins will define “Imported Type UTIs”. That basically replaces the “unkown” dyn.* UTIs with a developer set UTI. For example, in my extension I register org.opml.opml for *.opml files. On my system, for opml files, mdls returns that UTI instead of dyn.ah62d4rv4ge8086drru.

kMDItemContentType = "org.opml.opml"

This might be crazy talk, but I’d be happy if qlstephen showed me a text view of any file for which it can’t otherwise distinguish. (At least the first few kilobytes of larger files.) Not sure if this is possible.

So we still don’t have a solution ? For now I’m just adding the dyn.foobar entries into the plist

The LICENSE I happened to be trying did indeed have executable permissions (654) for some reason. Changing it to 644 solved the issue with that file. Thanks.

But I do have some actual unix executables (e.g. little extension-less shell scripts in a bin/ directory) that I’d like to see working.

Also, I tried creating a custom dyn.* adopting the public.data UTI without any extension. But that didn’t work either. Setting the extension to * or nothing at all, also didn’t work.

Somehow the UTI is not recognized as being inherited from public.data although the type tree clearly shows that it is. It also correctly displays the preview if the generator is specified directly:

qlmanage -g ~/Library/QuickLook/QLStephen.qlgenerator -c "_" -p MANIFEST.in

So the problem really is, that macOS can’t seem to map dyn.ah62d4rv4ge80w5u (*.in) to public.data. And therefore skips QLStephen because it does not seem to apply to the extension. Which is funny, because it clearly works for *.m files and the Text.qlgenerator.