├── .gitignore ├── Delegate.h ├── Delegate.m ├── Document.h ├── Document.m ├── Settings.h ├── Settings.m ├── SettingsMapping.h ├── SettingsMapping.m ├── Utils.m ├── WindowController.h ├── WindowController.m ├── Xcode.m ├── build.zsh └── main.m /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.app 3 | *.zip 4 | -------------------------------------------------------------------------------- /Delegate.h: -------------------------------------------------------------------------------- 1 | @interface Delegate:NSObject 2 | 3 | @property(assign) BOOL projectMode; 4 | @property(retain) NSString* currentScreenKey; 5 | 6 | +(Delegate*)shared; 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /Delegate.m: -------------------------------------------------------------------------------- 1 | enum 2 | { 3 | TagSetting=1, 4 | TagTheme, 5 | TagProjectMode, 6 | TagTab, 7 | TagFileAssociation 8 | }; 9 | 10 | @implementation Delegate 11 | 12 | +(Delegate*)shared 13 | { 14 | return (Delegate*)NSApp.delegate; 15 | } 16 | 17 | -(NSMenu*)addMenuWithTitle:(NSString*)title to:(NSMenu*)bar 18 | { 19 | NSMenuItem* top=NSMenuItem.alloc.init.autorelease; 20 | NSMenu* menu=[NSMenu.alloc initWithTitle:title].autorelease; 21 | top.submenu=menu; 22 | [bar addItem:top]; 23 | return menu; 24 | } 25 | 26 | -(NSMenuItem*)addItemWithTitle:(NSString*)title action:(NSString*)action key:(NSString*)key mask:(NSEventModifierFlags)mask to:(NSMenu*)menu 27 | { 28 | NSMenuItem* item=[NSMenuItem.alloc initWithTitle:title action:NSSelectorFromString(action) keyEquivalent:key?key:@""].autorelease; 29 | item.keyEquivalentModifierMask=mask; 30 | [menu addItem:item]; 31 | return item; 32 | } 33 | 34 | -(NSMenuItem*)addItemWithTitle:(NSString*)title action:(NSString*)action key:(NSString*)key to:(NSMenu*)menu 35 | { 36 | return [self addItemWithTitle:title action:action key:key mask:NSEventModifierFlagCommand to:menu]; 37 | } 38 | 39 | -(void)addSeparatorTo:(NSMenu*)menu 40 | { 41 | [menu addItem:NSMenuItem.separatorItem]; 42 | } 43 | 44 | -(void)applicationWillFinishLaunching:(NSNotification*)note 45 | { 46 | BOOL firstRun=![NSUserDefaults.standardUserDefaults boolForKey:@"launched"]; 47 | if(firstRun) 48 | { 49 | [NSUserDefaults.standardUserDefaults setBool:true forKey:@"launched"]; 50 | Settings.reset; 51 | } 52 | 53 | contextMenuHook=[^() 54 | { 55 | NSMenu* menu=NSMenu.alloc.init.autorelease; 56 | 57 | [self addItemWithTitle:@"Cut" action:@"cut:" key:nil to:menu]; 58 | [self addItemWithTitle:@"Copy" action:@"copy:" key:nil to:menu]; 59 | [self addItemWithTitle:@"Paste" action:@"paste:" key:nil to:menu]; 60 | 61 | return menu; 62 | } copy]; 63 | 64 | NSMenu* bar=NSMenu.alloc.init.autorelease; 65 | 66 | NSMenu* titleMenu=[self addMenuWithTitle:getAppName() to:bar]; 67 | [self addItemWithTitle:[@"About " stringByAppendingString:getAppName()] action:@"handleAbout:" key:nil to:titleMenu]; 68 | [self addSeparatorTo:titleMenu]; 69 | for(NSString* name in Settings.allMappingNames) 70 | { 71 | [self addItemWithTitle:name action:@"handleSettingsToggle:" key:nil to:titleMenu].tag=TagSetting; 72 | } 73 | [self addSeparatorTo:titleMenu]; 74 | [self addItemWithTitle:@"Reset Settings (May Need Reload)" action:@"handleSettingsReset:" key:nil to:titleMenu]; 75 | [self addSeparatorTo:titleMenu]; 76 | [self addItemWithTitle:[@"Hide " stringByAppendingString:getAppName()] action:@"hide:" key:@"h" to:titleMenu]; 77 | [self addItemWithTitle:@"Hide Others" action:@"hideOtherApplications:" key:@"h" mask:NSEventModifierFlagCommand|NSEventModifierFlagOption to:titleMenu]; 78 | [self addItemWithTitle:@"Show All" action:@"unhideAllApplications:" key:nil to:titleMenu]; 79 | [self addSeparatorTo:titleMenu]; 80 | [self addItemWithTitle:[@"Quit " stringByAppendingString:getAppName()] action:@"terminate:" key:@"q" to:titleMenu]; 81 | 82 | NSMenu* fileMenu=[self addMenuWithTitle:@"File" to:bar]; 83 | [self addItemWithTitle:@"New" action:@"newDocument:" key:@"n" to:fileMenu]; 84 | [self addItemWithTitle:@"Open" action:@"openDocument:" key:@"o" to:fileMenu]; 85 | [self addSeparatorTo:fileMenu]; 86 | [self addItemWithTitle:@"Close" action:@"performClose:" key:@"w" to:fileMenu]; 87 | [self addSeparatorTo:fileMenu]; 88 | [self addItemWithTitle:@"Save" action:@"handleSave:" key:@"s" to:fileMenu]; 89 | [self addSeparatorTo:fileMenu]; 90 | [self addItemWithTitle:@"" action:@"handleClaimFileAssociation:" key:nil to:fileMenu].tag=TagFileAssociation; 91 | 92 | NSMenu* editMenu=[self addMenuWithTitle:@"Edit" to:bar]; 93 | [self addItemWithTitle:@"Undo" action:@"undo:" key:@"z" to:editMenu]; 94 | [self addItemWithTitle:@"Redo" action:@"redo:" key:@"Z" to:editMenu]; 95 | [self addSeparatorTo:editMenu]; 96 | [self addItemWithTitle:@"Cut" action:@"cut:" key:@"x" to:editMenu]; 97 | [self addItemWithTitle:@"Copy" action:@"copy:" key:@"c" to:editMenu]; 98 | [self addItemWithTitle:@"Paste" action:@"paste:" key:@"v" to:editMenu]; 99 | [self addSeparatorTo:editMenu]; 100 | [self addItemWithTitle:@"Select All" action:@"selectAll:" key:@"a" to:editMenu]; 101 | [self addSeparatorTo:editMenu]; 102 | [self addItemWithTitle:@"Shift Left" action:@"shiftLeft:" key:@"[" to:editMenu]; 103 | [self addItemWithTitle:@"Shift Right" action:@"shiftRight:" key:@"]" to:editMenu]; 104 | [self addSeparatorTo:editMenu]; 105 | [self addItemWithTitle:@"Find" action:@"findAndReplace:" key:@"f" to:editMenu]; 106 | [self addItemWithTitle:@"Find Next" action:@"findNext:" key:@"g" to:editMenu]; 107 | [self addItemWithTitle:@"Find Previous" action:@"findPrevious:" key:@"G" to:editMenu]; 108 | 109 | NSMenu* viewMenu=[self addMenuWithTitle:@"View" to:bar]; 110 | for(NSString* name in Settings.allThemeNames) 111 | { 112 | [self addItemWithTitle:name action:@"handleSetTheme:" key:nil to:viewMenu].tag=TagTheme; 113 | } 114 | [self addSeparatorTo:viewMenu]; 115 | [self addItemWithTitle:@"Enter Full Screen" action:@"toggleFullScreen:" key:@"f" mask:NSEventModifierFlagCommand|NSEventModifierFlagControl to:viewMenu]; 116 | 117 | NSMenu* windowMenu=[self addMenuWithTitle:@"Window" to:bar]; 118 | [self addItemWithTitle:@"Project Mode" action:@"handleToggleProjectMode:" key:@"p" to:windowMenu].tag=TagProjectMode; 119 | [self addSeparatorTo:windowMenu]; 120 | [self addItemWithTitle:@"Minimize" action:@"performMiniaturize:" key:@"m" to:windowMenu]; 121 | [self addItemWithTitle:@"Zoom" action:@"performZoom:" key:nil to:windowMenu]; 122 | [self addSeparatorTo:windowMenu]; 123 | [self addItemWithTitle:@"Show Previous Tab" action:@"selectPreviousTab:" key:@"←" mask:NSEventModifierFlagCommand|NSEventModifierFlagOption to:windowMenu]; 124 | [self addItemWithTitle:@"Show Next Tab" action:@"selectNextTab:" key:@"→" mask:NSEventModifierFlagCommand|NSEventModifierFlagOption to:windowMenu]; 125 | [self addSeparatorTo:windowMenu]; 126 | for(int index=1;index<10;index++) 127 | { 128 | NSString* title=nil; 129 | if(index==9) 130 | { 131 | title=@"Show Last Tab"; 132 | } 133 | else 134 | { 135 | title=[NSString stringWithFormat:@"Show Tab %d",index]; 136 | } 137 | NSString* key=[NSString stringWithFormat:@"%d",index]; 138 | [self addItemWithTitle:title action:@"handleSelectTab:" key:key to:windowMenu].tag=TagTab; 139 | } 140 | 141 | NSApp.mainMenu=bar; 142 | 143 | self.currentScreenKey=Settings.screenKey; 144 | } 145 | 146 | -(BOOL)validateUserInterfaceItem:(NSObject*)item 147 | { 148 | if([item isKindOfClass:NSMenuItem.class]) 149 | { 150 | NSMenuItem* menuItem=(NSMenuItem*)item; 151 | BOOL checked=false; 152 | BOOL disabled=false; 153 | 154 | switch(menuItem.tag) 155 | { 156 | case TagSetting: 157 | ; 158 | SettingsMapping* mapping=[Settings mappingWithName:menuItem.title]; 159 | disabled=!mapping.supported; 160 | checked=mapping.getValue; 161 | break; 162 | case TagTheme: 163 | checked=[menuItem.title isEqual:Settings.currentThemeName]; 164 | break; 165 | case TagProjectMode: 166 | checked=self.projectMode; 167 | break; 168 | case TagTab: 169 | disabled=!self.projectMode; 170 | break; 171 | case TagFileAssociation: 172 | ; 173 | Document* document=NSApp.keyWindow.windowController.document; 174 | if(document) 175 | { 176 | menuItem.title=[NSString stringWithFormat:@"Claim File Type (%@)",document.xcodeDocument.fileType]; 177 | NSString* existing=((NSString*)LSCopyDefaultRoleHandlerForContentType((CFStringRef)document.xcodeDocument.fileType,kLSRolesAll)).autorelease; 178 | if([existing isEqual:NSBundle.mainBundle.bundleIdentifier]) 179 | { 180 | disabled=true; 181 | } 182 | } 183 | else 184 | { 185 | menuItem.title=@"Claim File Type"; 186 | disabled=true; 187 | } 188 | break; 189 | } 190 | 191 | menuItem.state=checked?NSControlStateValueOn:NSControlStateValueOff; 192 | if(disabled) 193 | { 194 | return false; 195 | } 196 | } 197 | 198 | return true; 199 | } 200 | 201 | -(void)handleSelectTab:(NSMenuItem*)sender 202 | { 203 | int value=sender.keyEquivalent.intValue; 204 | if(value==9) 205 | { 206 | value=INT_MAX; 207 | } 208 | 209 | NSArray* windows=NSApp.keyWindow.tabbedWindows; 210 | [windows[MIN(value,windows.count)-1] makeKeyAndOrderFront:nil]; 211 | } 212 | 213 | -(void)handleAbout:(NSMenuItem*)sender 214 | { 215 | NSString* gitInfo=[NSString stringWithUTF8String:stringify(gitHash)]; 216 | if(gitInfo.length==0) 217 | { 218 | gitInfo=@"[unknown commit]"; 219 | } 220 | 221 | alert([NSString stringWithFormat:@"Amy's meme text editor\n\n%@",gitInfo]); 222 | } 223 | 224 | -(void)handleSettingsToggle:(NSMenuItem*)sender 225 | { 226 | [Settings mappingWithName:sender.title].toggle; 227 | } 228 | 229 | -(void)handleSettingsReset:(NSMenuItem*)sender 230 | { 231 | Settings.reset; 232 | } 233 | 234 | -(void)handleSetTheme:(NSMenuItem*)sender 235 | { 236 | [Settings setCurrentThemeName:sender.title]; 237 | } 238 | 239 | -(void)handleToggleProjectMode:(NSMenuItem*)sender 240 | { 241 | self.projectMode=!self.projectMode; 242 | WindowController.syncProjectMode; 243 | } 244 | 245 | -(void)handleClaimFileAssociation:(NSMenuItem*)sender 246 | { 247 | Document* document=NSApp.keyWindow.windowController.document; 248 | LSSetDefaultRoleHandlerForContentType((CFStringRef)document.xcodeDocument.fileType,kLSRolesAll,(CFStringRef)NSBundle.mainBundle.bundleIdentifier); 249 | } 250 | 251 | -(void)handleFrameChange:(NSWindow*)window 252 | { 253 | if(!self.projectMode) 254 | { 255 | return; 256 | } 257 | 258 | if(![self.currentScreenKey isEqual:Settings.screenKey]) 259 | { 260 | return; 261 | } 262 | 263 | Settings.projectRect=window.frame; 264 | } 265 | 266 | -(void)windowDidResize:(NSNotification*)note 267 | { 268 | [self handleFrameChange:(NSWindow*)note.object]; 269 | } 270 | 271 | -(void)windowDidMove:(NSNotification*)note 272 | { 273 | [self handleFrameChange:(NSWindow*)note.object]; 274 | } 275 | 276 | -(void)applicationDidChangeScreenParameters:(NSNotification*)note 277 | { 278 | WindowController.syncProjectMode; 279 | self.currentScreenKey=Settings.screenKey; 280 | } 281 | 282 | -(void)dealloc 283 | { 284 | self.currentScreenKey=nil; 285 | 286 | super.dealloc; 287 | } 288 | 289 | @end 290 | -------------------------------------------------------------------------------- /Document.h: -------------------------------------------------------------------------------- 1 | @interface Document:NSDocument 2 | 3 | @property(retain) XcodeDocument* xcodeDocument; 4 | 5 | @end 6 | -------------------------------------------------------------------------------- /Document.m: -------------------------------------------------------------------------------- 1 | @implementation Document 2 | 3 | +(void)closeTransientIfNeeded 4 | { 5 | NSArray* documents=NSDocumentController.sharedDocumentController.documents; 6 | if(documents.count!=2) 7 | { 8 | return; 9 | } 10 | 11 | for(Document* document in documents) 12 | { 13 | if(document.fileURL||document.documentEdited) 14 | { 15 | continue; 16 | } 17 | 18 | [document.windowControllers.firstObject.window performClose:nil]; 19 | } 20 | } 21 | 22 | -(void)makeWindowControllers 23 | { 24 | if(self.fileURL) 25 | { 26 | Document.closeTransientIfNeeded; 27 | } 28 | 29 | [self addWindowController:WindowController.alloc.init.autorelease]; 30 | [self loadWithURL:self.fileURL]; 31 | } 32 | 33 | -(void)loadWithURL:(NSURL*)url 34 | { 35 | NSURL* tempURL=getTempURL(); 36 | NSString* type; 37 | if(url) 38 | { 39 | if(![NSFileManager.defaultManager copyItemAtURL:url toURL:tempURL error:nil]) 40 | { 41 | alertAbort(@"copy error"); 42 | } 43 | type=[NSDocumentController.sharedDocumentController typeForContentsOfURL:url error:nil]; 44 | } 45 | else 46 | { 47 | if(![NSFileManager.defaultManager createFileAtPath:tempURL.path contents:nil attributes:nil]) 48 | { 49 | alertAbort(@"touch error"); 50 | } 51 | type=self.fileType; 52 | } 53 | 54 | self.xcodeDocument.close; 55 | self.xcodeDocument=getXcodeDocument(tempURL,type); 56 | self.undoManager=self.xcodeDocument.undoManager; 57 | 58 | [(WindowController*)self.windowControllers.lastObject replaceDocument:self]; 59 | } 60 | 61 | -(BOOL)readFromURL:(NSURL*)url ofType:(NSString*)type error:(NSError**)error 62 | { 63 | return true; 64 | } 65 | 66 | -(BOOL)writeSafelyToURL:(NSURL*)url ofType:(NSString*)type forSaveOperation:(NSSaveOperationType)operation error:(NSError**)error 67 | { 68 | NSNumber* permissions=[NSFileManager.defaultManager attributesOfItemAtPath:url.path error:nil][NSFilePosixPermissions]; 69 | BOOL result=[self.xcodeDocument writeSafelyToURL:url ofType:type forSaveOperation:NSSaveToOperation error:error]; 70 | if(permissions) 71 | { 72 | [NSFileManager.defaultManager setAttributes:@{NSFilePosixPermissions:permissions} ofItemAtPath:url.path error:nil]; 73 | } 74 | 75 | if(!self.fileURL) 76 | { 77 | [self loadWithURL:url]; 78 | } 79 | 80 | return result; 81 | } 82 | 83 | -(void)handleSave:(NSMenuItem*)sender 84 | { 85 | [self saveDocument:nil]; 86 | } 87 | 88 | -(void)encodeRestorableStateWithCoder:(NSCoder*)coder 89 | { 90 | [super encodeRestorableStateWithCoder:coder]; 91 | 92 | [coder encodeBool:Delegate.shared.projectMode forKey:@"projectMode"]; 93 | } 94 | 95 | -(void)restoreStateWithCoder:(NSCoder*)coder 96 | { 97 | [super restoreStateWithCoder:coder]; 98 | 99 | BOOL mode=[coder decodeBoolForKey:@"projectMode"]; 100 | if(mode!=Delegate.shared.projectMode) 101 | { 102 | dispatch_async(dispatch_get_main_queue(),^() 103 | { 104 | Delegate.shared.projectMode=mode; 105 | WindowController.syncProjectMode; 106 | }); 107 | } 108 | } 109 | 110 | -(void)close 111 | { 112 | self.xcodeDocument.close; 113 | 114 | super.close; 115 | } 116 | 117 | -(void)dealloc 118 | { 119 | self.xcodeDocument=nil; 120 | 121 | super.dealloc; 122 | } 123 | 124 | @end 125 | -------------------------------------------------------------------------------- /Settings.h: -------------------------------------------------------------------------------- 1 | @interface Settings:NSObject 2 | @end 3 | -------------------------------------------------------------------------------- /Settings.m: -------------------------------------------------------------------------------- 1 | @implementation Settings 2 | 3 | +(NSArray*)allMappings 4 | { 5 | // TODO: implement numeric settings, cases that need reload currently, etc 6 | 7 | return @[ 8 | [SettingsMapping mappingWithName:@"Show Line Numbers" getter:@"showLineNumbers" defaultValue:true], 9 | [SettingsMapping mappingWithName:@"Show Folding Sidebar" getter:@"showCodeFoldingSidebar" defaultValue:false], 10 | [SettingsMapping mappingWithName:@"Show Minimap (May Need Reload)" getter:@"showMinimap" defaultValue:true], 11 | [SettingsMapping mappingWithName:@"Show Page Guide" getter:@"showPageGuide" defaultValue:false], 12 | [SettingsMapping mappingWithName:@"Show Structure Headers" getter:@"showStructureHeaders" defaultValue:true], 13 | [SettingsMapping mappingWithName:@"Show Invisible Characters (May Need Reload)" getter:@"showInvisibleCharacters" defaultValue:false], 14 | [SettingsMapping mappingWithName:@"Fade Comment Delimiters" getter:@"fadeCommentDelimiters" defaultValue:false], 15 | [SettingsMapping mappingWithName:@"Indent Using Tabs" getter:@"useTabsToIndent" defaultValue:true], 16 | [SettingsMapping mappingWithName:@"Use Syntax-Aware Indentation" getter:@"useSyntaxAwareIndenting" defaultValue:false], 17 | [SettingsMapping mappingWithName:@"Close Block Comments" getter:@"autoCloseBlockComment" defaultValue:false], 18 | [SettingsMapping mappingWithName:@"Close Braces" getter:@"autoInsertClosingBrace" defaultValue:false], 19 | [SettingsMapping mappingWithName:@"Match Closing Brackets" getter:@"autoInsertOpenBracket" defaultValue:false], 20 | [SettingsMapping mappingWithName:@"Use Type-Over Delimiters" getter:@"enableTypeOverCompletions" defaultValue:false], 21 | [SettingsMapping mappingWithName:@"Enclose Selection in Delimiters" getter:@"autoEncloseSelectionInDelimiters" defaultValue:false], 22 | [SettingsMapping mappingWithName:@"Soft Wrap Lines" getter:@"wrapLines" defaultValue:true], 23 | [SettingsMapping mappingWithName:@"Trim Trailing Whitespace" getter:@"trimTrailingWhitespace" defaultValue:true], 24 | [SettingsMapping mappingWithName:@"Suggest Completions" getter:@"autoSuggestCompletions" defaultValue:false], 25 | [SettingsMapping mappingWithName:@"Use Vi Mode" getter:@"useViKeyBindings" defaultValue:false] 26 | ]; 27 | } 28 | 29 | +(NSArray*)allMappingNames 30 | { 31 | return [Settings.allMappings valueForKey:@"name"]; 32 | } 33 | 34 | +(SettingsMapping*)mappingWithName:(NSString*)name 35 | { 36 | for(SettingsMapping* mapping in Settings.allMappings) 37 | { 38 | if([mapping.name isEqual:name]) 39 | { 40 | return mapping; 41 | } 42 | } 43 | 44 | return nil; 45 | } 46 | 47 | +(NSArray*)allThemeNames 48 | { 49 | NSArray* names=[getXcodeThemes() valueForKeyPath:@"localizedName"]; 50 | return [names sortedArrayUsingSelector:@selector(compare:)]; 51 | } 52 | 53 | +(NSString*)currentThemeName 54 | { 55 | return getXcodeTheme().localizedName; 56 | } 57 | 58 | +(void)setCurrentThemeName:(NSString*)name 59 | { 60 | XcodeTheme2* matched=nil; 61 | 62 | for(XcodeTheme2* theme in getXcodeThemes()) 63 | { 64 | if([theme.localizedName isEqual:name]) 65 | { 66 | matched=theme; 67 | break; 68 | } 69 | } 70 | 71 | if(!matched) 72 | { 73 | alert(@"theme missing"); 74 | return; 75 | } 76 | 77 | setXcodeTheme(matched); 78 | } 79 | 80 | +(NSString*)screenKey 81 | { 82 | CGRect screenRect=NSScreen.mainScreen.frame; 83 | return [NSString stringWithFormat:@"screen %ld %ld %ld %ld",(long)screenRect.origin.x,(long)screenRect.origin.y,(long)screenRect.size.width,(long)screenRect.size.height]; 84 | } 85 | 86 | +(NSString*)rectKeyWithPrefix:(NSString*)prefix suffix:(NSString*)suffix 87 | { 88 | return [NSString stringWithFormat:@"%@ - %@ - %@",prefix,Settings.screenKey,suffix]; 89 | } 90 | 91 | +(void)saveRect:(CGRect)rect withPrefix:(NSString*)prefix 92 | { 93 | NSUserDefaults* defaults=NSUserDefaults.standardUserDefaults; 94 | [defaults setDouble:rect.origin.x forKey:[Settings rectKeyWithPrefix:prefix suffix:@"x"]]; 95 | [defaults setDouble:rect.origin.y forKey:[Settings rectKeyWithPrefix:prefix suffix:@"y"]]; 96 | [defaults setDouble:rect.size.width forKey:[Settings rectKeyWithPrefix:prefix suffix:@"width"]]; 97 | [defaults setDouble:rect.size.height forKey:[Settings rectKeyWithPrefix:prefix suffix:@"height"]]; 98 | } 99 | 100 | +(CGRect)rectWithPrefix:(NSString*)prefix 101 | { 102 | NSUserDefaults* defaults=NSUserDefaults.standardUserDefaults; 103 | CGFloat x=[defaults doubleForKey:[Settings rectKeyWithPrefix:prefix suffix:@"x"]]; 104 | CGFloat y=[defaults doubleForKey:[Settings rectKeyWithPrefix:prefix suffix:@"y"]]; 105 | CGFloat width=[defaults doubleForKey:[Settings rectKeyWithPrefix:prefix suffix:@"width"]]; 106 | CGFloat height=[defaults doubleForKey:[Settings rectKeyWithPrefix:prefix suffix:@"height"]]; 107 | 108 | if(width<100||height<100) 109 | { 110 | return CGRectZero; 111 | } 112 | 113 | return CGRectMake(x,y,width,height); 114 | } 115 | 116 | +(void)setProjectRect:(CGRect)rect 117 | { 118 | [Settings saveRect:rect withPrefix:@"project"]; 119 | } 120 | 121 | +(CGRect)projectRect 122 | { 123 | return [Settings rectWithPrefix:@"project"]; 124 | } 125 | 126 | +(void)saveThemeWithName:(NSString*)name backgroundColor:(NSString*)backgroundColor highlightColor:(NSString*)highlightColor selectionColor:(NSString*)selectionColor defaultFont:(NSString*)defaultFont defaultColor:(NSString*)defaultColor commentFont:(NSString*)commentFont commentColor:(NSString*)commentColor preprocessorFont:(NSString*)preprocessorFont preprocessorColor:(NSString*)preprocessorColor classFont:(NSString*)classFont classColor:(NSString*)classColor functionFont:(NSString*)functionFont functionColor:(NSString*)functionColor keywordFont:(NSString*)keywordFont keywordColor:(NSString*)keywordColor stringFont:(NSString*)stringFont stringColor:(NSString*)stringColor numberFont:(NSString*)numberFont numberColor:(NSString*)numberColor 127 | { 128 | NSString* basePath=[getXcodeSystemThemesPath() stringByAppendingPathComponent:@"Default (Light).xccolortheme"]; 129 | NSData* baseData=[NSData dataWithContentsOfFile:basePath]; 130 | if(!baseData) 131 | { 132 | alertAbort(@"base theme missing"); 133 | } 134 | 135 | NSMutableDictionary* custom=[NSPropertyListSerialization propertyListWithData:baseData options:NSPropertyListMutableContainers format:nil error:nil]; 136 | if(!custom) 137 | { 138 | alertAbort(@"base theme broken"); 139 | } 140 | 141 | custom[XcodeThemeBackgroundKey]=backgroundColor; 142 | custom[XcodeThemeHighlightKey]=highlightColor; 143 | custom[XcodeThemeSelectionKey]=selectionColor; 144 | custom[XcodeThemeCursorKey]=defaultColor; 145 | custom[XcodeThemeInvisiblesKey]=commentColor; 146 | custom[XcodeThemeMarkdownCodeKey]=stringColor; 147 | 148 | NSMutableDictionary* innerFonts=custom[XcodeThemeFontsKey]; 149 | NSMutableDictionary* innerColors=custom[XcodeThemeColorsKey]; 150 | for(NSString* key in innerColors.allKeys) 151 | { 152 | NSString* font=defaultFont; 153 | NSString* color=defaultColor; 154 | 155 | if([XcodeThemeCommentKeys containsObject:key]) 156 | { 157 | font=commentFont; 158 | color=commentColor; 159 | } 160 | else if([XcodeThemePreprocessorKeys containsObject:key]) 161 | { 162 | font=preprocessorFont; 163 | color=preprocessorColor; 164 | } 165 | else if([XcodeThemeClassKeys containsObject:key]) 166 | { 167 | font=classFont; 168 | color=classColor; 169 | } 170 | else if([XcodeThemeFunctionKeys containsObject:key]) 171 | { 172 | font=functionFont; 173 | color=functionColor; 174 | } 175 | else if([XcodeThemeKeywordKeys containsObject:key]) 176 | { 177 | font=keywordFont; 178 | color=keywordColor; 179 | } 180 | else if([XcodeThemeStringKeys containsObject:key]) 181 | { 182 | font=stringFont; 183 | color=stringColor; 184 | } 185 | else if([XcodeThemeNumberKeys containsObject:key]) 186 | { 187 | font=numberFont; 188 | color=numberColor; 189 | } 190 | 191 | innerFonts[key]=font; 192 | innerColors[key]=color; 193 | } 194 | 195 | NSString* customPath=[getXcodeUserThemesPath() stringByAppendingPathComponent:[name stringByAppendingString:@".xccolortheme"]]; 196 | [NSFileManager.defaultManager createDirectoryAtPath:customPath.stringByDeletingLastPathComponent withIntermediateDirectories:true attributes:nil error:nil]; 197 | 198 | NSData* customData=[NSPropertyListSerialization dataWithPropertyList:custom format:NSPropertyListXMLFormat_v1_0 options:0 error:nil]; 199 | if(![customData writeToFile:customPath atomically:true]) 200 | { 201 | alertAbort(@"theme write failed"); 202 | } 203 | } 204 | 205 | +(void)saveSimpleThemeWithName:(NSString*)name background:(NSString*)backgroundColor highlight:(NSString*)highlightColor selection:(NSString*)selectionColor normal:(NSString*)normalColor meta:(NSString*)metaColor type:(NSString*)typeColor keyword:(NSString*)keywordColor string:(NSString*)stringColor number:(NSString*)numberColor 206 | { 207 | NSString* regular=@"SFMono-Regular - 13.0"; 208 | NSString* italic=@"SFMono-RegularItalic - 13.0"; 209 | NSString* bold=@"SFMono-Bold - 13.0"; 210 | 211 | [Settings saveThemeWithName:name backgroundColor:backgroundColor highlightColor:highlightColor selectionColor:selectionColor defaultFont:regular defaultColor:normalColor commentFont:italic commentColor:metaColor preprocessorFont:regular preprocessorColor:metaColor classFont:bold classColor:typeColor functionFont:bold functionColor:typeColor keywordFont:bold keywordColor:keywordColor stringFont:bold stringColor:stringColor numberFont:bold numberColor:numberColor]; 212 | } 213 | 214 | +(void)reset 215 | { 216 | for(SettingsMapping* mapping in Settings.allMappings) 217 | { 218 | mapping.reset; 219 | } 220 | 221 | [Settings saveSimpleThemeWithName:getAppName() background:@"1 1 1" highlight:@"0.95 0.925 1" selection:@"0.85 0.775 1" normal:@"0.4 0.3 0.7" meta:@"0.6 0.5 0.9" type:@"0.5 0.2 0.8" keyword:@"0.7 0.2 0.8" string:@"0.85 0.35 1" number:@"0.45 0.3 1"]; 222 | [Settings setCurrentThemeName:getAppName()]; 223 | } 224 | 225 | @end 226 | -------------------------------------------------------------------------------- /SettingsMapping.h: -------------------------------------------------------------------------------- 1 | @interface SettingsMapping:NSObject 2 | 3 | @property(retain) NSString* name; 4 | @property(assign) SEL getter; 5 | @property(assign) SEL setter; 6 | @property(assign) BOOL defaultValue; 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /SettingsMapping.m: -------------------------------------------------------------------------------- 1 | @implementation SettingsMapping 2 | 3 | +(instancetype)mappingWithName:(NSString*)name getter:(NSString*)getterName setter:(NSString*)setterName defaultValue:(BOOL)defaultValue 4 | { 5 | SettingsMapping* mapping=SettingsMapping.alloc.init.autorelease; 6 | 7 | mapping.name=name; 8 | mapping.getter=NSSelectorFromString(getterName); 9 | mapping.setter=NSSelectorFromString(setterName); 10 | mapping.defaultValue=defaultValue; 11 | 12 | return mapping; 13 | } 14 | 15 | +(instancetype)mappingWithName:(NSString*)name getter:(NSString*)getterName defaultValue:(BOOL)defaultValue 16 | { 17 | NSString* firstChar=[getterName substringToIndex:1].uppercaseString; 18 | NSString* otherChars=[getterName substringFromIndex:1]; 19 | NSString* setterName=[NSString stringWithFormat:@"set%@%@:",firstChar,otherChars]; 20 | 21 | return [SettingsMapping mappingWithName:name getter:getterName setter:setterName defaultValue:defaultValue]; 22 | } 23 | 24 | -(BOOL)supported 25 | { 26 | return [getXcodeSettings() respondsToSelector:self.getter]&&[getXcodeSettings() respondsToSelector:self.setter]; 27 | } 28 | 29 | -(BOOL)getValue 30 | { 31 | if(self.supported) 32 | { 33 | return (BOOL)(long)[getXcodeSettings() performSelector:self.getter]; 34 | } 35 | return false; 36 | } 37 | 38 | -(void)setValue:(BOOL)value 39 | { 40 | if(self.supported) 41 | { 42 | [getXcodeSettings() performSelector:self.setter withObject:(id)(long)value]; 43 | } 44 | } 45 | 46 | -(void)reset 47 | { 48 | [self setValue:self.defaultValue]; 49 | } 50 | 51 | -(void)toggle 52 | { 53 | [self setValue:!self.getValue]; 54 | } 55 | 56 | -(void)dealloc 57 | { 58 | self.name=nil; 59 | 60 | super.dealloc; 61 | } 62 | 63 | @end 64 | -------------------------------------------------------------------------------- /Utils.m: -------------------------------------------------------------------------------- 1 | #define trace NSLog 2 | 3 | #define stringify2(macro) #macro 4 | #define stringify(macro) stringify2(macro) 5 | 6 | CGImageRef createAppIcon(CGColorRef background,CGColorRef stroke,CGColorRef fill) 7 | { 8 | CGRect rect=CGRectMake(0,0,1024,1024); 9 | 10 | CGColorSpaceRef space=CGColorSpaceCreateDeviceRGB(); 11 | CGContextRef context=CGBitmapContextCreate(NULL,1024,1024,8,1024*4,space,kCGImageAlphaPremultipliedFirst); 12 | CFRelease(space); 13 | 14 | // TODO: doesn't precisely match Apple's template, but neither does NSIconGenericApplication, so.. 15 | 16 | CALayer* container=CALayer.layer; 17 | container.frame=rect; 18 | CALayer* round=CALayer.layer; 19 | round.frame=CGRectMake(100,100,824,824); 20 | round.backgroundColor=background; 21 | round.cornerRadius=186; 22 | if(@available(macOS 10.15,*)) 23 | { 24 | round.cornerCurve=kCACornerCurveContinuous; 25 | } 26 | round.shadowOpacity=0.25; 27 | round.shadowRadius=10; 28 | round.shadowOffset=CGSizeMake(0,-10); 29 | [container addSublayer:round]; 30 | [container renderInContext:context]; 31 | 32 | CGContextSetLineJoin(context,kCGLineJoinRound); 33 | CGContextSetLineWidth(context,40); 34 | CGContextSetTextDrawingMode(context,kCGTextFillStroke); 35 | CGContextSetFillColorWithColor(context,fill); 36 | CGContextSetStrokeColorWithColor(context,stroke); 37 | CGContextSelectFont(context,"Futura-Bold",650,kCGEncodingMacRoman); 38 | CGContextShowTextAtPoint(context,290,410,"y",1); 39 | 40 | CGImageRef image=CGBitmapContextCreateImage(context); 41 | CFRelease(context); 42 | return image; 43 | } 44 | 45 | NSString* getAppName() 46 | { 47 | return NSProcessInfo.processInfo.arguments[0].lastPathComponent; 48 | } 49 | 50 | NSURL* getTempURL() 51 | { 52 | NSString* name=[NSString stringWithFormat:@"%@.%ld.txt",getAppName(),(long)(NSDate.date.timeIntervalSince1970*NSEC_PER_SEC)]; 53 | return [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:name]]; 54 | } 55 | 56 | void alert(NSString* message) 57 | { 58 | NSAlert* alert=NSAlert.alloc.init.autorelease; 59 | alert.messageText=getAppName(); 60 | alert.informativeText=message; 61 | alert.runModal; 62 | } 63 | 64 | __attribute__((noreturn)) void alertAbort(NSString* message) 65 | { 66 | alert([NSString stringWithFormat:@"fatal: %@",message]); 67 | trace(@"%@ %@",message,NSThread.callStackSymbols); 68 | exit(1); 69 | } 70 | 71 | void swizzle(NSString* className,NSString* selName,BOOL isInstance,IMP newImp,IMP* oldImpOut) 72 | { 73 | Class class=NSClassFromString(className); 74 | if(!class) 75 | { 76 | alertAbort(@"swizzle class missing"); 77 | } 78 | 79 | SEL sel=NSSelectorFromString(selName); 80 | Method method=isInstance?class_getInstanceMethod(class,sel):class_getClassMethod(class,sel); 81 | if(!method) 82 | { 83 | alertAbort(@"swizzle method missing"); 84 | } 85 | 86 | IMP oldImp=method_setImplementation(method,newImp); 87 | if(oldImpOut) 88 | { 89 | *oldImpOut=oldImp; 90 | } 91 | } 92 | 93 | id returnNil() 94 | { 95 | return nil; 96 | } 97 | 98 | // TODO: hack to compile on older macOS (10.9+ but not in headers..?) 99 | 100 | @interface NSView() 101 | 102 | -(void)setClipsToBounds:(BOOL)value; 103 | 104 | @end 105 | -------------------------------------------------------------------------------- /WindowController.h: -------------------------------------------------------------------------------- 1 | @interface WindowController:NSWindowController 2 | 3 | @property(retain) XcodeViewController* xcodeViewController; 4 | 5 | +(void)syncProjectMode; 6 | -(void)replaceDocument:(Document*)document; 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /WindowController.m: -------------------------------------------------------------------------------- 1 | #define ScratchWidth 600 2 | #define ScratchHeight 500 3 | 4 | NSColor* (*hackRealColor)(NSObject*,SEL,NSString*,NSBundle*)=NULL; 5 | NSColor* hackFakeColor(NSObject* self,SEL sel,NSString* name,NSBundle* bundle) 6 | { 7 | if([name containsString:@"_NSTabBar"]) 8 | { 9 | if([@[@"_NSTabBarTabFillColorSelectedActiveWindow"] containsObject:name]) 10 | { 11 | return getXcodeTheme().sourceTextBackgroundColor; 12 | } 13 | 14 | if([@[@"_NSTabBarTabFillColorActiveWindow"] containsObject:name]) 15 | { 16 | return getXcodeTheme().sourceTextCurrentLineHighlightColor; 17 | } 18 | 19 | if([@[@"_NSTabBarInactiveTabHoverColor",@"_NSTabBarNewTabButtonHoverColor",@"_NSTabBarSemitransparentDividerColor"] containsObject:name]) 20 | { 21 | return getXcodeTheme().sourceTextSelectionColor; 22 | } 23 | } 24 | 25 | return hackRealColor(self,sel,name,bundle); 26 | } 27 | 28 | NSColor* hackFakeShadow() 29 | { 30 | return NSColor.clearColor; 31 | } 32 | 33 | void (*hackRealHeaderLayout)(NSView*,SEL); 34 | void hackFakeHeaderLayout(NSView* self,SEL sel) 35 | { 36 | hackRealHeaderLayout(self,sel); 37 | 38 | self.subviews.firstObject.hidden=true; 39 | self.layer.backgroundColor=getXcodeTheme().sourceTextBackgroundColor.CGColor; 40 | } 41 | 42 | CGImageRef createThemeAppIcon() 43 | { 44 | return createAppIcon(getXcodeTheme().sourceTextBackgroundColor.CGColor,getXcodeTheme().sourcePlainTextColor.CGColor,getXcodeTheme().sourceTextCurrentLineHighlightColor.CGColor); 45 | } 46 | 47 | dispatch_once_t windowControllerInitializeOnce; 48 | 49 | @implementation WindowController 50 | 51 | +(void)initialize 52 | { 53 | dispatch_once(&windowControllerInitializeOnce,^() 54 | { 55 | if(@available(macOS 11,*)) 56 | { 57 | swizzle(@"NSColor",@"colorNamed:bundle:",false,(IMP)hackFakeColor,(IMP*)&hackRealColor); 58 | swizzle(@"NSTitlebarSeparatorView",@"updateLayer",true,(IMP)returnNil,NULL); 59 | } 60 | 61 | // TODO: uhh 62 | 63 | if(NSClassFromString(@"_TtC12SourceEditor21StickyHeaderStackView")) 64 | { 65 | swizzle(@"_TtC12SourceEditor21StickyHeaderStackView",@"layout",true,(IMP)hackFakeHeaderLayout,(IMP*)&hackRealHeaderLayout); 66 | 67 | swizzle(@"NSColor",@"shadowWithLevel:",true,(IMP)hackFakeShadow,NULL); 68 | swizzle(@"NSColor",@"highlightWithLevel:",true,(IMP)hackFakeShadow,NULL); 69 | } 70 | 71 | [NSNotificationCenter.defaultCenter addObserverForName:XcodeThemeChangedKey object:nil queue:nil usingBlock:^(NSNotification* note) 72 | { 73 | [WindowController.allInstances makeObjectsPerformSelector:@selector(syncTheme)]; 74 | }]; 75 | }); 76 | } 77 | 78 | +(NSArray*)allInstances 79 | { 80 | NSMutableArray* result=NSMutableArray.alloc.init.autorelease; 81 | for(Document* document in NSDocumentController.sharedDocumentController.documents) 82 | { 83 | [result addObjectsFromArray:document.windowControllers]; 84 | } 85 | return result; 86 | } 87 | 88 | +(WindowController*)firstInstance 89 | { 90 | return WindowController.allInstances.firstObject; 91 | } 92 | 93 | +(WindowController*)lastInstance 94 | { 95 | return WindowController.allInstances.lastObject; 96 | } 97 | 98 | +(void)syncProjectMode 99 | { 100 | NSWindow* previousKeyWindow=NSApp.keyWindow; 101 | 102 | WindowController* previous=nil; 103 | for(WindowController* instance in WindowController.allInstances) 104 | { 105 | [instance syncProjectModeWithPrevious:previous]; 106 | previous=instance; 107 | } 108 | 109 | if(Delegate.shared.projectMode) 110 | { 111 | // TODO: hack to preserve window ordering 112 | // it seems key window becomes tab 1; who calls mergeAllWindows: is irrelevant 113 | 114 | [WindowController.firstInstance.window makeKeyAndOrderFront:nil]; 115 | [WindowController.firstInstance.window mergeAllWindows:nil]; 116 | } 117 | 118 | [previousKeyWindow makeKeyAndOrderFront:nil]; 119 | } 120 | 121 | -(instancetype)init 122 | { 123 | self=super.init; 124 | 125 | NSWindowStyleMask style=NSWindowStyleMaskTitled|NSWindowStyleMaskClosable|NSWindowStyleMaskResizable|NSWindowStyleMaskMiniaturizable; 126 | self.window=[NSWindow.alloc initWithContentRect:CGRectZero styleMask:style backing:NSBackingStoreBuffered defer:false].autorelease; 127 | self.window.titlebarAppearsTransparent=true; 128 | self.window.delegate=Delegate.shared; 129 | [self syncProjectModeWithPrevious:WindowController.lastInstance]; 130 | 131 | self.syncTheme; 132 | 133 | return self; 134 | } 135 | 136 | -(void)replaceDocument:(Document*)document 137 | { 138 | NSRange oldSelection=getXcodeViewControllerSelection(self.xcodeViewController); 139 | 140 | // TODO: this and the similar copy+pasted dealloc code in Document.m should be moved somewhere else, probably Xcode.m or custom property setter 141 | 142 | self.window.contentView=nil; 143 | self.xcodeViewController.invalidate; 144 | self.xcodeViewController=getXcodeViewController(document.xcodeDocument); 145 | self.window.contentView=self.xcodeViewController.view; 146 | 147 | focusXcodeViewController(self.xcodeViewController,oldSelection); 148 | } 149 | 150 | -(void)syncProjectModeWithPrevious:(WindowController*)previous 151 | { 152 | if(Delegate.shared.projectMode) 153 | { 154 | self.window.tabbingMode=NSWindowTabbingModePreferred; 155 | } 156 | else 157 | { 158 | self.window.tabbingMode=NSWindowTabbingModeDisallowed; 159 | [self.window moveTabToNewWindow:nil]; 160 | } 161 | 162 | // TODO: i feel like some of this should be in Settings, but we don't have access to previous window and toolbar height there.. 163 | 164 | CGRect previousRect=previous?previous.window.frame:NSScreen.mainScreen.visibleFrame; 165 | CGFloat toolbarHeight=[self.window frameRectForContentRect:CGRectZero].size.height; 166 | CGRect cascadedRect=CGRectMake(previousRect.origin.x+toolbarHeight,previousRect.origin.y+previousRect.size.height-ScratchHeight-toolbarHeight,ScratchWidth,ScratchHeight); 167 | 168 | if(CGRectEqualToRect(Settings.projectRect,CGRectZero)) 169 | { 170 | Settings.projectRect=cascadedRect; 171 | } 172 | 173 | if(Delegate.shared.projectMode) 174 | { 175 | [self.window setFrame:Settings.projectRect display:false]; 176 | } 177 | else 178 | { 179 | [self.window setFrame:cascadedRect display:false]; 180 | } 181 | } 182 | 183 | -(void)syncTheme 184 | { 185 | dispatch_async(dispatch_get_main_queue(),^() 186 | { 187 | NSAppearance* appearance=[NSAppearance appearanceNamed:getXcodeTheme().hasLightBackground?NSAppearanceNameAqua:NSAppearanceNameVibrantDark]; 188 | if(@available(macOS 10.14,*)) 189 | { 190 | NSApp.appearance=appearance; 191 | } 192 | else 193 | { 194 | self.window.appearance=appearance; 195 | } 196 | 197 | self.window.backgroundColor=getXcodeTheme().sourceTextBackgroundColor; 198 | 199 | // TODO: hack to refresh the "new tab" button 200 | 201 | if(self.window.isKeyWindow) 202 | { 203 | self.window.resignKeyWindow; 204 | self.window.becomeKeyWindow; 205 | } 206 | 207 | CGImageRef icon=createThemeAppIcon(); 208 | NSApp.applicationIconImage=[NSImage.alloc initWithCGImage:icon size:CGSizeZero].autorelease; 209 | CFRelease(icon); 210 | }); 211 | } 212 | 213 | -(void)dealloc 214 | { 215 | // TODO: idk exactly what this does, but it fixes the memory leak. empirically, XcodeViewController.invalidate and XcodeDocument.close are both needed in addition to releasing normally 216 | 217 | self.window.contentView=nil; 218 | self.xcodeViewController.invalidate; 219 | 220 | self.xcodeViewController=nil; 221 | 222 | super.dealloc; 223 | } 224 | 225 | @end 226 | -------------------------------------------------------------------------------- /Xcode.m: -------------------------------------------------------------------------------- 1 | NSString* xcodePath=nil; 2 | 3 | NSString* replaceXcodePath(NSString* path) 4 | { 5 | if(!xcodePath) 6 | { 7 | xcodePath=[NSWorkspace.sharedWorkspace URLForApplicationWithBundleIdentifier:@"com.apple.dt.Xcode"].path; 8 | if(!xcodePath) 9 | { 10 | alertAbort(@"xcode missing"); 11 | } 12 | } 13 | 14 | return [path stringByReplacingOccurrencesOfString:@"%" withString:xcodePath]; 15 | } 16 | 17 | void (*SoftInitialize)(int,NSError**); 18 | Class SoftDocument; 19 | Class SoftViewController; 20 | Class SoftTheme; 21 | Class SoftTheme2; 22 | Class SoftSettings; 23 | Class SoftSettings2; 24 | Class SoftDocumentLocation; 25 | 26 | @interface XcodeDocument:NSDocument 27 | 28 | -(instancetype)initWithContentsOfURL:(NSURL*)url ofType:(NSString*)type error:(NSError**)error; 29 | 30 | @end 31 | 32 | @interface XcodeDocumentLocation:NSObject 33 | 34 | -(instancetype)initWithDocumentURL:(NSURL*)url timestamp:(NSNumber*)timestamp characterRange:(NSRange)range; 35 | -(NSRange)characterRange; 36 | 37 | @end 38 | 39 | @interface XcodeViewController:NSViewController 40 | 41 | @property(retain) NSObject* representedExtension; 42 | @property(retain) NSObject* fileTextSettings; 43 | 44 | -(instancetype)initWithNibName:(NSString*)nib bundle:(NSBundle*)bundle document:(NSDocument*)document; 45 | -(void)selectDocumentLocations:(NSArray*)locations; 46 | -(NSArray*)currentSelectedDocumentLocations; 47 | -(void)invalidate; 48 | 49 | @end 50 | 51 | @interface XcodeSettings:NSObject 52 | 53 | +(instancetype)sharedPreferences; 54 | 55 | @end 56 | 57 | @class XcodeThemeManager; 58 | 59 | #define XcodeThemeBackgroundKey @"DVTSourceTextBackground" 60 | #define XcodeThemeHighlightKey @"DVTSourceTextCurrentLineHighlightColor" 61 | #define XcodeThemeSelectionKey @"DVTSourceTextSelectionColor" 62 | #define XcodeThemeCursorKey @"DVTSourceTextInsertionPointColor" 63 | #define XcodeThemeInvisiblesKey @"DVTSourceTextInvisiblesColor" 64 | #define XcodeThemeMarkdownCodeKey @"DVTMarkupTextInlineCodeColor" 65 | 66 | #define XcodeThemeFontsKey @"DVTSourceTextSyntaxFonts" 67 | #define XcodeThemeColorsKey @"DVTSourceTextSyntaxColors" 68 | #define XcodeThemeCommentKeys @[@"xcode.syntax.comment",@"xcode.syntax.comment.doc",@"xcode.syntax.comment.doc.keyword",@"xcode.syntax.mark",@"xcode.syntax.url"] 69 | #define XcodeThemePreprocessorKeys @[@"xcode.syntax.preprocessor"] 70 | #define XcodeThemeClassKeys @[@"xcode.syntax.declaration.type"] 71 | #define XcodeThemeFunctionKeys @[@"xcode.syntax.declaration.other",@"xcode.syntax.attribute"] 72 | #define XcodeThemeKeywordKeys @[@"xcode.syntax.keyword"] 73 | #define XcodeThemeStringKeys @[@"xcode.syntax.string"] 74 | #define XcodeThemeNumberKeys @[@"xcode.syntax.number",@"xcode.syntax.character"] 75 | 76 | @interface XcodeTheme2:NSObject 77 | 78 | +(XcodeThemeManager*)preferenceSetsManager; 79 | -(NSString*)localizedName; 80 | -(NSString*)name; 81 | -(BOOL)hasLightBackground; 82 | -(NSColor*)sourceTextBackgroundColor; 83 | -(NSColor*)sourceTextCurrentLineHighlightColor; 84 | -(NSColor*)sourceTextSelectionColor; 85 | -(NSColor*)sourcePlainTextColor; 86 | 87 | @end 88 | 89 | #define XcodeLightThemeKey @"XCFontAndColorCurrentTheme" 90 | #define XcodeDarkThemeKey @"XCFontAndColorCurrentDarkTheme" 91 | #define XcodeThemeChangedKey @"DVTFontAndColorSettingsChangedNotification" 92 | 93 | @interface XcodeThemeManager:NSObject 94 | 95 | @property(retain) XcodeTheme2* currentPreferenceSet; 96 | 97 | -(NSArray*)availablePreferenceSets; 98 | 99 | @end 100 | 101 | XcodeDocument* getXcodeDocument(NSURL* url,NSString* type) 102 | { 103 | return [(XcodeDocument*)[SoftDocument alloc] initWithContentsOfURL:url ofType:type error:nil].autorelease; 104 | } 105 | 106 | XcodeViewController* getXcodeViewController(XcodeDocument* document) 107 | { 108 | XcodeViewController* controller=[(XcodeViewController*)[SoftViewController alloc] initWithNibName:nil bundle:nil document:document].autorelease; 109 | controller.fileTextSettings=((NSObject*)[SoftSettings2 alloc]).init.autorelease; 110 | controller.view.clipsToBounds=true; 111 | return controller; 112 | } 113 | 114 | NSRange getXcodeViewControllerSelection(XcodeViewController* controller) 115 | { 116 | XcodeDocumentLocation* location=controller.currentSelectedDocumentLocations.firstObject; 117 | return location?location.characterRange:NSMakeRange(0,0); 118 | } 119 | 120 | void focusXcodeViewController(XcodeViewController* controller,NSRange selection) 121 | { 122 | NSURL* fakeURL=[NSURL.alloc initWithString:@""].autorelease; 123 | XcodeDocumentLocation* location=[(XcodeDocumentLocation*)[SoftDocumentLocation alloc] initWithDocumentURL:fakeURL timestamp:nil characterRange:selection].autorelease; 124 | [controller selectDocumentLocations:@[location]]; 125 | 126 | // TODO: confusing. make a general "recurse views with block" function 127 | 128 | NSMutableArray* views=NSMutableArray.alloc.init.autorelease; 129 | [views addObject:controller.view]; 130 | for(int index=0;index* getXcodeThemes() 153 | { 154 | return getXcodeThemeManager().availablePreferenceSets; 155 | } 156 | 157 | XcodeTheme2* getXcodeTheme() 158 | { 159 | return getXcodeThemeManager().currentPreferenceSet; 160 | } 161 | 162 | void setXcodeTheme(XcodeTheme2* theme) 163 | { 164 | getXcodeThemeManager().currentPreferenceSet=theme; 165 | 166 | [NSUserDefaults.standardUserDefaults setObject:theme.name forKey:XcodeLightThemeKey]; 167 | [NSUserDefaults.standardUserDefaults setObject:theme.name forKey:XcodeDarkThemeKey]; 168 | } 169 | 170 | NSString* getXcodeSystemThemesPath() 171 | { 172 | for(NSString* format in @[@"%/Contents/SharedFrameworks/DVTUserInterfaceKit.framework/Versions/A/Resources/FontAndColorThemes",@"%/Contents/SharedFrameworks/DVTKit.framework/Versions/A/Resources/FontAndColorThemes"]) 173 | { 174 | NSString* path=replaceXcodePath(format); 175 | if([NSFileManager.defaultManager fileExistsAtPath:path]) 176 | { 177 | return path; 178 | } 179 | } 180 | 181 | alertAbort(@"system themes folder missing"); 182 | } 183 | 184 | NSString* getXcodeUserThemesPath() 185 | { 186 | return [NSHomeDirectory() stringByAppendingPathComponent:@"Library/Developer/Xcode/UserData/FontAndColorThemes"]; 187 | } 188 | 189 | NSMenu* (^contextMenuHook)()=NULL; 190 | NSMenu* hackContextMenu() 191 | { 192 | return contextMenuHook(); 193 | } 194 | 195 | void linkLibrary(NSString* path) 196 | { 197 | if(!dlopen(path.UTF8String,RTLD_LAZY)) 198 | { 199 | alertAbort([NSString stringWithFormat:@"dlopen failed: %s",dlerror()]); 200 | } 201 | } 202 | 203 | void linkSymbol(NSString* name,void** pointer) 204 | { 205 | void* symbol=dlsym(RTLD_DEFAULT,name.UTF8String); 206 | if(!symbol) 207 | { 208 | alertAbort([NSString stringWithFormat:@"dlsym failed: %s",dlerror()]); 209 | } 210 | *pointer=symbol; 211 | } 212 | 213 | void linkClass(NSString* name,Class* pointer) 214 | { 215 | linkSymbol([NSString stringWithFormat:@"OBJC_CLASS_$_%@",name],(void**)pointer); 216 | } 217 | 218 | void linkXcode() 219 | { 220 | linkLibrary(replaceXcodePath(@"%/Contents/PlugIns/IDESourceEditor.framework/Versions/A/IDESourceEditor")); 221 | 222 | linkSymbol(@"IDEInitialize",(void**)&SoftInitialize); 223 | linkClass(@"_TtC15IDESourceEditor18SourceCodeDocument",&SoftDocument); 224 | linkClass(@"_TtC15IDESourceEditor16SourceCodeEditor",&SoftViewController); 225 | linkClass(@"DVTTheme",&SoftTheme); 226 | linkClass(@"DVTFontAndColorTheme",&SoftTheme2); 227 | linkClass(@"DVTTextPreferences",&SoftSettings); 228 | linkClass(@"IDEFileTextSettings",&SoftSettings2); 229 | linkClass(@"DVTTextDocumentLocation",&SoftDocumentLocation); 230 | 231 | // TODO: stupid 232 | 233 | swizzle(@"IDEDocumentController",@"sharedDocumentController",false,(IMP)returnNil,NULL); 234 | swizzle(@"_TtC12SourceEditor16SourceEditorView",@"menuForEvent:",true,(IMP)hackContextMenu,NULL); 235 | 236 | // TODO: aborts if Xcode present but never opened 237 | 238 | NSError* error=nil; 239 | SoftInitialize(0,&error); 240 | if(error) 241 | { 242 | alertAbort([NSString stringWithFormat:@"xcode init failed: %@",error]); 243 | } 244 | 245 | // TODO: is there a more normal way to call this? 246 | 247 | [SoftTheme initialize]; 248 | 249 | // TODO: still missing xcode indexing-based colors 250 | // TODO: source control sidebar? 251 | } 252 | 253 | void restartIfNeeded(char** argv) 254 | { 255 | // TODO: case where it's already set? maybe we should instead check if IDESourceEditor fails to load? 256 | 257 | if(!getenv("DYLD_FRAMEWORK_PATH")) 258 | { 259 | NSString* dylibPaths=replaceXcodePath(@"%/Contents/Frameworks:%/Contents/SharedFrameworks:%/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Frameworks:%/Contents/Developer/Library/Frameworks"); 260 | setenv("DYLD_FRAMEWORK_PATH",dylibPaths.UTF8String,true); 261 | setenv("DYLD_LIBRARY_PATH",dylibPaths.UTF8String,true); 262 | execv(argv[0],argv); 263 | 264 | alertAbort(@"re-exec failed"); 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /build.zsh: -------------------------------------------------------------------------------- 1 | set -e 2 | cd "$(dirname "$0")" 3 | 4 | name=Ycode 5 | id=website.amys.ycode2 6 | class=Document 7 | type=public.data 8 | minVersion=10.13 9 | 10 | if [[ $1 == test ]] 11 | then 12 | name+=' Test' 13 | id+=-test 14 | fi 15 | 16 | rm -rf "$name.app" "$name.zip" icon.iconset 17 | mkdir -p "$name.app/Contents/MacOS" 18 | mkdir -p "$name.app/Contents/Resources" 19 | 20 | clang -fmodules -mmacosx-version-min=$minVersion -arch x86_64 -arch arm64 -DgitHash=$(git log -1 --format=%H) main.m -o "$name.app/Contents/MacOS/$name" 21 | 22 | clang -fmodules -D iconMode main.m -o icon 23 | ./icon 24 | 25 | mkdir icon.iconset 26 | for size in 16 32 128 256 512 27 | do 28 | sips -Z $size icon.png --out icon.iconset/icon_${size}x${size}.png 29 | sips -Z $(($size*2)) icon.png --out icon.iconset/icon_${size}x${size}@2x.png 30 | done 31 | iconutil -c icns icon.iconset -o "$name.app/Contents/Resources/Icon.icns" 32 | 33 | echo "add CFBundleExecutable string $name 34 | add CFBundleIdentifier string $id 35 | add CFBundleIconFile string Icon.icns 36 | add NSHighResolutionCapable bool true 37 | add CFBundleDocumentTypes array 38 | add CFBundleDocumentTypes: dict 39 | add CFBundleDocumentTypes:0:NSDocumentClass string $class 40 | add CFBundleDocumentTypes:0:CFBundleTypeRole string Editor 41 | add CFBundleDocumentTypes:0:LSItemContentTypes array 42 | add CFBundleDocumentTypes:0:LSItemContentTypes: string $type 43 | add NSSupportsAutomaticTermination bool true" | while read command 44 | do 45 | /usr/libexec/PlistBuddy "$name.app/Contents/Info.plist" -c "$command" 46 | done 47 | 48 | codesign -f -s - "$name.app" 49 | zip -r "$name.zip" "$name.app" 50 | 51 | rm -rf icon icon.png icon.iconset ~'/Library/Developer/Xcode/UserData/FontAndColorThemes/icon.xccolortheme' 52 | 53 | if [[ $1 != test ]] 54 | then 55 | exit 56 | fi 57 | 58 | set +e 59 | 60 | defaults delete $id 61 | rm ~"/Library/Developer/Xcode/UserData/FontAndColorThemes/$name.xccolortheme" 62 | 63 | "$name.app/Contents/MacOS/$name" 64 | -------------------------------------------------------------------------------- /main.m: -------------------------------------------------------------------------------- 1 | @import AppKit; 2 | @import Darwin; 3 | @import ObjectiveC; 4 | 5 | #pragma clang diagnostic ignored "-Wunused-getter-return-value" 6 | #pragma clang diagnostic ignored "-Wobjc-missing-super-calls" 7 | #pragma clang diagnostic ignored "-Wdeprecated-declarations" 8 | 9 | // TODO: lol 10 | 11 | #import "Utils.m" 12 | #import "Xcode.m" 13 | 14 | #import "SettingsMapping.h" 15 | #import "Settings.h" 16 | #import "Document.h" 17 | #import "WindowController.h" 18 | #import "Delegate.h" 19 | 20 | #import "SettingsMapping.m" 21 | #import "Settings.m" 22 | #import "Document.m" 23 | #import "WindowController.m" 24 | #import "Delegate.m" 25 | 26 | int main(int argc,char** argv) 27 | { 28 | @autoreleasepool 29 | { 30 | restartIfNeeded(argv); 31 | linkXcode(); 32 | 33 | #ifdef iconMode 34 | Settings.reset; 35 | 36 | CGImageRef image=createThemeAppIcon(); 37 | NSURL* url=[NSURL fileURLWithPath:@"icon.png"]; 38 | CGImageDestinationRef destination=CGImageDestinationCreateWithURL((CFURLRef)url,kUTTypePNG,1,NULL); 39 | CGImageDestinationAddImage(destination,image,NULL); 40 | CGImageDestinationFinalize(destination); 41 | 42 | CFRelease(image); 43 | CFRelease(destination); 44 | #else 45 | NSApplication.sharedApplication.delegate=Delegate.alloc.init; 46 | NSApp.run; 47 | #endif 48 | } 49 | } 50 | --------------------------------------------------------------------------------