diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 1cd73164..febb553b 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -69,6 +69,7 @@ body: - Window Corners - App Icon - Hide Window Controls + - Skip Account - Allow 1Password - Allow Bitwarden - Allow iCloud Passkeys diff --git a/src/dylib/Patches/SkipAccount/Code.m b/src/dylib/Patches/SkipAccount/Code.m new file mode 100644 index 00000000..b5e1501f --- /dev/null +++ b/src/dylib/Patches/SkipAccount/Code.m @@ -0,0 +1,36 @@ +#import "Startup.h" +#import +#import +#import +#import + +uint32_t *DPSkipFindCode(const DPSkipInstruction *signature, NSUInteger length) { + const struct mach_header_64 *header = NULL; + for (uint32_t i = 0; i < _dyld_image_count(); i++) + if (_dyld_get_image_header(i)->filetype == MH_EXECUTE) header = (const struct mach_header_64 *)_dyld_get_image_header(i); + if (!header) return NULL; + unsigned long size = 0; + const uint32_t *text = (const uint32_t *)getsectiondata(header, "__TEXT", "__text", &size); + uint32_t *found = NULL; + for (NSUInteger i = 0; text && i + length <= size / sizeof *text; i++) { + NSUInteger j = 0; + while (j < length && (text[i + j] & signature[j].mask) == signature[j].value) j++; + if (j != length) continue; + if (found) return NULL; + found = (uint32_t *)(text + i); + } + return found; +} + +BOOL DPSkipWriteCode(uint32_t *site, uint32_t expected, uint32_t value) { + if (*site != expected) return NO; + mach_vm_address_t page = (mach_vm_address_t)site & ~((mach_vm_address_t)vm_page_size - 1); + if (mach_vm_protect(mach_task_self(), page, vm_page_size, FALSE, VM_PROT_READ | VM_PROT_WRITE | VM_PROT_COPY)) return NO; + *site = value; + sys_icache_invalidate(site, sizeof *site); + if (!mach_vm_protect(mach_task_self(), page, vm_page_size, FALSE, VM_PROT_READ | VM_PROT_EXECUTE)) return YES; + *site = expected; + sys_icache_invalidate(site, sizeof *site); + mach_vm_protect(mach_task_self(), page, vm_page_size, FALSE, VM_PROT_READ | VM_PROT_EXECUTE); + return NO; +} diff --git a/src/dylib/Patches/SkipAccount/Menu.m b/src/dylib/Patches/SkipAccount/Menu.m new file mode 100644 index 00000000..2a733160 --- /dev/null +++ b/src/dylib/Patches/SkipAccount/Menu.m @@ -0,0 +1,56 @@ +#import "Startup.h" + +static BOOL DPIsSignIn(NSMenuItem *item) { + return item.menu && item.menu == NSApp.mainMenu.itemArray.firstObject.submenu + && [@[ @"Sign In…", @"Sign In..." ] containsObject:item.title]; +} + +static BOOL DPIsLocalAction(NSMenuItem *item) { + if (!item.menu) return NO; + NSMenu *bar = NSApp.mainMenu; + if (item.menu == bar.itemArray.firstObject.submenu) + return [@[ @"Settings…", @"Settings...", @"Set as Default Browser", @"Import from Another Browser…" ] containsObject:item.title]; + return item.menu && item.menu == [bar itemWithTitle:@"File"].submenu + && [@[ @"New Window", @"New Incognito Window" ] containsObject:item.title]; +} + +static void DPItem_setEnabled(NSMenuItem *self, SEL _cmd, BOOL enabled) { + DPCallOriginal(DPItem_setEnabled, self, enabled || DPIsLocalAction(self)); +} + +static void DPUpdateMenu(NSMenu *menu) { + for (NSMenuItem *item in menu.itemArray) { + if (DPIsSignIn(item)) item.hidden = YES; + if (DPIsLocalAction(item)) item.enabled = YES; + } +} + +static void DPItem_setHidden(NSMenuItem *self, SEL _cmd, BOOL hidden) { + DPCallOriginal(DPItem_setHidden, self, hidden || DPIsSignIn(self)); +} + +static void DPMenu_addItem(NSMenu *self, SEL _cmd, NSMenuItem *item) { + DPCallOriginal(DPMenu_addItem, self, item); + if (DPIsSignIn(item)) item.hidden = YES; + if (DPIsLocalAction(item)) item.enabled = YES; +} + +static void DPMenu_insertItem(NSMenu *self, SEL _cmd, NSMenuItem *item, NSInteger index) { + DPCallOriginal(DPMenu_insertItem, self, item, index); + if (DPIsSignIn(item)) item.hidden = YES; + if (DPIsLocalAction(item)) item.enabled = YES; +} + +static void DPMenu_update(NSMenu *self, SEL _cmd) { + DPCallOriginal(DPMenu_update, self); + DPUpdateMenu(self); +} + +void DPSkipAccountMenus(void) { + DPHookClass(NSMenuItem.class, @selector(setHidden:), (IMP)DPItem_setHidden); + DPHookClass(NSMenuItem.class, @selector(setEnabled:), (IMP)DPItem_setEnabled); + DPHookClass(NSMenu.class, @selector(addItem:), (IMP)DPMenu_addItem); + DPHookClass(NSMenu.class, @selector(insertItem:atIndex:), (IMP)DPMenu_insertItem); + DPHookClass(NSMenu.class, @selector(update), (IMP)DPMenu_update); + DPOnLaunch(^{ DPUpdateMenu(NSApp.mainMenu.itemArray.firstObject.submenu); }); +} diff --git a/src/dylib/Patches/SkipAccount/Settings.m b/src/dylib/Patches/SkipAccount/Settings.m new file mode 100644 index 00000000..b000191d --- /dev/null +++ b/src/dylib/Patches/SkipAccount/Settings.m @@ -0,0 +1,34 @@ +#import "Startup.h" + +static NSHashTable *DPAccountSections; + +static BOOL DPIsAccountHeader(NSView *header) { + NSTextField *title = (NSTextField *)DPFindView(header, ^BOOL(NSView *view) { return [view isKindOfClass:NSTextField.class]; }); + return title.stringValue && [@[ @"Account", @"Morning Brief", @"Sync" ] containsObject:title.stringValue]; +} + +static void DPSection_setHidden(NSView *self, SEL _cmd, BOOL hidden) { + DPCallOriginal(DPSection_setHidden, self, hidden || [DPAccountSections containsObject:self]); +} + +static void DPHeader_layout(NSView *self, SEL _cmd) { + DPCallOriginal(DPHeader_layout, self); + // Hide the arranged section, including its heading, controls, and any account error message. + if (!DPIsAccountHeader(self) || ![self.superview isKindOfClass:NSStackView.class]) return; + [DPAccountSections addObject:self.superview]; + self.superview.hidden = YES; +} + +void DPSkipAccountSettings(void) { + DPAccountSections = [NSHashTable weakObjectsHashTable]; + DPHookClass(NSStackView.class, @selector(setHidden:), (IMP)DPSection_setHidden); + DPHook(DPDiaSettingsHeader, @selector(layout), (IMP)DPHeader_layout); +} + +NSString *DPSkipVerifyAccountSettings(void) { + for (NSView *header in DPFindViews(DPDiaSettingsHeader)) + if (DPIsAccountHeader(header) && !header.isHiddenOrHasHiddenAncestor) return @"account-only settings section is visible"; + for (NSView *button in DPFindViews("NSButton")) + if (DPHasLabel(button, @"More account options") && !button.isHiddenOrHasHiddenAncestor) return @"account options button is visible"; + return nil; +} diff --git a/src/dylib/Patches/SkipAccount/SkipAccount.m b/src/dylib/Patches/SkipAccount/SkipAccount.m new file mode 100644 index 00000000..95b19225 --- /dev/null +++ b/src/dylib/Patches/SkipAccount/SkipAccount.m @@ -0,0 +1,87 @@ +#import "Startup.h" +#import "Updates/Updates.h" + +extern DPPatch DPSkipAccount; +static BOOL gSkipping; + +static id DPDefaults_objectForKey(NSUserDefaults *self, SEL _cmd, NSString *key) { + return [key isEqual:@"hasPresentedOnboardingIntro"] ? @YES : DPCallOriginal(DPDefaults_objectForKey, self, key); +} + +static BOOL DPDefaults_boolForKey(NSUserDefaults *self, SEL _cmd, NSString *key) { + return [key isEqual:@"hasPresentedOnboardingIntro"] ? YES : DPCallOriginal(DPDefaults_boolForKey, self, key); +} + +static void DPWaitForBrowser(void) { + __block NSUInteger polls = 0; + [NSTimer scheduledTimerWithTimeInterval:0.5 repeats:YES block:^(NSTimer *timer) { + void **slot = DPIvar(NSApp.delegate, "controller"); + id controller = slot ? (__bridge id)*slot : nil; + BOOL restored = [DPSwiftValue(controller, @"hasFinishedRestoringWindows") boolValue]; + if (!restored && ++polls < 120) return; + [timer invalidate]; + if (!restored) return; + for (NSWindow *window in NSApp.windows) if (DPIsKind(window, DPDiaWindow)) return; + // The File item carries the default profile even before any windows have been opened. + NSMenu *file = [NSApp.mainMenu itemWithTitle:@"File"].submenu; + for (NSMenuItem *item in file.itemArray) { + if (![item.title hasPrefix:@"New Window"]) continue; + [NSApp sendAction:item.action to:item.target from:item]; + break; + } + }]; +} + +static NSString *DPSkipAccountProblem(DPPatch *patch) { + if (gSkipping || ![NSUserDefaults.standardUserDefaults boolForKey:@"hasUserAuthenticated"]) return nil; + return @"This Dia has signed in to an account before. Skip Account only works on a Dia that never has."; +} + +static void DPInstallSkipAccount(void) { + if (!DPSkipAccount.on || DPSkipAccountProblem(&DPSkipAccount)) return; + gSkipping = DPSkipAccountStartup(); + if (gSkipping) { + DPHookClass(NSUserDefaults.class, @selector(objectForKey:), (IMP)DPDefaults_objectForKey); + DPHookClass(NSUserDefaults.class, @selector(boolForKey:), (IMP)DPDefaults_boolForKey); + DPSkipAccountMenus(); + DPSkipAccountSettings(); + DPOnLaunch(^{ DPWaitForBrowser(); }); + return; + } + DPSkipAccount.unavailable = YES; + DPSkipAccount.on = NO; + NSLog(@"DiaPatches: Skip Account doesn't know this Dia's startup code, so it stays off"); +} + +static void DPApplySkipAccount(DPPatch *patch) { + if (patch->on == gSkipping || patch->on != DPSavedOn(patch) || DPSkipAccountProblem(patch)) return; + NSAlert *alert = [NSAlert new]; + alert.messageText = patch->on ? @"Relaunch Dia to skip account sign-in" : @"Relaunch Dia to use account sign-in"; + [alert addButtonWithTitle:@"Relaunch"]; + [alert addButtonWithTitle:@"Later"]; + if ([alert runModal] == NSAlertFirstButtonReturn) DPHandOffAndQuit(@[ @"apply" ]); +} + +static NSString *DPVerifySkipAccount(DPPatch *patch) { + if (!gSkipping || !patch->on) return nil; // Startup changes need separate launches, not the self-test's unsaved toggles. + for (NSMenuItem *item in NSApp.mainMenu.itemArray.firstObject.submenu.itemArray) + if ([item.title hasPrefix:@"Sign In"] && !item.hidden) return @"sign-in menu item is visible"; + NSString *settings = DPSkipVerifyAccountSettings(); + if (settings) return settings; + return DPFindViews(DPDiaContentPane).count ? nil : @"no browser page opened after skipping sign-in"; +} + +DP_PATCH(DPSkipAccount) { + .key = @"skipAccount", + .order = 120, + .title = @"Skip Account", + .detail = @"Use Dia without signing in. Relaunch to apply. Hides Sign In and the Account, Morning Brief and Sync settings. Only works on a Dia that has never signed in. Chat needs an account, so pair it with Turn Off AI. Experimental.", + .symbol = @"person.crop.circle.badge.minus person.crop.circle", + .tint = @"systemBrownColor", + .section = @"Window", + .classes = DPClasses(DPDiaAppDelegate, DPDiaSettingsHeader), + .install = DPInstallSkipAccount, + .apply = DPApplySkipAccount, + .verify = DPVerifySkipAccount, + .ready = DPSkipAccountProblem, +}; diff --git a/src/dylib/Patches/SkipAccount/Startup.h b/src/dylib/Patches/SkipAccount/Startup.h new file mode 100644 index 00000000..2f77bf7e --- /dev/null +++ b/src/dylib/Patches/SkipAccount/Startup.h @@ -0,0 +1,9 @@ +#import "StdPatch/StdPatch.h" + +typedef struct { uint32_t value, mask; } DPSkipInstruction; +uint32_t *DPSkipFindCode(const DPSkipInstruction *signature, NSUInteger length); +BOOL DPSkipWriteCode(uint32_t *site, uint32_t expected, uint32_t value); +BOOL DPSkipAccountStartup(void); +void DPSkipAccountMenus(void); +void DPSkipAccountSettings(void); +NSString *DPSkipVerifyAccountSettings(void); diff --git a/src/dylib/Patches/SkipAccount/Startup.m b/src/dylib/Patches/SkipAccount/Startup.m new file mode 100644 index 00000000..027b632e --- /dev/null +++ b/src/dylib/Patches/SkipAccount/Startup.m @@ -0,0 +1,53 @@ +#import "Startup.h" + +// Dia 1.57.1 (90935), arm64. Unknown or ambiguous code stays untouched. +// initializeAuth() has finished initializing auth here. With no user it waits for sign-in; otherwise it returns .restored. +static const DPSkipInstruction DPLaunch[] = { + {0xb24403bd, 0xffffffff}, + {0xd10083ff, 0xffffffff}, + {0xa9017bfd, 0xffffffff}, + {0xf90007f6, 0xffffffff}, + {0x910043fd, 0xffffffff}, + {0x3944e6c8, 0xffffffff}, + {0x7100051f, 0xffffffff}, + {0x540001a1, 0xffffffff}, + {0xf940b2c0, 0xffffffff}, + {0xf940aad4, 0xffffffff}, + {0x94000000, 0xfc000000}, + {0x528000e0, 0xffffffff}, + {0x94000000, 0xfc000000}, + {0xf94006c1, 0xffffffff}, + {0x52800040, 0xffffffff}, + {0xd2800014, 0xffffffff}, + {0xa9417bfd, 0xffffffff}, + {0x9243fbbd, 0xffffffff}, + {0x910083ff, 0xffffffff}, + {0xd61f0020, 0xffffffff}, +}; + +// Window creation separately rejects a signed-out user. Keep the readiness and termination checks around it. +// The first load's field offset changes in every Dia release (0xf28, 0xf78, 0xf68), so only its registers are matched. +static const DPSkipInstruction DPWindow[] = { + {0xf9400114, 0xffc003ff}, + {0xb4000014, 0xff00001f}, + {0xaa1403e0, 0xffffffff}, + {0x94000000, 0xfc000000}, + {0x94000000, 0xfc000000}, + {0xaa0003f3, 0xffffffff}, + {0xaa1403e0, 0xffffffff}, + {0x94000000, 0xfc000000}, + {0x36002133, 0xffffffff}, + {0xf946eed4, 0xffffffff}, + {0xf946eac0, 0xffffffff}, + {0x52800021, 0xffffffff}, +}; + +BOOL DPSkipAccountStartup(void) { + uint32_t *launch = DPSkipFindCode(DPLaunch, sizeof DPLaunch / sizeof *DPLaunch); + uint32_t *window = DPSkipFindCode(DPWindow, sizeof DPWindow / sizeof *DPWindow); + if (!launch || !window) return NO; + if (!DPSkipWriteCode(launch + 7, DPLaunch[7].value, 0xd503201f)) return NO; + if (DPSkipWriteCode(window + 8, DPWindow[8].value, 0xd503201f)) return YES; + DPSkipWriteCode(launch + 7, 0xd503201f, DPLaunch[7].value); + return NO; +} diff --git a/src/dylib/Patches/TurnOffAI/TurnOffAI.m b/src/dylib/Patches/TurnOffAI/TurnOffAI.m index 23f30c36..18b06495 100644 --- a/src/dylib/Patches/TurnOffAI/TurnOffAI.m +++ b/src/dylib/Patches/TurnOffAI/TurnOffAI.m @@ -3,7 +3,6 @@ #define DPDiaCapabilitiesToolbar "_TtC23AssistantCapabilitiesUI32AssistantCapabilitiesToolbarView" #define DPDiaTabCardActions "_TtC5TabUI21TabPopoverActionsView" -#define DPDiaSettingsHeader "_TtC11Preferences25SettingsSectionHeaderView" #define DPDiaPlaceholderText "_TtC5ARCUI23TextViewWithPlaceholder" #define DPDiaChatSidebar "_TtC6Window30AssistantSidebarViewController" diff --git a/src/dylib/StdPatch/Dia.h b/src/dylib/StdPatch/Dia.h index e121efb2..cb066d9b 100644 --- a/src/dylib/StdPatch/Dia.h +++ b/src/dylib/StdPatch/Dia.h @@ -37,6 +37,7 @@ #define DPDiaTabSwitcher "_TtC10RecentTabs26RecentTabsWindowController" #define DPDiaTabSwitcherList "_TtC10RecentTabs24RecentTabsViewController" #define DPDiaSettingsController "_TtC11Preferences38SidebarPreferencesWindowViewController" +#define DPDiaSettingsHeader "_TtC11Preferences25SettingsSectionHeaderView" #define DPDiaSettingsSidebar "_TtC11Preferences15SidebarListView" #define DPDiaSpaceSwitcher "_TtC10TabSidebar17SpaceSwitcherView" #define DPDiaSpaceSwitcherItem "_TtC10TabSidebar21SpaceSwitcherItemView"