Preference Bundles

A preference bundle is how your tweak gets a settings pane. There are two kinds: declarative, described entirely by a plist, and a view controller you write yourself.

Bundles install to /Library/TweakInject/Preferences/PreferenceBundles/. Declare a dependency on com.doraorak.preferenceloaderx if you want the pane to appear in System Settings as well as in the app.

Declarative

A Root.plist describing rows. No code, so nothing to load and nothing to crash. Prefer this unless you need a control the format cannot express.

Root.plist
<plist version="1.0">
<dict>
    <key>title</key><string>MyTweak</string>
    <key>items</key>
    <array>
        <dict>
            <key>cell</key><string>PSSwitchCell</string>
            <key>label</key><string>Enabled</string>
            <key>key</key><string>enabled</string>
            <key>default</key><true/>
        </dict>
        <dict>
            <key>cell</key><string>PSSliderCell</string>
            <key>label</key><string>Intensity</string>
            <key>key</key><string>intensity</string>
            <key>min</key><real>0.0</real>
            <key>max</key><real>1.0</real>
            <key>default</key><real>0.5</real>
        </dict>
    </array>
</dict>
</plist>

The key of each row is the preference key your tweak reads. The domain comes from the bundle's Info.plist.

Info.plist

Info.plist keys
<key>CFBundleIdentifier</key><string>com.example.mytweakprefs</string>
<key>NSPrincipalClass</key><string>MyPrefsController</string>
<key>title</key><string>MyTweak</string>
<key>defaults</key><string>com.example.mytweak</string>
<key>icon</key><string>icon.svg</string>

defaults is the domain. It has to be the same string your tweak reads from, or the pane writes where nothing is looking. NSPrincipalClass is only needed for a custom pane.

Custom panes

Write a view controller and adopt PSPreferenceController, which tells you the domain and when the pane appears and disappears.

A custom pane is loaded as code, so a mistake in one can take the settings window down with it. A declarative pane cannot, which is the main reason to prefer one.

Where your bundle has to be

PreferenceLoaderX looks in two places:

  • /Library/TweakInject/Preferences/PreferenceBundles/
  • /Library/TweakInject/Tweaks/Bundles/, where your tweak already is

In each, it checks a bundle for Root.plist, Resources/Root.plist or Contents/Resources/Root.plist.

So a declarative pane needs no separate bundle at all: put Root.plist in your tweak's own Contents/Resources/ and it is found where it sits.

What does not work is nesting a whole .bundle inside your tweak bundle and expecting it to be picked up. The search does not descend into a bundle within a bundle. A separate preference bundle has to be installed to PreferenceBundles/ by your .deb, which is what you want anyway for a custom view-controller pane.

Reading the values back

From the tweak, use PSUserDefaults with the same domain, and register the same defaults the plist declares so both sides agree before anything has been written.