HaptiX is a MobileSubstrate (Substitute/libhooker/ElleKit) dylib that is injected by the jailbreak's process-injection daemon into every process whose bundle ID matches the filter declared in HaptiX.plist. The filter specifies com.apple.UIKit (catches all UIKit-linked apps) and com.apple.springboard (the home screen process).
At runtime the Logos %ctor constructor fires immediately after injection. It calls loadPrefs() to read the user's preferences via CFPreferencesCopyAppValue, which bypasses the app sandbox by communicating directly with cfprefsd. A Darwin notification observer is then registered on the com.eolnmsuk.haptix/ReloadPrefs channel so that changes made in Settings are applied live without a respring.
Two Logos %group blocks partition the hooks:
- UIKitHooks — installed unconditionally in every injected process. Hooks
UIKeyboardImpl,UIControl,UISwitch,UITableViewCell, andUIScrollView. - SpringBoardHooks — installed only when
bundleIdentifier == com.apple.springboard. HooksSBVolumeControl,SBLockHardwareButton,SBLockScreenManager,SBIconView,SBHomeGesturePanGestureRecognizer, andSBFluidSwitcherViewController.
Haptic feedback is delivered via UIImpactFeedbackGenerator on the main queue through triggerHapticWithOverride(NSInteger overrideStyle). Each hook passes its corresponding style_hook* static variable; when that value is 0 the global hapticStyle setting is used, otherwise the per-trigger value (1–5 → Light/Medium/Heavy/Soft/Rigid, shifted by 1 to align with zero-indexed UIImpactFeedbackStyle) takes precedence. A 50ms time-gate (lastHapticTime) prevents the Taptic Engine from double-firing when rapid sequential UI events occur.
The preferences bundle (haptixprefs) is a separate compiled target installed to /Library/PreferenceBundles/haptixprefs.bundle. PreferenceLoader discovers it via the entry.plist copied to /Library/PreferenceLoader/Preferences/HaptiX.plist by the root Makefile's internal-stage:: hook.
Three .deb packages are produced per CI run by .github/workflows/build.yml:
| Package | THEOS_PACKAGE_SCHEME |
ARCHS |
AltList | Target |
|---|---|---|---|---|
_iphoneos-arm.deb (rootful) |
(unset) | arm64 + arm64e | New (vendor/AltList.framework) |
palera1n (rootful) |
_iphoneos-arm64.deb (rootless) |
rootless |
arm64 + arm64e | New (vendor/AltList.framework) |
Dopamine 2, RootHide, palera1n (rootless) |
_iphoneos-arm_legacy.deb (legacy rootful) |
(unset) | arm64 only | Old (swapped in-place) | Checkra1n, unc0ver, Electra |
CI is split into two parallel jobs: build-modern produces both rootful and rootless .deb files (iOS 15+, SDK 16.5); build-legacy produces the iOS 13–14 .deb (SDK 14.5). A release job waits on both via needs:, collects the artifacts, and opens a draft GitHub Release tagged v{VERSION}-b{RUN_NUMBER}.
All three builds use the root Makefile — TARGET and ARCHS are passed as CLI overrides. Makefile.legacy is provided only for local development convenience; CI always uses explicit CLI args.
The build-legacy job (and build_all.sh locally) swaps vendor/AltList.framework in-place: it copies vendor/AltList_Old.framework over vendor/AltList.framework, then runs lipo -thin arm64 to produce an arm64-only binary. AltList_Old.framework/AltList contains an arm64e slice whose fat-header entry is labeled arm64e but whose slice data uses the old ABI (arm64e.old). Xcode 15/16 linkers validate all fat-header entries before extracting any slice, so even an arm64-only link hard-errors on the mismatch; lipo -thin arm64 avoids this by discarding every other slice. vendor/AltList.framework is restored from vendor/AltList_New.framework after the legacy build completes.
Restricting the legacy build to ARCHS="arm64" is correct — iOS 13–14 legacy jailbreaks target A8–A11 (arm64) devices, and the Theos toolchain cannot produce valid arm64e binaries for iOS < 14.0.
haptixprefs/Makefile uses -F$(THEOS_PROJECT_DIR)/vendor hardcoded for the AltList framework search path. Because the vendor swap replaces the binary at that path in-place, all three builds resolve to the correct AltList binary without any per-build path variable. No THEOS_PACKAGE_SCHEME or ALTLIST_FRAMEWORK_SEARCH_PATH variables exist in any Makefile.
Theos's aggregate.mk propagates the root -f Makefile.legacy flag to every subproject sub-make (via $(firstword $(MAKEFILE_LIST))), so haptixprefs/Makefile.legacy must also exist when using make -f Makefile.legacy locally. Both prefs Makefiles are identical.
After each build, rm -rf packages/* runs alongside make clean because Theos does not remove packages/ on clean, and accumulated .deb files from prior builds would cause the glob staging step to fail.
HaptiX/
│
├── Tweak.x # All hook logic, prefs loading, haptic engine
│ ├── readBoolPref() / readIntegerPref() # Sandbox-bypassing CFPreferences readers
│ ├── loadPrefs() # Reads all pref keys + 10 style_hook* overrides;
│ │ # sets isBlacklisted
│ ├── triggerHapticWithOverride() # UIImpactFeedbackGenerator dispatch; 50ms gate;
│ │ # per-trigger style override (0 = use global)
│ ├── %group UIKitHooks # UIKit process hooks (all injected apps)
│ └── %group SpringBoardHooks # SpringBoard-only hardware hooks
│
├── Makefile # Root build; exports ARCHS ?= arm64 arm64e
│ # and TARGET ?= iphone:clang:16.5:15.0;
│ # no THEOS_PACKAGE_SCHEME default (rootful
│ # by absence); internal-stage:: copies entry.plist
│
├── Makefile.legacy # Root build for legacy package (local use);
│ # exports ARCHS ?= arm64 and
│ # TARGET ?= iphone:clang:14.5:13.0;
│ # no THEOS_PACKAGE_SCHEME (rootful by absence);
│ # requires in-place vendor swap before use
│
├── build_all.sh # Local full build script; mirrors CI exactly:
│ # swaps AltList in-place, builds all 3 .deb
│ # packages, restores vendor/AltList.framework
│
├── HaptiX.plist # Substrate filter: com.apple.UIKit +
│ # com.apple.springboard
│
├── control # Debian package metadata (version, deps, arch)
├── depiction.json # Sileo/Zebra native depiction page
├── icon@2x.png # 512×512 repo-root icon for Sileo depiction
│ # and package manager listing (referenced in
│ # control Icon: field and depiction.json)
├── LICENSE # Project license
├── CHANGELOG.md # Version history
├── COMPATIBILITY.md # Device/jailbreak matrix, conflict guide
├── ProjectStructure.md # This file
├── README.md # User-facing documentation
│
├── haptixprefs/ # PreferenceBundle target
│ ├── Makefile # BUNDLE_NAME=haptixprefs;
│ │ # -F$(THEOS_PROJECT_DIR)/vendor for AltList;
│ │ # TARGET ?= iphone:clang:16.5:13.0 (overridable)
│ ├── Makefile.legacy # Identical to Makefile; required because Theos
│ │ # aggregate.mk re-passes -f <name> to sub-makes,
│ │ # so make -f Makefile.legacy at root causes Theos
│ │ # to invoke make -f Makefile.legacy in haptixprefs/
│ ├── entry.plist # PreferenceLoader entry point (copied to
│ │ # /Library/PreferenceLoader/Preferences/ at build)
│ ├── HaptixPrefsRootListController.h
│ ├── HaptixPrefsRootListController.m # resetSettings() — clears prefs via
│ │ # CFPreferences API; posts ReloadPrefs notify;
│ │ # viewDidLoad installs banner header + footer;
│ │ # traitCollectionDidChange: swaps banner live;
│ │ # buildFooterView — version/env label footer;
│ │ # openGitHub / openDonate — Info section actions;
│ │ # HXDetectJailbreakEnvironment() — passive env
│ │ # detection (bundle path + RootHide marker)
│ └── Resources/
│ ├── Info.plist # Bundle metadata for the PreferenceBundle
│ ├── Root.plist # Declarative Settings UI (PSSpecifiers):
│ │ # enabled, hapticStyle (5 profiles), 10 hook
│ │ # toggles, Advanced Settings (10 per-trigger
│ │ # PSLinkListCell overrides), AltList exclusion
│ │ # list, Info (GitHub/Donate), Reset button
│ ├── icon.png # Settings app icon — 29×29 (@1x)
│ ├── icon@2x.png # Settings app icon — 58×58 (@2x)
│ ├── icon@3x.png # Settings app icon — 87×87 (@3x)
│ ├── iconRaw.png # Uncompressed source for icon variants
│ ├── banner.png # Settings pane header — Dark Mode
│ ├── banner2.png # Settings pane header — Light Mode
│ ├── bannerRaw.png # Uncompressed source for banner.png
│ └── banner2Raw.png # Uncompressed source for banner2.png
│
├── vendor/
│ ├── AltList.framework/ # Active AltList for current build; swapped
│ │ # in-place by CI and build_all.sh — new AltList
│ │ # for rootful/rootless, Old (stripped) for legacy
│ │ ├── AltList # Framework binary
│ │ ├── Info.plist
│ │ ├── Headers/ # Public Objective-C headers
│ │ │ ├── ATLApplicationListMultiSelectionController.h
│ │ │ ├── ATLApplicationListSelectionController.h
│ │ │ ├── ATLApplicationListControllerBase.h
│ │ │ ├── ATLApplicationListSubcontroller.h
│ │ │ ├── ATLApplicationListSubcontrollerController.h
│ │ │ ├── ATLApplicationSection.h
│ │ │ ├── ATLApplicationSelectionCell.h
│ │ │ ├── ATLApplicationSubtitleCell.h
│ │ │ ├── ATLApplicationSubtitleSwitchCell.h
│ │ │ └── LSApplicationProxy+AltList.h
│ │ └── *.lproj/ # Localizations (ar, de, en, fr, it, ja, ko,
│ │ # nl, pl, pt, ru, sk, tr, zh, zh-Hant)
│ │
│ ├── AltList_New.framework/ # New AltList — arm64 + arm64e universal;
│ │ # source for rootful/rootless builds; restored
│ │ # to vendor/AltList.framework after legacy build
│ │
│ └── AltList_Old.framework/ # Legacy AltList — armv7 + arm64 + arm64e
│ # universal (arm64e slice uses old ABI /
│ # arm64e.old, causing fat-header mismatch on
│ # Xcode 15/16 linkers). CI and build_all.sh
│ # copy it in-place then run lipo -thin arm64
│ # to strip to arm64-only before linking.
│
└── .github/
└── workflows/
└── build.yml # GitHub Actions: parallel build-modern job
# (rootful + rootless, SDK 16.5) and
# build-legacy job (iOS 13-14, SDK 14.5,
# in-place AltList swap + lipo -thin arm64);
# release job waits on both, uploads 3 .deb
# artifacts, opens draft release